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.mjs
CHANGED
|
@@ -389,7 +389,7 @@ import { readFile as readFile10 } from "fs/promises";
|
|
|
389
389
|
import path10 from "path";
|
|
390
390
|
|
|
391
391
|
// src/core/discovery.ts
|
|
392
|
-
import
|
|
392
|
+
import { access as access2 } from "fs/promises";
|
|
393
393
|
|
|
394
394
|
// src/core/fs.ts
|
|
395
395
|
import { access, readdir } from "fs/promises";
|
|
@@ -446,25 +446,50 @@ async function exists(target) {
|
|
|
446
446
|
}
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
// src/core/
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
449
|
+
// src/core/specLayout.ts
|
|
450
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
451
|
+
import path3 from "path";
|
|
452
|
+
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
453
|
+
async function collectSpecEntries(specsRoot) {
|
|
454
|
+
const dirs = await listSpecDirs(specsRoot);
|
|
455
|
+
const entries = dirs.map((dir) => ({
|
|
456
|
+
dir,
|
|
457
|
+
specPath: path3.join(dir, "spec.md"),
|
|
458
|
+
deltaPath: path3.join(dir, "delta.md"),
|
|
459
|
+
scenarioPath: path3.join(dir, "scenario.md")
|
|
460
|
+
}));
|
|
461
|
+
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
462
|
+
}
|
|
463
|
+
async function listSpecDirs(specsRoot) {
|
|
464
|
+
try {
|
|
465
|
+
const items = await readdir2(specsRoot, { withFileTypes: true });
|
|
466
|
+
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => path3.join(specsRoot, name));
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (isMissingFileError(error)) {
|
|
469
|
+
return [];
|
|
457
470
|
}
|
|
471
|
+
throw error;
|
|
458
472
|
}
|
|
459
|
-
|
|
473
|
+
}
|
|
474
|
+
function isMissingFileError(error) {
|
|
475
|
+
if (!error || typeof error !== "object") {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
return error.code === "ENOENT";
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/core/discovery.ts
|
|
482
|
+
async function collectSpecPackDirs(specsRoot) {
|
|
483
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
484
|
+
return entries.map((entry) => entry.dir);
|
|
460
485
|
}
|
|
461
486
|
async function collectSpecFiles(specsRoot) {
|
|
462
|
-
const
|
|
463
|
-
return
|
|
487
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
488
|
+
return filterExisting(entries.map((entry) => entry.specPath));
|
|
464
489
|
}
|
|
465
490
|
async function collectScenarioFiles(specsRoot) {
|
|
466
|
-
const
|
|
467
|
-
return
|
|
491
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
492
|
+
return filterExisting(entries.map((entry) => entry.scenarioPath));
|
|
468
493
|
}
|
|
469
494
|
async function collectUiContractFiles(uiRoot) {
|
|
470
495
|
return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
@@ -483,12 +508,22 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
|
483
508
|
]);
|
|
484
509
|
return { ui, api, db };
|
|
485
510
|
}
|
|
486
|
-
function
|
|
487
|
-
|
|
511
|
+
async function filterExisting(files) {
|
|
512
|
+
const existing = [];
|
|
513
|
+
for (const file of files) {
|
|
514
|
+
if (await exists2(file)) {
|
|
515
|
+
existing.push(file);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return existing;
|
|
519
|
+
}
|
|
520
|
+
async function exists2(target) {
|
|
521
|
+
try {
|
|
522
|
+
await access2(target);
|
|
523
|
+
return true;
|
|
524
|
+
} catch {
|
|
488
525
|
return false;
|
|
489
526
|
}
|
|
490
|
-
const dirName = path3.basename(path3.dirname(filePath)).toLowerCase();
|
|
491
|
-
return SPEC_PACK_DIR_PATTERN.test(dirName);
|
|
492
527
|
}
|
|
493
528
|
|
|
494
529
|
// src/core/types.ts
|
|
@@ -499,8 +534,8 @@ import { readFile as readFile2 } from "fs/promises";
|
|
|
499
534
|
import path4 from "path";
|
|
500
535
|
import { fileURLToPath } from "url";
|
|
501
536
|
async function resolveToolVersion() {
|
|
502
|
-
if ("0.3.
|
|
503
|
-
return "0.3.
|
|
537
|
+
if ("0.3.3".length > 0) {
|
|
538
|
+
return "0.3.3";
|
|
504
539
|
}
|
|
505
540
|
try {
|
|
506
541
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -833,7 +868,7 @@ async function validateDeltas(root, config) {
|
|
|
833
868
|
try {
|
|
834
869
|
text = await readFile4(deltaPath, "utf-8");
|
|
835
870
|
} catch (error) {
|
|
836
|
-
if (
|
|
871
|
+
if (isMissingFileError2(error)) {
|
|
837
872
|
issues.push(
|
|
838
873
|
issue2(
|
|
839
874
|
"QFAI-DELTA-001",
|
|
@@ -878,7 +913,7 @@ async function validateDeltas(root, config) {
|
|
|
878
913
|
}
|
|
879
914
|
return issues;
|
|
880
915
|
}
|
|
881
|
-
function
|
|
916
|
+
function isMissingFileError2(error) {
|
|
882
917
|
if (!error || typeof error !== "object") {
|
|
883
918
|
return false;
|
|
884
919
|
}
|
|
@@ -967,66 +1002,6 @@ function record(index, id, file) {
|
|
|
967
1002
|
index.idToFiles.set(id, current);
|
|
968
1003
|
}
|
|
969
1004
|
|
|
970
|
-
// src/core/parse/gherkin.ts
|
|
971
|
-
var FEATURE_RE = /^\s*Feature:\s+/;
|
|
972
|
-
var SCENARIO_RE = /^\s*Scenario(?: Outline)?:\s*(.+)\s*$/;
|
|
973
|
-
var TAG_LINE_RE = /^\s*@/;
|
|
974
|
-
function parseTags(line) {
|
|
975
|
-
return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
|
|
976
|
-
}
|
|
977
|
-
function parseGherkinFeature(text, file) {
|
|
978
|
-
const lines = text.split(/\r?\n/);
|
|
979
|
-
const scenarios = [];
|
|
980
|
-
let featurePresent = false;
|
|
981
|
-
let featureTags = [];
|
|
982
|
-
let pendingTags = [];
|
|
983
|
-
let current = null;
|
|
984
|
-
const flush = () => {
|
|
985
|
-
if (!current) return;
|
|
986
|
-
scenarios.push({
|
|
987
|
-
...current,
|
|
988
|
-
body: current.body.trim()
|
|
989
|
-
});
|
|
990
|
-
current = null;
|
|
991
|
-
};
|
|
992
|
-
for (let i = 0; i < lines.length; i++) {
|
|
993
|
-
const line = lines[i] ?? "";
|
|
994
|
-
const trimmed = line.trim();
|
|
995
|
-
if (TAG_LINE_RE.test(trimmed)) {
|
|
996
|
-
pendingTags.push(...parseTags(trimmed));
|
|
997
|
-
continue;
|
|
998
|
-
}
|
|
999
|
-
if (FEATURE_RE.test(trimmed)) {
|
|
1000
|
-
featurePresent = true;
|
|
1001
|
-
featureTags = [...pendingTags];
|
|
1002
|
-
pendingTags = [];
|
|
1003
|
-
continue;
|
|
1004
|
-
}
|
|
1005
|
-
const match = trimmed.match(SCENARIO_RE);
|
|
1006
|
-
if (match) {
|
|
1007
|
-
const scenarioName = match[1]?.trim();
|
|
1008
|
-
if (!scenarioName) {
|
|
1009
|
-
continue;
|
|
1010
|
-
}
|
|
1011
|
-
flush();
|
|
1012
|
-
current = {
|
|
1013
|
-
name: scenarioName,
|
|
1014
|
-
line: i + 1,
|
|
1015
|
-
tags: [...featureTags, ...pendingTags],
|
|
1016
|
-
body: ""
|
|
1017
|
-
};
|
|
1018
|
-
pendingTags = [];
|
|
1019
|
-
continue;
|
|
1020
|
-
}
|
|
1021
|
-
if (current) {
|
|
1022
|
-
current.body += `${line}
|
|
1023
|
-
`;
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
flush();
|
|
1027
|
-
return { file, featurePresent, scenarios };
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
1005
|
// src/core/parse/markdown.ts
|
|
1031
1006
|
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1032
1007
|
function parseHeadings(md) {
|
|
@@ -1145,8 +1120,166 @@ function parseSpec(md, file) {
|
|
|
1145
1120
|
return parsed;
|
|
1146
1121
|
}
|
|
1147
1122
|
|
|
1148
|
-
// src/core/
|
|
1123
|
+
// src/core/gherkin/parse.ts
|
|
1124
|
+
import {
|
|
1125
|
+
AstBuilder,
|
|
1126
|
+
GherkinClassicTokenMatcher,
|
|
1127
|
+
Parser
|
|
1128
|
+
} from "@cucumber/gherkin";
|
|
1129
|
+
import { randomUUID } from "crypto";
|
|
1130
|
+
function parseGherkin(source, uri) {
|
|
1131
|
+
const errors = [];
|
|
1132
|
+
const uuidFn = () => randomUUID();
|
|
1133
|
+
const builder = new AstBuilder(uuidFn);
|
|
1134
|
+
const matcher = new GherkinClassicTokenMatcher();
|
|
1135
|
+
const parser = new Parser(builder, matcher);
|
|
1136
|
+
try {
|
|
1137
|
+
const gherkinDocument = parser.parse(source);
|
|
1138
|
+
gherkinDocument.uri = uri;
|
|
1139
|
+
return { gherkinDocument, errors };
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
errors.push(formatError3(error));
|
|
1142
|
+
return { gherkinDocument: null, errors };
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
function formatError3(error) {
|
|
1146
|
+
if (error instanceof Error) {
|
|
1147
|
+
return error.message;
|
|
1148
|
+
}
|
|
1149
|
+
return String(error);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// src/core/scenarioModel.ts
|
|
1153
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1149
1154
|
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1155
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1156
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1157
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
1158
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1159
|
+
function parseScenarioDocument(text, uri) {
|
|
1160
|
+
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1161
|
+
if (!gherkinDocument) {
|
|
1162
|
+
return { document: null, errors };
|
|
1163
|
+
}
|
|
1164
|
+
const feature = gherkinDocument.feature;
|
|
1165
|
+
if (!feature) {
|
|
1166
|
+
return {
|
|
1167
|
+
document: { uri, featureTags: [], scenarios: [] },
|
|
1168
|
+
errors
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
const featureTags = collectTagNames(feature.tags);
|
|
1172
|
+
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
1173
|
+
return {
|
|
1174
|
+
document: {
|
|
1175
|
+
uri,
|
|
1176
|
+
featureName: feature.name,
|
|
1177
|
+
featureTags,
|
|
1178
|
+
scenarios
|
|
1179
|
+
},
|
|
1180
|
+
errors
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
function buildScenarioAtoms(document) {
|
|
1184
|
+
return document.scenarios.map((scenario) => {
|
|
1185
|
+
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1186
|
+
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1187
|
+
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1188
|
+
const contractIds = /* @__PURE__ */ new Set();
|
|
1189
|
+
scenario.tags.forEach((tag) => {
|
|
1190
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1191
|
+
contractIds.add(tag);
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
for (const step of scenario.steps) {
|
|
1195
|
+
for (const text of collectStepTexts(step)) {
|
|
1196
|
+
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1197
|
+
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1198
|
+
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
const atom = {
|
|
1202
|
+
uri: document.uri,
|
|
1203
|
+
featureName: document.featureName ?? "",
|
|
1204
|
+
scenarioName: scenario.name,
|
|
1205
|
+
kind: scenario.kind,
|
|
1206
|
+
brIds,
|
|
1207
|
+
contractIds: Array.from(contractIds).sort()
|
|
1208
|
+
};
|
|
1209
|
+
if (scenario.line !== void 0) {
|
|
1210
|
+
atom.line = scenario.line;
|
|
1211
|
+
}
|
|
1212
|
+
if (specIds.length === 1) {
|
|
1213
|
+
const specId = specIds[0];
|
|
1214
|
+
if (specId) {
|
|
1215
|
+
atom.specId = specId;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (scIds.length === 1) {
|
|
1219
|
+
const scId = scIds[0];
|
|
1220
|
+
if (scId) {
|
|
1221
|
+
atom.scId = scId;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return atom;
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
function collectScenarioNodes(feature, featureTags) {
|
|
1228
|
+
const scenarios = [];
|
|
1229
|
+
for (const child of feature.children) {
|
|
1230
|
+
if (child.scenario) {
|
|
1231
|
+
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
1232
|
+
}
|
|
1233
|
+
if (child.rule) {
|
|
1234
|
+
const ruleTags = collectTagNames(child.rule.tags);
|
|
1235
|
+
for (const ruleChild of child.rule.children) {
|
|
1236
|
+
if (ruleChild.scenario) {
|
|
1237
|
+
scenarios.push(
|
|
1238
|
+
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
return scenarios;
|
|
1245
|
+
}
|
|
1246
|
+
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
1247
|
+
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
1248
|
+
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
1249
|
+
return {
|
|
1250
|
+
name: scenario.name,
|
|
1251
|
+
kind,
|
|
1252
|
+
line: scenario.location?.line,
|
|
1253
|
+
tags,
|
|
1254
|
+
steps: scenario.steps
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
function collectTagNames(tags) {
|
|
1258
|
+
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1259
|
+
}
|
|
1260
|
+
function collectStepTexts(step) {
|
|
1261
|
+
const texts = [];
|
|
1262
|
+
if (step.text) {
|
|
1263
|
+
texts.push(step.text);
|
|
1264
|
+
}
|
|
1265
|
+
if (step.docString?.content) {
|
|
1266
|
+
texts.push(step.docString.content);
|
|
1267
|
+
}
|
|
1268
|
+
if (step.dataTable?.rows) {
|
|
1269
|
+
for (const row of step.dataTable.rows) {
|
|
1270
|
+
for (const cell of row.cells) {
|
|
1271
|
+
texts.push(cell.value);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
return texts;
|
|
1276
|
+
}
|
|
1277
|
+
function unique2(values) {
|
|
1278
|
+
return Array.from(new Set(values));
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/core/validators/ids.ts
|
|
1282
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1150
1283
|
async function validateDefinedIds(root, config) {
|
|
1151
1284
|
const issues = [];
|
|
1152
1285
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1191,10 +1324,13 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1191
1324
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1192
1325
|
for (const file of files) {
|
|
1193
1326
|
const text = await readFile6(file, "utf-8");
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1327
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1328
|
+
if (!document || errors.length > 0) {
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
for (const scenario of document.scenarios) {
|
|
1196
1332
|
for (const tag of scenario.tags) {
|
|
1197
|
-
if (
|
|
1333
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
1198
1334
|
recordId(out, tag, file);
|
|
1199
1335
|
}
|
|
1200
1336
|
}
|
|
@@ -1235,17 +1371,19 @@ import { readFile as readFile7 } from "fs/promises";
|
|
|
1235
1371
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1236
1372
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1237
1373
|
var THEN_PATTERN = /\bThen\b/;
|
|
1238
|
-
var
|
|
1239
|
-
var
|
|
1240
|
-
var
|
|
1374
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1375
|
+
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1376
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1241
1377
|
async function validateScenarios(root, config) {
|
|
1242
1378
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1243
|
-
const
|
|
1244
|
-
if (
|
|
1379
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1380
|
+
if (entries.length === 0) {
|
|
1381
|
+
const expected = "spec-0001/scenario.md";
|
|
1382
|
+
const legacy = "spec-001/scenario.md";
|
|
1245
1383
|
return [
|
|
1246
1384
|
issue4(
|
|
1247
1385
|
"QFAI-SC-000",
|
|
1248
|
-
|
|
1386
|
+
`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)`,
|
|
1249
1387
|
"info",
|
|
1250
1388
|
specsRoot,
|
|
1251
1389
|
"scenario.files"
|
|
@@ -1253,15 +1391,31 @@ async function validateScenarios(root, config) {
|
|
|
1253
1391
|
];
|
|
1254
1392
|
}
|
|
1255
1393
|
const issues = [];
|
|
1256
|
-
for (const
|
|
1257
|
-
|
|
1258
|
-
|
|
1394
|
+
for (const entry of entries) {
|
|
1395
|
+
let text;
|
|
1396
|
+
try {
|
|
1397
|
+
text = await readFile7(entry.scenarioPath, "utf-8");
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
if (isMissingFileError3(error)) {
|
|
1400
|
+
issues.push(
|
|
1401
|
+
issue4(
|
|
1402
|
+
"QFAI-SC-001",
|
|
1403
|
+
"scenario.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1404
|
+
"error",
|
|
1405
|
+
entry.scenarioPath,
|
|
1406
|
+
"scenario.exists"
|
|
1407
|
+
)
|
|
1408
|
+
);
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
throw error;
|
|
1412
|
+
}
|
|
1413
|
+
issues.push(...validateScenarioContent(text, entry.scenarioPath));
|
|
1259
1414
|
}
|
|
1260
1415
|
return issues;
|
|
1261
1416
|
}
|
|
1262
1417
|
function validateScenarioContent(text, file) {
|
|
1263
1418
|
const issues = [];
|
|
1264
|
-
const parsed = parseGherkinFeature(text, file);
|
|
1265
1419
|
const invalidIds = extractInvalidIds(text, [
|
|
1266
1420
|
"SPEC",
|
|
1267
1421
|
"BR",
|
|
@@ -1283,9 +1437,47 @@ function validateScenarioContent(text, file) {
|
|
|
1283
1437
|
)
|
|
1284
1438
|
);
|
|
1285
1439
|
}
|
|
1440
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1441
|
+
if (!document || errors.length > 0) {
|
|
1442
|
+
issues.push(
|
|
1443
|
+
issue4(
|
|
1444
|
+
"QFAI-SC-010",
|
|
1445
|
+
`Gherkin \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errors.join(", ") || "unknown"}`,
|
|
1446
|
+
"error",
|
|
1447
|
+
file,
|
|
1448
|
+
"scenario.parse"
|
|
1449
|
+
)
|
|
1450
|
+
);
|
|
1451
|
+
return issues;
|
|
1452
|
+
}
|
|
1453
|
+
const featureSpecTags = document.featureTags.filter(
|
|
1454
|
+
(tag) => SPEC_TAG_RE2.test(tag)
|
|
1455
|
+
);
|
|
1456
|
+
if (featureSpecTags.length === 0) {
|
|
1457
|
+
issues.push(
|
|
1458
|
+
issue4(
|
|
1459
|
+
"QFAI-SC-009",
|
|
1460
|
+
"Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1461
|
+
"error",
|
|
1462
|
+
file,
|
|
1463
|
+
"scenario.featureSpec"
|
|
1464
|
+
)
|
|
1465
|
+
);
|
|
1466
|
+
} else if (featureSpecTags.length > 1) {
|
|
1467
|
+
issues.push(
|
|
1468
|
+
issue4(
|
|
1469
|
+
"QFAI-SC-009",
|
|
1470
|
+
`Feature \u306E SPEC \u30BF\u30B0\u304C\u8907\u6570\u3042\u308A\u307E\u3059: ${featureSpecTags.join(", ")}`,
|
|
1471
|
+
"error",
|
|
1472
|
+
file,
|
|
1473
|
+
"scenario.featureSpec",
|
|
1474
|
+
featureSpecTags
|
|
1475
|
+
)
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1286
1478
|
const missingStructure = [];
|
|
1287
|
-
if (!
|
|
1288
|
-
if (
|
|
1479
|
+
if (!document.featureName) missingStructure.push("Feature");
|
|
1480
|
+
if (document.scenarios.length === 0) missingStructure.push("Scenario");
|
|
1289
1481
|
if (missingStructure.length > 0) {
|
|
1290
1482
|
issues.push(
|
|
1291
1483
|
issue4(
|
|
@@ -1299,7 +1491,7 @@ function validateScenarioContent(text, file) {
|
|
|
1299
1491
|
)
|
|
1300
1492
|
);
|
|
1301
1493
|
}
|
|
1302
|
-
for (const scenario of
|
|
1494
|
+
for (const scenario of document.scenarios) {
|
|
1303
1495
|
if (scenario.tags.length === 0) {
|
|
1304
1496
|
issues.push(
|
|
1305
1497
|
issue4(
|
|
@@ -1313,16 +1505,16 @@ function validateScenarioContent(text, file) {
|
|
|
1313
1505
|
continue;
|
|
1314
1506
|
}
|
|
1315
1507
|
const missingTags = [];
|
|
1316
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1508
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
|
|
1317
1509
|
if (scTags.length === 0) {
|
|
1318
1510
|
missingTags.push("SC(0\u4EF6)");
|
|
1319
1511
|
} else if (scTags.length > 1) {
|
|
1320
1512
|
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1321
1513
|
}
|
|
1322
|
-
if (!scenario.tags.some((tag) =>
|
|
1514
|
+
if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
|
|
1323
1515
|
missingTags.push("SPEC");
|
|
1324
1516
|
}
|
|
1325
|
-
if (!scenario.tags.some((tag) =>
|
|
1517
|
+
if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
|
|
1326
1518
|
missingTags.push("BR");
|
|
1327
1519
|
}
|
|
1328
1520
|
if (missingTags.length > 0) {
|
|
@@ -1337,15 +1529,16 @@ function validateScenarioContent(text, file) {
|
|
|
1337
1529
|
);
|
|
1338
1530
|
}
|
|
1339
1531
|
}
|
|
1340
|
-
for (const scenario of
|
|
1532
|
+
for (const scenario of document.scenarios) {
|
|
1341
1533
|
const missingSteps = [];
|
|
1342
|
-
|
|
1534
|
+
const keywords = scenario.steps.map((step) => step.keyword.trim());
|
|
1535
|
+
if (!keywords.some((keyword) => GIVEN_PATTERN.test(keyword))) {
|
|
1343
1536
|
missingSteps.push("Given");
|
|
1344
1537
|
}
|
|
1345
|
-
if (!WHEN_PATTERN.test(
|
|
1538
|
+
if (!keywords.some((keyword) => WHEN_PATTERN.test(keyword))) {
|
|
1346
1539
|
missingSteps.push("When");
|
|
1347
1540
|
}
|
|
1348
|
-
if (!THEN_PATTERN.test(
|
|
1541
|
+
if (!keywords.some((keyword) => THEN_PATTERN.test(keyword))) {
|
|
1349
1542
|
missingSteps.push("Then");
|
|
1350
1543
|
}
|
|
1351
1544
|
if (missingSteps.length > 0) {
|
|
@@ -1379,18 +1572,25 @@ function issue4(code, message, severity, file, rule, refs) {
|
|
|
1379
1572
|
}
|
|
1380
1573
|
return issue7;
|
|
1381
1574
|
}
|
|
1575
|
+
function isMissingFileError3(error) {
|
|
1576
|
+
if (!error || typeof error !== "object") {
|
|
1577
|
+
return false;
|
|
1578
|
+
}
|
|
1579
|
+
return error.code === "ENOENT";
|
|
1580
|
+
}
|
|
1382
1581
|
|
|
1383
1582
|
// src/core/validators/spec.ts
|
|
1384
1583
|
import { readFile as readFile8 } from "fs/promises";
|
|
1385
1584
|
async function validateSpecs(root, config) {
|
|
1386
1585
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1387
|
-
const
|
|
1388
|
-
if (
|
|
1389
|
-
const expected = "spec-
|
|
1586
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1587
|
+
if (entries.length === 0) {
|
|
1588
|
+
const expected = "spec-0001/spec.md";
|
|
1589
|
+
const legacy = "spec-001/spec.md";
|
|
1390
1590
|
return [
|
|
1391
1591
|
issue5(
|
|
1392
1592
|
"QFAI-SPEC-000",
|
|
1393
|
-
`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}`,
|
|
1593
|
+
`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)`,
|
|
1394
1594
|
"info",
|
|
1395
1595
|
specsRoot,
|
|
1396
1596
|
"spec.files"
|
|
@@ -1398,12 +1598,29 @@ async function validateSpecs(root, config) {
|
|
|
1398
1598
|
];
|
|
1399
1599
|
}
|
|
1400
1600
|
const issues = [];
|
|
1401
|
-
for (const
|
|
1402
|
-
|
|
1601
|
+
for (const entry of entries) {
|
|
1602
|
+
let text;
|
|
1603
|
+
try {
|
|
1604
|
+
text = await readFile8(entry.specPath, "utf-8");
|
|
1605
|
+
} catch (error) {
|
|
1606
|
+
if (isMissingFileError4(error)) {
|
|
1607
|
+
issues.push(
|
|
1608
|
+
issue5(
|
|
1609
|
+
"QFAI-SPEC-005",
|
|
1610
|
+
"spec.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1611
|
+
"error",
|
|
1612
|
+
entry.specPath,
|
|
1613
|
+
"spec.exists"
|
|
1614
|
+
)
|
|
1615
|
+
);
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
throw error;
|
|
1619
|
+
}
|
|
1403
1620
|
issues.push(
|
|
1404
1621
|
...validateSpecContent(
|
|
1405
1622
|
text,
|
|
1406
|
-
|
|
1623
|
+
entry.specPath,
|
|
1407
1624
|
config.validation.require.specSections
|
|
1408
1625
|
)
|
|
1409
1626
|
);
|
|
@@ -1525,15 +1742,18 @@ function issue5(code, message, severity, file, rule, refs) {
|
|
|
1525
1742
|
}
|
|
1526
1743
|
return issue7;
|
|
1527
1744
|
}
|
|
1745
|
+
function isMissingFileError4(error) {
|
|
1746
|
+
if (!error || typeof error !== "object") {
|
|
1747
|
+
return false;
|
|
1748
|
+
}
|
|
1749
|
+
return error.code === "ENOENT";
|
|
1750
|
+
}
|
|
1528
1751
|
|
|
1529
1752
|
// src/core/validators/traceability.ts
|
|
1530
1753
|
import { readFile as readFile9 } from "fs/promises";
|
|
1531
|
-
var
|
|
1532
|
-
var
|
|
1533
|
-
var
|
|
1534
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1535
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
1536
|
-
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1754
|
+
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1755
|
+
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1756
|
+
var BR_TAG_RE3 = /^BR-\d{4}$/;
|
|
1537
1757
|
async function validateTraceability(root, config) {
|
|
1538
1758
|
const issues = [];
|
|
1539
1759
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1591,104 +1811,97 @@ async function validateTraceability(root, config) {
|
|
|
1591
1811
|
for (const file of scenarioFiles) {
|
|
1592
1812
|
const text = await readFile9(file, "utf-8");
|
|
1593
1813
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1594
|
-
const
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
const scIds = /* @__PURE__ */ new Set();
|
|
1598
|
-
const scenarioIds = /* @__PURE__ */ new Set();
|
|
1599
|
-
for (const scenario of parsed.scenarios) {
|
|
1600
|
-
for (const tag of scenario.tags) {
|
|
1601
|
-
if (SPEC_TAG_RE2.test(tag)) {
|
|
1602
|
-
specIdsInScenario.add(tag);
|
|
1603
|
-
}
|
|
1604
|
-
if (BR_TAG_RE2.test(tag)) {
|
|
1605
|
-
brIds.add(tag);
|
|
1606
|
-
}
|
|
1607
|
-
if (SC_TAG_RE3.test(tag)) {
|
|
1608
|
-
scIds.add(tag);
|
|
1609
|
-
}
|
|
1610
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1611
|
-
scenarioIds.add(tag);
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
const specIdsList = Array.from(specIdsInScenario);
|
|
1616
|
-
const brIdsList = Array.from(brIds);
|
|
1617
|
-
const scIdsList = Array.from(scIds);
|
|
1618
|
-
const scenarioIdsList = Array.from(scenarioIds);
|
|
1619
|
-
brIdsList.forEach((id) => brIdsInScenarios.add(id));
|
|
1620
|
-
scIdsList.forEach((id) => scIdsInScenarios.add(id));
|
|
1621
|
-
scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
|
|
1622
|
-
if (scenarioIdsList.length > 0) {
|
|
1623
|
-
scIdsList.forEach((id) => scWithContracts.add(id));
|
|
1624
|
-
}
|
|
1625
|
-
const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
|
|
1626
|
-
if (unknownSpecIds.length > 0) {
|
|
1627
|
-
issues.push(
|
|
1628
|
-
issue6(
|
|
1629
|
-
"QFAI-TRACE-005",
|
|
1630
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1631
|
-
"error",
|
|
1632
|
-
file,
|
|
1633
|
-
"traceability.scenarioSpecExists",
|
|
1634
|
-
unknownSpecIds
|
|
1635
|
-
)
|
|
1636
|
-
);
|
|
1637
|
-
}
|
|
1638
|
-
const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
|
|
1639
|
-
if (unknownBrIds.length > 0) {
|
|
1640
|
-
issues.push(
|
|
1641
|
-
issue6(
|
|
1642
|
-
"QFAI-TRACE-006",
|
|
1643
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1644
|
-
"error",
|
|
1645
|
-
file,
|
|
1646
|
-
"traceability.scenarioBrExists",
|
|
1647
|
-
unknownBrIds
|
|
1648
|
-
)
|
|
1649
|
-
);
|
|
1650
|
-
}
|
|
1651
|
-
const unknownContractIds = scenarioIdsList.filter(
|
|
1652
|
-
(id) => !contractIds.has(id)
|
|
1653
|
-
);
|
|
1654
|
-
if (unknownContractIds.length > 0) {
|
|
1655
|
-
issues.push(
|
|
1656
|
-
issue6(
|
|
1657
|
-
"QFAI-TRACE-008",
|
|
1658
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1659
|
-
", "
|
|
1660
|
-
)}`,
|
|
1661
|
-
config.validation.traceability.unknownContractIdSeverity,
|
|
1662
|
-
file,
|
|
1663
|
-
"traceability.scenarioContractExists",
|
|
1664
|
-
unknownContractIds
|
|
1665
|
-
)
|
|
1666
|
-
);
|
|
1814
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1815
|
+
if (!document || errors.length > 0) {
|
|
1816
|
+
continue;
|
|
1667
1817
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1818
|
+
const atoms = buildScenarioAtoms(document);
|
|
1819
|
+
for (const [index, scenario] of document.scenarios.entries()) {
|
|
1820
|
+
const atom = atoms[index];
|
|
1821
|
+
if (!atom) {
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
1825
|
+
const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
|
|
1826
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
|
|
1827
|
+
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
1828
|
+
scTags.forEach((id) => scIdsInScenarios.add(id));
|
|
1829
|
+
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
1830
|
+
if (atom.contractIds.length > 0) {
|
|
1831
|
+
scTags.forEach((id) => scWithContracts.add(id));
|
|
1832
|
+
}
|
|
1833
|
+
const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
|
|
1834
|
+
if (unknownSpecIds.length > 0) {
|
|
1835
|
+
issues.push(
|
|
1836
|
+
issue6(
|
|
1837
|
+
"QFAI-TRACE-005",
|
|
1838
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(
|
|
1839
|
+
", "
|
|
1840
|
+
)} (${scenario.name})`,
|
|
1841
|
+
"error",
|
|
1842
|
+
file,
|
|
1843
|
+
"traceability.scenarioSpecExists",
|
|
1844
|
+
unknownSpecIds
|
|
1845
|
+
)
|
|
1846
|
+
);
|
|
1676
1847
|
}
|
|
1677
|
-
const
|
|
1678
|
-
if (
|
|
1848
|
+
const unknownBrIds = brTags.filter((id) => !brIdsInSpecs.has(id));
|
|
1849
|
+
if (unknownBrIds.length > 0) {
|
|
1679
1850
|
issues.push(
|
|
1680
1851
|
issue6(
|
|
1681
|
-
"QFAI-TRACE-
|
|
1682
|
-
`Scenario \
|
|
1852
|
+
"QFAI-TRACE-006",
|
|
1853
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(
|
|
1683
1854
|
", "
|
|
1684
|
-
)} (
|
|
1855
|
+
)} (${scenario.name})`,
|
|
1685
1856
|
"error",
|
|
1686
1857
|
file,
|
|
1687
|
-
"traceability.
|
|
1688
|
-
|
|
1858
|
+
"traceability.scenarioBrExists",
|
|
1859
|
+
unknownBrIds
|
|
1860
|
+
)
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
const unknownContractIds = atom.contractIds.filter(
|
|
1864
|
+
(id) => !contractIds.has(id)
|
|
1865
|
+
);
|
|
1866
|
+
if (unknownContractIds.length > 0) {
|
|
1867
|
+
issues.push(
|
|
1868
|
+
issue6(
|
|
1869
|
+
"QFAI-TRACE-008",
|
|
1870
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1871
|
+
", "
|
|
1872
|
+
)} (${scenario.name})`,
|
|
1873
|
+
config.validation.traceability.unknownContractIdSeverity,
|
|
1874
|
+
file,
|
|
1875
|
+
"traceability.scenarioContractExists",
|
|
1876
|
+
unknownContractIds
|
|
1689
1877
|
)
|
|
1690
1878
|
);
|
|
1691
1879
|
}
|
|
1880
|
+
if (specTags.length > 0 && brTags.length > 0) {
|
|
1881
|
+
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
1882
|
+
for (const specId of specTags) {
|
|
1883
|
+
const brIdsForSpec = specToBrIds.get(specId);
|
|
1884
|
+
if (!brIdsForSpec) {
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
1888
|
+
}
|
|
1889
|
+
const invalidBrIds = brTags.filter((id) => !allowedBrIds.has(id));
|
|
1890
|
+
if (invalidBrIds.length > 0) {
|
|
1891
|
+
issues.push(
|
|
1892
|
+
issue6(
|
|
1893
|
+
"QFAI-TRACE-007",
|
|
1894
|
+
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
1895
|
+
", "
|
|
1896
|
+
)} (SPEC: ${specTags.join(", ")}) (${scenario.name})`,
|
|
1897
|
+
"error",
|
|
1898
|
+
file,
|
|
1899
|
+
"traceability.scenarioBrUnderSpec",
|
|
1900
|
+
invalidBrIds
|
|
1901
|
+
)
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1692
1905
|
}
|
|
1693
1906
|
}
|
|
1694
1907
|
if (upstreamIds.size === 0) {
|