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.
@@ -161,7 +161,7 @@ function report(copied, skipped, dryRun, label) {
161
161
 
162
162
  // src/cli/commands/report.ts
163
163
  var import_promises13 = require("fs/promises");
164
- var import_node_path10 = __toESM(require("path"), 1);
164
+ var import_node_path14 = __toESM(require("path"), 1);
165
165
 
166
166
  // src/core/config.ts
167
167
  var import_promises2 = require("fs/promises");
@@ -169,13 +169,11 @@ var import_node_path4 = __toESM(require("path"), 1);
169
169
  var import_yaml = require("yaml");
170
170
  var defaultConfig = {
171
171
  paths: {
172
- specDir: ".qfai/spec",
173
- decisionsDir: ".qfai/spec/decisions",
174
- scenariosDir: ".qfai/spec/scenarios",
175
172
  contractsDir: ".qfai/contracts",
176
- uiContractsDir: ".qfai/contracts/ui",
177
- apiContractsDir: ".qfai/contracts/api",
178
- dataContractsDir: ".qfai/contracts/db",
173
+ specsDir: ".qfai/specs",
174
+ rulesDir: ".qfai/rules",
175
+ outDir: ".qfai/out",
176
+ promptsDir: ".qfai/prompts",
179
177
  srcDir: "src",
180
178
  testsDir: "tests"
181
179
  },
@@ -200,8 +198,7 @@ var defaultConfig = {
200
198
  }
201
199
  },
202
200
  output: {
203
- format: "text",
204
- jsonPath: ".qfai/out/validate.json"
201
+ validateJsonPath: ".qfai/out/validate.json"
205
202
  }
206
203
  };
207
204
  function getConfigPath(root) {
@@ -250,27 +247,6 @@ function normalizePaths(raw, configPath, issues) {
250
247
  return base;
251
248
  }
252
249
  return {
253
- specDir: readString(
254
- raw.specDir,
255
- base.specDir,
256
- "paths.specDir",
257
- configPath,
258
- issues
259
- ),
260
- decisionsDir: readString(
261
- raw.decisionsDir,
262
- base.decisionsDir,
263
- "paths.decisionsDir",
264
- configPath,
265
- issues
266
- ),
267
- scenariosDir: readString(
268
- raw.scenariosDir,
269
- base.scenariosDir,
270
- "paths.scenariosDir",
271
- configPath,
272
- issues
273
- ),
274
250
  contractsDir: readString(
275
251
  raw.contractsDir,
276
252
  base.contractsDir,
@@ -278,24 +254,31 @@ function normalizePaths(raw, configPath, issues) {
278
254
  configPath,
279
255
  issues
280
256
  ),
281
- uiContractsDir: readString(
282
- raw.uiContractsDir,
283
- base.uiContractsDir,
284
- "paths.uiContractsDir",
257
+ specsDir: readString(
258
+ raw.specsDir,
259
+ base.specsDir,
260
+ "paths.specsDir",
261
+ configPath,
262
+ issues
263
+ ),
264
+ rulesDir: readString(
265
+ raw.rulesDir,
266
+ base.rulesDir,
267
+ "paths.rulesDir",
285
268
  configPath,
286
269
  issues
287
270
  ),
288
- apiContractsDir: readString(
289
- raw.apiContractsDir,
290
- base.apiContractsDir,
291
- "paths.apiContractsDir",
271
+ outDir: readString(
272
+ raw.outDir,
273
+ base.outDir,
274
+ "paths.outDir",
292
275
  configPath,
293
276
  issues
294
277
  ),
295
- dataContractsDir: readString(
296
- raw.dataContractsDir,
297
- base.dataContractsDir,
298
- "paths.dataContractsDir",
278
+ promptsDir: readString(
279
+ raw.promptsDir,
280
+ base.promptsDir,
281
+ "paths.promptsDir",
299
282
  configPath,
300
283
  issues
301
284
  ),
@@ -418,17 +401,10 @@ function normalizeOutput(raw, configPath, issues) {
418
401
  return base;
419
402
  }
420
403
  return {
421
- format: readOutputFormat(
422
- raw.format,
423
- base.format,
424
- "output.format",
425
- configPath,
426
- issues
427
- ),
428
- jsonPath: readString(
429
- raw.jsonPath,
430
- base.jsonPath,
431
- "output.jsonPath",
404
+ validateJsonPath: readString(
405
+ raw.validateJsonPath,
406
+ base.validateJsonPath,
407
+ "output.validateJsonPath",
432
408
  configPath,
433
409
  issues
434
410
  )
@@ -495,20 +471,6 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
495
471
  }
496
472
  return fallback;
497
473
  }
498
- function readOutputFormat(value, fallback, label, configPath, issues) {
499
- if (value === "text" || value === "json" || value === "github") {
500
- return value;
501
- }
502
- if (value !== void 0) {
503
- issues.push(
504
- configIssue(
505
- configPath,
506
- `${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
507
- )
508
- );
509
- }
510
- return fallback;
511
- }
512
474
  function configIssue(file, message) {
513
475
  return {
514
476
  code: "QFAI_CONFIG_INVALID",
@@ -536,6 +498,7 @@ function isRecord(value) {
536
498
 
537
499
  // src/core/report.ts
538
500
  var import_promises12 = require("fs/promises");
501
+ var import_node_path13 = __toESM(require("path"), 1);
539
502
 
540
503
  // src/core/discovery.ts
541
504
  var import_node_path6 = __toESM(require("path"), 1);
@@ -596,10 +559,24 @@ async function exists2(target) {
596
559
  }
597
560
 
598
561
  // src/core/discovery.ts
599
- var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/;
600
- async function collectSpecFiles(specRoot) {
601
- const files = await collectFiles(specRoot, { extensions: [".md"] });
602
- return files.filter((file) => isSpecFile(file));
562
+ var SPEC_PACK_DIR_PATTERN = /^spec-\d{3}$/;
563
+ async function collectSpecPackDirs(specsRoot) {
564
+ const files = await collectFiles(specsRoot, { extensions: [".md"] });
565
+ const packs = /* @__PURE__ */ new Set();
566
+ for (const file of files) {
567
+ if (isSpecPackFile(file, "spec.md")) {
568
+ packs.add(import_node_path6.default.dirname(file));
569
+ }
570
+ }
571
+ return Array.from(packs).sort();
572
+ }
573
+ async function collectSpecFiles(specsRoot) {
574
+ const files = await collectFiles(specsRoot, { extensions: [".md"] });
575
+ return files.filter((file) => isSpecPackFile(file, "spec.md"));
576
+ }
577
+ async function collectScenarioFiles(specsRoot) {
578
+ const files = await collectFiles(specsRoot, { extensions: [".md"] });
579
+ return files.filter((file) => isSpecPackFile(file, "scenario.md"));
603
580
  }
604
581
  async function collectUiContractFiles(uiRoot) {
605
582
  return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
@@ -618,9 +595,12 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
618
595
  ]);
619
596
  return { ui, api, db };
620
597
  }
621
- function isSpecFile(filePath) {
622
- const name = import_node_path6.default.basename(filePath).toLowerCase();
623
- return SPEC_NAMED_PATTERN.test(name);
598
+ function isSpecPackFile(filePath, baseName) {
599
+ if (import_node_path6.default.basename(filePath).toLowerCase() !== baseName) {
600
+ return false;
601
+ }
602
+ const dirName = import_node_path6.default.basename(import_node_path6.default.dirname(filePath)).toLowerCase();
603
+ return SPEC_PACK_DIR_PATTERN.test(dirName);
624
604
  }
625
605
 
626
606
  // src/core/ids.ts
@@ -684,8 +664,8 @@ var import_promises4 = require("fs/promises");
684
664
  var import_node_path7 = __toESM(require("path"), 1);
685
665
  var import_node_url2 = require("url");
686
666
  async function resolveToolVersion() {
687
- if ("0.3.0".length > 0) {
688
- return "0.3.0";
667
+ if ("0.3.1".length > 0) {
668
+ return "0.3.1";
689
669
  }
690
670
  try {
691
671
  const packagePath = resolvePackageJsonPath();
@@ -705,6 +685,7 @@ function resolvePackageJsonPath() {
705
685
 
706
686
  // src/core/validators/contracts.ts
707
687
  var import_promises5 = require("fs/promises");
688
+ var import_node_path9 = __toESM(require("path"), 1);
708
689
 
709
690
  // src/core/contracts.ts
710
691
  var import_node_path8 = __toESM(require("path"), 1);
@@ -760,19 +741,10 @@ var SQL_DANGEROUS_PATTERNS = [
760
741
  ];
761
742
  async function validateContracts(root, config) {
762
743
  const issues = [];
763
- issues.push(
764
- ...await validateUiContracts(resolvePath(root, config, "uiContractsDir"))
765
- );
766
- issues.push(
767
- ...await validateApiContracts(
768
- resolvePath(root, config, "apiContractsDir")
769
- )
770
- );
771
- issues.push(
772
- ...await validateDataContracts(
773
- resolvePath(root, config, "dataContractsDir")
774
- )
775
- );
744
+ const contractsRoot = resolvePath(root, config, "contractsDir");
745
+ issues.push(...await validateUiContracts(import_node_path9.default.join(contractsRoot, "ui")));
746
+ issues.push(...await validateApiContracts(import_node_path9.default.join(contractsRoot, "api")));
747
+ issues.push(...await validateDataContracts(import_node_path9.default.join(contractsRoot, "db")));
776
748
  return issues;
777
749
  }
778
750
  async function validateUiContracts(uiRoot) {
@@ -1005,72 +977,78 @@ function issue(code, message, severity, file, rule, refs) {
1005
977
  return issue7;
1006
978
  }
1007
979
 
1008
- // src/core/validators/decisions.ts
980
+ // src/core/validators/delta.ts
1009
981
  var import_promises6 = require("fs/promises");
1010
-
1011
- // src/core/parse/adr.ts
1012
- var ADR_ID_RE = /\bADR-\d{4}\b/;
1013
- function extractField(md, key) {
1014
- const pattern = new RegExp(`^\\s*-\\s*${key}:\\s*(.+)\\s*$`, "m");
1015
- return md.match(pattern)?.[1]?.trim();
1016
- }
1017
- function parseAdr(md, file) {
1018
- const adrId = md.match(ADR_ID_RE)?.[0];
1019
- const fields = {};
1020
- const status = extractField(md, "Status");
1021
- const context = extractField(md, "Context");
1022
- const decision = extractField(md, "Decision");
1023
- const consequences = extractField(md, "Consequences");
1024
- const related = extractField(md, "Related");
1025
- if (status) fields.status = status;
1026
- if (context) fields.context = context;
1027
- if (decision) fields.decision = decision;
1028
- if (consequences) fields.consequences = consequences;
1029
- if (related) fields.related = related;
1030
- const parsed = {
1031
- file,
1032
- fields
1033
- };
1034
- if (adrId) {
1035
- parsed.adrId = adrId;
1036
- }
1037
- return parsed;
1038
- }
1039
-
1040
- // src/core/validators/decisions.ts
1041
- var REQUIRED_FIELDS = [
1042
- { key: "status", label: "Status" },
1043
- { key: "context", label: "Context" },
1044
- { key: "decision", label: "Decision" },
1045
- { key: "consequences", label: "Consequences" }
1046
- ];
1047
- async function validateDecisions(root, config) {
1048
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1049
- const files = await collectFiles(decisionsRoot, { extensions: [".md"] });
1050
- if (files.length === 0) {
982
+ var import_node_path10 = __toESM(require("path"), 1);
983
+ var SECTION_RE = /^##\s+変更区分/m;
984
+ var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
985
+ var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
986
+ var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
987
+ var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
988
+ async function validateDeltas(root, config) {
989
+ const specsRoot = resolvePath(root, config, "specsDir");
990
+ const packs = await collectSpecPackDirs(specsRoot);
991
+ if (packs.length === 0) {
1051
992
  return [];
1052
993
  }
1053
994
  const issues = [];
1054
- for (const file of files) {
1055
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1056
- const parsed = parseAdr(text, file);
1057
- const missing = REQUIRED_FIELDS.filter(
1058
- (field) => !parsed.fields[field.key]
1059
- );
1060
- if (missing.length > 0) {
995
+ for (const pack of packs) {
996
+ const deltaPath = import_node_path10.default.join(pack, "delta.md");
997
+ let text;
998
+ try {
999
+ text = await (0, import_promises6.readFile)(deltaPath, "utf-8");
1000
+ } catch (error2) {
1001
+ if (isMissingFileError(error2)) {
1002
+ issues.push(
1003
+ issue2(
1004
+ "QFAI-DELTA-001",
1005
+ "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1006
+ "error",
1007
+ deltaPath,
1008
+ "delta.exists"
1009
+ )
1010
+ );
1011
+ continue;
1012
+ }
1013
+ throw error2;
1014
+ }
1015
+ const hasSection = SECTION_RE.test(text);
1016
+ const hasCompatibility = COMPAT_LINE_RE.test(text);
1017
+ const hasChange = CHANGE_LINE_RE.test(text);
1018
+ if (!hasSection || !hasCompatibility || !hasChange) {
1061
1019
  issues.push(
1062
1020
  issue2(
1063
- "QFAI-ADR-001",
1064
- `ADR \u5FC5\u9808\u30D5\u30A3\u30FC\u30EB\u30C9\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missing.map((field) => field.label).join(", ")}`,
1021
+ "QFAI-DELTA-002",
1022
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
1065
1023
  "error",
1066
- file,
1067
- "adr.requiredFields"
1024
+ deltaPath,
1025
+ "delta.section"
1026
+ )
1027
+ );
1028
+ continue;
1029
+ }
1030
+ const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
1031
+ const changeChecked = CHANGE_CHECKED_RE.test(text);
1032
+ if (compatibilityChecked === changeChecked) {
1033
+ issues.push(
1034
+ issue2(
1035
+ "QFAI-DELTA-003",
1036
+ "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",
1037
+ "error",
1038
+ deltaPath,
1039
+ "delta.classification"
1068
1040
  )
1069
1041
  );
1070
1042
  }
1071
1043
  }
1072
1044
  return issues;
1073
1045
  }
1046
+ function isMissingFileError(error2) {
1047
+ if (!error2 || typeof error2 !== "object") {
1048
+ return false;
1049
+ }
1050
+ return error2.code === "ENOENT";
1051
+ }
1074
1052
  function issue2(code, message, severity, file, rule, refs) {
1075
1053
  const issue7 = {
1076
1054
  code,
@@ -1091,14 +1069,16 @@ function issue2(code, message, severity, file, rule, refs) {
1091
1069
 
1092
1070
  // src/core/validators/ids.ts
1093
1071
  var import_promises8 = require("fs/promises");
1094
- var import_node_path9 = __toESM(require("path"), 1);
1072
+ var import_node_path12 = __toESM(require("path"), 1);
1095
1073
 
1096
1074
  // src/core/contractIndex.ts
1097
1075
  var import_promises7 = require("fs/promises");
1076
+ var import_node_path11 = __toESM(require("path"), 1);
1098
1077
  async function buildContractIndex(root, config) {
1099
- const uiRoot = resolvePath(root, config, "uiContractsDir");
1100
- const apiRoot = resolvePath(root, config, "apiContractsDir");
1101
- const dataRoot = resolvePath(root, config, "dataContractsDir");
1078
+ const contractsRoot = resolvePath(root, config, "contractsDir");
1079
+ const uiRoot = import_node_path11.default.join(contractsRoot, "ui");
1080
+ const apiRoot = import_node_path11.default.join(contractsRoot, "api");
1081
+ const dataRoot = import_node_path11.default.join(contractsRoot, "db");
1102
1082
  const [uiFiles, apiFiles, dataFiles] = await Promise.all([
1103
1083
  collectUiContractFiles(uiRoot),
1104
1084
  collectApiContractFiles(apiRoot),
@@ -1154,7 +1134,7 @@ function record(index, id, file) {
1154
1134
 
1155
1135
  // src/core/parse/gherkin.ts
1156
1136
  var FEATURE_RE = /^\s*Feature:\s+/;
1157
- var SCENARIO_RE = /^\s*Scenario:\s*(.+)\s*$/;
1137
+ var SCENARIO_RE = /^\s*Scenario(?: Outline)?:\s*(.+)\s*$/;
1158
1138
  var TAG_LINE_RE = /^\s*@/;
1159
1139
  function parseTags(line) {
1160
1140
  return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
@@ -1163,24 +1143,52 @@ function parseGherkinFeature(text, file) {
1163
1143
  const lines = text.split(/\r?\n/);
1164
1144
  const scenarios = [];
1165
1145
  let featurePresent = false;
1146
+ let featureTags = [];
1147
+ let pendingTags = [];
1148
+ let current = null;
1149
+ const flush = () => {
1150
+ if (!current) return;
1151
+ scenarios.push({
1152
+ ...current,
1153
+ body: current.body.trim()
1154
+ });
1155
+ current = null;
1156
+ };
1166
1157
  for (let i = 0; i < lines.length; i++) {
1167
1158
  const line = lines[i] ?? "";
1168
- if (FEATURE_RE.test(line)) {
1159
+ const trimmed = line.trim();
1160
+ if (TAG_LINE_RE.test(trimmed)) {
1161
+ pendingTags.push(...parseTags(trimmed));
1162
+ continue;
1163
+ }
1164
+ if (FEATURE_RE.test(trimmed)) {
1169
1165
  featurePresent = true;
1166
+ featureTags = [...pendingTags];
1167
+ pendingTags = [];
1168
+ continue;
1170
1169
  }
1171
- const match = line.match(SCENARIO_RE);
1172
- if (!match) continue;
1173
- const scenarioName = match[1];
1174
- if (!scenarioName) continue;
1175
- const tags = [];
1176
- for (let j = i - 1; j >= 0; j--) {
1177
- const previous = lines[j] ?? "";
1178
- if (previous.trim() === "") continue;
1179
- if (!TAG_LINE_RE.test(previous)) break;
1180
- tags.unshift(...parseTags(previous));
1170
+ const match = trimmed.match(SCENARIO_RE);
1171
+ if (match) {
1172
+ const scenarioName = match[1]?.trim();
1173
+ if (!scenarioName) {
1174
+ continue;
1175
+ }
1176
+ flush();
1177
+ current = {
1178
+ name: scenarioName,
1179
+ line: i + 1,
1180
+ tags: [...featureTags, ...pendingTags],
1181
+ body: ""
1182
+ };
1183
+ pendingTags = [];
1184
+ continue;
1185
+ }
1186
+ if (current) {
1187
+ current.body += `${line}
1188
+ `;
1181
1189
  }
1182
- scenarios.push({ name: scenarioName, line: i + 1, tags });
1183
1190
  }
1191
+ flush();
1184
1192
  return { file, featurePresent, scenarios };
1185
1193
  }
1186
1194
 
@@ -1227,9 +1235,9 @@ function extractH2Sections(md) {
1227
1235
 
1228
1236
  // src/core/parse/spec.ts
1229
1237
  var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1230
- var BR_LINE_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[0-3])\)\s*(.+)$/;
1231
- var BR_LINE_ANY_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[^)]+)\)\s*(.+)$/;
1232
- var BR_LINE_NO_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s+(?!\()(.*\S.*)$/;
1238
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1239
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1240
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1233
1241
  var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1234
1242
  var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1235
1243
  function parseSpec(md, file) {
@@ -1306,12 +1314,9 @@ function parseSpec(md, file) {
1306
1314
  var SC_TAG_RE = /^SC-\d{4}$/;
1307
1315
  async function validateDefinedIds(root, config) {
1308
1316
  const issues = [];
1309
- const specRoot = resolvePath(root, config, "specDir");
1310
- const scenarioRoot = resolvePath(root, config, "scenariosDir");
1311
- const specFiles = await collectSpecFiles(specRoot);
1312
- const scenarioFiles = await collectFiles(scenarioRoot, {
1313
- extensions: [".feature"]
1314
- });
1317
+ const specsRoot = resolvePath(root, config, "specsDir");
1318
+ const specFiles = await collectSpecFiles(specsRoot);
1319
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1315
1320
  const defined = /* @__PURE__ */ new Map();
1316
1321
  await collectSpecDefinitionIds(specFiles, defined);
1317
1322
  await collectScenarioDefinitionIds(scenarioFiles, defined);
@@ -1368,7 +1373,7 @@ function recordId(out, id, file) {
1368
1373
  }
1369
1374
  function formatFileList(files, root) {
1370
1375
  return files.map((file) => {
1371
- const relative = import_node_path9.default.relative(root, file);
1376
+ const relative = import_node_path12.default.relative(root, file);
1372
1377
  return relative.length > 0 ? relative : file;
1373
1378
  }).join(", ");
1374
1379
  }
@@ -1399,17 +1404,15 @@ var SC_TAG_RE2 = /^SC-\d{4}$/;
1399
1404
  var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1400
1405
  var BR_TAG_RE = /^BR-\d{4}$/;
1401
1406
  async function validateScenarios(root, config) {
1402
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1403
- const files = await collectFiles(scenariosRoot, {
1404
- extensions: [".feature"]
1405
- });
1407
+ const specsRoot = resolvePath(root, config, "specsDir");
1408
+ const files = await collectScenarioFiles(specsRoot);
1406
1409
  if (files.length === 0) {
1407
1410
  return [
1408
1411
  issue4(
1409
1412
  "QFAI-SC-000",
1410
1413
  "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1411
1414
  "info",
1412
- scenariosRoot,
1415
+ specsRoot,
1413
1416
  "scenario.files"
1414
1417
  )
1415
1418
  ];
@@ -1475,8 +1478,11 @@ function validateScenarioContent(text, file) {
1475
1478
  continue;
1476
1479
  }
1477
1480
  const missingTags = [];
1478
- if (!scenario.tags.some((tag) => SC_TAG_RE2.test(tag))) {
1479
- missingTags.push("SC");
1481
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
1482
+ if (scTags.length === 0) {
1483
+ missingTags.push("SC(0\u4EF6)");
1484
+ } else if (scTags.length > 1) {
1485
+ missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1480
1486
  }
1481
1487
  if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
1482
1488
  missingTags.push("SPEC");
@@ -1496,26 +1502,28 @@ function validateScenarioContent(text, file) {
1496
1502
  );
1497
1503
  }
1498
1504
  }
1499
- const missingSteps = [];
1500
- if (!GIVEN_PATTERN.test(text)) {
1501
- missingSteps.push("Given");
1502
- }
1503
- if (!WHEN_PATTERN.test(text)) {
1504
- missingSteps.push("When");
1505
- }
1506
- if (!THEN_PATTERN.test(text)) {
1507
- missingSteps.push("Then");
1508
- }
1509
- if (missingSteps.length > 0) {
1510
- issues.push(
1511
- issue4(
1512
- "QFAI-SC-005",
1513
- `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
1514
- "warning",
1515
- file,
1516
- "scenario.steps"
1517
- )
1518
- );
1505
+ for (const scenario of parsed.scenarios) {
1506
+ const missingSteps = [];
1507
+ if (!GIVEN_PATTERN.test(scenario.body)) {
1508
+ missingSteps.push("Given");
1509
+ }
1510
+ if (!WHEN_PATTERN.test(scenario.body)) {
1511
+ missingSteps.push("When");
1512
+ }
1513
+ if (!THEN_PATTERN.test(scenario.body)) {
1514
+ missingSteps.push("Then");
1515
+ }
1516
+ if (missingSteps.length > 0) {
1517
+ issues.push(
1518
+ issue4(
1519
+ "QFAI-SC-005",
1520
+ `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")} (${scenario.name})`,
1521
+ "warning",
1522
+ file,
1523
+ "scenario.steps"
1524
+ )
1525
+ );
1526
+ }
1519
1527
  }
1520
1528
  return issues;
1521
1529
  }
@@ -1540,14 +1548,14 @@ function issue4(code, message, severity, file, rule, refs) {
1540
1548
  // src/core/validators/spec.ts
1541
1549
  var import_promises10 = require("fs/promises");
1542
1550
  async function validateSpecs(root, config) {
1543
- const specsRoot = resolvePath(root, config, "specDir");
1551
+ const specsRoot = resolvePath(root, config, "specsDir");
1544
1552
  const files = await collectSpecFiles(specsRoot);
1545
1553
  if (files.length === 0) {
1546
- const expected = "spec-0001-<slug>.md";
1554
+ const expected = "spec-001/spec.md";
1547
1555
  return [
1548
1556
  issue5(
1549
1557
  "QFAI-SPEC-000",
1550
- `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}`,
1558
+ `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}`,
1551
1559
  "info",
1552
1560
  specsRoot,
1553
1561
  "spec.files"
@@ -1693,18 +1701,11 @@ var API_TAG_RE = /^API-\d{4}$/;
1693
1701
  var DATA_TAG_RE = /^DATA-\d{4}$/;
1694
1702
  async function validateTraceability(root, config) {
1695
1703
  const issues = [];
1696
- const specsRoot = resolvePath(root, config, "specDir");
1697
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1698
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1704
+ const specsRoot = resolvePath(root, config, "specsDir");
1699
1705
  const srcRoot = resolvePath(root, config, "srcDir");
1700
1706
  const testsRoot = resolvePath(root, config, "testsDir");
1701
1707
  const specFiles = await collectSpecFiles(specsRoot);
1702
- const decisionFiles = await collectFiles(decisionsRoot, {
1703
- extensions: [".md"]
1704
- });
1705
- const scenarioFiles = await collectFiles(scenariosRoot, {
1706
- extensions: [".feature"]
1707
- });
1708
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1708
1709
  const upstreamIds = /* @__PURE__ */ new Set();
1709
1710
  const specIds = /* @__PURE__ */ new Set();
1710
1711
  const brIdsInSpecs = /* @__PURE__ */ new Set();
@@ -1752,10 +1753,6 @@ async function validateTraceability(root, config) {
1752
1753
  specToBrIds.set(parsed.specId, current);
1753
1754
  }
1754
1755
  }
1755
- for (const file of decisionFiles) {
1756
- const text = await (0, import_promises11.readFile)(file, "utf-8");
1757
- extractAllIds(text).forEach((id) => upstreamIds.add(id));
1758
- }
1759
1756
  for (const file of scenarioFiles) {
1760
1757
  const text = await (0, import_promises11.readFile)(file, "utf-8");
1761
1758
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
@@ -1899,7 +1896,7 @@ async function validateTraceability(root, config) {
1899
1896
  ", "
1900
1897
  )}`,
1901
1898
  "error",
1902
- scenariosRoot,
1899
+ specsRoot,
1903
1900
  "traceability.scMustTouchContracts",
1904
1901
  scWithoutContracts
1905
1902
  )
@@ -1917,7 +1914,7 @@ async function validateTraceability(root, config) {
1917
1914
  "QFAI_CONTRACT_ORPHAN",
1918
1915
  `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
1919
1916
  "error",
1920
- scenariosRoot,
1917
+ specsRoot,
1921
1918
  "traceability.allowOrphanContracts",
1922
1919
  orphanContracts
1923
1920
  )
@@ -2002,8 +1999,8 @@ async function validateProject(root, configResult) {
2002
1999
  const issues = [
2003
2000
  ...configIssues,
2004
2001
  ...await validateSpecs(root, config),
2002
+ ...await validateDeltas(root, config),
2005
2003
  ...await validateScenarios(root, config),
2006
- ...await validateDecisions(root, config),
2007
2004
  ...await validateContracts(root, config),
2008
2005
  ...await validateDefinedIds(root, config),
2009
2006
  ...await validateTraceability(root, config)
@@ -2032,21 +2029,15 @@ async function createReportData(root, validation, configResult) {
2032
2029
  const resolved = configResult ?? await loadConfig(root);
2033
2030
  const config = resolved.config;
2034
2031
  const configPath = resolved.configPath;
2035
- const specRoot = resolvePath(root, config, "specDir");
2036
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
2037
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
2038
- const apiRoot = resolvePath(root, config, "apiContractsDir");
2039
- const uiRoot = resolvePath(root, config, "uiContractsDir");
2040
- const dbRoot = resolvePath(root, config, "dataContractsDir");
2032
+ const specsRoot = resolvePath(root, config, "specsDir");
2033
+ const contractsRoot = resolvePath(root, config, "contractsDir");
2034
+ const apiRoot = import_node_path13.default.join(contractsRoot, "api");
2035
+ const uiRoot = import_node_path13.default.join(contractsRoot, "ui");
2036
+ const dbRoot = import_node_path13.default.join(contractsRoot, "db");
2041
2037
  const srcRoot = resolvePath(root, config, "srcDir");
2042
2038
  const testsRoot = resolvePath(root, config, "testsDir");
2043
- const specFiles = await collectSpecFiles(specRoot);
2044
- const scenarioFiles = await collectFiles(scenariosRoot, {
2045
- extensions: [".feature"]
2046
- });
2047
- const decisionFiles = await collectFiles(decisionsRoot, {
2048
- extensions: [".md"]
2049
- });
2039
+ const specFiles = await collectSpecFiles(specsRoot);
2040
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
2050
2041
  const {
2051
2042
  api: apiFiles,
2052
2043
  ui: uiFiles,
@@ -2055,7 +2046,6 @@ async function createReportData(root, validation, configResult) {
2055
2046
  const idsByPrefix = await collectIds([
2056
2047
  ...specFiles,
2057
2048
  ...scenarioFiles,
2058
- ...decisionFiles,
2059
2049
  ...apiFiles,
2060
2050
  ...uiFiles,
2061
2051
  ...dbFiles
@@ -2080,7 +2070,6 @@ async function createReportData(root, validation, configResult) {
2080
2070
  summary: {
2081
2071
  specs: specFiles.length,
2082
2072
  scenarios: scenarioFiles.length,
2083
- decisions: decisionFiles.length,
2084
2073
  contracts: {
2085
2074
  api: apiFiles.length,
2086
2075
  ui: uiFiles.length,
@@ -2114,7 +2103,6 @@ function formatReportMarkdown(data) {
2114
2103
  lines.push("## \u6982\u8981");
2115
2104
  lines.push(`- specs: ${data.summary.specs}`);
2116
2105
  lines.push(`- scenarios: ${data.summary.scenarios}`);
2117
- lines.push(`- decisions: ${data.summary.decisions}`);
2118
2106
  lines.push(
2119
2107
  `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
2120
2108
  );
@@ -2273,21 +2261,22 @@ function buildHotspots(issues) {
2273
2261
 
2274
2262
  // src/cli/commands/report.ts
2275
2263
  async function runReport(options) {
2276
- const root = import_node_path10.default.resolve(options.root);
2264
+ const root = import_node_path14.default.resolve(options.root);
2277
2265
  const configResult = await loadConfig(root);
2278
- const input = options.jsonPath ?? configResult.config.output.jsonPath;
2279
- const inputPath = import_node_path10.default.isAbsolute(input) ? input : import_node_path10.default.resolve(root, input);
2266
+ const input = configResult.config.output.validateJsonPath;
2267
+ const inputPath = import_node_path14.default.isAbsolute(input) ? input : import_node_path14.default.resolve(root, input);
2280
2268
  let validation;
2281
2269
  try {
2282
2270
  validation = await readValidationResult(inputPath);
2283
2271
  } catch (err) {
2284
- if (isMissingFileError(err)) {
2272
+ if (isMissingFileError2(err)) {
2285
2273
  error(
2286
2274
  [
2287
2275
  `qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
2288
2276
  "",
2289
- "\u307E\u305A validate.json \u3092\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
2290
- ` qfai validate --json-path ${input}`,
2277
+ "\u307E\u305A qfai validate \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
2278
+ " qfai validate",
2279
+ "\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/out/validate.json\uFF09",
2291
2280
  "",
2292
2281
  "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"
2293
2282
  ].join("\n")
@@ -2299,10 +2288,11 @@ async function runReport(options) {
2299
2288
  }
2300
2289
  const data = await createReportData(root, validation, configResult);
2301
2290
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
2302
- const defaultOut = options.format === "json" ? ".qfai/out/report.json" : ".qfai/out/report.md";
2291
+ const outRoot = resolvePath(root, configResult.config, "outDir");
2292
+ const defaultOut = options.format === "json" ? import_node_path14.default.join(outRoot, "report.json") : import_node_path14.default.join(outRoot, "report.md");
2303
2293
  const out = options.outPath ?? defaultOut;
2304
- const outPath = import_node_path10.default.isAbsolute(out) ? out : import_node_path10.default.resolve(root, out);
2305
- await (0, import_promises13.mkdir)(import_node_path10.default.dirname(outPath), { recursive: true });
2294
+ const outPath = import_node_path14.default.isAbsolute(out) ? out : import_node_path14.default.resolve(root, out);
2295
+ await (0, import_promises13.mkdir)(import_node_path14.default.dirname(outPath), { recursive: true });
2306
2296
  await (0, import_promises13.writeFile)(outPath, `${output}
2307
2297
  `, "utf-8");
2308
2298
  info(
@@ -2343,7 +2333,7 @@ function isValidationResult(value) {
2343
2333
  }
2344
2334
  return typeof counts.info === "number" && typeof counts.warning === "number" && typeof counts.error === "number";
2345
2335
  }
2346
- function isMissingFileError(error2) {
2336
+ function isMissingFileError2(error2) {
2347
2337
  if (!error2 || typeof error2 !== "object") {
2348
2338
  return false;
2349
2339
  }
@@ -2353,7 +2343,7 @@ function isMissingFileError(error2) {
2353
2343
 
2354
2344
  // src/cli/commands/validate.ts
2355
2345
  var import_promises14 = require("fs/promises");
2356
- var import_node_path11 = __toESM(require("path"), 1);
2346
+ var import_node_path15 = __toESM(require("path"), 1);
2357
2347
 
2358
2348
  // src/cli/lib/failOn.ts
2359
2349
  function shouldFail(result, failOn) {
@@ -2368,24 +2358,17 @@ function shouldFail(result, failOn) {
2368
2358
 
2369
2359
  // src/cli/commands/validate.ts
2370
2360
  async function runValidate(options) {
2371
- const root = import_node_path11.default.resolve(options.root);
2361
+ const root = import_node_path15.default.resolve(options.root);
2372
2362
  const configResult = await loadConfig(root);
2373
2363
  const result = await validateProject(root, configResult);
2374
- const format = options.format ?? configResult.config.output.format;
2375
- const explicitJsonPath = options.jsonPath;
2364
+ const format = options.format ?? "text";
2376
2365
  if (format === "text") {
2377
2366
  emitText(result);
2378
2367
  }
2379
2368
  if (format === "github") {
2380
2369
  result.issues.forEach(emitGitHub);
2381
2370
  }
2382
- const shouldWriteJson = format === "json" || explicitJsonPath !== void 0;
2383
- if (shouldWriteJson) {
2384
- const jsonPath = format === "json" ? options.jsonPath ?? configResult.config.output.jsonPath : explicitJsonPath;
2385
- if (jsonPath) {
2386
- await emitJson(result, root, jsonPath);
2387
- }
2388
- }
2371
+ await emitJson(result, root, configResult.config.output.validateJsonPath);
2389
2372
  const failOn = resolveFailOn(options, configResult.config.validation.failOn);
2390
2373
  return shouldFail(result, failOn) ? 1 : 0;
2391
2374
  }
@@ -2424,8 +2407,8 @@ function emitGitHub(issue7) {
2424
2407
  );
2425
2408
  }
2426
2409
  async function emitJson(result, root, jsonPath) {
2427
- const abs = import_node_path11.default.isAbsolute(jsonPath) ? jsonPath : import_node_path11.default.resolve(root, jsonPath);
2428
- await (0, import_promises14.mkdir)(import_node_path11.default.dirname(abs), { recursive: true });
2410
+ const abs = import_node_path15.default.isAbsolute(jsonPath) ? jsonPath : import_node_path15.default.resolve(root, jsonPath);
2411
+ await (0, import_promises14.mkdir)(import_node_path15.default.dirname(abs), { recursive: true });
2429
2412
  await (0, import_promises14.writeFile)(abs, `${JSON.stringify(result, null, 2)}
2430
2413
  `, "utf-8");
2431
2414
  }
@@ -2486,15 +2469,6 @@ function parseArgs(argv, cwd) {
2486
2469
  i += 1;
2487
2470
  break;
2488
2471
  }
2489
- case "--json-path":
2490
- {
2491
- const next = args[i + 1];
2492
- if (next) {
2493
- options.jsonPath = next;
2494
- }
2495
- }
2496
- i += 1;
2497
- break;
2498
2472
  case "--out":
2499
2473
  {
2500
2474
  const next = args[i + 1];
@@ -2525,7 +2499,7 @@ function applyFormatOption(command, value, options) {
2525
2499
  return;
2526
2500
  }
2527
2501
  if (command === "validate") {
2528
- if (value === "text" || value === "json" || value === "github") {
2502
+ if (value === "text" || value === "github") {
2529
2503
  options.validateFormat = value;
2530
2504
  }
2531
2505
  return;
@@ -2533,7 +2507,7 @@ function applyFormatOption(command, value, options) {
2533
2507
  if (value === "md" || value === "json") {
2534
2508
  options.reportFormat = value;
2535
2509
  }
2536
- if (value === "text" || value === "json" || value === "github") {
2510
+ if (value === "text" || value === "github") {
2537
2511
  options.validateFormat = value;
2538
2512
  }
2539
2513
  }
@@ -2559,15 +2533,13 @@ async function run(argv, cwd) {
2559
2533
  root: options.root,
2560
2534
  strict: options.strict,
2561
2535
  format: options.validateFormat,
2562
- ...options.failOn !== void 0 ? { failOn: options.failOn } : {},
2563
- ...options.jsonPath !== void 0 ? { jsonPath: options.jsonPath } : {}
2536
+ ...options.failOn !== void 0 ? { failOn: options.failOn } : {}
2564
2537
  });
2565
2538
  return;
2566
2539
  case "report":
2567
2540
  await runReport({
2568
2541
  root: options.root,
2569
2542
  format: options.reportFormat,
2570
- ...options.jsonPath !== void 0 ? { jsonPath: options.jsonPath } : {},
2571
2543
  ...options.reportOut !== void 0 ? { outPath: options.reportOut } : {}
2572
2544
  });
2573
2545
  return;
@@ -2589,14 +2561,13 @@ Options:
2589
2561
  --root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
2590
2562
  --dir <path> init \u306E\u51FA\u529B\u5148
2591
2563
  --force \u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u3092\u4E0A\u66F8\u304D
2592
- --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
2564
+ --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
2593
2565
  --dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
2594
- --format <text|json|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
2566
+ --format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
2595
2567
  --format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
2596
- --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
2568
+ --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
2597
2569
  --fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
2598
- --json-path <path> validate: JSON \u51FA\u529B\u5148 / report: validate JSON \u5165\u529B
2599
- --out <path> report: \u51FA\u529B\u5148
2570
+ --out <path> report: \u51FA\u529B\u5148
2600
2571
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A
2601
2572
  `;
2602
2573
  }