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.
package/dist/index.mjs CHANGED
@@ -4,13 +4,11 @@ import path from "path";
4
4
  import { parse as parseYaml } from "yaml";
5
5
  var defaultConfig = {
6
6
  paths: {
7
- specDir: ".qfai/spec",
8
- decisionsDir: ".qfai/spec/decisions",
9
- scenariosDir: ".qfai/spec/scenarios",
10
7
  contractsDir: ".qfai/contracts",
11
- uiContractsDir: ".qfai/contracts/ui",
12
- apiContractsDir: ".qfai/contracts/api",
13
- dataContractsDir: ".qfai/contracts/db",
8
+ specsDir: ".qfai/specs",
9
+ rulesDir: ".qfai/rules",
10
+ outDir: ".qfai/out",
11
+ promptsDir: ".qfai/prompts",
14
12
  srcDir: "src",
15
13
  testsDir: "tests"
16
14
  },
@@ -35,8 +33,7 @@ var defaultConfig = {
35
33
  }
36
34
  },
37
35
  output: {
38
- format: "text",
39
- jsonPath: ".qfai/out/validate.json"
36
+ validateJsonPath: ".qfai/out/validate.json"
40
37
  }
41
38
  };
42
39
  function getConfigPath(root) {
@@ -85,27 +82,6 @@ function normalizePaths(raw, configPath, issues) {
85
82
  return base;
86
83
  }
87
84
  return {
88
- specDir: readString(
89
- raw.specDir,
90
- base.specDir,
91
- "paths.specDir",
92
- configPath,
93
- issues
94
- ),
95
- decisionsDir: readString(
96
- raw.decisionsDir,
97
- base.decisionsDir,
98
- "paths.decisionsDir",
99
- configPath,
100
- issues
101
- ),
102
- scenariosDir: readString(
103
- raw.scenariosDir,
104
- base.scenariosDir,
105
- "paths.scenariosDir",
106
- configPath,
107
- issues
108
- ),
109
85
  contractsDir: readString(
110
86
  raw.contractsDir,
111
87
  base.contractsDir,
@@ -113,24 +89,31 @@ function normalizePaths(raw, configPath, issues) {
113
89
  configPath,
114
90
  issues
115
91
  ),
116
- uiContractsDir: readString(
117
- raw.uiContractsDir,
118
- base.uiContractsDir,
119
- "paths.uiContractsDir",
92
+ specsDir: readString(
93
+ raw.specsDir,
94
+ base.specsDir,
95
+ "paths.specsDir",
96
+ configPath,
97
+ issues
98
+ ),
99
+ rulesDir: readString(
100
+ raw.rulesDir,
101
+ base.rulesDir,
102
+ "paths.rulesDir",
120
103
  configPath,
121
104
  issues
122
105
  ),
123
- apiContractsDir: readString(
124
- raw.apiContractsDir,
125
- base.apiContractsDir,
126
- "paths.apiContractsDir",
106
+ outDir: readString(
107
+ raw.outDir,
108
+ base.outDir,
109
+ "paths.outDir",
127
110
  configPath,
128
111
  issues
129
112
  ),
130
- dataContractsDir: readString(
131
- raw.dataContractsDir,
132
- base.dataContractsDir,
133
- "paths.dataContractsDir",
113
+ promptsDir: readString(
114
+ raw.promptsDir,
115
+ base.promptsDir,
116
+ "paths.promptsDir",
134
117
  configPath,
135
118
  issues
136
119
  ),
@@ -253,17 +236,10 @@ function normalizeOutput(raw, configPath, issues) {
253
236
  return base;
254
237
  }
255
238
  return {
256
- format: readOutputFormat(
257
- raw.format,
258
- base.format,
259
- "output.format",
260
- configPath,
261
- issues
262
- ),
263
- jsonPath: readString(
264
- raw.jsonPath,
265
- base.jsonPath,
266
- "output.jsonPath",
239
+ validateJsonPath: readString(
240
+ raw.validateJsonPath,
241
+ base.validateJsonPath,
242
+ "output.validateJsonPath",
267
243
  configPath,
268
244
  issues
269
245
  )
@@ -330,20 +306,6 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
330
306
  }
331
307
  return fallback;
332
308
  }
333
- function readOutputFormat(value, fallback, label, configPath, issues) {
334
- if (value === "text" || value === "json" || value === "github") {
335
- return value;
336
- }
337
- if (value !== void 0) {
338
- issues.push(
339
- configIssue(
340
- configPath,
341
- `${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
342
- )
343
- );
344
- }
345
- return fallback;
346
- }
347
309
  function configIssue(file, message) {
348
310
  return {
349
311
  code: "QFAI_CONFIG_INVALID",
@@ -424,9 +386,10 @@ function isValidId(value, prefix) {
424
386
 
425
387
  // src/core/report.ts
426
388
  import { readFile as readFile10 } from "fs/promises";
389
+ import path10 from "path";
427
390
 
428
391
  // src/core/discovery.ts
429
- import path3 from "path";
392
+ import { access as access2 } from "fs/promises";
430
393
 
431
394
  // src/core/fs.ts
432
395
  import { access, readdir } from "fs/promises";
@@ -483,11 +446,50 @@ async function exists(target) {
483
446
  }
484
447
  }
485
448
 
449
+ // src/core/specLayout.ts
450
+ import { readdir as readdir2 } from "fs/promises";
451
+ import path3 from "path";
452
+ var SPEC_DIR_RE = /^spec-\d{4}$/;
453
+ async function collectSpecEntries(specsRoot) {
454
+ const dirs = await listSpecDirs(specsRoot);
455
+ const entries = dirs.map((dir) => ({
456
+ dir,
457
+ specPath: path3.join(dir, "spec.md"),
458
+ deltaPath: path3.join(dir, "delta.md"),
459
+ scenarioPath: path3.join(dir, "scenario.md")
460
+ }));
461
+ return entries.sort((a, b) => a.dir.localeCompare(b.dir));
462
+ }
463
+ async function listSpecDirs(specsRoot) {
464
+ try {
465
+ const items = await readdir2(specsRoot, { withFileTypes: true });
466
+ return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => path3.join(specsRoot, name));
467
+ } catch (error) {
468
+ if (isMissingFileError(error)) {
469
+ return [];
470
+ }
471
+ throw error;
472
+ }
473
+ }
474
+ function isMissingFileError(error) {
475
+ if (!error || typeof error !== "object") {
476
+ return false;
477
+ }
478
+ return error.code === "ENOENT";
479
+ }
480
+
486
481
  // src/core/discovery.ts
487
- var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/;
488
- async function collectSpecFiles(specRoot) {
489
- const files = await collectFiles(specRoot, { extensions: [".md"] });
490
- return files.filter((file) => isSpecFile(file));
482
+ async function collectSpecPackDirs(specsRoot) {
483
+ const entries = await collectSpecEntries(specsRoot);
484
+ return entries.map((entry) => entry.dir);
485
+ }
486
+ async function collectSpecFiles(specsRoot) {
487
+ const entries = await collectSpecEntries(specsRoot);
488
+ return filterExisting(entries.map((entry) => entry.specPath));
489
+ }
490
+ async function collectScenarioFiles(specsRoot) {
491
+ const entries = await collectSpecEntries(specsRoot);
492
+ return filterExisting(entries.map((entry) => entry.scenarioPath));
491
493
  }
492
494
  async function collectUiContractFiles(uiRoot) {
493
495
  return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
@@ -506,9 +508,22 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
506
508
  ]);
507
509
  return { ui, api, db };
508
510
  }
509
- function isSpecFile(filePath) {
510
- const name = path3.basename(filePath).toLowerCase();
511
- return SPEC_NAMED_PATTERN.test(name);
511
+ async function filterExisting(files) {
512
+ const existing = [];
513
+ for (const file of files) {
514
+ if (await exists2(file)) {
515
+ existing.push(file);
516
+ }
517
+ }
518
+ return existing;
519
+ }
520
+ async function exists2(target) {
521
+ try {
522
+ await access2(target);
523
+ return true;
524
+ } catch {
525
+ return false;
526
+ }
512
527
  }
513
528
 
514
529
  // src/core/types.ts
@@ -519,8 +534,8 @@ import { readFile as readFile2 } from "fs/promises";
519
534
  import path4 from "path";
520
535
  import { fileURLToPath } from "url";
521
536
  async function resolveToolVersion() {
522
- if ("0.3.0".length > 0) {
523
- return "0.3.0";
537
+ if ("0.3.2".length > 0) {
538
+ return "0.3.2";
524
539
  }
525
540
  try {
526
541
  const packagePath = resolvePackageJsonPath();
@@ -540,6 +555,7 @@ function resolvePackageJsonPath() {
540
555
 
541
556
  // src/core/validators/contracts.ts
542
557
  import { readFile as readFile3 } from "fs/promises";
558
+ import path6 from "path";
543
559
 
544
560
  // src/core/contracts.ts
545
561
  import path5 from "path";
@@ -595,19 +611,10 @@ var SQL_DANGEROUS_PATTERNS = [
595
611
  ];
596
612
  async function validateContracts(root, config) {
597
613
  const issues = [];
598
- issues.push(
599
- ...await validateUiContracts(resolvePath(root, config, "uiContractsDir"))
600
- );
601
- issues.push(
602
- ...await validateApiContracts(
603
- resolvePath(root, config, "apiContractsDir")
604
- )
605
- );
606
- issues.push(
607
- ...await validateDataContracts(
608
- resolvePath(root, config, "dataContractsDir")
609
- )
610
- );
614
+ const contractsRoot = resolvePath(root, config, "contractsDir");
615
+ issues.push(...await validateUiContracts(path6.join(contractsRoot, "ui")));
616
+ issues.push(...await validateApiContracts(path6.join(contractsRoot, "api")));
617
+ issues.push(...await validateDataContracts(path6.join(contractsRoot, "db")));
611
618
  return issues;
612
619
  }
613
620
  async function validateUiContracts(uiRoot) {
@@ -840,72 +847,78 @@ function issue(code, message, severity, file, rule, refs) {
840
847
  return issue7;
841
848
  }
842
849
 
843
- // src/core/validators/decisions.ts
850
+ // src/core/validators/delta.ts
844
851
  import { readFile as readFile4 } from "fs/promises";
845
-
846
- // src/core/parse/adr.ts
847
- var ADR_ID_RE = /\bADR-\d{4}\b/;
848
- function extractField(md, key) {
849
- const pattern = new RegExp(`^\\s*-\\s*${key}:\\s*(.+)\\s*$`, "m");
850
- return md.match(pattern)?.[1]?.trim();
851
- }
852
- function parseAdr(md, file) {
853
- const adrId = md.match(ADR_ID_RE)?.[0];
854
- const fields = {};
855
- const status = extractField(md, "Status");
856
- const context = extractField(md, "Context");
857
- const decision = extractField(md, "Decision");
858
- const consequences = extractField(md, "Consequences");
859
- const related = extractField(md, "Related");
860
- if (status) fields.status = status;
861
- if (context) fields.context = context;
862
- if (decision) fields.decision = decision;
863
- if (consequences) fields.consequences = consequences;
864
- if (related) fields.related = related;
865
- const parsed = {
866
- file,
867
- fields
868
- };
869
- if (adrId) {
870
- parsed.adrId = adrId;
871
- }
872
- return parsed;
873
- }
874
-
875
- // src/core/validators/decisions.ts
876
- var REQUIRED_FIELDS = [
877
- { key: "status", label: "Status" },
878
- { key: "context", label: "Context" },
879
- { key: "decision", label: "Decision" },
880
- { key: "consequences", label: "Consequences" }
881
- ];
882
- async function validateDecisions(root, config) {
883
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
884
- const files = await collectFiles(decisionsRoot, { extensions: [".md"] });
885
- if (files.length === 0) {
852
+ import path7 from "path";
853
+ var SECTION_RE = /^##\s+変更区分/m;
854
+ var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
855
+ var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
856
+ var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
857
+ var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
858
+ async function validateDeltas(root, config) {
859
+ const specsRoot = resolvePath(root, config, "specsDir");
860
+ const packs = await collectSpecPackDirs(specsRoot);
861
+ if (packs.length === 0) {
886
862
  return [];
887
863
  }
888
864
  const issues = [];
889
- for (const file of files) {
890
- const text = await readFile4(file, "utf-8");
891
- const parsed = parseAdr(text, file);
892
- const missing = REQUIRED_FIELDS.filter(
893
- (field) => !parsed.fields[field.key]
894
- );
895
- if (missing.length > 0) {
865
+ for (const pack of packs) {
866
+ const deltaPath = path7.join(pack, "delta.md");
867
+ let text;
868
+ try {
869
+ text = await readFile4(deltaPath, "utf-8");
870
+ } catch (error) {
871
+ if (isMissingFileError2(error)) {
872
+ issues.push(
873
+ issue2(
874
+ "QFAI-DELTA-001",
875
+ "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
876
+ "error",
877
+ deltaPath,
878
+ "delta.exists"
879
+ )
880
+ );
881
+ continue;
882
+ }
883
+ throw error;
884
+ }
885
+ const hasSection = SECTION_RE.test(text);
886
+ const hasCompatibility = COMPAT_LINE_RE.test(text);
887
+ const hasChange = CHANGE_LINE_RE.test(text);
888
+ if (!hasSection || !hasCompatibility || !hasChange) {
896
889
  issues.push(
897
890
  issue2(
898
- "QFAI-ADR-001",
899
- `ADR \u5FC5\u9808\u30D5\u30A3\u30FC\u30EB\u30C9\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missing.map((field) => field.label).join(", ")}`,
891
+ "QFAI-DELTA-002",
892
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
900
893
  "error",
901
- file,
902
- "adr.requiredFields"
894
+ deltaPath,
895
+ "delta.section"
896
+ )
897
+ );
898
+ continue;
899
+ }
900
+ const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
901
+ const changeChecked = CHANGE_CHECKED_RE.test(text);
902
+ if (compatibilityChecked === changeChecked) {
903
+ issues.push(
904
+ issue2(
905
+ "QFAI-DELTA-003",
906
+ "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",
907
+ "error",
908
+ deltaPath,
909
+ "delta.classification"
903
910
  )
904
911
  );
905
912
  }
906
913
  }
907
914
  return issues;
908
915
  }
916
+ function isMissingFileError2(error) {
917
+ if (!error || typeof error !== "object") {
918
+ return false;
919
+ }
920
+ return error.code === "ENOENT";
921
+ }
909
922
  function issue2(code, message, severity, file, rule, refs) {
910
923
  const issue7 = {
911
924
  code,
@@ -926,14 +939,16 @@ function issue2(code, message, severity, file, rule, refs) {
926
939
 
927
940
  // src/core/validators/ids.ts
928
941
  import { readFile as readFile6 } from "fs/promises";
929
- import path6 from "path";
942
+ import path9 from "path";
930
943
 
931
944
  // src/core/contractIndex.ts
932
945
  import { readFile as readFile5 } from "fs/promises";
946
+ import path8 from "path";
933
947
  async function buildContractIndex(root, config) {
934
- const uiRoot = resolvePath(root, config, "uiContractsDir");
935
- const apiRoot = resolvePath(root, config, "apiContractsDir");
936
- const dataRoot = resolvePath(root, config, "dataContractsDir");
948
+ const contractsRoot = resolvePath(root, config, "contractsDir");
949
+ const uiRoot = path8.join(contractsRoot, "ui");
950
+ const apiRoot = path8.join(contractsRoot, "api");
951
+ const dataRoot = path8.join(contractsRoot, "db");
937
952
  const [uiFiles, apiFiles, dataFiles] = await Promise.all([
938
953
  collectUiContractFiles(uiRoot),
939
954
  collectApiContractFiles(apiRoot),
@@ -987,38 +1002,6 @@ function record(index, id, file) {
987
1002
  index.idToFiles.set(id, current);
988
1003
  }
989
1004
 
990
- // src/core/parse/gherkin.ts
991
- var FEATURE_RE = /^\s*Feature:\s+/;
992
- var SCENARIO_RE = /^\s*Scenario:\s*(.+)\s*$/;
993
- var TAG_LINE_RE = /^\s*@/;
994
- function parseTags(line) {
995
- return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
996
- }
997
- function parseGherkinFeature(text, file) {
998
- const lines = text.split(/\r?\n/);
999
- const scenarios = [];
1000
- let featurePresent = false;
1001
- for (let i = 0; i < lines.length; i++) {
1002
- const line = lines[i] ?? "";
1003
- if (FEATURE_RE.test(line)) {
1004
- featurePresent = true;
1005
- }
1006
- const match = line.match(SCENARIO_RE);
1007
- if (!match) continue;
1008
- const scenarioName = match[1];
1009
- if (!scenarioName) continue;
1010
- const tags = [];
1011
- for (let j = i - 1; j >= 0; j--) {
1012
- const previous = lines[j] ?? "";
1013
- if (previous.trim() === "") continue;
1014
- if (!TAG_LINE_RE.test(previous)) break;
1015
- tags.unshift(...parseTags(previous));
1016
- }
1017
- scenarios.push({ name: scenarioName, line: i + 1, tags });
1018
- }
1019
- return { file, featurePresent, scenarios };
1020
- }
1021
-
1022
1005
  // src/core/parse/markdown.ts
1023
1006
  var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1024
1007
  function parseHeadings(md) {
@@ -1062,9 +1045,9 @@ function extractH2Sections(md) {
1062
1045
 
1063
1046
  // src/core/parse/spec.ts
1064
1047
  var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1065
- var BR_LINE_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[0-3])\)\s*(.+)$/;
1066
- var BR_LINE_ANY_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[^)]+)\)\s*(.+)$/;
1067
- var BR_LINE_NO_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s+(?!\()(.*\S.*)$/;
1048
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1049
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1050
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1068
1051
  var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1069
1052
  var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1070
1053
  function parseSpec(md, file) {
@@ -1137,16 +1120,171 @@ function parseSpec(md, file) {
1137
1120
  return parsed;
1138
1121
  }
1139
1122
 
1140
- // src/core/validators/ids.ts
1123
+ // src/core/gherkin/parse.ts
1124
+ import {
1125
+ AstBuilder,
1126
+ GherkinClassicTokenMatcher,
1127
+ Parser
1128
+ } from "@cucumber/gherkin";
1129
+ import { randomUUID } from "crypto";
1130
+ function parseGherkin(source, uri) {
1131
+ const errors = [];
1132
+ const uuidFn = () => randomUUID();
1133
+ const builder = new AstBuilder(uuidFn);
1134
+ const matcher = new GherkinClassicTokenMatcher();
1135
+ const parser = new Parser(builder, matcher);
1136
+ try {
1137
+ const gherkinDocument = parser.parse(source);
1138
+ gherkinDocument.uri = uri;
1139
+ return { gherkinDocument, errors };
1140
+ } catch (error) {
1141
+ errors.push(formatError3(error));
1142
+ return { gherkinDocument: null, errors };
1143
+ }
1144
+ }
1145
+ function formatError3(error) {
1146
+ if (error instanceof Error) {
1147
+ return error.message;
1148
+ }
1149
+ return String(error);
1150
+ }
1151
+
1152
+ // src/core/scenarioModel.ts
1153
+ var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1141
1154
  var SC_TAG_RE = /^SC-\d{4}$/;
1155
+ var BR_TAG_RE = /^BR-\d{4}$/;
1156
+ var UI_TAG_RE = /^UI-\d{4}$/;
1157
+ var API_TAG_RE = /^API-\d{4}$/;
1158
+ var DATA_TAG_RE = /^DATA-\d{4}$/;
1159
+ function parseScenarioDocument(text, uri) {
1160
+ const { gherkinDocument, errors } = parseGherkin(text, uri);
1161
+ if (!gherkinDocument) {
1162
+ return { document: null, errors };
1163
+ }
1164
+ const feature = gherkinDocument.feature;
1165
+ if (!feature) {
1166
+ return {
1167
+ document: { uri, featureTags: [], scenarios: [] },
1168
+ errors
1169
+ };
1170
+ }
1171
+ const featureTags = collectTagNames(feature.tags);
1172
+ const scenarios = collectScenarioNodes(feature, featureTags);
1173
+ return {
1174
+ document: {
1175
+ uri,
1176
+ featureName: feature.name,
1177
+ featureTags,
1178
+ scenarios
1179
+ },
1180
+ errors
1181
+ };
1182
+ }
1183
+ function buildScenarioAtoms(document) {
1184
+ return document.scenarios.map((scenario) => {
1185
+ const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
1186
+ const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
1187
+ const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1188
+ const contractIds = /* @__PURE__ */ new Set();
1189
+ scenario.tags.forEach((tag) => {
1190
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1191
+ contractIds.add(tag);
1192
+ }
1193
+ });
1194
+ for (const step of scenario.steps) {
1195
+ for (const text of collectStepTexts(step)) {
1196
+ extractIds(text, "UI").forEach((id) => contractIds.add(id));
1197
+ extractIds(text, "API").forEach((id) => contractIds.add(id));
1198
+ extractIds(text, "DATA").forEach((id) => contractIds.add(id));
1199
+ }
1200
+ }
1201
+ const atom = {
1202
+ uri: document.uri,
1203
+ featureName: document.featureName ?? "",
1204
+ scenarioName: scenario.name,
1205
+ kind: scenario.kind,
1206
+ brIds,
1207
+ contractIds: Array.from(contractIds).sort()
1208
+ };
1209
+ if (scenario.line !== void 0) {
1210
+ atom.line = scenario.line;
1211
+ }
1212
+ if (specIds.length === 1) {
1213
+ const specId = specIds[0];
1214
+ if (specId) {
1215
+ atom.specId = specId;
1216
+ }
1217
+ }
1218
+ if (scIds.length === 1) {
1219
+ const scId = scIds[0];
1220
+ if (scId) {
1221
+ atom.scId = scId;
1222
+ }
1223
+ }
1224
+ return atom;
1225
+ });
1226
+ }
1227
+ function collectScenarioNodes(feature, featureTags) {
1228
+ const scenarios = [];
1229
+ for (const child of feature.children) {
1230
+ if (child.scenario) {
1231
+ scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
1232
+ }
1233
+ if (child.rule) {
1234
+ const ruleTags = collectTagNames(child.rule.tags);
1235
+ for (const ruleChild of child.rule.children) {
1236
+ if (ruleChild.scenario) {
1237
+ scenarios.push(
1238
+ buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
1239
+ );
1240
+ }
1241
+ }
1242
+ }
1243
+ }
1244
+ return scenarios;
1245
+ }
1246
+ function buildScenarioNode(scenario, featureTags, ruleTags) {
1247
+ const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
1248
+ const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
1249
+ return {
1250
+ name: scenario.name,
1251
+ kind,
1252
+ line: scenario.location?.line,
1253
+ tags,
1254
+ steps: scenario.steps
1255
+ };
1256
+ }
1257
+ function collectTagNames(tags) {
1258
+ return tags.map((tag) => tag.name.replace(/^@/, ""));
1259
+ }
1260
+ function collectStepTexts(step) {
1261
+ const texts = [];
1262
+ if (step.text) {
1263
+ texts.push(step.text);
1264
+ }
1265
+ if (step.docString?.content) {
1266
+ texts.push(step.docString.content);
1267
+ }
1268
+ if (step.dataTable?.rows) {
1269
+ for (const row of step.dataTable.rows) {
1270
+ for (const cell of row.cells) {
1271
+ texts.push(cell.value);
1272
+ }
1273
+ }
1274
+ }
1275
+ return texts;
1276
+ }
1277
+ function unique2(values) {
1278
+ return Array.from(new Set(values));
1279
+ }
1280
+
1281
+ // src/core/validators/ids.ts
1282
+ var SC_TAG_RE2 = /^SC-\d{4}$/;
1142
1283
  async function validateDefinedIds(root, config) {
1143
1284
  const issues = [];
1144
- const specRoot = resolvePath(root, config, "specDir");
1145
- const scenarioRoot = resolvePath(root, config, "scenariosDir");
1146
- const specFiles = await collectSpecFiles(specRoot);
1147
- const scenarioFiles = await collectFiles(scenarioRoot, {
1148
- extensions: [".feature"]
1149
- });
1285
+ const specsRoot = resolvePath(root, config, "specsDir");
1286
+ const specFiles = await collectSpecFiles(specsRoot);
1287
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1150
1288
  const defined = /* @__PURE__ */ new Map();
1151
1289
  await collectSpecDefinitionIds(specFiles, defined);
1152
1290
  await collectScenarioDefinitionIds(scenarioFiles, defined);
@@ -1186,10 +1324,13 @@ async function collectSpecDefinitionIds(files, out) {
1186
1324
  async function collectScenarioDefinitionIds(files, out) {
1187
1325
  for (const file of files) {
1188
1326
  const text = await readFile6(file, "utf-8");
1189
- const parsed = parseGherkinFeature(text, file);
1190
- for (const scenario of parsed.scenarios) {
1327
+ const { document, errors } = parseScenarioDocument(text, file);
1328
+ if (!document || errors.length > 0) {
1329
+ continue;
1330
+ }
1331
+ for (const scenario of document.scenarios) {
1191
1332
  for (const tag of scenario.tags) {
1192
- if (SC_TAG_RE.test(tag)) {
1333
+ if (SC_TAG_RE2.test(tag)) {
1193
1334
  recordId(out, tag, file);
1194
1335
  }
1195
1336
  }
@@ -1203,7 +1344,7 @@ function recordId(out, id, file) {
1203
1344
  }
1204
1345
  function formatFileList(files, root) {
1205
1346
  return files.map((file) => {
1206
- const relative = path6.relative(root, file);
1347
+ const relative = path9.relative(root, file);
1207
1348
  return relative.length > 0 ? relative : file;
1208
1349
  }).join(", ");
1209
1350
  }
@@ -1230,35 +1371,51 @@ import { readFile as readFile7 } from "fs/promises";
1230
1371
  var GIVEN_PATTERN = /\bGiven\b/;
1231
1372
  var WHEN_PATTERN = /\bWhen\b/;
1232
1373
  var THEN_PATTERN = /\bThen\b/;
1233
- var SC_TAG_RE2 = /^SC-\d{4}$/;
1234
- var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1235
- var BR_TAG_RE = /^BR-\d{4}$/;
1374
+ var SC_TAG_RE3 = /^SC-\d{4}$/;
1375
+ var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1376
+ var BR_TAG_RE2 = /^BR-\d{4}$/;
1236
1377
  async function validateScenarios(root, config) {
1237
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1238
- const files = await collectFiles(scenariosRoot, {
1239
- extensions: [".feature"]
1240
- });
1241
- if (files.length === 0) {
1378
+ const specsRoot = resolvePath(root, config, "specsDir");
1379
+ const entries = await collectSpecEntries(specsRoot);
1380
+ if (entries.length === 0) {
1381
+ const expected = "spec-0001/scenario.md";
1382
+ const legacy = "spec-001/scenario.md";
1242
1383
  return [
1243
1384
  issue4(
1244
1385
  "QFAI-SC-000",
1245
- "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1386
+ `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)`,
1246
1387
  "info",
1247
- scenariosRoot,
1388
+ specsRoot,
1248
1389
  "scenario.files"
1249
1390
  )
1250
1391
  ];
1251
1392
  }
1252
1393
  const issues = [];
1253
- for (const file of files) {
1254
- const text = await readFile7(file, "utf-8");
1255
- issues.push(...validateScenarioContent(text, file));
1394
+ for (const entry of entries) {
1395
+ let text;
1396
+ try {
1397
+ text = await readFile7(entry.scenarioPath, "utf-8");
1398
+ } catch (error) {
1399
+ if (isMissingFileError3(error)) {
1400
+ issues.push(
1401
+ issue4(
1402
+ "QFAI-SC-001",
1403
+ "scenario.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1404
+ "error",
1405
+ entry.scenarioPath,
1406
+ "scenario.exists"
1407
+ )
1408
+ );
1409
+ continue;
1410
+ }
1411
+ throw error;
1412
+ }
1413
+ issues.push(...validateScenarioContent(text, entry.scenarioPath));
1256
1414
  }
1257
1415
  return issues;
1258
1416
  }
1259
1417
  function validateScenarioContent(text, file) {
1260
1418
  const issues = [];
1261
- const parsed = parseGherkinFeature(text, file);
1262
1419
  const invalidIds = extractInvalidIds(text, [
1263
1420
  "SPEC",
1264
1421
  "BR",
@@ -1280,9 +1437,47 @@ function validateScenarioContent(text, file) {
1280
1437
  )
1281
1438
  );
1282
1439
  }
1440
+ const { document, errors } = parseScenarioDocument(text, file);
1441
+ if (!document || errors.length > 0) {
1442
+ issues.push(
1443
+ issue4(
1444
+ "QFAI-SC-010",
1445
+ `Gherkin \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errors.join(", ") || "unknown"}`,
1446
+ "error",
1447
+ file,
1448
+ "scenario.parse"
1449
+ )
1450
+ );
1451
+ return issues;
1452
+ }
1453
+ const featureSpecTags = document.featureTags.filter(
1454
+ (tag) => SPEC_TAG_RE2.test(tag)
1455
+ );
1456
+ if (featureSpecTags.length === 0) {
1457
+ issues.push(
1458
+ issue4(
1459
+ "QFAI-SC-009",
1460
+ "Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1461
+ "error",
1462
+ file,
1463
+ "scenario.featureSpec"
1464
+ )
1465
+ );
1466
+ } else if (featureSpecTags.length > 1) {
1467
+ issues.push(
1468
+ issue4(
1469
+ "QFAI-SC-009",
1470
+ `Feature \u306E SPEC \u30BF\u30B0\u304C\u8907\u6570\u3042\u308A\u307E\u3059: ${featureSpecTags.join(", ")}`,
1471
+ "error",
1472
+ file,
1473
+ "scenario.featureSpec",
1474
+ featureSpecTags
1475
+ )
1476
+ );
1477
+ }
1283
1478
  const missingStructure = [];
1284
- if (!parsed.featurePresent) missingStructure.push("Feature");
1285
- if (parsed.scenarios.length === 0) missingStructure.push("Scenario");
1479
+ if (!document.featureName) missingStructure.push("Feature");
1480
+ if (document.scenarios.length === 0) missingStructure.push("Scenario");
1286
1481
  if (missingStructure.length > 0) {
1287
1482
  issues.push(
1288
1483
  issue4(
@@ -1296,7 +1491,7 @@ function validateScenarioContent(text, file) {
1296
1491
  )
1297
1492
  );
1298
1493
  }
1299
- for (const scenario of parsed.scenarios) {
1494
+ for (const scenario of document.scenarios) {
1300
1495
  if (scenario.tags.length === 0) {
1301
1496
  issues.push(
1302
1497
  issue4(
@@ -1310,13 +1505,16 @@ function validateScenarioContent(text, file) {
1310
1505
  continue;
1311
1506
  }
1312
1507
  const missingTags = [];
1313
- if (!scenario.tags.some((tag) => SC_TAG_RE2.test(tag))) {
1314
- missingTags.push("SC");
1508
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
1509
+ if (scTags.length === 0) {
1510
+ missingTags.push("SC(0\u4EF6)");
1511
+ } else if (scTags.length > 1) {
1512
+ missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1315
1513
  }
1316
- if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
1514
+ if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
1317
1515
  missingTags.push("SPEC");
1318
1516
  }
1319
- if (!scenario.tags.some((tag) => BR_TAG_RE.test(tag))) {
1517
+ if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
1320
1518
  missingTags.push("BR");
1321
1519
  }
1322
1520
  if (missingTags.length > 0) {
@@ -1331,26 +1529,29 @@ function validateScenarioContent(text, file) {
1331
1529
  );
1332
1530
  }
1333
1531
  }
1334
- const missingSteps = [];
1335
- if (!GIVEN_PATTERN.test(text)) {
1336
- missingSteps.push("Given");
1337
- }
1338
- if (!WHEN_PATTERN.test(text)) {
1339
- missingSteps.push("When");
1340
- }
1341
- if (!THEN_PATTERN.test(text)) {
1342
- missingSteps.push("Then");
1343
- }
1344
- if (missingSteps.length > 0) {
1345
- issues.push(
1346
- issue4(
1347
- "QFAI-SC-005",
1348
- `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
1349
- "warning",
1350
- file,
1351
- "scenario.steps"
1352
- )
1353
- );
1532
+ for (const scenario of document.scenarios) {
1533
+ const missingSteps = [];
1534
+ const keywords = scenario.steps.map((step) => step.keyword.trim());
1535
+ if (!keywords.some((keyword) => GIVEN_PATTERN.test(keyword))) {
1536
+ missingSteps.push("Given");
1537
+ }
1538
+ if (!keywords.some((keyword) => WHEN_PATTERN.test(keyword))) {
1539
+ missingSteps.push("When");
1540
+ }
1541
+ if (!keywords.some((keyword) => THEN_PATTERN.test(keyword))) {
1542
+ missingSteps.push("Then");
1543
+ }
1544
+ if (missingSteps.length > 0) {
1545
+ issues.push(
1546
+ issue4(
1547
+ "QFAI-SC-005",
1548
+ `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")} (${scenario.name})`,
1549
+ "warning",
1550
+ file,
1551
+ "scenario.steps"
1552
+ )
1553
+ );
1554
+ }
1354
1555
  }
1355
1556
  return issues;
1356
1557
  }
@@ -1371,18 +1572,25 @@ function issue4(code, message, severity, file, rule, refs) {
1371
1572
  }
1372
1573
  return issue7;
1373
1574
  }
1575
+ function isMissingFileError3(error) {
1576
+ if (!error || typeof error !== "object") {
1577
+ return false;
1578
+ }
1579
+ return error.code === "ENOENT";
1580
+ }
1374
1581
 
1375
1582
  // src/core/validators/spec.ts
1376
1583
  import { readFile as readFile8 } from "fs/promises";
1377
1584
  async function validateSpecs(root, config) {
1378
- const specsRoot = resolvePath(root, config, "specDir");
1379
- const files = await collectSpecFiles(specsRoot);
1380
- if (files.length === 0) {
1381
- const expected = "spec-0001-<slug>.md";
1585
+ const specsRoot = resolvePath(root, config, "specsDir");
1586
+ const entries = await collectSpecEntries(specsRoot);
1587
+ if (entries.length === 0) {
1588
+ const expected = "spec-0001/spec.md";
1589
+ const legacy = "spec-001/spec.md";
1382
1590
  return [
1383
1591
  issue5(
1384
1592
  "QFAI-SPEC-000",
1385
- `Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}`,
1593
+ `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)`,
1386
1594
  "info",
1387
1595
  specsRoot,
1388
1596
  "spec.files"
@@ -1390,12 +1598,29 @@ async function validateSpecs(root, config) {
1390
1598
  ];
1391
1599
  }
1392
1600
  const issues = [];
1393
- for (const file of files) {
1394
- const text = await readFile8(file, "utf-8");
1601
+ for (const entry of entries) {
1602
+ let text;
1603
+ try {
1604
+ text = await readFile8(entry.specPath, "utf-8");
1605
+ } catch (error) {
1606
+ if (isMissingFileError4(error)) {
1607
+ issues.push(
1608
+ issue5(
1609
+ "QFAI-SPEC-005",
1610
+ "spec.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1611
+ "error",
1612
+ entry.specPath,
1613
+ "spec.exists"
1614
+ )
1615
+ );
1616
+ continue;
1617
+ }
1618
+ throw error;
1619
+ }
1395
1620
  issues.push(
1396
1621
  ...validateSpecContent(
1397
1622
  text,
1398
- file,
1623
+ entry.specPath,
1399
1624
  config.validation.require.specSections
1400
1625
  )
1401
1626
  );
@@ -1517,29 +1742,25 @@ function issue5(code, message, severity, file, rule, refs) {
1517
1742
  }
1518
1743
  return issue7;
1519
1744
  }
1745
+ function isMissingFileError4(error) {
1746
+ if (!error || typeof error !== "object") {
1747
+ return false;
1748
+ }
1749
+ return error.code === "ENOENT";
1750
+ }
1520
1751
 
1521
1752
  // src/core/validators/traceability.ts
1522
1753
  import { readFile as readFile9 } from "fs/promises";
1523
- var SC_TAG_RE3 = /^SC-\d{4}$/;
1524
- var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1525
- var BR_TAG_RE2 = /^BR-\d{4}$/;
1526
- var UI_TAG_RE = /^UI-\d{4}$/;
1527
- var API_TAG_RE = /^API-\d{4}$/;
1528
- var DATA_TAG_RE = /^DATA-\d{4}$/;
1754
+ var SC_TAG_RE4 = /^SC-\d{4}$/;
1755
+ var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
1756
+ var BR_TAG_RE3 = /^BR-\d{4}$/;
1529
1757
  async function validateTraceability(root, config) {
1530
1758
  const issues = [];
1531
- const specsRoot = resolvePath(root, config, "specDir");
1532
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1533
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1759
+ const specsRoot = resolvePath(root, config, "specsDir");
1534
1760
  const srcRoot = resolvePath(root, config, "srcDir");
1535
1761
  const testsRoot = resolvePath(root, config, "testsDir");
1536
1762
  const specFiles = await collectSpecFiles(specsRoot);
1537
- const decisionFiles = await collectFiles(decisionsRoot, {
1538
- extensions: [".md"]
1539
- });
1540
- const scenarioFiles = await collectFiles(scenariosRoot, {
1541
- extensions: [".feature"]
1542
- });
1763
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1543
1764
  const upstreamIds = /* @__PURE__ */ new Set();
1544
1765
  const specIds = /* @__PURE__ */ new Set();
1545
1766
  const brIdsInSpecs = /* @__PURE__ */ new Set();
@@ -1587,111 +1808,100 @@ async function validateTraceability(root, config) {
1587
1808
  specToBrIds.set(parsed.specId, current);
1588
1809
  }
1589
1810
  }
1590
- for (const file of decisionFiles) {
1591
- const text = await readFile9(file, "utf-8");
1592
- extractAllIds(text).forEach((id) => upstreamIds.add(id));
1593
- }
1594
1811
  for (const file of scenarioFiles) {
1595
1812
  const text = await readFile9(file, "utf-8");
1596
1813
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1597
- const parsed = parseGherkinFeature(text, file);
1598
- const specIdsInScenario = /* @__PURE__ */ new Set();
1599
- const brIds = /* @__PURE__ */ new Set();
1600
- const scIds = /* @__PURE__ */ new Set();
1601
- const scenarioIds = /* @__PURE__ */ new Set();
1602
- for (const scenario of parsed.scenarios) {
1603
- for (const tag of scenario.tags) {
1604
- if (SPEC_TAG_RE2.test(tag)) {
1605
- specIdsInScenario.add(tag);
1606
- }
1607
- if (BR_TAG_RE2.test(tag)) {
1608
- brIds.add(tag);
1609
- }
1610
- if (SC_TAG_RE3.test(tag)) {
1611
- scIds.add(tag);
1612
- }
1613
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1614
- scenarioIds.add(tag);
1615
- }
1616
- }
1617
- }
1618
- const specIdsList = Array.from(specIdsInScenario);
1619
- const brIdsList = Array.from(brIds);
1620
- const scIdsList = Array.from(scIds);
1621
- const scenarioIdsList = Array.from(scenarioIds);
1622
- brIdsList.forEach((id) => brIdsInScenarios.add(id));
1623
- scIdsList.forEach((id) => scIdsInScenarios.add(id));
1624
- scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
1625
- if (scenarioIdsList.length > 0) {
1626
- scIdsList.forEach((id) => scWithContracts.add(id));
1627
- }
1628
- const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
1629
- if (unknownSpecIds.length > 0) {
1630
- issues.push(
1631
- issue6(
1632
- "QFAI-TRACE-005",
1633
- `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
1634
- "error",
1635
- file,
1636
- "traceability.scenarioSpecExists",
1637
- unknownSpecIds
1638
- )
1639
- );
1640
- }
1641
- const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
1642
- if (unknownBrIds.length > 0) {
1643
- issues.push(
1644
- issue6(
1645
- "QFAI-TRACE-006",
1646
- `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
1647
- "error",
1648
- file,
1649
- "traceability.scenarioBrExists",
1650
- unknownBrIds
1651
- )
1652
- );
1653
- }
1654
- const unknownContractIds = scenarioIdsList.filter(
1655
- (id) => !contractIds.has(id)
1656
- );
1657
- if (unknownContractIds.length > 0) {
1658
- issues.push(
1659
- issue6(
1660
- "QFAI-TRACE-008",
1661
- `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1662
- ", "
1663
- )}`,
1664
- config.validation.traceability.unknownContractIdSeverity,
1665
- file,
1666
- "traceability.scenarioContractExists",
1667
- unknownContractIds
1668
- )
1669
- );
1814
+ const { document, errors } = parseScenarioDocument(text, file);
1815
+ if (!document || errors.length > 0) {
1816
+ continue;
1670
1817
  }
1671
- if (specIdsList.length > 0) {
1672
- const allowedBrIds = /* @__PURE__ */ new Set();
1673
- for (const specId of specIdsList) {
1674
- const brIdsForSpec = specToBrIds.get(specId);
1675
- if (!brIdsForSpec) {
1676
- continue;
1677
- }
1678
- brIdsForSpec.forEach((id) => allowedBrIds.add(id));
1818
+ const atoms = buildScenarioAtoms(document);
1819
+ for (const [index, scenario] of document.scenarios.entries()) {
1820
+ const atom = atoms[index];
1821
+ if (!atom) {
1822
+ continue;
1823
+ }
1824
+ const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
1825
+ const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
1826
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
1827
+ brTags.forEach((id) => brIdsInScenarios.add(id));
1828
+ scTags.forEach((id) => scIdsInScenarios.add(id));
1829
+ atom.contractIds.forEach((id) => scenarioContractIds.add(id));
1830
+ if (atom.contractIds.length > 0) {
1831
+ scTags.forEach((id) => scWithContracts.add(id));
1832
+ }
1833
+ const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
1834
+ if (unknownSpecIds.length > 0) {
1835
+ issues.push(
1836
+ issue6(
1837
+ "QFAI-TRACE-005",
1838
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(
1839
+ ", "
1840
+ )} (${scenario.name})`,
1841
+ "error",
1842
+ file,
1843
+ "traceability.scenarioSpecExists",
1844
+ unknownSpecIds
1845
+ )
1846
+ );
1679
1847
  }
1680
- const invalidBrIds = brIdsList.filter((id) => !allowedBrIds.has(id));
1681
- if (invalidBrIds.length > 0) {
1848
+ const unknownBrIds = brTags.filter((id) => !brIdsInSpecs.has(id));
1849
+ if (unknownBrIds.length > 0) {
1682
1850
  issues.push(
1683
1851
  issue6(
1684
- "QFAI-TRACE-007",
1685
- `Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
1852
+ "QFAI-TRACE-006",
1853
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(
1686
1854
  ", "
1687
- )} (SPEC: ${specIdsList.join(", ")})`,
1855
+ )} (${scenario.name})`,
1688
1856
  "error",
1689
1857
  file,
1690
- "traceability.scenarioBrUnderSpec",
1691
- invalidBrIds
1858
+ "traceability.scenarioBrExists",
1859
+ unknownBrIds
1692
1860
  )
1693
1861
  );
1694
1862
  }
1863
+ const unknownContractIds = atom.contractIds.filter(
1864
+ (id) => !contractIds.has(id)
1865
+ );
1866
+ if (unknownContractIds.length > 0) {
1867
+ issues.push(
1868
+ issue6(
1869
+ "QFAI-TRACE-008",
1870
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1871
+ ", "
1872
+ )} (${scenario.name})`,
1873
+ config.validation.traceability.unknownContractIdSeverity,
1874
+ file,
1875
+ "traceability.scenarioContractExists",
1876
+ unknownContractIds
1877
+ )
1878
+ );
1879
+ }
1880
+ if (specTags.length > 0 && brTags.length > 0) {
1881
+ const allowedBrIds = /* @__PURE__ */ new Set();
1882
+ for (const specId of specTags) {
1883
+ const brIdsForSpec = specToBrIds.get(specId);
1884
+ if (!brIdsForSpec) {
1885
+ continue;
1886
+ }
1887
+ brIdsForSpec.forEach((id) => allowedBrIds.add(id));
1888
+ }
1889
+ const invalidBrIds = brTags.filter((id) => !allowedBrIds.has(id));
1890
+ if (invalidBrIds.length > 0) {
1891
+ issues.push(
1892
+ issue6(
1893
+ "QFAI-TRACE-007",
1894
+ `Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
1895
+ ", "
1896
+ )} (SPEC: ${specTags.join(", ")}) (${scenario.name})`,
1897
+ "error",
1898
+ file,
1899
+ "traceability.scenarioBrUnderSpec",
1900
+ invalidBrIds
1901
+ )
1902
+ );
1903
+ }
1904
+ }
1695
1905
  }
1696
1906
  }
1697
1907
  if (upstreamIds.size === 0) {
@@ -1734,7 +1944,7 @@ async function validateTraceability(root, config) {
1734
1944
  ", "
1735
1945
  )}`,
1736
1946
  "error",
1737
- scenariosRoot,
1947
+ specsRoot,
1738
1948
  "traceability.scMustTouchContracts",
1739
1949
  scWithoutContracts
1740
1950
  )
@@ -1752,7 +1962,7 @@ async function validateTraceability(root, config) {
1752
1962
  "QFAI_CONTRACT_ORPHAN",
1753
1963
  `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
1754
1964
  "error",
1755
- scenariosRoot,
1965
+ specsRoot,
1756
1966
  "traceability.allowOrphanContracts",
1757
1967
  orphanContracts
1758
1968
  )
@@ -1837,8 +2047,8 @@ async function validateProject(root, configResult) {
1837
2047
  const issues = [
1838
2048
  ...configIssues,
1839
2049
  ...await validateSpecs(root, config),
2050
+ ...await validateDeltas(root, config),
1840
2051
  ...await validateScenarios(root, config),
1841
- ...await validateDecisions(root, config),
1842
2052
  ...await validateContracts(root, config),
1843
2053
  ...await validateDefinedIds(root, config),
1844
2054
  ...await validateTraceability(root, config)
@@ -1867,21 +2077,15 @@ async function createReportData(root, validation, configResult) {
1867
2077
  const resolved = configResult ?? await loadConfig(root);
1868
2078
  const config = resolved.config;
1869
2079
  const configPath = resolved.configPath;
1870
- const specRoot = resolvePath(root, config, "specDir");
1871
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1872
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1873
- const apiRoot = resolvePath(root, config, "apiContractsDir");
1874
- const uiRoot = resolvePath(root, config, "uiContractsDir");
1875
- const dbRoot = resolvePath(root, config, "dataContractsDir");
2080
+ const specsRoot = resolvePath(root, config, "specsDir");
2081
+ const contractsRoot = resolvePath(root, config, "contractsDir");
2082
+ const apiRoot = path10.join(contractsRoot, "api");
2083
+ const uiRoot = path10.join(contractsRoot, "ui");
2084
+ const dbRoot = path10.join(contractsRoot, "db");
1876
2085
  const srcRoot = resolvePath(root, config, "srcDir");
1877
2086
  const testsRoot = resolvePath(root, config, "testsDir");
1878
- const specFiles = await collectSpecFiles(specRoot);
1879
- const scenarioFiles = await collectFiles(scenariosRoot, {
1880
- extensions: [".feature"]
1881
- });
1882
- const decisionFiles = await collectFiles(decisionsRoot, {
1883
- extensions: [".md"]
1884
- });
2087
+ const specFiles = await collectSpecFiles(specsRoot);
2088
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1885
2089
  const {
1886
2090
  api: apiFiles,
1887
2091
  ui: uiFiles,
@@ -1890,7 +2094,6 @@ async function createReportData(root, validation, configResult) {
1890
2094
  const idsByPrefix = await collectIds([
1891
2095
  ...specFiles,
1892
2096
  ...scenarioFiles,
1893
- ...decisionFiles,
1894
2097
  ...apiFiles,
1895
2098
  ...uiFiles,
1896
2099
  ...dbFiles
@@ -1915,7 +2118,6 @@ async function createReportData(root, validation, configResult) {
1915
2118
  summary: {
1916
2119
  specs: specFiles.length,
1917
2120
  scenarios: scenarioFiles.length,
1918
- decisions: decisionFiles.length,
1919
2121
  contracts: {
1920
2122
  api: apiFiles.length,
1921
2123
  ui: uiFiles.length,
@@ -1949,7 +2151,6 @@ function formatReportMarkdown(data) {
1949
2151
  lines.push("## \u6982\u8981");
1950
2152
  lines.push(`- specs: ${data.summary.specs}`);
1951
2153
  lines.push(`- scenarios: ${data.summary.scenarios}`);
1952
- lines.push(`- decisions: ${data.summary.decisions}`);
1953
2154
  lines.push(
1954
2155
  `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
1955
2156
  );
@@ -2120,8 +2321,8 @@ export {
2120
2321
  resolvePath,
2121
2322
  resolveToolVersion,
2122
2323
  validateContracts,
2123
- validateDecisions,
2124
2324
  validateDefinedIds,
2325
+ validateDeltas,
2125
2326
  validateProject,
2126
2327
  validateScenarioContent,
2127
2328
  validateScenarios,