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/index.cjs
CHANGED
|
@@ -44,6 +44,7 @@ __export(src_exports, {
|
|
|
44
44
|
resolvePath: () => resolvePath,
|
|
45
45
|
resolveToolVersion: () => resolveToolVersion,
|
|
46
46
|
validateContracts: () => validateContracts,
|
|
47
|
+
validateDecisions: () => validateDecisions,
|
|
47
48
|
validateDefinedIds: () => validateDefinedIds,
|
|
48
49
|
validateProject: () => validateProject,
|
|
49
50
|
validateScenarioContent: () => validateScenarioContent,
|
|
@@ -479,7 +480,7 @@ function isValidId(value, prefix) {
|
|
|
479
480
|
}
|
|
480
481
|
|
|
481
482
|
// src/core/report.ts
|
|
482
|
-
var
|
|
483
|
+
var import_promises11 = require("fs/promises");
|
|
483
484
|
|
|
484
485
|
// src/core/discovery.ts
|
|
485
486
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
@@ -575,8 +576,8 @@ var import_promises3 = require("fs/promises");
|
|
|
575
576
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
576
577
|
var import_node_url = require("url");
|
|
577
578
|
async function resolveToolVersion() {
|
|
578
|
-
if ("0.
|
|
579
|
-
return "0.
|
|
579
|
+
if ("0.3.0".length > 0) {
|
|
580
|
+
return "0.3.0";
|
|
580
581
|
}
|
|
581
582
|
try {
|
|
582
583
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -879,29 +880,113 @@ function formatError2(error) {
|
|
|
879
880
|
return String(error);
|
|
880
881
|
}
|
|
881
882
|
function issue(code, message, severity, file, rule, refs) {
|
|
882
|
-
const
|
|
883
|
+
const issue7 = {
|
|
884
|
+
code,
|
|
885
|
+
severity,
|
|
886
|
+
message
|
|
887
|
+
};
|
|
888
|
+
if (file) {
|
|
889
|
+
issue7.file = file;
|
|
890
|
+
}
|
|
891
|
+
if (rule) {
|
|
892
|
+
issue7.rule = rule;
|
|
893
|
+
}
|
|
894
|
+
if (refs && refs.length > 0) {
|
|
895
|
+
issue7.refs = refs;
|
|
896
|
+
}
|
|
897
|
+
return issue7;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// src/core/validators/decisions.ts
|
|
901
|
+
var import_promises5 = require("fs/promises");
|
|
902
|
+
|
|
903
|
+
// src/core/parse/adr.ts
|
|
904
|
+
var ADR_ID_RE = /\bADR-\d{4}\b/;
|
|
905
|
+
function extractField(md, key) {
|
|
906
|
+
const pattern = new RegExp(`^\\s*-\\s*${key}:\\s*(.+)\\s*$`, "m");
|
|
907
|
+
return md.match(pattern)?.[1]?.trim();
|
|
908
|
+
}
|
|
909
|
+
function parseAdr(md, file) {
|
|
910
|
+
const adrId = md.match(ADR_ID_RE)?.[0];
|
|
911
|
+
const fields = {};
|
|
912
|
+
const status = extractField(md, "Status");
|
|
913
|
+
const context = extractField(md, "Context");
|
|
914
|
+
const decision = extractField(md, "Decision");
|
|
915
|
+
const consequences = extractField(md, "Consequences");
|
|
916
|
+
const related = extractField(md, "Related");
|
|
917
|
+
if (status) fields.status = status;
|
|
918
|
+
if (context) fields.context = context;
|
|
919
|
+
if (decision) fields.decision = decision;
|
|
920
|
+
if (consequences) fields.consequences = consequences;
|
|
921
|
+
if (related) fields.related = related;
|
|
922
|
+
const parsed = {
|
|
923
|
+
file,
|
|
924
|
+
fields
|
|
925
|
+
};
|
|
926
|
+
if (adrId) {
|
|
927
|
+
parsed.adrId = adrId;
|
|
928
|
+
}
|
|
929
|
+
return parsed;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/core/validators/decisions.ts
|
|
933
|
+
var REQUIRED_FIELDS = [
|
|
934
|
+
{ key: "status", label: "Status" },
|
|
935
|
+
{ key: "context", label: "Context" },
|
|
936
|
+
{ key: "decision", label: "Decision" },
|
|
937
|
+
{ key: "consequences", label: "Consequences" }
|
|
938
|
+
];
|
|
939
|
+
async function validateDecisions(root, config) {
|
|
940
|
+
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
941
|
+
const files = await collectFiles(decisionsRoot, { extensions: [".md"] });
|
|
942
|
+
if (files.length === 0) {
|
|
943
|
+
return [];
|
|
944
|
+
}
|
|
945
|
+
const issues = [];
|
|
946
|
+
for (const file of files) {
|
|
947
|
+
const text = await (0, import_promises5.readFile)(file, "utf-8");
|
|
948
|
+
const parsed = parseAdr(text, file);
|
|
949
|
+
const missing = REQUIRED_FIELDS.filter(
|
|
950
|
+
(field) => !parsed.fields[field.key]
|
|
951
|
+
);
|
|
952
|
+
if (missing.length > 0) {
|
|
953
|
+
issues.push(
|
|
954
|
+
issue2(
|
|
955
|
+
"QFAI-ADR-001",
|
|
956
|
+
`ADR \u5FC5\u9808\u30D5\u30A3\u30FC\u30EB\u30C9\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missing.map((field) => field.label).join(", ")}`,
|
|
957
|
+
"error",
|
|
958
|
+
file,
|
|
959
|
+
"adr.requiredFields"
|
|
960
|
+
)
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return issues;
|
|
965
|
+
}
|
|
966
|
+
function issue2(code, message, severity, file, rule, refs) {
|
|
967
|
+
const issue7 = {
|
|
883
968
|
code,
|
|
884
969
|
severity,
|
|
885
970
|
message
|
|
886
971
|
};
|
|
887
972
|
if (file) {
|
|
888
|
-
|
|
973
|
+
issue7.file = file;
|
|
889
974
|
}
|
|
890
975
|
if (rule) {
|
|
891
|
-
|
|
976
|
+
issue7.rule = rule;
|
|
892
977
|
}
|
|
893
978
|
if (refs && refs.length > 0) {
|
|
894
|
-
|
|
979
|
+
issue7.refs = refs;
|
|
895
980
|
}
|
|
896
|
-
return
|
|
981
|
+
return issue7;
|
|
897
982
|
}
|
|
898
983
|
|
|
899
984
|
// src/core/validators/ids.ts
|
|
900
|
-
var
|
|
985
|
+
var import_promises7 = require("fs/promises");
|
|
901
986
|
var import_node_path6 = __toESM(require("path"), 1);
|
|
902
987
|
|
|
903
988
|
// src/core/contractIndex.ts
|
|
904
|
-
var
|
|
989
|
+
var import_promises6 = require("fs/promises");
|
|
905
990
|
async function buildContractIndex(root, config) {
|
|
906
991
|
const uiRoot = resolvePath(root, config, "uiContractsDir");
|
|
907
992
|
const apiRoot = resolvePath(root, config, "apiContractsDir");
|
|
@@ -924,7 +1009,7 @@ async function buildContractIndex(root, config) {
|
|
|
924
1009
|
}
|
|
925
1010
|
async function indexUiContracts(files, index) {
|
|
926
1011
|
for (const file of files) {
|
|
927
|
-
const text = await (0,
|
|
1012
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
928
1013
|
try {
|
|
929
1014
|
const doc = parseStructuredContract(file, text);
|
|
930
1015
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -936,7 +1021,7 @@ async function indexUiContracts(files, index) {
|
|
|
936
1021
|
}
|
|
937
1022
|
async function indexApiContracts(files, index) {
|
|
938
1023
|
for (const file of files) {
|
|
939
|
-
const text = await (0,
|
|
1024
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
940
1025
|
try {
|
|
941
1026
|
const doc = parseStructuredContract(file, text);
|
|
942
1027
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -948,7 +1033,7 @@ async function indexApiContracts(files, index) {
|
|
|
948
1033
|
}
|
|
949
1034
|
async function indexDataContracts(files, index) {
|
|
950
1035
|
for (const file of files) {
|
|
951
|
-
const text = await (0,
|
|
1036
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
952
1037
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
953
1038
|
}
|
|
954
1039
|
}
|
|
@@ -959,7 +1044,158 @@ function record(index, id, file) {
|
|
|
959
1044
|
index.idToFiles.set(id, current);
|
|
960
1045
|
}
|
|
961
1046
|
|
|
1047
|
+
// src/core/parse/gherkin.ts
|
|
1048
|
+
var FEATURE_RE = /^\s*Feature:\s+/;
|
|
1049
|
+
var SCENARIO_RE = /^\s*Scenario:\s*(.+)\s*$/;
|
|
1050
|
+
var TAG_LINE_RE = /^\s*@/;
|
|
1051
|
+
function parseTags(line) {
|
|
1052
|
+
return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
|
|
1053
|
+
}
|
|
1054
|
+
function parseGherkinFeature(text, file) {
|
|
1055
|
+
const lines = text.split(/\r?\n/);
|
|
1056
|
+
const scenarios = [];
|
|
1057
|
+
let featurePresent = false;
|
|
1058
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1059
|
+
const line = lines[i] ?? "";
|
|
1060
|
+
if (FEATURE_RE.test(line)) {
|
|
1061
|
+
featurePresent = true;
|
|
1062
|
+
}
|
|
1063
|
+
const match = line.match(SCENARIO_RE);
|
|
1064
|
+
if (!match) continue;
|
|
1065
|
+
const scenarioName = match[1];
|
|
1066
|
+
if (!scenarioName) continue;
|
|
1067
|
+
const tags = [];
|
|
1068
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
1069
|
+
const previous = lines[j] ?? "";
|
|
1070
|
+
if (previous.trim() === "") continue;
|
|
1071
|
+
if (!TAG_LINE_RE.test(previous)) break;
|
|
1072
|
+
tags.unshift(...parseTags(previous));
|
|
1073
|
+
}
|
|
1074
|
+
scenarios.push({ name: scenarioName, line: i + 1, tags });
|
|
1075
|
+
}
|
|
1076
|
+
return { file, featurePresent, scenarios };
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/core/parse/markdown.ts
|
|
1080
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1081
|
+
function parseHeadings(md) {
|
|
1082
|
+
const lines = md.split(/\r?\n/);
|
|
1083
|
+
const headings = [];
|
|
1084
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1085
|
+
const line = lines[i] ?? "";
|
|
1086
|
+
const match = line.match(HEADING_RE);
|
|
1087
|
+
if (!match) continue;
|
|
1088
|
+
const levelToken = match[1];
|
|
1089
|
+
const title = match[2];
|
|
1090
|
+
if (!levelToken || !title) continue;
|
|
1091
|
+
headings.push({
|
|
1092
|
+
level: levelToken.length,
|
|
1093
|
+
title: title.trim(),
|
|
1094
|
+
line: i + 1
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
return headings;
|
|
1098
|
+
}
|
|
1099
|
+
function extractH2Sections(md) {
|
|
1100
|
+
const lines = md.split(/\r?\n/);
|
|
1101
|
+
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1102
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1103
|
+
for (let i = 0; i < headings.length; i++) {
|
|
1104
|
+
const current = headings[i];
|
|
1105
|
+
if (!current) continue;
|
|
1106
|
+
const next = headings[i + 1];
|
|
1107
|
+
const startLine = current.line + 1;
|
|
1108
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1109
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1110
|
+
sections.set(current.title.trim(), {
|
|
1111
|
+
title: current.title.trim(),
|
|
1112
|
+
startLine,
|
|
1113
|
+
endLine,
|
|
1114
|
+
body
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
return sections;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/core/parse/spec.ts
|
|
1121
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1122
|
+
var BR_LINE_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[0-3])\)\s*(.+)$/;
|
|
1123
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[^)]+)\)\s*(.+)$/;
|
|
1124
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s+(?!\()(.*\S.*)$/;
|
|
1125
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1126
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1127
|
+
function parseSpec(md, file) {
|
|
1128
|
+
const headings = parseHeadings(md);
|
|
1129
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
1130
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1131
|
+
const sections = extractH2Sections(md);
|
|
1132
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1133
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1134
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1135
|
+
const startLine = brSection?.startLine ?? 1;
|
|
1136
|
+
const brs = [];
|
|
1137
|
+
const brsWithoutPriority = [];
|
|
1138
|
+
const brsWithInvalidPriority = [];
|
|
1139
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
1140
|
+
const lineText = brLines[i] ?? "";
|
|
1141
|
+
const lineNumber = startLine + i;
|
|
1142
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
1143
|
+
if (validMatch) {
|
|
1144
|
+
const id = validMatch[1];
|
|
1145
|
+
const priority = validMatch[2];
|
|
1146
|
+
const text = validMatch[3];
|
|
1147
|
+
if (!id || !priority || !text) continue;
|
|
1148
|
+
brs.push({
|
|
1149
|
+
id,
|
|
1150
|
+
priority,
|
|
1151
|
+
text: text.trim(),
|
|
1152
|
+
line: lineNumber
|
|
1153
|
+
});
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1157
|
+
if (anyPriorityMatch) {
|
|
1158
|
+
const id = anyPriorityMatch[1];
|
|
1159
|
+
const priority = anyPriorityMatch[2];
|
|
1160
|
+
const text = anyPriorityMatch[3];
|
|
1161
|
+
if (!id || !priority || !text) continue;
|
|
1162
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
1163
|
+
brsWithInvalidPriority.push({
|
|
1164
|
+
id,
|
|
1165
|
+
priority,
|
|
1166
|
+
text: text.trim(),
|
|
1167
|
+
line: lineNumber
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1173
|
+
if (noPriorityMatch) {
|
|
1174
|
+
const id = noPriorityMatch[1];
|
|
1175
|
+
const text = noPriorityMatch[2];
|
|
1176
|
+
if (!id || !text) continue;
|
|
1177
|
+
brsWithoutPriority.push({
|
|
1178
|
+
id,
|
|
1179
|
+
text: text.trim(),
|
|
1180
|
+
line: lineNumber
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const parsed = {
|
|
1185
|
+
file,
|
|
1186
|
+
sections: sectionNames,
|
|
1187
|
+
brs,
|
|
1188
|
+
brsWithoutPriority,
|
|
1189
|
+
brsWithInvalidPriority
|
|
1190
|
+
};
|
|
1191
|
+
if (specId) {
|
|
1192
|
+
parsed.specId = specId;
|
|
1193
|
+
}
|
|
1194
|
+
return parsed;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
962
1197
|
// src/core/validators/ids.ts
|
|
1198
|
+
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
963
1199
|
async function validateDefinedIds(root, config) {
|
|
964
1200
|
const issues = [];
|
|
965
1201
|
const specRoot = resolvePath(root, config, "specDir");
|
|
@@ -983,7 +1219,7 @@ async function validateDefinedIds(root, config) {
|
|
|
983
1219
|
}
|
|
984
1220
|
const sorted = Array.from(files).sort();
|
|
985
1221
|
issues.push(
|
|
986
|
-
|
|
1222
|
+
issue3(
|
|
987
1223
|
"QFAI-ID-001",
|
|
988
1224
|
`ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
|
|
989
1225
|
"error",
|
|
@@ -996,15 +1232,25 @@ async function validateDefinedIds(root, config) {
|
|
|
996
1232
|
}
|
|
997
1233
|
async function collectSpecDefinitionIds(files, out) {
|
|
998
1234
|
for (const file of files) {
|
|
999
|
-
const text = await (0,
|
|
1000
|
-
|
|
1001
|
-
|
|
1235
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
1236
|
+
const parsed = parseSpec(text, file);
|
|
1237
|
+
if (parsed.specId) {
|
|
1238
|
+
recordId(out, parsed.specId, file);
|
|
1239
|
+
}
|
|
1240
|
+
parsed.brs.forEach((br) => recordId(out, br.id, file));
|
|
1002
1241
|
}
|
|
1003
1242
|
}
|
|
1004
1243
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1005
1244
|
for (const file of files) {
|
|
1006
|
-
const text = await (0,
|
|
1007
|
-
|
|
1245
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
1246
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1247
|
+
for (const scenario of parsed.scenarios) {
|
|
1248
|
+
for (const tag of scenario.tags) {
|
|
1249
|
+
if (SC_TAG_RE.test(tag)) {
|
|
1250
|
+
recordId(out, tag, file);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1008
1254
|
}
|
|
1009
1255
|
}
|
|
1010
1256
|
function recordId(out, id, file) {
|
|
@@ -1018,29 +1264,32 @@ function formatFileList(files, root) {
|
|
|
1018
1264
|
return relative.length > 0 ? relative : file;
|
|
1019
1265
|
}).join(", ");
|
|
1020
1266
|
}
|
|
1021
|
-
function
|
|
1022
|
-
const
|
|
1267
|
+
function issue3(code, message, severity, file, rule, refs) {
|
|
1268
|
+
const issue7 = {
|
|
1023
1269
|
code,
|
|
1024
1270
|
severity,
|
|
1025
1271
|
message
|
|
1026
1272
|
};
|
|
1027
1273
|
if (file) {
|
|
1028
|
-
|
|
1274
|
+
issue7.file = file;
|
|
1029
1275
|
}
|
|
1030
1276
|
if (rule) {
|
|
1031
|
-
|
|
1277
|
+
issue7.rule = rule;
|
|
1032
1278
|
}
|
|
1033
1279
|
if (refs && refs.length > 0) {
|
|
1034
|
-
|
|
1280
|
+
issue7.refs = refs;
|
|
1035
1281
|
}
|
|
1036
|
-
return
|
|
1282
|
+
return issue7;
|
|
1037
1283
|
}
|
|
1038
1284
|
|
|
1039
1285
|
// src/core/validators/scenario.ts
|
|
1040
|
-
var
|
|
1286
|
+
var import_promises8 = require("fs/promises");
|
|
1041
1287
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1042
1288
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1043
1289
|
var THEN_PATTERN = /\bThen\b/;
|
|
1290
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1291
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1292
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1044
1293
|
async function validateScenarios(root, config) {
|
|
1045
1294
|
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1046
1295
|
const files = await collectFiles(scenariosRoot, {
|
|
@@ -1048,7 +1297,7 @@ async function validateScenarios(root, config) {
|
|
|
1048
1297
|
});
|
|
1049
1298
|
if (files.length === 0) {
|
|
1050
1299
|
return [
|
|
1051
|
-
|
|
1300
|
+
issue4(
|
|
1052
1301
|
"QFAI-SC-000",
|
|
1053
1302
|
"Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1054
1303
|
"info",
|
|
@@ -1059,13 +1308,14 @@ async function validateScenarios(root, config) {
|
|
|
1059
1308
|
}
|
|
1060
1309
|
const issues = [];
|
|
1061
1310
|
for (const file of files) {
|
|
1062
|
-
const text = await (0,
|
|
1311
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
1063
1312
|
issues.push(...validateScenarioContent(text, file));
|
|
1064
1313
|
}
|
|
1065
1314
|
return issues;
|
|
1066
1315
|
}
|
|
1067
1316
|
function validateScenarioContent(text, file) {
|
|
1068
1317
|
const issues = [];
|
|
1318
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1069
1319
|
const invalidIds = extractInvalidIds(text, [
|
|
1070
1320
|
"SPEC",
|
|
1071
1321
|
"BR",
|
|
@@ -1077,7 +1327,7 @@ function validateScenarioContent(text, file) {
|
|
|
1077
1327
|
]);
|
|
1078
1328
|
if (invalidIds.length > 0) {
|
|
1079
1329
|
issues.push(
|
|
1080
|
-
|
|
1330
|
+
issue4(
|
|
1081
1331
|
"QFAI-ID-002",
|
|
1082
1332
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
1083
1333
|
"error",
|
|
@@ -1087,41 +1337,56 @@ function validateScenarioContent(text, file) {
|
|
|
1087
1337
|
)
|
|
1088
1338
|
);
|
|
1089
1339
|
}
|
|
1090
|
-
const
|
|
1091
|
-
if (
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
"QFAI-SC-001",
|
|
1095
|
-
"SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1096
|
-
"error",
|
|
1097
|
-
file,
|
|
1098
|
-
"scenario.id"
|
|
1099
|
-
)
|
|
1100
|
-
);
|
|
1101
|
-
}
|
|
1102
|
-
const specIds = extractIds(text, "SPEC");
|
|
1103
|
-
if (specIds.length === 0) {
|
|
1340
|
+
const missingStructure = [];
|
|
1341
|
+
if (!parsed.featurePresent) missingStructure.push("Feature");
|
|
1342
|
+
if (parsed.scenarios.length === 0) missingStructure.push("Scenario");
|
|
1343
|
+
if (missingStructure.length > 0) {
|
|
1104
1344
|
issues.push(
|
|
1105
|
-
|
|
1106
|
-
"QFAI-SC-
|
|
1107
|
-
|
|
1345
|
+
issue4(
|
|
1346
|
+
"QFAI-SC-006",
|
|
1347
|
+
`Scenario \u30D5\u30A1\u30A4\u30EB\u306B\u5FC5\u8981\u306A\u69CB\u9020\u304C\u3042\u308A\u307E\u305B\u3093: ${missingStructure.join(
|
|
1348
|
+
", "
|
|
1349
|
+
)}`,
|
|
1108
1350
|
"error",
|
|
1109
1351
|
file,
|
|
1110
|
-
"scenario.
|
|
1352
|
+
"scenario.structure"
|
|
1111
1353
|
)
|
|
1112
1354
|
);
|
|
1113
1355
|
}
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1356
|
+
for (const scenario of parsed.scenarios) {
|
|
1357
|
+
if (scenario.tags.length === 0) {
|
|
1358
|
+
issues.push(
|
|
1359
|
+
issue4(
|
|
1360
|
+
"QFAI-SC-007",
|
|
1361
|
+
`Scenario \u30BF\u30B0\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${scenario.name}`,
|
|
1362
|
+
"error",
|
|
1363
|
+
file,
|
|
1364
|
+
"scenario.tags"
|
|
1365
|
+
)
|
|
1366
|
+
);
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
const missingTags = [];
|
|
1370
|
+
if (!scenario.tags.some((tag) => SC_TAG_RE2.test(tag))) {
|
|
1371
|
+
missingTags.push("SC");
|
|
1372
|
+
}
|
|
1373
|
+
if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
|
|
1374
|
+
missingTags.push("SPEC");
|
|
1375
|
+
}
|
|
1376
|
+
if (!scenario.tags.some((tag) => BR_TAG_RE.test(tag))) {
|
|
1377
|
+
missingTags.push("BR");
|
|
1378
|
+
}
|
|
1379
|
+
if (missingTags.length > 0) {
|
|
1380
|
+
issues.push(
|
|
1381
|
+
issue4(
|
|
1382
|
+
"QFAI-SC-008",
|
|
1383
|
+
`Scenario \u30BF\u30B0\u306B\u4E0D\u8DB3\u304C\u3042\u308A\u307E\u3059: ${missingTags.join(", ")} (${scenario.name})`,
|
|
1384
|
+
"error",
|
|
1385
|
+
file,
|
|
1386
|
+
"scenario.tagIds"
|
|
1387
|
+
)
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1125
1390
|
}
|
|
1126
1391
|
const missingSteps = [];
|
|
1127
1392
|
if (!GIVEN_PATTERN.test(text)) {
|
|
@@ -1135,7 +1400,7 @@ function validateScenarioContent(text, file) {
|
|
|
1135
1400
|
}
|
|
1136
1401
|
if (missingSteps.length > 0) {
|
|
1137
1402
|
issues.push(
|
|
1138
|
-
|
|
1403
|
+
issue4(
|
|
1139
1404
|
"QFAI-SC-005",
|
|
1140
1405
|
`Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
|
|
1141
1406
|
"warning",
|
|
@@ -1146,33 +1411,33 @@ function validateScenarioContent(text, file) {
|
|
|
1146
1411
|
}
|
|
1147
1412
|
return issues;
|
|
1148
1413
|
}
|
|
1149
|
-
function
|
|
1150
|
-
const
|
|
1414
|
+
function issue4(code, message, severity, file, rule, refs) {
|
|
1415
|
+
const issue7 = {
|
|
1151
1416
|
code,
|
|
1152
1417
|
severity,
|
|
1153
1418
|
message
|
|
1154
1419
|
};
|
|
1155
1420
|
if (file) {
|
|
1156
|
-
|
|
1421
|
+
issue7.file = file;
|
|
1157
1422
|
}
|
|
1158
1423
|
if (rule) {
|
|
1159
|
-
|
|
1424
|
+
issue7.rule = rule;
|
|
1160
1425
|
}
|
|
1161
1426
|
if (refs && refs.length > 0) {
|
|
1162
|
-
|
|
1427
|
+
issue7.refs = refs;
|
|
1163
1428
|
}
|
|
1164
|
-
return
|
|
1429
|
+
return issue7;
|
|
1165
1430
|
}
|
|
1166
1431
|
|
|
1167
1432
|
// src/core/validators/spec.ts
|
|
1168
|
-
var
|
|
1433
|
+
var import_promises9 = require("fs/promises");
|
|
1169
1434
|
async function validateSpecs(root, config) {
|
|
1170
1435
|
const specsRoot = resolvePath(root, config, "specDir");
|
|
1171
1436
|
const files = await collectSpecFiles(specsRoot);
|
|
1172
1437
|
if (files.length === 0) {
|
|
1173
1438
|
const expected = "spec-0001-<slug>.md";
|
|
1174
1439
|
return [
|
|
1175
|
-
|
|
1440
|
+
issue5(
|
|
1176
1441
|
"QFAI-SPEC-000",
|
|
1177
1442
|
`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}`,
|
|
1178
1443
|
"info",
|
|
@@ -1183,7 +1448,7 @@ async function validateSpecs(root, config) {
|
|
|
1183
1448
|
}
|
|
1184
1449
|
const issues = [];
|
|
1185
1450
|
for (const file of files) {
|
|
1186
|
-
const text = await (0,
|
|
1451
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1187
1452
|
issues.push(
|
|
1188
1453
|
...validateSpecContent(
|
|
1189
1454
|
text,
|
|
@@ -1196,6 +1461,7 @@ async function validateSpecs(root, config) {
|
|
|
1196
1461
|
}
|
|
1197
1462
|
function validateSpecContent(text, file, requiredSections) {
|
|
1198
1463
|
const issues = [];
|
|
1464
|
+
const parsed = parseSpec(text, file);
|
|
1199
1465
|
const invalidIds = extractInvalidIds(text, [
|
|
1200
1466
|
"SPEC",
|
|
1201
1467
|
"BR",
|
|
@@ -1207,7 +1473,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1207
1473
|
]);
|
|
1208
1474
|
if (invalidIds.length > 0) {
|
|
1209
1475
|
issues.push(
|
|
1210
|
-
|
|
1476
|
+
issue5(
|
|
1211
1477
|
"QFAI-ID-002",
|
|
1212
1478
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
1213
1479
|
"error",
|
|
@@ -1217,10 +1483,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1217
1483
|
)
|
|
1218
1484
|
);
|
|
1219
1485
|
}
|
|
1220
|
-
|
|
1221
|
-
if (specIds.length === 0) {
|
|
1486
|
+
if (!parsed.specId) {
|
|
1222
1487
|
issues.push(
|
|
1223
|
-
|
|
1488
|
+
issue5(
|
|
1224
1489
|
"QFAI-SPEC-001",
|
|
1225
1490
|
"SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1226
1491
|
"error",
|
|
@@ -1229,10 +1494,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1229
1494
|
)
|
|
1230
1495
|
);
|
|
1231
1496
|
}
|
|
1232
|
-
|
|
1233
|
-
if (brIds.length === 0) {
|
|
1497
|
+
if (parsed.brs.length === 0) {
|
|
1234
1498
|
issues.push(
|
|
1235
|
-
|
|
1499
|
+
issue5(
|
|
1236
1500
|
"QFAI-SPEC-002",
|
|
1237
1501
|
"BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1238
1502
|
"error",
|
|
@@ -1241,10 +1505,34 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1241
1505
|
)
|
|
1242
1506
|
);
|
|
1243
1507
|
}
|
|
1508
|
+
for (const br of parsed.brsWithoutPriority) {
|
|
1509
|
+
issues.push(
|
|
1510
|
+
issue5(
|
|
1511
|
+
"QFAI-BR-001",
|
|
1512
|
+
`BR \u884C\u306B Priority \u304C\u3042\u308A\u307E\u305B\u3093: ${br.id}`,
|
|
1513
|
+
"error",
|
|
1514
|
+
file,
|
|
1515
|
+
"spec.brPriority",
|
|
1516
|
+
[br.id]
|
|
1517
|
+
)
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
for (const br of parsed.brsWithInvalidPriority) {
|
|
1521
|
+
issues.push(
|
|
1522
|
+
issue5(
|
|
1523
|
+
"QFAI-BR-002",
|
|
1524
|
+
`BR Priority \u304C\u4E0D\u6B63\u3067\u3059: ${br.id} (${br.priority})`,
|
|
1525
|
+
"error",
|
|
1526
|
+
file,
|
|
1527
|
+
"spec.brPriority",
|
|
1528
|
+
[br.id]
|
|
1529
|
+
)
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1244
1532
|
const scIds = extractIds(text, "SC");
|
|
1245
1533
|
if (scIds.length > 0) {
|
|
1246
1534
|
issues.push(
|
|
1247
|
-
|
|
1535
|
+
issue5(
|
|
1248
1536
|
"QFAI-SPEC-003",
|
|
1249
1537
|
"Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
|
|
1250
1538
|
"warning",
|
|
@@ -1255,9 +1543,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1255
1543
|
);
|
|
1256
1544
|
}
|
|
1257
1545
|
for (const section of requiredSections) {
|
|
1258
|
-
if (!
|
|
1546
|
+
if (!parsed.sections.has(section)) {
|
|
1259
1547
|
issues.push(
|
|
1260
|
-
|
|
1548
|
+
issue5(
|
|
1261
1549
|
"QFAI-SPEC-004",
|
|
1262
1550
|
`\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
|
|
1263
1551
|
"error",
|
|
@@ -1269,26 +1557,32 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1269
1557
|
}
|
|
1270
1558
|
return issues;
|
|
1271
1559
|
}
|
|
1272
|
-
function
|
|
1273
|
-
const
|
|
1560
|
+
function issue5(code, message, severity, file, rule, refs) {
|
|
1561
|
+
const issue7 = {
|
|
1274
1562
|
code,
|
|
1275
1563
|
severity,
|
|
1276
1564
|
message
|
|
1277
1565
|
};
|
|
1278
1566
|
if (file) {
|
|
1279
|
-
|
|
1567
|
+
issue7.file = file;
|
|
1280
1568
|
}
|
|
1281
1569
|
if (rule) {
|
|
1282
|
-
|
|
1570
|
+
issue7.rule = rule;
|
|
1283
1571
|
}
|
|
1284
1572
|
if (refs && refs.length > 0) {
|
|
1285
|
-
|
|
1573
|
+
issue7.refs = refs;
|
|
1286
1574
|
}
|
|
1287
|
-
return
|
|
1575
|
+
return issue7;
|
|
1288
1576
|
}
|
|
1289
1577
|
|
|
1290
1578
|
// src/core/validators/traceability.ts
|
|
1291
|
-
var
|
|
1579
|
+
var import_promises10 = require("fs/promises");
|
|
1580
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1581
|
+
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1582
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1583
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1584
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
1585
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1292
1586
|
async function validateTraceability(root, config) {
|
|
1293
1587
|
const issues = [];
|
|
1294
1588
|
const specsRoot = resolvePath(root, config, "specDir");
|
|
@@ -1314,11 +1608,13 @@ async function validateTraceability(root, config) {
|
|
|
1314
1608
|
const contractIndex = await buildContractIndex(root, config);
|
|
1315
1609
|
const contractIds = contractIndex.ids;
|
|
1316
1610
|
for (const file of specFiles) {
|
|
1317
|
-
const text = await (0,
|
|
1611
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1318
1612
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1613
|
+
const parsed = parseSpec(text, file);
|
|
1614
|
+
if (parsed.specId) {
|
|
1615
|
+
specIds.add(parsed.specId);
|
|
1616
|
+
}
|
|
1617
|
+
const brIds = parsed.brs.map((br) => br.id);
|
|
1322
1618
|
brIds.forEach((id) => brIdsInSpecs.add(id));
|
|
1323
1619
|
const referencedContractIds = /* @__PURE__ */ new Set([
|
|
1324
1620
|
...extractIds(text, "UI"),
|
|
@@ -1330,7 +1626,7 @@ async function validateTraceability(root, config) {
|
|
|
1330
1626
|
);
|
|
1331
1627
|
if (unknownContractIds.length > 0) {
|
|
1332
1628
|
issues.push(
|
|
1333
|
-
|
|
1629
|
+
issue6(
|
|
1334
1630
|
"QFAI-TRACE-009",
|
|
1335
1631
|
`Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1336
1632
|
", "
|
|
@@ -1342,37 +1638,54 @@ async function validateTraceability(root, config) {
|
|
|
1342
1638
|
)
|
|
1343
1639
|
);
|
|
1344
1640
|
}
|
|
1345
|
-
|
|
1346
|
-
const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
|
|
1641
|
+
if (parsed.specId) {
|
|
1642
|
+
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
1347
1643
|
brIds.forEach((id) => current.add(id));
|
|
1348
|
-
specToBrIds.set(specId, current);
|
|
1644
|
+
specToBrIds.set(parsed.specId, current);
|
|
1349
1645
|
}
|
|
1350
1646
|
}
|
|
1351
1647
|
for (const file of decisionFiles) {
|
|
1352
|
-
const text = await (0,
|
|
1648
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1353
1649
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1354
1650
|
}
|
|
1355
1651
|
for (const file of scenarioFiles) {
|
|
1356
|
-
const text = await (0,
|
|
1652
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1357
1653
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1358
|
-
const
|
|
1359
|
-
const
|
|
1360
|
-
const
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1654
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1655
|
+
const specIdsInScenario = /* @__PURE__ */ new Set();
|
|
1656
|
+
const brIds = /* @__PURE__ */ new Set();
|
|
1657
|
+
const scIds = /* @__PURE__ */ new Set();
|
|
1658
|
+
const scenarioIds = /* @__PURE__ */ new Set();
|
|
1659
|
+
for (const scenario of parsed.scenarios) {
|
|
1660
|
+
for (const tag of scenario.tags) {
|
|
1661
|
+
if (SPEC_TAG_RE2.test(tag)) {
|
|
1662
|
+
specIdsInScenario.add(tag);
|
|
1663
|
+
}
|
|
1664
|
+
if (BR_TAG_RE2.test(tag)) {
|
|
1665
|
+
brIds.add(tag);
|
|
1666
|
+
}
|
|
1667
|
+
if (SC_TAG_RE3.test(tag)) {
|
|
1668
|
+
scIds.add(tag);
|
|
1669
|
+
}
|
|
1670
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1671
|
+
scenarioIds.add(tag);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
const specIdsList = Array.from(specIdsInScenario);
|
|
1676
|
+
const brIdsList = Array.from(brIds);
|
|
1677
|
+
const scIdsList = Array.from(scIds);
|
|
1678
|
+
const scenarioIdsList = Array.from(scenarioIds);
|
|
1679
|
+
brIdsList.forEach((id) => brIdsInScenarios.add(id));
|
|
1680
|
+
scIdsList.forEach((id) => scIdsInScenarios.add(id));
|
|
1681
|
+
scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
|
|
1682
|
+
if (scenarioIdsList.length > 0) {
|
|
1683
|
+
scIdsList.forEach((id) => scWithContracts.add(id));
|
|
1371
1684
|
}
|
|
1372
|
-
const unknownSpecIds =
|
|
1685
|
+
const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
|
|
1373
1686
|
if (unknownSpecIds.length > 0) {
|
|
1374
1687
|
issues.push(
|
|
1375
|
-
|
|
1688
|
+
issue6(
|
|
1376
1689
|
"QFAI-TRACE-005",
|
|
1377
1690
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1378
1691
|
"error",
|
|
@@ -1382,10 +1695,10 @@ async function validateTraceability(root, config) {
|
|
|
1382
1695
|
)
|
|
1383
1696
|
);
|
|
1384
1697
|
}
|
|
1385
|
-
const unknownBrIds =
|
|
1698
|
+
const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
|
|
1386
1699
|
if (unknownBrIds.length > 0) {
|
|
1387
1700
|
issues.push(
|
|
1388
|
-
|
|
1701
|
+
issue6(
|
|
1389
1702
|
"QFAI-TRACE-006",
|
|
1390
1703
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1391
1704
|
"error",
|
|
@@ -1395,10 +1708,12 @@ async function validateTraceability(root, config) {
|
|
|
1395
1708
|
)
|
|
1396
1709
|
);
|
|
1397
1710
|
}
|
|
1398
|
-
const unknownContractIds =
|
|
1711
|
+
const unknownContractIds = scenarioIdsList.filter(
|
|
1712
|
+
(id) => !contractIds.has(id)
|
|
1713
|
+
);
|
|
1399
1714
|
if (unknownContractIds.length > 0) {
|
|
1400
1715
|
issues.push(
|
|
1401
|
-
|
|
1716
|
+
issue6(
|
|
1402
1717
|
"QFAI-TRACE-008",
|
|
1403
1718
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1404
1719
|
", "
|
|
@@ -1410,23 +1725,23 @@ async function validateTraceability(root, config) {
|
|
|
1410
1725
|
)
|
|
1411
1726
|
);
|
|
1412
1727
|
}
|
|
1413
|
-
if (
|
|
1728
|
+
if (specIdsList.length > 0) {
|
|
1414
1729
|
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
1415
|
-
for (const specId of
|
|
1730
|
+
for (const specId of specIdsList) {
|
|
1416
1731
|
const brIdsForSpec = specToBrIds.get(specId);
|
|
1417
1732
|
if (!brIdsForSpec) {
|
|
1418
1733
|
continue;
|
|
1419
1734
|
}
|
|
1420
1735
|
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
1421
1736
|
}
|
|
1422
|
-
const invalidBrIds =
|
|
1737
|
+
const invalidBrIds = brIdsList.filter((id) => !allowedBrIds.has(id));
|
|
1423
1738
|
if (invalidBrIds.length > 0) {
|
|
1424
1739
|
issues.push(
|
|
1425
|
-
|
|
1740
|
+
issue6(
|
|
1426
1741
|
"QFAI-TRACE-007",
|
|
1427
1742
|
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
1428
1743
|
", "
|
|
1429
|
-
)} (SPEC: ${
|
|
1744
|
+
)} (SPEC: ${specIdsList.join(", ")})`,
|
|
1430
1745
|
"error",
|
|
1431
1746
|
file,
|
|
1432
1747
|
"traceability.scenarioBrUnderSpec",
|
|
@@ -1438,7 +1753,7 @@ async function validateTraceability(root, config) {
|
|
|
1438
1753
|
}
|
|
1439
1754
|
if (upstreamIds.size === 0) {
|
|
1440
1755
|
return [
|
|
1441
|
-
|
|
1756
|
+
issue6(
|
|
1442
1757
|
"QFAI-TRACE-000",
|
|
1443
1758
|
"\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1444
1759
|
"info",
|
|
@@ -1453,7 +1768,7 @@ async function validateTraceability(root, config) {
|
|
|
1453
1768
|
);
|
|
1454
1769
|
if (orphanBrIds.length > 0) {
|
|
1455
1770
|
issues.push(
|
|
1456
|
-
|
|
1771
|
+
issue6(
|
|
1457
1772
|
"QFAI_TRACE_BR_ORPHAN",
|
|
1458
1773
|
`BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
|
|
1459
1774
|
"error",
|
|
@@ -1470,7 +1785,7 @@ async function validateTraceability(root, config) {
|
|
|
1470
1785
|
);
|
|
1471
1786
|
if (scWithoutContracts.length > 0) {
|
|
1472
1787
|
issues.push(
|
|
1473
|
-
|
|
1788
|
+
issue6(
|
|
1474
1789
|
"QFAI_TRACE_SC_NO_CONTRACT",
|
|
1475
1790
|
`SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
|
|
1476
1791
|
", "
|
|
@@ -1490,7 +1805,7 @@ async function validateTraceability(root, config) {
|
|
|
1490
1805
|
);
|
|
1491
1806
|
if (orphanContracts.length > 0) {
|
|
1492
1807
|
issues.push(
|
|
1493
|
-
|
|
1808
|
+
issue6(
|
|
1494
1809
|
"QFAI_CONTRACT_ORPHAN",
|
|
1495
1810
|
`\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
1496
1811
|
"error",
|
|
@@ -1518,7 +1833,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1518
1833
|
const targetFiles = [...codeFiles, ...testFiles];
|
|
1519
1834
|
if (targetFiles.length === 0) {
|
|
1520
1835
|
issues.push(
|
|
1521
|
-
|
|
1836
|
+
issue6(
|
|
1522
1837
|
"QFAI-TRACE-001",
|
|
1523
1838
|
"\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1524
1839
|
"info",
|
|
@@ -1531,7 +1846,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1531
1846
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
1532
1847
|
let found = false;
|
|
1533
1848
|
for (const file of targetFiles) {
|
|
1534
|
-
const text = await (0,
|
|
1849
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1535
1850
|
if (pattern.test(text)) {
|
|
1536
1851
|
found = true;
|
|
1537
1852
|
break;
|
|
@@ -1539,7 +1854,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1539
1854
|
}
|
|
1540
1855
|
if (!found) {
|
|
1541
1856
|
issues.push(
|
|
1542
|
-
|
|
1857
|
+
issue6(
|
|
1543
1858
|
"QFAI-TRACE-002",
|
|
1544
1859
|
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
1545
1860
|
"warning",
|
|
@@ -1554,22 +1869,22 @@ function buildIdPattern(ids) {
|
|
|
1554
1869
|
const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
1555
1870
|
return new RegExp(`\\b(${escaped.join("|")})\\b`);
|
|
1556
1871
|
}
|
|
1557
|
-
function
|
|
1558
|
-
const
|
|
1872
|
+
function issue6(code, message, severity, file, rule, refs) {
|
|
1873
|
+
const issue7 = {
|
|
1559
1874
|
code,
|
|
1560
1875
|
severity,
|
|
1561
1876
|
message
|
|
1562
1877
|
};
|
|
1563
1878
|
if (file) {
|
|
1564
|
-
|
|
1879
|
+
issue7.file = file;
|
|
1565
1880
|
}
|
|
1566
1881
|
if (rule) {
|
|
1567
|
-
|
|
1882
|
+
issue7.rule = rule;
|
|
1568
1883
|
}
|
|
1569
1884
|
if (refs && refs.length > 0) {
|
|
1570
|
-
|
|
1885
|
+
issue7.refs = refs;
|
|
1571
1886
|
}
|
|
1572
|
-
return
|
|
1887
|
+
return issue7;
|
|
1573
1888
|
}
|
|
1574
1889
|
|
|
1575
1890
|
// src/core/validate.ts
|
|
@@ -1580,6 +1895,7 @@ async function validateProject(root, configResult) {
|
|
|
1580
1895
|
...configIssues,
|
|
1581
1896
|
...await validateSpecs(root, config),
|
|
1582
1897
|
...await validateScenarios(root, config),
|
|
1898
|
+
...await validateDecisions(root, config),
|
|
1583
1899
|
...await validateContracts(root, config),
|
|
1584
1900
|
...await validateDefinedIds(root, config),
|
|
1585
1901
|
...await validateTraceability(root, config)
|
|
@@ -1594,8 +1910,8 @@ async function validateProject(root, configResult) {
|
|
|
1594
1910
|
}
|
|
1595
1911
|
function countIssues(issues) {
|
|
1596
1912
|
return issues.reduce(
|
|
1597
|
-
(acc,
|
|
1598
|
-
acc[
|
|
1913
|
+
(acc, issue7) => {
|
|
1914
|
+
acc[issue7.severity] += 1;
|
|
1599
1915
|
return acc;
|
|
1600
1916
|
},
|
|
1601
1917
|
{ info: 0, warning: 0, error: 0 }
|
|
@@ -1766,7 +2082,7 @@ async function collectIds(files) {
|
|
|
1766
2082
|
DATA: /* @__PURE__ */ new Set()
|
|
1767
2083
|
};
|
|
1768
2084
|
for (const file of files) {
|
|
1769
|
-
const text = await (0,
|
|
2085
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1770
2086
|
for (const prefix of ID_PREFIXES2) {
|
|
1771
2087
|
const ids = extractIds(text, prefix);
|
|
1772
2088
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -1784,7 +2100,7 @@ async function collectIds(files) {
|
|
|
1784
2100
|
async function collectUpstreamIds(files) {
|
|
1785
2101
|
const ids = /* @__PURE__ */ new Set();
|
|
1786
2102
|
for (const file of files) {
|
|
1787
|
-
const text = await (0,
|
|
2103
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1788
2104
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
1789
2105
|
}
|
|
1790
2106
|
return ids;
|
|
@@ -1805,7 +2121,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
1805
2121
|
}
|
|
1806
2122
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
1807
2123
|
for (const file of targetFiles) {
|
|
1808
|
-
const text = await (0,
|
|
2124
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1809
2125
|
if (pattern.test(text)) {
|
|
1810
2126
|
return true;
|
|
1811
2127
|
}
|
|
@@ -1827,20 +2143,20 @@ function toSortedArray(values) {
|
|
|
1827
2143
|
}
|
|
1828
2144
|
function buildHotspots(issues) {
|
|
1829
2145
|
const map = /* @__PURE__ */ new Map();
|
|
1830
|
-
for (const
|
|
1831
|
-
if (!
|
|
2146
|
+
for (const issue7 of issues) {
|
|
2147
|
+
if (!issue7.file) {
|
|
1832
2148
|
continue;
|
|
1833
2149
|
}
|
|
1834
|
-
const current = map.get(
|
|
1835
|
-
file:
|
|
2150
|
+
const current = map.get(issue7.file) ?? {
|
|
2151
|
+
file: issue7.file,
|
|
1836
2152
|
total: 0,
|
|
1837
2153
|
error: 0,
|
|
1838
2154
|
warning: 0,
|
|
1839
2155
|
info: 0
|
|
1840
2156
|
};
|
|
1841
2157
|
current.total += 1;
|
|
1842
|
-
current[
|
|
1843
|
-
map.set(
|
|
2158
|
+
current[issue7.severity] += 1;
|
|
2159
|
+
map.set(issue7.file, current);
|
|
1844
2160
|
}
|
|
1845
2161
|
return Array.from(map.values()).sort(
|
|
1846
2162
|
(a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
|
|
@@ -1862,6 +2178,7 @@ function buildHotspots(issues) {
|
|
|
1862
2178
|
resolvePath,
|
|
1863
2179
|
resolveToolVersion,
|
|
1864
2180
|
validateContracts,
|
|
2181
|
+
validateDecisions,
|
|
1865
2182
|
validateDefinedIds,
|
|
1866
2183
|
validateProject,
|
|
1867
2184
|
validateScenarioContent,
|