qfai 0.3.0 → 0.3.2

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",
262
238
  configPath,
263
239
  issues
264
240
  ),
265
- apiContractsDir: readString(
266
- raw.apiContractsDir,
267
- base.apiContractsDir,
268
- "paths.apiContractsDir",
241
+ rulesDir: readString(
242
+ raw.rulesDir,
243
+ base.rulesDir,
244
+ "paths.rulesDir",
269
245
  configPath,
270
246
  issues
271
247
  ),
272
- dataContractsDir: readString(
273
- raw.dataContractsDir,
274
- base.dataContractsDir,
275
- "paths.dataContractsDir",
248
+ outDir: readString(
249
+ raw.outDir,
250
+ base.outDir,
251
+ "paths.outDir",
252
+ configPath,
253
+ issues
254
+ ),
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,9 +475,10 @@ 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
- import path6 from "path";
481
+ import { access as access3 } from "fs/promises";
519
482
 
520
483
  // src/core/fs.ts
521
484
  import { access as access2, readdir as readdir2 } from "fs/promises";
@@ -572,11 +535,50 @@ async function exists2(target) {
572
535
  }
573
536
  }
574
537
 
538
+ // src/core/specLayout.ts
539
+ import { readdir as readdir3 } from "fs/promises";
540
+ import path6 from "path";
541
+ var SPEC_DIR_RE = /^spec-\d{4}$/;
542
+ async function collectSpecEntries(specsRoot) {
543
+ const dirs = await listSpecDirs(specsRoot);
544
+ const entries = dirs.map((dir) => ({
545
+ dir,
546
+ specPath: path6.join(dir, "spec.md"),
547
+ deltaPath: path6.join(dir, "delta.md"),
548
+ scenarioPath: path6.join(dir, "scenario.md")
549
+ }));
550
+ return entries.sort((a, b) => a.dir.localeCompare(b.dir));
551
+ }
552
+ async function listSpecDirs(specsRoot) {
553
+ try {
554
+ const items = await readdir3(specsRoot, { withFileTypes: true });
555
+ return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => path6.join(specsRoot, name));
556
+ } catch (error2) {
557
+ if (isMissingFileError(error2)) {
558
+ return [];
559
+ }
560
+ throw error2;
561
+ }
562
+ }
563
+ function isMissingFileError(error2) {
564
+ if (!error2 || typeof error2 !== "object") {
565
+ return false;
566
+ }
567
+ return error2.code === "ENOENT";
568
+ }
569
+
575
570
  // 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));
571
+ async function collectSpecPackDirs(specsRoot) {
572
+ const entries = await collectSpecEntries(specsRoot);
573
+ return entries.map((entry) => entry.dir);
574
+ }
575
+ async function collectSpecFiles(specsRoot) {
576
+ const entries = await collectSpecEntries(specsRoot);
577
+ return filterExisting(entries.map((entry) => entry.specPath));
578
+ }
579
+ async function collectScenarioFiles(specsRoot) {
580
+ const entries = await collectSpecEntries(specsRoot);
581
+ return filterExisting(entries.map((entry) => entry.scenarioPath));
580
582
  }
581
583
  async function collectUiContractFiles(uiRoot) {
582
584
  return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
@@ -595,9 +597,22 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
595
597
  ]);
596
598
  return { ui, api, db };
597
599
  }
598
- function isSpecFile(filePath) {
599
- const name = path6.basename(filePath).toLowerCase();
600
- return SPEC_NAMED_PATTERN.test(name);
600
+ async function filterExisting(files) {
601
+ const existing = [];
602
+ for (const file of files) {
603
+ if (await exists3(file)) {
604
+ existing.push(file);
605
+ }
606
+ }
607
+ return existing;
608
+ }
609
+ async function exists3(target) {
610
+ try {
611
+ await access3(target);
612
+ return true;
613
+ } catch {
614
+ return false;
615
+ }
601
616
  }
602
617
 
603
618
  // src/core/ids.ts
@@ -661,8 +676,8 @@ import { readFile as readFile2 } from "fs/promises";
661
676
  import path7 from "path";
662
677
  import { fileURLToPath as fileURLToPath2 } from "url";
663
678
  async function resolveToolVersion() {
664
- if ("0.3.0".length > 0) {
665
- return "0.3.0";
679
+ if ("0.3.2".length > 0) {
680
+ return "0.3.2";
666
681
  }
667
682
  try {
668
683
  const packagePath = resolvePackageJsonPath();
@@ -682,6 +697,7 @@ function resolvePackageJsonPath() {
682
697
 
683
698
  // src/core/validators/contracts.ts
684
699
  import { readFile as readFile3 } from "fs/promises";
700
+ import path9 from "path";
685
701
 
686
702
  // src/core/contracts.ts
687
703
  import path8 from "path";
@@ -737,19 +753,10 @@ var SQL_DANGEROUS_PATTERNS = [
737
753
  ];
738
754
  async function validateContracts(root, config) {
739
755
  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
- );
756
+ const contractsRoot = resolvePath(root, config, "contractsDir");
757
+ issues.push(...await validateUiContracts(path9.join(contractsRoot, "ui")));
758
+ issues.push(...await validateApiContracts(path9.join(contractsRoot, "api")));
759
+ issues.push(...await validateDataContracts(path9.join(contractsRoot, "db")));
753
760
  return issues;
754
761
  }
755
762
  async function validateUiContracts(uiRoot) {
@@ -982,72 +989,78 @@ function issue(code, message, severity, file, rule, refs) {
982
989
  return issue7;
983
990
  }
984
991
 
985
- // src/core/validators/decisions.ts
992
+ // src/core/validators/delta.ts
986
993
  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) {
994
+ import path10 from "path";
995
+ var SECTION_RE = /^##\s+変更区分/m;
996
+ var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
997
+ var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
998
+ var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
999
+ var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
1000
+ async function validateDeltas(root, config) {
1001
+ const specsRoot = resolvePath(root, config, "specsDir");
1002
+ const packs = await collectSpecPackDirs(specsRoot);
1003
+ if (packs.length === 0) {
1028
1004
  return [];
1029
1005
  }
1030
1006
  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) {
1007
+ for (const pack of packs) {
1008
+ const deltaPath = path10.join(pack, "delta.md");
1009
+ let text;
1010
+ try {
1011
+ text = await readFile4(deltaPath, "utf-8");
1012
+ } catch (error2) {
1013
+ if (isMissingFileError2(error2)) {
1014
+ issues.push(
1015
+ issue2(
1016
+ "QFAI-DELTA-001",
1017
+ "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1018
+ "error",
1019
+ deltaPath,
1020
+ "delta.exists"
1021
+ )
1022
+ );
1023
+ continue;
1024
+ }
1025
+ throw error2;
1026
+ }
1027
+ const hasSection = SECTION_RE.test(text);
1028
+ const hasCompatibility = COMPAT_LINE_RE.test(text);
1029
+ const hasChange = CHANGE_LINE_RE.test(text);
1030
+ if (!hasSection || !hasCompatibility || !hasChange) {
1038
1031
  issues.push(
1039
1032
  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(", ")}`,
1033
+ "QFAI-DELTA-002",
1034
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
1042
1035
  "error",
1043
- file,
1044
- "adr.requiredFields"
1036
+ deltaPath,
1037
+ "delta.section"
1038
+ )
1039
+ );
1040
+ continue;
1041
+ }
1042
+ const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
1043
+ const changeChecked = CHANGE_CHECKED_RE.test(text);
1044
+ if (compatibilityChecked === changeChecked) {
1045
+ issues.push(
1046
+ issue2(
1047
+ "QFAI-DELTA-003",
1048
+ "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",
1049
+ "error",
1050
+ deltaPath,
1051
+ "delta.classification"
1045
1052
  )
1046
1053
  );
1047
1054
  }
1048
1055
  }
1049
1056
  return issues;
1050
1057
  }
1058
+ function isMissingFileError2(error2) {
1059
+ if (!error2 || typeof error2 !== "object") {
1060
+ return false;
1061
+ }
1062
+ return error2.code === "ENOENT";
1063
+ }
1051
1064
  function issue2(code, message, severity, file, rule, refs) {
1052
1065
  const issue7 = {
1053
1066
  code,
@@ -1068,14 +1081,16 @@ function issue2(code, message, severity, file, rule, refs) {
1068
1081
 
1069
1082
  // src/core/validators/ids.ts
1070
1083
  import { readFile as readFile6 } from "fs/promises";
1071
- import path9 from "path";
1084
+ import path12 from "path";
1072
1085
 
1073
1086
  // src/core/contractIndex.ts
1074
1087
  import { readFile as readFile5 } from "fs/promises";
1088
+ import path11 from "path";
1075
1089
  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");
1090
+ const contractsRoot = resolvePath(root, config, "contractsDir");
1091
+ const uiRoot = path11.join(contractsRoot, "ui");
1092
+ const apiRoot = path11.join(contractsRoot, "api");
1093
+ const dataRoot = path11.join(contractsRoot, "db");
1079
1094
  const [uiFiles, apiFiles, dataFiles] = await Promise.all([
1080
1095
  collectUiContractFiles(uiRoot),
1081
1096
  collectApiContractFiles(apiRoot),
@@ -1129,38 +1144,6 @@ function record(index, id, file) {
1129
1144
  index.idToFiles.set(id, current);
1130
1145
  }
1131
1146
 
1132
- // src/core/parse/gherkin.ts
1133
- var FEATURE_RE = /^\s*Feature:\s+/;
1134
- var SCENARIO_RE = /^\s*Scenario:\s*(.+)\s*$/;
1135
- var TAG_LINE_RE = /^\s*@/;
1136
- function parseTags(line) {
1137
- return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
1138
- }
1139
- function parseGherkinFeature(text, file) {
1140
- const lines = text.split(/\r?\n/);
1141
- const scenarios = [];
1142
- let featurePresent = false;
1143
- for (let i = 0; i < lines.length; i++) {
1144
- const line = lines[i] ?? "";
1145
- if (FEATURE_RE.test(line)) {
1146
- featurePresent = true;
1147
- }
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));
1158
- }
1159
- scenarios.push({ name: scenarioName, line: i + 1, tags });
1160
- }
1161
- return { file, featurePresent, scenarios };
1162
- }
1163
-
1164
1147
  // src/core/parse/markdown.ts
1165
1148
  var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1166
1149
  function parseHeadings(md) {
@@ -1204,9 +1187,9 @@ function extractH2Sections(md) {
1204
1187
 
1205
1188
  // src/core/parse/spec.ts
1206
1189
  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.*)$/;
1190
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1191
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1192
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1210
1193
  var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1211
1194
  var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1212
1195
  function parseSpec(md, file) {
@@ -1279,16 +1262,171 @@ function parseSpec(md, file) {
1279
1262
  return parsed;
1280
1263
  }
1281
1264
 
1282
- // src/core/validators/ids.ts
1265
+ // src/core/gherkin/parse.ts
1266
+ import {
1267
+ AstBuilder,
1268
+ GherkinClassicTokenMatcher,
1269
+ Parser
1270
+ } from "@cucumber/gherkin";
1271
+ import { randomUUID } from "crypto";
1272
+ function parseGherkin(source, uri) {
1273
+ const errors = [];
1274
+ const uuidFn = () => randomUUID();
1275
+ const builder = new AstBuilder(uuidFn);
1276
+ const matcher = new GherkinClassicTokenMatcher();
1277
+ const parser = new Parser(builder, matcher);
1278
+ try {
1279
+ const gherkinDocument = parser.parse(source);
1280
+ gherkinDocument.uri = uri;
1281
+ return { gherkinDocument, errors };
1282
+ } catch (error2) {
1283
+ errors.push(formatError3(error2));
1284
+ return { gherkinDocument: null, errors };
1285
+ }
1286
+ }
1287
+ function formatError3(error2) {
1288
+ if (error2 instanceof Error) {
1289
+ return error2.message;
1290
+ }
1291
+ return String(error2);
1292
+ }
1293
+
1294
+ // src/core/scenarioModel.ts
1295
+ var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1283
1296
  var SC_TAG_RE = /^SC-\d{4}$/;
1297
+ var BR_TAG_RE = /^BR-\d{4}$/;
1298
+ var UI_TAG_RE = /^UI-\d{4}$/;
1299
+ var API_TAG_RE = /^API-\d{4}$/;
1300
+ var DATA_TAG_RE = /^DATA-\d{4}$/;
1301
+ function parseScenarioDocument(text, uri) {
1302
+ const { gherkinDocument, errors } = parseGherkin(text, uri);
1303
+ if (!gherkinDocument) {
1304
+ return { document: null, errors };
1305
+ }
1306
+ const feature = gherkinDocument.feature;
1307
+ if (!feature) {
1308
+ return {
1309
+ document: { uri, featureTags: [], scenarios: [] },
1310
+ errors
1311
+ };
1312
+ }
1313
+ const featureTags = collectTagNames(feature.tags);
1314
+ const scenarios = collectScenarioNodes(feature, featureTags);
1315
+ return {
1316
+ document: {
1317
+ uri,
1318
+ featureName: feature.name,
1319
+ featureTags,
1320
+ scenarios
1321
+ },
1322
+ errors
1323
+ };
1324
+ }
1325
+ function buildScenarioAtoms(document) {
1326
+ return document.scenarios.map((scenario) => {
1327
+ const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
1328
+ const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
1329
+ const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1330
+ const contractIds = /* @__PURE__ */ new Set();
1331
+ scenario.tags.forEach((tag) => {
1332
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1333
+ contractIds.add(tag);
1334
+ }
1335
+ });
1336
+ for (const step of scenario.steps) {
1337
+ for (const text of collectStepTexts(step)) {
1338
+ extractIds(text, "UI").forEach((id) => contractIds.add(id));
1339
+ extractIds(text, "API").forEach((id) => contractIds.add(id));
1340
+ extractIds(text, "DATA").forEach((id) => contractIds.add(id));
1341
+ }
1342
+ }
1343
+ const atom = {
1344
+ uri: document.uri,
1345
+ featureName: document.featureName ?? "",
1346
+ scenarioName: scenario.name,
1347
+ kind: scenario.kind,
1348
+ brIds,
1349
+ contractIds: Array.from(contractIds).sort()
1350
+ };
1351
+ if (scenario.line !== void 0) {
1352
+ atom.line = scenario.line;
1353
+ }
1354
+ if (specIds.length === 1) {
1355
+ const specId = specIds[0];
1356
+ if (specId) {
1357
+ atom.specId = specId;
1358
+ }
1359
+ }
1360
+ if (scIds.length === 1) {
1361
+ const scId = scIds[0];
1362
+ if (scId) {
1363
+ atom.scId = scId;
1364
+ }
1365
+ }
1366
+ return atom;
1367
+ });
1368
+ }
1369
+ function collectScenarioNodes(feature, featureTags) {
1370
+ const scenarios = [];
1371
+ for (const child of feature.children) {
1372
+ if (child.scenario) {
1373
+ scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
1374
+ }
1375
+ if (child.rule) {
1376
+ const ruleTags = collectTagNames(child.rule.tags);
1377
+ for (const ruleChild of child.rule.children) {
1378
+ if (ruleChild.scenario) {
1379
+ scenarios.push(
1380
+ buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
1381
+ );
1382
+ }
1383
+ }
1384
+ }
1385
+ }
1386
+ return scenarios;
1387
+ }
1388
+ function buildScenarioNode(scenario, featureTags, ruleTags) {
1389
+ const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
1390
+ const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
1391
+ return {
1392
+ name: scenario.name,
1393
+ kind,
1394
+ line: scenario.location?.line,
1395
+ tags,
1396
+ steps: scenario.steps
1397
+ };
1398
+ }
1399
+ function collectTagNames(tags) {
1400
+ return tags.map((tag) => tag.name.replace(/^@/, ""));
1401
+ }
1402
+ function collectStepTexts(step) {
1403
+ const texts = [];
1404
+ if (step.text) {
1405
+ texts.push(step.text);
1406
+ }
1407
+ if (step.docString?.content) {
1408
+ texts.push(step.docString.content);
1409
+ }
1410
+ if (step.dataTable?.rows) {
1411
+ for (const row of step.dataTable.rows) {
1412
+ for (const cell of row.cells) {
1413
+ texts.push(cell.value);
1414
+ }
1415
+ }
1416
+ }
1417
+ return texts;
1418
+ }
1419
+ function unique2(values) {
1420
+ return Array.from(new Set(values));
1421
+ }
1422
+
1423
+ // src/core/validators/ids.ts
1424
+ var SC_TAG_RE2 = /^SC-\d{4}$/;
1284
1425
  async function validateDefinedIds(root, config) {
1285
1426
  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
- });
1427
+ const specsRoot = resolvePath(root, config, "specsDir");
1428
+ const specFiles = await collectSpecFiles(specsRoot);
1429
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1292
1430
  const defined = /* @__PURE__ */ new Map();
1293
1431
  await collectSpecDefinitionIds(specFiles, defined);
1294
1432
  await collectScenarioDefinitionIds(scenarioFiles, defined);
@@ -1328,10 +1466,13 @@ async function collectSpecDefinitionIds(files, out) {
1328
1466
  async function collectScenarioDefinitionIds(files, out) {
1329
1467
  for (const file of files) {
1330
1468
  const text = await readFile6(file, "utf-8");
1331
- const parsed = parseGherkinFeature(text, file);
1332
- for (const scenario of parsed.scenarios) {
1469
+ const { document, errors } = parseScenarioDocument(text, file);
1470
+ if (!document || errors.length > 0) {
1471
+ continue;
1472
+ }
1473
+ for (const scenario of document.scenarios) {
1333
1474
  for (const tag of scenario.tags) {
1334
- if (SC_TAG_RE.test(tag)) {
1475
+ if (SC_TAG_RE2.test(tag)) {
1335
1476
  recordId(out, tag, file);
1336
1477
  }
1337
1478
  }
@@ -1345,7 +1486,7 @@ function recordId(out, id, file) {
1345
1486
  }
1346
1487
  function formatFileList(files, root) {
1347
1488
  return files.map((file) => {
1348
- const relative = path9.relative(root, file);
1489
+ const relative = path12.relative(root, file);
1349
1490
  return relative.length > 0 ? relative : file;
1350
1491
  }).join(", ");
1351
1492
  }
@@ -1372,35 +1513,51 @@ import { readFile as readFile7 } from "fs/promises";
1372
1513
  var GIVEN_PATTERN = /\bGiven\b/;
1373
1514
  var WHEN_PATTERN = /\bWhen\b/;
1374
1515
  var THEN_PATTERN = /\bThen\b/;
1375
- var SC_TAG_RE2 = /^SC-\d{4}$/;
1376
- var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1377
- var BR_TAG_RE = /^BR-\d{4}$/;
1516
+ var SC_TAG_RE3 = /^SC-\d{4}$/;
1517
+ var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1518
+ var BR_TAG_RE2 = /^BR-\d{4}$/;
1378
1519
  async function validateScenarios(root, config) {
1379
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1380
- const files = await collectFiles(scenariosRoot, {
1381
- extensions: [".feature"]
1382
- });
1383
- if (files.length === 0) {
1520
+ const specsRoot = resolvePath(root, config, "specsDir");
1521
+ const entries = await collectSpecEntries(specsRoot);
1522
+ if (entries.length === 0) {
1523
+ const expected = "spec-0001/scenario.md";
1524
+ const legacy = "spec-001/scenario.md";
1384
1525
  return [
1385
1526
  issue4(
1386
1527
  "QFAI-SC-000",
1387
- "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1528
+ `Scenario \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} (${legacy} \u306F\u975E\u5BFE\u5FDC)`,
1388
1529
  "info",
1389
- scenariosRoot,
1530
+ specsRoot,
1390
1531
  "scenario.files"
1391
1532
  )
1392
1533
  ];
1393
1534
  }
1394
1535
  const issues = [];
1395
- for (const file of files) {
1396
- const text = await readFile7(file, "utf-8");
1397
- issues.push(...validateScenarioContent(text, file));
1536
+ for (const entry of entries) {
1537
+ let text;
1538
+ try {
1539
+ text = await readFile7(entry.scenarioPath, "utf-8");
1540
+ } catch (error2) {
1541
+ if (isMissingFileError3(error2)) {
1542
+ issues.push(
1543
+ issue4(
1544
+ "QFAI-SC-001",
1545
+ "scenario.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1546
+ "error",
1547
+ entry.scenarioPath,
1548
+ "scenario.exists"
1549
+ )
1550
+ );
1551
+ continue;
1552
+ }
1553
+ throw error2;
1554
+ }
1555
+ issues.push(...validateScenarioContent(text, entry.scenarioPath));
1398
1556
  }
1399
1557
  return issues;
1400
1558
  }
1401
1559
  function validateScenarioContent(text, file) {
1402
1560
  const issues = [];
1403
- const parsed = parseGherkinFeature(text, file);
1404
1561
  const invalidIds = extractInvalidIds(text, [
1405
1562
  "SPEC",
1406
1563
  "BR",
@@ -1422,9 +1579,47 @@ function validateScenarioContent(text, file) {
1422
1579
  )
1423
1580
  );
1424
1581
  }
1582
+ const { document, errors } = parseScenarioDocument(text, file);
1583
+ if (!document || errors.length > 0) {
1584
+ issues.push(
1585
+ issue4(
1586
+ "QFAI-SC-010",
1587
+ `Gherkin \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errors.join(", ") || "unknown"}`,
1588
+ "error",
1589
+ file,
1590
+ "scenario.parse"
1591
+ )
1592
+ );
1593
+ return issues;
1594
+ }
1595
+ const featureSpecTags = document.featureTags.filter(
1596
+ (tag) => SPEC_TAG_RE2.test(tag)
1597
+ );
1598
+ if (featureSpecTags.length === 0) {
1599
+ issues.push(
1600
+ issue4(
1601
+ "QFAI-SC-009",
1602
+ "Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1603
+ "error",
1604
+ file,
1605
+ "scenario.featureSpec"
1606
+ )
1607
+ );
1608
+ } else if (featureSpecTags.length > 1) {
1609
+ issues.push(
1610
+ issue4(
1611
+ "QFAI-SC-009",
1612
+ `Feature \u306E SPEC \u30BF\u30B0\u304C\u8907\u6570\u3042\u308A\u307E\u3059: ${featureSpecTags.join(", ")}`,
1613
+ "error",
1614
+ file,
1615
+ "scenario.featureSpec",
1616
+ featureSpecTags
1617
+ )
1618
+ );
1619
+ }
1425
1620
  const missingStructure = [];
1426
- if (!parsed.featurePresent) missingStructure.push("Feature");
1427
- if (parsed.scenarios.length === 0) missingStructure.push("Scenario");
1621
+ if (!document.featureName) missingStructure.push("Feature");
1622
+ if (document.scenarios.length === 0) missingStructure.push("Scenario");
1428
1623
  if (missingStructure.length > 0) {
1429
1624
  issues.push(
1430
1625
  issue4(
@@ -1438,7 +1633,7 @@ function validateScenarioContent(text, file) {
1438
1633
  )
1439
1634
  );
1440
1635
  }
1441
- for (const scenario of parsed.scenarios) {
1636
+ for (const scenario of document.scenarios) {
1442
1637
  if (scenario.tags.length === 0) {
1443
1638
  issues.push(
1444
1639
  issue4(
@@ -1452,13 +1647,16 @@ function validateScenarioContent(text, file) {
1452
1647
  continue;
1453
1648
  }
1454
1649
  const missingTags = [];
1455
- if (!scenario.tags.some((tag) => SC_TAG_RE2.test(tag))) {
1456
- missingTags.push("SC");
1650
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
1651
+ if (scTags.length === 0) {
1652
+ missingTags.push("SC(0\u4EF6)");
1653
+ } else if (scTags.length > 1) {
1654
+ missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1457
1655
  }
1458
- if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
1656
+ if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
1459
1657
  missingTags.push("SPEC");
1460
1658
  }
1461
- if (!scenario.tags.some((tag) => BR_TAG_RE.test(tag))) {
1659
+ if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
1462
1660
  missingTags.push("BR");
1463
1661
  }
1464
1662
  if (missingTags.length > 0) {
@@ -1473,26 +1671,29 @@ function validateScenarioContent(text, file) {
1473
1671
  );
1474
1672
  }
1475
1673
  }
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
- );
1674
+ for (const scenario of document.scenarios) {
1675
+ const missingSteps = [];
1676
+ const keywords = scenario.steps.map((step) => step.keyword.trim());
1677
+ if (!keywords.some((keyword) => GIVEN_PATTERN.test(keyword))) {
1678
+ missingSteps.push("Given");
1679
+ }
1680
+ if (!keywords.some((keyword) => WHEN_PATTERN.test(keyword))) {
1681
+ missingSteps.push("When");
1682
+ }
1683
+ if (!keywords.some((keyword) => THEN_PATTERN.test(keyword))) {
1684
+ missingSteps.push("Then");
1685
+ }
1686
+ if (missingSteps.length > 0) {
1687
+ issues.push(
1688
+ issue4(
1689
+ "QFAI-SC-005",
1690
+ `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")} (${scenario.name})`,
1691
+ "warning",
1692
+ file,
1693
+ "scenario.steps"
1694
+ )
1695
+ );
1696
+ }
1496
1697
  }
1497
1698
  return issues;
1498
1699
  }
@@ -1513,18 +1714,25 @@ function issue4(code, message, severity, file, rule, refs) {
1513
1714
  }
1514
1715
  return issue7;
1515
1716
  }
1717
+ function isMissingFileError3(error2) {
1718
+ if (!error2 || typeof error2 !== "object") {
1719
+ return false;
1720
+ }
1721
+ return error2.code === "ENOENT";
1722
+ }
1516
1723
 
1517
1724
  // src/core/validators/spec.ts
1518
1725
  import { readFile as readFile8 } from "fs/promises";
1519
1726
  async function validateSpecs(root, config) {
1520
- const specsRoot = resolvePath(root, config, "specDir");
1521
- const files = await collectSpecFiles(specsRoot);
1522
- if (files.length === 0) {
1523
- const expected = "spec-0001-<slug>.md";
1727
+ const specsRoot = resolvePath(root, config, "specsDir");
1728
+ const entries = await collectSpecEntries(specsRoot);
1729
+ if (entries.length === 0) {
1730
+ const expected = "spec-0001/spec.md";
1731
+ const legacy = "spec-001/spec.md";
1524
1732
  return [
1525
1733
  issue5(
1526
1734
  "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}`,
1735
+ `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} (${legacy} \u306F\u975E\u5BFE\u5FDC)`,
1528
1736
  "info",
1529
1737
  specsRoot,
1530
1738
  "spec.files"
@@ -1532,12 +1740,29 @@ async function validateSpecs(root, config) {
1532
1740
  ];
1533
1741
  }
1534
1742
  const issues = [];
1535
- for (const file of files) {
1536
- const text = await readFile8(file, "utf-8");
1743
+ for (const entry of entries) {
1744
+ let text;
1745
+ try {
1746
+ text = await readFile8(entry.specPath, "utf-8");
1747
+ } catch (error2) {
1748
+ if (isMissingFileError4(error2)) {
1749
+ issues.push(
1750
+ issue5(
1751
+ "QFAI-SPEC-005",
1752
+ "spec.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1753
+ "error",
1754
+ entry.specPath,
1755
+ "spec.exists"
1756
+ )
1757
+ );
1758
+ continue;
1759
+ }
1760
+ throw error2;
1761
+ }
1537
1762
  issues.push(
1538
1763
  ...validateSpecContent(
1539
1764
  text,
1540
- file,
1765
+ entry.specPath,
1541
1766
  config.validation.require.specSections
1542
1767
  )
1543
1768
  );
@@ -1659,29 +1884,25 @@ function issue5(code, message, severity, file, rule, refs) {
1659
1884
  }
1660
1885
  return issue7;
1661
1886
  }
1887
+ function isMissingFileError4(error2) {
1888
+ if (!error2 || typeof error2 !== "object") {
1889
+ return false;
1890
+ }
1891
+ return error2.code === "ENOENT";
1892
+ }
1662
1893
 
1663
1894
  // src/core/validators/traceability.ts
1664
1895
  import { readFile as readFile9 } from "fs/promises";
1665
- var SC_TAG_RE3 = /^SC-\d{4}$/;
1666
- var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1667
- var BR_TAG_RE2 = /^BR-\d{4}$/;
1668
- var UI_TAG_RE = /^UI-\d{4}$/;
1669
- var API_TAG_RE = /^API-\d{4}$/;
1670
- var DATA_TAG_RE = /^DATA-\d{4}$/;
1896
+ var SC_TAG_RE4 = /^SC-\d{4}$/;
1897
+ var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
1898
+ var BR_TAG_RE3 = /^BR-\d{4}$/;
1671
1899
  async function validateTraceability(root, config) {
1672
1900
  const issues = [];
1673
- const specsRoot = resolvePath(root, config, "specDir");
1674
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1675
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1901
+ const specsRoot = resolvePath(root, config, "specsDir");
1676
1902
  const srcRoot = resolvePath(root, config, "srcDir");
1677
1903
  const testsRoot = resolvePath(root, config, "testsDir");
1678
1904
  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
- });
1905
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1685
1906
  const upstreamIds = /* @__PURE__ */ new Set();
1686
1907
  const specIds = /* @__PURE__ */ new Set();
1687
1908
  const brIdsInSpecs = /* @__PURE__ */ new Set();
@@ -1729,111 +1950,100 @@ async function validateTraceability(root, config) {
1729
1950
  specToBrIds.set(parsed.specId, current);
1730
1951
  }
1731
1952
  }
1732
- for (const file of decisionFiles) {
1733
- const text = await readFile9(file, "utf-8");
1734
- extractAllIds(text).forEach((id) => upstreamIds.add(id));
1735
- }
1736
1953
  for (const file of scenarioFiles) {
1737
1954
  const text = await readFile9(file, "utf-8");
1738
1955
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1739
- const parsed = parseGherkinFeature(text, file);
1740
- const specIdsInScenario = /* @__PURE__ */ new Set();
1741
- const brIds = /* @__PURE__ */ new Set();
1742
- const scIds = /* @__PURE__ */ new Set();
1743
- const scenarioIds = /* @__PURE__ */ new Set();
1744
- for (const scenario of parsed.scenarios) {
1745
- for (const tag of scenario.tags) {
1746
- if (SPEC_TAG_RE2.test(tag)) {
1747
- specIdsInScenario.add(tag);
1748
- }
1749
- if (BR_TAG_RE2.test(tag)) {
1750
- brIds.add(tag);
1751
- }
1752
- if (SC_TAG_RE3.test(tag)) {
1753
- scIds.add(tag);
1754
- }
1755
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1756
- scenarioIds.add(tag);
1757
- }
1758
- }
1759
- }
1760
- const specIdsList = Array.from(specIdsInScenario);
1761
- const brIdsList = Array.from(brIds);
1762
- const scIdsList = Array.from(scIds);
1763
- const scenarioIdsList = Array.from(scenarioIds);
1764
- brIdsList.forEach((id) => brIdsInScenarios.add(id));
1765
- scIdsList.forEach((id) => scIdsInScenarios.add(id));
1766
- scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
1767
- if (scenarioIdsList.length > 0) {
1768
- scIdsList.forEach((id) => scWithContracts.add(id));
1769
- }
1770
- const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
1771
- if (unknownSpecIds.length > 0) {
1772
- issues.push(
1773
- issue6(
1774
- "QFAI-TRACE-005",
1775
- `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
1776
- "error",
1777
- file,
1778
- "traceability.scenarioSpecExists",
1779
- unknownSpecIds
1780
- )
1781
- );
1782
- }
1783
- const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
1784
- if (unknownBrIds.length > 0) {
1785
- issues.push(
1786
- issue6(
1787
- "QFAI-TRACE-006",
1788
- `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
1789
- "error",
1790
- file,
1791
- "traceability.scenarioBrExists",
1792
- unknownBrIds
1793
- )
1794
- );
1795
- }
1796
- const unknownContractIds = scenarioIdsList.filter(
1797
- (id) => !contractIds.has(id)
1798
- );
1799
- if (unknownContractIds.length > 0) {
1800
- issues.push(
1801
- issue6(
1802
- "QFAI-TRACE-008",
1803
- `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1804
- ", "
1805
- )}`,
1806
- config.validation.traceability.unknownContractIdSeverity,
1807
- file,
1808
- "traceability.scenarioContractExists",
1809
- unknownContractIds
1810
- )
1811
- );
1956
+ const { document, errors } = parseScenarioDocument(text, file);
1957
+ if (!document || errors.length > 0) {
1958
+ continue;
1812
1959
  }
1813
- if (specIdsList.length > 0) {
1814
- const allowedBrIds = /* @__PURE__ */ new Set();
1815
- for (const specId of specIdsList) {
1816
- const brIdsForSpec = specToBrIds.get(specId);
1817
- if (!brIdsForSpec) {
1818
- continue;
1819
- }
1820
- brIdsForSpec.forEach((id) => allowedBrIds.add(id));
1960
+ const atoms = buildScenarioAtoms(document);
1961
+ for (const [index, scenario] of document.scenarios.entries()) {
1962
+ const atom = atoms[index];
1963
+ if (!atom) {
1964
+ continue;
1965
+ }
1966
+ const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
1967
+ const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
1968
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
1969
+ brTags.forEach((id) => brIdsInScenarios.add(id));
1970
+ scTags.forEach((id) => scIdsInScenarios.add(id));
1971
+ atom.contractIds.forEach((id) => scenarioContractIds.add(id));
1972
+ if (atom.contractIds.length > 0) {
1973
+ scTags.forEach((id) => scWithContracts.add(id));
1974
+ }
1975
+ const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
1976
+ if (unknownSpecIds.length > 0) {
1977
+ issues.push(
1978
+ issue6(
1979
+ "QFAI-TRACE-005",
1980
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(
1981
+ ", "
1982
+ )} (${scenario.name})`,
1983
+ "error",
1984
+ file,
1985
+ "traceability.scenarioSpecExists",
1986
+ unknownSpecIds
1987
+ )
1988
+ );
1821
1989
  }
1822
- const invalidBrIds = brIdsList.filter((id) => !allowedBrIds.has(id));
1823
- if (invalidBrIds.length > 0) {
1990
+ const unknownBrIds = brTags.filter((id) => !brIdsInSpecs.has(id));
1991
+ if (unknownBrIds.length > 0) {
1824
1992
  issues.push(
1825
1993
  issue6(
1826
- "QFAI-TRACE-007",
1827
- `Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
1994
+ "QFAI-TRACE-006",
1995
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(
1828
1996
  ", "
1829
- )} (SPEC: ${specIdsList.join(", ")})`,
1997
+ )} (${scenario.name})`,
1830
1998
  "error",
1831
1999
  file,
1832
- "traceability.scenarioBrUnderSpec",
1833
- invalidBrIds
2000
+ "traceability.scenarioBrExists",
2001
+ unknownBrIds
2002
+ )
2003
+ );
2004
+ }
2005
+ const unknownContractIds = atom.contractIds.filter(
2006
+ (id) => !contractIds.has(id)
2007
+ );
2008
+ if (unknownContractIds.length > 0) {
2009
+ issues.push(
2010
+ issue6(
2011
+ "QFAI-TRACE-008",
2012
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
2013
+ ", "
2014
+ )} (${scenario.name})`,
2015
+ config.validation.traceability.unknownContractIdSeverity,
2016
+ file,
2017
+ "traceability.scenarioContractExists",
2018
+ unknownContractIds
1834
2019
  )
1835
2020
  );
1836
2021
  }
2022
+ if (specTags.length > 0 && brTags.length > 0) {
2023
+ const allowedBrIds = /* @__PURE__ */ new Set();
2024
+ for (const specId of specTags) {
2025
+ const brIdsForSpec = specToBrIds.get(specId);
2026
+ if (!brIdsForSpec) {
2027
+ continue;
2028
+ }
2029
+ brIdsForSpec.forEach((id) => allowedBrIds.add(id));
2030
+ }
2031
+ const invalidBrIds = brTags.filter((id) => !allowedBrIds.has(id));
2032
+ if (invalidBrIds.length > 0) {
2033
+ issues.push(
2034
+ issue6(
2035
+ "QFAI-TRACE-007",
2036
+ `Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
2037
+ ", "
2038
+ )} (SPEC: ${specTags.join(", ")}) (${scenario.name})`,
2039
+ "error",
2040
+ file,
2041
+ "traceability.scenarioBrUnderSpec",
2042
+ invalidBrIds
2043
+ )
2044
+ );
2045
+ }
2046
+ }
1837
2047
  }
1838
2048
  }
1839
2049
  if (upstreamIds.size === 0) {
@@ -1876,7 +2086,7 @@ async function validateTraceability(root, config) {
1876
2086
  ", "
1877
2087
  )}`,
1878
2088
  "error",
1879
- scenariosRoot,
2089
+ specsRoot,
1880
2090
  "traceability.scMustTouchContracts",
1881
2091
  scWithoutContracts
1882
2092
  )
@@ -1894,7 +2104,7 @@ async function validateTraceability(root, config) {
1894
2104
  "QFAI_CONTRACT_ORPHAN",
1895
2105
  `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
1896
2106
  "error",
1897
- scenariosRoot,
2107
+ specsRoot,
1898
2108
  "traceability.allowOrphanContracts",
1899
2109
  orphanContracts
1900
2110
  )
@@ -1979,8 +2189,8 @@ async function validateProject(root, configResult) {
1979
2189
  const issues = [
1980
2190
  ...configIssues,
1981
2191
  ...await validateSpecs(root, config),
2192
+ ...await validateDeltas(root, config),
1982
2193
  ...await validateScenarios(root, config),
1983
- ...await validateDecisions(root, config),
1984
2194
  ...await validateContracts(root, config),
1985
2195
  ...await validateDefinedIds(root, config),
1986
2196
  ...await validateTraceability(root, config)
@@ -2009,21 +2219,15 @@ async function createReportData(root, validation, configResult) {
2009
2219
  const resolved = configResult ?? await loadConfig(root);
2010
2220
  const config = resolved.config;
2011
2221
  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");
2222
+ const specsRoot = resolvePath(root, config, "specsDir");
2223
+ const contractsRoot = resolvePath(root, config, "contractsDir");
2224
+ const apiRoot = path13.join(contractsRoot, "api");
2225
+ const uiRoot = path13.join(contractsRoot, "ui");
2226
+ const dbRoot = path13.join(contractsRoot, "db");
2018
2227
  const srcRoot = resolvePath(root, config, "srcDir");
2019
2228
  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
- });
2229
+ const specFiles = await collectSpecFiles(specsRoot);
2230
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
2027
2231
  const {
2028
2232
  api: apiFiles,
2029
2233
  ui: uiFiles,
@@ -2032,7 +2236,6 @@ async function createReportData(root, validation, configResult) {
2032
2236
  const idsByPrefix = await collectIds([
2033
2237
  ...specFiles,
2034
2238
  ...scenarioFiles,
2035
- ...decisionFiles,
2036
2239
  ...apiFiles,
2037
2240
  ...uiFiles,
2038
2241
  ...dbFiles
@@ -2057,7 +2260,6 @@ async function createReportData(root, validation, configResult) {
2057
2260
  summary: {
2058
2261
  specs: specFiles.length,
2059
2262
  scenarios: scenarioFiles.length,
2060
- decisions: decisionFiles.length,
2061
2263
  contracts: {
2062
2264
  api: apiFiles.length,
2063
2265
  ui: uiFiles.length,
@@ -2091,7 +2293,6 @@ function formatReportMarkdown(data) {
2091
2293
  lines.push("## \u6982\u8981");
2092
2294
  lines.push(`- specs: ${data.summary.specs}`);
2093
2295
  lines.push(`- scenarios: ${data.summary.scenarios}`);
2094
- lines.push(`- decisions: ${data.summary.decisions}`);
2095
2296
  lines.push(
2096
2297
  `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
2097
2298
  );
@@ -2250,21 +2451,22 @@ function buildHotspots(issues) {
2250
2451
 
2251
2452
  // src/cli/commands/report.ts
2252
2453
  async function runReport(options) {
2253
- const root = path10.resolve(options.root);
2454
+ const root = path14.resolve(options.root);
2254
2455
  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);
2456
+ const input = configResult.config.output.validateJsonPath;
2457
+ const inputPath = path14.isAbsolute(input) ? input : path14.resolve(root, input);
2257
2458
  let validation;
2258
2459
  try {
2259
2460
  validation = await readValidationResult(inputPath);
2260
2461
  } catch (err) {
2261
- if (isMissingFileError(err)) {
2462
+ if (isMissingFileError5(err)) {
2262
2463
  error(
2263
2464
  [
2264
2465
  `qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
2265
2466
  "",
2266
- "\u307E\u305A validate.json \u3092\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
2267
- ` qfai validate --json-path ${input}`,
2467
+ "\u307E\u305A qfai validate \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
2468
+ " qfai validate",
2469
+ "\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/out/validate.json\uFF09",
2268
2470
  "",
2269
2471
  "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
2472
  ].join("\n")
@@ -2276,10 +2478,11 @@ async function runReport(options) {
2276
2478
  }
2277
2479
  const data = await createReportData(root, validation, configResult);
2278
2480
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
2279
- const defaultOut = options.format === "json" ? ".qfai/out/report.json" : ".qfai/out/report.md";
2481
+ const outRoot = resolvePath(root, configResult.config, "outDir");
2482
+ const defaultOut = options.format === "json" ? path14.join(outRoot, "report.json") : path14.join(outRoot, "report.md");
2280
2483
  const out = options.outPath ?? defaultOut;
2281
- const outPath = path10.isAbsolute(out) ? out : path10.resolve(root, out);
2282
- await mkdir2(path10.dirname(outPath), { recursive: true });
2484
+ const outPath = path14.isAbsolute(out) ? out : path14.resolve(root, out);
2485
+ await mkdir2(path14.dirname(outPath), { recursive: true });
2283
2486
  await writeFile(outPath, `${output}
2284
2487
  `, "utf-8");
2285
2488
  info(
@@ -2320,7 +2523,7 @@ function isValidationResult(value) {
2320
2523
  }
2321
2524
  return typeof counts.info === "number" && typeof counts.warning === "number" && typeof counts.error === "number";
2322
2525
  }
2323
- function isMissingFileError(error2) {
2526
+ function isMissingFileError5(error2) {
2324
2527
  if (!error2 || typeof error2 !== "object") {
2325
2528
  return false;
2326
2529
  }
@@ -2330,7 +2533,7 @@ function isMissingFileError(error2) {
2330
2533
 
2331
2534
  // src/cli/commands/validate.ts
2332
2535
  import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
2333
- import path11 from "path";
2536
+ import path15 from "path";
2334
2537
 
2335
2538
  // src/cli/lib/failOn.ts
2336
2539
  function shouldFail(result, failOn) {
@@ -2345,24 +2548,17 @@ function shouldFail(result, failOn) {
2345
2548
 
2346
2549
  // src/cli/commands/validate.ts
2347
2550
  async function runValidate(options) {
2348
- const root = path11.resolve(options.root);
2551
+ const root = path15.resolve(options.root);
2349
2552
  const configResult = await loadConfig(root);
2350
2553
  const result = await validateProject(root, configResult);
2351
- const format = options.format ?? configResult.config.output.format;
2352
- const explicitJsonPath = options.jsonPath;
2554
+ const format = options.format ?? "text";
2353
2555
  if (format === "text") {
2354
2556
  emitText(result);
2355
2557
  }
2356
2558
  if (format === "github") {
2357
2559
  result.issues.forEach(emitGitHub);
2358
2560
  }
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
- }
2561
+ await emitJson(result, root, configResult.config.output.validateJsonPath);
2366
2562
  const failOn = resolveFailOn(options, configResult.config.validation.failOn);
2367
2563
  return shouldFail(result, failOn) ? 1 : 0;
2368
2564
  }
@@ -2401,8 +2597,8 @@ function emitGitHub(issue7) {
2401
2597
  );
2402
2598
  }
2403
2599
  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 });
2600
+ const abs = path15.isAbsolute(jsonPath) ? jsonPath : path15.resolve(root, jsonPath);
2601
+ await mkdir3(path15.dirname(abs), { recursive: true });
2406
2602
  await writeFile2(abs, `${JSON.stringify(result, null, 2)}
2407
2603
  `, "utf-8");
2408
2604
  }
@@ -2463,15 +2659,6 @@ function parseArgs(argv, cwd) {
2463
2659
  i += 1;
2464
2660
  break;
2465
2661
  }
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
2662
  case "--out":
2476
2663
  {
2477
2664
  const next = args[i + 1];
@@ -2502,7 +2689,7 @@ function applyFormatOption(command, value, options) {
2502
2689
  return;
2503
2690
  }
2504
2691
  if (command === "validate") {
2505
- if (value === "text" || value === "json" || value === "github") {
2692
+ if (value === "text" || value === "github") {
2506
2693
  options.validateFormat = value;
2507
2694
  }
2508
2695
  return;
@@ -2510,7 +2697,7 @@ function applyFormatOption(command, value, options) {
2510
2697
  if (value === "md" || value === "json") {
2511
2698
  options.reportFormat = value;
2512
2699
  }
2513
- if (value === "text" || value === "json" || value === "github") {
2700
+ if (value === "text" || value === "github") {
2514
2701
  options.validateFormat = value;
2515
2702
  }
2516
2703
  }
@@ -2536,15 +2723,13 @@ async function run(argv, cwd) {
2536
2723
  root: options.root,
2537
2724
  strict: options.strict,
2538
2725
  format: options.validateFormat,
2539
- ...options.failOn !== void 0 ? { failOn: options.failOn } : {},
2540
- ...options.jsonPath !== void 0 ? { jsonPath: options.jsonPath } : {}
2726
+ ...options.failOn !== void 0 ? { failOn: options.failOn } : {}
2541
2727
  });
2542
2728
  return;
2543
2729
  case "report":
2544
2730
  await runReport({
2545
2731
  root: options.root,
2546
2732
  format: options.reportFormat,
2547
- ...options.jsonPath !== void 0 ? { jsonPath: options.jsonPath } : {},
2548
2733
  ...options.reportOut !== void 0 ? { outPath: options.reportOut } : {}
2549
2734
  });
2550
2735
  return;
@@ -2566,14 +2751,13 @@ Options:
2566
2751
  --root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
2567
2752
  --dir <path> init \u306E\u51FA\u529B\u5148
2568
2753
  --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
2754
+ --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
2755
  --dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
2571
- --format <text|json|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
2756
+ --format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
2572
2757
  --format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
2573
- --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
2758
+ --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
2574
2759
  --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
2760
+ --out <path> report: \u51FA\u529B\u5148
2577
2761
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A
2578
2762
  `;
2579
2763
  }