qfai 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/assets/init/.qfai/specs/README.md +5 -5
- package/dist/cli/index.cjs +450 -241
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +422 -209
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +441 -232
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +420 -207
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
- /package/assets/init/.qfai/specs/{spec-001 → spec-0001}/delta.md +0 -0
- /package/assets/init/.qfai/specs/{spec-001 → spec-0001}/scenario.md +0 -0
- /package/assets/init/.qfai/specs/{spec-001 → spec-0001}/spec.md +0 -0
package/dist/cli/index.mjs
CHANGED
|
@@ -478,7 +478,7 @@ import { readFile as readFile10 } from "fs/promises";
|
|
|
478
478
|
import path13 from "path";
|
|
479
479
|
|
|
480
480
|
// src/core/discovery.ts
|
|
481
|
-
import
|
|
481
|
+
import { access as access3 } from "fs/promises";
|
|
482
482
|
|
|
483
483
|
// src/core/fs.ts
|
|
484
484
|
import { access as access2, readdir as readdir2 } from "fs/promises";
|
|
@@ -535,25 +535,50 @@ async function exists2(target) {
|
|
|
535
535
|
}
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
-
// src/core/
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
538
|
+
// src/core/specLayout.ts
|
|
539
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
540
|
+
import path6 from "path";
|
|
541
|
+
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
542
|
+
async function collectSpecEntries(specsRoot) {
|
|
543
|
+
const dirs = await listSpecDirs(specsRoot);
|
|
544
|
+
const entries = dirs.map((dir) => ({
|
|
545
|
+
dir,
|
|
546
|
+
specPath: path6.join(dir, "spec.md"),
|
|
547
|
+
deltaPath: path6.join(dir, "delta.md"),
|
|
548
|
+
scenarioPath: path6.join(dir, "scenario.md")
|
|
549
|
+
}));
|
|
550
|
+
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
551
|
+
}
|
|
552
|
+
async function listSpecDirs(specsRoot) {
|
|
553
|
+
try {
|
|
554
|
+
const items = await readdir3(specsRoot, { withFileTypes: true });
|
|
555
|
+
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => path6.join(specsRoot, name));
|
|
556
|
+
} catch (error2) {
|
|
557
|
+
if (isMissingFileError(error2)) {
|
|
558
|
+
return [];
|
|
546
559
|
}
|
|
560
|
+
throw error2;
|
|
547
561
|
}
|
|
548
|
-
|
|
562
|
+
}
|
|
563
|
+
function isMissingFileError(error2) {
|
|
564
|
+
if (!error2 || typeof error2 !== "object") {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
return error2.code === "ENOENT";
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/core/discovery.ts
|
|
571
|
+
async function collectSpecPackDirs(specsRoot) {
|
|
572
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
573
|
+
return entries.map((entry) => entry.dir);
|
|
549
574
|
}
|
|
550
575
|
async function collectSpecFiles(specsRoot) {
|
|
551
|
-
const
|
|
552
|
-
return
|
|
576
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
577
|
+
return filterExisting(entries.map((entry) => entry.specPath));
|
|
553
578
|
}
|
|
554
579
|
async function collectScenarioFiles(specsRoot) {
|
|
555
|
-
const
|
|
556
|
-
return
|
|
580
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
581
|
+
return filterExisting(entries.map((entry) => entry.scenarioPath));
|
|
557
582
|
}
|
|
558
583
|
async function collectUiContractFiles(uiRoot) {
|
|
559
584
|
return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
@@ -572,12 +597,22 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
|
572
597
|
]);
|
|
573
598
|
return { ui, api, db };
|
|
574
599
|
}
|
|
575
|
-
function
|
|
576
|
-
|
|
600
|
+
async function filterExisting(files) {
|
|
601
|
+
const existing = [];
|
|
602
|
+
for (const file of files) {
|
|
603
|
+
if (await exists3(file)) {
|
|
604
|
+
existing.push(file);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return existing;
|
|
608
|
+
}
|
|
609
|
+
async function exists3(target) {
|
|
610
|
+
try {
|
|
611
|
+
await access3(target);
|
|
612
|
+
return true;
|
|
613
|
+
} catch {
|
|
577
614
|
return false;
|
|
578
615
|
}
|
|
579
|
-
const dirName = path6.basename(path6.dirname(filePath)).toLowerCase();
|
|
580
|
-
return SPEC_PACK_DIR_PATTERN.test(dirName);
|
|
581
616
|
}
|
|
582
617
|
|
|
583
618
|
// src/core/ids.ts
|
|
@@ -641,8 +676,8 @@ import { readFile as readFile2 } from "fs/promises";
|
|
|
641
676
|
import path7 from "path";
|
|
642
677
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
643
678
|
async function resolveToolVersion() {
|
|
644
|
-
if ("0.3.
|
|
645
|
-
return "0.3.
|
|
679
|
+
if ("0.3.2".length > 0) {
|
|
680
|
+
return "0.3.2";
|
|
646
681
|
}
|
|
647
682
|
try {
|
|
648
683
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -975,7 +1010,7 @@ async function validateDeltas(root, config) {
|
|
|
975
1010
|
try {
|
|
976
1011
|
text = await readFile4(deltaPath, "utf-8");
|
|
977
1012
|
} catch (error2) {
|
|
978
|
-
if (
|
|
1013
|
+
if (isMissingFileError2(error2)) {
|
|
979
1014
|
issues.push(
|
|
980
1015
|
issue2(
|
|
981
1016
|
"QFAI-DELTA-001",
|
|
@@ -1020,7 +1055,7 @@ async function validateDeltas(root, config) {
|
|
|
1020
1055
|
}
|
|
1021
1056
|
return issues;
|
|
1022
1057
|
}
|
|
1023
|
-
function
|
|
1058
|
+
function isMissingFileError2(error2) {
|
|
1024
1059
|
if (!error2 || typeof error2 !== "object") {
|
|
1025
1060
|
return false;
|
|
1026
1061
|
}
|
|
@@ -1109,66 +1144,6 @@ function record(index, id, file) {
|
|
|
1109
1144
|
index.idToFiles.set(id, current);
|
|
1110
1145
|
}
|
|
1111
1146
|
|
|
1112
|
-
// src/core/parse/gherkin.ts
|
|
1113
|
-
var FEATURE_RE = /^\s*Feature:\s+/;
|
|
1114
|
-
var SCENARIO_RE = /^\s*Scenario(?: Outline)?:\s*(.+)\s*$/;
|
|
1115
|
-
var TAG_LINE_RE = /^\s*@/;
|
|
1116
|
-
function parseTags(line) {
|
|
1117
|
-
return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
|
|
1118
|
-
}
|
|
1119
|
-
function parseGherkinFeature(text, file) {
|
|
1120
|
-
const lines = text.split(/\r?\n/);
|
|
1121
|
-
const scenarios = [];
|
|
1122
|
-
let featurePresent = false;
|
|
1123
|
-
let featureTags = [];
|
|
1124
|
-
let pendingTags = [];
|
|
1125
|
-
let current = null;
|
|
1126
|
-
const flush = () => {
|
|
1127
|
-
if (!current) return;
|
|
1128
|
-
scenarios.push({
|
|
1129
|
-
...current,
|
|
1130
|
-
body: current.body.trim()
|
|
1131
|
-
});
|
|
1132
|
-
current = null;
|
|
1133
|
-
};
|
|
1134
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1135
|
-
const line = lines[i] ?? "";
|
|
1136
|
-
const trimmed = line.trim();
|
|
1137
|
-
if (TAG_LINE_RE.test(trimmed)) {
|
|
1138
|
-
pendingTags.push(...parseTags(trimmed));
|
|
1139
|
-
continue;
|
|
1140
|
-
}
|
|
1141
|
-
if (FEATURE_RE.test(trimmed)) {
|
|
1142
|
-
featurePresent = true;
|
|
1143
|
-
featureTags = [...pendingTags];
|
|
1144
|
-
pendingTags = [];
|
|
1145
|
-
continue;
|
|
1146
|
-
}
|
|
1147
|
-
const match = trimmed.match(SCENARIO_RE);
|
|
1148
|
-
if (match) {
|
|
1149
|
-
const scenarioName = match[1]?.trim();
|
|
1150
|
-
if (!scenarioName) {
|
|
1151
|
-
continue;
|
|
1152
|
-
}
|
|
1153
|
-
flush();
|
|
1154
|
-
current = {
|
|
1155
|
-
name: scenarioName,
|
|
1156
|
-
line: i + 1,
|
|
1157
|
-
tags: [...featureTags, ...pendingTags],
|
|
1158
|
-
body: ""
|
|
1159
|
-
};
|
|
1160
|
-
pendingTags = [];
|
|
1161
|
-
continue;
|
|
1162
|
-
}
|
|
1163
|
-
if (current) {
|
|
1164
|
-
current.body += `${line}
|
|
1165
|
-
`;
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
flush();
|
|
1169
|
-
return { file, featurePresent, scenarios };
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
1147
|
// src/core/parse/markdown.ts
|
|
1173
1148
|
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1174
1149
|
function parseHeadings(md) {
|
|
@@ -1287,8 +1262,166 @@ function parseSpec(md, file) {
|
|
|
1287
1262
|
return parsed;
|
|
1288
1263
|
}
|
|
1289
1264
|
|
|
1290
|
-
// src/core/
|
|
1265
|
+
// src/core/gherkin/parse.ts
|
|
1266
|
+
import {
|
|
1267
|
+
AstBuilder,
|
|
1268
|
+
GherkinClassicTokenMatcher,
|
|
1269
|
+
Parser
|
|
1270
|
+
} from "@cucumber/gherkin";
|
|
1271
|
+
import { randomUUID } from "crypto";
|
|
1272
|
+
function parseGherkin(source, uri) {
|
|
1273
|
+
const errors = [];
|
|
1274
|
+
const uuidFn = () => randomUUID();
|
|
1275
|
+
const builder = new AstBuilder(uuidFn);
|
|
1276
|
+
const matcher = new GherkinClassicTokenMatcher();
|
|
1277
|
+
const parser = new Parser(builder, matcher);
|
|
1278
|
+
try {
|
|
1279
|
+
const gherkinDocument = parser.parse(source);
|
|
1280
|
+
gherkinDocument.uri = uri;
|
|
1281
|
+
return { gherkinDocument, errors };
|
|
1282
|
+
} catch (error2) {
|
|
1283
|
+
errors.push(formatError3(error2));
|
|
1284
|
+
return { gherkinDocument: null, errors };
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
function formatError3(error2) {
|
|
1288
|
+
if (error2 instanceof Error) {
|
|
1289
|
+
return error2.message;
|
|
1290
|
+
}
|
|
1291
|
+
return String(error2);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// src/core/scenarioModel.ts
|
|
1295
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1291
1296
|
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1297
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1298
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1299
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
1300
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1301
|
+
function parseScenarioDocument(text, uri) {
|
|
1302
|
+
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1303
|
+
if (!gherkinDocument) {
|
|
1304
|
+
return { document: null, errors };
|
|
1305
|
+
}
|
|
1306
|
+
const feature = gherkinDocument.feature;
|
|
1307
|
+
if (!feature) {
|
|
1308
|
+
return {
|
|
1309
|
+
document: { uri, featureTags: [], scenarios: [] },
|
|
1310
|
+
errors
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
const featureTags = collectTagNames(feature.tags);
|
|
1314
|
+
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
1315
|
+
return {
|
|
1316
|
+
document: {
|
|
1317
|
+
uri,
|
|
1318
|
+
featureName: feature.name,
|
|
1319
|
+
featureTags,
|
|
1320
|
+
scenarios
|
|
1321
|
+
},
|
|
1322
|
+
errors
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
function buildScenarioAtoms(document) {
|
|
1326
|
+
return document.scenarios.map((scenario) => {
|
|
1327
|
+
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1328
|
+
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1329
|
+
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1330
|
+
const contractIds = /* @__PURE__ */ new Set();
|
|
1331
|
+
scenario.tags.forEach((tag) => {
|
|
1332
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1333
|
+
contractIds.add(tag);
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
for (const step of scenario.steps) {
|
|
1337
|
+
for (const text of collectStepTexts(step)) {
|
|
1338
|
+
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1339
|
+
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1340
|
+
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const atom = {
|
|
1344
|
+
uri: document.uri,
|
|
1345
|
+
featureName: document.featureName ?? "",
|
|
1346
|
+
scenarioName: scenario.name,
|
|
1347
|
+
kind: scenario.kind,
|
|
1348
|
+
brIds,
|
|
1349
|
+
contractIds: Array.from(contractIds).sort()
|
|
1350
|
+
};
|
|
1351
|
+
if (scenario.line !== void 0) {
|
|
1352
|
+
atom.line = scenario.line;
|
|
1353
|
+
}
|
|
1354
|
+
if (specIds.length === 1) {
|
|
1355
|
+
const specId = specIds[0];
|
|
1356
|
+
if (specId) {
|
|
1357
|
+
atom.specId = specId;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
if (scIds.length === 1) {
|
|
1361
|
+
const scId = scIds[0];
|
|
1362
|
+
if (scId) {
|
|
1363
|
+
atom.scId = scId;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return atom;
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
function collectScenarioNodes(feature, featureTags) {
|
|
1370
|
+
const scenarios = [];
|
|
1371
|
+
for (const child of feature.children) {
|
|
1372
|
+
if (child.scenario) {
|
|
1373
|
+
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
1374
|
+
}
|
|
1375
|
+
if (child.rule) {
|
|
1376
|
+
const ruleTags = collectTagNames(child.rule.tags);
|
|
1377
|
+
for (const ruleChild of child.rule.children) {
|
|
1378
|
+
if (ruleChild.scenario) {
|
|
1379
|
+
scenarios.push(
|
|
1380
|
+
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return scenarios;
|
|
1387
|
+
}
|
|
1388
|
+
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
1389
|
+
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
1390
|
+
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
1391
|
+
return {
|
|
1392
|
+
name: scenario.name,
|
|
1393
|
+
kind,
|
|
1394
|
+
line: scenario.location?.line,
|
|
1395
|
+
tags,
|
|
1396
|
+
steps: scenario.steps
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
function collectTagNames(tags) {
|
|
1400
|
+
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1401
|
+
}
|
|
1402
|
+
function collectStepTexts(step) {
|
|
1403
|
+
const texts = [];
|
|
1404
|
+
if (step.text) {
|
|
1405
|
+
texts.push(step.text);
|
|
1406
|
+
}
|
|
1407
|
+
if (step.docString?.content) {
|
|
1408
|
+
texts.push(step.docString.content);
|
|
1409
|
+
}
|
|
1410
|
+
if (step.dataTable?.rows) {
|
|
1411
|
+
for (const row of step.dataTable.rows) {
|
|
1412
|
+
for (const cell of row.cells) {
|
|
1413
|
+
texts.push(cell.value);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return texts;
|
|
1418
|
+
}
|
|
1419
|
+
function unique2(values) {
|
|
1420
|
+
return Array.from(new Set(values));
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// src/core/validators/ids.ts
|
|
1424
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1292
1425
|
async function validateDefinedIds(root, config) {
|
|
1293
1426
|
const issues = [];
|
|
1294
1427
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1333,10 +1466,13 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1333
1466
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1334
1467
|
for (const file of files) {
|
|
1335
1468
|
const text = await readFile6(file, "utf-8");
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1469
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1470
|
+
if (!document || errors.length > 0) {
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
for (const scenario of document.scenarios) {
|
|
1338
1474
|
for (const tag of scenario.tags) {
|
|
1339
|
-
if (
|
|
1475
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
1340
1476
|
recordId(out, tag, file);
|
|
1341
1477
|
}
|
|
1342
1478
|
}
|
|
@@ -1377,17 +1513,19 @@ import { readFile as readFile7 } from "fs/promises";
|
|
|
1377
1513
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1378
1514
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1379
1515
|
var THEN_PATTERN = /\bThen\b/;
|
|
1380
|
-
var
|
|
1381
|
-
var
|
|
1382
|
-
var
|
|
1516
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1517
|
+
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1518
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1383
1519
|
async function validateScenarios(root, config) {
|
|
1384
1520
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1385
|
-
const
|
|
1386
|
-
if (
|
|
1521
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1522
|
+
if (entries.length === 0) {
|
|
1523
|
+
const expected = "spec-0001/scenario.md";
|
|
1524
|
+
const legacy = "spec-001/scenario.md";
|
|
1387
1525
|
return [
|
|
1388
1526
|
issue4(
|
|
1389
1527
|
"QFAI-SC-000",
|
|
1390
|
-
|
|
1528
|
+
`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)`,
|
|
1391
1529
|
"info",
|
|
1392
1530
|
specsRoot,
|
|
1393
1531
|
"scenario.files"
|
|
@@ -1395,15 +1533,31 @@ async function validateScenarios(root, config) {
|
|
|
1395
1533
|
];
|
|
1396
1534
|
}
|
|
1397
1535
|
const issues = [];
|
|
1398
|
-
for (const
|
|
1399
|
-
|
|
1400
|
-
|
|
1536
|
+
for (const entry of entries) {
|
|
1537
|
+
let text;
|
|
1538
|
+
try {
|
|
1539
|
+
text = await readFile7(entry.scenarioPath, "utf-8");
|
|
1540
|
+
} catch (error2) {
|
|
1541
|
+
if (isMissingFileError3(error2)) {
|
|
1542
|
+
issues.push(
|
|
1543
|
+
issue4(
|
|
1544
|
+
"QFAI-SC-001",
|
|
1545
|
+
"scenario.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1546
|
+
"error",
|
|
1547
|
+
entry.scenarioPath,
|
|
1548
|
+
"scenario.exists"
|
|
1549
|
+
)
|
|
1550
|
+
);
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1553
|
+
throw error2;
|
|
1554
|
+
}
|
|
1555
|
+
issues.push(...validateScenarioContent(text, entry.scenarioPath));
|
|
1401
1556
|
}
|
|
1402
1557
|
return issues;
|
|
1403
1558
|
}
|
|
1404
1559
|
function validateScenarioContent(text, file) {
|
|
1405
1560
|
const issues = [];
|
|
1406
|
-
const parsed = parseGherkinFeature(text, file);
|
|
1407
1561
|
const invalidIds = extractInvalidIds(text, [
|
|
1408
1562
|
"SPEC",
|
|
1409
1563
|
"BR",
|
|
@@ -1425,9 +1579,47 @@ function validateScenarioContent(text, file) {
|
|
|
1425
1579
|
)
|
|
1426
1580
|
);
|
|
1427
1581
|
}
|
|
1582
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1583
|
+
if (!document || errors.length > 0) {
|
|
1584
|
+
issues.push(
|
|
1585
|
+
issue4(
|
|
1586
|
+
"QFAI-SC-010",
|
|
1587
|
+
`Gherkin \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errors.join(", ") || "unknown"}`,
|
|
1588
|
+
"error",
|
|
1589
|
+
file,
|
|
1590
|
+
"scenario.parse"
|
|
1591
|
+
)
|
|
1592
|
+
);
|
|
1593
|
+
return issues;
|
|
1594
|
+
}
|
|
1595
|
+
const featureSpecTags = document.featureTags.filter(
|
|
1596
|
+
(tag) => SPEC_TAG_RE2.test(tag)
|
|
1597
|
+
);
|
|
1598
|
+
if (featureSpecTags.length === 0) {
|
|
1599
|
+
issues.push(
|
|
1600
|
+
issue4(
|
|
1601
|
+
"QFAI-SC-009",
|
|
1602
|
+
"Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1603
|
+
"error",
|
|
1604
|
+
file,
|
|
1605
|
+
"scenario.featureSpec"
|
|
1606
|
+
)
|
|
1607
|
+
);
|
|
1608
|
+
} else if (featureSpecTags.length > 1) {
|
|
1609
|
+
issues.push(
|
|
1610
|
+
issue4(
|
|
1611
|
+
"QFAI-SC-009",
|
|
1612
|
+
`Feature \u306E SPEC \u30BF\u30B0\u304C\u8907\u6570\u3042\u308A\u307E\u3059: ${featureSpecTags.join(", ")}`,
|
|
1613
|
+
"error",
|
|
1614
|
+
file,
|
|
1615
|
+
"scenario.featureSpec",
|
|
1616
|
+
featureSpecTags
|
|
1617
|
+
)
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1428
1620
|
const missingStructure = [];
|
|
1429
|
-
if (!
|
|
1430
|
-
if (
|
|
1621
|
+
if (!document.featureName) missingStructure.push("Feature");
|
|
1622
|
+
if (document.scenarios.length === 0) missingStructure.push("Scenario");
|
|
1431
1623
|
if (missingStructure.length > 0) {
|
|
1432
1624
|
issues.push(
|
|
1433
1625
|
issue4(
|
|
@@ -1441,7 +1633,7 @@ function validateScenarioContent(text, file) {
|
|
|
1441
1633
|
)
|
|
1442
1634
|
);
|
|
1443
1635
|
}
|
|
1444
|
-
for (const scenario of
|
|
1636
|
+
for (const scenario of document.scenarios) {
|
|
1445
1637
|
if (scenario.tags.length === 0) {
|
|
1446
1638
|
issues.push(
|
|
1447
1639
|
issue4(
|
|
@@ -1455,16 +1647,16 @@ function validateScenarioContent(text, file) {
|
|
|
1455
1647
|
continue;
|
|
1456
1648
|
}
|
|
1457
1649
|
const missingTags = [];
|
|
1458
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1650
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
|
|
1459
1651
|
if (scTags.length === 0) {
|
|
1460
1652
|
missingTags.push("SC(0\u4EF6)");
|
|
1461
1653
|
} else if (scTags.length > 1) {
|
|
1462
1654
|
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1463
1655
|
}
|
|
1464
|
-
if (!scenario.tags.some((tag) =>
|
|
1656
|
+
if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
|
|
1465
1657
|
missingTags.push("SPEC");
|
|
1466
1658
|
}
|
|
1467
|
-
if (!scenario.tags.some((tag) =>
|
|
1659
|
+
if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
|
|
1468
1660
|
missingTags.push("BR");
|
|
1469
1661
|
}
|
|
1470
1662
|
if (missingTags.length > 0) {
|
|
@@ -1479,15 +1671,16 @@ function validateScenarioContent(text, file) {
|
|
|
1479
1671
|
);
|
|
1480
1672
|
}
|
|
1481
1673
|
}
|
|
1482
|
-
for (const scenario of
|
|
1674
|
+
for (const scenario of document.scenarios) {
|
|
1483
1675
|
const missingSteps = [];
|
|
1484
|
-
|
|
1676
|
+
const keywords = scenario.steps.map((step) => step.keyword.trim());
|
|
1677
|
+
if (!keywords.some((keyword) => GIVEN_PATTERN.test(keyword))) {
|
|
1485
1678
|
missingSteps.push("Given");
|
|
1486
1679
|
}
|
|
1487
|
-
if (!WHEN_PATTERN.test(
|
|
1680
|
+
if (!keywords.some((keyword) => WHEN_PATTERN.test(keyword))) {
|
|
1488
1681
|
missingSteps.push("When");
|
|
1489
1682
|
}
|
|
1490
|
-
if (!THEN_PATTERN.test(
|
|
1683
|
+
if (!keywords.some((keyword) => THEN_PATTERN.test(keyword))) {
|
|
1491
1684
|
missingSteps.push("Then");
|
|
1492
1685
|
}
|
|
1493
1686
|
if (missingSteps.length > 0) {
|
|
@@ -1521,18 +1714,25 @@ function issue4(code, message, severity, file, rule, refs) {
|
|
|
1521
1714
|
}
|
|
1522
1715
|
return issue7;
|
|
1523
1716
|
}
|
|
1717
|
+
function isMissingFileError3(error2) {
|
|
1718
|
+
if (!error2 || typeof error2 !== "object") {
|
|
1719
|
+
return false;
|
|
1720
|
+
}
|
|
1721
|
+
return error2.code === "ENOENT";
|
|
1722
|
+
}
|
|
1524
1723
|
|
|
1525
1724
|
// src/core/validators/spec.ts
|
|
1526
1725
|
import { readFile as readFile8 } from "fs/promises";
|
|
1527
1726
|
async function validateSpecs(root, config) {
|
|
1528
1727
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1529
|
-
const
|
|
1530
|
-
if (
|
|
1531
|
-
const expected = "spec-
|
|
1728
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1729
|
+
if (entries.length === 0) {
|
|
1730
|
+
const expected = "spec-0001/spec.md";
|
|
1731
|
+
const legacy = "spec-001/spec.md";
|
|
1532
1732
|
return [
|
|
1533
1733
|
issue5(
|
|
1534
1734
|
"QFAI-SPEC-000",
|
|
1535
|
-
`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}`,
|
|
1735
|
+
`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)`,
|
|
1536
1736
|
"info",
|
|
1537
1737
|
specsRoot,
|
|
1538
1738
|
"spec.files"
|
|
@@ -1540,12 +1740,29 @@ async function validateSpecs(root, config) {
|
|
|
1540
1740
|
];
|
|
1541
1741
|
}
|
|
1542
1742
|
const issues = [];
|
|
1543
|
-
for (const
|
|
1544
|
-
|
|
1743
|
+
for (const entry of entries) {
|
|
1744
|
+
let text;
|
|
1745
|
+
try {
|
|
1746
|
+
text = await readFile8(entry.specPath, "utf-8");
|
|
1747
|
+
} catch (error2) {
|
|
1748
|
+
if (isMissingFileError4(error2)) {
|
|
1749
|
+
issues.push(
|
|
1750
|
+
issue5(
|
|
1751
|
+
"QFAI-SPEC-005",
|
|
1752
|
+
"spec.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1753
|
+
"error",
|
|
1754
|
+
entry.specPath,
|
|
1755
|
+
"spec.exists"
|
|
1756
|
+
)
|
|
1757
|
+
);
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
throw error2;
|
|
1761
|
+
}
|
|
1545
1762
|
issues.push(
|
|
1546
1763
|
...validateSpecContent(
|
|
1547
1764
|
text,
|
|
1548
|
-
|
|
1765
|
+
entry.specPath,
|
|
1549
1766
|
config.validation.require.specSections
|
|
1550
1767
|
)
|
|
1551
1768
|
);
|
|
@@ -1667,15 +1884,18 @@ function issue5(code, message, severity, file, rule, refs) {
|
|
|
1667
1884
|
}
|
|
1668
1885
|
return issue7;
|
|
1669
1886
|
}
|
|
1887
|
+
function isMissingFileError4(error2) {
|
|
1888
|
+
if (!error2 || typeof error2 !== "object") {
|
|
1889
|
+
return false;
|
|
1890
|
+
}
|
|
1891
|
+
return error2.code === "ENOENT";
|
|
1892
|
+
}
|
|
1670
1893
|
|
|
1671
1894
|
// src/core/validators/traceability.ts
|
|
1672
1895
|
import { readFile as readFile9 } from "fs/promises";
|
|
1673
|
-
var
|
|
1674
|
-
var
|
|
1675
|
-
var
|
|
1676
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1677
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
1678
|
-
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1896
|
+
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1897
|
+
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1898
|
+
var BR_TAG_RE3 = /^BR-\d{4}$/;
|
|
1679
1899
|
async function validateTraceability(root, config) {
|
|
1680
1900
|
const issues = [];
|
|
1681
1901
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1733,104 +1953,97 @@ async function validateTraceability(root, config) {
|
|
|
1733
1953
|
for (const file of scenarioFiles) {
|
|
1734
1954
|
const text = await readFile9(file, "utf-8");
|
|
1735
1955
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1736
|
-
const
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
const scIds = /* @__PURE__ */ new Set();
|
|
1740
|
-
const scenarioIds = /* @__PURE__ */ new Set();
|
|
1741
|
-
for (const scenario of parsed.scenarios) {
|
|
1742
|
-
for (const tag of scenario.tags) {
|
|
1743
|
-
if (SPEC_TAG_RE2.test(tag)) {
|
|
1744
|
-
specIdsInScenario.add(tag);
|
|
1745
|
-
}
|
|
1746
|
-
if (BR_TAG_RE2.test(tag)) {
|
|
1747
|
-
brIds.add(tag);
|
|
1748
|
-
}
|
|
1749
|
-
if (SC_TAG_RE3.test(tag)) {
|
|
1750
|
-
scIds.add(tag);
|
|
1751
|
-
}
|
|
1752
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1753
|
-
scenarioIds.add(tag);
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
const specIdsList = Array.from(specIdsInScenario);
|
|
1758
|
-
const brIdsList = Array.from(brIds);
|
|
1759
|
-
const scIdsList = Array.from(scIds);
|
|
1760
|
-
const scenarioIdsList = Array.from(scenarioIds);
|
|
1761
|
-
brIdsList.forEach((id) => brIdsInScenarios.add(id));
|
|
1762
|
-
scIdsList.forEach((id) => scIdsInScenarios.add(id));
|
|
1763
|
-
scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
|
|
1764
|
-
if (scenarioIdsList.length > 0) {
|
|
1765
|
-
scIdsList.forEach((id) => scWithContracts.add(id));
|
|
1766
|
-
}
|
|
1767
|
-
const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
|
|
1768
|
-
if (unknownSpecIds.length > 0) {
|
|
1769
|
-
issues.push(
|
|
1770
|
-
issue6(
|
|
1771
|
-
"QFAI-TRACE-005",
|
|
1772
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1773
|
-
"error",
|
|
1774
|
-
file,
|
|
1775
|
-
"traceability.scenarioSpecExists",
|
|
1776
|
-
unknownSpecIds
|
|
1777
|
-
)
|
|
1778
|
-
);
|
|
1779
|
-
}
|
|
1780
|
-
const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
|
|
1781
|
-
if (unknownBrIds.length > 0) {
|
|
1782
|
-
issues.push(
|
|
1783
|
-
issue6(
|
|
1784
|
-
"QFAI-TRACE-006",
|
|
1785
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1786
|
-
"error",
|
|
1787
|
-
file,
|
|
1788
|
-
"traceability.scenarioBrExists",
|
|
1789
|
-
unknownBrIds
|
|
1790
|
-
)
|
|
1791
|
-
);
|
|
1792
|
-
}
|
|
1793
|
-
const unknownContractIds = scenarioIdsList.filter(
|
|
1794
|
-
(id) => !contractIds.has(id)
|
|
1795
|
-
);
|
|
1796
|
-
if (unknownContractIds.length > 0) {
|
|
1797
|
-
issues.push(
|
|
1798
|
-
issue6(
|
|
1799
|
-
"QFAI-TRACE-008",
|
|
1800
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1801
|
-
", "
|
|
1802
|
-
)}`,
|
|
1803
|
-
config.validation.traceability.unknownContractIdSeverity,
|
|
1804
|
-
file,
|
|
1805
|
-
"traceability.scenarioContractExists",
|
|
1806
|
-
unknownContractIds
|
|
1807
|
-
)
|
|
1808
|
-
);
|
|
1956
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1957
|
+
if (!document || errors.length > 0) {
|
|
1958
|
+
continue;
|
|
1809
1959
|
}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1960
|
+
const atoms = buildScenarioAtoms(document);
|
|
1961
|
+
for (const [index, scenario] of document.scenarios.entries()) {
|
|
1962
|
+
const atom = atoms[index];
|
|
1963
|
+
if (!atom) {
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
1967
|
+
const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
|
|
1968
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
|
|
1969
|
+
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
1970
|
+
scTags.forEach((id) => scIdsInScenarios.add(id));
|
|
1971
|
+
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
1972
|
+
if (atom.contractIds.length > 0) {
|
|
1973
|
+
scTags.forEach((id) => scWithContracts.add(id));
|
|
1974
|
+
}
|
|
1975
|
+
const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
|
|
1976
|
+
if (unknownSpecIds.length > 0) {
|
|
1977
|
+
issues.push(
|
|
1978
|
+
issue6(
|
|
1979
|
+
"QFAI-TRACE-005",
|
|
1980
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(
|
|
1981
|
+
", "
|
|
1982
|
+
)} (${scenario.name})`,
|
|
1983
|
+
"error",
|
|
1984
|
+
file,
|
|
1985
|
+
"traceability.scenarioSpecExists",
|
|
1986
|
+
unknownSpecIds
|
|
1987
|
+
)
|
|
1988
|
+
);
|
|
1818
1989
|
}
|
|
1819
|
-
const
|
|
1820
|
-
if (
|
|
1990
|
+
const unknownBrIds = brTags.filter((id) => !brIdsInSpecs.has(id));
|
|
1991
|
+
if (unknownBrIds.length > 0) {
|
|
1821
1992
|
issues.push(
|
|
1822
1993
|
issue6(
|
|
1823
|
-
"QFAI-TRACE-
|
|
1824
|
-
`Scenario \
|
|
1994
|
+
"QFAI-TRACE-006",
|
|
1995
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(
|
|
1825
1996
|
", "
|
|
1826
|
-
)} (
|
|
1997
|
+
)} (${scenario.name})`,
|
|
1827
1998
|
"error",
|
|
1828
1999
|
file,
|
|
1829
|
-
"traceability.
|
|
1830
|
-
|
|
2000
|
+
"traceability.scenarioBrExists",
|
|
2001
|
+
unknownBrIds
|
|
2002
|
+
)
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
const unknownContractIds = atom.contractIds.filter(
|
|
2006
|
+
(id) => !contractIds.has(id)
|
|
2007
|
+
);
|
|
2008
|
+
if (unknownContractIds.length > 0) {
|
|
2009
|
+
issues.push(
|
|
2010
|
+
issue6(
|
|
2011
|
+
"QFAI-TRACE-008",
|
|
2012
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
2013
|
+
", "
|
|
2014
|
+
)} (${scenario.name})`,
|
|
2015
|
+
config.validation.traceability.unknownContractIdSeverity,
|
|
2016
|
+
file,
|
|
2017
|
+
"traceability.scenarioContractExists",
|
|
2018
|
+
unknownContractIds
|
|
1831
2019
|
)
|
|
1832
2020
|
);
|
|
1833
2021
|
}
|
|
2022
|
+
if (specTags.length > 0 && brTags.length > 0) {
|
|
2023
|
+
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
2024
|
+
for (const specId of specTags) {
|
|
2025
|
+
const brIdsForSpec = specToBrIds.get(specId);
|
|
2026
|
+
if (!brIdsForSpec) {
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
2030
|
+
}
|
|
2031
|
+
const invalidBrIds = brTags.filter((id) => !allowedBrIds.has(id));
|
|
2032
|
+
if (invalidBrIds.length > 0) {
|
|
2033
|
+
issues.push(
|
|
2034
|
+
issue6(
|
|
2035
|
+
"QFAI-TRACE-007",
|
|
2036
|
+
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
2037
|
+
", "
|
|
2038
|
+
)} (SPEC: ${specTags.join(", ")}) (${scenario.name})`,
|
|
2039
|
+
"error",
|
|
2040
|
+
file,
|
|
2041
|
+
"traceability.scenarioBrUnderSpec",
|
|
2042
|
+
invalidBrIds
|
|
2043
|
+
)
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
1834
2047
|
}
|
|
1835
2048
|
}
|
|
1836
2049
|
if (upstreamIds.size === 0) {
|
|
@@ -2246,7 +2459,7 @@ async function runReport(options) {
|
|
|
2246
2459
|
try {
|
|
2247
2460
|
validation = await readValidationResult(inputPath);
|
|
2248
2461
|
} catch (err) {
|
|
2249
|
-
if (
|
|
2462
|
+
if (isMissingFileError5(err)) {
|
|
2250
2463
|
error(
|
|
2251
2464
|
[
|
|
2252
2465
|
`qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
|
|
@@ -2310,7 +2523,7 @@ function isValidationResult(value) {
|
|
|
2310
2523
|
}
|
|
2311
2524
|
return typeof counts.info === "number" && typeof counts.warning === "number" && typeof counts.error === "number";
|
|
2312
2525
|
}
|
|
2313
|
-
function
|
|
2526
|
+
function isMissingFileError5(error2) {
|
|
2314
2527
|
if (!error2 || typeof error2 !== "object") {
|
|
2315
2528
|
return false;
|
|
2316
2529
|
}
|