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/README.md +1 -1
- package/assets/init/.qfai/spec/README.md +5 -3
- package/assets/init/.qfai/spec/decisions/README.md +1 -0
- 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.cjs
CHANGED
|
@@ -160,7 +160,7 @@ function report(copied, skipped, dryRun, label) {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
// src/cli/commands/report.ts
|
|
163
|
-
var
|
|
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
|
|
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.
|
|
688
|
-
return "0.
|
|
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
|
|
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
|
-
|
|
1081
|
+
issue7.file = file;
|
|
998
1082
|
}
|
|
999
1083
|
if (rule) {
|
|
1000
|
-
|
|
1084
|
+
issue7.rule = rule;
|
|
1001
1085
|
}
|
|
1002
1086
|
if (refs && refs.length > 0) {
|
|
1003
|
-
|
|
1087
|
+
issue7.refs = refs;
|
|
1004
1088
|
}
|
|
1005
|
-
return
|
|
1089
|
+
return issue7;
|
|
1006
1090
|
}
|
|
1007
1091
|
|
|
1008
1092
|
// src/core/validators/ids.ts
|
|
1009
|
-
var
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
1109
|
-
|
|
1110
|
-
|
|
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,
|
|
1116
|
-
|
|
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
|
|
1131
|
-
const
|
|
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
|
-
|
|
1382
|
+
issue7.file = file;
|
|
1138
1383
|
}
|
|
1139
1384
|
if (rule) {
|
|
1140
|
-
|
|
1385
|
+
issue7.rule = rule;
|
|
1141
1386
|
}
|
|
1142
1387
|
if (refs && refs.length > 0) {
|
|
1143
|
-
|
|
1388
|
+
issue7.refs = refs;
|
|
1144
1389
|
}
|
|
1145
|
-
return
|
|
1390
|
+
return issue7;
|
|
1146
1391
|
}
|
|
1147
1392
|
|
|
1148
1393
|
// src/core/validators/scenario.ts
|
|
1149
|
-
var
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
1200
|
-
if (
|
|
1201
|
-
|
|
1202
|
-
|
|
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
|
-
|
|
1215
|
-
"QFAI-SC-
|
|
1216
|
-
|
|
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.
|
|
1460
|
+
"scenario.structure"
|
|
1220
1461
|
)
|
|
1221
1462
|
);
|
|
1222
1463
|
}
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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
|
-
|
|
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
|
|
1259
|
-
const
|
|
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
|
-
|
|
1529
|
+
issue7.file = file;
|
|
1266
1530
|
}
|
|
1267
1531
|
if (rule) {
|
|
1268
|
-
|
|
1532
|
+
issue7.rule = rule;
|
|
1269
1533
|
}
|
|
1270
1534
|
if (refs && refs.length > 0) {
|
|
1271
|
-
|
|
1535
|
+
issue7.refs = refs;
|
|
1272
1536
|
}
|
|
1273
|
-
return
|
|
1537
|
+
return issue7;
|
|
1274
1538
|
}
|
|
1275
1539
|
|
|
1276
1540
|
// src/core/validators/spec.ts
|
|
1277
|
-
var
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1330
|
-
if (specIds.length === 0) {
|
|
1594
|
+
if (!parsed.specId) {
|
|
1331
1595
|
issues.push(
|
|
1332
|
-
|
|
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
|
-
|
|
1342
|
-
if (brIds.length === 0) {
|
|
1605
|
+
if (parsed.brs.length === 0) {
|
|
1343
1606
|
issues.push(
|
|
1344
|
-
|
|
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
|
-
|
|
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 (!
|
|
1654
|
+
if (!parsed.sections.has(section)) {
|
|
1368
1655
|
issues.push(
|
|
1369
|
-
|
|
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
|
|
1382
|
-
const
|
|
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
|
-
|
|
1675
|
+
issue7.file = file;
|
|
1389
1676
|
}
|
|
1390
1677
|
if (rule) {
|
|
1391
|
-
|
|
1678
|
+
issue7.rule = rule;
|
|
1392
1679
|
}
|
|
1393
1680
|
if (refs && refs.length > 0) {
|
|
1394
|
-
|
|
1681
|
+
issue7.refs = refs;
|
|
1395
1682
|
}
|
|
1396
|
-
return
|
|
1683
|
+
return issue7;
|
|
1397
1684
|
}
|
|
1398
1685
|
|
|
1399
1686
|
// src/core/validators/traceability.ts
|
|
1400
|
-
var
|
|
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,
|
|
1719
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1427
1720
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
1760
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1466
1761
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1467
|
-
const
|
|
1468
|
-
const
|
|
1469
|
-
const
|
|
1470
|
-
const
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
1806
|
+
const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
|
|
1495
1807
|
if (unknownBrIds.length > 0) {
|
|
1496
1808
|
issues.push(
|
|
1497
|
-
|
|
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 =
|
|
1819
|
+
const unknownContractIds = scenarioIdsList.filter(
|
|
1820
|
+
(id) => !contractIds.has(id)
|
|
1821
|
+
);
|
|
1508
1822
|
if (unknownContractIds.length > 0) {
|
|
1509
1823
|
issues.push(
|
|
1510
|
-
|
|
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 (
|
|
1836
|
+
if (specIdsList.length > 0) {
|
|
1523
1837
|
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
1524
|
-
for (const specId of
|
|
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 =
|
|
1845
|
+
const invalidBrIds = brIdsList.filter((id) => !allowedBrIds.has(id));
|
|
1532
1846
|
if (invalidBrIds.length > 0) {
|
|
1533
1847
|
issues.push(
|
|
1534
|
-
|
|
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: ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
1667
|
-
const
|
|
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
|
-
|
|
1987
|
+
issue7.file = file;
|
|
1674
1988
|
}
|
|
1675
1989
|
if (rule) {
|
|
1676
|
-
|
|
1990
|
+
issue7.rule = rule;
|
|
1677
1991
|
}
|
|
1678
1992
|
if (refs && refs.length > 0) {
|
|
1679
|
-
|
|
1993
|
+
issue7.refs = refs;
|
|
1680
1994
|
}
|
|
1681
|
-
return
|
|
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,
|
|
1707
|
-
acc[
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
1940
|
-
if (!
|
|
2254
|
+
for (const issue7 of issues) {
|
|
2255
|
+
if (!issue7.file) {
|
|
1941
2256
|
continue;
|
|
1942
2257
|
}
|
|
1943
|
-
const current = map.get(
|
|
1944
|
-
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[
|
|
1952
|
-
map.set(
|
|
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,
|
|
1991
|
-
await (0,
|
|
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,
|
|
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
|
|
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(
|
|
2101
|
-
const level =
|
|
2102
|
-
const file =
|
|
2103
|
-
const line =
|
|
2104
|
-
const 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}::${
|
|
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,
|
|
2114
|
-
await (0,
|
|
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
|
|