qfai 0.2.8 → 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.
@@ -160,7 +160,7 @@ function report(copied, skipped, dryRun, label) {
160
160
  }
161
161
 
162
162
  // src/cli/commands/report.ts
163
- var import_promises12 = require("fs/promises");
163
+ var import_promises13 = require("fs/promises");
164
164
  var import_node_path10 = __toESM(require("path"), 1);
165
165
 
166
166
  // src/core/config.ts
@@ -535,7 +535,7 @@ function isRecord(value) {
535
535
  }
536
536
 
537
537
  // src/core/report.ts
538
- var import_promises11 = require("fs/promises");
538
+ var import_promises12 = require("fs/promises");
539
539
 
540
540
  // src/core/discovery.ts
541
541
  var import_node_path6 = __toESM(require("path"), 1);
@@ -684,8 +684,8 @@ var import_promises4 = require("fs/promises");
684
684
  var import_node_path7 = __toESM(require("path"), 1);
685
685
  var import_node_url2 = require("url");
686
686
  async function resolveToolVersion() {
687
- if ("0.2.9".length > 0) {
688
- return "0.2.9";
687
+ if ("0.3.0".length > 0) {
688
+ return "0.3.0";
689
689
  }
690
690
  try {
691
691
  const packagePath = resolvePackageJsonPath();
@@ -988,29 +988,113 @@ function formatError2(error2) {
988
988
  return String(error2);
989
989
  }
990
990
  function issue(code, message, severity, file, rule, refs) {
991
- const issue6 = {
991
+ const issue7 = {
992
+ code,
993
+ severity,
994
+ message
995
+ };
996
+ if (file) {
997
+ issue7.file = file;
998
+ }
999
+ if (rule) {
1000
+ issue7.rule = rule;
1001
+ }
1002
+ if (refs && refs.length > 0) {
1003
+ issue7.refs = refs;
1004
+ }
1005
+ return issue7;
1006
+ }
1007
+
1008
+ // src/core/validators/decisions.ts
1009
+ var import_promises6 = require("fs/promises");
1010
+
1011
+ // src/core/parse/adr.ts
1012
+ var ADR_ID_RE = /\bADR-\d{4}\b/;
1013
+ function extractField(md, key) {
1014
+ const pattern = new RegExp(`^\\s*-\\s*${key}:\\s*(.+)\\s*$`, "m");
1015
+ return md.match(pattern)?.[1]?.trim();
1016
+ }
1017
+ function parseAdr(md, file) {
1018
+ const adrId = md.match(ADR_ID_RE)?.[0];
1019
+ const fields = {};
1020
+ const status = extractField(md, "Status");
1021
+ const context = extractField(md, "Context");
1022
+ const decision = extractField(md, "Decision");
1023
+ const consequences = extractField(md, "Consequences");
1024
+ const related = extractField(md, "Related");
1025
+ if (status) fields.status = status;
1026
+ if (context) fields.context = context;
1027
+ if (decision) fields.decision = decision;
1028
+ if (consequences) fields.consequences = consequences;
1029
+ if (related) fields.related = related;
1030
+ const parsed = {
1031
+ file,
1032
+ fields
1033
+ };
1034
+ if (adrId) {
1035
+ parsed.adrId = adrId;
1036
+ }
1037
+ return parsed;
1038
+ }
1039
+
1040
+ // src/core/validators/decisions.ts
1041
+ var REQUIRED_FIELDS = [
1042
+ { key: "status", label: "Status" },
1043
+ { key: "context", label: "Context" },
1044
+ { key: "decision", label: "Decision" },
1045
+ { key: "consequences", label: "Consequences" }
1046
+ ];
1047
+ async function validateDecisions(root, config) {
1048
+ const decisionsRoot = resolvePath(root, config, "decisionsDir");
1049
+ const files = await collectFiles(decisionsRoot, { extensions: [".md"] });
1050
+ if (files.length === 0) {
1051
+ return [];
1052
+ }
1053
+ const issues = [];
1054
+ for (const file of files) {
1055
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
1056
+ const parsed = parseAdr(text, file);
1057
+ const missing = REQUIRED_FIELDS.filter(
1058
+ (field) => !parsed.fields[field.key]
1059
+ );
1060
+ if (missing.length > 0) {
1061
+ issues.push(
1062
+ issue2(
1063
+ "QFAI-ADR-001",
1064
+ `ADR \u5FC5\u9808\u30D5\u30A3\u30FC\u30EB\u30C9\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missing.map((field) => field.label).join(", ")}`,
1065
+ "error",
1066
+ file,
1067
+ "adr.requiredFields"
1068
+ )
1069
+ );
1070
+ }
1071
+ }
1072
+ return issues;
1073
+ }
1074
+ function issue2(code, message, severity, file, rule, refs) {
1075
+ const issue7 = {
992
1076
  code,
993
1077
  severity,
994
1078
  message
995
1079
  };
996
1080
  if (file) {
997
- issue6.file = file;
1081
+ issue7.file = file;
998
1082
  }
999
1083
  if (rule) {
1000
- issue6.rule = rule;
1084
+ issue7.rule = rule;
1001
1085
  }
1002
1086
  if (refs && refs.length > 0) {
1003
- issue6.refs = refs;
1087
+ issue7.refs = refs;
1004
1088
  }
1005
- return issue6;
1089
+ return issue7;
1006
1090
  }
1007
1091
 
1008
1092
  // src/core/validators/ids.ts
1009
- var import_promises7 = require("fs/promises");
1093
+ var import_promises8 = require("fs/promises");
1010
1094
  var import_node_path9 = __toESM(require("path"), 1);
1011
1095
 
1012
1096
  // src/core/contractIndex.ts
1013
- var import_promises6 = require("fs/promises");
1097
+ var import_promises7 = require("fs/promises");
1014
1098
  async function buildContractIndex(root, config) {
1015
1099
  const uiRoot = resolvePath(root, config, "uiContractsDir");
1016
1100
  const apiRoot = resolvePath(root, config, "apiContractsDir");
@@ -1033,7 +1117,7 @@ async function buildContractIndex(root, config) {
1033
1117
  }
1034
1118
  async function indexUiContracts(files, index) {
1035
1119
  for (const file of files) {
1036
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1120
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
1037
1121
  try {
1038
1122
  const doc = parseStructuredContract(file, text);
1039
1123
  extractUiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1045,7 +1129,7 @@ async function indexUiContracts(files, index) {
1045
1129
  }
1046
1130
  async function indexApiContracts(files, index) {
1047
1131
  for (const file of files) {
1048
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1132
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
1049
1133
  try {
1050
1134
  const doc = parseStructuredContract(file, text);
1051
1135
  extractApiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1057,7 +1141,7 @@ async function indexApiContracts(files, index) {
1057
1141
  }
1058
1142
  async function indexDataContracts(files, index) {
1059
1143
  for (const file of files) {
1060
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1144
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
1061
1145
  extractIds(text, "DATA").forEach((id) => record(index, id, file));
1062
1146
  }
1063
1147
  }
@@ -1068,7 +1152,158 @@ function record(index, id, file) {
1068
1152
  index.idToFiles.set(id, current);
1069
1153
  }
1070
1154
 
1155
+ // src/core/parse/gherkin.ts
1156
+ var FEATURE_RE = /^\s*Feature:\s+/;
1157
+ var SCENARIO_RE = /^\s*Scenario:\s*(.+)\s*$/;
1158
+ var TAG_LINE_RE = /^\s*@/;
1159
+ function parseTags(line) {
1160
+ return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
1161
+ }
1162
+ function parseGherkinFeature(text, file) {
1163
+ const lines = text.split(/\r?\n/);
1164
+ const scenarios = [];
1165
+ let featurePresent = false;
1166
+ for (let i = 0; i < lines.length; i++) {
1167
+ const line = lines[i] ?? "";
1168
+ if (FEATURE_RE.test(line)) {
1169
+ featurePresent = true;
1170
+ }
1171
+ const match = line.match(SCENARIO_RE);
1172
+ if (!match) continue;
1173
+ const scenarioName = match[1];
1174
+ if (!scenarioName) continue;
1175
+ const tags = [];
1176
+ for (let j = i - 1; j >= 0; j--) {
1177
+ const previous = lines[j] ?? "";
1178
+ if (previous.trim() === "") continue;
1179
+ if (!TAG_LINE_RE.test(previous)) break;
1180
+ tags.unshift(...parseTags(previous));
1181
+ }
1182
+ scenarios.push({ name: scenarioName, line: i + 1, tags });
1183
+ }
1184
+ return { file, featurePresent, scenarios };
1185
+ }
1186
+
1187
+ // src/core/parse/markdown.ts
1188
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1189
+ function parseHeadings(md) {
1190
+ const lines = md.split(/\r?\n/);
1191
+ const headings = [];
1192
+ for (let i = 0; i < lines.length; i++) {
1193
+ const line = lines[i] ?? "";
1194
+ const match = line.match(HEADING_RE);
1195
+ if (!match) continue;
1196
+ const levelToken = match[1];
1197
+ const title = match[2];
1198
+ if (!levelToken || !title) continue;
1199
+ headings.push({
1200
+ level: levelToken.length,
1201
+ title: title.trim(),
1202
+ line: i + 1
1203
+ });
1204
+ }
1205
+ return headings;
1206
+ }
1207
+ function extractH2Sections(md) {
1208
+ const lines = md.split(/\r?\n/);
1209
+ const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1210
+ const sections = /* @__PURE__ */ new Map();
1211
+ for (let i = 0; i < headings.length; i++) {
1212
+ const current = headings[i];
1213
+ if (!current) continue;
1214
+ const next = headings[i + 1];
1215
+ const startLine = current.line + 1;
1216
+ const endLine = (next?.line ?? lines.length + 1) - 1;
1217
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1218
+ sections.set(current.title.trim(), {
1219
+ title: current.title.trim(),
1220
+ startLine,
1221
+ endLine,
1222
+ body
1223
+ });
1224
+ }
1225
+ return sections;
1226
+ }
1227
+
1228
+ // src/core/parse/spec.ts
1229
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1230
+ var BR_LINE_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[0-3])\)\s*(.+)$/;
1231
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[^)]+)\)\s*(.+)$/;
1232
+ var BR_LINE_NO_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s+(?!\()(.*\S.*)$/;
1233
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1234
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1235
+ function parseSpec(md, file) {
1236
+ const headings = parseHeadings(md);
1237
+ const h1 = headings.find((heading) => heading.level === 1);
1238
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1239
+ const sections = extractH2Sections(md);
1240
+ const sectionNames = new Set(Array.from(sections.keys()));
1241
+ const brSection = sections.get(BR_SECTION_TITLE);
1242
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1243
+ const startLine = brSection?.startLine ?? 1;
1244
+ const brs = [];
1245
+ const brsWithoutPriority = [];
1246
+ const brsWithInvalidPriority = [];
1247
+ for (let i = 0; i < brLines.length; i++) {
1248
+ const lineText = brLines[i] ?? "";
1249
+ const lineNumber = startLine + i;
1250
+ const validMatch = lineText.match(BR_LINE_RE);
1251
+ if (validMatch) {
1252
+ const id = validMatch[1];
1253
+ const priority = validMatch[2];
1254
+ const text = validMatch[3];
1255
+ if (!id || !priority || !text) continue;
1256
+ brs.push({
1257
+ id,
1258
+ priority,
1259
+ text: text.trim(),
1260
+ line: lineNumber
1261
+ });
1262
+ continue;
1263
+ }
1264
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1265
+ if (anyPriorityMatch) {
1266
+ const id = anyPriorityMatch[1];
1267
+ const priority = anyPriorityMatch[2];
1268
+ const text = anyPriorityMatch[3];
1269
+ if (!id || !priority || !text) continue;
1270
+ if (!VALID_PRIORITIES.has(priority)) {
1271
+ brsWithInvalidPriority.push({
1272
+ id,
1273
+ priority,
1274
+ text: text.trim(),
1275
+ line: lineNumber
1276
+ });
1277
+ }
1278
+ continue;
1279
+ }
1280
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1281
+ if (noPriorityMatch) {
1282
+ const id = noPriorityMatch[1];
1283
+ const text = noPriorityMatch[2];
1284
+ if (!id || !text) continue;
1285
+ brsWithoutPriority.push({
1286
+ id,
1287
+ text: text.trim(),
1288
+ line: lineNumber
1289
+ });
1290
+ }
1291
+ }
1292
+ const parsed = {
1293
+ file,
1294
+ sections: sectionNames,
1295
+ brs,
1296
+ brsWithoutPriority,
1297
+ brsWithInvalidPriority
1298
+ };
1299
+ if (specId) {
1300
+ parsed.specId = specId;
1301
+ }
1302
+ return parsed;
1303
+ }
1304
+
1071
1305
  // src/core/validators/ids.ts
1306
+ var SC_TAG_RE = /^SC-\d{4}$/;
1072
1307
  async function validateDefinedIds(root, config) {
1073
1308
  const issues = [];
1074
1309
  const specRoot = resolvePath(root, config, "specDir");
@@ -1092,7 +1327,7 @@ async function validateDefinedIds(root, config) {
1092
1327
  }
1093
1328
  const sorted = Array.from(files).sort();
1094
1329
  issues.push(
1095
- issue2(
1330
+ issue3(
1096
1331
  "QFAI-ID-001",
1097
1332
  `ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
1098
1333
  "error",
@@ -1105,15 +1340,25 @@ async function validateDefinedIds(root, config) {
1105
1340
  }
1106
1341
  async function collectSpecDefinitionIds(files, out) {
1107
1342
  for (const file of files) {
1108
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1109
- extractIds(text, "SPEC").forEach((id) => recordId(out, id, file));
1110
- extractIds(text, "BR").forEach((id) => recordId(out, id, file));
1343
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
1344
+ const parsed = parseSpec(text, file);
1345
+ if (parsed.specId) {
1346
+ recordId(out, parsed.specId, file);
1347
+ }
1348
+ parsed.brs.forEach((br) => recordId(out, br.id, file));
1111
1349
  }
1112
1350
  }
1113
1351
  async function collectScenarioDefinitionIds(files, out) {
1114
1352
  for (const file of files) {
1115
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1116
- extractIds(text, "SC").forEach((id) => recordId(out, id, file));
1353
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
1354
+ const parsed = parseGherkinFeature(text, file);
1355
+ for (const scenario of parsed.scenarios) {
1356
+ for (const tag of scenario.tags) {
1357
+ if (SC_TAG_RE.test(tag)) {
1358
+ recordId(out, tag, file);
1359
+ }
1360
+ }
1361
+ }
1117
1362
  }
1118
1363
  }
1119
1364
  function recordId(out, id, file) {
@@ -1127,29 +1372,32 @@ function formatFileList(files, root) {
1127
1372
  return relative.length > 0 ? relative : file;
1128
1373
  }).join(", ");
1129
1374
  }
1130
- function issue2(code, message, severity, file, rule, refs) {
1131
- const issue6 = {
1375
+ function issue3(code, message, severity, file, rule, refs) {
1376
+ const issue7 = {
1132
1377
  code,
1133
1378
  severity,
1134
1379
  message
1135
1380
  };
1136
1381
  if (file) {
1137
- issue6.file = file;
1382
+ issue7.file = file;
1138
1383
  }
1139
1384
  if (rule) {
1140
- issue6.rule = rule;
1385
+ issue7.rule = rule;
1141
1386
  }
1142
1387
  if (refs && refs.length > 0) {
1143
- issue6.refs = refs;
1388
+ issue7.refs = refs;
1144
1389
  }
1145
- return issue6;
1390
+ return issue7;
1146
1391
  }
1147
1392
 
1148
1393
  // src/core/validators/scenario.ts
1149
- var import_promises8 = require("fs/promises");
1394
+ var import_promises9 = require("fs/promises");
1150
1395
  var GIVEN_PATTERN = /\bGiven\b/;
1151
1396
  var WHEN_PATTERN = /\bWhen\b/;
1152
1397
  var THEN_PATTERN = /\bThen\b/;
1398
+ var SC_TAG_RE2 = /^SC-\d{4}$/;
1399
+ var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1400
+ var BR_TAG_RE = /^BR-\d{4}$/;
1153
1401
  async function validateScenarios(root, config) {
1154
1402
  const scenariosRoot = resolvePath(root, config, "scenariosDir");
1155
1403
  const files = await collectFiles(scenariosRoot, {
@@ -1157,7 +1405,7 @@ async function validateScenarios(root, config) {
1157
1405
  });
1158
1406
  if (files.length === 0) {
1159
1407
  return [
1160
- issue3(
1408
+ issue4(
1161
1409
  "QFAI-SC-000",
1162
1410
  "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1163
1411
  "info",
@@ -1168,13 +1416,14 @@ async function validateScenarios(root, config) {
1168
1416
  }
1169
1417
  const issues = [];
1170
1418
  for (const file of files) {
1171
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1419
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1172
1420
  issues.push(...validateScenarioContent(text, file));
1173
1421
  }
1174
1422
  return issues;
1175
1423
  }
1176
1424
  function validateScenarioContent(text, file) {
1177
1425
  const issues = [];
1426
+ const parsed = parseGherkinFeature(text, file);
1178
1427
  const invalidIds = extractInvalidIds(text, [
1179
1428
  "SPEC",
1180
1429
  "BR",
@@ -1186,7 +1435,7 @@ function validateScenarioContent(text, file) {
1186
1435
  ]);
1187
1436
  if (invalidIds.length > 0) {
1188
1437
  issues.push(
1189
- issue3(
1438
+ issue4(
1190
1439
  "QFAI-ID-002",
1191
1440
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
1192
1441
  "error",
@@ -1196,41 +1445,56 @@ function validateScenarioContent(text, file) {
1196
1445
  )
1197
1446
  );
1198
1447
  }
1199
- const scIds = extractIds(text, "SC");
1200
- if (scIds.length === 0) {
1201
- issues.push(
1202
- issue3(
1203
- "QFAI-SC-001",
1204
- "SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1205
- "error",
1206
- file,
1207
- "scenario.id"
1208
- )
1209
- );
1210
- }
1211
- const specIds = extractIds(text, "SPEC");
1212
- if (specIds.length === 0) {
1448
+ const missingStructure = [];
1449
+ if (!parsed.featurePresent) missingStructure.push("Feature");
1450
+ if (parsed.scenarios.length === 0) missingStructure.push("Scenario");
1451
+ if (missingStructure.length > 0) {
1213
1452
  issues.push(
1214
- issue3(
1215
- "QFAI-SC-002",
1216
- "SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
1453
+ issue4(
1454
+ "QFAI-SC-006",
1455
+ `Scenario \u30D5\u30A1\u30A4\u30EB\u306B\u5FC5\u8981\u306A\u69CB\u9020\u304C\u3042\u308A\u307E\u305B\u3093: ${missingStructure.join(
1456
+ ", "
1457
+ )}`,
1217
1458
  "error",
1218
1459
  file,
1219
- "scenario.spec"
1460
+ "scenario.structure"
1220
1461
  )
1221
1462
  );
1222
1463
  }
1223
- const brIds = extractIds(text, "BR");
1224
- if (brIds.length === 0) {
1225
- issues.push(
1226
- issue3(
1227
- "QFAI-SC-003",
1228
- "SC \u306F BR \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
1229
- "error",
1230
- file,
1231
- "scenario.br"
1232
- )
1233
- );
1464
+ for (const scenario of parsed.scenarios) {
1465
+ if (scenario.tags.length === 0) {
1466
+ issues.push(
1467
+ issue4(
1468
+ "QFAI-SC-007",
1469
+ `Scenario \u30BF\u30B0\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${scenario.name}`,
1470
+ "error",
1471
+ file,
1472
+ "scenario.tags"
1473
+ )
1474
+ );
1475
+ continue;
1476
+ }
1477
+ const missingTags = [];
1478
+ if (!scenario.tags.some((tag) => SC_TAG_RE2.test(tag))) {
1479
+ missingTags.push("SC");
1480
+ }
1481
+ if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
1482
+ missingTags.push("SPEC");
1483
+ }
1484
+ if (!scenario.tags.some((tag) => BR_TAG_RE.test(tag))) {
1485
+ missingTags.push("BR");
1486
+ }
1487
+ if (missingTags.length > 0) {
1488
+ issues.push(
1489
+ issue4(
1490
+ "QFAI-SC-008",
1491
+ `Scenario \u30BF\u30B0\u306B\u4E0D\u8DB3\u304C\u3042\u308A\u307E\u3059: ${missingTags.join(", ")} (${scenario.name})`,
1492
+ "error",
1493
+ file,
1494
+ "scenario.tagIds"
1495
+ )
1496
+ );
1497
+ }
1234
1498
  }
1235
1499
  const missingSteps = [];
1236
1500
  if (!GIVEN_PATTERN.test(text)) {
@@ -1244,7 +1508,7 @@ function validateScenarioContent(text, file) {
1244
1508
  }
1245
1509
  if (missingSteps.length > 0) {
1246
1510
  issues.push(
1247
- issue3(
1511
+ issue4(
1248
1512
  "QFAI-SC-005",
1249
1513
  `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
1250
1514
  "warning",
@@ -1255,33 +1519,33 @@ function validateScenarioContent(text, file) {
1255
1519
  }
1256
1520
  return issues;
1257
1521
  }
1258
- function issue3(code, message, severity, file, rule, refs) {
1259
- const issue6 = {
1522
+ function issue4(code, message, severity, file, rule, refs) {
1523
+ const issue7 = {
1260
1524
  code,
1261
1525
  severity,
1262
1526
  message
1263
1527
  };
1264
1528
  if (file) {
1265
- issue6.file = file;
1529
+ issue7.file = file;
1266
1530
  }
1267
1531
  if (rule) {
1268
- issue6.rule = rule;
1532
+ issue7.rule = rule;
1269
1533
  }
1270
1534
  if (refs && refs.length > 0) {
1271
- issue6.refs = refs;
1535
+ issue7.refs = refs;
1272
1536
  }
1273
- return issue6;
1537
+ return issue7;
1274
1538
  }
1275
1539
 
1276
1540
  // src/core/validators/spec.ts
1277
- var import_promises9 = require("fs/promises");
1541
+ var import_promises10 = require("fs/promises");
1278
1542
  async function validateSpecs(root, config) {
1279
1543
  const specsRoot = resolvePath(root, config, "specDir");
1280
1544
  const files = await collectSpecFiles(specsRoot);
1281
1545
  if (files.length === 0) {
1282
1546
  const expected = "spec-0001-<slug>.md";
1283
1547
  return [
1284
- issue4(
1548
+ issue5(
1285
1549
  "QFAI-SPEC-000",
1286
1550
  `Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}`,
1287
1551
  "info",
@@ -1292,7 +1556,7 @@ async function validateSpecs(root, config) {
1292
1556
  }
1293
1557
  const issues = [];
1294
1558
  for (const file of files) {
1295
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1559
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1296
1560
  issues.push(
1297
1561
  ...validateSpecContent(
1298
1562
  text,
@@ -1305,6 +1569,7 @@ async function validateSpecs(root, config) {
1305
1569
  }
1306
1570
  function validateSpecContent(text, file, requiredSections) {
1307
1571
  const issues = [];
1572
+ const parsed = parseSpec(text, file);
1308
1573
  const invalidIds = extractInvalidIds(text, [
1309
1574
  "SPEC",
1310
1575
  "BR",
@@ -1316,7 +1581,7 @@ function validateSpecContent(text, file, requiredSections) {
1316
1581
  ]);
1317
1582
  if (invalidIds.length > 0) {
1318
1583
  issues.push(
1319
- issue4(
1584
+ issue5(
1320
1585
  "QFAI-ID-002",
1321
1586
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
1322
1587
  "error",
@@ -1326,10 +1591,9 @@ function validateSpecContent(text, file, requiredSections) {
1326
1591
  )
1327
1592
  );
1328
1593
  }
1329
- const specIds = extractIds(text, "SPEC");
1330
- if (specIds.length === 0) {
1594
+ if (!parsed.specId) {
1331
1595
  issues.push(
1332
- issue4(
1596
+ issue5(
1333
1597
  "QFAI-SPEC-001",
1334
1598
  "SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1335
1599
  "error",
@@ -1338,10 +1602,9 @@ function validateSpecContent(text, file, requiredSections) {
1338
1602
  )
1339
1603
  );
1340
1604
  }
1341
- const brIds = extractIds(text, "BR");
1342
- if (brIds.length === 0) {
1605
+ if (parsed.brs.length === 0) {
1343
1606
  issues.push(
1344
- issue4(
1607
+ issue5(
1345
1608
  "QFAI-SPEC-002",
1346
1609
  "BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1347
1610
  "error",
@@ -1350,10 +1613,34 @@ function validateSpecContent(text, file, requiredSections) {
1350
1613
  )
1351
1614
  );
1352
1615
  }
1616
+ for (const br of parsed.brsWithoutPriority) {
1617
+ issues.push(
1618
+ issue5(
1619
+ "QFAI-BR-001",
1620
+ `BR \u884C\u306B Priority \u304C\u3042\u308A\u307E\u305B\u3093: ${br.id}`,
1621
+ "error",
1622
+ file,
1623
+ "spec.brPriority",
1624
+ [br.id]
1625
+ )
1626
+ );
1627
+ }
1628
+ for (const br of parsed.brsWithInvalidPriority) {
1629
+ issues.push(
1630
+ issue5(
1631
+ "QFAI-BR-002",
1632
+ `BR Priority \u304C\u4E0D\u6B63\u3067\u3059: ${br.id} (${br.priority})`,
1633
+ "error",
1634
+ file,
1635
+ "spec.brPriority",
1636
+ [br.id]
1637
+ )
1638
+ );
1639
+ }
1353
1640
  const scIds = extractIds(text, "SC");
1354
1641
  if (scIds.length > 0) {
1355
1642
  issues.push(
1356
- issue4(
1643
+ issue5(
1357
1644
  "QFAI-SPEC-003",
1358
1645
  "Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
1359
1646
  "warning",
@@ -1364,9 +1651,9 @@ function validateSpecContent(text, file, requiredSections) {
1364
1651
  );
1365
1652
  }
1366
1653
  for (const section of requiredSections) {
1367
- if (!text.includes(section)) {
1654
+ if (!parsed.sections.has(section)) {
1368
1655
  issues.push(
1369
- issue4(
1656
+ issue5(
1370
1657
  "QFAI-SPEC-004",
1371
1658
  `\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
1372
1659
  "error",
@@ -1378,26 +1665,32 @@ function validateSpecContent(text, file, requiredSections) {
1378
1665
  }
1379
1666
  return issues;
1380
1667
  }
1381
- function issue4(code, message, severity, file, rule, refs) {
1382
- const issue6 = {
1668
+ function issue5(code, message, severity, file, rule, refs) {
1669
+ const issue7 = {
1383
1670
  code,
1384
1671
  severity,
1385
1672
  message
1386
1673
  };
1387
1674
  if (file) {
1388
- issue6.file = file;
1675
+ issue7.file = file;
1389
1676
  }
1390
1677
  if (rule) {
1391
- issue6.rule = rule;
1678
+ issue7.rule = rule;
1392
1679
  }
1393
1680
  if (refs && refs.length > 0) {
1394
- issue6.refs = refs;
1681
+ issue7.refs = refs;
1395
1682
  }
1396
- return issue6;
1683
+ return issue7;
1397
1684
  }
1398
1685
 
1399
1686
  // src/core/validators/traceability.ts
1400
- var import_promises10 = require("fs/promises");
1687
+ var import_promises11 = require("fs/promises");
1688
+ var SC_TAG_RE3 = /^SC-\d{4}$/;
1689
+ var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1690
+ var BR_TAG_RE2 = /^BR-\d{4}$/;
1691
+ var UI_TAG_RE = /^UI-\d{4}$/;
1692
+ var API_TAG_RE = /^API-\d{4}$/;
1693
+ var DATA_TAG_RE = /^DATA-\d{4}$/;
1401
1694
  async function validateTraceability(root, config) {
1402
1695
  const issues = [];
1403
1696
  const specsRoot = resolvePath(root, config, "specDir");
@@ -1423,11 +1716,13 @@ async function validateTraceability(root, config) {
1423
1716
  const contractIndex = await buildContractIndex(root, config);
1424
1717
  const contractIds = contractIndex.ids;
1425
1718
  for (const file of specFiles) {
1426
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1719
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1427
1720
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1428
- const specIdsInFile = extractIds(text, "SPEC");
1429
- specIdsInFile.forEach((id) => specIds.add(id));
1430
- const brIds = extractIds(text, "BR");
1721
+ const parsed = parseSpec(text, file);
1722
+ if (parsed.specId) {
1723
+ specIds.add(parsed.specId);
1724
+ }
1725
+ const brIds = parsed.brs.map((br) => br.id);
1431
1726
  brIds.forEach((id) => brIdsInSpecs.add(id));
1432
1727
  const referencedContractIds = /* @__PURE__ */ new Set([
1433
1728
  ...extractIds(text, "UI"),
@@ -1439,7 +1734,7 @@ async function validateTraceability(root, config) {
1439
1734
  );
1440
1735
  if (unknownContractIds.length > 0) {
1441
1736
  issues.push(
1442
- issue5(
1737
+ issue6(
1443
1738
  "QFAI-TRACE-009",
1444
1739
  `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1445
1740
  ", "
@@ -1451,37 +1746,54 @@ async function validateTraceability(root, config) {
1451
1746
  )
1452
1747
  );
1453
1748
  }
1454
- for (const specId of specIdsInFile) {
1455
- const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
1749
+ if (parsed.specId) {
1750
+ const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
1456
1751
  brIds.forEach((id) => current.add(id));
1457
- specToBrIds.set(specId, current);
1752
+ specToBrIds.set(parsed.specId, current);
1458
1753
  }
1459
1754
  }
1460
1755
  for (const file of decisionFiles) {
1461
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1756
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1462
1757
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1463
1758
  }
1464
1759
  for (const file of scenarioFiles) {
1465
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1760
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1466
1761
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1467
- const specIdsInScenario = extractIds(text, "SPEC");
1468
- const brIds = extractIds(text, "BR");
1469
- const scIds = extractIds(text, "SC");
1470
- const scenarioIds = [
1471
- ...extractIds(text, "UI"),
1472
- ...extractIds(text, "API"),
1473
- ...extractIds(text, "DATA")
1474
- ];
1475
- brIds.forEach((id) => brIdsInScenarios.add(id));
1476
- scIds.forEach((id) => scIdsInScenarios.add(id));
1477
- scenarioIds.forEach((id) => scenarioContractIds.add(id));
1478
- if (scenarioIds.length > 0) {
1479
- scIds.forEach((id) => scWithContracts.add(id));
1762
+ const parsed = parseGherkinFeature(text, file);
1763
+ const specIdsInScenario = /* @__PURE__ */ new Set();
1764
+ const brIds = /* @__PURE__ */ new Set();
1765
+ const scIds = /* @__PURE__ */ new Set();
1766
+ const scenarioIds = /* @__PURE__ */ new Set();
1767
+ for (const scenario of parsed.scenarios) {
1768
+ for (const tag of scenario.tags) {
1769
+ if (SPEC_TAG_RE2.test(tag)) {
1770
+ specIdsInScenario.add(tag);
1771
+ }
1772
+ if (BR_TAG_RE2.test(tag)) {
1773
+ brIds.add(tag);
1774
+ }
1775
+ if (SC_TAG_RE3.test(tag)) {
1776
+ scIds.add(tag);
1777
+ }
1778
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1779
+ scenarioIds.add(tag);
1780
+ }
1781
+ }
1480
1782
  }
1481
- const unknownSpecIds = specIdsInScenario.filter((id) => !specIds.has(id));
1783
+ const specIdsList = Array.from(specIdsInScenario);
1784
+ const brIdsList = Array.from(brIds);
1785
+ const scIdsList = Array.from(scIds);
1786
+ const scenarioIdsList = Array.from(scenarioIds);
1787
+ brIdsList.forEach((id) => brIdsInScenarios.add(id));
1788
+ scIdsList.forEach((id) => scIdsInScenarios.add(id));
1789
+ scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
1790
+ if (scenarioIdsList.length > 0) {
1791
+ scIdsList.forEach((id) => scWithContracts.add(id));
1792
+ }
1793
+ const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
1482
1794
  if (unknownSpecIds.length > 0) {
1483
1795
  issues.push(
1484
- issue5(
1796
+ issue6(
1485
1797
  "QFAI-TRACE-005",
1486
1798
  `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
1487
1799
  "error",
@@ -1491,10 +1803,10 @@ async function validateTraceability(root, config) {
1491
1803
  )
1492
1804
  );
1493
1805
  }
1494
- const unknownBrIds = brIds.filter((id) => !brIdsInSpecs.has(id));
1806
+ const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
1495
1807
  if (unknownBrIds.length > 0) {
1496
1808
  issues.push(
1497
- issue5(
1809
+ issue6(
1498
1810
  "QFAI-TRACE-006",
1499
1811
  `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
1500
1812
  "error",
@@ -1504,10 +1816,12 @@ async function validateTraceability(root, config) {
1504
1816
  )
1505
1817
  );
1506
1818
  }
1507
- const unknownContractIds = scenarioIds.filter((id) => !contractIds.has(id));
1819
+ const unknownContractIds = scenarioIdsList.filter(
1820
+ (id) => !contractIds.has(id)
1821
+ );
1508
1822
  if (unknownContractIds.length > 0) {
1509
1823
  issues.push(
1510
- issue5(
1824
+ issue6(
1511
1825
  "QFAI-TRACE-008",
1512
1826
  `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1513
1827
  ", "
@@ -1519,23 +1833,23 @@ async function validateTraceability(root, config) {
1519
1833
  )
1520
1834
  );
1521
1835
  }
1522
- if (specIdsInScenario.length > 0) {
1836
+ if (specIdsList.length > 0) {
1523
1837
  const allowedBrIds = /* @__PURE__ */ new Set();
1524
- for (const specId of specIdsInScenario) {
1838
+ for (const specId of specIdsList) {
1525
1839
  const brIdsForSpec = specToBrIds.get(specId);
1526
1840
  if (!brIdsForSpec) {
1527
1841
  continue;
1528
1842
  }
1529
1843
  brIdsForSpec.forEach((id) => allowedBrIds.add(id));
1530
1844
  }
1531
- const invalidBrIds = brIds.filter((id) => !allowedBrIds.has(id));
1845
+ const invalidBrIds = brIdsList.filter((id) => !allowedBrIds.has(id));
1532
1846
  if (invalidBrIds.length > 0) {
1533
1847
  issues.push(
1534
- issue5(
1848
+ issue6(
1535
1849
  "QFAI-TRACE-007",
1536
1850
  `Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
1537
1851
  ", "
1538
- )} (SPEC: ${specIdsInScenario.join(", ")})`,
1852
+ )} (SPEC: ${specIdsList.join(", ")})`,
1539
1853
  "error",
1540
1854
  file,
1541
1855
  "traceability.scenarioBrUnderSpec",
@@ -1547,7 +1861,7 @@ async function validateTraceability(root, config) {
1547
1861
  }
1548
1862
  if (upstreamIds.size === 0) {
1549
1863
  return [
1550
- issue5(
1864
+ issue6(
1551
1865
  "QFAI-TRACE-000",
1552
1866
  "\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1553
1867
  "info",
@@ -1562,7 +1876,7 @@ async function validateTraceability(root, config) {
1562
1876
  );
1563
1877
  if (orphanBrIds.length > 0) {
1564
1878
  issues.push(
1565
- issue5(
1879
+ issue6(
1566
1880
  "QFAI_TRACE_BR_ORPHAN",
1567
1881
  `BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
1568
1882
  "error",
@@ -1579,7 +1893,7 @@ async function validateTraceability(root, config) {
1579
1893
  );
1580
1894
  if (scWithoutContracts.length > 0) {
1581
1895
  issues.push(
1582
- issue5(
1896
+ issue6(
1583
1897
  "QFAI_TRACE_SC_NO_CONTRACT",
1584
1898
  `SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
1585
1899
  ", "
@@ -1599,7 +1913,7 @@ async function validateTraceability(root, config) {
1599
1913
  );
1600
1914
  if (orphanContracts.length > 0) {
1601
1915
  issues.push(
1602
- issue5(
1916
+ issue6(
1603
1917
  "QFAI_CONTRACT_ORPHAN",
1604
1918
  `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
1605
1919
  "error",
@@ -1627,7 +1941,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1627
1941
  const targetFiles = [...codeFiles, ...testFiles];
1628
1942
  if (targetFiles.length === 0) {
1629
1943
  issues.push(
1630
- issue5(
1944
+ issue6(
1631
1945
  "QFAI-TRACE-001",
1632
1946
  "\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1633
1947
  "info",
@@ -1640,7 +1954,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1640
1954
  const pattern = buildIdPattern(Array.from(upstreamIds));
1641
1955
  let found = false;
1642
1956
  for (const file of targetFiles) {
1643
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1957
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1644
1958
  if (pattern.test(text)) {
1645
1959
  found = true;
1646
1960
  break;
@@ -1648,7 +1962,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1648
1962
  }
1649
1963
  if (!found) {
1650
1964
  issues.push(
1651
- issue5(
1965
+ issue6(
1652
1966
  "QFAI-TRACE-002",
1653
1967
  "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
1654
1968
  "warning",
@@ -1663,22 +1977,22 @@ function buildIdPattern(ids) {
1663
1977
  const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
1664
1978
  return new RegExp(`\\b(${escaped.join("|")})\\b`);
1665
1979
  }
1666
- function issue5(code, message, severity, file, rule, refs) {
1667
- const issue6 = {
1980
+ function issue6(code, message, severity, file, rule, refs) {
1981
+ const issue7 = {
1668
1982
  code,
1669
1983
  severity,
1670
1984
  message
1671
1985
  };
1672
1986
  if (file) {
1673
- issue6.file = file;
1987
+ issue7.file = file;
1674
1988
  }
1675
1989
  if (rule) {
1676
- issue6.rule = rule;
1990
+ issue7.rule = rule;
1677
1991
  }
1678
1992
  if (refs && refs.length > 0) {
1679
- issue6.refs = refs;
1993
+ issue7.refs = refs;
1680
1994
  }
1681
- return issue6;
1995
+ return issue7;
1682
1996
  }
1683
1997
 
1684
1998
  // src/core/validate.ts
@@ -1689,6 +2003,7 @@ async function validateProject(root, configResult) {
1689
2003
  ...configIssues,
1690
2004
  ...await validateSpecs(root, config),
1691
2005
  ...await validateScenarios(root, config),
2006
+ ...await validateDecisions(root, config),
1692
2007
  ...await validateContracts(root, config),
1693
2008
  ...await validateDefinedIds(root, config),
1694
2009
  ...await validateTraceability(root, config)
@@ -1703,8 +2018,8 @@ async function validateProject(root, configResult) {
1703
2018
  }
1704
2019
  function countIssues(issues) {
1705
2020
  return issues.reduce(
1706
- (acc, issue6) => {
1707
- acc[issue6.severity] += 1;
2021
+ (acc, issue7) => {
2022
+ acc[issue7.severity] += 1;
1708
2023
  return acc;
1709
2024
  },
1710
2025
  { info: 0, warning: 0, error: 0 }
@@ -1875,7 +2190,7 @@ async function collectIds(files) {
1875
2190
  DATA: /* @__PURE__ */ new Set()
1876
2191
  };
1877
2192
  for (const file of files) {
1878
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2193
+ const text = await (0, import_promises12.readFile)(file, "utf-8");
1879
2194
  for (const prefix of ID_PREFIXES2) {
1880
2195
  const ids = extractIds(text, prefix);
1881
2196
  ids.forEach((id) => result[prefix].add(id));
@@ -1893,7 +2208,7 @@ async function collectIds(files) {
1893
2208
  async function collectUpstreamIds(files) {
1894
2209
  const ids = /* @__PURE__ */ new Set();
1895
2210
  for (const file of files) {
1896
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2211
+ const text = await (0, import_promises12.readFile)(file, "utf-8");
1897
2212
  extractAllIds(text).forEach((id) => ids.add(id));
1898
2213
  }
1899
2214
  return ids;
@@ -1914,7 +2229,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
1914
2229
  }
1915
2230
  const pattern = buildIdPattern2(Array.from(upstreamIds));
1916
2231
  for (const file of targetFiles) {
1917
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2232
+ const text = await (0, import_promises12.readFile)(file, "utf-8");
1918
2233
  if (pattern.test(text)) {
1919
2234
  return true;
1920
2235
  }
@@ -1936,20 +2251,20 @@ function toSortedArray(values) {
1936
2251
  }
1937
2252
  function buildHotspots(issues) {
1938
2253
  const map = /* @__PURE__ */ new Map();
1939
- for (const issue6 of issues) {
1940
- if (!issue6.file) {
2254
+ for (const issue7 of issues) {
2255
+ if (!issue7.file) {
1941
2256
  continue;
1942
2257
  }
1943
- const current = map.get(issue6.file) ?? {
1944
- file: issue6.file,
2258
+ const current = map.get(issue7.file) ?? {
2259
+ file: issue7.file,
1945
2260
  total: 0,
1946
2261
  error: 0,
1947
2262
  warning: 0,
1948
2263
  info: 0
1949
2264
  };
1950
2265
  current.total += 1;
1951
- current[issue6.severity] += 1;
1952
- map.set(issue6.file, current);
2266
+ current[issue7.severity] += 1;
2267
+ map.set(issue7.file, current);
1953
2268
  }
1954
2269
  return Array.from(map.values()).sort(
1955
2270
  (a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
@@ -1987,8 +2302,8 @@ async function runReport(options) {
1987
2302
  const defaultOut = options.format === "json" ? ".qfai/out/report.json" : ".qfai/out/report.md";
1988
2303
  const out = options.outPath ?? defaultOut;
1989
2304
  const outPath = import_node_path10.default.isAbsolute(out) ? out : import_node_path10.default.resolve(root, out);
1990
- await (0, import_promises12.mkdir)(import_node_path10.default.dirname(outPath), { recursive: true });
1991
- await (0, import_promises12.writeFile)(outPath, `${output}
2305
+ await (0, import_promises13.mkdir)(import_node_path10.default.dirname(outPath), { recursive: true });
2306
+ await (0, import_promises13.writeFile)(outPath, `${output}
1992
2307
  `, "utf-8");
1993
2308
  info(
1994
2309
  `report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
@@ -1996,7 +2311,7 @@ async function runReport(options) {
1996
2311
  info(`wrote report: ${outPath}`);
1997
2312
  }
1998
2313
  async function readValidationResult(inputPath) {
1999
- const raw = await (0, import_promises12.readFile)(inputPath, "utf-8");
2314
+ const raw = await (0, import_promises13.readFile)(inputPath, "utf-8");
2000
2315
  const parsed = JSON.parse(raw);
2001
2316
  if (!isValidationResult(parsed)) {
2002
2317
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
@@ -2037,7 +2352,7 @@ function isMissingFileError(error2) {
2037
2352
  }
2038
2353
 
2039
2354
  // src/cli/commands/validate.ts
2040
- var import_promises13 = require("fs/promises");
2355
+ var import_promises14 = require("fs/promises");
2041
2356
  var import_node_path11 = __toESM(require("path"), 1);
2042
2357
 
2043
2358
  // src/cli/lib/failOn.ts
@@ -2097,21 +2412,21 @@ function emitText(result) {
2097
2412
  `
2098
2413
  );
2099
2414
  }
2100
- function emitGitHub(issue6) {
2101
- const level = issue6.severity === "error" ? "error" : issue6.severity === "warning" ? "warning" : "notice";
2102
- const file = issue6.file ? `file=${issue6.file}` : "";
2103
- const line = issue6.loc?.line ? `,line=${issue6.loc.line}` : "";
2104
- const column = issue6.loc?.column ? `,col=${issue6.loc.column}` : "";
2415
+ function emitGitHub(issue7) {
2416
+ const level = issue7.severity === "error" ? "error" : issue7.severity === "warning" ? "warning" : "notice";
2417
+ const file = issue7.file ? `file=${issue7.file}` : "";
2418
+ const line = issue7.loc?.line ? `,line=${issue7.loc.line}` : "";
2419
+ const column = issue7.loc?.column ? `,col=${issue7.loc.column}` : "";
2105
2420
  const location = file ? ` ${file}${line}${column}` : "";
2106
2421
  process.stdout.write(
2107
- `::${level}${location}::${issue6.code}: ${issue6.message}
2422
+ `::${level}${location}::${issue7.code}: ${issue7.message}
2108
2423
  `
2109
2424
  );
2110
2425
  }
2111
2426
  async function emitJson(result, root, jsonPath) {
2112
2427
  const abs = import_node_path11.default.isAbsolute(jsonPath) ? jsonPath : import_node_path11.default.resolve(root, jsonPath);
2113
- await (0, import_promises13.mkdir)(import_node_path11.default.dirname(abs), { recursive: true });
2114
- await (0, import_promises13.writeFile)(abs, `${JSON.stringify(result, null, 2)}
2428
+ await (0, import_promises14.mkdir)(import_node_path11.default.dirname(abs), { recursive: true });
2429
+ await (0, import_promises14.writeFile)(abs, `${JSON.stringify(result, null, 2)}
2115
2430
  `, "utf-8");
2116
2431
  }
2117
2432