qfai 0.3.1 → 0.3.3
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 +7 -1
- package/assets/init/.qfai/README.md +2 -0
- package/assets/init/.qfai/prompts/README.md +6 -0
- package/assets/init/.qfai/prompts/require-to-spec.md +39 -0
- package/assets/init/.qfai/rules/pnpm.md +29 -0
- package/assets/init/.qfai/specs/README.md +5 -5
- package/assets/init/root/require/README.md +28 -0
- package/dist/cli/index.cjs +468 -242
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +440 -210
- 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/index.cjs
CHANGED
|
@@ -442,11 +442,11 @@ function isValidId(value, prefix) {
|
|
|
442
442
|
}
|
|
443
443
|
|
|
444
444
|
// src/core/report.ts
|
|
445
|
-
var
|
|
445
|
+
var import_promises13 = require("fs/promises");
|
|
446
446
|
var import_node_path10 = __toESM(require("path"), 1);
|
|
447
447
|
|
|
448
448
|
// src/core/discovery.ts
|
|
449
|
-
var
|
|
449
|
+
var import_promises4 = require("fs/promises");
|
|
450
450
|
|
|
451
451
|
// src/core/fs.ts
|
|
452
452
|
var import_promises2 = require("fs/promises");
|
|
@@ -503,25 +503,50 @@ async function exists(target) {
|
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
-
// src/core/
|
|
507
|
-
var
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
506
|
+
// src/core/specLayout.ts
|
|
507
|
+
var import_promises3 = require("fs/promises");
|
|
508
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
509
|
+
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
510
|
+
async function collectSpecEntries(specsRoot) {
|
|
511
|
+
const dirs = await listSpecDirs(specsRoot);
|
|
512
|
+
const entries = dirs.map((dir) => ({
|
|
513
|
+
dir,
|
|
514
|
+
specPath: import_node_path3.default.join(dir, "spec.md"),
|
|
515
|
+
deltaPath: import_node_path3.default.join(dir, "delta.md"),
|
|
516
|
+
scenarioPath: import_node_path3.default.join(dir, "scenario.md")
|
|
517
|
+
}));
|
|
518
|
+
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
519
|
+
}
|
|
520
|
+
async function listSpecDirs(specsRoot) {
|
|
521
|
+
try {
|
|
522
|
+
const items = await (0, import_promises3.readdir)(specsRoot, { withFileTypes: true });
|
|
523
|
+
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => import_node_path3.default.join(specsRoot, name));
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (isMissingFileError(error)) {
|
|
526
|
+
return [];
|
|
514
527
|
}
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function isMissingFileError(error) {
|
|
532
|
+
if (!error || typeof error !== "object") {
|
|
533
|
+
return false;
|
|
515
534
|
}
|
|
516
|
-
return
|
|
535
|
+
return error.code === "ENOENT";
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/core/discovery.ts
|
|
539
|
+
async function collectSpecPackDirs(specsRoot) {
|
|
540
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
541
|
+
return entries.map((entry) => entry.dir);
|
|
517
542
|
}
|
|
518
543
|
async function collectSpecFiles(specsRoot) {
|
|
519
|
-
const
|
|
520
|
-
return
|
|
544
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
545
|
+
return filterExisting(entries.map((entry) => entry.specPath));
|
|
521
546
|
}
|
|
522
547
|
async function collectScenarioFiles(specsRoot) {
|
|
523
|
-
const
|
|
524
|
-
return
|
|
548
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
549
|
+
return filterExisting(entries.map((entry) => entry.scenarioPath));
|
|
525
550
|
}
|
|
526
551
|
async function collectUiContractFiles(uiRoot) {
|
|
527
552
|
return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
@@ -540,28 +565,38 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
|
540
565
|
]);
|
|
541
566
|
return { ui, api, db };
|
|
542
567
|
}
|
|
543
|
-
function
|
|
544
|
-
|
|
568
|
+
async function filterExisting(files) {
|
|
569
|
+
const existing = [];
|
|
570
|
+
for (const file of files) {
|
|
571
|
+
if (await exists2(file)) {
|
|
572
|
+
existing.push(file);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return existing;
|
|
576
|
+
}
|
|
577
|
+
async function exists2(target) {
|
|
578
|
+
try {
|
|
579
|
+
await (0, import_promises4.access)(target);
|
|
580
|
+
return true;
|
|
581
|
+
} catch {
|
|
545
582
|
return false;
|
|
546
583
|
}
|
|
547
|
-
const dirName = import_node_path3.default.basename(import_node_path3.default.dirname(filePath)).toLowerCase();
|
|
548
|
-
return SPEC_PACK_DIR_PATTERN.test(dirName);
|
|
549
584
|
}
|
|
550
585
|
|
|
551
586
|
// src/core/types.ts
|
|
552
587
|
var VALIDATION_SCHEMA_VERSION = "0.2";
|
|
553
588
|
|
|
554
589
|
// src/core/version.ts
|
|
555
|
-
var
|
|
590
|
+
var import_promises5 = require("fs/promises");
|
|
556
591
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
557
592
|
var import_node_url = require("url");
|
|
558
593
|
async function resolveToolVersion() {
|
|
559
|
-
if ("0.3.
|
|
560
|
-
return "0.3.
|
|
594
|
+
if ("0.3.3".length > 0) {
|
|
595
|
+
return "0.3.3";
|
|
561
596
|
}
|
|
562
597
|
try {
|
|
563
598
|
const packagePath = resolvePackageJsonPath();
|
|
564
|
-
const raw = await (0,
|
|
599
|
+
const raw = await (0, import_promises5.readFile)(packagePath, "utf-8");
|
|
565
600
|
const parsed = JSON.parse(raw);
|
|
566
601
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
567
602
|
return version.length > 0 ? version : "unknown";
|
|
@@ -576,7 +611,7 @@ function resolvePackageJsonPath() {
|
|
|
576
611
|
}
|
|
577
612
|
|
|
578
613
|
// src/core/validators/contracts.ts
|
|
579
|
-
var
|
|
614
|
+
var import_promises6 = require("fs/promises");
|
|
580
615
|
var import_node_path6 = __toESM(require("path"), 1);
|
|
581
616
|
|
|
582
617
|
// src/core/contracts.ts
|
|
@@ -654,7 +689,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
654
689
|
}
|
|
655
690
|
const issues = [];
|
|
656
691
|
for (const file of files) {
|
|
657
|
-
const text = await (0,
|
|
692
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
658
693
|
const invalidIds = extractInvalidIds(text, [
|
|
659
694
|
"SPEC",
|
|
660
695
|
"BR",
|
|
@@ -721,7 +756,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
721
756
|
}
|
|
722
757
|
const issues = [];
|
|
723
758
|
for (const file of files) {
|
|
724
|
-
const text = await (0,
|
|
759
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
725
760
|
const invalidIds = extractInvalidIds(text, [
|
|
726
761
|
"SPEC",
|
|
727
762
|
"BR",
|
|
@@ -799,7 +834,7 @@ async function validateDataContracts(dataRoot) {
|
|
|
799
834
|
}
|
|
800
835
|
const issues = [];
|
|
801
836
|
for (const file of files) {
|
|
802
|
-
const text = await (0,
|
|
837
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
803
838
|
const invalidIds = extractInvalidIds(text, [
|
|
804
839
|
"SPEC",
|
|
805
840
|
"BR",
|
|
@@ -870,7 +905,7 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
870
905
|
}
|
|
871
906
|
|
|
872
907
|
// src/core/validators/delta.ts
|
|
873
|
-
var
|
|
908
|
+
var import_promises7 = require("fs/promises");
|
|
874
909
|
var import_node_path7 = __toESM(require("path"), 1);
|
|
875
910
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
876
911
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
@@ -888,9 +923,9 @@ async function validateDeltas(root, config) {
|
|
|
888
923
|
const deltaPath = import_node_path7.default.join(pack, "delta.md");
|
|
889
924
|
let text;
|
|
890
925
|
try {
|
|
891
|
-
text = await (0,
|
|
926
|
+
text = await (0, import_promises7.readFile)(deltaPath, "utf-8");
|
|
892
927
|
} catch (error) {
|
|
893
|
-
if (
|
|
928
|
+
if (isMissingFileError2(error)) {
|
|
894
929
|
issues.push(
|
|
895
930
|
issue2(
|
|
896
931
|
"QFAI-DELTA-001",
|
|
@@ -935,7 +970,7 @@ async function validateDeltas(root, config) {
|
|
|
935
970
|
}
|
|
936
971
|
return issues;
|
|
937
972
|
}
|
|
938
|
-
function
|
|
973
|
+
function isMissingFileError2(error) {
|
|
939
974
|
if (!error || typeof error !== "object") {
|
|
940
975
|
return false;
|
|
941
976
|
}
|
|
@@ -960,11 +995,11 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
960
995
|
}
|
|
961
996
|
|
|
962
997
|
// src/core/validators/ids.ts
|
|
963
|
-
var
|
|
998
|
+
var import_promises9 = require("fs/promises");
|
|
964
999
|
var import_node_path9 = __toESM(require("path"), 1);
|
|
965
1000
|
|
|
966
1001
|
// src/core/contractIndex.ts
|
|
967
|
-
var
|
|
1002
|
+
var import_promises8 = require("fs/promises");
|
|
968
1003
|
var import_node_path8 = __toESM(require("path"), 1);
|
|
969
1004
|
async function buildContractIndex(root, config) {
|
|
970
1005
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
@@ -989,7 +1024,7 @@ async function buildContractIndex(root, config) {
|
|
|
989
1024
|
}
|
|
990
1025
|
async function indexUiContracts(files, index) {
|
|
991
1026
|
for (const file of files) {
|
|
992
|
-
const text = await (0,
|
|
1027
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
993
1028
|
try {
|
|
994
1029
|
const doc = parseStructuredContract(file, text);
|
|
995
1030
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1001,7 +1036,7 @@ async function indexUiContracts(files, index) {
|
|
|
1001
1036
|
}
|
|
1002
1037
|
async function indexApiContracts(files, index) {
|
|
1003
1038
|
for (const file of files) {
|
|
1004
|
-
const text = await (0,
|
|
1039
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
1005
1040
|
try {
|
|
1006
1041
|
const doc = parseStructuredContract(file, text);
|
|
1007
1042
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1013,7 +1048,7 @@ async function indexApiContracts(files, index) {
|
|
|
1013
1048
|
}
|
|
1014
1049
|
async function indexDataContracts(files, index) {
|
|
1015
1050
|
for (const file of files) {
|
|
1016
|
-
const text = await (0,
|
|
1051
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
1017
1052
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1018
1053
|
}
|
|
1019
1054
|
}
|
|
@@ -1024,66 +1059,6 @@ function record(index, id, file) {
|
|
|
1024
1059
|
index.idToFiles.set(id, current);
|
|
1025
1060
|
}
|
|
1026
1061
|
|
|
1027
|
-
// src/core/parse/gherkin.ts
|
|
1028
|
-
var FEATURE_RE = /^\s*Feature:\s+/;
|
|
1029
|
-
var SCENARIO_RE = /^\s*Scenario(?: Outline)?:\s*(.+)\s*$/;
|
|
1030
|
-
var TAG_LINE_RE = /^\s*@/;
|
|
1031
|
-
function parseTags(line) {
|
|
1032
|
-
return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
|
|
1033
|
-
}
|
|
1034
|
-
function parseGherkinFeature(text, file) {
|
|
1035
|
-
const lines = text.split(/\r?\n/);
|
|
1036
|
-
const scenarios = [];
|
|
1037
|
-
let featurePresent = false;
|
|
1038
|
-
let featureTags = [];
|
|
1039
|
-
let pendingTags = [];
|
|
1040
|
-
let current = null;
|
|
1041
|
-
const flush = () => {
|
|
1042
|
-
if (!current) return;
|
|
1043
|
-
scenarios.push({
|
|
1044
|
-
...current,
|
|
1045
|
-
body: current.body.trim()
|
|
1046
|
-
});
|
|
1047
|
-
current = null;
|
|
1048
|
-
};
|
|
1049
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1050
|
-
const line = lines[i] ?? "";
|
|
1051
|
-
const trimmed = line.trim();
|
|
1052
|
-
if (TAG_LINE_RE.test(trimmed)) {
|
|
1053
|
-
pendingTags.push(...parseTags(trimmed));
|
|
1054
|
-
continue;
|
|
1055
|
-
}
|
|
1056
|
-
if (FEATURE_RE.test(trimmed)) {
|
|
1057
|
-
featurePresent = true;
|
|
1058
|
-
featureTags = [...pendingTags];
|
|
1059
|
-
pendingTags = [];
|
|
1060
|
-
continue;
|
|
1061
|
-
}
|
|
1062
|
-
const match = trimmed.match(SCENARIO_RE);
|
|
1063
|
-
if (match) {
|
|
1064
|
-
const scenarioName = match[1]?.trim();
|
|
1065
|
-
if (!scenarioName) {
|
|
1066
|
-
continue;
|
|
1067
|
-
}
|
|
1068
|
-
flush();
|
|
1069
|
-
current = {
|
|
1070
|
-
name: scenarioName,
|
|
1071
|
-
line: i + 1,
|
|
1072
|
-
tags: [...featureTags, ...pendingTags],
|
|
1073
|
-
body: ""
|
|
1074
|
-
};
|
|
1075
|
-
pendingTags = [];
|
|
1076
|
-
continue;
|
|
1077
|
-
}
|
|
1078
|
-
if (current) {
|
|
1079
|
-
current.body += `${line}
|
|
1080
|
-
`;
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
flush();
|
|
1084
|
-
return { file, featurePresent, scenarios };
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
1062
|
// src/core/parse/markdown.ts
|
|
1088
1063
|
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1089
1064
|
function parseHeadings(md) {
|
|
@@ -1202,8 +1177,162 @@ function parseSpec(md, file) {
|
|
|
1202
1177
|
return parsed;
|
|
1203
1178
|
}
|
|
1204
1179
|
|
|
1205
|
-
// src/core/
|
|
1180
|
+
// src/core/gherkin/parse.ts
|
|
1181
|
+
var import_gherkin = require("@cucumber/gherkin");
|
|
1182
|
+
var import_node_crypto = require("crypto");
|
|
1183
|
+
function parseGherkin(source, uri) {
|
|
1184
|
+
const errors = [];
|
|
1185
|
+
const uuidFn = () => (0, import_node_crypto.randomUUID)();
|
|
1186
|
+
const builder = new import_gherkin.AstBuilder(uuidFn);
|
|
1187
|
+
const matcher = new import_gherkin.GherkinClassicTokenMatcher();
|
|
1188
|
+
const parser = new import_gherkin.Parser(builder, matcher);
|
|
1189
|
+
try {
|
|
1190
|
+
const gherkinDocument = parser.parse(source);
|
|
1191
|
+
gherkinDocument.uri = uri;
|
|
1192
|
+
return { gherkinDocument, errors };
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
errors.push(formatError3(error));
|
|
1195
|
+
return { gherkinDocument: null, errors };
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
function formatError3(error) {
|
|
1199
|
+
if (error instanceof Error) {
|
|
1200
|
+
return error.message;
|
|
1201
|
+
}
|
|
1202
|
+
return String(error);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/core/scenarioModel.ts
|
|
1206
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1206
1207
|
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1208
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1209
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1210
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
1211
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1212
|
+
function parseScenarioDocument(text, uri) {
|
|
1213
|
+
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1214
|
+
if (!gherkinDocument) {
|
|
1215
|
+
return { document: null, errors };
|
|
1216
|
+
}
|
|
1217
|
+
const feature = gherkinDocument.feature;
|
|
1218
|
+
if (!feature) {
|
|
1219
|
+
return {
|
|
1220
|
+
document: { uri, featureTags: [], scenarios: [] },
|
|
1221
|
+
errors
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
const featureTags = collectTagNames(feature.tags);
|
|
1225
|
+
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
1226
|
+
return {
|
|
1227
|
+
document: {
|
|
1228
|
+
uri,
|
|
1229
|
+
featureName: feature.name,
|
|
1230
|
+
featureTags,
|
|
1231
|
+
scenarios
|
|
1232
|
+
},
|
|
1233
|
+
errors
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
function buildScenarioAtoms(document) {
|
|
1237
|
+
return document.scenarios.map((scenario) => {
|
|
1238
|
+
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1239
|
+
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1240
|
+
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1241
|
+
const contractIds = /* @__PURE__ */ new Set();
|
|
1242
|
+
scenario.tags.forEach((tag) => {
|
|
1243
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1244
|
+
contractIds.add(tag);
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
for (const step of scenario.steps) {
|
|
1248
|
+
for (const text of collectStepTexts(step)) {
|
|
1249
|
+
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1250
|
+
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1251
|
+
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
const atom = {
|
|
1255
|
+
uri: document.uri,
|
|
1256
|
+
featureName: document.featureName ?? "",
|
|
1257
|
+
scenarioName: scenario.name,
|
|
1258
|
+
kind: scenario.kind,
|
|
1259
|
+
brIds,
|
|
1260
|
+
contractIds: Array.from(contractIds).sort()
|
|
1261
|
+
};
|
|
1262
|
+
if (scenario.line !== void 0) {
|
|
1263
|
+
atom.line = scenario.line;
|
|
1264
|
+
}
|
|
1265
|
+
if (specIds.length === 1) {
|
|
1266
|
+
const specId = specIds[0];
|
|
1267
|
+
if (specId) {
|
|
1268
|
+
atom.specId = specId;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (scIds.length === 1) {
|
|
1272
|
+
const scId = scIds[0];
|
|
1273
|
+
if (scId) {
|
|
1274
|
+
atom.scId = scId;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return atom;
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
function collectScenarioNodes(feature, featureTags) {
|
|
1281
|
+
const scenarios = [];
|
|
1282
|
+
for (const child of feature.children) {
|
|
1283
|
+
if (child.scenario) {
|
|
1284
|
+
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
1285
|
+
}
|
|
1286
|
+
if (child.rule) {
|
|
1287
|
+
const ruleTags = collectTagNames(child.rule.tags);
|
|
1288
|
+
for (const ruleChild of child.rule.children) {
|
|
1289
|
+
if (ruleChild.scenario) {
|
|
1290
|
+
scenarios.push(
|
|
1291
|
+
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return scenarios;
|
|
1298
|
+
}
|
|
1299
|
+
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
1300
|
+
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
1301
|
+
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
1302
|
+
return {
|
|
1303
|
+
name: scenario.name,
|
|
1304
|
+
kind,
|
|
1305
|
+
line: scenario.location?.line,
|
|
1306
|
+
tags,
|
|
1307
|
+
steps: scenario.steps
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function collectTagNames(tags) {
|
|
1311
|
+
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1312
|
+
}
|
|
1313
|
+
function collectStepTexts(step) {
|
|
1314
|
+
const texts = [];
|
|
1315
|
+
if (step.text) {
|
|
1316
|
+
texts.push(step.text);
|
|
1317
|
+
}
|
|
1318
|
+
if (step.docString?.content) {
|
|
1319
|
+
texts.push(step.docString.content);
|
|
1320
|
+
}
|
|
1321
|
+
if (step.dataTable?.rows) {
|
|
1322
|
+
for (const row of step.dataTable.rows) {
|
|
1323
|
+
for (const cell of row.cells) {
|
|
1324
|
+
texts.push(cell.value);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
return texts;
|
|
1329
|
+
}
|
|
1330
|
+
function unique2(values) {
|
|
1331
|
+
return Array.from(new Set(values));
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// src/core/validators/ids.ts
|
|
1335
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1207
1336
|
async function validateDefinedIds(root, config) {
|
|
1208
1337
|
const issues = [];
|
|
1209
1338
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1237,7 +1366,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1237
1366
|
}
|
|
1238
1367
|
async function collectSpecDefinitionIds(files, out) {
|
|
1239
1368
|
for (const file of files) {
|
|
1240
|
-
const text = await (0,
|
|
1369
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1241
1370
|
const parsed = parseSpec(text, file);
|
|
1242
1371
|
if (parsed.specId) {
|
|
1243
1372
|
recordId(out, parsed.specId, file);
|
|
@@ -1247,11 +1376,14 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1247
1376
|
}
|
|
1248
1377
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1249
1378
|
for (const file of files) {
|
|
1250
|
-
const text = await (0,
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1379
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1380
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1381
|
+
if (!document || errors.length > 0) {
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
for (const scenario of document.scenarios) {
|
|
1253
1385
|
for (const tag of scenario.tags) {
|
|
1254
|
-
if (
|
|
1386
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
1255
1387
|
recordId(out, tag, file);
|
|
1256
1388
|
}
|
|
1257
1389
|
}
|
|
@@ -1288,21 +1420,23 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1288
1420
|
}
|
|
1289
1421
|
|
|
1290
1422
|
// src/core/validators/scenario.ts
|
|
1291
|
-
var
|
|
1423
|
+
var import_promises10 = require("fs/promises");
|
|
1292
1424
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1293
1425
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1294
1426
|
var THEN_PATTERN = /\bThen\b/;
|
|
1295
|
-
var
|
|
1296
|
-
var
|
|
1297
|
-
var
|
|
1427
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1428
|
+
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1429
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1298
1430
|
async function validateScenarios(root, config) {
|
|
1299
1431
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1300
|
-
const
|
|
1301
|
-
if (
|
|
1432
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1433
|
+
if (entries.length === 0) {
|
|
1434
|
+
const expected = "spec-0001/scenario.md";
|
|
1435
|
+
const legacy = "spec-001/scenario.md";
|
|
1302
1436
|
return [
|
|
1303
1437
|
issue4(
|
|
1304
1438
|
"QFAI-SC-000",
|
|
1305
|
-
|
|
1439
|
+
`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)`,
|
|
1306
1440
|
"info",
|
|
1307
1441
|
specsRoot,
|
|
1308
1442
|
"scenario.files"
|
|
@@ -1310,15 +1444,31 @@ async function validateScenarios(root, config) {
|
|
|
1310
1444
|
];
|
|
1311
1445
|
}
|
|
1312
1446
|
const issues = [];
|
|
1313
|
-
for (const
|
|
1314
|
-
|
|
1315
|
-
|
|
1447
|
+
for (const entry of entries) {
|
|
1448
|
+
let text;
|
|
1449
|
+
try {
|
|
1450
|
+
text = await (0, import_promises10.readFile)(entry.scenarioPath, "utf-8");
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
if (isMissingFileError3(error)) {
|
|
1453
|
+
issues.push(
|
|
1454
|
+
issue4(
|
|
1455
|
+
"QFAI-SC-001",
|
|
1456
|
+
"scenario.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1457
|
+
"error",
|
|
1458
|
+
entry.scenarioPath,
|
|
1459
|
+
"scenario.exists"
|
|
1460
|
+
)
|
|
1461
|
+
);
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
throw error;
|
|
1465
|
+
}
|
|
1466
|
+
issues.push(...validateScenarioContent(text, entry.scenarioPath));
|
|
1316
1467
|
}
|
|
1317
1468
|
return issues;
|
|
1318
1469
|
}
|
|
1319
1470
|
function validateScenarioContent(text, file) {
|
|
1320
1471
|
const issues = [];
|
|
1321
|
-
const parsed = parseGherkinFeature(text, file);
|
|
1322
1472
|
const invalidIds = extractInvalidIds(text, [
|
|
1323
1473
|
"SPEC",
|
|
1324
1474
|
"BR",
|
|
@@ -1340,9 +1490,47 @@ function validateScenarioContent(text, file) {
|
|
|
1340
1490
|
)
|
|
1341
1491
|
);
|
|
1342
1492
|
}
|
|
1493
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1494
|
+
if (!document || errors.length > 0) {
|
|
1495
|
+
issues.push(
|
|
1496
|
+
issue4(
|
|
1497
|
+
"QFAI-SC-010",
|
|
1498
|
+
`Gherkin \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errors.join(", ") || "unknown"}`,
|
|
1499
|
+
"error",
|
|
1500
|
+
file,
|
|
1501
|
+
"scenario.parse"
|
|
1502
|
+
)
|
|
1503
|
+
);
|
|
1504
|
+
return issues;
|
|
1505
|
+
}
|
|
1506
|
+
const featureSpecTags = document.featureTags.filter(
|
|
1507
|
+
(tag) => SPEC_TAG_RE2.test(tag)
|
|
1508
|
+
);
|
|
1509
|
+
if (featureSpecTags.length === 0) {
|
|
1510
|
+
issues.push(
|
|
1511
|
+
issue4(
|
|
1512
|
+
"QFAI-SC-009",
|
|
1513
|
+
"Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1514
|
+
"error",
|
|
1515
|
+
file,
|
|
1516
|
+
"scenario.featureSpec"
|
|
1517
|
+
)
|
|
1518
|
+
);
|
|
1519
|
+
} else if (featureSpecTags.length > 1) {
|
|
1520
|
+
issues.push(
|
|
1521
|
+
issue4(
|
|
1522
|
+
"QFAI-SC-009",
|
|
1523
|
+
`Feature \u306E SPEC \u30BF\u30B0\u304C\u8907\u6570\u3042\u308A\u307E\u3059: ${featureSpecTags.join(", ")}`,
|
|
1524
|
+
"error",
|
|
1525
|
+
file,
|
|
1526
|
+
"scenario.featureSpec",
|
|
1527
|
+
featureSpecTags
|
|
1528
|
+
)
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1343
1531
|
const missingStructure = [];
|
|
1344
|
-
if (!
|
|
1345
|
-
if (
|
|
1532
|
+
if (!document.featureName) missingStructure.push("Feature");
|
|
1533
|
+
if (document.scenarios.length === 0) missingStructure.push("Scenario");
|
|
1346
1534
|
if (missingStructure.length > 0) {
|
|
1347
1535
|
issues.push(
|
|
1348
1536
|
issue4(
|
|
@@ -1356,7 +1544,7 @@ function validateScenarioContent(text, file) {
|
|
|
1356
1544
|
)
|
|
1357
1545
|
);
|
|
1358
1546
|
}
|
|
1359
|
-
for (const scenario of
|
|
1547
|
+
for (const scenario of document.scenarios) {
|
|
1360
1548
|
if (scenario.tags.length === 0) {
|
|
1361
1549
|
issues.push(
|
|
1362
1550
|
issue4(
|
|
@@ -1370,16 +1558,16 @@ function validateScenarioContent(text, file) {
|
|
|
1370
1558
|
continue;
|
|
1371
1559
|
}
|
|
1372
1560
|
const missingTags = [];
|
|
1373
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1561
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
|
|
1374
1562
|
if (scTags.length === 0) {
|
|
1375
1563
|
missingTags.push("SC(0\u4EF6)");
|
|
1376
1564
|
} else if (scTags.length > 1) {
|
|
1377
1565
|
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1378
1566
|
}
|
|
1379
|
-
if (!scenario.tags.some((tag) =>
|
|
1567
|
+
if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
|
|
1380
1568
|
missingTags.push("SPEC");
|
|
1381
1569
|
}
|
|
1382
|
-
if (!scenario.tags.some((tag) =>
|
|
1570
|
+
if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
|
|
1383
1571
|
missingTags.push("BR");
|
|
1384
1572
|
}
|
|
1385
1573
|
if (missingTags.length > 0) {
|
|
@@ -1394,15 +1582,16 @@ function validateScenarioContent(text, file) {
|
|
|
1394
1582
|
);
|
|
1395
1583
|
}
|
|
1396
1584
|
}
|
|
1397
|
-
for (const scenario of
|
|
1585
|
+
for (const scenario of document.scenarios) {
|
|
1398
1586
|
const missingSteps = [];
|
|
1399
|
-
|
|
1587
|
+
const keywords = scenario.steps.map((step) => step.keyword.trim());
|
|
1588
|
+
if (!keywords.some((keyword) => GIVEN_PATTERN.test(keyword))) {
|
|
1400
1589
|
missingSteps.push("Given");
|
|
1401
1590
|
}
|
|
1402
|
-
if (!WHEN_PATTERN.test(
|
|
1591
|
+
if (!keywords.some((keyword) => WHEN_PATTERN.test(keyword))) {
|
|
1403
1592
|
missingSteps.push("When");
|
|
1404
1593
|
}
|
|
1405
|
-
if (!THEN_PATTERN.test(
|
|
1594
|
+
if (!keywords.some((keyword) => THEN_PATTERN.test(keyword))) {
|
|
1406
1595
|
missingSteps.push("Then");
|
|
1407
1596
|
}
|
|
1408
1597
|
if (missingSteps.length > 0) {
|
|
@@ -1436,18 +1625,25 @@ function issue4(code, message, severity, file, rule, refs) {
|
|
|
1436
1625
|
}
|
|
1437
1626
|
return issue7;
|
|
1438
1627
|
}
|
|
1628
|
+
function isMissingFileError3(error) {
|
|
1629
|
+
if (!error || typeof error !== "object") {
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
return error.code === "ENOENT";
|
|
1633
|
+
}
|
|
1439
1634
|
|
|
1440
1635
|
// src/core/validators/spec.ts
|
|
1441
|
-
var
|
|
1636
|
+
var import_promises11 = require("fs/promises");
|
|
1442
1637
|
async function validateSpecs(root, config) {
|
|
1443
1638
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1444
|
-
const
|
|
1445
|
-
if (
|
|
1446
|
-
const expected = "spec-
|
|
1639
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1640
|
+
if (entries.length === 0) {
|
|
1641
|
+
const expected = "spec-0001/spec.md";
|
|
1642
|
+
const legacy = "spec-001/spec.md";
|
|
1447
1643
|
return [
|
|
1448
1644
|
issue5(
|
|
1449
1645
|
"QFAI-SPEC-000",
|
|
1450
|
-
`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}`,
|
|
1646
|
+
`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)`,
|
|
1451
1647
|
"info",
|
|
1452
1648
|
specsRoot,
|
|
1453
1649
|
"spec.files"
|
|
@@ -1455,12 +1651,29 @@ async function validateSpecs(root, config) {
|
|
|
1455
1651
|
];
|
|
1456
1652
|
}
|
|
1457
1653
|
const issues = [];
|
|
1458
|
-
for (const
|
|
1459
|
-
|
|
1654
|
+
for (const entry of entries) {
|
|
1655
|
+
let text;
|
|
1656
|
+
try {
|
|
1657
|
+
text = await (0, import_promises11.readFile)(entry.specPath, "utf-8");
|
|
1658
|
+
} catch (error) {
|
|
1659
|
+
if (isMissingFileError4(error)) {
|
|
1660
|
+
issues.push(
|
|
1661
|
+
issue5(
|
|
1662
|
+
"QFAI-SPEC-005",
|
|
1663
|
+
"spec.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1664
|
+
"error",
|
|
1665
|
+
entry.specPath,
|
|
1666
|
+
"spec.exists"
|
|
1667
|
+
)
|
|
1668
|
+
);
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
throw error;
|
|
1672
|
+
}
|
|
1460
1673
|
issues.push(
|
|
1461
1674
|
...validateSpecContent(
|
|
1462
1675
|
text,
|
|
1463
|
-
|
|
1676
|
+
entry.specPath,
|
|
1464
1677
|
config.validation.require.specSections
|
|
1465
1678
|
)
|
|
1466
1679
|
);
|
|
@@ -1582,15 +1795,18 @@ function issue5(code, message, severity, file, rule, refs) {
|
|
|
1582
1795
|
}
|
|
1583
1796
|
return issue7;
|
|
1584
1797
|
}
|
|
1798
|
+
function isMissingFileError4(error) {
|
|
1799
|
+
if (!error || typeof error !== "object") {
|
|
1800
|
+
return false;
|
|
1801
|
+
}
|
|
1802
|
+
return error.code === "ENOENT";
|
|
1803
|
+
}
|
|
1585
1804
|
|
|
1586
1805
|
// src/core/validators/traceability.ts
|
|
1587
|
-
var
|
|
1588
|
-
var
|
|
1589
|
-
var
|
|
1590
|
-
var
|
|
1591
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1592
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
1593
|
-
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1806
|
+
var import_promises12 = require("fs/promises");
|
|
1807
|
+
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1808
|
+
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1809
|
+
var BR_TAG_RE3 = /^BR-\d{4}$/;
|
|
1594
1810
|
async function validateTraceability(root, config) {
|
|
1595
1811
|
const issues = [];
|
|
1596
1812
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1609,7 +1825,7 @@ async function validateTraceability(root, config) {
|
|
|
1609
1825
|
const contractIndex = await buildContractIndex(root, config);
|
|
1610
1826
|
const contractIds = contractIndex.ids;
|
|
1611
1827
|
for (const file of specFiles) {
|
|
1612
|
-
const text = await (0,
|
|
1828
|
+
const text = await (0, import_promises12.readFile)(file, "utf-8");
|
|
1613
1829
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1614
1830
|
const parsed = parseSpec(text, file);
|
|
1615
1831
|
if (parsed.specId) {
|
|
@@ -1646,106 +1862,99 @@ async function validateTraceability(root, config) {
|
|
|
1646
1862
|
}
|
|
1647
1863
|
}
|
|
1648
1864
|
for (const file of scenarioFiles) {
|
|
1649
|
-
const text = await (0,
|
|
1865
|
+
const text = await (0, import_promises12.readFile)(file, "utf-8");
|
|
1650
1866
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1651
|
-
const
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
const scIds = /* @__PURE__ */ new Set();
|
|
1655
|
-
const scenarioIds = /* @__PURE__ */ new Set();
|
|
1656
|
-
for (const scenario of parsed.scenarios) {
|
|
1657
|
-
for (const tag of scenario.tags) {
|
|
1658
|
-
if (SPEC_TAG_RE2.test(tag)) {
|
|
1659
|
-
specIdsInScenario.add(tag);
|
|
1660
|
-
}
|
|
1661
|
-
if (BR_TAG_RE2.test(tag)) {
|
|
1662
|
-
brIds.add(tag);
|
|
1663
|
-
}
|
|
1664
|
-
if (SC_TAG_RE3.test(tag)) {
|
|
1665
|
-
scIds.add(tag);
|
|
1666
|
-
}
|
|
1667
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1668
|
-
scenarioIds.add(tag);
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
const specIdsList = Array.from(specIdsInScenario);
|
|
1673
|
-
const brIdsList = Array.from(brIds);
|
|
1674
|
-
const scIdsList = Array.from(scIds);
|
|
1675
|
-
const scenarioIdsList = Array.from(scenarioIds);
|
|
1676
|
-
brIdsList.forEach((id) => brIdsInScenarios.add(id));
|
|
1677
|
-
scIdsList.forEach((id) => scIdsInScenarios.add(id));
|
|
1678
|
-
scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
|
|
1679
|
-
if (scenarioIdsList.length > 0) {
|
|
1680
|
-
scIdsList.forEach((id) => scWithContracts.add(id));
|
|
1681
|
-
}
|
|
1682
|
-
const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
|
|
1683
|
-
if (unknownSpecIds.length > 0) {
|
|
1684
|
-
issues.push(
|
|
1685
|
-
issue6(
|
|
1686
|
-
"QFAI-TRACE-005",
|
|
1687
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1688
|
-
"error",
|
|
1689
|
-
file,
|
|
1690
|
-
"traceability.scenarioSpecExists",
|
|
1691
|
-
unknownSpecIds
|
|
1692
|
-
)
|
|
1693
|
-
);
|
|
1694
|
-
}
|
|
1695
|
-
const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
|
|
1696
|
-
if (unknownBrIds.length > 0) {
|
|
1697
|
-
issues.push(
|
|
1698
|
-
issue6(
|
|
1699
|
-
"QFAI-TRACE-006",
|
|
1700
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1701
|
-
"error",
|
|
1702
|
-
file,
|
|
1703
|
-
"traceability.scenarioBrExists",
|
|
1704
|
-
unknownBrIds
|
|
1705
|
-
)
|
|
1706
|
-
);
|
|
1707
|
-
}
|
|
1708
|
-
const unknownContractIds = scenarioIdsList.filter(
|
|
1709
|
-
(id) => !contractIds.has(id)
|
|
1710
|
-
);
|
|
1711
|
-
if (unknownContractIds.length > 0) {
|
|
1712
|
-
issues.push(
|
|
1713
|
-
issue6(
|
|
1714
|
-
"QFAI-TRACE-008",
|
|
1715
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1716
|
-
", "
|
|
1717
|
-
)}`,
|
|
1718
|
-
config.validation.traceability.unknownContractIdSeverity,
|
|
1719
|
-
file,
|
|
1720
|
-
"traceability.scenarioContractExists",
|
|
1721
|
-
unknownContractIds
|
|
1722
|
-
)
|
|
1723
|
-
);
|
|
1867
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1868
|
+
if (!document || errors.length > 0) {
|
|
1869
|
+
continue;
|
|
1724
1870
|
}
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1871
|
+
const atoms = buildScenarioAtoms(document);
|
|
1872
|
+
for (const [index, scenario] of document.scenarios.entries()) {
|
|
1873
|
+
const atom = atoms[index];
|
|
1874
|
+
if (!atom) {
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
1878
|
+
const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
|
|
1879
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
|
|
1880
|
+
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
1881
|
+
scTags.forEach((id) => scIdsInScenarios.add(id));
|
|
1882
|
+
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
1883
|
+
if (atom.contractIds.length > 0) {
|
|
1884
|
+
scTags.forEach((id) => scWithContracts.add(id));
|
|
1885
|
+
}
|
|
1886
|
+
const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
|
|
1887
|
+
if (unknownSpecIds.length > 0) {
|
|
1888
|
+
issues.push(
|
|
1889
|
+
issue6(
|
|
1890
|
+
"QFAI-TRACE-005",
|
|
1891
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(
|
|
1892
|
+
", "
|
|
1893
|
+
)} (${scenario.name})`,
|
|
1894
|
+
"error",
|
|
1895
|
+
file,
|
|
1896
|
+
"traceability.scenarioSpecExists",
|
|
1897
|
+
unknownSpecIds
|
|
1898
|
+
)
|
|
1899
|
+
);
|
|
1733
1900
|
}
|
|
1734
|
-
const
|
|
1735
|
-
if (
|
|
1901
|
+
const unknownBrIds = brTags.filter((id) => !brIdsInSpecs.has(id));
|
|
1902
|
+
if (unknownBrIds.length > 0) {
|
|
1736
1903
|
issues.push(
|
|
1737
1904
|
issue6(
|
|
1738
|
-
"QFAI-TRACE-
|
|
1739
|
-
`Scenario \
|
|
1905
|
+
"QFAI-TRACE-006",
|
|
1906
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(
|
|
1740
1907
|
", "
|
|
1741
|
-
)} (
|
|
1908
|
+
)} (${scenario.name})`,
|
|
1742
1909
|
"error",
|
|
1743
1910
|
file,
|
|
1744
|
-
"traceability.
|
|
1745
|
-
|
|
1911
|
+
"traceability.scenarioBrExists",
|
|
1912
|
+
unknownBrIds
|
|
1913
|
+
)
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
const unknownContractIds = atom.contractIds.filter(
|
|
1917
|
+
(id) => !contractIds.has(id)
|
|
1918
|
+
);
|
|
1919
|
+
if (unknownContractIds.length > 0) {
|
|
1920
|
+
issues.push(
|
|
1921
|
+
issue6(
|
|
1922
|
+
"QFAI-TRACE-008",
|
|
1923
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1924
|
+
", "
|
|
1925
|
+
)} (${scenario.name})`,
|
|
1926
|
+
config.validation.traceability.unknownContractIdSeverity,
|
|
1927
|
+
file,
|
|
1928
|
+
"traceability.scenarioContractExists",
|
|
1929
|
+
unknownContractIds
|
|
1746
1930
|
)
|
|
1747
1931
|
);
|
|
1748
1932
|
}
|
|
1933
|
+
if (specTags.length > 0 && brTags.length > 0) {
|
|
1934
|
+
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
1935
|
+
for (const specId of specTags) {
|
|
1936
|
+
const brIdsForSpec = specToBrIds.get(specId);
|
|
1937
|
+
if (!brIdsForSpec) {
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
1941
|
+
}
|
|
1942
|
+
const invalidBrIds = brTags.filter((id) => !allowedBrIds.has(id));
|
|
1943
|
+
if (invalidBrIds.length > 0) {
|
|
1944
|
+
issues.push(
|
|
1945
|
+
issue6(
|
|
1946
|
+
"QFAI-TRACE-007",
|
|
1947
|
+
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
1948
|
+
", "
|
|
1949
|
+
)} (SPEC: ${specTags.join(", ")}) (${scenario.name})`,
|
|
1950
|
+
"error",
|
|
1951
|
+
file,
|
|
1952
|
+
"traceability.scenarioBrUnderSpec",
|
|
1953
|
+
invalidBrIds
|
|
1954
|
+
)
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1749
1958
|
}
|
|
1750
1959
|
}
|
|
1751
1960
|
if (upstreamIds.size === 0) {
|
|
@@ -1843,7 +2052,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1843
2052
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
1844
2053
|
let found = false;
|
|
1845
2054
|
for (const file of targetFiles) {
|
|
1846
|
-
const text = await (0,
|
|
2055
|
+
const text = await (0, import_promises12.readFile)(file, "utf-8");
|
|
1847
2056
|
if (pattern.test(text)) {
|
|
1848
2057
|
found = true;
|
|
1849
2058
|
break;
|
|
@@ -2070,7 +2279,7 @@ async function collectIds(files) {
|
|
|
2070
2279
|
DATA: /* @__PURE__ */ new Set()
|
|
2071
2280
|
};
|
|
2072
2281
|
for (const file of files) {
|
|
2073
|
-
const text = await (0,
|
|
2282
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
2074
2283
|
for (const prefix of ID_PREFIXES2) {
|
|
2075
2284
|
const ids = extractIds(text, prefix);
|
|
2076
2285
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -2088,7 +2297,7 @@ async function collectIds(files) {
|
|
|
2088
2297
|
async function collectUpstreamIds(files) {
|
|
2089
2298
|
const ids = /* @__PURE__ */ new Set();
|
|
2090
2299
|
for (const file of files) {
|
|
2091
|
-
const text = await (0,
|
|
2300
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
2092
2301
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
2093
2302
|
}
|
|
2094
2303
|
return ids;
|
|
@@ -2109,7 +2318,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
2109
2318
|
}
|
|
2110
2319
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
2111
2320
|
for (const file of targetFiles) {
|
|
2112
|
-
const text = await (0,
|
|
2321
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
2113
2322
|
if (pattern.test(text)) {
|
|
2114
2323
|
return true;
|
|
2115
2324
|
}
|