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/cli/index.cjs
CHANGED
|
@@ -109,12 +109,29 @@ async function exists(target) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// src/cli/lib/assets.ts
|
|
112
|
+
var import_node_fs = require("fs");
|
|
112
113
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
113
114
|
var import_node_url = require("url");
|
|
114
115
|
function getInitAssetsDir() {
|
|
115
116
|
const base = __filename;
|
|
116
117
|
const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
|
|
117
|
-
|
|
118
|
+
const baseDir = import_node_path2.default.dirname(basePath);
|
|
119
|
+
const candidates = [
|
|
120
|
+
import_node_path2.default.resolve(baseDir, "../../../assets/init"),
|
|
121
|
+
import_node_path2.default.resolve(baseDir, "../../assets/init")
|
|
122
|
+
];
|
|
123
|
+
for (const candidate of candidates) {
|
|
124
|
+
if ((0, import_node_fs.existsSync)(candidate)) {
|
|
125
|
+
return candidate;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
throw new Error(
|
|
129
|
+
[
|
|
130
|
+
"init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
|
|
131
|
+
"\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
|
|
132
|
+
...candidates.map((candidate) => `- ${candidate}`)
|
|
133
|
+
].join("\n")
|
|
134
|
+
);
|
|
118
135
|
}
|
|
119
136
|
|
|
120
137
|
// src/cli/lib/logger.ts
|
|
@@ -160,7 +177,7 @@ function report(copied, skipped, dryRun, label) {
|
|
|
160
177
|
}
|
|
161
178
|
|
|
162
179
|
// src/cli/commands/report.ts
|
|
163
|
-
var
|
|
180
|
+
var import_promises15 = require("fs/promises");
|
|
164
181
|
var import_node_path14 = __toESM(require("path"), 1);
|
|
165
182
|
|
|
166
183
|
// src/core/config.ts
|
|
@@ -497,11 +514,11 @@ function isRecord(value) {
|
|
|
497
514
|
}
|
|
498
515
|
|
|
499
516
|
// src/core/report.ts
|
|
500
|
-
var
|
|
517
|
+
var import_promises14 = require("fs/promises");
|
|
501
518
|
var import_node_path13 = __toESM(require("path"), 1);
|
|
502
519
|
|
|
503
520
|
// src/core/discovery.ts
|
|
504
|
-
var
|
|
521
|
+
var import_promises5 = require("fs/promises");
|
|
505
522
|
|
|
506
523
|
// src/core/fs.ts
|
|
507
524
|
var import_promises3 = require("fs/promises");
|
|
@@ -558,25 +575,50 @@ async function exists2(target) {
|
|
|
558
575
|
}
|
|
559
576
|
}
|
|
560
577
|
|
|
561
|
-
// src/core/
|
|
562
|
-
var
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
578
|
+
// src/core/specLayout.ts
|
|
579
|
+
var import_promises4 = require("fs/promises");
|
|
580
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
581
|
+
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
582
|
+
async function collectSpecEntries(specsRoot) {
|
|
583
|
+
const dirs = await listSpecDirs(specsRoot);
|
|
584
|
+
const entries = dirs.map((dir) => ({
|
|
585
|
+
dir,
|
|
586
|
+
specPath: import_node_path6.default.join(dir, "spec.md"),
|
|
587
|
+
deltaPath: import_node_path6.default.join(dir, "delta.md"),
|
|
588
|
+
scenarioPath: import_node_path6.default.join(dir, "scenario.md")
|
|
589
|
+
}));
|
|
590
|
+
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
591
|
+
}
|
|
592
|
+
async function listSpecDirs(specsRoot) {
|
|
593
|
+
try {
|
|
594
|
+
const items = await (0, import_promises4.readdir)(specsRoot, { withFileTypes: true });
|
|
595
|
+
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));
|
|
596
|
+
} catch (error2) {
|
|
597
|
+
if (isMissingFileError(error2)) {
|
|
598
|
+
return [];
|
|
569
599
|
}
|
|
600
|
+
throw error2;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function isMissingFileError(error2) {
|
|
604
|
+
if (!error2 || typeof error2 !== "object") {
|
|
605
|
+
return false;
|
|
570
606
|
}
|
|
571
|
-
return
|
|
607
|
+
return error2.code === "ENOENT";
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/core/discovery.ts
|
|
611
|
+
async function collectSpecPackDirs(specsRoot) {
|
|
612
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
613
|
+
return entries.map((entry) => entry.dir);
|
|
572
614
|
}
|
|
573
615
|
async function collectSpecFiles(specsRoot) {
|
|
574
|
-
const
|
|
575
|
-
return
|
|
616
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
617
|
+
return filterExisting(entries.map((entry) => entry.specPath));
|
|
576
618
|
}
|
|
577
619
|
async function collectScenarioFiles(specsRoot) {
|
|
578
|
-
const
|
|
579
|
-
return
|
|
620
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
621
|
+
return filterExisting(entries.map((entry) => entry.scenarioPath));
|
|
580
622
|
}
|
|
581
623
|
async function collectUiContractFiles(uiRoot) {
|
|
582
624
|
return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
@@ -595,12 +637,22 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
|
595
637
|
]);
|
|
596
638
|
return { ui, api, db };
|
|
597
639
|
}
|
|
598
|
-
function
|
|
599
|
-
|
|
640
|
+
async function filterExisting(files) {
|
|
641
|
+
const existing = [];
|
|
642
|
+
for (const file of files) {
|
|
643
|
+
if (await exists3(file)) {
|
|
644
|
+
existing.push(file);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return existing;
|
|
648
|
+
}
|
|
649
|
+
async function exists3(target) {
|
|
650
|
+
try {
|
|
651
|
+
await (0, import_promises5.access)(target);
|
|
652
|
+
return true;
|
|
653
|
+
} catch {
|
|
600
654
|
return false;
|
|
601
655
|
}
|
|
602
|
-
const dirName = import_node_path6.default.basename(import_node_path6.default.dirname(filePath)).toLowerCase();
|
|
603
|
-
return SPEC_PACK_DIR_PATTERN.test(dirName);
|
|
604
656
|
}
|
|
605
657
|
|
|
606
658
|
// src/core/ids.ts
|
|
@@ -660,16 +712,16 @@ function isValidId(value, prefix) {
|
|
|
660
712
|
var VALIDATION_SCHEMA_VERSION = "0.2";
|
|
661
713
|
|
|
662
714
|
// src/core/version.ts
|
|
663
|
-
var
|
|
715
|
+
var import_promises6 = require("fs/promises");
|
|
664
716
|
var import_node_path7 = __toESM(require("path"), 1);
|
|
665
717
|
var import_node_url2 = require("url");
|
|
666
718
|
async function resolveToolVersion() {
|
|
667
|
-
if ("0.3.
|
|
668
|
-
return "0.3.
|
|
719
|
+
if ("0.3.3".length > 0) {
|
|
720
|
+
return "0.3.3";
|
|
669
721
|
}
|
|
670
722
|
try {
|
|
671
723
|
const packagePath = resolvePackageJsonPath();
|
|
672
|
-
const raw = await (0,
|
|
724
|
+
const raw = await (0, import_promises6.readFile)(packagePath, "utf-8");
|
|
673
725
|
const parsed = JSON.parse(raw);
|
|
674
726
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
675
727
|
return version.length > 0 ? version : "unknown";
|
|
@@ -684,7 +736,7 @@ function resolvePackageJsonPath() {
|
|
|
684
736
|
}
|
|
685
737
|
|
|
686
738
|
// src/core/validators/contracts.ts
|
|
687
|
-
var
|
|
739
|
+
var import_promises7 = require("fs/promises");
|
|
688
740
|
var import_node_path9 = __toESM(require("path"), 1);
|
|
689
741
|
|
|
690
742
|
// src/core/contracts.ts
|
|
@@ -762,7 +814,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
762
814
|
}
|
|
763
815
|
const issues = [];
|
|
764
816
|
for (const file of files) {
|
|
765
|
-
const text = await (0,
|
|
817
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
766
818
|
const invalidIds = extractInvalidIds(text, [
|
|
767
819
|
"SPEC",
|
|
768
820
|
"BR",
|
|
@@ -829,7 +881,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
829
881
|
}
|
|
830
882
|
const issues = [];
|
|
831
883
|
for (const file of files) {
|
|
832
|
-
const text = await (0,
|
|
884
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
833
885
|
const invalidIds = extractInvalidIds(text, [
|
|
834
886
|
"SPEC",
|
|
835
887
|
"BR",
|
|
@@ -907,7 +959,7 @@ async function validateDataContracts(dataRoot) {
|
|
|
907
959
|
}
|
|
908
960
|
const issues = [];
|
|
909
961
|
for (const file of files) {
|
|
910
|
-
const text = await (0,
|
|
962
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
911
963
|
const invalidIds = extractInvalidIds(text, [
|
|
912
964
|
"SPEC",
|
|
913
965
|
"BR",
|
|
@@ -978,7 +1030,7 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
978
1030
|
}
|
|
979
1031
|
|
|
980
1032
|
// src/core/validators/delta.ts
|
|
981
|
-
var
|
|
1033
|
+
var import_promises8 = require("fs/promises");
|
|
982
1034
|
var import_node_path10 = __toESM(require("path"), 1);
|
|
983
1035
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
984
1036
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
@@ -996,9 +1048,9 @@ async function validateDeltas(root, config) {
|
|
|
996
1048
|
const deltaPath = import_node_path10.default.join(pack, "delta.md");
|
|
997
1049
|
let text;
|
|
998
1050
|
try {
|
|
999
|
-
text = await (0,
|
|
1051
|
+
text = await (0, import_promises8.readFile)(deltaPath, "utf-8");
|
|
1000
1052
|
} catch (error2) {
|
|
1001
|
-
if (
|
|
1053
|
+
if (isMissingFileError2(error2)) {
|
|
1002
1054
|
issues.push(
|
|
1003
1055
|
issue2(
|
|
1004
1056
|
"QFAI-DELTA-001",
|
|
@@ -1043,7 +1095,7 @@ async function validateDeltas(root, config) {
|
|
|
1043
1095
|
}
|
|
1044
1096
|
return issues;
|
|
1045
1097
|
}
|
|
1046
|
-
function
|
|
1098
|
+
function isMissingFileError2(error2) {
|
|
1047
1099
|
if (!error2 || typeof error2 !== "object") {
|
|
1048
1100
|
return false;
|
|
1049
1101
|
}
|
|
@@ -1068,11 +1120,11 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1068
1120
|
}
|
|
1069
1121
|
|
|
1070
1122
|
// src/core/validators/ids.ts
|
|
1071
|
-
var
|
|
1123
|
+
var import_promises10 = require("fs/promises");
|
|
1072
1124
|
var import_node_path12 = __toESM(require("path"), 1);
|
|
1073
1125
|
|
|
1074
1126
|
// src/core/contractIndex.ts
|
|
1075
|
-
var
|
|
1127
|
+
var import_promises9 = require("fs/promises");
|
|
1076
1128
|
var import_node_path11 = __toESM(require("path"), 1);
|
|
1077
1129
|
async function buildContractIndex(root, config) {
|
|
1078
1130
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
@@ -1097,7 +1149,7 @@ async function buildContractIndex(root, config) {
|
|
|
1097
1149
|
}
|
|
1098
1150
|
async function indexUiContracts(files, index) {
|
|
1099
1151
|
for (const file of files) {
|
|
1100
|
-
const text = await (0,
|
|
1152
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1101
1153
|
try {
|
|
1102
1154
|
const doc = parseStructuredContract(file, text);
|
|
1103
1155
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1109,7 +1161,7 @@ async function indexUiContracts(files, index) {
|
|
|
1109
1161
|
}
|
|
1110
1162
|
async function indexApiContracts(files, index) {
|
|
1111
1163
|
for (const file of files) {
|
|
1112
|
-
const text = await (0,
|
|
1164
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1113
1165
|
try {
|
|
1114
1166
|
const doc = parseStructuredContract(file, text);
|
|
1115
1167
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1121,7 +1173,7 @@ async function indexApiContracts(files, index) {
|
|
|
1121
1173
|
}
|
|
1122
1174
|
async function indexDataContracts(files, index) {
|
|
1123
1175
|
for (const file of files) {
|
|
1124
|
-
const text = await (0,
|
|
1176
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1125
1177
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1126
1178
|
}
|
|
1127
1179
|
}
|
|
@@ -1132,66 +1184,6 @@ function record(index, id, file) {
|
|
|
1132
1184
|
index.idToFiles.set(id, current);
|
|
1133
1185
|
}
|
|
1134
1186
|
|
|
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
1187
|
// src/core/parse/markdown.ts
|
|
1196
1188
|
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1197
1189
|
function parseHeadings(md) {
|
|
@@ -1310,8 +1302,162 @@ function parseSpec(md, file) {
|
|
|
1310
1302
|
return parsed;
|
|
1311
1303
|
}
|
|
1312
1304
|
|
|
1313
|
-
// src/core/
|
|
1305
|
+
// src/core/gherkin/parse.ts
|
|
1306
|
+
var import_gherkin = require("@cucumber/gherkin");
|
|
1307
|
+
var import_node_crypto = require("crypto");
|
|
1308
|
+
function parseGherkin(source, uri) {
|
|
1309
|
+
const errors = [];
|
|
1310
|
+
const uuidFn = () => (0, import_node_crypto.randomUUID)();
|
|
1311
|
+
const builder = new import_gherkin.AstBuilder(uuidFn);
|
|
1312
|
+
const matcher = new import_gherkin.GherkinClassicTokenMatcher();
|
|
1313
|
+
const parser = new import_gherkin.Parser(builder, matcher);
|
|
1314
|
+
try {
|
|
1315
|
+
const gherkinDocument = parser.parse(source);
|
|
1316
|
+
gherkinDocument.uri = uri;
|
|
1317
|
+
return { gherkinDocument, errors };
|
|
1318
|
+
} catch (error2) {
|
|
1319
|
+
errors.push(formatError3(error2));
|
|
1320
|
+
return { gherkinDocument: null, errors };
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
function formatError3(error2) {
|
|
1324
|
+
if (error2 instanceof Error) {
|
|
1325
|
+
return error2.message;
|
|
1326
|
+
}
|
|
1327
|
+
return String(error2);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/core/scenarioModel.ts
|
|
1331
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1314
1332
|
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1333
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1334
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1335
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
1336
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1337
|
+
function parseScenarioDocument(text, uri) {
|
|
1338
|
+
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1339
|
+
if (!gherkinDocument) {
|
|
1340
|
+
return { document: null, errors };
|
|
1341
|
+
}
|
|
1342
|
+
const feature = gherkinDocument.feature;
|
|
1343
|
+
if (!feature) {
|
|
1344
|
+
return {
|
|
1345
|
+
document: { uri, featureTags: [], scenarios: [] },
|
|
1346
|
+
errors
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
const featureTags = collectTagNames(feature.tags);
|
|
1350
|
+
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
1351
|
+
return {
|
|
1352
|
+
document: {
|
|
1353
|
+
uri,
|
|
1354
|
+
featureName: feature.name,
|
|
1355
|
+
featureTags,
|
|
1356
|
+
scenarios
|
|
1357
|
+
},
|
|
1358
|
+
errors
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
function buildScenarioAtoms(document) {
|
|
1362
|
+
return document.scenarios.map((scenario) => {
|
|
1363
|
+
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1364
|
+
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1365
|
+
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1366
|
+
const contractIds = /* @__PURE__ */ new Set();
|
|
1367
|
+
scenario.tags.forEach((tag) => {
|
|
1368
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1369
|
+
contractIds.add(tag);
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
for (const step of scenario.steps) {
|
|
1373
|
+
for (const text of collectStepTexts(step)) {
|
|
1374
|
+
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1375
|
+
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1376
|
+
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
const atom = {
|
|
1380
|
+
uri: document.uri,
|
|
1381
|
+
featureName: document.featureName ?? "",
|
|
1382
|
+
scenarioName: scenario.name,
|
|
1383
|
+
kind: scenario.kind,
|
|
1384
|
+
brIds,
|
|
1385
|
+
contractIds: Array.from(contractIds).sort()
|
|
1386
|
+
};
|
|
1387
|
+
if (scenario.line !== void 0) {
|
|
1388
|
+
atom.line = scenario.line;
|
|
1389
|
+
}
|
|
1390
|
+
if (specIds.length === 1) {
|
|
1391
|
+
const specId = specIds[0];
|
|
1392
|
+
if (specId) {
|
|
1393
|
+
atom.specId = specId;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
if (scIds.length === 1) {
|
|
1397
|
+
const scId = scIds[0];
|
|
1398
|
+
if (scId) {
|
|
1399
|
+
atom.scId = scId;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return atom;
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
function collectScenarioNodes(feature, featureTags) {
|
|
1406
|
+
const scenarios = [];
|
|
1407
|
+
for (const child of feature.children) {
|
|
1408
|
+
if (child.scenario) {
|
|
1409
|
+
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
1410
|
+
}
|
|
1411
|
+
if (child.rule) {
|
|
1412
|
+
const ruleTags = collectTagNames(child.rule.tags);
|
|
1413
|
+
for (const ruleChild of child.rule.children) {
|
|
1414
|
+
if (ruleChild.scenario) {
|
|
1415
|
+
scenarios.push(
|
|
1416
|
+
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return scenarios;
|
|
1423
|
+
}
|
|
1424
|
+
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
1425
|
+
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
1426
|
+
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
1427
|
+
return {
|
|
1428
|
+
name: scenario.name,
|
|
1429
|
+
kind,
|
|
1430
|
+
line: scenario.location?.line,
|
|
1431
|
+
tags,
|
|
1432
|
+
steps: scenario.steps
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
function collectTagNames(tags) {
|
|
1436
|
+
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1437
|
+
}
|
|
1438
|
+
function collectStepTexts(step) {
|
|
1439
|
+
const texts = [];
|
|
1440
|
+
if (step.text) {
|
|
1441
|
+
texts.push(step.text);
|
|
1442
|
+
}
|
|
1443
|
+
if (step.docString?.content) {
|
|
1444
|
+
texts.push(step.docString.content);
|
|
1445
|
+
}
|
|
1446
|
+
if (step.dataTable?.rows) {
|
|
1447
|
+
for (const row of step.dataTable.rows) {
|
|
1448
|
+
for (const cell of row.cells) {
|
|
1449
|
+
texts.push(cell.value);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return texts;
|
|
1454
|
+
}
|
|
1455
|
+
function unique2(values) {
|
|
1456
|
+
return Array.from(new Set(values));
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// src/core/validators/ids.ts
|
|
1460
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1315
1461
|
async function validateDefinedIds(root, config) {
|
|
1316
1462
|
const issues = [];
|
|
1317
1463
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1345,7 +1491,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1345
1491
|
}
|
|
1346
1492
|
async function collectSpecDefinitionIds(files, out) {
|
|
1347
1493
|
for (const file of files) {
|
|
1348
|
-
const text = await (0,
|
|
1494
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1349
1495
|
const parsed = parseSpec(text, file);
|
|
1350
1496
|
if (parsed.specId) {
|
|
1351
1497
|
recordId(out, parsed.specId, file);
|
|
@@ -1355,11 +1501,14 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1355
1501
|
}
|
|
1356
1502
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1357
1503
|
for (const file of files) {
|
|
1358
|
-
const text = await (0,
|
|
1359
|
-
const
|
|
1360
|
-
|
|
1504
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1505
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1506
|
+
if (!document || errors.length > 0) {
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
for (const scenario of document.scenarios) {
|
|
1361
1510
|
for (const tag of scenario.tags) {
|
|
1362
|
-
if (
|
|
1511
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
1363
1512
|
recordId(out, tag, file);
|
|
1364
1513
|
}
|
|
1365
1514
|
}
|
|
@@ -1396,21 +1545,23 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1396
1545
|
}
|
|
1397
1546
|
|
|
1398
1547
|
// src/core/validators/scenario.ts
|
|
1399
|
-
var
|
|
1548
|
+
var import_promises11 = require("fs/promises");
|
|
1400
1549
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1401
1550
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1402
1551
|
var THEN_PATTERN = /\bThen\b/;
|
|
1403
|
-
var
|
|
1404
|
-
var
|
|
1405
|
-
var
|
|
1552
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1553
|
+
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1554
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1406
1555
|
async function validateScenarios(root, config) {
|
|
1407
1556
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1408
|
-
const
|
|
1409
|
-
if (
|
|
1557
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1558
|
+
if (entries.length === 0) {
|
|
1559
|
+
const expected = "spec-0001/scenario.md";
|
|
1560
|
+
const legacy = "spec-001/scenario.md";
|
|
1410
1561
|
return [
|
|
1411
1562
|
issue4(
|
|
1412
1563
|
"QFAI-SC-000",
|
|
1413
|
-
|
|
1564
|
+
`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
1565
|
"info",
|
|
1415
1566
|
specsRoot,
|
|
1416
1567
|
"scenario.files"
|
|
@@ -1418,15 +1569,31 @@ async function validateScenarios(root, config) {
|
|
|
1418
1569
|
];
|
|
1419
1570
|
}
|
|
1420
1571
|
const issues = [];
|
|
1421
|
-
for (const
|
|
1422
|
-
|
|
1423
|
-
|
|
1572
|
+
for (const entry of entries) {
|
|
1573
|
+
let text;
|
|
1574
|
+
try {
|
|
1575
|
+
text = await (0, import_promises11.readFile)(entry.scenarioPath, "utf-8");
|
|
1576
|
+
} catch (error2) {
|
|
1577
|
+
if (isMissingFileError3(error2)) {
|
|
1578
|
+
issues.push(
|
|
1579
|
+
issue4(
|
|
1580
|
+
"QFAI-SC-001",
|
|
1581
|
+
"scenario.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1582
|
+
"error",
|
|
1583
|
+
entry.scenarioPath,
|
|
1584
|
+
"scenario.exists"
|
|
1585
|
+
)
|
|
1586
|
+
);
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
throw error2;
|
|
1590
|
+
}
|
|
1591
|
+
issues.push(...validateScenarioContent(text, entry.scenarioPath));
|
|
1424
1592
|
}
|
|
1425
1593
|
return issues;
|
|
1426
1594
|
}
|
|
1427
1595
|
function validateScenarioContent(text, file) {
|
|
1428
1596
|
const issues = [];
|
|
1429
|
-
const parsed = parseGherkinFeature(text, file);
|
|
1430
1597
|
const invalidIds = extractInvalidIds(text, [
|
|
1431
1598
|
"SPEC",
|
|
1432
1599
|
"BR",
|
|
@@ -1448,9 +1615,47 @@ function validateScenarioContent(text, file) {
|
|
|
1448
1615
|
)
|
|
1449
1616
|
);
|
|
1450
1617
|
}
|
|
1618
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1619
|
+
if (!document || errors.length > 0) {
|
|
1620
|
+
issues.push(
|
|
1621
|
+
issue4(
|
|
1622
|
+
"QFAI-SC-010",
|
|
1623
|
+
`Gherkin \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errors.join(", ") || "unknown"}`,
|
|
1624
|
+
"error",
|
|
1625
|
+
file,
|
|
1626
|
+
"scenario.parse"
|
|
1627
|
+
)
|
|
1628
|
+
);
|
|
1629
|
+
return issues;
|
|
1630
|
+
}
|
|
1631
|
+
const featureSpecTags = document.featureTags.filter(
|
|
1632
|
+
(tag) => SPEC_TAG_RE2.test(tag)
|
|
1633
|
+
);
|
|
1634
|
+
if (featureSpecTags.length === 0) {
|
|
1635
|
+
issues.push(
|
|
1636
|
+
issue4(
|
|
1637
|
+
"QFAI-SC-009",
|
|
1638
|
+
"Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1639
|
+
"error",
|
|
1640
|
+
file,
|
|
1641
|
+
"scenario.featureSpec"
|
|
1642
|
+
)
|
|
1643
|
+
);
|
|
1644
|
+
} else if (featureSpecTags.length > 1) {
|
|
1645
|
+
issues.push(
|
|
1646
|
+
issue4(
|
|
1647
|
+
"QFAI-SC-009",
|
|
1648
|
+
`Feature \u306E SPEC \u30BF\u30B0\u304C\u8907\u6570\u3042\u308A\u307E\u3059: ${featureSpecTags.join(", ")}`,
|
|
1649
|
+
"error",
|
|
1650
|
+
file,
|
|
1651
|
+
"scenario.featureSpec",
|
|
1652
|
+
featureSpecTags
|
|
1653
|
+
)
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1451
1656
|
const missingStructure = [];
|
|
1452
|
-
if (!
|
|
1453
|
-
if (
|
|
1657
|
+
if (!document.featureName) missingStructure.push("Feature");
|
|
1658
|
+
if (document.scenarios.length === 0) missingStructure.push("Scenario");
|
|
1454
1659
|
if (missingStructure.length > 0) {
|
|
1455
1660
|
issues.push(
|
|
1456
1661
|
issue4(
|
|
@@ -1464,7 +1669,7 @@ function validateScenarioContent(text, file) {
|
|
|
1464
1669
|
)
|
|
1465
1670
|
);
|
|
1466
1671
|
}
|
|
1467
|
-
for (const scenario of
|
|
1672
|
+
for (const scenario of document.scenarios) {
|
|
1468
1673
|
if (scenario.tags.length === 0) {
|
|
1469
1674
|
issues.push(
|
|
1470
1675
|
issue4(
|
|
@@ -1478,16 +1683,16 @@ function validateScenarioContent(text, file) {
|
|
|
1478
1683
|
continue;
|
|
1479
1684
|
}
|
|
1480
1685
|
const missingTags = [];
|
|
1481
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1686
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
|
|
1482
1687
|
if (scTags.length === 0) {
|
|
1483
1688
|
missingTags.push("SC(0\u4EF6)");
|
|
1484
1689
|
} else if (scTags.length > 1) {
|
|
1485
1690
|
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1486
1691
|
}
|
|
1487
|
-
if (!scenario.tags.some((tag) =>
|
|
1692
|
+
if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
|
|
1488
1693
|
missingTags.push("SPEC");
|
|
1489
1694
|
}
|
|
1490
|
-
if (!scenario.tags.some((tag) =>
|
|
1695
|
+
if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
|
|
1491
1696
|
missingTags.push("BR");
|
|
1492
1697
|
}
|
|
1493
1698
|
if (missingTags.length > 0) {
|
|
@@ -1502,15 +1707,16 @@ function validateScenarioContent(text, file) {
|
|
|
1502
1707
|
);
|
|
1503
1708
|
}
|
|
1504
1709
|
}
|
|
1505
|
-
for (const scenario of
|
|
1710
|
+
for (const scenario of document.scenarios) {
|
|
1506
1711
|
const missingSteps = [];
|
|
1507
|
-
|
|
1712
|
+
const keywords = scenario.steps.map((step) => step.keyword.trim());
|
|
1713
|
+
if (!keywords.some((keyword) => GIVEN_PATTERN.test(keyword))) {
|
|
1508
1714
|
missingSteps.push("Given");
|
|
1509
1715
|
}
|
|
1510
|
-
if (!WHEN_PATTERN.test(
|
|
1716
|
+
if (!keywords.some((keyword) => WHEN_PATTERN.test(keyword))) {
|
|
1511
1717
|
missingSteps.push("When");
|
|
1512
1718
|
}
|
|
1513
|
-
if (!THEN_PATTERN.test(
|
|
1719
|
+
if (!keywords.some((keyword) => THEN_PATTERN.test(keyword))) {
|
|
1514
1720
|
missingSteps.push("Then");
|
|
1515
1721
|
}
|
|
1516
1722
|
if (missingSteps.length > 0) {
|
|
@@ -1544,18 +1750,25 @@ function issue4(code, message, severity, file, rule, refs) {
|
|
|
1544
1750
|
}
|
|
1545
1751
|
return issue7;
|
|
1546
1752
|
}
|
|
1753
|
+
function isMissingFileError3(error2) {
|
|
1754
|
+
if (!error2 || typeof error2 !== "object") {
|
|
1755
|
+
return false;
|
|
1756
|
+
}
|
|
1757
|
+
return error2.code === "ENOENT";
|
|
1758
|
+
}
|
|
1547
1759
|
|
|
1548
1760
|
// src/core/validators/spec.ts
|
|
1549
|
-
var
|
|
1761
|
+
var import_promises12 = require("fs/promises");
|
|
1550
1762
|
async function validateSpecs(root, config) {
|
|
1551
1763
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1552
|
-
const
|
|
1553
|
-
if (
|
|
1554
|
-
const expected = "spec-
|
|
1764
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1765
|
+
if (entries.length === 0) {
|
|
1766
|
+
const expected = "spec-0001/spec.md";
|
|
1767
|
+
const legacy = "spec-001/spec.md";
|
|
1555
1768
|
return [
|
|
1556
1769
|
issue5(
|
|
1557
1770
|
"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}`,
|
|
1771
|
+
`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
1772
|
"info",
|
|
1560
1773
|
specsRoot,
|
|
1561
1774
|
"spec.files"
|
|
@@ -1563,12 +1776,29 @@ async function validateSpecs(root, config) {
|
|
|
1563
1776
|
];
|
|
1564
1777
|
}
|
|
1565
1778
|
const issues = [];
|
|
1566
|
-
for (const
|
|
1567
|
-
|
|
1779
|
+
for (const entry of entries) {
|
|
1780
|
+
let text;
|
|
1781
|
+
try {
|
|
1782
|
+
text = await (0, import_promises12.readFile)(entry.specPath, "utf-8");
|
|
1783
|
+
} catch (error2) {
|
|
1784
|
+
if (isMissingFileError4(error2)) {
|
|
1785
|
+
issues.push(
|
|
1786
|
+
issue5(
|
|
1787
|
+
"QFAI-SPEC-005",
|
|
1788
|
+
"spec.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1789
|
+
"error",
|
|
1790
|
+
entry.specPath,
|
|
1791
|
+
"spec.exists"
|
|
1792
|
+
)
|
|
1793
|
+
);
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
throw error2;
|
|
1797
|
+
}
|
|
1568
1798
|
issues.push(
|
|
1569
1799
|
...validateSpecContent(
|
|
1570
1800
|
text,
|
|
1571
|
-
|
|
1801
|
+
entry.specPath,
|
|
1572
1802
|
config.validation.require.specSections
|
|
1573
1803
|
)
|
|
1574
1804
|
);
|
|
@@ -1690,15 +1920,18 @@ function issue5(code, message, severity, file, rule, refs) {
|
|
|
1690
1920
|
}
|
|
1691
1921
|
return issue7;
|
|
1692
1922
|
}
|
|
1923
|
+
function isMissingFileError4(error2) {
|
|
1924
|
+
if (!error2 || typeof error2 !== "object") {
|
|
1925
|
+
return false;
|
|
1926
|
+
}
|
|
1927
|
+
return error2.code === "ENOENT";
|
|
1928
|
+
}
|
|
1693
1929
|
|
|
1694
1930
|
// 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}$/;
|
|
1931
|
+
var import_promises13 = require("fs/promises");
|
|
1932
|
+
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1933
|
+
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1934
|
+
var BR_TAG_RE3 = /^BR-\d{4}$/;
|
|
1702
1935
|
async function validateTraceability(root, config) {
|
|
1703
1936
|
const issues = [];
|
|
1704
1937
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1717,7 +1950,7 @@ async function validateTraceability(root, config) {
|
|
|
1717
1950
|
const contractIndex = await buildContractIndex(root, config);
|
|
1718
1951
|
const contractIds = contractIndex.ids;
|
|
1719
1952
|
for (const file of specFiles) {
|
|
1720
|
-
const text = await (0,
|
|
1953
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1721
1954
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1722
1955
|
const parsed = parseSpec(text, file);
|
|
1723
1956
|
if (parsed.specId) {
|
|
@@ -1754,106 +1987,99 @@ async function validateTraceability(root, config) {
|
|
|
1754
1987
|
}
|
|
1755
1988
|
}
|
|
1756
1989
|
for (const file of scenarioFiles) {
|
|
1757
|
-
const text = await (0,
|
|
1990
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1758
1991
|
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
|
-
);
|
|
1992
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1993
|
+
if (!document || errors.length > 0) {
|
|
1994
|
+
continue;
|
|
1832
1995
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1996
|
+
const atoms = buildScenarioAtoms(document);
|
|
1997
|
+
for (const [index, scenario] of document.scenarios.entries()) {
|
|
1998
|
+
const atom = atoms[index];
|
|
1999
|
+
if (!atom) {
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
2003
|
+
const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
|
|
2004
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
|
|
2005
|
+
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
2006
|
+
scTags.forEach((id) => scIdsInScenarios.add(id));
|
|
2007
|
+
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
2008
|
+
if (atom.contractIds.length > 0) {
|
|
2009
|
+
scTags.forEach((id) => scWithContracts.add(id));
|
|
1841
2010
|
}
|
|
1842
|
-
const
|
|
1843
|
-
if (
|
|
2011
|
+
const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
|
|
2012
|
+
if (unknownSpecIds.length > 0) {
|
|
1844
2013
|
issues.push(
|
|
1845
2014
|
issue6(
|
|
1846
|
-
"QFAI-TRACE-
|
|
1847
|
-
`Scenario \
|
|
2015
|
+
"QFAI-TRACE-005",
|
|
2016
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(
|
|
1848
2017
|
", "
|
|
1849
|
-
)} (
|
|
2018
|
+
)} (${scenario.name})`,
|
|
1850
2019
|
"error",
|
|
1851
2020
|
file,
|
|
1852
|
-
"traceability.
|
|
1853
|
-
|
|
2021
|
+
"traceability.scenarioSpecExists",
|
|
2022
|
+
unknownSpecIds
|
|
1854
2023
|
)
|
|
1855
2024
|
);
|
|
1856
2025
|
}
|
|
2026
|
+
const unknownBrIds = brTags.filter((id) => !brIdsInSpecs.has(id));
|
|
2027
|
+
if (unknownBrIds.length > 0) {
|
|
2028
|
+
issues.push(
|
|
2029
|
+
issue6(
|
|
2030
|
+
"QFAI-TRACE-006",
|
|
2031
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(
|
|
2032
|
+
", "
|
|
2033
|
+
)} (${scenario.name})`,
|
|
2034
|
+
"error",
|
|
2035
|
+
file,
|
|
2036
|
+
"traceability.scenarioBrExists",
|
|
2037
|
+
unknownBrIds
|
|
2038
|
+
)
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
const unknownContractIds = atom.contractIds.filter(
|
|
2042
|
+
(id) => !contractIds.has(id)
|
|
2043
|
+
);
|
|
2044
|
+
if (unknownContractIds.length > 0) {
|
|
2045
|
+
issues.push(
|
|
2046
|
+
issue6(
|
|
2047
|
+
"QFAI-TRACE-008",
|
|
2048
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
2049
|
+
", "
|
|
2050
|
+
)} (${scenario.name})`,
|
|
2051
|
+
config.validation.traceability.unknownContractIdSeverity,
|
|
2052
|
+
file,
|
|
2053
|
+
"traceability.scenarioContractExists",
|
|
2054
|
+
unknownContractIds
|
|
2055
|
+
)
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
if (specTags.length > 0 && brTags.length > 0) {
|
|
2059
|
+
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
2060
|
+
for (const specId of specTags) {
|
|
2061
|
+
const brIdsForSpec = specToBrIds.get(specId);
|
|
2062
|
+
if (!brIdsForSpec) {
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
2066
|
+
}
|
|
2067
|
+
const invalidBrIds = brTags.filter((id) => !allowedBrIds.has(id));
|
|
2068
|
+
if (invalidBrIds.length > 0) {
|
|
2069
|
+
issues.push(
|
|
2070
|
+
issue6(
|
|
2071
|
+
"QFAI-TRACE-007",
|
|
2072
|
+
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
2073
|
+
", "
|
|
2074
|
+
)} (SPEC: ${specTags.join(", ")}) (${scenario.name})`,
|
|
2075
|
+
"error",
|
|
2076
|
+
file,
|
|
2077
|
+
"traceability.scenarioBrUnderSpec",
|
|
2078
|
+
invalidBrIds
|
|
2079
|
+
)
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
1857
2083
|
}
|
|
1858
2084
|
}
|
|
1859
2085
|
if (upstreamIds.size === 0) {
|
|
@@ -1951,7 +2177,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1951
2177
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
1952
2178
|
let found = false;
|
|
1953
2179
|
for (const file of targetFiles) {
|
|
1954
|
-
const text = await (0,
|
|
2180
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1955
2181
|
if (pattern.test(text)) {
|
|
1956
2182
|
found = true;
|
|
1957
2183
|
break;
|
|
@@ -2178,7 +2404,7 @@ async function collectIds(files) {
|
|
|
2178
2404
|
DATA: /* @__PURE__ */ new Set()
|
|
2179
2405
|
};
|
|
2180
2406
|
for (const file of files) {
|
|
2181
|
-
const text = await (0,
|
|
2407
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2182
2408
|
for (const prefix of ID_PREFIXES2) {
|
|
2183
2409
|
const ids = extractIds(text, prefix);
|
|
2184
2410
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -2196,7 +2422,7 @@ async function collectIds(files) {
|
|
|
2196
2422
|
async function collectUpstreamIds(files) {
|
|
2197
2423
|
const ids = /* @__PURE__ */ new Set();
|
|
2198
2424
|
for (const file of files) {
|
|
2199
|
-
const text = await (0,
|
|
2425
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2200
2426
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
2201
2427
|
}
|
|
2202
2428
|
return ids;
|
|
@@ -2217,7 +2443,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
2217
2443
|
}
|
|
2218
2444
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
2219
2445
|
for (const file of targetFiles) {
|
|
2220
|
-
const text = await (0,
|
|
2446
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2221
2447
|
if (pattern.test(text)) {
|
|
2222
2448
|
return true;
|
|
2223
2449
|
}
|
|
@@ -2269,7 +2495,7 @@ async function runReport(options) {
|
|
|
2269
2495
|
try {
|
|
2270
2496
|
validation = await readValidationResult(inputPath);
|
|
2271
2497
|
} catch (err) {
|
|
2272
|
-
if (
|
|
2498
|
+
if (isMissingFileError5(err)) {
|
|
2273
2499
|
error(
|
|
2274
2500
|
[
|
|
2275
2501
|
`qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
|
|
@@ -2292,8 +2518,8 @@ async function runReport(options) {
|
|
|
2292
2518
|
const defaultOut = options.format === "json" ? import_node_path14.default.join(outRoot, "report.json") : import_node_path14.default.join(outRoot, "report.md");
|
|
2293
2519
|
const out = options.outPath ?? defaultOut;
|
|
2294
2520
|
const outPath = import_node_path14.default.isAbsolute(out) ? out : import_node_path14.default.resolve(root, out);
|
|
2295
|
-
await (0,
|
|
2296
|
-
await (0,
|
|
2521
|
+
await (0, import_promises15.mkdir)(import_node_path14.default.dirname(outPath), { recursive: true });
|
|
2522
|
+
await (0, import_promises15.writeFile)(outPath, `${output}
|
|
2297
2523
|
`, "utf-8");
|
|
2298
2524
|
info(
|
|
2299
2525
|
`report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
|
|
@@ -2301,7 +2527,7 @@ async function runReport(options) {
|
|
|
2301
2527
|
info(`wrote report: ${outPath}`);
|
|
2302
2528
|
}
|
|
2303
2529
|
async function readValidationResult(inputPath) {
|
|
2304
|
-
const raw = await (0,
|
|
2530
|
+
const raw = await (0, import_promises15.readFile)(inputPath, "utf-8");
|
|
2305
2531
|
const parsed = JSON.parse(raw);
|
|
2306
2532
|
if (!isValidationResult(parsed)) {
|
|
2307
2533
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
@@ -2333,7 +2559,7 @@ function isValidationResult(value) {
|
|
|
2333
2559
|
}
|
|
2334
2560
|
return typeof counts.info === "number" && typeof counts.warning === "number" && typeof counts.error === "number";
|
|
2335
2561
|
}
|
|
2336
|
-
function
|
|
2562
|
+
function isMissingFileError5(error2) {
|
|
2337
2563
|
if (!error2 || typeof error2 !== "object") {
|
|
2338
2564
|
return false;
|
|
2339
2565
|
}
|
|
@@ -2342,7 +2568,7 @@ function isMissingFileError2(error2) {
|
|
|
2342
2568
|
}
|
|
2343
2569
|
|
|
2344
2570
|
// src/cli/commands/validate.ts
|
|
2345
|
-
var
|
|
2571
|
+
var import_promises16 = require("fs/promises");
|
|
2346
2572
|
var import_node_path15 = __toESM(require("path"), 1);
|
|
2347
2573
|
|
|
2348
2574
|
// src/cli/lib/failOn.ts
|
|
@@ -2408,8 +2634,8 @@ function emitGitHub(issue7) {
|
|
|
2408
2634
|
}
|
|
2409
2635
|
async function emitJson(result, root, jsonPath) {
|
|
2410
2636
|
const abs = import_node_path15.default.isAbsolute(jsonPath) ? jsonPath : import_node_path15.default.resolve(root, jsonPath);
|
|
2411
|
-
await (0,
|
|
2412
|
-
await (0,
|
|
2637
|
+
await (0, import_promises16.mkdir)(import_node_path15.default.dirname(abs), { recursive: true });
|
|
2638
|
+
await (0, import_promises16.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
2413
2639
|
`, "utf-8");
|
|
2414
2640
|
}
|
|
2415
2641
|
|