qfai 0.2.9 → 0.3.0

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
@@ -423,7 +423,7 @@ function isValidId(value, prefix) {
423
423
  }
424
424
 
425
425
  // src/core/report.ts
426
- import { readFile as readFile9 } from "fs/promises";
426
+ import { readFile as readFile10 } from "fs/promises";
427
427
 
428
428
  // src/core/discovery.ts
429
429
  import path3 from "path";
@@ -519,8 +519,8 @@ import { readFile as readFile2 } from "fs/promises";
519
519
  import path4 from "path";
520
520
  import { fileURLToPath } from "url";
521
521
  async function resolveToolVersion() {
522
- if ("0.2.9".length > 0) {
523
- return "0.2.9";
522
+ if ("0.3.0".length > 0) {
523
+ return "0.3.0";
524
524
  }
525
525
  try {
526
526
  const packagePath = resolvePackageJsonPath();
@@ -823,29 +823,113 @@ function formatError2(error) {
823
823
  return String(error);
824
824
  }
825
825
  function issue(code, message, severity, file, rule, refs) {
826
- const issue6 = {
826
+ const issue7 = {
827
+ code,
828
+ severity,
829
+ message
830
+ };
831
+ if (file) {
832
+ issue7.file = file;
833
+ }
834
+ if (rule) {
835
+ issue7.rule = rule;
836
+ }
837
+ if (refs && refs.length > 0) {
838
+ issue7.refs = refs;
839
+ }
840
+ return issue7;
841
+ }
842
+
843
+ // src/core/validators/decisions.ts
844
+ 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) {
886
+ return [];
887
+ }
888
+ 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) {
896
+ issues.push(
897
+ 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(", ")}`,
900
+ "error",
901
+ file,
902
+ "adr.requiredFields"
903
+ )
904
+ );
905
+ }
906
+ }
907
+ return issues;
908
+ }
909
+ function issue2(code, message, severity, file, rule, refs) {
910
+ const issue7 = {
827
911
  code,
828
912
  severity,
829
913
  message
830
914
  };
831
915
  if (file) {
832
- issue6.file = file;
916
+ issue7.file = file;
833
917
  }
834
918
  if (rule) {
835
- issue6.rule = rule;
919
+ issue7.rule = rule;
836
920
  }
837
921
  if (refs && refs.length > 0) {
838
- issue6.refs = refs;
922
+ issue7.refs = refs;
839
923
  }
840
- return issue6;
924
+ return issue7;
841
925
  }
842
926
 
843
927
  // src/core/validators/ids.ts
844
- import { readFile as readFile5 } from "fs/promises";
928
+ import { readFile as readFile6 } from "fs/promises";
845
929
  import path6 from "path";
846
930
 
847
931
  // src/core/contractIndex.ts
848
- import { readFile as readFile4 } from "fs/promises";
932
+ import { readFile as readFile5 } from "fs/promises";
849
933
  async function buildContractIndex(root, config) {
850
934
  const uiRoot = resolvePath(root, config, "uiContractsDir");
851
935
  const apiRoot = resolvePath(root, config, "apiContractsDir");
@@ -868,7 +952,7 @@ async function buildContractIndex(root, config) {
868
952
  }
869
953
  async function indexUiContracts(files, index) {
870
954
  for (const file of files) {
871
- const text = await readFile4(file, "utf-8");
955
+ const text = await readFile5(file, "utf-8");
872
956
  try {
873
957
  const doc = parseStructuredContract(file, text);
874
958
  extractUiContractIds(doc).forEach((id) => record(index, id, file));
@@ -880,7 +964,7 @@ async function indexUiContracts(files, index) {
880
964
  }
881
965
  async function indexApiContracts(files, index) {
882
966
  for (const file of files) {
883
- const text = await readFile4(file, "utf-8");
967
+ const text = await readFile5(file, "utf-8");
884
968
  try {
885
969
  const doc = parseStructuredContract(file, text);
886
970
  extractApiContractIds(doc).forEach((id) => record(index, id, file));
@@ -892,7 +976,7 @@ async function indexApiContracts(files, index) {
892
976
  }
893
977
  async function indexDataContracts(files, index) {
894
978
  for (const file of files) {
895
- const text = await readFile4(file, "utf-8");
979
+ const text = await readFile5(file, "utf-8");
896
980
  extractIds(text, "DATA").forEach((id) => record(index, id, file));
897
981
  }
898
982
  }
@@ -903,7 +987,158 @@ function record(index, id, file) {
903
987
  index.idToFiles.set(id, current);
904
988
  }
905
989
 
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
+ // src/core/parse/markdown.ts
1023
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1024
+ function parseHeadings(md) {
1025
+ const lines = md.split(/\r?\n/);
1026
+ const headings = [];
1027
+ for (let i = 0; i < lines.length; i++) {
1028
+ const line = lines[i] ?? "";
1029
+ const match = line.match(HEADING_RE);
1030
+ if (!match) continue;
1031
+ const levelToken = match[1];
1032
+ const title = match[2];
1033
+ if (!levelToken || !title) continue;
1034
+ headings.push({
1035
+ level: levelToken.length,
1036
+ title: title.trim(),
1037
+ line: i + 1
1038
+ });
1039
+ }
1040
+ return headings;
1041
+ }
1042
+ function extractH2Sections(md) {
1043
+ const lines = md.split(/\r?\n/);
1044
+ const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1045
+ const sections = /* @__PURE__ */ new Map();
1046
+ for (let i = 0; i < headings.length; i++) {
1047
+ const current = headings[i];
1048
+ if (!current) continue;
1049
+ const next = headings[i + 1];
1050
+ const startLine = current.line + 1;
1051
+ const endLine = (next?.line ?? lines.length + 1) - 1;
1052
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1053
+ sections.set(current.title.trim(), {
1054
+ title: current.title.trim(),
1055
+ startLine,
1056
+ endLine,
1057
+ body
1058
+ });
1059
+ }
1060
+ return sections;
1061
+ }
1062
+
1063
+ // src/core/parse/spec.ts
1064
+ 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.*)$/;
1068
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1069
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1070
+ function parseSpec(md, file) {
1071
+ const headings = parseHeadings(md);
1072
+ const h1 = headings.find((heading) => heading.level === 1);
1073
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1074
+ const sections = extractH2Sections(md);
1075
+ const sectionNames = new Set(Array.from(sections.keys()));
1076
+ const brSection = sections.get(BR_SECTION_TITLE);
1077
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1078
+ const startLine = brSection?.startLine ?? 1;
1079
+ const brs = [];
1080
+ const brsWithoutPriority = [];
1081
+ const brsWithInvalidPriority = [];
1082
+ for (let i = 0; i < brLines.length; i++) {
1083
+ const lineText = brLines[i] ?? "";
1084
+ const lineNumber = startLine + i;
1085
+ const validMatch = lineText.match(BR_LINE_RE);
1086
+ if (validMatch) {
1087
+ const id = validMatch[1];
1088
+ const priority = validMatch[2];
1089
+ const text = validMatch[3];
1090
+ if (!id || !priority || !text) continue;
1091
+ brs.push({
1092
+ id,
1093
+ priority,
1094
+ text: text.trim(),
1095
+ line: lineNumber
1096
+ });
1097
+ continue;
1098
+ }
1099
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1100
+ if (anyPriorityMatch) {
1101
+ const id = anyPriorityMatch[1];
1102
+ const priority = anyPriorityMatch[2];
1103
+ const text = anyPriorityMatch[3];
1104
+ if (!id || !priority || !text) continue;
1105
+ if (!VALID_PRIORITIES.has(priority)) {
1106
+ brsWithInvalidPriority.push({
1107
+ id,
1108
+ priority,
1109
+ text: text.trim(),
1110
+ line: lineNumber
1111
+ });
1112
+ }
1113
+ continue;
1114
+ }
1115
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1116
+ if (noPriorityMatch) {
1117
+ const id = noPriorityMatch[1];
1118
+ const text = noPriorityMatch[2];
1119
+ if (!id || !text) continue;
1120
+ brsWithoutPriority.push({
1121
+ id,
1122
+ text: text.trim(),
1123
+ line: lineNumber
1124
+ });
1125
+ }
1126
+ }
1127
+ const parsed = {
1128
+ file,
1129
+ sections: sectionNames,
1130
+ brs,
1131
+ brsWithoutPriority,
1132
+ brsWithInvalidPriority
1133
+ };
1134
+ if (specId) {
1135
+ parsed.specId = specId;
1136
+ }
1137
+ return parsed;
1138
+ }
1139
+
906
1140
  // src/core/validators/ids.ts
1141
+ var SC_TAG_RE = /^SC-\d{4}$/;
907
1142
  async function validateDefinedIds(root, config) {
908
1143
  const issues = [];
909
1144
  const specRoot = resolvePath(root, config, "specDir");
@@ -927,7 +1162,7 @@ async function validateDefinedIds(root, config) {
927
1162
  }
928
1163
  const sorted = Array.from(files).sort();
929
1164
  issues.push(
930
- issue2(
1165
+ issue3(
931
1166
  "QFAI-ID-001",
932
1167
  `ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
933
1168
  "error",
@@ -940,15 +1175,25 @@ async function validateDefinedIds(root, config) {
940
1175
  }
941
1176
  async function collectSpecDefinitionIds(files, out) {
942
1177
  for (const file of files) {
943
- const text = await readFile5(file, "utf-8");
944
- extractIds(text, "SPEC").forEach((id) => recordId(out, id, file));
945
- extractIds(text, "BR").forEach((id) => recordId(out, id, file));
1178
+ const text = await readFile6(file, "utf-8");
1179
+ const parsed = parseSpec(text, file);
1180
+ if (parsed.specId) {
1181
+ recordId(out, parsed.specId, file);
1182
+ }
1183
+ parsed.brs.forEach((br) => recordId(out, br.id, file));
946
1184
  }
947
1185
  }
948
1186
  async function collectScenarioDefinitionIds(files, out) {
949
1187
  for (const file of files) {
950
- const text = await readFile5(file, "utf-8");
951
- extractIds(text, "SC").forEach((id) => recordId(out, id, file));
1188
+ const text = await readFile6(file, "utf-8");
1189
+ const parsed = parseGherkinFeature(text, file);
1190
+ for (const scenario of parsed.scenarios) {
1191
+ for (const tag of scenario.tags) {
1192
+ if (SC_TAG_RE.test(tag)) {
1193
+ recordId(out, tag, file);
1194
+ }
1195
+ }
1196
+ }
952
1197
  }
953
1198
  }
954
1199
  function recordId(out, id, file) {
@@ -962,29 +1207,32 @@ function formatFileList(files, root) {
962
1207
  return relative.length > 0 ? relative : file;
963
1208
  }).join(", ");
964
1209
  }
965
- function issue2(code, message, severity, file, rule, refs) {
966
- const issue6 = {
1210
+ function issue3(code, message, severity, file, rule, refs) {
1211
+ const issue7 = {
967
1212
  code,
968
1213
  severity,
969
1214
  message
970
1215
  };
971
1216
  if (file) {
972
- issue6.file = file;
1217
+ issue7.file = file;
973
1218
  }
974
1219
  if (rule) {
975
- issue6.rule = rule;
1220
+ issue7.rule = rule;
976
1221
  }
977
1222
  if (refs && refs.length > 0) {
978
- issue6.refs = refs;
1223
+ issue7.refs = refs;
979
1224
  }
980
- return issue6;
1225
+ return issue7;
981
1226
  }
982
1227
 
983
1228
  // src/core/validators/scenario.ts
984
- import { readFile as readFile6 } from "fs/promises";
1229
+ import { readFile as readFile7 } from "fs/promises";
985
1230
  var GIVEN_PATTERN = /\bGiven\b/;
986
1231
  var WHEN_PATTERN = /\bWhen\b/;
987
1232
  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}$/;
988
1236
  async function validateScenarios(root, config) {
989
1237
  const scenariosRoot = resolvePath(root, config, "scenariosDir");
990
1238
  const files = await collectFiles(scenariosRoot, {
@@ -992,7 +1240,7 @@ async function validateScenarios(root, config) {
992
1240
  });
993
1241
  if (files.length === 0) {
994
1242
  return [
995
- issue3(
1243
+ issue4(
996
1244
  "QFAI-SC-000",
997
1245
  "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
998
1246
  "info",
@@ -1003,13 +1251,14 @@ async function validateScenarios(root, config) {
1003
1251
  }
1004
1252
  const issues = [];
1005
1253
  for (const file of files) {
1006
- const text = await readFile6(file, "utf-8");
1254
+ const text = await readFile7(file, "utf-8");
1007
1255
  issues.push(...validateScenarioContent(text, file));
1008
1256
  }
1009
1257
  return issues;
1010
1258
  }
1011
1259
  function validateScenarioContent(text, file) {
1012
1260
  const issues = [];
1261
+ const parsed = parseGherkinFeature(text, file);
1013
1262
  const invalidIds = extractInvalidIds(text, [
1014
1263
  "SPEC",
1015
1264
  "BR",
@@ -1021,7 +1270,7 @@ function validateScenarioContent(text, file) {
1021
1270
  ]);
1022
1271
  if (invalidIds.length > 0) {
1023
1272
  issues.push(
1024
- issue3(
1273
+ issue4(
1025
1274
  "QFAI-ID-002",
1026
1275
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
1027
1276
  "error",
@@ -1031,41 +1280,56 @@ function validateScenarioContent(text, file) {
1031
1280
  )
1032
1281
  );
1033
1282
  }
1034
- const scIds = extractIds(text, "SC");
1035
- if (scIds.length === 0) {
1036
- issues.push(
1037
- issue3(
1038
- "QFAI-SC-001",
1039
- "SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1040
- "error",
1041
- file,
1042
- "scenario.id"
1043
- )
1044
- );
1045
- }
1046
- const specIds = extractIds(text, "SPEC");
1047
- if (specIds.length === 0) {
1283
+ const missingStructure = [];
1284
+ if (!parsed.featurePresent) missingStructure.push("Feature");
1285
+ if (parsed.scenarios.length === 0) missingStructure.push("Scenario");
1286
+ if (missingStructure.length > 0) {
1048
1287
  issues.push(
1049
- issue3(
1050
- "QFAI-SC-002",
1051
- "SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
1288
+ issue4(
1289
+ "QFAI-SC-006",
1290
+ `Scenario \u30D5\u30A1\u30A4\u30EB\u306B\u5FC5\u8981\u306A\u69CB\u9020\u304C\u3042\u308A\u307E\u305B\u3093: ${missingStructure.join(
1291
+ ", "
1292
+ )}`,
1052
1293
  "error",
1053
1294
  file,
1054
- "scenario.spec"
1295
+ "scenario.structure"
1055
1296
  )
1056
1297
  );
1057
1298
  }
1058
- const brIds = extractIds(text, "BR");
1059
- if (brIds.length === 0) {
1060
- issues.push(
1061
- issue3(
1062
- "QFAI-SC-003",
1063
- "SC \u306F BR \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
1064
- "error",
1065
- file,
1066
- "scenario.br"
1067
- )
1068
- );
1299
+ for (const scenario of parsed.scenarios) {
1300
+ if (scenario.tags.length === 0) {
1301
+ issues.push(
1302
+ issue4(
1303
+ "QFAI-SC-007",
1304
+ `Scenario \u30BF\u30B0\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${scenario.name}`,
1305
+ "error",
1306
+ file,
1307
+ "scenario.tags"
1308
+ )
1309
+ );
1310
+ continue;
1311
+ }
1312
+ const missingTags = [];
1313
+ if (!scenario.tags.some((tag) => SC_TAG_RE2.test(tag))) {
1314
+ missingTags.push("SC");
1315
+ }
1316
+ if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
1317
+ missingTags.push("SPEC");
1318
+ }
1319
+ if (!scenario.tags.some((tag) => BR_TAG_RE.test(tag))) {
1320
+ missingTags.push("BR");
1321
+ }
1322
+ if (missingTags.length > 0) {
1323
+ issues.push(
1324
+ issue4(
1325
+ "QFAI-SC-008",
1326
+ `Scenario \u30BF\u30B0\u306B\u4E0D\u8DB3\u304C\u3042\u308A\u307E\u3059: ${missingTags.join(", ")} (${scenario.name})`,
1327
+ "error",
1328
+ file,
1329
+ "scenario.tagIds"
1330
+ )
1331
+ );
1332
+ }
1069
1333
  }
1070
1334
  const missingSteps = [];
1071
1335
  if (!GIVEN_PATTERN.test(text)) {
@@ -1079,7 +1343,7 @@ function validateScenarioContent(text, file) {
1079
1343
  }
1080
1344
  if (missingSteps.length > 0) {
1081
1345
  issues.push(
1082
- issue3(
1346
+ issue4(
1083
1347
  "QFAI-SC-005",
1084
1348
  `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
1085
1349
  "warning",
@@ -1090,33 +1354,33 @@ function validateScenarioContent(text, file) {
1090
1354
  }
1091
1355
  return issues;
1092
1356
  }
1093
- function issue3(code, message, severity, file, rule, refs) {
1094
- const issue6 = {
1357
+ function issue4(code, message, severity, file, rule, refs) {
1358
+ const issue7 = {
1095
1359
  code,
1096
1360
  severity,
1097
1361
  message
1098
1362
  };
1099
1363
  if (file) {
1100
- issue6.file = file;
1364
+ issue7.file = file;
1101
1365
  }
1102
1366
  if (rule) {
1103
- issue6.rule = rule;
1367
+ issue7.rule = rule;
1104
1368
  }
1105
1369
  if (refs && refs.length > 0) {
1106
- issue6.refs = refs;
1370
+ issue7.refs = refs;
1107
1371
  }
1108
- return issue6;
1372
+ return issue7;
1109
1373
  }
1110
1374
 
1111
1375
  // src/core/validators/spec.ts
1112
- import { readFile as readFile7 } from "fs/promises";
1376
+ import { readFile as readFile8 } from "fs/promises";
1113
1377
  async function validateSpecs(root, config) {
1114
1378
  const specsRoot = resolvePath(root, config, "specDir");
1115
1379
  const files = await collectSpecFiles(specsRoot);
1116
1380
  if (files.length === 0) {
1117
1381
  const expected = "spec-0001-<slug>.md";
1118
1382
  return [
1119
- issue4(
1383
+ issue5(
1120
1384
  "QFAI-SPEC-000",
1121
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}`,
1122
1386
  "info",
@@ -1127,7 +1391,7 @@ async function validateSpecs(root, config) {
1127
1391
  }
1128
1392
  const issues = [];
1129
1393
  for (const file of files) {
1130
- const text = await readFile7(file, "utf-8");
1394
+ const text = await readFile8(file, "utf-8");
1131
1395
  issues.push(
1132
1396
  ...validateSpecContent(
1133
1397
  text,
@@ -1140,6 +1404,7 @@ async function validateSpecs(root, config) {
1140
1404
  }
1141
1405
  function validateSpecContent(text, file, requiredSections) {
1142
1406
  const issues = [];
1407
+ const parsed = parseSpec(text, file);
1143
1408
  const invalidIds = extractInvalidIds(text, [
1144
1409
  "SPEC",
1145
1410
  "BR",
@@ -1151,7 +1416,7 @@ function validateSpecContent(text, file, requiredSections) {
1151
1416
  ]);
1152
1417
  if (invalidIds.length > 0) {
1153
1418
  issues.push(
1154
- issue4(
1419
+ issue5(
1155
1420
  "QFAI-ID-002",
1156
1421
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
1157
1422
  "error",
@@ -1161,10 +1426,9 @@ function validateSpecContent(text, file, requiredSections) {
1161
1426
  )
1162
1427
  );
1163
1428
  }
1164
- const specIds = extractIds(text, "SPEC");
1165
- if (specIds.length === 0) {
1429
+ if (!parsed.specId) {
1166
1430
  issues.push(
1167
- issue4(
1431
+ issue5(
1168
1432
  "QFAI-SPEC-001",
1169
1433
  "SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1170
1434
  "error",
@@ -1173,10 +1437,9 @@ function validateSpecContent(text, file, requiredSections) {
1173
1437
  )
1174
1438
  );
1175
1439
  }
1176
- const brIds = extractIds(text, "BR");
1177
- if (brIds.length === 0) {
1440
+ if (parsed.brs.length === 0) {
1178
1441
  issues.push(
1179
- issue4(
1442
+ issue5(
1180
1443
  "QFAI-SPEC-002",
1181
1444
  "BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1182
1445
  "error",
@@ -1185,10 +1448,34 @@ function validateSpecContent(text, file, requiredSections) {
1185
1448
  )
1186
1449
  );
1187
1450
  }
1451
+ for (const br of parsed.brsWithoutPriority) {
1452
+ issues.push(
1453
+ issue5(
1454
+ "QFAI-BR-001",
1455
+ `BR \u884C\u306B Priority \u304C\u3042\u308A\u307E\u305B\u3093: ${br.id}`,
1456
+ "error",
1457
+ file,
1458
+ "spec.brPriority",
1459
+ [br.id]
1460
+ )
1461
+ );
1462
+ }
1463
+ for (const br of parsed.brsWithInvalidPriority) {
1464
+ issues.push(
1465
+ issue5(
1466
+ "QFAI-BR-002",
1467
+ `BR Priority \u304C\u4E0D\u6B63\u3067\u3059: ${br.id} (${br.priority})`,
1468
+ "error",
1469
+ file,
1470
+ "spec.brPriority",
1471
+ [br.id]
1472
+ )
1473
+ );
1474
+ }
1188
1475
  const scIds = extractIds(text, "SC");
1189
1476
  if (scIds.length > 0) {
1190
1477
  issues.push(
1191
- issue4(
1478
+ issue5(
1192
1479
  "QFAI-SPEC-003",
1193
1480
  "Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
1194
1481
  "warning",
@@ -1199,9 +1486,9 @@ function validateSpecContent(text, file, requiredSections) {
1199
1486
  );
1200
1487
  }
1201
1488
  for (const section of requiredSections) {
1202
- if (!text.includes(section)) {
1489
+ if (!parsed.sections.has(section)) {
1203
1490
  issues.push(
1204
- issue4(
1491
+ issue5(
1205
1492
  "QFAI-SPEC-004",
1206
1493
  `\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
1207
1494
  "error",
@@ -1213,26 +1500,32 @@ function validateSpecContent(text, file, requiredSections) {
1213
1500
  }
1214
1501
  return issues;
1215
1502
  }
1216
- function issue4(code, message, severity, file, rule, refs) {
1217
- const issue6 = {
1503
+ function issue5(code, message, severity, file, rule, refs) {
1504
+ const issue7 = {
1218
1505
  code,
1219
1506
  severity,
1220
1507
  message
1221
1508
  };
1222
1509
  if (file) {
1223
- issue6.file = file;
1510
+ issue7.file = file;
1224
1511
  }
1225
1512
  if (rule) {
1226
- issue6.rule = rule;
1513
+ issue7.rule = rule;
1227
1514
  }
1228
1515
  if (refs && refs.length > 0) {
1229
- issue6.refs = refs;
1516
+ issue7.refs = refs;
1230
1517
  }
1231
- return issue6;
1518
+ return issue7;
1232
1519
  }
1233
1520
 
1234
1521
  // src/core/validators/traceability.ts
1235
- import { readFile as readFile8 } from "fs/promises";
1522
+ 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}$/;
1236
1529
  async function validateTraceability(root, config) {
1237
1530
  const issues = [];
1238
1531
  const specsRoot = resolvePath(root, config, "specDir");
@@ -1258,11 +1551,13 @@ async function validateTraceability(root, config) {
1258
1551
  const contractIndex = await buildContractIndex(root, config);
1259
1552
  const contractIds = contractIndex.ids;
1260
1553
  for (const file of specFiles) {
1261
- const text = await readFile8(file, "utf-8");
1554
+ const text = await readFile9(file, "utf-8");
1262
1555
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1263
- const specIdsInFile = extractIds(text, "SPEC");
1264
- specIdsInFile.forEach((id) => specIds.add(id));
1265
- const brIds = extractIds(text, "BR");
1556
+ const parsed = parseSpec(text, file);
1557
+ if (parsed.specId) {
1558
+ specIds.add(parsed.specId);
1559
+ }
1560
+ const brIds = parsed.brs.map((br) => br.id);
1266
1561
  brIds.forEach((id) => brIdsInSpecs.add(id));
1267
1562
  const referencedContractIds = /* @__PURE__ */ new Set([
1268
1563
  ...extractIds(text, "UI"),
@@ -1274,7 +1569,7 @@ async function validateTraceability(root, config) {
1274
1569
  );
1275
1570
  if (unknownContractIds.length > 0) {
1276
1571
  issues.push(
1277
- issue5(
1572
+ issue6(
1278
1573
  "QFAI-TRACE-009",
1279
1574
  `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1280
1575
  ", "
@@ -1286,37 +1581,54 @@ async function validateTraceability(root, config) {
1286
1581
  )
1287
1582
  );
1288
1583
  }
1289
- for (const specId of specIdsInFile) {
1290
- const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
1584
+ if (parsed.specId) {
1585
+ const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
1291
1586
  brIds.forEach((id) => current.add(id));
1292
- specToBrIds.set(specId, current);
1587
+ specToBrIds.set(parsed.specId, current);
1293
1588
  }
1294
1589
  }
1295
1590
  for (const file of decisionFiles) {
1296
- const text = await readFile8(file, "utf-8");
1591
+ const text = await readFile9(file, "utf-8");
1297
1592
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1298
1593
  }
1299
1594
  for (const file of scenarioFiles) {
1300
- const text = await readFile8(file, "utf-8");
1595
+ const text = await readFile9(file, "utf-8");
1301
1596
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1302
- const specIdsInScenario = extractIds(text, "SPEC");
1303
- const brIds = extractIds(text, "BR");
1304
- const scIds = extractIds(text, "SC");
1305
- const scenarioIds = [
1306
- ...extractIds(text, "UI"),
1307
- ...extractIds(text, "API"),
1308
- ...extractIds(text, "DATA")
1309
- ];
1310
- brIds.forEach((id) => brIdsInScenarios.add(id));
1311
- scIds.forEach((id) => scIdsInScenarios.add(id));
1312
- scenarioIds.forEach((id) => scenarioContractIds.add(id));
1313
- if (scenarioIds.length > 0) {
1314
- scIds.forEach((id) => scWithContracts.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));
1315
1627
  }
1316
- const unknownSpecIds = specIdsInScenario.filter((id) => !specIds.has(id));
1628
+ const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
1317
1629
  if (unknownSpecIds.length > 0) {
1318
1630
  issues.push(
1319
- issue5(
1631
+ issue6(
1320
1632
  "QFAI-TRACE-005",
1321
1633
  `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
1322
1634
  "error",
@@ -1326,10 +1638,10 @@ async function validateTraceability(root, config) {
1326
1638
  )
1327
1639
  );
1328
1640
  }
1329
- const unknownBrIds = brIds.filter((id) => !brIdsInSpecs.has(id));
1641
+ const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
1330
1642
  if (unknownBrIds.length > 0) {
1331
1643
  issues.push(
1332
- issue5(
1644
+ issue6(
1333
1645
  "QFAI-TRACE-006",
1334
1646
  `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
1335
1647
  "error",
@@ -1339,10 +1651,12 @@ async function validateTraceability(root, config) {
1339
1651
  )
1340
1652
  );
1341
1653
  }
1342
- const unknownContractIds = scenarioIds.filter((id) => !contractIds.has(id));
1654
+ const unknownContractIds = scenarioIdsList.filter(
1655
+ (id) => !contractIds.has(id)
1656
+ );
1343
1657
  if (unknownContractIds.length > 0) {
1344
1658
  issues.push(
1345
- issue5(
1659
+ issue6(
1346
1660
  "QFAI-TRACE-008",
1347
1661
  `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1348
1662
  ", "
@@ -1354,23 +1668,23 @@ async function validateTraceability(root, config) {
1354
1668
  )
1355
1669
  );
1356
1670
  }
1357
- if (specIdsInScenario.length > 0) {
1671
+ if (specIdsList.length > 0) {
1358
1672
  const allowedBrIds = /* @__PURE__ */ new Set();
1359
- for (const specId of specIdsInScenario) {
1673
+ for (const specId of specIdsList) {
1360
1674
  const brIdsForSpec = specToBrIds.get(specId);
1361
1675
  if (!brIdsForSpec) {
1362
1676
  continue;
1363
1677
  }
1364
1678
  brIdsForSpec.forEach((id) => allowedBrIds.add(id));
1365
1679
  }
1366
- const invalidBrIds = brIds.filter((id) => !allowedBrIds.has(id));
1680
+ const invalidBrIds = brIdsList.filter((id) => !allowedBrIds.has(id));
1367
1681
  if (invalidBrIds.length > 0) {
1368
1682
  issues.push(
1369
- issue5(
1683
+ issue6(
1370
1684
  "QFAI-TRACE-007",
1371
1685
  `Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
1372
1686
  ", "
1373
- )} (SPEC: ${specIdsInScenario.join(", ")})`,
1687
+ )} (SPEC: ${specIdsList.join(", ")})`,
1374
1688
  "error",
1375
1689
  file,
1376
1690
  "traceability.scenarioBrUnderSpec",
@@ -1382,7 +1696,7 @@ async function validateTraceability(root, config) {
1382
1696
  }
1383
1697
  if (upstreamIds.size === 0) {
1384
1698
  return [
1385
- issue5(
1699
+ issue6(
1386
1700
  "QFAI-TRACE-000",
1387
1701
  "\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1388
1702
  "info",
@@ -1397,7 +1711,7 @@ async function validateTraceability(root, config) {
1397
1711
  );
1398
1712
  if (orphanBrIds.length > 0) {
1399
1713
  issues.push(
1400
- issue5(
1714
+ issue6(
1401
1715
  "QFAI_TRACE_BR_ORPHAN",
1402
1716
  `BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
1403
1717
  "error",
@@ -1414,7 +1728,7 @@ async function validateTraceability(root, config) {
1414
1728
  );
1415
1729
  if (scWithoutContracts.length > 0) {
1416
1730
  issues.push(
1417
- issue5(
1731
+ issue6(
1418
1732
  "QFAI_TRACE_SC_NO_CONTRACT",
1419
1733
  `SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
1420
1734
  ", "
@@ -1434,7 +1748,7 @@ async function validateTraceability(root, config) {
1434
1748
  );
1435
1749
  if (orphanContracts.length > 0) {
1436
1750
  issues.push(
1437
- issue5(
1751
+ issue6(
1438
1752
  "QFAI_CONTRACT_ORPHAN",
1439
1753
  `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
1440
1754
  "error",
@@ -1462,7 +1776,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1462
1776
  const targetFiles = [...codeFiles, ...testFiles];
1463
1777
  if (targetFiles.length === 0) {
1464
1778
  issues.push(
1465
- issue5(
1779
+ issue6(
1466
1780
  "QFAI-TRACE-001",
1467
1781
  "\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1468
1782
  "info",
@@ -1475,7 +1789,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1475
1789
  const pattern = buildIdPattern(Array.from(upstreamIds));
1476
1790
  let found = false;
1477
1791
  for (const file of targetFiles) {
1478
- const text = await readFile8(file, "utf-8");
1792
+ const text = await readFile9(file, "utf-8");
1479
1793
  if (pattern.test(text)) {
1480
1794
  found = true;
1481
1795
  break;
@@ -1483,7 +1797,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1483
1797
  }
1484
1798
  if (!found) {
1485
1799
  issues.push(
1486
- issue5(
1800
+ issue6(
1487
1801
  "QFAI-TRACE-002",
1488
1802
  "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
1489
1803
  "warning",
@@ -1498,22 +1812,22 @@ function buildIdPattern(ids) {
1498
1812
  const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
1499
1813
  return new RegExp(`\\b(${escaped.join("|")})\\b`);
1500
1814
  }
1501
- function issue5(code, message, severity, file, rule, refs) {
1502
- const issue6 = {
1815
+ function issue6(code, message, severity, file, rule, refs) {
1816
+ const issue7 = {
1503
1817
  code,
1504
1818
  severity,
1505
1819
  message
1506
1820
  };
1507
1821
  if (file) {
1508
- issue6.file = file;
1822
+ issue7.file = file;
1509
1823
  }
1510
1824
  if (rule) {
1511
- issue6.rule = rule;
1825
+ issue7.rule = rule;
1512
1826
  }
1513
1827
  if (refs && refs.length > 0) {
1514
- issue6.refs = refs;
1828
+ issue7.refs = refs;
1515
1829
  }
1516
- return issue6;
1830
+ return issue7;
1517
1831
  }
1518
1832
 
1519
1833
  // src/core/validate.ts
@@ -1524,6 +1838,7 @@ async function validateProject(root, configResult) {
1524
1838
  ...configIssues,
1525
1839
  ...await validateSpecs(root, config),
1526
1840
  ...await validateScenarios(root, config),
1841
+ ...await validateDecisions(root, config),
1527
1842
  ...await validateContracts(root, config),
1528
1843
  ...await validateDefinedIds(root, config),
1529
1844
  ...await validateTraceability(root, config)
@@ -1538,8 +1853,8 @@ async function validateProject(root, configResult) {
1538
1853
  }
1539
1854
  function countIssues(issues) {
1540
1855
  return issues.reduce(
1541
- (acc, issue6) => {
1542
- acc[issue6.severity] += 1;
1856
+ (acc, issue7) => {
1857
+ acc[issue7.severity] += 1;
1543
1858
  return acc;
1544
1859
  },
1545
1860
  { info: 0, warning: 0, error: 0 }
@@ -1710,7 +2025,7 @@ async function collectIds(files) {
1710
2025
  DATA: /* @__PURE__ */ new Set()
1711
2026
  };
1712
2027
  for (const file of files) {
1713
- const text = await readFile9(file, "utf-8");
2028
+ const text = await readFile10(file, "utf-8");
1714
2029
  for (const prefix of ID_PREFIXES2) {
1715
2030
  const ids = extractIds(text, prefix);
1716
2031
  ids.forEach((id) => result[prefix].add(id));
@@ -1728,7 +2043,7 @@ async function collectIds(files) {
1728
2043
  async function collectUpstreamIds(files) {
1729
2044
  const ids = /* @__PURE__ */ new Set();
1730
2045
  for (const file of files) {
1731
- const text = await readFile9(file, "utf-8");
2046
+ const text = await readFile10(file, "utf-8");
1732
2047
  extractAllIds(text).forEach((id) => ids.add(id));
1733
2048
  }
1734
2049
  return ids;
@@ -1749,7 +2064,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
1749
2064
  }
1750
2065
  const pattern = buildIdPattern2(Array.from(upstreamIds));
1751
2066
  for (const file of targetFiles) {
1752
- const text = await readFile9(file, "utf-8");
2067
+ const text = await readFile10(file, "utf-8");
1753
2068
  if (pattern.test(text)) {
1754
2069
  return true;
1755
2070
  }
@@ -1771,20 +2086,20 @@ function toSortedArray(values) {
1771
2086
  }
1772
2087
  function buildHotspots(issues) {
1773
2088
  const map = /* @__PURE__ */ new Map();
1774
- for (const issue6 of issues) {
1775
- if (!issue6.file) {
2089
+ for (const issue7 of issues) {
2090
+ if (!issue7.file) {
1776
2091
  continue;
1777
2092
  }
1778
- const current = map.get(issue6.file) ?? {
1779
- file: issue6.file,
2093
+ const current = map.get(issue7.file) ?? {
2094
+ file: issue7.file,
1780
2095
  total: 0,
1781
2096
  error: 0,
1782
2097
  warning: 0,
1783
2098
  info: 0
1784
2099
  };
1785
2100
  current.total += 1;
1786
- current[issue6.severity] += 1;
1787
- map.set(issue6.file, current);
2101
+ current[issue7.severity] += 1;
2102
+ map.set(issue7.file, current);
1788
2103
  }
1789
2104
  return Array.from(map.values()).sort(
1790
2105
  (a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
@@ -1805,6 +2120,7 @@ export {
1805
2120
  resolvePath,
1806
2121
  resolveToolVersion,
1807
2122
  validateContracts,
2123
+ validateDecisions,
1808
2124
  validateDefinedIds,
1809
2125
  validateProject,
1810
2126
  validateScenarioContent,