qfai 0.3.1 → 0.3.2
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/specs/README.md +5 -5
- package/dist/cli/index.cjs +450 -241
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +422 -209
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +441 -232
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +420 -207
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
- /package/assets/init/.qfai/specs/{spec-001 → spec-0001}/delta.md +0 -0
- /package/assets/init/.qfai/specs/{spec-001 → spec-0001}/scenario.md +0 -0
- /package/assets/init/.qfai/specs/{spec-001 → spec-0001}/spec.md +0 -0
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_promises15 = require("fs/promises");
|
|
164
164
|
var import_node_path14 = __toESM(require("path"), 1);
|
|
165
165
|
|
|
166
166
|
// src/core/config.ts
|
|
@@ -497,11 +497,11 @@ function isRecord(value) {
|
|
|
497
497
|
}
|
|
498
498
|
|
|
499
499
|
// src/core/report.ts
|
|
500
|
-
var
|
|
500
|
+
var import_promises14 = require("fs/promises");
|
|
501
501
|
var import_node_path13 = __toESM(require("path"), 1);
|
|
502
502
|
|
|
503
503
|
// src/core/discovery.ts
|
|
504
|
-
var
|
|
504
|
+
var import_promises5 = require("fs/promises");
|
|
505
505
|
|
|
506
506
|
// src/core/fs.ts
|
|
507
507
|
var import_promises3 = require("fs/promises");
|
|
@@ -558,25 +558,50 @@ async function exists2(target) {
|
|
|
558
558
|
}
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
-
// src/core/
|
|
562
|
-
var
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
561
|
+
// src/core/specLayout.ts
|
|
562
|
+
var import_promises4 = require("fs/promises");
|
|
563
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
564
|
+
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
565
|
+
async function collectSpecEntries(specsRoot) {
|
|
566
|
+
const dirs = await listSpecDirs(specsRoot);
|
|
567
|
+
const entries = dirs.map((dir) => ({
|
|
568
|
+
dir,
|
|
569
|
+
specPath: import_node_path6.default.join(dir, "spec.md"),
|
|
570
|
+
deltaPath: import_node_path6.default.join(dir, "delta.md"),
|
|
571
|
+
scenarioPath: import_node_path6.default.join(dir, "scenario.md")
|
|
572
|
+
}));
|
|
573
|
+
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
574
|
+
}
|
|
575
|
+
async function listSpecDirs(specsRoot) {
|
|
576
|
+
try {
|
|
577
|
+
const items = await (0, import_promises4.readdir)(specsRoot, { withFileTypes: true });
|
|
578
|
+
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => import_node_path6.default.join(specsRoot, name));
|
|
579
|
+
} catch (error2) {
|
|
580
|
+
if (isMissingFileError(error2)) {
|
|
581
|
+
return [];
|
|
569
582
|
}
|
|
583
|
+
throw error2;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function isMissingFileError(error2) {
|
|
587
|
+
if (!error2 || typeof error2 !== "object") {
|
|
588
|
+
return false;
|
|
570
589
|
}
|
|
571
|
-
return
|
|
590
|
+
return error2.code === "ENOENT";
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/core/discovery.ts
|
|
594
|
+
async function collectSpecPackDirs(specsRoot) {
|
|
595
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
596
|
+
return entries.map((entry) => entry.dir);
|
|
572
597
|
}
|
|
573
598
|
async function collectSpecFiles(specsRoot) {
|
|
574
|
-
const
|
|
575
|
-
return
|
|
599
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
600
|
+
return filterExisting(entries.map((entry) => entry.specPath));
|
|
576
601
|
}
|
|
577
602
|
async function collectScenarioFiles(specsRoot) {
|
|
578
|
-
const
|
|
579
|
-
return
|
|
603
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
604
|
+
return filterExisting(entries.map((entry) => entry.scenarioPath));
|
|
580
605
|
}
|
|
581
606
|
async function collectUiContractFiles(uiRoot) {
|
|
582
607
|
return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
@@ -595,12 +620,22 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
|
595
620
|
]);
|
|
596
621
|
return { ui, api, db };
|
|
597
622
|
}
|
|
598
|
-
function
|
|
599
|
-
|
|
623
|
+
async function filterExisting(files) {
|
|
624
|
+
const existing = [];
|
|
625
|
+
for (const file of files) {
|
|
626
|
+
if (await exists3(file)) {
|
|
627
|
+
existing.push(file);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return existing;
|
|
631
|
+
}
|
|
632
|
+
async function exists3(target) {
|
|
633
|
+
try {
|
|
634
|
+
await (0, import_promises5.access)(target);
|
|
635
|
+
return true;
|
|
636
|
+
} catch {
|
|
600
637
|
return false;
|
|
601
638
|
}
|
|
602
|
-
const dirName = import_node_path6.default.basename(import_node_path6.default.dirname(filePath)).toLowerCase();
|
|
603
|
-
return SPEC_PACK_DIR_PATTERN.test(dirName);
|
|
604
639
|
}
|
|
605
640
|
|
|
606
641
|
// src/core/ids.ts
|
|
@@ -660,16 +695,16 @@ function isValidId(value, prefix) {
|
|
|
660
695
|
var VALIDATION_SCHEMA_VERSION = "0.2";
|
|
661
696
|
|
|
662
697
|
// src/core/version.ts
|
|
663
|
-
var
|
|
698
|
+
var import_promises6 = require("fs/promises");
|
|
664
699
|
var import_node_path7 = __toESM(require("path"), 1);
|
|
665
700
|
var import_node_url2 = require("url");
|
|
666
701
|
async function resolveToolVersion() {
|
|
667
|
-
if ("0.3.
|
|
668
|
-
return "0.3.
|
|
702
|
+
if ("0.3.2".length > 0) {
|
|
703
|
+
return "0.3.2";
|
|
669
704
|
}
|
|
670
705
|
try {
|
|
671
706
|
const packagePath = resolvePackageJsonPath();
|
|
672
|
-
const raw = await (0,
|
|
707
|
+
const raw = await (0, import_promises6.readFile)(packagePath, "utf-8");
|
|
673
708
|
const parsed = JSON.parse(raw);
|
|
674
709
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
675
710
|
return version.length > 0 ? version : "unknown";
|
|
@@ -684,7 +719,7 @@ function resolvePackageJsonPath() {
|
|
|
684
719
|
}
|
|
685
720
|
|
|
686
721
|
// src/core/validators/contracts.ts
|
|
687
|
-
var
|
|
722
|
+
var import_promises7 = require("fs/promises");
|
|
688
723
|
var import_node_path9 = __toESM(require("path"), 1);
|
|
689
724
|
|
|
690
725
|
// src/core/contracts.ts
|
|
@@ -762,7 +797,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
762
797
|
}
|
|
763
798
|
const issues = [];
|
|
764
799
|
for (const file of files) {
|
|
765
|
-
const text = await (0,
|
|
800
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
766
801
|
const invalidIds = extractInvalidIds(text, [
|
|
767
802
|
"SPEC",
|
|
768
803
|
"BR",
|
|
@@ -829,7 +864,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
829
864
|
}
|
|
830
865
|
const issues = [];
|
|
831
866
|
for (const file of files) {
|
|
832
|
-
const text = await (0,
|
|
867
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
833
868
|
const invalidIds = extractInvalidIds(text, [
|
|
834
869
|
"SPEC",
|
|
835
870
|
"BR",
|
|
@@ -907,7 +942,7 @@ async function validateDataContracts(dataRoot) {
|
|
|
907
942
|
}
|
|
908
943
|
const issues = [];
|
|
909
944
|
for (const file of files) {
|
|
910
|
-
const text = await (0,
|
|
945
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
911
946
|
const invalidIds = extractInvalidIds(text, [
|
|
912
947
|
"SPEC",
|
|
913
948
|
"BR",
|
|
@@ -978,7 +1013,7 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
978
1013
|
}
|
|
979
1014
|
|
|
980
1015
|
// src/core/validators/delta.ts
|
|
981
|
-
var
|
|
1016
|
+
var import_promises8 = require("fs/promises");
|
|
982
1017
|
var import_node_path10 = __toESM(require("path"), 1);
|
|
983
1018
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
984
1019
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
@@ -996,9 +1031,9 @@ async function validateDeltas(root, config) {
|
|
|
996
1031
|
const deltaPath = import_node_path10.default.join(pack, "delta.md");
|
|
997
1032
|
let text;
|
|
998
1033
|
try {
|
|
999
|
-
text = await (0,
|
|
1034
|
+
text = await (0, import_promises8.readFile)(deltaPath, "utf-8");
|
|
1000
1035
|
} catch (error2) {
|
|
1001
|
-
if (
|
|
1036
|
+
if (isMissingFileError2(error2)) {
|
|
1002
1037
|
issues.push(
|
|
1003
1038
|
issue2(
|
|
1004
1039
|
"QFAI-DELTA-001",
|
|
@@ -1043,7 +1078,7 @@ async function validateDeltas(root, config) {
|
|
|
1043
1078
|
}
|
|
1044
1079
|
return issues;
|
|
1045
1080
|
}
|
|
1046
|
-
function
|
|
1081
|
+
function isMissingFileError2(error2) {
|
|
1047
1082
|
if (!error2 || typeof error2 !== "object") {
|
|
1048
1083
|
return false;
|
|
1049
1084
|
}
|
|
@@ -1068,11 +1103,11 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1068
1103
|
}
|
|
1069
1104
|
|
|
1070
1105
|
// src/core/validators/ids.ts
|
|
1071
|
-
var
|
|
1106
|
+
var import_promises10 = require("fs/promises");
|
|
1072
1107
|
var import_node_path12 = __toESM(require("path"), 1);
|
|
1073
1108
|
|
|
1074
1109
|
// src/core/contractIndex.ts
|
|
1075
|
-
var
|
|
1110
|
+
var import_promises9 = require("fs/promises");
|
|
1076
1111
|
var import_node_path11 = __toESM(require("path"), 1);
|
|
1077
1112
|
async function buildContractIndex(root, config) {
|
|
1078
1113
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
@@ -1097,7 +1132,7 @@ async function buildContractIndex(root, config) {
|
|
|
1097
1132
|
}
|
|
1098
1133
|
async function indexUiContracts(files, index) {
|
|
1099
1134
|
for (const file of files) {
|
|
1100
|
-
const text = await (0,
|
|
1135
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1101
1136
|
try {
|
|
1102
1137
|
const doc = parseStructuredContract(file, text);
|
|
1103
1138
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1109,7 +1144,7 @@ async function indexUiContracts(files, index) {
|
|
|
1109
1144
|
}
|
|
1110
1145
|
async function indexApiContracts(files, index) {
|
|
1111
1146
|
for (const file of files) {
|
|
1112
|
-
const text = await (0,
|
|
1147
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1113
1148
|
try {
|
|
1114
1149
|
const doc = parseStructuredContract(file, text);
|
|
1115
1150
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1121,7 +1156,7 @@ async function indexApiContracts(files, index) {
|
|
|
1121
1156
|
}
|
|
1122
1157
|
async function indexDataContracts(files, index) {
|
|
1123
1158
|
for (const file of files) {
|
|
1124
|
-
const text = await (0,
|
|
1159
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1125
1160
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1126
1161
|
}
|
|
1127
1162
|
}
|
|
@@ -1132,66 +1167,6 @@ function record(index, id, file) {
|
|
|
1132
1167
|
index.idToFiles.set(id, current);
|
|
1133
1168
|
}
|
|
1134
1169
|
|
|
1135
|
-
// src/core/parse/gherkin.ts
|
|
1136
|
-
var FEATURE_RE = /^\s*Feature:\s+/;
|
|
1137
|
-
var SCENARIO_RE = /^\s*Scenario(?: Outline)?:\s*(.+)\s*$/;
|
|
1138
|
-
var TAG_LINE_RE = /^\s*@/;
|
|
1139
|
-
function parseTags(line) {
|
|
1140
|
-
return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
|
|
1141
|
-
}
|
|
1142
|
-
function parseGherkinFeature(text, file) {
|
|
1143
|
-
const lines = text.split(/\r?\n/);
|
|
1144
|
-
const scenarios = [];
|
|
1145
|
-
let featurePresent = false;
|
|
1146
|
-
let featureTags = [];
|
|
1147
|
-
let pendingTags = [];
|
|
1148
|
-
let current = null;
|
|
1149
|
-
const flush = () => {
|
|
1150
|
-
if (!current) return;
|
|
1151
|
-
scenarios.push({
|
|
1152
|
-
...current,
|
|
1153
|
-
body: current.body.trim()
|
|
1154
|
-
});
|
|
1155
|
-
current = null;
|
|
1156
|
-
};
|
|
1157
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1158
|
-
const line = lines[i] ?? "";
|
|
1159
|
-
const trimmed = line.trim();
|
|
1160
|
-
if (TAG_LINE_RE.test(trimmed)) {
|
|
1161
|
-
pendingTags.push(...parseTags(trimmed));
|
|
1162
|
-
continue;
|
|
1163
|
-
}
|
|
1164
|
-
if (FEATURE_RE.test(trimmed)) {
|
|
1165
|
-
featurePresent = true;
|
|
1166
|
-
featureTags = [...pendingTags];
|
|
1167
|
-
pendingTags = [];
|
|
1168
|
-
continue;
|
|
1169
|
-
}
|
|
1170
|
-
const match = trimmed.match(SCENARIO_RE);
|
|
1171
|
-
if (match) {
|
|
1172
|
-
const scenarioName = match[1]?.trim();
|
|
1173
|
-
if (!scenarioName) {
|
|
1174
|
-
continue;
|
|
1175
|
-
}
|
|
1176
|
-
flush();
|
|
1177
|
-
current = {
|
|
1178
|
-
name: scenarioName,
|
|
1179
|
-
line: i + 1,
|
|
1180
|
-
tags: [...featureTags, ...pendingTags],
|
|
1181
|
-
body: ""
|
|
1182
|
-
};
|
|
1183
|
-
pendingTags = [];
|
|
1184
|
-
continue;
|
|
1185
|
-
}
|
|
1186
|
-
if (current) {
|
|
1187
|
-
current.body += `${line}
|
|
1188
|
-
`;
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
flush();
|
|
1192
|
-
return { file, featurePresent, scenarios };
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
1170
|
// src/core/parse/markdown.ts
|
|
1196
1171
|
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1197
1172
|
function parseHeadings(md) {
|
|
@@ -1310,8 +1285,162 @@ function parseSpec(md, file) {
|
|
|
1310
1285
|
return parsed;
|
|
1311
1286
|
}
|
|
1312
1287
|
|
|
1313
|
-
// src/core/
|
|
1288
|
+
// src/core/gherkin/parse.ts
|
|
1289
|
+
var import_gherkin = require("@cucumber/gherkin");
|
|
1290
|
+
var import_node_crypto = require("crypto");
|
|
1291
|
+
function parseGherkin(source, uri) {
|
|
1292
|
+
const errors = [];
|
|
1293
|
+
const uuidFn = () => (0, import_node_crypto.randomUUID)();
|
|
1294
|
+
const builder = new import_gherkin.AstBuilder(uuidFn);
|
|
1295
|
+
const matcher = new import_gherkin.GherkinClassicTokenMatcher();
|
|
1296
|
+
const parser = new import_gherkin.Parser(builder, matcher);
|
|
1297
|
+
try {
|
|
1298
|
+
const gherkinDocument = parser.parse(source);
|
|
1299
|
+
gherkinDocument.uri = uri;
|
|
1300
|
+
return { gherkinDocument, errors };
|
|
1301
|
+
} catch (error2) {
|
|
1302
|
+
errors.push(formatError3(error2));
|
|
1303
|
+
return { gherkinDocument: null, errors };
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
function formatError3(error2) {
|
|
1307
|
+
if (error2 instanceof Error) {
|
|
1308
|
+
return error2.message;
|
|
1309
|
+
}
|
|
1310
|
+
return String(error2);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/core/scenarioModel.ts
|
|
1314
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1314
1315
|
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1316
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1317
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1318
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
1319
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1320
|
+
function parseScenarioDocument(text, uri) {
|
|
1321
|
+
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1322
|
+
if (!gherkinDocument) {
|
|
1323
|
+
return { document: null, errors };
|
|
1324
|
+
}
|
|
1325
|
+
const feature = gherkinDocument.feature;
|
|
1326
|
+
if (!feature) {
|
|
1327
|
+
return {
|
|
1328
|
+
document: { uri, featureTags: [], scenarios: [] },
|
|
1329
|
+
errors
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
const featureTags = collectTagNames(feature.tags);
|
|
1333
|
+
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
1334
|
+
return {
|
|
1335
|
+
document: {
|
|
1336
|
+
uri,
|
|
1337
|
+
featureName: feature.name,
|
|
1338
|
+
featureTags,
|
|
1339
|
+
scenarios
|
|
1340
|
+
},
|
|
1341
|
+
errors
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
function buildScenarioAtoms(document) {
|
|
1345
|
+
return document.scenarios.map((scenario) => {
|
|
1346
|
+
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1347
|
+
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1348
|
+
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1349
|
+
const contractIds = /* @__PURE__ */ new Set();
|
|
1350
|
+
scenario.tags.forEach((tag) => {
|
|
1351
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1352
|
+
contractIds.add(tag);
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
for (const step of scenario.steps) {
|
|
1356
|
+
for (const text of collectStepTexts(step)) {
|
|
1357
|
+
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1358
|
+
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1359
|
+
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
const atom = {
|
|
1363
|
+
uri: document.uri,
|
|
1364
|
+
featureName: document.featureName ?? "",
|
|
1365
|
+
scenarioName: scenario.name,
|
|
1366
|
+
kind: scenario.kind,
|
|
1367
|
+
brIds,
|
|
1368
|
+
contractIds: Array.from(contractIds).sort()
|
|
1369
|
+
};
|
|
1370
|
+
if (scenario.line !== void 0) {
|
|
1371
|
+
atom.line = scenario.line;
|
|
1372
|
+
}
|
|
1373
|
+
if (specIds.length === 1) {
|
|
1374
|
+
const specId = specIds[0];
|
|
1375
|
+
if (specId) {
|
|
1376
|
+
atom.specId = specId;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
if (scIds.length === 1) {
|
|
1380
|
+
const scId = scIds[0];
|
|
1381
|
+
if (scId) {
|
|
1382
|
+
atom.scId = scId;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return atom;
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
function collectScenarioNodes(feature, featureTags) {
|
|
1389
|
+
const scenarios = [];
|
|
1390
|
+
for (const child of feature.children) {
|
|
1391
|
+
if (child.scenario) {
|
|
1392
|
+
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
1393
|
+
}
|
|
1394
|
+
if (child.rule) {
|
|
1395
|
+
const ruleTags = collectTagNames(child.rule.tags);
|
|
1396
|
+
for (const ruleChild of child.rule.children) {
|
|
1397
|
+
if (ruleChild.scenario) {
|
|
1398
|
+
scenarios.push(
|
|
1399
|
+
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return scenarios;
|
|
1406
|
+
}
|
|
1407
|
+
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
1408
|
+
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
1409
|
+
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
1410
|
+
return {
|
|
1411
|
+
name: scenario.name,
|
|
1412
|
+
kind,
|
|
1413
|
+
line: scenario.location?.line,
|
|
1414
|
+
tags,
|
|
1415
|
+
steps: scenario.steps
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
function collectTagNames(tags) {
|
|
1419
|
+
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1420
|
+
}
|
|
1421
|
+
function collectStepTexts(step) {
|
|
1422
|
+
const texts = [];
|
|
1423
|
+
if (step.text) {
|
|
1424
|
+
texts.push(step.text);
|
|
1425
|
+
}
|
|
1426
|
+
if (step.docString?.content) {
|
|
1427
|
+
texts.push(step.docString.content);
|
|
1428
|
+
}
|
|
1429
|
+
if (step.dataTable?.rows) {
|
|
1430
|
+
for (const row of step.dataTable.rows) {
|
|
1431
|
+
for (const cell of row.cells) {
|
|
1432
|
+
texts.push(cell.value);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
return texts;
|
|
1437
|
+
}
|
|
1438
|
+
function unique2(values) {
|
|
1439
|
+
return Array.from(new Set(values));
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// src/core/validators/ids.ts
|
|
1443
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1315
1444
|
async function validateDefinedIds(root, config) {
|
|
1316
1445
|
const issues = [];
|
|
1317
1446
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1345,7 +1474,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1345
1474
|
}
|
|
1346
1475
|
async function collectSpecDefinitionIds(files, out) {
|
|
1347
1476
|
for (const file of files) {
|
|
1348
|
-
const text = await (0,
|
|
1477
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1349
1478
|
const parsed = parseSpec(text, file);
|
|
1350
1479
|
if (parsed.specId) {
|
|
1351
1480
|
recordId(out, parsed.specId, file);
|
|
@@ -1355,11 +1484,14 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1355
1484
|
}
|
|
1356
1485
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1357
1486
|
for (const file of files) {
|
|
1358
|
-
const text = await (0,
|
|
1359
|
-
const
|
|
1360
|
-
|
|
1487
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1488
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1489
|
+
if (!document || errors.length > 0) {
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
for (const scenario of document.scenarios) {
|
|
1361
1493
|
for (const tag of scenario.tags) {
|
|
1362
|
-
if (
|
|
1494
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
1363
1495
|
recordId(out, tag, file);
|
|
1364
1496
|
}
|
|
1365
1497
|
}
|
|
@@ -1396,21 +1528,23 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1396
1528
|
}
|
|
1397
1529
|
|
|
1398
1530
|
// src/core/validators/scenario.ts
|
|
1399
|
-
var
|
|
1531
|
+
var import_promises11 = require("fs/promises");
|
|
1400
1532
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1401
1533
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1402
1534
|
var THEN_PATTERN = /\bThen\b/;
|
|
1403
|
-
var
|
|
1404
|
-
var
|
|
1405
|
-
var
|
|
1535
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1536
|
+
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1537
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1406
1538
|
async function validateScenarios(root, config) {
|
|
1407
1539
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1408
|
-
const
|
|
1409
|
-
if (
|
|
1540
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1541
|
+
if (entries.length === 0) {
|
|
1542
|
+
const expected = "spec-0001/scenario.md";
|
|
1543
|
+
const legacy = "spec-001/scenario.md";
|
|
1410
1544
|
return [
|
|
1411
1545
|
issue4(
|
|
1412
1546
|
"QFAI-SC-000",
|
|
1413
|
-
|
|
1547
|
+
`Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected} (${legacy} \u306F\u975E\u5BFE\u5FDC)`,
|
|
1414
1548
|
"info",
|
|
1415
1549
|
specsRoot,
|
|
1416
1550
|
"scenario.files"
|
|
@@ -1418,15 +1552,31 @@ async function validateScenarios(root, config) {
|
|
|
1418
1552
|
];
|
|
1419
1553
|
}
|
|
1420
1554
|
const issues = [];
|
|
1421
|
-
for (const
|
|
1422
|
-
|
|
1423
|
-
|
|
1555
|
+
for (const entry of entries) {
|
|
1556
|
+
let text;
|
|
1557
|
+
try {
|
|
1558
|
+
text = await (0, import_promises11.readFile)(entry.scenarioPath, "utf-8");
|
|
1559
|
+
} catch (error2) {
|
|
1560
|
+
if (isMissingFileError3(error2)) {
|
|
1561
|
+
issues.push(
|
|
1562
|
+
issue4(
|
|
1563
|
+
"QFAI-SC-001",
|
|
1564
|
+
"scenario.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1565
|
+
"error",
|
|
1566
|
+
entry.scenarioPath,
|
|
1567
|
+
"scenario.exists"
|
|
1568
|
+
)
|
|
1569
|
+
);
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
throw error2;
|
|
1573
|
+
}
|
|
1574
|
+
issues.push(...validateScenarioContent(text, entry.scenarioPath));
|
|
1424
1575
|
}
|
|
1425
1576
|
return issues;
|
|
1426
1577
|
}
|
|
1427
1578
|
function validateScenarioContent(text, file) {
|
|
1428
1579
|
const issues = [];
|
|
1429
|
-
const parsed = parseGherkinFeature(text, file);
|
|
1430
1580
|
const invalidIds = extractInvalidIds(text, [
|
|
1431
1581
|
"SPEC",
|
|
1432
1582
|
"BR",
|
|
@@ -1448,9 +1598,47 @@ function validateScenarioContent(text, file) {
|
|
|
1448
1598
|
)
|
|
1449
1599
|
);
|
|
1450
1600
|
}
|
|
1601
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1602
|
+
if (!document || errors.length > 0) {
|
|
1603
|
+
issues.push(
|
|
1604
|
+
issue4(
|
|
1605
|
+
"QFAI-SC-010",
|
|
1606
|
+
`Gherkin \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errors.join(", ") || "unknown"}`,
|
|
1607
|
+
"error",
|
|
1608
|
+
file,
|
|
1609
|
+
"scenario.parse"
|
|
1610
|
+
)
|
|
1611
|
+
);
|
|
1612
|
+
return issues;
|
|
1613
|
+
}
|
|
1614
|
+
const featureSpecTags = document.featureTags.filter(
|
|
1615
|
+
(tag) => SPEC_TAG_RE2.test(tag)
|
|
1616
|
+
);
|
|
1617
|
+
if (featureSpecTags.length === 0) {
|
|
1618
|
+
issues.push(
|
|
1619
|
+
issue4(
|
|
1620
|
+
"QFAI-SC-009",
|
|
1621
|
+
"Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1622
|
+
"error",
|
|
1623
|
+
file,
|
|
1624
|
+
"scenario.featureSpec"
|
|
1625
|
+
)
|
|
1626
|
+
);
|
|
1627
|
+
} else if (featureSpecTags.length > 1) {
|
|
1628
|
+
issues.push(
|
|
1629
|
+
issue4(
|
|
1630
|
+
"QFAI-SC-009",
|
|
1631
|
+
`Feature \u306E SPEC \u30BF\u30B0\u304C\u8907\u6570\u3042\u308A\u307E\u3059: ${featureSpecTags.join(", ")}`,
|
|
1632
|
+
"error",
|
|
1633
|
+
file,
|
|
1634
|
+
"scenario.featureSpec",
|
|
1635
|
+
featureSpecTags
|
|
1636
|
+
)
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1451
1639
|
const missingStructure = [];
|
|
1452
|
-
if (!
|
|
1453
|
-
if (
|
|
1640
|
+
if (!document.featureName) missingStructure.push("Feature");
|
|
1641
|
+
if (document.scenarios.length === 0) missingStructure.push("Scenario");
|
|
1454
1642
|
if (missingStructure.length > 0) {
|
|
1455
1643
|
issues.push(
|
|
1456
1644
|
issue4(
|
|
@@ -1464,7 +1652,7 @@ function validateScenarioContent(text, file) {
|
|
|
1464
1652
|
)
|
|
1465
1653
|
);
|
|
1466
1654
|
}
|
|
1467
|
-
for (const scenario of
|
|
1655
|
+
for (const scenario of document.scenarios) {
|
|
1468
1656
|
if (scenario.tags.length === 0) {
|
|
1469
1657
|
issues.push(
|
|
1470
1658
|
issue4(
|
|
@@ -1478,16 +1666,16 @@ function validateScenarioContent(text, file) {
|
|
|
1478
1666
|
continue;
|
|
1479
1667
|
}
|
|
1480
1668
|
const missingTags = [];
|
|
1481
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1669
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
|
|
1482
1670
|
if (scTags.length === 0) {
|
|
1483
1671
|
missingTags.push("SC(0\u4EF6)");
|
|
1484
1672
|
} else if (scTags.length > 1) {
|
|
1485
1673
|
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1486
1674
|
}
|
|
1487
|
-
if (!scenario.tags.some((tag) =>
|
|
1675
|
+
if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
|
|
1488
1676
|
missingTags.push("SPEC");
|
|
1489
1677
|
}
|
|
1490
|
-
if (!scenario.tags.some((tag) =>
|
|
1678
|
+
if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
|
|
1491
1679
|
missingTags.push("BR");
|
|
1492
1680
|
}
|
|
1493
1681
|
if (missingTags.length > 0) {
|
|
@@ -1502,15 +1690,16 @@ function validateScenarioContent(text, file) {
|
|
|
1502
1690
|
);
|
|
1503
1691
|
}
|
|
1504
1692
|
}
|
|
1505
|
-
for (const scenario of
|
|
1693
|
+
for (const scenario of document.scenarios) {
|
|
1506
1694
|
const missingSteps = [];
|
|
1507
|
-
|
|
1695
|
+
const keywords = scenario.steps.map((step) => step.keyword.trim());
|
|
1696
|
+
if (!keywords.some((keyword) => GIVEN_PATTERN.test(keyword))) {
|
|
1508
1697
|
missingSteps.push("Given");
|
|
1509
1698
|
}
|
|
1510
|
-
if (!WHEN_PATTERN.test(
|
|
1699
|
+
if (!keywords.some((keyword) => WHEN_PATTERN.test(keyword))) {
|
|
1511
1700
|
missingSteps.push("When");
|
|
1512
1701
|
}
|
|
1513
|
-
if (!THEN_PATTERN.test(
|
|
1702
|
+
if (!keywords.some((keyword) => THEN_PATTERN.test(keyword))) {
|
|
1514
1703
|
missingSteps.push("Then");
|
|
1515
1704
|
}
|
|
1516
1705
|
if (missingSteps.length > 0) {
|
|
@@ -1544,18 +1733,25 @@ function issue4(code, message, severity, file, rule, refs) {
|
|
|
1544
1733
|
}
|
|
1545
1734
|
return issue7;
|
|
1546
1735
|
}
|
|
1736
|
+
function isMissingFileError3(error2) {
|
|
1737
|
+
if (!error2 || typeof error2 !== "object") {
|
|
1738
|
+
return false;
|
|
1739
|
+
}
|
|
1740
|
+
return error2.code === "ENOENT";
|
|
1741
|
+
}
|
|
1547
1742
|
|
|
1548
1743
|
// src/core/validators/spec.ts
|
|
1549
|
-
var
|
|
1744
|
+
var import_promises12 = require("fs/promises");
|
|
1550
1745
|
async function validateSpecs(root, config) {
|
|
1551
1746
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1552
|
-
const
|
|
1553
|
-
if (
|
|
1554
|
-
const expected = "spec-
|
|
1747
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1748
|
+
if (entries.length === 0) {
|
|
1749
|
+
const expected = "spec-0001/spec.md";
|
|
1750
|
+
const legacy = "spec-001/spec.md";
|
|
1555
1751
|
return [
|
|
1556
1752
|
issue5(
|
|
1557
1753
|
"QFAI-SPEC-000",
|
|
1558
|
-
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}`,
|
|
1754
|
+
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected} (${legacy} \u306F\u975E\u5BFE\u5FDC)`,
|
|
1559
1755
|
"info",
|
|
1560
1756
|
specsRoot,
|
|
1561
1757
|
"spec.files"
|
|
@@ -1563,12 +1759,29 @@ async function validateSpecs(root, config) {
|
|
|
1563
1759
|
];
|
|
1564
1760
|
}
|
|
1565
1761
|
const issues = [];
|
|
1566
|
-
for (const
|
|
1567
|
-
|
|
1762
|
+
for (const entry of entries) {
|
|
1763
|
+
let text;
|
|
1764
|
+
try {
|
|
1765
|
+
text = await (0, import_promises12.readFile)(entry.specPath, "utf-8");
|
|
1766
|
+
} catch (error2) {
|
|
1767
|
+
if (isMissingFileError4(error2)) {
|
|
1768
|
+
issues.push(
|
|
1769
|
+
issue5(
|
|
1770
|
+
"QFAI-SPEC-005",
|
|
1771
|
+
"spec.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1772
|
+
"error",
|
|
1773
|
+
entry.specPath,
|
|
1774
|
+
"spec.exists"
|
|
1775
|
+
)
|
|
1776
|
+
);
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
throw error2;
|
|
1780
|
+
}
|
|
1568
1781
|
issues.push(
|
|
1569
1782
|
...validateSpecContent(
|
|
1570
1783
|
text,
|
|
1571
|
-
|
|
1784
|
+
entry.specPath,
|
|
1572
1785
|
config.validation.require.specSections
|
|
1573
1786
|
)
|
|
1574
1787
|
);
|
|
@@ -1690,15 +1903,18 @@ function issue5(code, message, severity, file, rule, refs) {
|
|
|
1690
1903
|
}
|
|
1691
1904
|
return issue7;
|
|
1692
1905
|
}
|
|
1906
|
+
function isMissingFileError4(error2) {
|
|
1907
|
+
if (!error2 || typeof error2 !== "object") {
|
|
1908
|
+
return false;
|
|
1909
|
+
}
|
|
1910
|
+
return error2.code === "ENOENT";
|
|
1911
|
+
}
|
|
1693
1912
|
|
|
1694
1913
|
// src/core/validators/traceability.ts
|
|
1695
|
-
var
|
|
1696
|
-
var
|
|
1697
|
-
var
|
|
1698
|
-
var
|
|
1699
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1700
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
1701
|
-
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1914
|
+
var import_promises13 = require("fs/promises");
|
|
1915
|
+
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1916
|
+
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1917
|
+
var BR_TAG_RE3 = /^BR-\d{4}$/;
|
|
1702
1918
|
async function validateTraceability(root, config) {
|
|
1703
1919
|
const issues = [];
|
|
1704
1920
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1717,7 +1933,7 @@ async function validateTraceability(root, config) {
|
|
|
1717
1933
|
const contractIndex = await buildContractIndex(root, config);
|
|
1718
1934
|
const contractIds = contractIndex.ids;
|
|
1719
1935
|
for (const file of specFiles) {
|
|
1720
|
-
const text = await (0,
|
|
1936
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1721
1937
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1722
1938
|
const parsed = parseSpec(text, file);
|
|
1723
1939
|
if (parsed.specId) {
|
|
@@ -1754,106 +1970,99 @@ async function validateTraceability(root, config) {
|
|
|
1754
1970
|
}
|
|
1755
1971
|
}
|
|
1756
1972
|
for (const file of scenarioFiles) {
|
|
1757
|
-
const text = await (0,
|
|
1973
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1758
1974
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1759
|
-
const
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
const scIds = /* @__PURE__ */ new Set();
|
|
1763
|
-
const scenarioIds = /* @__PURE__ */ new Set();
|
|
1764
|
-
for (const scenario of parsed.scenarios) {
|
|
1765
|
-
for (const tag of scenario.tags) {
|
|
1766
|
-
if (SPEC_TAG_RE2.test(tag)) {
|
|
1767
|
-
specIdsInScenario.add(tag);
|
|
1768
|
-
}
|
|
1769
|
-
if (BR_TAG_RE2.test(tag)) {
|
|
1770
|
-
brIds.add(tag);
|
|
1771
|
-
}
|
|
1772
|
-
if (SC_TAG_RE3.test(tag)) {
|
|
1773
|
-
scIds.add(tag);
|
|
1774
|
-
}
|
|
1775
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1776
|
-
scenarioIds.add(tag);
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
const specIdsList = Array.from(specIdsInScenario);
|
|
1781
|
-
const brIdsList = Array.from(brIds);
|
|
1782
|
-
const scIdsList = Array.from(scIds);
|
|
1783
|
-
const scenarioIdsList = Array.from(scenarioIds);
|
|
1784
|
-
brIdsList.forEach((id) => brIdsInScenarios.add(id));
|
|
1785
|
-
scIdsList.forEach((id) => scIdsInScenarios.add(id));
|
|
1786
|
-
scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
|
|
1787
|
-
if (scenarioIdsList.length > 0) {
|
|
1788
|
-
scIdsList.forEach((id) => scWithContracts.add(id));
|
|
1789
|
-
}
|
|
1790
|
-
const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
|
|
1791
|
-
if (unknownSpecIds.length > 0) {
|
|
1792
|
-
issues.push(
|
|
1793
|
-
issue6(
|
|
1794
|
-
"QFAI-TRACE-005",
|
|
1795
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1796
|
-
"error",
|
|
1797
|
-
file,
|
|
1798
|
-
"traceability.scenarioSpecExists",
|
|
1799
|
-
unknownSpecIds
|
|
1800
|
-
)
|
|
1801
|
-
);
|
|
1802
|
-
}
|
|
1803
|
-
const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
|
|
1804
|
-
if (unknownBrIds.length > 0) {
|
|
1805
|
-
issues.push(
|
|
1806
|
-
issue6(
|
|
1807
|
-
"QFAI-TRACE-006",
|
|
1808
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1809
|
-
"error",
|
|
1810
|
-
file,
|
|
1811
|
-
"traceability.scenarioBrExists",
|
|
1812
|
-
unknownBrIds
|
|
1813
|
-
)
|
|
1814
|
-
);
|
|
1815
|
-
}
|
|
1816
|
-
const unknownContractIds = scenarioIdsList.filter(
|
|
1817
|
-
(id) => !contractIds.has(id)
|
|
1818
|
-
);
|
|
1819
|
-
if (unknownContractIds.length > 0) {
|
|
1820
|
-
issues.push(
|
|
1821
|
-
issue6(
|
|
1822
|
-
"QFAI-TRACE-008",
|
|
1823
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1824
|
-
", "
|
|
1825
|
-
)}`,
|
|
1826
|
-
config.validation.traceability.unknownContractIdSeverity,
|
|
1827
|
-
file,
|
|
1828
|
-
"traceability.scenarioContractExists",
|
|
1829
|
-
unknownContractIds
|
|
1830
|
-
)
|
|
1831
|
-
);
|
|
1975
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1976
|
+
if (!document || errors.length > 0) {
|
|
1977
|
+
continue;
|
|
1832
1978
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1979
|
+
const atoms = buildScenarioAtoms(document);
|
|
1980
|
+
for (const [index, scenario] of document.scenarios.entries()) {
|
|
1981
|
+
const atom = atoms[index];
|
|
1982
|
+
if (!atom) {
|
|
1983
|
+
continue;
|
|
1984
|
+
}
|
|
1985
|
+
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
1986
|
+
const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
|
|
1987
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
|
|
1988
|
+
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
1989
|
+
scTags.forEach((id) => scIdsInScenarios.add(id));
|
|
1990
|
+
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
1991
|
+
if (atom.contractIds.length > 0) {
|
|
1992
|
+
scTags.forEach((id) => scWithContracts.add(id));
|
|
1841
1993
|
}
|
|
1842
|
-
const
|
|
1843
|
-
if (
|
|
1994
|
+
const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
|
|
1995
|
+
if (unknownSpecIds.length > 0) {
|
|
1844
1996
|
issues.push(
|
|
1845
1997
|
issue6(
|
|
1846
|
-
"QFAI-TRACE-
|
|
1847
|
-
`Scenario \
|
|
1998
|
+
"QFAI-TRACE-005",
|
|
1999
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(
|
|
1848
2000
|
", "
|
|
1849
|
-
)} (
|
|
2001
|
+
)} (${scenario.name})`,
|
|
1850
2002
|
"error",
|
|
1851
2003
|
file,
|
|
1852
|
-
"traceability.
|
|
1853
|
-
|
|
2004
|
+
"traceability.scenarioSpecExists",
|
|
2005
|
+
unknownSpecIds
|
|
1854
2006
|
)
|
|
1855
2007
|
);
|
|
1856
2008
|
}
|
|
2009
|
+
const unknownBrIds = brTags.filter((id) => !brIdsInSpecs.has(id));
|
|
2010
|
+
if (unknownBrIds.length > 0) {
|
|
2011
|
+
issues.push(
|
|
2012
|
+
issue6(
|
|
2013
|
+
"QFAI-TRACE-006",
|
|
2014
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(
|
|
2015
|
+
", "
|
|
2016
|
+
)} (${scenario.name})`,
|
|
2017
|
+
"error",
|
|
2018
|
+
file,
|
|
2019
|
+
"traceability.scenarioBrExists",
|
|
2020
|
+
unknownBrIds
|
|
2021
|
+
)
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
const unknownContractIds = atom.contractIds.filter(
|
|
2025
|
+
(id) => !contractIds.has(id)
|
|
2026
|
+
);
|
|
2027
|
+
if (unknownContractIds.length > 0) {
|
|
2028
|
+
issues.push(
|
|
2029
|
+
issue6(
|
|
2030
|
+
"QFAI-TRACE-008",
|
|
2031
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
2032
|
+
", "
|
|
2033
|
+
)} (${scenario.name})`,
|
|
2034
|
+
config.validation.traceability.unknownContractIdSeverity,
|
|
2035
|
+
file,
|
|
2036
|
+
"traceability.scenarioContractExists",
|
|
2037
|
+
unknownContractIds
|
|
2038
|
+
)
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
if (specTags.length > 0 && brTags.length > 0) {
|
|
2042
|
+
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
2043
|
+
for (const specId of specTags) {
|
|
2044
|
+
const brIdsForSpec = specToBrIds.get(specId);
|
|
2045
|
+
if (!brIdsForSpec) {
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
2049
|
+
}
|
|
2050
|
+
const invalidBrIds = brTags.filter((id) => !allowedBrIds.has(id));
|
|
2051
|
+
if (invalidBrIds.length > 0) {
|
|
2052
|
+
issues.push(
|
|
2053
|
+
issue6(
|
|
2054
|
+
"QFAI-TRACE-007",
|
|
2055
|
+
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
2056
|
+
", "
|
|
2057
|
+
)} (SPEC: ${specTags.join(", ")}) (${scenario.name})`,
|
|
2058
|
+
"error",
|
|
2059
|
+
file,
|
|
2060
|
+
"traceability.scenarioBrUnderSpec",
|
|
2061
|
+
invalidBrIds
|
|
2062
|
+
)
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
1857
2066
|
}
|
|
1858
2067
|
}
|
|
1859
2068
|
if (upstreamIds.size === 0) {
|
|
@@ -1951,7 +2160,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1951
2160
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
1952
2161
|
let found = false;
|
|
1953
2162
|
for (const file of targetFiles) {
|
|
1954
|
-
const text = await (0,
|
|
2163
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1955
2164
|
if (pattern.test(text)) {
|
|
1956
2165
|
found = true;
|
|
1957
2166
|
break;
|
|
@@ -2178,7 +2387,7 @@ async function collectIds(files) {
|
|
|
2178
2387
|
DATA: /* @__PURE__ */ new Set()
|
|
2179
2388
|
};
|
|
2180
2389
|
for (const file of files) {
|
|
2181
|
-
const text = await (0,
|
|
2390
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2182
2391
|
for (const prefix of ID_PREFIXES2) {
|
|
2183
2392
|
const ids = extractIds(text, prefix);
|
|
2184
2393
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -2196,7 +2405,7 @@ async function collectIds(files) {
|
|
|
2196
2405
|
async function collectUpstreamIds(files) {
|
|
2197
2406
|
const ids = /* @__PURE__ */ new Set();
|
|
2198
2407
|
for (const file of files) {
|
|
2199
|
-
const text = await (0,
|
|
2408
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2200
2409
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
2201
2410
|
}
|
|
2202
2411
|
return ids;
|
|
@@ -2217,7 +2426,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
2217
2426
|
}
|
|
2218
2427
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
2219
2428
|
for (const file of targetFiles) {
|
|
2220
|
-
const text = await (0,
|
|
2429
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2221
2430
|
if (pattern.test(text)) {
|
|
2222
2431
|
return true;
|
|
2223
2432
|
}
|
|
@@ -2269,7 +2478,7 @@ async function runReport(options) {
|
|
|
2269
2478
|
try {
|
|
2270
2479
|
validation = await readValidationResult(inputPath);
|
|
2271
2480
|
} catch (err) {
|
|
2272
|
-
if (
|
|
2481
|
+
if (isMissingFileError5(err)) {
|
|
2273
2482
|
error(
|
|
2274
2483
|
[
|
|
2275
2484
|
`qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
|
|
@@ -2292,8 +2501,8 @@ async function runReport(options) {
|
|
|
2292
2501
|
const defaultOut = options.format === "json" ? import_node_path14.default.join(outRoot, "report.json") : import_node_path14.default.join(outRoot, "report.md");
|
|
2293
2502
|
const out = options.outPath ?? defaultOut;
|
|
2294
2503
|
const outPath = import_node_path14.default.isAbsolute(out) ? out : import_node_path14.default.resolve(root, out);
|
|
2295
|
-
await (0,
|
|
2296
|
-
await (0,
|
|
2504
|
+
await (0, import_promises15.mkdir)(import_node_path14.default.dirname(outPath), { recursive: true });
|
|
2505
|
+
await (0, import_promises15.writeFile)(outPath, `${output}
|
|
2297
2506
|
`, "utf-8");
|
|
2298
2507
|
info(
|
|
2299
2508
|
`report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
|
|
@@ -2301,7 +2510,7 @@ async function runReport(options) {
|
|
|
2301
2510
|
info(`wrote report: ${outPath}`);
|
|
2302
2511
|
}
|
|
2303
2512
|
async function readValidationResult(inputPath) {
|
|
2304
|
-
const raw = await (0,
|
|
2513
|
+
const raw = await (0, import_promises15.readFile)(inputPath, "utf-8");
|
|
2305
2514
|
const parsed = JSON.parse(raw);
|
|
2306
2515
|
if (!isValidationResult(parsed)) {
|
|
2307
2516
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
@@ -2333,7 +2542,7 @@ function isValidationResult(value) {
|
|
|
2333
2542
|
}
|
|
2334
2543
|
return typeof counts.info === "number" && typeof counts.warning === "number" && typeof counts.error === "number";
|
|
2335
2544
|
}
|
|
2336
|
-
function
|
|
2545
|
+
function isMissingFileError5(error2) {
|
|
2337
2546
|
if (!error2 || typeof error2 !== "object") {
|
|
2338
2547
|
return false;
|
|
2339
2548
|
}
|
|
@@ -2342,7 +2551,7 @@ function isMissingFileError2(error2) {
|
|
|
2342
2551
|
}
|
|
2343
2552
|
|
|
2344
2553
|
// src/cli/commands/validate.ts
|
|
2345
|
-
var
|
|
2554
|
+
var import_promises16 = require("fs/promises");
|
|
2346
2555
|
var import_node_path15 = __toESM(require("path"), 1);
|
|
2347
2556
|
|
|
2348
2557
|
// src/cli/lib/failOn.ts
|
|
@@ -2408,8 +2617,8 @@ function emitGitHub(issue7) {
|
|
|
2408
2617
|
}
|
|
2409
2618
|
async function emitJson(result, root, jsonPath) {
|
|
2410
2619
|
const abs = import_node_path15.default.isAbsolute(jsonPath) ? jsonPath : import_node_path15.default.resolve(root, jsonPath);
|
|
2411
|
-
await (0,
|
|
2412
|
-
await (0,
|
|
2620
|
+
await (0, import_promises16.mkdir)(import_node_path15.default.dirname(abs), { recursive: true });
|
|
2621
|
+
await (0, import_promises16.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
2413
2622
|
`, "utf-8");
|
|
2414
2623
|
}
|
|
2415
2624
|
|