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.
- package/README.md +1 -1
- package/assets/init/.qfai/README.md +1 -1
- package/assets/init/.qfai/contracts/api/api-0001-sample.yaml +1 -4
- package/assets/init/.qfai/prompts/README.md +1 -1
- package/assets/init/.qfai/spec/README.md +5 -3
- package/assets/init/.qfai/spec/decisions/ADR-0001.md +1 -1
- package/assets/init/.qfai/spec/decisions/README.md +2 -1
- package/assets/init/.qfai/spec/scenarios/scenarios.feature +1 -1
- package/assets/init/.qfai/spec/spec-0001-sample.md +2 -1
- package/dist/cli/index.cjs +471 -156
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +466 -151
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +460 -143
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.mjs +459 -143
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -137,7 +137,7 @@ function report(copied, skipped, dryRun, label) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// src/cli/commands/report.ts
|
|
140
|
-
import { mkdir as mkdir2, readFile as
|
|
140
|
+
import { mkdir as mkdir2, readFile as readFile11, writeFile } from "fs/promises";
|
|
141
141
|
import path10 from "path";
|
|
142
142
|
|
|
143
143
|
// src/core/config.ts
|
|
@@ -512,7 +512,7 @@ function isRecord(value) {
|
|
|
512
512
|
}
|
|
513
513
|
|
|
514
514
|
// src/core/report.ts
|
|
515
|
-
import { readFile as
|
|
515
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
516
516
|
|
|
517
517
|
// src/core/discovery.ts
|
|
518
518
|
import path6 from "path";
|
|
@@ -661,8 +661,8 @@ import { readFile as readFile2 } from "fs/promises";
|
|
|
661
661
|
import path7 from "path";
|
|
662
662
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
663
663
|
async function resolveToolVersion() {
|
|
664
|
-
if ("0.
|
|
665
|
-
return "0.
|
|
664
|
+
if ("0.3.0".length > 0) {
|
|
665
|
+
return "0.3.0";
|
|
666
666
|
}
|
|
667
667
|
try {
|
|
668
668
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -965,29 +965,113 @@ function formatError2(error2) {
|
|
|
965
965
|
return String(error2);
|
|
966
966
|
}
|
|
967
967
|
function issue(code, message, severity, file, rule, refs) {
|
|
968
|
-
const
|
|
968
|
+
const issue7 = {
|
|
969
969
|
code,
|
|
970
970
|
severity,
|
|
971
971
|
message
|
|
972
972
|
};
|
|
973
973
|
if (file) {
|
|
974
|
-
|
|
974
|
+
issue7.file = file;
|
|
975
975
|
}
|
|
976
976
|
if (rule) {
|
|
977
|
-
|
|
977
|
+
issue7.rule = rule;
|
|
978
978
|
}
|
|
979
979
|
if (refs && refs.length > 0) {
|
|
980
|
-
|
|
980
|
+
issue7.refs = refs;
|
|
981
981
|
}
|
|
982
|
-
return
|
|
982
|
+
return issue7;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// src/core/validators/decisions.ts
|
|
986
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
987
|
+
|
|
988
|
+
// src/core/parse/adr.ts
|
|
989
|
+
var ADR_ID_RE = /\bADR-\d{4}\b/;
|
|
990
|
+
function extractField(md, key) {
|
|
991
|
+
const pattern = new RegExp(`^\\s*-\\s*${key}:\\s*(.+)\\s*$`, "m");
|
|
992
|
+
return md.match(pattern)?.[1]?.trim();
|
|
993
|
+
}
|
|
994
|
+
function parseAdr(md, file) {
|
|
995
|
+
const adrId = md.match(ADR_ID_RE)?.[0];
|
|
996
|
+
const fields = {};
|
|
997
|
+
const status = extractField(md, "Status");
|
|
998
|
+
const context = extractField(md, "Context");
|
|
999
|
+
const decision = extractField(md, "Decision");
|
|
1000
|
+
const consequences = extractField(md, "Consequences");
|
|
1001
|
+
const related = extractField(md, "Related");
|
|
1002
|
+
if (status) fields.status = status;
|
|
1003
|
+
if (context) fields.context = context;
|
|
1004
|
+
if (decision) fields.decision = decision;
|
|
1005
|
+
if (consequences) fields.consequences = consequences;
|
|
1006
|
+
if (related) fields.related = related;
|
|
1007
|
+
const parsed = {
|
|
1008
|
+
file,
|
|
1009
|
+
fields
|
|
1010
|
+
};
|
|
1011
|
+
if (adrId) {
|
|
1012
|
+
parsed.adrId = adrId;
|
|
1013
|
+
}
|
|
1014
|
+
return parsed;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// src/core/validators/decisions.ts
|
|
1018
|
+
var REQUIRED_FIELDS = [
|
|
1019
|
+
{ key: "status", label: "Status" },
|
|
1020
|
+
{ key: "context", label: "Context" },
|
|
1021
|
+
{ key: "decision", label: "Decision" },
|
|
1022
|
+
{ key: "consequences", label: "Consequences" }
|
|
1023
|
+
];
|
|
1024
|
+
async function validateDecisions(root, config) {
|
|
1025
|
+
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1026
|
+
const files = await collectFiles(decisionsRoot, { extensions: [".md"] });
|
|
1027
|
+
if (files.length === 0) {
|
|
1028
|
+
return [];
|
|
1029
|
+
}
|
|
1030
|
+
const issues = [];
|
|
1031
|
+
for (const file of files) {
|
|
1032
|
+
const text = await readFile4(file, "utf-8");
|
|
1033
|
+
const parsed = parseAdr(text, file);
|
|
1034
|
+
const missing = REQUIRED_FIELDS.filter(
|
|
1035
|
+
(field) => !parsed.fields[field.key]
|
|
1036
|
+
);
|
|
1037
|
+
if (missing.length > 0) {
|
|
1038
|
+
issues.push(
|
|
1039
|
+
issue2(
|
|
1040
|
+
"QFAI-ADR-001",
|
|
1041
|
+
`ADR \u5FC5\u9808\u30D5\u30A3\u30FC\u30EB\u30C9\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missing.map((field) => field.label).join(", ")}`,
|
|
1042
|
+
"error",
|
|
1043
|
+
file,
|
|
1044
|
+
"adr.requiredFields"
|
|
1045
|
+
)
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return issues;
|
|
1050
|
+
}
|
|
1051
|
+
function issue2(code, message, severity, file, rule, refs) {
|
|
1052
|
+
const issue7 = {
|
|
1053
|
+
code,
|
|
1054
|
+
severity,
|
|
1055
|
+
message
|
|
1056
|
+
};
|
|
1057
|
+
if (file) {
|
|
1058
|
+
issue7.file = file;
|
|
1059
|
+
}
|
|
1060
|
+
if (rule) {
|
|
1061
|
+
issue7.rule = rule;
|
|
1062
|
+
}
|
|
1063
|
+
if (refs && refs.length > 0) {
|
|
1064
|
+
issue7.refs = refs;
|
|
1065
|
+
}
|
|
1066
|
+
return issue7;
|
|
983
1067
|
}
|
|
984
1068
|
|
|
985
1069
|
// src/core/validators/ids.ts
|
|
986
|
-
import { readFile as
|
|
1070
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
987
1071
|
import path9 from "path";
|
|
988
1072
|
|
|
989
1073
|
// src/core/contractIndex.ts
|
|
990
|
-
import { readFile as
|
|
1074
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
991
1075
|
async function buildContractIndex(root, config) {
|
|
992
1076
|
const uiRoot = resolvePath(root, config, "uiContractsDir");
|
|
993
1077
|
const apiRoot = resolvePath(root, config, "apiContractsDir");
|
|
@@ -1010,7 +1094,7 @@ async function buildContractIndex(root, config) {
|
|
|
1010
1094
|
}
|
|
1011
1095
|
async function indexUiContracts(files, index) {
|
|
1012
1096
|
for (const file of files) {
|
|
1013
|
-
const text = await
|
|
1097
|
+
const text = await readFile5(file, "utf-8");
|
|
1014
1098
|
try {
|
|
1015
1099
|
const doc = parseStructuredContract(file, text);
|
|
1016
1100
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1022,7 +1106,7 @@ async function indexUiContracts(files, index) {
|
|
|
1022
1106
|
}
|
|
1023
1107
|
async function indexApiContracts(files, index) {
|
|
1024
1108
|
for (const file of files) {
|
|
1025
|
-
const text = await
|
|
1109
|
+
const text = await readFile5(file, "utf-8");
|
|
1026
1110
|
try {
|
|
1027
1111
|
const doc = parseStructuredContract(file, text);
|
|
1028
1112
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1034,7 +1118,7 @@ async function indexApiContracts(files, index) {
|
|
|
1034
1118
|
}
|
|
1035
1119
|
async function indexDataContracts(files, index) {
|
|
1036
1120
|
for (const file of files) {
|
|
1037
|
-
const text = await
|
|
1121
|
+
const text = await readFile5(file, "utf-8");
|
|
1038
1122
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1039
1123
|
}
|
|
1040
1124
|
}
|
|
@@ -1045,7 +1129,158 @@ function record(index, id, file) {
|
|
|
1045
1129
|
index.idToFiles.set(id, current);
|
|
1046
1130
|
}
|
|
1047
1131
|
|
|
1132
|
+
// src/core/parse/gherkin.ts
|
|
1133
|
+
var FEATURE_RE = /^\s*Feature:\s+/;
|
|
1134
|
+
var SCENARIO_RE = /^\s*Scenario:\s*(.+)\s*$/;
|
|
1135
|
+
var TAG_LINE_RE = /^\s*@/;
|
|
1136
|
+
function parseTags(line) {
|
|
1137
|
+
return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
|
|
1138
|
+
}
|
|
1139
|
+
function parseGherkinFeature(text, file) {
|
|
1140
|
+
const lines = text.split(/\r?\n/);
|
|
1141
|
+
const scenarios = [];
|
|
1142
|
+
let featurePresent = false;
|
|
1143
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1144
|
+
const line = lines[i] ?? "";
|
|
1145
|
+
if (FEATURE_RE.test(line)) {
|
|
1146
|
+
featurePresent = true;
|
|
1147
|
+
}
|
|
1148
|
+
const match = line.match(SCENARIO_RE);
|
|
1149
|
+
if (!match) continue;
|
|
1150
|
+
const scenarioName = match[1];
|
|
1151
|
+
if (!scenarioName) continue;
|
|
1152
|
+
const tags = [];
|
|
1153
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
1154
|
+
const previous = lines[j] ?? "";
|
|
1155
|
+
if (previous.trim() === "") continue;
|
|
1156
|
+
if (!TAG_LINE_RE.test(previous)) break;
|
|
1157
|
+
tags.unshift(...parseTags(previous));
|
|
1158
|
+
}
|
|
1159
|
+
scenarios.push({ name: scenarioName, line: i + 1, tags });
|
|
1160
|
+
}
|
|
1161
|
+
return { file, featurePresent, scenarios };
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// src/core/parse/markdown.ts
|
|
1165
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1166
|
+
function parseHeadings(md) {
|
|
1167
|
+
const lines = md.split(/\r?\n/);
|
|
1168
|
+
const headings = [];
|
|
1169
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1170
|
+
const line = lines[i] ?? "";
|
|
1171
|
+
const match = line.match(HEADING_RE);
|
|
1172
|
+
if (!match) continue;
|
|
1173
|
+
const levelToken = match[1];
|
|
1174
|
+
const title = match[2];
|
|
1175
|
+
if (!levelToken || !title) continue;
|
|
1176
|
+
headings.push({
|
|
1177
|
+
level: levelToken.length,
|
|
1178
|
+
title: title.trim(),
|
|
1179
|
+
line: i + 1
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
return headings;
|
|
1183
|
+
}
|
|
1184
|
+
function extractH2Sections(md) {
|
|
1185
|
+
const lines = md.split(/\r?\n/);
|
|
1186
|
+
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1187
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1188
|
+
for (let i = 0; i < headings.length; i++) {
|
|
1189
|
+
const current = headings[i];
|
|
1190
|
+
if (!current) continue;
|
|
1191
|
+
const next = headings[i + 1];
|
|
1192
|
+
const startLine = current.line + 1;
|
|
1193
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1194
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1195
|
+
sections.set(current.title.trim(), {
|
|
1196
|
+
title: current.title.trim(),
|
|
1197
|
+
startLine,
|
|
1198
|
+
endLine,
|
|
1199
|
+
body
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
return sections;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/core/parse/spec.ts
|
|
1206
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1207
|
+
var BR_LINE_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[0-3])\)\s*(.+)$/;
|
|
1208
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[^)]+)\)\s*(.+)$/;
|
|
1209
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s+(?!\()(.*\S.*)$/;
|
|
1210
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1211
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1212
|
+
function parseSpec(md, file) {
|
|
1213
|
+
const headings = parseHeadings(md);
|
|
1214
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
1215
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1216
|
+
const sections = extractH2Sections(md);
|
|
1217
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1218
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1219
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1220
|
+
const startLine = brSection?.startLine ?? 1;
|
|
1221
|
+
const brs = [];
|
|
1222
|
+
const brsWithoutPriority = [];
|
|
1223
|
+
const brsWithInvalidPriority = [];
|
|
1224
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
1225
|
+
const lineText = brLines[i] ?? "";
|
|
1226
|
+
const lineNumber = startLine + i;
|
|
1227
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
1228
|
+
if (validMatch) {
|
|
1229
|
+
const id = validMatch[1];
|
|
1230
|
+
const priority = validMatch[2];
|
|
1231
|
+
const text = validMatch[3];
|
|
1232
|
+
if (!id || !priority || !text) continue;
|
|
1233
|
+
brs.push({
|
|
1234
|
+
id,
|
|
1235
|
+
priority,
|
|
1236
|
+
text: text.trim(),
|
|
1237
|
+
line: lineNumber
|
|
1238
|
+
});
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1242
|
+
if (anyPriorityMatch) {
|
|
1243
|
+
const id = anyPriorityMatch[1];
|
|
1244
|
+
const priority = anyPriorityMatch[2];
|
|
1245
|
+
const text = anyPriorityMatch[3];
|
|
1246
|
+
if (!id || !priority || !text) continue;
|
|
1247
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
1248
|
+
brsWithInvalidPriority.push({
|
|
1249
|
+
id,
|
|
1250
|
+
priority,
|
|
1251
|
+
text: text.trim(),
|
|
1252
|
+
line: lineNumber
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1258
|
+
if (noPriorityMatch) {
|
|
1259
|
+
const id = noPriorityMatch[1];
|
|
1260
|
+
const text = noPriorityMatch[2];
|
|
1261
|
+
if (!id || !text) continue;
|
|
1262
|
+
brsWithoutPriority.push({
|
|
1263
|
+
id,
|
|
1264
|
+
text: text.trim(),
|
|
1265
|
+
line: lineNumber
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
const parsed = {
|
|
1270
|
+
file,
|
|
1271
|
+
sections: sectionNames,
|
|
1272
|
+
brs,
|
|
1273
|
+
brsWithoutPriority,
|
|
1274
|
+
brsWithInvalidPriority
|
|
1275
|
+
};
|
|
1276
|
+
if (specId) {
|
|
1277
|
+
parsed.specId = specId;
|
|
1278
|
+
}
|
|
1279
|
+
return parsed;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1048
1282
|
// src/core/validators/ids.ts
|
|
1283
|
+
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1049
1284
|
async function validateDefinedIds(root, config) {
|
|
1050
1285
|
const issues = [];
|
|
1051
1286
|
const specRoot = resolvePath(root, config, "specDir");
|
|
@@ -1069,7 +1304,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1069
1304
|
}
|
|
1070
1305
|
const sorted = Array.from(files).sort();
|
|
1071
1306
|
issues.push(
|
|
1072
|
-
|
|
1307
|
+
issue3(
|
|
1073
1308
|
"QFAI-ID-001",
|
|
1074
1309
|
`ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
|
|
1075
1310
|
"error",
|
|
@@ -1082,15 +1317,25 @@ async function validateDefinedIds(root, config) {
|
|
|
1082
1317
|
}
|
|
1083
1318
|
async function collectSpecDefinitionIds(files, out) {
|
|
1084
1319
|
for (const file of files) {
|
|
1085
|
-
const text = await
|
|
1086
|
-
|
|
1087
|
-
|
|
1320
|
+
const text = await readFile6(file, "utf-8");
|
|
1321
|
+
const parsed = parseSpec(text, file);
|
|
1322
|
+
if (parsed.specId) {
|
|
1323
|
+
recordId(out, parsed.specId, file);
|
|
1324
|
+
}
|
|
1325
|
+
parsed.brs.forEach((br) => recordId(out, br.id, file));
|
|
1088
1326
|
}
|
|
1089
1327
|
}
|
|
1090
1328
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1091
1329
|
for (const file of files) {
|
|
1092
|
-
const text = await
|
|
1093
|
-
|
|
1330
|
+
const text = await readFile6(file, "utf-8");
|
|
1331
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1332
|
+
for (const scenario of parsed.scenarios) {
|
|
1333
|
+
for (const tag of scenario.tags) {
|
|
1334
|
+
if (SC_TAG_RE.test(tag)) {
|
|
1335
|
+
recordId(out, tag, file);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1094
1339
|
}
|
|
1095
1340
|
}
|
|
1096
1341
|
function recordId(out, id, file) {
|
|
@@ -1104,29 +1349,32 @@ function formatFileList(files, root) {
|
|
|
1104
1349
|
return relative.length > 0 ? relative : file;
|
|
1105
1350
|
}).join(", ");
|
|
1106
1351
|
}
|
|
1107
|
-
function
|
|
1108
|
-
const
|
|
1352
|
+
function issue3(code, message, severity, file, rule, refs) {
|
|
1353
|
+
const issue7 = {
|
|
1109
1354
|
code,
|
|
1110
1355
|
severity,
|
|
1111
1356
|
message
|
|
1112
1357
|
};
|
|
1113
1358
|
if (file) {
|
|
1114
|
-
|
|
1359
|
+
issue7.file = file;
|
|
1115
1360
|
}
|
|
1116
1361
|
if (rule) {
|
|
1117
|
-
|
|
1362
|
+
issue7.rule = rule;
|
|
1118
1363
|
}
|
|
1119
1364
|
if (refs && refs.length > 0) {
|
|
1120
|
-
|
|
1365
|
+
issue7.refs = refs;
|
|
1121
1366
|
}
|
|
1122
|
-
return
|
|
1367
|
+
return issue7;
|
|
1123
1368
|
}
|
|
1124
1369
|
|
|
1125
1370
|
// src/core/validators/scenario.ts
|
|
1126
|
-
import { readFile as
|
|
1371
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1127
1372
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1128
1373
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1129
1374
|
var THEN_PATTERN = /\bThen\b/;
|
|
1375
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1376
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1377
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1130
1378
|
async function validateScenarios(root, config) {
|
|
1131
1379
|
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1132
1380
|
const files = await collectFiles(scenariosRoot, {
|
|
@@ -1134,7 +1382,7 @@ async function validateScenarios(root, config) {
|
|
|
1134
1382
|
});
|
|
1135
1383
|
if (files.length === 0) {
|
|
1136
1384
|
return [
|
|
1137
|
-
|
|
1385
|
+
issue4(
|
|
1138
1386
|
"QFAI-SC-000",
|
|
1139
1387
|
"Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1140
1388
|
"info",
|
|
@@ -1145,13 +1393,14 @@ async function validateScenarios(root, config) {
|
|
|
1145
1393
|
}
|
|
1146
1394
|
const issues = [];
|
|
1147
1395
|
for (const file of files) {
|
|
1148
|
-
const text = await
|
|
1396
|
+
const text = await readFile7(file, "utf-8");
|
|
1149
1397
|
issues.push(...validateScenarioContent(text, file));
|
|
1150
1398
|
}
|
|
1151
1399
|
return issues;
|
|
1152
1400
|
}
|
|
1153
1401
|
function validateScenarioContent(text, file) {
|
|
1154
1402
|
const issues = [];
|
|
1403
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1155
1404
|
const invalidIds = extractInvalidIds(text, [
|
|
1156
1405
|
"SPEC",
|
|
1157
1406
|
"BR",
|
|
@@ -1163,7 +1412,7 @@ function validateScenarioContent(text, file) {
|
|
|
1163
1412
|
]);
|
|
1164
1413
|
if (invalidIds.length > 0) {
|
|
1165
1414
|
issues.push(
|
|
1166
|
-
|
|
1415
|
+
issue4(
|
|
1167
1416
|
"QFAI-ID-002",
|
|
1168
1417
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
1169
1418
|
"error",
|
|
@@ -1173,41 +1422,56 @@ function validateScenarioContent(text, file) {
|
|
|
1173
1422
|
)
|
|
1174
1423
|
);
|
|
1175
1424
|
}
|
|
1176
|
-
const
|
|
1177
|
-
if (
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
"QFAI-SC-001",
|
|
1181
|
-
"SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1182
|
-
"error",
|
|
1183
|
-
file,
|
|
1184
|
-
"scenario.id"
|
|
1185
|
-
)
|
|
1186
|
-
);
|
|
1187
|
-
}
|
|
1188
|
-
const specIds = extractIds(text, "SPEC");
|
|
1189
|
-
if (specIds.length === 0) {
|
|
1425
|
+
const missingStructure = [];
|
|
1426
|
+
if (!parsed.featurePresent) missingStructure.push("Feature");
|
|
1427
|
+
if (parsed.scenarios.length === 0) missingStructure.push("Scenario");
|
|
1428
|
+
if (missingStructure.length > 0) {
|
|
1190
1429
|
issues.push(
|
|
1191
|
-
|
|
1192
|
-
"QFAI-SC-
|
|
1193
|
-
|
|
1430
|
+
issue4(
|
|
1431
|
+
"QFAI-SC-006",
|
|
1432
|
+
`Scenario \u30D5\u30A1\u30A4\u30EB\u306B\u5FC5\u8981\u306A\u69CB\u9020\u304C\u3042\u308A\u307E\u305B\u3093: ${missingStructure.join(
|
|
1433
|
+
", "
|
|
1434
|
+
)}`,
|
|
1194
1435
|
"error",
|
|
1195
1436
|
file,
|
|
1196
|
-
"scenario.
|
|
1437
|
+
"scenario.structure"
|
|
1197
1438
|
)
|
|
1198
1439
|
);
|
|
1199
1440
|
}
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1441
|
+
for (const scenario of parsed.scenarios) {
|
|
1442
|
+
if (scenario.tags.length === 0) {
|
|
1443
|
+
issues.push(
|
|
1444
|
+
issue4(
|
|
1445
|
+
"QFAI-SC-007",
|
|
1446
|
+
`Scenario \u30BF\u30B0\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${scenario.name}`,
|
|
1447
|
+
"error",
|
|
1448
|
+
file,
|
|
1449
|
+
"scenario.tags"
|
|
1450
|
+
)
|
|
1451
|
+
);
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
const missingTags = [];
|
|
1455
|
+
if (!scenario.tags.some((tag) => SC_TAG_RE2.test(tag))) {
|
|
1456
|
+
missingTags.push("SC");
|
|
1457
|
+
}
|
|
1458
|
+
if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
|
|
1459
|
+
missingTags.push("SPEC");
|
|
1460
|
+
}
|
|
1461
|
+
if (!scenario.tags.some((tag) => BR_TAG_RE.test(tag))) {
|
|
1462
|
+
missingTags.push("BR");
|
|
1463
|
+
}
|
|
1464
|
+
if (missingTags.length > 0) {
|
|
1465
|
+
issues.push(
|
|
1466
|
+
issue4(
|
|
1467
|
+
"QFAI-SC-008",
|
|
1468
|
+
`Scenario \u30BF\u30B0\u306B\u4E0D\u8DB3\u304C\u3042\u308A\u307E\u3059: ${missingTags.join(", ")} (${scenario.name})`,
|
|
1469
|
+
"error",
|
|
1470
|
+
file,
|
|
1471
|
+
"scenario.tagIds"
|
|
1472
|
+
)
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1211
1475
|
}
|
|
1212
1476
|
const missingSteps = [];
|
|
1213
1477
|
if (!GIVEN_PATTERN.test(text)) {
|
|
@@ -1221,7 +1485,7 @@ function validateScenarioContent(text, file) {
|
|
|
1221
1485
|
}
|
|
1222
1486
|
if (missingSteps.length > 0) {
|
|
1223
1487
|
issues.push(
|
|
1224
|
-
|
|
1488
|
+
issue4(
|
|
1225
1489
|
"QFAI-SC-005",
|
|
1226
1490
|
`Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
|
|
1227
1491
|
"warning",
|
|
@@ -1232,33 +1496,33 @@ function validateScenarioContent(text, file) {
|
|
|
1232
1496
|
}
|
|
1233
1497
|
return issues;
|
|
1234
1498
|
}
|
|
1235
|
-
function
|
|
1236
|
-
const
|
|
1499
|
+
function issue4(code, message, severity, file, rule, refs) {
|
|
1500
|
+
const issue7 = {
|
|
1237
1501
|
code,
|
|
1238
1502
|
severity,
|
|
1239
1503
|
message
|
|
1240
1504
|
};
|
|
1241
1505
|
if (file) {
|
|
1242
|
-
|
|
1506
|
+
issue7.file = file;
|
|
1243
1507
|
}
|
|
1244
1508
|
if (rule) {
|
|
1245
|
-
|
|
1509
|
+
issue7.rule = rule;
|
|
1246
1510
|
}
|
|
1247
1511
|
if (refs && refs.length > 0) {
|
|
1248
|
-
|
|
1512
|
+
issue7.refs = refs;
|
|
1249
1513
|
}
|
|
1250
|
-
return
|
|
1514
|
+
return issue7;
|
|
1251
1515
|
}
|
|
1252
1516
|
|
|
1253
1517
|
// src/core/validators/spec.ts
|
|
1254
|
-
import { readFile as
|
|
1518
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1255
1519
|
async function validateSpecs(root, config) {
|
|
1256
1520
|
const specsRoot = resolvePath(root, config, "specDir");
|
|
1257
1521
|
const files = await collectSpecFiles(specsRoot);
|
|
1258
1522
|
if (files.length === 0) {
|
|
1259
1523
|
const expected = "spec-0001-<slug>.md";
|
|
1260
1524
|
return [
|
|
1261
|
-
|
|
1525
|
+
issue5(
|
|
1262
1526
|
"QFAI-SPEC-000",
|
|
1263
1527
|
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}`,
|
|
1264
1528
|
"info",
|
|
@@ -1269,7 +1533,7 @@ async function validateSpecs(root, config) {
|
|
|
1269
1533
|
}
|
|
1270
1534
|
const issues = [];
|
|
1271
1535
|
for (const file of files) {
|
|
1272
|
-
const text = await
|
|
1536
|
+
const text = await readFile8(file, "utf-8");
|
|
1273
1537
|
issues.push(
|
|
1274
1538
|
...validateSpecContent(
|
|
1275
1539
|
text,
|
|
@@ -1282,6 +1546,7 @@ async function validateSpecs(root, config) {
|
|
|
1282
1546
|
}
|
|
1283
1547
|
function validateSpecContent(text, file, requiredSections) {
|
|
1284
1548
|
const issues = [];
|
|
1549
|
+
const parsed = parseSpec(text, file);
|
|
1285
1550
|
const invalidIds = extractInvalidIds(text, [
|
|
1286
1551
|
"SPEC",
|
|
1287
1552
|
"BR",
|
|
@@ -1293,7 +1558,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1293
1558
|
]);
|
|
1294
1559
|
if (invalidIds.length > 0) {
|
|
1295
1560
|
issues.push(
|
|
1296
|
-
|
|
1561
|
+
issue5(
|
|
1297
1562
|
"QFAI-ID-002",
|
|
1298
1563
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
1299
1564
|
"error",
|
|
@@ -1303,10 +1568,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1303
1568
|
)
|
|
1304
1569
|
);
|
|
1305
1570
|
}
|
|
1306
|
-
|
|
1307
|
-
if (specIds.length === 0) {
|
|
1571
|
+
if (!parsed.specId) {
|
|
1308
1572
|
issues.push(
|
|
1309
|
-
|
|
1573
|
+
issue5(
|
|
1310
1574
|
"QFAI-SPEC-001",
|
|
1311
1575
|
"SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1312
1576
|
"error",
|
|
@@ -1315,10 +1579,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1315
1579
|
)
|
|
1316
1580
|
);
|
|
1317
1581
|
}
|
|
1318
|
-
|
|
1319
|
-
if (brIds.length === 0) {
|
|
1582
|
+
if (parsed.brs.length === 0) {
|
|
1320
1583
|
issues.push(
|
|
1321
|
-
|
|
1584
|
+
issue5(
|
|
1322
1585
|
"QFAI-SPEC-002",
|
|
1323
1586
|
"BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1324
1587
|
"error",
|
|
@@ -1327,10 +1590,34 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1327
1590
|
)
|
|
1328
1591
|
);
|
|
1329
1592
|
}
|
|
1593
|
+
for (const br of parsed.brsWithoutPriority) {
|
|
1594
|
+
issues.push(
|
|
1595
|
+
issue5(
|
|
1596
|
+
"QFAI-BR-001",
|
|
1597
|
+
`BR \u884C\u306B Priority \u304C\u3042\u308A\u307E\u305B\u3093: ${br.id}`,
|
|
1598
|
+
"error",
|
|
1599
|
+
file,
|
|
1600
|
+
"spec.brPriority",
|
|
1601
|
+
[br.id]
|
|
1602
|
+
)
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
for (const br of parsed.brsWithInvalidPriority) {
|
|
1606
|
+
issues.push(
|
|
1607
|
+
issue5(
|
|
1608
|
+
"QFAI-BR-002",
|
|
1609
|
+
`BR Priority \u304C\u4E0D\u6B63\u3067\u3059: ${br.id} (${br.priority})`,
|
|
1610
|
+
"error",
|
|
1611
|
+
file,
|
|
1612
|
+
"spec.brPriority",
|
|
1613
|
+
[br.id]
|
|
1614
|
+
)
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1330
1617
|
const scIds = extractIds(text, "SC");
|
|
1331
1618
|
if (scIds.length > 0) {
|
|
1332
1619
|
issues.push(
|
|
1333
|
-
|
|
1620
|
+
issue5(
|
|
1334
1621
|
"QFAI-SPEC-003",
|
|
1335
1622
|
"Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
|
|
1336
1623
|
"warning",
|
|
@@ -1341,9 +1628,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1341
1628
|
);
|
|
1342
1629
|
}
|
|
1343
1630
|
for (const section of requiredSections) {
|
|
1344
|
-
if (!
|
|
1631
|
+
if (!parsed.sections.has(section)) {
|
|
1345
1632
|
issues.push(
|
|
1346
|
-
|
|
1633
|
+
issue5(
|
|
1347
1634
|
"QFAI-SPEC-004",
|
|
1348
1635
|
`\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
|
|
1349
1636
|
"error",
|
|
@@ -1355,26 +1642,32 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1355
1642
|
}
|
|
1356
1643
|
return issues;
|
|
1357
1644
|
}
|
|
1358
|
-
function
|
|
1359
|
-
const
|
|
1645
|
+
function issue5(code, message, severity, file, rule, refs) {
|
|
1646
|
+
const issue7 = {
|
|
1360
1647
|
code,
|
|
1361
1648
|
severity,
|
|
1362
1649
|
message
|
|
1363
1650
|
};
|
|
1364
1651
|
if (file) {
|
|
1365
|
-
|
|
1652
|
+
issue7.file = file;
|
|
1366
1653
|
}
|
|
1367
1654
|
if (rule) {
|
|
1368
|
-
|
|
1655
|
+
issue7.rule = rule;
|
|
1369
1656
|
}
|
|
1370
1657
|
if (refs && refs.length > 0) {
|
|
1371
|
-
|
|
1658
|
+
issue7.refs = refs;
|
|
1372
1659
|
}
|
|
1373
|
-
return
|
|
1660
|
+
return issue7;
|
|
1374
1661
|
}
|
|
1375
1662
|
|
|
1376
1663
|
// src/core/validators/traceability.ts
|
|
1377
|
-
import { readFile as
|
|
1664
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1665
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1666
|
+
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1667
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1668
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1669
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
1670
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1378
1671
|
async function validateTraceability(root, config) {
|
|
1379
1672
|
const issues = [];
|
|
1380
1673
|
const specsRoot = resolvePath(root, config, "specDir");
|
|
@@ -1400,11 +1693,13 @@ async function validateTraceability(root, config) {
|
|
|
1400
1693
|
const contractIndex = await buildContractIndex(root, config);
|
|
1401
1694
|
const contractIds = contractIndex.ids;
|
|
1402
1695
|
for (const file of specFiles) {
|
|
1403
|
-
const text = await
|
|
1696
|
+
const text = await readFile9(file, "utf-8");
|
|
1404
1697
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1405
|
-
const
|
|
1406
|
-
|
|
1407
|
-
|
|
1698
|
+
const parsed = parseSpec(text, file);
|
|
1699
|
+
if (parsed.specId) {
|
|
1700
|
+
specIds.add(parsed.specId);
|
|
1701
|
+
}
|
|
1702
|
+
const brIds = parsed.brs.map((br) => br.id);
|
|
1408
1703
|
brIds.forEach((id) => brIdsInSpecs.add(id));
|
|
1409
1704
|
const referencedContractIds = /* @__PURE__ */ new Set([
|
|
1410
1705
|
...extractIds(text, "UI"),
|
|
@@ -1416,7 +1711,7 @@ async function validateTraceability(root, config) {
|
|
|
1416
1711
|
);
|
|
1417
1712
|
if (unknownContractIds.length > 0) {
|
|
1418
1713
|
issues.push(
|
|
1419
|
-
|
|
1714
|
+
issue6(
|
|
1420
1715
|
"QFAI-TRACE-009",
|
|
1421
1716
|
`Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1422
1717
|
", "
|
|
@@ -1428,37 +1723,54 @@ async function validateTraceability(root, config) {
|
|
|
1428
1723
|
)
|
|
1429
1724
|
);
|
|
1430
1725
|
}
|
|
1431
|
-
|
|
1432
|
-
const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
|
|
1726
|
+
if (parsed.specId) {
|
|
1727
|
+
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
1433
1728
|
brIds.forEach((id) => current.add(id));
|
|
1434
|
-
specToBrIds.set(specId, current);
|
|
1729
|
+
specToBrIds.set(parsed.specId, current);
|
|
1435
1730
|
}
|
|
1436
1731
|
}
|
|
1437
1732
|
for (const file of decisionFiles) {
|
|
1438
|
-
const text = await
|
|
1733
|
+
const text = await readFile9(file, "utf-8");
|
|
1439
1734
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1440
1735
|
}
|
|
1441
1736
|
for (const file of scenarioFiles) {
|
|
1442
|
-
const text = await
|
|
1737
|
+
const text = await readFile9(file, "utf-8");
|
|
1443
1738
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1444
|
-
const
|
|
1445
|
-
const
|
|
1446
|
-
const
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1739
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1740
|
+
const specIdsInScenario = /* @__PURE__ */ new Set();
|
|
1741
|
+
const brIds = /* @__PURE__ */ new Set();
|
|
1742
|
+
const scIds = /* @__PURE__ */ new Set();
|
|
1743
|
+
const scenarioIds = /* @__PURE__ */ new Set();
|
|
1744
|
+
for (const scenario of parsed.scenarios) {
|
|
1745
|
+
for (const tag of scenario.tags) {
|
|
1746
|
+
if (SPEC_TAG_RE2.test(tag)) {
|
|
1747
|
+
specIdsInScenario.add(tag);
|
|
1748
|
+
}
|
|
1749
|
+
if (BR_TAG_RE2.test(tag)) {
|
|
1750
|
+
brIds.add(tag);
|
|
1751
|
+
}
|
|
1752
|
+
if (SC_TAG_RE3.test(tag)) {
|
|
1753
|
+
scIds.add(tag);
|
|
1754
|
+
}
|
|
1755
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1756
|
+
scenarioIds.add(tag);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
const specIdsList = Array.from(specIdsInScenario);
|
|
1761
|
+
const brIdsList = Array.from(brIds);
|
|
1762
|
+
const scIdsList = Array.from(scIds);
|
|
1763
|
+
const scenarioIdsList = Array.from(scenarioIds);
|
|
1764
|
+
brIdsList.forEach((id) => brIdsInScenarios.add(id));
|
|
1765
|
+
scIdsList.forEach((id) => scIdsInScenarios.add(id));
|
|
1766
|
+
scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
|
|
1767
|
+
if (scenarioIdsList.length > 0) {
|
|
1768
|
+
scIdsList.forEach((id) => scWithContracts.add(id));
|
|
1457
1769
|
}
|
|
1458
|
-
const unknownSpecIds =
|
|
1770
|
+
const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
|
|
1459
1771
|
if (unknownSpecIds.length > 0) {
|
|
1460
1772
|
issues.push(
|
|
1461
|
-
|
|
1773
|
+
issue6(
|
|
1462
1774
|
"QFAI-TRACE-005",
|
|
1463
1775
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1464
1776
|
"error",
|
|
@@ -1468,10 +1780,10 @@ async function validateTraceability(root, config) {
|
|
|
1468
1780
|
)
|
|
1469
1781
|
);
|
|
1470
1782
|
}
|
|
1471
|
-
const unknownBrIds =
|
|
1783
|
+
const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
|
|
1472
1784
|
if (unknownBrIds.length > 0) {
|
|
1473
1785
|
issues.push(
|
|
1474
|
-
|
|
1786
|
+
issue6(
|
|
1475
1787
|
"QFAI-TRACE-006",
|
|
1476
1788
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1477
1789
|
"error",
|
|
@@ -1481,10 +1793,12 @@ async function validateTraceability(root, config) {
|
|
|
1481
1793
|
)
|
|
1482
1794
|
);
|
|
1483
1795
|
}
|
|
1484
|
-
const unknownContractIds =
|
|
1796
|
+
const unknownContractIds = scenarioIdsList.filter(
|
|
1797
|
+
(id) => !contractIds.has(id)
|
|
1798
|
+
);
|
|
1485
1799
|
if (unknownContractIds.length > 0) {
|
|
1486
1800
|
issues.push(
|
|
1487
|
-
|
|
1801
|
+
issue6(
|
|
1488
1802
|
"QFAI-TRACE-008",
|
|
1489
1803
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1490
1804
|
", "
|
|
@@ -1496,23 +1810,23 @@ async function validateTraceability(root, config) {
|
|
|
1496
1810
|
)
|
|
1497
1811
|
);
|
|
1498
1812
|
}
|
|
1499
|
-
if (
|
|
1813
|
+
if (specIdsList.length > 0) {
|
|
1500
1814
|
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
1501
|
-
for (const specId of
|
|
1815
|
+
for (const specId of specIdsList) {
|
|
1502
1816
|
const brIdsForSpec = specToBrIds.get(specId);
|
|
1503
1817
|
if (!brIdsForSpec) {
|
|
1504
1818
|
continue;
|
|
1505
1819
|
}
|
|
1506
1820
|
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
1507
1821
|
}
|
|
1508
|
-
const invalidBrIds =
|
|
1822
|
+
const invalidBrIds = brIdsList.filter((id) => !allowedBrIds.has(id));
|
|
1509
1823
|
if (invalidBrIds.length > 0) {
|
|
1510
1824
|
issues.push(
|
|
1511
|
-
|
|
1825
|
+
issue6(
|
|
1512
1826
|
"QFAI-TRACE-007",
|
|
1513
1827
|
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
1514
1828
|
", "
|
|
1515
|
-
)} (SPEC: ${
|
|
1829
|
+
)} (SPEC: ${specIdsList.join(", ")})`,
|
|
1516
1830
|
"error",
|
|
1517
1831
|
file,
|
|
1518
1832
|
"traceability.scenarioBrUnderSpec",
|
|
@@ -1524,7 +1838,7 @@ async function validateTraceability(root, config) {
|
|
|
1524
1838
|
}
|
|
1525
1839
|
if (upstreamIds.size === 0) {
|
|
1526
1840
|
return [
|
|
1527
|
-
|
|
1841
|
+
issue6(
|
|
1528
1842
|
"QFAI-TRACE-000",
|
|
1529
1843
|
"\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1530
1844
|
"info",
|
|
@@ -1539,7 +1853,7 @@ async function validateTraceability(root, config) {
|
|
|
1539
1853
|
);
|
|
1540
1854
|
if (orphanBrIds.length > 0) {
|
|
1541
1855
|
issues.push(
|
|
1542
|
-
|
|
1856
|
+
issue6(
|
|
1543
1857
|
"QFAI_TRACE_BR_ORPHAN",
|
|
1544
1858
|
`BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
|
|
1545
1859
|
"error",
|
|
@@ -1556,7 +1870,7 @@ async function validateTraceability(root, config) {
|
|
|
1556
1870
|
);
|
|
1557
1871
|
if (scWithoutContracts.length > 0) {
|
|
1558
1872
|
issues.push(
|
|
1559
|
-
|
|
1873
|
+
issue6(
|
|
1560
1874
|
"QFAI_TRACE_SC_NO_CONTRACT",
|
|
1561
1875
|
`SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
|
|
1562
1876
|
", "
|
|
@@ -1576,7 +1890,7 @@ async function validateTraceability(root, config) {
|
|
|
1576
1890
|
);
|
|
1577
1891
|
if (orphanContracts.length > 0) {
|
|
1578
1892
|
issues.push(
|
|
1579
|
-
|
|
1893
|
+
issue6(
|
|
1580
1894
|
"QFAI_CONTRACT_ORPHAN",
|
|
1581
1895
|
`\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
1582
1896
|
"error",
|
|
@@ -1604,7 +1918,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1604
1918
|
const targetFiles = [...codeFiles, ...testFiles];
|
|
1605
1919
|
if (targetFiles.length === 0) {
|
|
1606
1920
|
issues.push(
|
|
1607
|
-
|
|
1921
|
+
issue6(
|
|
1608
1922
|
"QFAI-TRACE-001",
|
|
1609
1923
|
"\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1610
1924
|
"info",
|
|
@@ -1617,7 +1931,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1617
1931
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
1618
1932
|
let found = false;
|
|
1619
1933
|
for (const file of targetFiles) {
|
|
1620
|
-
const text = await
|
|
1934
|
+
const text = await readFile9(file, "utf-8");
|
|
1621
1935
|
if (pattern.test(text)) {
|
|
1622
1936
|
found = true;
|
|
1623
1937
|
break;
|
|
@@ -1625,7 +1939,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1625
1939
|
}
|
|
1626
1940
|
if (!found) {
|
|
1627
1941
|
issues.push(
|
|
1628
|
-
|
|
1942
|
+
issue6(
|
|
1629
1943
|
"QFAI-TRACE-002",
|
|
1630
1944
|
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
1631
1945
|
"warning",
|
|
@@ -1640,22 +1954,22 @@ function buildIdPattern(ids) {
|
|
|
1640
1954
|
const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
1641
1955
|
return new RegExp(`\\b(${escaped.join("|")})\\b`);
|
|
1642
1956
|
}
|
|
1643
|
-
function
|
|
1644
|
-
const
|
|
1957
|
+
function issue6(code, message, severity, file, rule, refs) {
|
|
1958
|
+
const issue7 = {
|
|
1645
1959
|
code,
|
|
1646
1960
|
severity,
|
|
1647
1961
|
message
|
|
1648
1962
|
};
|
|
1649
1963
|
if (file) {
|
|
1650
|
-
|
|
1964
|
+
issue7.file = file;
|
|
1651
1965
|
}
|
|
1652
1966
|
if (rule) {
|
|
1653
|
-
|
|
1967
|
+
issue7.rule = rule;
|
|
1654
1968
|
}
|
|
1655
1969
|
if (refs && refs.length > 0) {
|
|
1656
|
-
|
|
1970
|
+
issue7.refs = refs;
|
|
1657
1971
|
}
|
|
1658
|
-
return
|
|
1972
|
+
return issue7;
|
|
1659
1973
|
}
|
|
1660
1974
|
|
|
1661
1975
|
// src/core/validate.ts
|
|
@@ -1666,6 +1980,7 @@ async function validateProject(root, configResult) {
|
|
|
1666
1980
|
...configIssues,
|
|
1667
1981
|
...await validateSpecs(root, config),
|
|
1668
1982
|
...await validateScenarios(root, config),
|
|
1983
|
+
...await validateDecisions(root, config),
|
|
1669
1984
|
...await validateContracts(root, config),
|
|
1670
1985
|
...await validateDefinedIds(root, config),
|
|
1671
1986
|
...await validateTraceability(root, config)
|
|
@@ -1680,8 +1995,8 @@ async function validateProject(root, configResult) {
|
|
|
1680
1995
|
}
|
|
1681
1996
|
function countIssues(issues) {
|
|
1682
1997
|
return issues.reduce(
|
|
1683
|
-
(acc,
|
|
1684
|
-
acc[
|
|
1998
|
+
(acc, issue7) => {
|
|
1999
|
+
acc[issue7.severity] += 1;
|
|
1685
2000
|
return acc;
|
|
1686
2001
|
},
|
|
1687
2002
|
{ info: 0, warning: 0, error: 0 }
|
|
@@ -1852,7 +2167,7 @@ async function collectIds(files) {
|
|
|
1852
2167
|
DATA: /* @__PURE__ */ new Set()
|
|
1853
2168
|
};
|
|
1854
2169
|
for (const file of files) {
|
|
1855
|
-
const text = await
|
|
2170
|
+
const text = await readFile10(file, "utf-8");
|
|
1856
2171
|
for (const prefix of ID_PREFIXES2) {
|
|
1857
2172
|
const ids = extractIds(text, prefix);
|
|
1858
2173
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -1870,7 +2185,7 @@ async function collectIds(files) {
|
|
|
1870
2185
|
async function collectUpstreamIds(files) {
|
|
1871
2186
|
const ids = /* @__PURE__ */ new Set();
|
|
1872
2187
|
for (const file of files) {
|
|
1873
|
-
const text = await
|
|
2188
|
+
const text = await readFile10(file, "utf-8");
|
|
1874
2189
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
1875
2190
|
}
|
|
1876
2191
|
return ids;
|
|
@@ -1891,7 +2206,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
1891
2206
|
}
|
|
1892
2207
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
1893
2208
|
for (const file of targetFiles) {
|
|
1894
|
-
const text = await
|
|
2209
|
+
const text = await readFile10(file, "utf-8");
|
|
1895
2210
|
if (pattern.test(text)) {
|
|
1896
2211
|
return true;
|
|
1897
2212
|
}
|
|
@@ -1913,20 +2228,20 @@ function toSortedArray(values) {
|
|
|
1913
2228
|
}
|
|
1914
2229
|
function buildHotspots(issues) {
|
|
1915
2230
|
const map = /* @__PURE__ */ new Map();
|
|
1916
|
-
for (const
|
|
1917
|
-
if (!
|
|
2231
|
+
for (const issue7 of issues) {
|
|
2232
|
+
if (!issue7.file) {
|
|
1918
2233
|
continue;
|
|
1919
2234
|
}
|
|
1920
|
-
const current = map.get(
|
|
1921
|
-
file:
|
|
2235
|
+
const current = map.get(issue7.file) ?? {
|
|
2236
|
+
file: issue7.file,
|
|
1922
2237
|
total: 0,
|
|
1923
2238
|
error: 0,
|
|
1924
2239
|
warning: 0,
|
|
1925
2240
|
info: 0
|
|
1926
2241
|
};
|
|
1927
2242
|
current.total += 1;
|
|
1928
|
-
current[
|
|
1929
|
-
map.set(
|
|
2243
|
+
current[issue7.severity] += 1;
|
|
2244
|
+
map.set(issue7.file, current);
|
|
1930
2245
|
}
|
|
1931
2246
|
return Array.from(map.values()).sort(
|
|
1932
2247
|
(a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
|
|
@@ -1973,7 +2288,7 @@ async function runReport(options) {
|
|
|
1973
2288
|
info(`wrote report: ${outPath}`);
|
|
1974
2289
|
}
|
|
1975
2290
|
async function readValidationResult(inputPath) {
|
|
1976
|
-
const raw = await
|
|
2291
|
+
const raw = await readFile11(inputPath, "utf-8");
|
|
1977
2292
|
const parsed = JSON.parse(raw);
|
|
1978
2293
|
if (!isValidationResult(parsed)) {
|
|
1979
2294
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
@@ -2074,14 +2389,14 @@ function emitText(result) {
|
|
|
2074
2389
|
`
|
|
2075
2390
|
);
|
|
2076
2391
|
}
|
|
2077
|
-
function emitGitHub(
|
|
2078
|
-
const level =
|
|
2079
|
-
const file =
|
|
2080
|
-
const line =
|
|
2081
|
-
const column =
|
|
2392
|
+
function emitGitHub(issue7) {
|
|
2393
|
+
const level = issue7.severity === "error" ? "error" : issue7.severity === "warning" ? "warning" : "notice";
|
|
2394
|
+
const file = issue7.file ? `file=${issue7.file}` : "";
|
|
2395
|
+
const line = issue7.loc?.line ? `,line=${issue7.loc.line}` : "";
|
|
2396
|
+
const column = issue7.loc?.column ? `,col=${issue7.loc.column}` : "";
|
|
2082
2397
|
const location = file ? ` ${file}${line}${column}` : "";
|
|
2083
2398
|
process.stdout.write(
|
|
2084
|
-
`::${level}${location}::${
|
|
2399
|
+
`::${level}${location}::${issue7.code}: ${issue7.message}
|
|
2085
2400
|
`
|
|
2086
2401
|
);
|
|
2087
2402
|
}
|