qfai 0.2.9 → 0.3.1
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 +6 -8
- package/assets/init/.qfai/README.md +6 -5
- package/assets/init/.qfai/rules/conventions.md +21 -0
- package/assets/init/.qfai/specs/README.md +51 -0
- package/assets/init/.qfai/specs/spec-001/delta.md +30 -0
- package/assets/init/.qfai/specs/spec-001/scenario.md +10 -0
- package/assets/init/.qfai/{spec/spec-0001-sample.md → specs/spec-001/spec.md} +3 -1
- package/assets/init/root/.github/workflows/qfai.yml +1 -1
- package/assets/init/root/qfai.config.yaml +5 -8
- package/dist/cli/index.cjs +637 -351
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +634 -348
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +602 -297
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -11
- package/dist/index.d.ts +9 -11
- package/dist/index.mjs +601 -297
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/assets/init/.qfai/spec/README.md +0 -80
- package/assets/init/.qfai/spec/decisions/ADR-0001.md +0 -9
- package/assets/init/.qfai/spec/decisions/README.md +0 -36
- package/assets/init/.qfai/spec/scenarios/scenarios.feature +0 -6
package/dist/cli/index.mjs
CHANGED
|
@@ -137,8 +137,8 @@ function report(copied, skipped, dryRun, label) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// src/cli/commands/report.ts
|
|
140
|
-
import { mkdir as mkdir2, readFile as
|
|
141
|
-
import
|
|
140
|
+
import { mkdir as mkdir2, readFile as readFile11, writeFile } from "fs/promises";
|
|
141
|
+
import path14 from "path";
|
|
142
142
|
|
|
143
143
|
// src/core/config.ts
|
|
144
144
|
import { readFile } from "fs/promises";
|
|
@@ -146,13 +146,11 @@ import path4 from "path";
|
|
|
146
146
|
import { parse as parseYaml } from "yaml";
|
|
147
147
|
var defaultConfig = {
|
|
148
148
|
paths: {
|
|
149
|
-
specDir: ".qfai/spec",
|
|
150
|
-
decisionsDir: ".qfai/spec/decisions",
|
|
151
|
-
scenariosDir: ".qfai/spec/scenarios",
|
|
152
149
|
contractsDir: ".qfai/contracts",
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
specsDir: ".qfai/specs",
|
|
151
|
+
rulesDir: ".qfai/rules",
|
|
152
|
+
outDir: ".qfai/out",
|
|
153
|
+
promptsDir: ".qfai/prompts",
|
|
156
154
|
srcDir: "src",
|
|
157
155
|
testsDir: "tests"
|
|
158
156
|
},
|
|
@@ -177,8 +175,7 @@ var defaultConfig = {
|
|
|
177
175
|
}
|
|
178
176
|
},
|
|
179
177
|
output: {
|
|
180
|
-
|
|
181
|
-
jsonPath: ".qfai/out/validate.json"
|
|
178
|
+
validateJsonPath: ".qfai/out/validate.json"
|
|
182
179
|
}
|
|
183
180
|
};
|
|
184
181
|
function getConfigPath(root) {
|
|
@@ -227,27 +224,6 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
227
224
|
return base;
|
|
228
225
|
}
|
|
229
226
|
return {
|
|
230
|
-
specDir: readString(
|
|
231
|
-
raw.specDir,
|
|
232
|
-
base.specDir,
|
|
233
|
-
"paths.specDir",
|
|
234
|
-
configPath,
|
|
235
|
-
issues
|
|
236
|
-
),
|
|
237
|
-
decisionsDir: readString(
|
|
238
|
-
raw.decisionsDir,
|
|
239
|
-
base.decisionsDir,
|
|
240
|
-
"paths.decisionsDir",
|
|
241
|
-
configPath,
|
|
242
|
-
issues
|
|
243
|
-
),
|
|
244
|
-
scenariosDir: readString(
|
|
245
|
-
raw.scenariosDir,
|
|
246
|
-
base.scenariosDir,
|
|
247
|
-
"paths.scenariosDir",
|
|
248
|
-
configPath,
|
|
249
|
-
issues
|
|
250
|
-
),
|
|
251
227
|
contractsDir: readString(
|
|
252
228
|
raw.contractsDir,
|
|
253
229
|
base.contractsDir,
|
|
@@ -255,24 +231,31 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
255
231
|
configPath,
|
|
256
232
|
issues
|
|
257
233
|
),
|
|
258
|
-
|
|
259
|
-
raw.
|
|
260
|
-
base.
|
|
261
|
-
"paths.
|
|
234
|
+
specsDir: readString(
|
|
235
|
+
raw.specsDir,
|
|
236
|
+
base.specsDir,
|
|
237
|
+
"paths.specsDir",
|
|
262
238
|
configPath,
|
|
263
239
|
issues
|
|
264
240
|
),
|
|
265
|
-
|
|
266
|
-
raw.
|
|
267
|
-
base.
|
|
268
|
-
"paths.
|
|
241
|
+
rulesDir: readString(
|
|
242
|
+
raw.rulesDir,
|
|
243
|
+
base.rulesDir,
|
|
244
|
+
"paths.rulesDir",
|
|
269
245
|
configPath,
|
|
270
246
|
issues
|
|
271
247
|
),
|
|
272
|
-
|
|
273
|
-
raw.
|
|
274
|
-
base.
|
|
275
|
-
"paths.
|
|
248
|
+
outDir: readString(
|
|
249
|
+
raw.outDir,
|
|
250
|
+
base.outDir,
|
|
251
|
+
"paths.outDir",
|
|
252
|
+
configPath,
|
|
253
|
+
issues
|
|
254
|
+
),
|
|
255
|
+
promptsDir: readString(
|
|
256
|
+
raw.promptsDir,
|
|
257
|
+
base.promptsDir,
|
|
258
|
+
"paths.promptsDir",
|
|
276
259
|
configPath,
|
|
277
260
|
issues
|
|
278
261
|
),
|
|
@@ -395,17 +378,10 @@ function normalizeOutput(raw, configPath, issues) {
|
|
|
395
378
|
return base;
|
|
396
379
|
}
|
|
397
380
|
return {
|
|
398
|
-
|
|
399
|
-
raw.
|
|
400
|
-
base.
|
|
401
|
-
"output.
|
|
402
|
-
configPath,
|
|
403
|
-
issues
|
|
404
|
-
),
|
|
405
|
-
jsonPath: readString(
|
|
406
|
-
raw.jsonPath,
|
|
407
|
-
base.jsonPath,
|
|
408
|
-
"output.jsonPath",
|
|
381
|
+
validateJsonPath: readString(
|
|
382
|
+
raw.validateJsonPath,
|
|
383
|
+
base.validateJsonPath,
|
|
384
|
+
"output.validateJsonPath",
|
|
409
385
|
configPath,
|
|
410
386
|
issues
|
|
411
387
|
)
|
|
@@ -472,20 +448,6 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
|
472
448
|
}
|
|
473
449
|
return fallback;
|
|
474
450
|
}
|
|
475
|
-
function readOutputFormat(value, fallback, label, configPath, issues) {
|
|
476
|
-
if (value === "text" || value === "json" || value === "github") {
|
|
477
|
-
return value;
|
|
478
|
-
}
|
|
479
|
-
if (value !== void 0) {
|
|
480
|
-
issues.push(
|
|
481
|
-
configIssue(
|
|
482
|
-
configPath,
|
|
483
|
-
`${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
484
|
-
)
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
return fallback;
|
|
488
|
-
}
|
|
489
451
|
function configIssue(file, message) {
|
|
490
452
|
return {
|
|
491
453
|
code: "QFAI_CONFIG_INVALID",
|
|
@@ -512,7 +474,8 @@ function isRecord(value) {
|
|
|
512
474
|
}
|
|
513
475
|
|
|
514
476
|
// src/core/report.ts
|
|
515
|
-
import { readFile as
|
|
477
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
478
|
+
import path13 from "path";
|
|
516
479
|
|
|
517
480
|
// src/core/discovery.ts
|
|
518
481
|
import path6 from "path";
|
|
@@ -573,10 +536,24 @@ async function exists2(target) {
|
|
|
573
536
|
}
|
|
574
537
|
|
|
575
538
|
// src/core/discovery.ts
|
|
576
|
-
var
|
|
577
|
-
async function
|
|
578
|
-
const files = await collectFiles(
|
|
579
|
-
|
|
539
|
+
var SPEC_PACK_DIR_PATTERN = /^spec-\d{3}$/;
|
|
540
|
+
async function collectSpecPackDirs(specsRoot) {
|
|
541
|
+
const files = await collectFiles(specsRoot, { extensions: [".md"] });
|
|
542
|
+
const packs = /* @__PURE__ */ new Set();
|
|
543
|
+
for (const file of files) {
|
|
544
|
+
if (isSpecPackFile(file, "spec.md")) {
|
|
545
|
+
packs.add(path6.dirname(file));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return Array.from(packs).sort();
|
|
549
|
+
}
|
|
550
|
+
async function collectSpecFiles(specsRoot) {
|
|
551
|
+
const files = await collectFiles(specsRoot, { extensions: [".md"] });
|
|
552
|
+
return files.filter((file) => isSpecPackFile(file, "spec.md"));
|
|
553
|
+
}
|
|
554
|
+
async function collectScenarioFiles(specsRoot) {
|
|
555
|
+
const files = await collectFiles(specsRoot, { extensions: [".md"] });
|
|
556
|
+
return files.filter((file) => isSpecPackFile(file, "scenario.md"));
|
|
580
557
|
}
|
|
581
558
|
async function collectUiContractFiles(uiRoot) {
|
|
582
559
|
return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
@@ -595,9 +572,12 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
|
595
572
|
]);
|
|
596
573
|
return { ui, api, db };
|
|
597
574
|
}
|
|
598
|
-
function
|
|
599
|
-
|
|
600
|
-
|
|
575
|
+
function isSpecPackFile(filePath, baseName) {
|
|
576
|
+
if (path6.basename(filePath).toLowerCase() !== baseName) {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
const dirName = path6.basename(path6.dirname(filePath)).toLowerCase();
|
|
580
|
+
return SPEC_PACK_DIR_PATTERN.test(dirName);
|
|
601
581
|
}
|
|
602
582
|
|
|
603
583
|
// src/core/ids.ts
|
|
@@ -661,8 +641,8 @@ import { readFile as readFile2 } from "fs/promises";
|
|
|
661
641
|
import path7 from "path";
|
|
662
642
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
663
643
|
async function resolveToolVersion() {
|
|
664
|
-
if ("0.
|
|
665
|
-
return "0.
|
|
644
|
+
if ("0.3.1".length > 0) {
|
|
645
|
+
return "0.3.1";
|
|
666
646
|
}
|
|
667
647
|
try {
|
|
668
648
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -682,6 +662,7 @@ function resolvePackageJsonPath() {
|
|
|
682
662
|
|
|
683
663
|
// src/core/validators/contracts.ts
|
|
684
664
|
import { readFile as readFile3 } from "fs/promises";
|
|
665
|
+
import path9 from "path";
|
|
685
666
|
|
|
686
667
|
// src/core/contracts.ts
|
|
687
668
|
import path8 from "path";
|
|
@@ -737,19 +718,10 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
737
718
|
];
|
|
738
719
|
async function validateContracts(root, config) {
|
|
739
720
|
const issues = [];
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
);
|
|
743
|
-
issues.push(
|
|
744
|
-
...await validateApiContracts(
|
|
745
|
-
resolvePath(root, config, "apiContractsDir")
|
|
746
|
-
)
|
|
747
|
-
);
|
|
748
|
-
issues.push(
|
|
749
|
-
...await validateDataContracts(
|
|
750
|
-
resolvePath(root, config, "dataContractsDir")
|
|
751
|
-
)
|
|
752
|
-
);
|
|
721
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
722
|
+
issues.push(...await validateUiContracts(path9.join(contractsRoot, "ui")));
|
|
723
|
+
issues.push(...await validateApiContracts(path9.join(contractsRoot, "api")));
|
|
724
|
+
issues.push(...await validateDataContracts(path9.join(contractsRoot, "db")));
|
|
753
725
|
return issues;
|
|
754
726
|
}
|
|
755
727
|
async function validateUiContracts(uiRoot) {
|
|
@@ -965,33 +937,125 @@ function formatError2(error2) {
|
|
|
965
937
|
return String(error2);
|
|
966
938
|
}
|
|
967
939
|
function issue(code, message, severity, file, rule, refs) {
|
|
968
|
-
const
|
|
940
|
+
const issue7 = {
|
|
969
941
|
code,
|
|
970
942
|
severity,
|
|
971
943
|
message
|
|
972
944
|
};
|
|
973
945
|
if (file) {
|
|
974
|
-
|
|
946
|
+
issue7.file = file;
|
|
975
947
|
}
|
|
976
948
|
if (rule) {
|
|
977
|
-
|
|
949
|
+
issue7.rule = rule;
|
|
978
950
|
}
|
|
979
951
|
if (refs && refs.length > 0) {
|
|
980
|
-
|
|
952
|
+
issue7.refs = refs;
|
|
981
953
|
}
|
|
982
|
-
return
|
|
954
|
+
return issue7;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// src/core/validators/delta.ts
|
|
958
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
959
|
+
import path10 from "path";
|
|
960
|
+
var SECTION_RE = /^##\s+変更区分/m;
|
|
961
|
+
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
962
|
+
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
963
|
+
var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
|
|
964
|
+
var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
|
|
965
|
+
async function validateDeltas(root, config) {
|
|
966
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
967
|
+
const packs = await collectSpecPackDirs(specsRoot);
|
|
968
|
+
if (packs.length === 0) {
|
|
969
|
+
return [];
|
|
970
|
+
}
|
|
971
|
+
const issues = [];
|
|
972
|
+
for (const pack of packs) {
|
|
973
|
+
const deltaPath = path10.join(pack, "delta.md");
|
|
974
|
+
let text;
|
|
975
|
+
try {
|
|
976
|
+
text = await readFile4(deltaPath, "utf-8");
|
|
977
|
+
} catch (error2) {
|
|
978
|
+
if (isMissingFileError(error2)) {
|
|
979
|
+
issues.push(
|
|
980
|
+
issue2(
|
|
981
|
+
"QFAI-DELTA-001",
|
|
982
|
+
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
983
|
+
"error",
|
|
984
|
+
deltaPath,
|
|
985
|
+
"delta.exists"
|
|
986
|
+
)
|
|
987
|
+
);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
throw error2;
|
|
991
|
+
}
|
|
992
|
+
const hasSection = SECTION_RE.test(text);
|
|
993
|
+
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
994
|
+
const hasChange = CHANGE_LINE_RE.test(text);
|
|
995
|
+
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
996
|
+
issues.push(
|
|
997
|
+
issue2(
|
|
998
|
+
"QFAI-DELTA-002",
|
|
999
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
1000
|
+
"error",
|
|
1001
|
+
deltaPath,
|
|
1002
|
+
"delta.section"
|
|
1003
|
+
)
|
|
1004
|
+
);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
1008
|
+
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
1009
|
+
if (compatibilityChecked === changeChecked) {
|
|
1010
|
+
issues.push(
|
|
1011
|
+
issue2(
|
|
1012
|
+
"QFAI-DELTA-003",
|
|
1013
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u306F\u3069\u3061\u3089\u304B1\u3064\u3060\u3051\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4E21\u65B9ON/\u4E21\u65B9OFF\u306F\u7121\u52B9\u3067\u3059\uFF09\u3002",
|
|
1014
|
+
"error",
|
|
1015
|
+
deltaPath,
|
|
1016
|
+
"delta.classification"
|
|
1017
|
+
)
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return issues;
|
|
1022
|
+
}
|
|
1023
|
+
function isMissingFileError(error2) {
|
|
1024
|
+
if (!error2 || typeof error2 !== "object") {
|
|
1025
|
+
return false;
|
|
1026
|
+
}
|
|
1027
|
+
return error2.code === "ENOENT";
|
|
1028
|
+
}
|
|
1029
|
+
function issue2(code, message, severity, file, rule, refs) {
|
|
1030
|
+
const issue7 = {
|
|
1031
|
+
code,
|
|
1032
|
+
severity,
|
|
1033
|
+
message
|
|
1034
|
+
};
|
|
1035
|
+
if (file) {
|
|
1036
|
+
issue7.file = file;
|
|
1037
|
+
}
|
|
1038
|
+
if (rule) {
|
|
1039
|
+
issue7.rule = rule;
|
|
1040
|
+
}
|
|
1041
|
+
if (refs && refs.length > 0) {
|
|
1042
|
+
issue7.refs = refs;
|
|
1043
|
+
}
|
|
1044
|
+
return issue7;
|
|
983
1045
|
}
|
|
984
1046
|
|
|
985
1047
|
// src/core/validators/ids.ts
|
|
986
|
-
import { readFile as
|
|
987
|
-
import
|
|
1048
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1049
|
+
import path12 from "path";
|
|
988
1050
|
|
|
989
1051
|
// src/core/contractIndex.ts
|
|
990
|
-
import { readFile as
|
|
1052
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1053
|
+
import path11 from "path";
|
|
991
1054
|
async function buildContractIndex(root, config) {
|
|
992
|
-
const
|
|
993
|
-
const
|
|
994
|
-
const
|
|
1055
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1056
|
+
const uiRoot = path11.join(contractsRoot, "ui");
|
|
1057
|
+
const apiRoot = path11.join(contractsRoot, "api");
|
|
1058
|
+
const dataRoot = path11.join(contractsRoot, "db");
|
|
995
1059
|
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
996
1060
|
collectUiContractFiles(uiRoot),
|
|
997
1061
|
collectApiContractFiles(apiRoot),
|
|
@@ -1010,7 +1074,7 @@ async function buildContractIndex(root, config) {
|
|
|
1010
1074
|
}
|
|
1011
1075
|
async function indexUiContracts(files, index) {
|
|
1012
1076
|
for (const file of files) {
|
|
1013
|
-
const text = await
|
|
1077
|
+
const text = await readFile5(file, "utf-8");
|
|
1014
1078
|
try {
|
|
1015
1079
|
const doc = parseStructuredContract(file, text);
|
|
1016
1080
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1022,7 +1086,7 @@ async function indexUiContracts(files, index) {
|
|
|
1022
1086
|
}
|
|
1023
1087
|
async function indexApiContracts(files, index) {
|
|
1024
1088
|
for (const file of files) {
|
|
1025
|
-
const text = await
|
|
1089
|
+
const text = await readFile5(file, "utf-8");
|
|
1026
1090
|
try {
|
|
1027
1091
|
const doc = parseStructuredContract(file, text);
|
|
1028
1092
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1034,7 +1098,7 @@ async function indexApiContracts(files, index) {
|
|
|
1034
1098
|
}
|
|
1035
1099
|
async function indexDataContracts(files, index) {
|
|
1036
1100
|
for (const file of files) {
|
|
1037
|
-
const text = await
|
|
1101
|
+
const text = await readFile5(file, "utf-8");
|
|
1038
1102
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1039
1103
|
}
|
|
1040
1104
|
}
|
|
@@ -1045,15 +1109,191 @@ function record(index, id, file) {
|
|
|
1045
1109
|
index.idToFiles.set(id, current);
|
|
1046
1110
|
}
|
|
1047
1111
|
|
|
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
|
+
// src/core/parse/markdown.ts
|
|
1173
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1174
|
+
function parseHeadings(md) {
|
|
1175
|
+
const lines = md.split(/\r?\n/);
|
|
1176
|
+
const headings = [];
|
|
1177
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1178
|
+
const line = lines[i] ?? "";
|
|
1179
|
+
const match = line.match(HEADING_RE);
|
|
1180
|
+
if (!match) continue;
|
|
1181
|
+
const levelToken = match[1];
|
|
1182
|
+
const title = match[2];
|
|
1183
|
+
if (!levelToken || !title) continue;
|
|
1184
|
+
headings.push({
|
|
1185
|
+
level: levelToken.length,
|
|
1186
|
+
title: title.trim(),
|
|
1187
|
+
line: i + 1
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
return headings;
|
|
1191
|
+
}
|
|
1192
|
+
function extractH2Sections(md) {
|
|
1193
|
+
const lines = md.split(/\r?\n/);
|
|
1194
|
+
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1195
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1196
|
+
for (let i = 0; i < headings.length; i++) {
|
|
1197
|
+
const current = headings[i];
|
|
1198
|
+
if (!current) continue;
|
|
1199
|
+
const next = headings[i + 1];
|
|
1200
|
+
const startLine = current.line + 1;
|
|
1201
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1202
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1203
|
+
sections.set(current.title.trim(), {
|
|
1204
|
+
title: current.title.trim(),
|
|
1205
|
+
startLine,
|
|
1206
|
+
endLine,
|
|
1207
|
+
body
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
return sections;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// src/core/parse/spec.ts
|
|
1214
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1215
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1216
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1217
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1218
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1219
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1220
|
+
function parseSpec(md, file) {
|
|
1221
|
+
const headings = parseHeadings(md);
|
|
1222
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
1223
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1224
|
+
const sections = extractH2Sections(md);
|
|
1225
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1226
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1227
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1228
|
+
const startLine = brSection?.startLine ?? 1;
|
|
1229
|
+
const brs = [];
|
|
1230
|
+
const brsWithoutPriority = [];
|
|
1231
|
+
const brsWithInvalidPriority = [];
|
|
1232
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
1233
|
+
const lineText = brLines[i] ?? "";
|
|
1234
|
+
const lineNumber = startLine + i;
|
|
1235
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
1236
|
+
if (validMatch) {
|
|
1237
|
+
const id = validMatch[1];
|
|
1238
|
+
const priority = validMatch[2];
|
|
1239
|
+
const text = validMatch[3];
|
|
1240
|
+
if (!id || !priority || !text) continue;
|
|
1241
|
+
brs.push({
|
|
1242
|
+
id,
|
|
1243
|
+
priority,
|
|
1244
|
+
text: text.trim(),
|
|
1245
|
+
line: lineNumber
|
|
1246
|
+
});
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1250
|
+
if (anyPriorityMatch) {
|
|
1251
|
+
const id = anyPriorityMatch[1];
|
|
1252
|
+
const priority = anyPriorityMatch[2];
|
|
1253
|
+
const text = anyPriorityMatch[3];
|
|
1254
|
+
if (!id || !priority || !text) continue;
|
|
1255
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
1256
|
+
brsWithInvalidPriority.push({
|
|
1257
|
+
id,
|
|
1258
|
+
priority,
|
|
1259
|
+
text: text.trim(),
|
|
1260
|
+
line: lineNumber
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1266
|
+
if (noPriorityMatch) {
|
|
1267
|
+
const id = noPriorityMatch[1];
|
|
1268
|
+
const text = noPriorityMatch[2];
|
|
1269
|
+
if (!id || !text) continue;
|
|
1270
|
+
brsWithoutPriority.push({
|
|
1271
|
+
id,
|
|
1272
|
+
text: text.trim(),
|
|
1273
|
+
line: lineNumber
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
const parsed = {
|
|
1278
|
+
file,
|
|
1279
|
+
sections: sectionNames,
|
|
1280
|
+
brs,
|
|
1281
|
+
brsWithoutPriority,
|
|
1282
|
+
brsWithInvalidPriority
|
|
1283
|
+
};
|
|
1284
|
+
if (specId) {
|
|
1285
|
+
parsed.specId = specId;
|
|
1286
|
+
}
|
|
1287
|
+
return parsed;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1048
1290
|
// src/core/validators/ids.ts
|
|
1291
|
+
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1049
1292
|
async function validateDefinedIds(root, config) {
|
|
1050
1293
|
const issues = [];
|
|
1051
|
-
const
|
|
1052
|
-
const
|
|
1053
|
-
const
|
|
1054
|
-
const scenarioFiles = await collectFiles(scenarioRoot, {
|
|
1055
|
-
extensions: [".feature"]
|
|
1056
|
-
});
|
|
1294
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1295
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
1296
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1057
1297
|
const defined = /* @__PURE__ */ new Map();
|
|
1058
1298
|
await collectSpecDefinitionIds(specFiles, defined);
|
|
1059
1299
|
await collectScenarioDefinitionIds(scenarioFiles, defined);
|
|
@@ -1069,7 +1309,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1069
1309
|
}
|
|
1070
1310
|
const sorted = Array.from(files).sort();
|
|
1071
1311
|
issues.push(
|
|
1072
|
-
|
|
1312
|
+
issue3(
|
|
1073
1313
|
"QFAI-ID-001",
|
|
1074
1314
|
`ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
|
|
1075
1315
|
"error",
|
|
@@ -1082,15 +1322,25 @@ async function validateDefinedIds(root, config) {
|
|
|
1082
1322
|
}
|
|
1083
1323
|
async function collectSpecDefinitionIds(files, out) {
|
|
1084
1324
|
for (const file of files) {
|
|
1085
|
-
const text = await
|
|
1086
|
-
|
|
1087
|
-
|
|
1325
|
+
const text = await readFile6(file, "utf-8");
|
|
1326
|
+
const parsed = parseSpec(text, file);
|
|
1327
|
+
if (parsed.specId) {
|
|
1328
|
+
recordId(out, parsed.specId, file);
|
|
1329
|
+
}
|
|
1330
|
+
parsed.brs.forEach((br) => recordId(out, br.id, file));
|
|
1088
1331
|
}
|
|
1089
1332
|
}
|
|
1090
1333
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1091
1334
|
for (const file of files) {
|
|
1092
|
-
const text = await
|
|
1093
|
-
|
|
1335
|
+
const text = await readFile6(file, "utf-8");
|
|
1336
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1337
|
+
for (const scenario of parsed.scenarios) {
|
|
1338
|
+
for (const tag of scenario.tags) {
|
|
1339
|
+
if (SC_TAG_RE.test(tag)) {
|
|
1340
|
+
recordId(out, tag, file);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1094
1344
|
}
|
|
1095
1345
|
}
|
|
1096
1346
|
function recordId(out, id, file) {
|
|
@@ -1100,58 +1350,60 @@ function recordId(out, id, file) {
|
|
|
1100
1350
|
}
|
|
1101
1351
|
function formatFileList(files, root) {
|
|
1102
1352
|
return files.map((file) => {
|
|
1103
|
-
const relative =
|
|
1353
|
+
const relative = path12.relative(root, file);
|
|
1104
1354
|
return relative.length > 0 ? relative : file;
|
|
1105
1355
|
}).join(", ");
|
|
1106
1356
|
}
|
|
1107
|
-
function
|
|
1108
|
-
const
|
|
1357
|
+
function issue3(code, message, severity, file, rule, refs) {
|
|
1358
|
+
const issue7 = {
|
|
1109
1359
|
code,
|
|
1110
1360
|
severity,
|
|
1111
1361
|
message
|
|
1112
1362
|
};
|
|
1113
1363
|
if (file) {
|
|
1114
|
-
|
|
1364
|
+
issue7.file = file;
|
|
1115
1365
|
}
|
|
1116
1366
|
if (rule) {
|
|
1117
|
-
|
|
1367
|
+
issue7.rule = rule;
|
|
1118
1368
|
}
|
|
1119
1369
|
if (refs && refs.length > 0) {
|
|
1120
|
-
|
|
1370
|
+
issue7.refs = refs;
|
|
1121
1371
|
}
|
|
1122
|
-
return
|
|
1372
|
+
return issue7;
|
|
1123
1373
|
}
|
|
1124
1374
|
|
|
1125
1375
|
// src/core/validators/scenario.ts
|
|
1126
|
-
import { readFile as
|
|
1376
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1127
1377
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1128
1378
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1129
1379
|
var THEN_PATTERN = /\bThen\b/;
|
|
1380
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1381
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1382
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1130
1383
|
async function validateScenarios(root, config) {
|
|
1131
|
-
const
|
|
1132
|
-
const files = await
|
|
1133
|
-
extensions: [".feature"]
|
|
1134
|
-
});
|
|
1384
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1385
|
+
const files = await collectScenarioFiles(specsRoot);
|
|
1135
1386
|
if (files.length === 0) {
|
|
1136
1387
|
return [
|
|
1137
|
-
|
|
1388
|
+
issue4(
|
|
1138
1389
|
"QFAI-SC-000",
|
|
1139
1390
|
"Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1140
1391
|
"info",
|
|
1141
|
-
|
|
1392
|
+
specsRoot,
|
|
1142
1393
|
"scenario.files"
|
|
1143
1394
|
)
|
|
1144
1395
|
];
|
|
1145
1396
|
}
|
|
1146
1397
|
const issues = [];
|
|
1147
1398
|
for (const file of files) {
|
|
1148
|
-
const text = await
|
|
1399
|
+
const text = await readFile7(file, "utf-8");
|
|
1149
1400
|
issues.push(...validateScenarioContent(text, file));
|
|
1150
1401
|
}
|
|
1151
1402
|
return issues;
|
|
1152
1403
|
}
|
|
1153
1404
|
function validateScenarioContent(text, file) {
|
|
1154
1405
|
const issues = [];
|
|
1406
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1155
1407
|
const invalidIds = extractInvalidIds(text, [
|
|
1156
1408
|
"SPEC",
|
|
1157
1409
|
"BR",
|
|
@@ -1163,7 +1415,7 @@ function validateScenarioContent(text, file) {
|
|
|
1163
1415
|
]);
|
|
1164
1416
|
if (invalidIds.length > 0) {
|
|
1165
1417
|
issues.push(
|
|
1166
|
-
|
|
1418
|
+
issue4(
|
|
1167
1419
|
"QFAI-ID-002",
|
|
1168
1420
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
1169
1421
|
"error",
|
|
@@ -1173,94 +1425,114 @@ function validateScenarioContent(text, file) {
|
|
|
1173
1425
|
)
|
|
1174
1426
|
);
|
|
1175
1427
|
}
|
|
1176
|
-
const
|
|
1177
|
-
if (
|
|
1428
|
+
const missingStructure = [];
|
|
1429
|
+
if (!parsed.featurePresent) missingStructure.push("Feature");
|
|
1430
|
+
if (parsed.scenarios.length === 0) missingStructure.push("Scenario");
|
|
1431
|
+
if (missingStructure.length > 0) {
|
|
1178
1432
|
issues.push(
|
|
1179
|
-
|
|
1180
|
-
"QFAI-SC-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
"scenario.id"
|
|
1185
|
-
)
|
|
1186
|
-
);
|
|
1187
|
-
}
|
|
1188
|
-
const specIds = extractIds(text, "SPEC");
|
|
1189
|
-
if (specIds.length === 0) {
|
|
1190
|
-
issues.push(
|
|
1191
|
-
issue3(
|
|
1192
|
-
"QFAI-SC-002",
|
|
1193
|
-
"SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
1194
|
-
"error",
|
|
1195
|
-
file,
|
|
1196
|
-
"scenario.spec"
|
|
1197
|
-
)
|
|
1198
|
-
);
|
|
1199
|
-
}
|
|
1200
|
-
const brIds = extractIds(text, "BR");
|
|
1201
|
-
if (brIds.length === 0) {
|
|
1202
|
-
issues.push(
|
|
1203
|
-
issue3(
|
|
1204
|
-
"QFAI-SC-003",
|
|
1205
|
-
"SC \u306F BR \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
1433
|
+
issue4(
|
|
1434
|
+
"QFAI-SC-006",
|
|
1435
|
+
`Scenario \u30D5\u30A1\u30A4\u30EB\u306B\u5FC5\u8981\u306A\u69CB\u9020\u304C\u3042\u308A\u307E\u305B\u3093: ${missingStructure.join(
|
|
1436
|
+
", "
|
|
1437
|
+
)}`,
|
|
1206
1438
|
"error",
|
|
1207
1439
|
file,
|
|
1208
|
-
"scenario.
|
|
1440
|
+
"scenario.structure"
|
|
1209
1441
|
)
|
|
1210
1442
|
);
|
|
1211
1443
|
}
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1444
|
+
for (const scenario of parsed.scenarios) {
|
|
1445
|
+
if (scenario.tags.length === 0) {
|
|
1446
|
+
issues.push(
|
|
1447
|
+
issue4(
|
|
1448
|
+
"QFAI-SC-007",
|
|
1449
|
+
`Scenario \u30BF\u30B0\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${scenario.name}`,
|
|
1450
|
+
"error",
|
|
1451
|
+
file,
|
|
1452
|
+
"scenario.tags"
|
|
1453
|
+
)
|
|
1454
|
+
);
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
const missingTags = [];
|
|
1458
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
|
|
1459
|
+
if (scTags.length === 0) {
|
|
1460
|
+
missingTags.push("SC(0\u4EF6)");
|
|
1461
|
+
} else if (scTags.length > 1) {
|
|
1462
|
+
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1463
|
+
}
|
|
1464
|
+
if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
|
|
1465
|
+
missingTags.push("SPEC");
|
|
1466
|
+
}
|
|
1467
|
+
if (!scenario.tags.some((tag) => BR_TAG_RE.test(tag))) {
|
|
1468
|
+
missingTags.push("BR");
|
|
1469
|
+
}
|
|
1470
|
+
if (missingTags.length > 0) {
|
|
1471
|
+
issues.push(
|
|
1472
|
+
issue4(
|
|
1473
|
+
"QFAI-SC-008",
|
|
1474
|
+
`Scenario \u30BF\u30B0\u306B\u4E0D\u8DB3\u304C\u3042\u308A\u307E\u3059: ${missingTags.join(", ")} (${scenario.name})`,
|
|
1475
|
+
"error",
|
|
1476
|
+
file,
|
|
1477
|
+
"scenario.tagIds"
|
|
1478
|
+
)
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1221
1481
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1482
|
+
for (const scenario of parsed.scenarios) {
|
|
1483
|
+
const missingSteps = [];
|
|
1484
|
+
if (!GIVEN_PATTERN.test(scenario.body)) {
|
|
1485
|
+
missingSteps.push("Given");
|
|
1486
|
+
}
|
|
1487
|
+
if (!WHEN_PATTERN.test(scenario.body)) {
|
|
1488
|
+
missingSteps.push("When");
|
|
1489
|
+
}
|
|
1490
|
+
if (!THEN_PATTERN.test(scenario.body)) {
|
|
1491
|
+
missingSteps.push("Then");
|
|
1492
|
+
}
|
|
1493
|
+
if (missingSteps.length > 0) {
|
|
1494
|
+
issues.push(
|
|
1495
|
+
issue4(
|
|
1496
|
+
"QFAI-SC-005",
|
|
1497
|
+
`Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")} (${scenario.name})`,
|
|
1498
|
+
"warning",
|
|
1499
|
+
file,
|
|
1500
|
+
"scenario.steps"
|
|
1501
|
+
)
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1232
1504
|
}
|
|
1233
1505
|
return issues;
|
|
1234
1506
|
}
|
|
1235
|
-
function
|
|
1236
|
-
const
|
|
1507
|
+
function issue4(code, message, severity, file, rule, refs) {
|
|
1508
|
+
const issue7 = {
|
|
1237
1509
|
code,
|
|
1238
1510
|
severity,
|
|
1239
1511
|
message
|
|
1240
1512
|
};
|
|
1241
1513
|
if (file) {
|
|
1242
|
-
|
|
1514
|
+
issue7.file = file;
|
|
1243
1515
|
}
|
|
1244
1516
|
if (rule) {
|
|
1245
|
-
|
|
1517
|
+
issue7.rule = rule;
|
|
1246
1518
|
}
|
|
1247
1519
|
if (refs && refs.length > 0) {
|
|
1248
|
-
|
|
1520
|
+
issue7.refs = refs;
|
|
1249
1521
|
}
|
|
1250
|
-
return
|
|
1522
|
+
return issue7;
|
|
1251
1523
|
}
|
|
1252
1524
|
|
|
1253
1525
|
// src/core/validators/spec.ts
|
|
1254
|
-
import { readFile as
|
|
1526
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1255
1527
|
async function validateSpecs(root, config) {
|
|
1256
|
-
const specsRoot = resolvePath(root, config, "
|
|
1528
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1257
1529
|
const files = await collectSpecFiles(specsRoot);
|
|
1258
1530
|
if (files.length === 0) {
|
|
1259
|
-
const expected = "spec-
|
|
1531
|
+
const expected = "spec-001/spec.md";
|
|
1260
1532
|
return [
|
|
1261
|
-
|
|
1533
|
+
issue5(
|
|
1262
1534
|
"QFAI-SPEC-000",
|
|
1263
|
-
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.
|
|
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}`,
|
|
1264
1536
|
"info",
|
|
1265
1537
|
specsRoot,
|
|
1266
1538
|
"spec.files"
|
|
@@ -1269,7 +1541,7 @@ async function validateSpecs(root, config) {
|
|
|
1269
1541
|
}
|
|
1270
1542
|
const issues = [];
|
|
1271
1543
|
for (const file of files) {
|
|
1272
|
-
const text = await
|
|
1544
|
+
const text = await readFile8(file, "utf-8");
|
|
1273
1545
|
issues.push(
|
|
1274
1546
|
...validateSpecContent(
|
|
1275
1547
|
text,
|
|
@@ -1282,6 +1554,7 @@ async function validateSpecs(root, config) {
|
|
|
1282
1554
|
}
|
|
1283
1555
|
function validateSpecContent(text, file, requiredSections) {
|
|
1284
1556
|
const issues = [];
|
|
1557
|
+
const parsed = parseSpec(text, file);
|
|
1285
1558
|
const invalidIds = extractInvalidIds(text, [
|
|
1286
1559
|
"SPEC",
|
|
1287
1560
|
"BR",
|
|
@@ -1293,7 +1566,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1293
1566
|
]);
|
|
1294
1567
|
if (invalidIds.length > 0) {
|
|
1295
1568
|
issues.push(
|
|
1296
|
-
|
|
1569
|
+
issue5(
|
|
1297
1570
|
"QFAI-ID-002",
|
|
1298
1571
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
1299
1572
|
"error",
|
|
@@ -1303,10 +1576,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1303
1576
|
)
|
|
1304
1577
|
);
|
|
1305
1578
|
}
|
|
1306
|
-
|
|
1307
|
-
if (specIds.length === 0) {
|
|
1579
|
+
if (!parsed.specId) {
|
|
1308
1580
|
issues.push(
|
|
1309
|
-
|
|
1581
|
+
issue5(
|
|
1310
1582
|
"QFAI-SPEC-001",
|
|
1311
1583
|
"SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1312
1584
|
"error",
|
|
@@ -1315,10 +1587,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1315
1587
|
)
|
|
1316
1588
|
);
|
|
1317
1589
|
}
|
|
1318
|
-
|
|
1319
|
-
if (brIds.length === 0) {
|
|
1590
|
+
if (parsed.brs.length === 0) {
|
|
1320
1591
|
issues.push(
|
|
1321
|
-
|
|
1592
|
+
issue5(
|
|
1322
1593
|
"QFAI-SPEC-002",
|
|
1323
1594
|
"BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1324
1595
|
"error",
|
|
@@ -1327,10 +1598,34 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1327
1598
|
)
|
|
1328
1599
|
);
|
|
1329
1600
|
}
|
|
1601
|
+
for (const br of parsed.brsWithoutPriority) {
|
|
1602
|
+
issues.push(
|
|
1603
|
+
issue5(
|
|
1604
|
+
"QFAI-BR-001",
|
|
1605
|
+
`BR \u884C\u306B Priority \u304C\u3042\u308A\u307E\u305B\u3093: ${br.id}`,
|
|
1606
|
+
"error",
|
|
1607
|
+
file,
|
|
1608
|
+
"spec.brPriority",
|
|
1609
|
+
[br.id]
|
|
1610
|
+
)
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
for (const br of parsed.brsWithInvalidPriority) {
|
|
1614
|
+
issues.push(
|
|
1615
|
+
issue5(
|
|
1616
|
+
"QFAI-BR-002",
|
|
1617
|
+
`BR Priority \u304C\u4E0D\u6B63\u3067\u3059: ${br.id} (${br.priority})`,
|
|
1618
|
+
"error",
|
|
1619
|
+
file,
|
|
1620
|
+
"spec.brPriority",
|
|
1621
|
+
[br.id]
|
|
1622
|
+
)
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1330
1625
|
const scIds = extractIds(text, "SC");
|
|
1331
1626
|
if (scIds.length > 0) {
|
|
1332
1627
|
issues.push(
|
|
1333
|
-
|
|
1628
|
+
issue5(
|
|
1334
1629
|
"QFAI-SPEC-003",
|
|
1335
1630
|
"Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
|
|
1336
1631
|
"warning",
|
|
@@ -1341,9 +1636,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1341
1636
|
);
|
|
1342
1637
|
}
|
|
1343
1638
|
for (const section of requiredSections) {
|
|
1344
|
-
if (!
|
|
1639
|
+
if (!parsed.sections.has(section)) {
|
|
1345
1640
|
issues.push(
|
|
1346
|
-
|
|
1641
|
+
issue5(
|
|
1347
1642
|
"QFAI-SPEC-004",
|
|
1348
1643
|
`\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
|
|
1349
1644
|
"error",
|
|
@@ -1355,40 +1650,39 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1355
1650
|
}
|
|
1356
1651
|
return issues;
|
|
1357
1652
|
}
|
|
1358
|
-
function
|
|
1359
|
-
const
|
|
1653
|
+
function issue5(code, message, severity, file, rule, refs) {
|
|
1654
|
+
const issue7 = {
|
|
1360
1655
|
code,
|
|
1361
1656
|
severity,
|
|
1362
1657
|
message
|
|
1363
1658
|
};
|
|
1364
1659
|
if (file) {
|
|
1365
|
-
|
|
1660
|
+
issue7.file = file;
|
|
1366
1661
|
}
|
|
1367
1662
|
if (rule) {
|
|
1368
|
-
|
|
1663
|
+
issue7.rule = rule;
|
|
1369
1664
|
}
|
|
1370
1665
|
if (refs && refs.length > 0) {
|
|
1371
|
-
|
|
1666
|
+
issue7.refs = refs;
|
|
1372
1667
|
}
|
|
1373
|
-
return
|
|
1668
|
+
return issue7;
|
|
1374
1669
|
}
|
|
1375
1670
|
|
|
1376
1671
|
// src/core/validators/traceability.ts
|
|
1377
|
-
import { readFile as
|
|
1672
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1673
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1674
|
+
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1675
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1676
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1677
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
1678
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1378
1679
|
async function validateTraceability(root, config) {
|
|
1379
1680
|
const issues = [];
|
|
1380
|
-
const specsRoot = resolvePath(root, config, "
|
|
1381
|
-
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1382
|
-
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1681
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1383
1682
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1384
1683
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1385
1684
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
1386
|
-
const
|
|
1387
|
-
extensions: [".md"]
|
|
1388
|
-
});
|
|
1389
|
-
const scenarioFiles = await collectFiles(scenariosRoot, {
|
|
1390
|
-
extensions: [".feature"]
|
|
1391
|
-
});
|
|
1685
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1392
1686
|
const upstreamIds = /* @__PURE__ */ new Set();
|
|
1393
1687
|
const specIds = /* @__PURE__ */ new Set();
|
|
1394
1688
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
@@ -1400,11 +1694,13 @@ async function validateTraceability(root, config) {
|
|
|
1400
1694
|
const contractIndex = await buildContractIndex(root, config);
|
|
1401
1695
|
const contractIds = contractIndex.ids;
|
|
1402
1696
|
for (const file of specFiles) {
|
|
1403
|
-
const text = await
|
|
1697
|
+
const text = await readFile9(file, "utf-8");
|
|
1404
1698
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1405
|
-
const
|
|
1406
|
-
|
|
1407
|
-
|
|
1699
|
+
const parsed = parseSpec(text, file);
|
|
1700
|
+
if (parsed.specId) {
|
|
1701
|
+
specIds.add(parsed.specId);
|
|
1702
|
+
}
|
|
1703
|
+
const brIds = parsed.brs.map((br) => br.id);
|
|
1408
1704
|
brIds.forEach((id) => brIdsInSpecs.add(id));
|
|
1409
1705
|
const referencedContractIds = /* @__PURE__ */ new Set([
|
|
1410
1706
|
...extractIds(text, "UI"),
|
|
@@ -1416,7 +1712,7 @@ async function validateTraceability(root, config) {
|
|
|
1416
1712
|
);
|
|
1417
1713
|
if (unknownContractIds.length > 0) {
|
|
1418
1714
|
issues.push(
|
|
1419
|
-
|
|
1715
|
+
issue6(
|
|
1420
1716
|
"QFAI-TRACE-009",
|
|
1421
1717
|
`Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1422
1718
|
", "
|
|
@@ -1428,37 +1724,50 @@ async function validateTraceability(root, config) {
|
|
|
1428
1724
|
)
|
|
1429
1725
|
);
|
|
1430
1726
|
}
|
|
1431
|
-
|
|
1432
|
-
const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
|
|
1727
|
+
if (parsed.specId) {
|
|
1728
|
+
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
1433
1729
|
brIds.forEach((id) => current.add(id));
|
|
1434
|
-
specToBrIds.set(specId, current);
|
|
1730
|
+
specToBrIds.set(parsed.specId, current);
|
|
1435
1731
|
}
|
|
1436
1732
|
}
|
|
1437
|
-
for (const file of decisionFiles) {
|
|
1438
|
-
const text = await readFile8(file, "utf-8");
|
|
1439
|
-
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1440
|
-
}
|
|
1441
1733
|
for (const file of scenarioFiles) {
|
|
1442
|
-
const text = await
|
|
1734
|
+
const text = await readFile9(file, "utf-8");
|
|
1443
1735
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1444
|
-
const
|
|
1445
|
-
const
|
|
1446
|
-
const
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1736
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1737
|
+
const specIdsInScenario = /* @__PURE__ */ new Set();
|
|
1738
|
+
const brIds = /* @__PURE__ */ new Set();
|
|
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));
|
|
1457
1766
|
}
|
|
1458
|
-
const unknownSpecIds =
|
|
1767
|
+
const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
|
|
1459
1768
|
if (unknownSpecIds.length > 0) {
|
|
1460
1769
|
issues.push(
|
|
1461
|
-
|
|
1770
|
+
issue6(
|
|
1462
1771
|
"QFAI-TRACE-005",
|
|
1463
1772
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1464
1773
|
"error",
|
|
@@ -1468,10 +1777,10 @@ async function validateTraceability(root, config) {
|
|
|
1468
1777
|
)
|
|
1469
1778
|
);
|
|
1470
1779
|
}
|
|
1471
|
-
const unknownBrIds =
|
|
1780
|
+
const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
|
|
1472
1781
|
if (unknownBrIds.length > 0) {
|
|
1473
1782
|
issues.push(
|
|
1474
|
-
|
|
1783
|
+
issue6(
|
|
1475
1784
|
"QFAI-TRACE-006",
|
|
1476
1785
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1477
1786
|
"error",
|
|
@@ -1481,10 +1790,12 @@ async function validateTraceability(root, config) {
|
|
|
1481
1790
|
)
|
|
1482
1791
|
);
|
|
1483
1792
|
}
|
|
1484
|
-
const unknownContractIds =
|
|
1793
|
+
const unknownContractIds = scenarioIdsList.filter(
|
|
1794
|
+
(id) => !contractIds.has(id)
|
|
1795
|
+
);
|
|
1485
1796
|
if (unknownContractIds.length > 0) {
|
|
1486
1797
|
issues.push(
|
|
1487
|
-
|
|
1798
|
+
issue6(
|
|
1488
1799
|
"QFAI-TRACE-008",
|
|
1489
1800
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1490
1801
|
", "
|
|
@@ -1496,23 +1807,23 @@ async function validateTraceability(root, config) {
|
|
|
1496
1807
|
)
|
|
1497
1808
|
);
|
|
1498
1809
|
}
|
|
1499
|
-
if (
|
|
1810
|
+
if (specIdsList.length > 0) {
|
|
1500
1811
|
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
1501
|
-
for (const specId of
|
|
1812
|
+
for (const specId of specIdsList) {
|
|
1502
1813
|
const brIdsForSpec = specToBrIds.get(specId);
|
|
1503
1814
|
if (!brIdsForSpec) {
|
|
1504
1815
|
continue;
|
|
1505
1816
|
}
|
|
1506
1817
|
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
1507
1818
|
}
|
|
1508
|
-
const invalidBrIds =
|
|
1819
|
+
const invalidBrIds = brIdsList.filter((id) => !allowedBrIds.has(id));
|
|
1509
1820
|
if (invalidBrIds.length > 0) {
|
|
1510
1821
|
issues.push(
|
|
1511
|
-
|
|
1822
|
+
issue6(
|
|
1512
1823
|
"QFAI-TRACE-007",
|
|
1513
1824
|
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
1514
1825
|
", "
|
|
1515
|
-
)} (SPEC: ${
|
|
1826
|
+
)} (SPEC: ${specIdsList.join(", ")})`,
|
|
1516
1827
|
"error",
|
|
1517
1828
|
file,
|
|
1518
1829
|
"traceability.scenarioBrUnderSpec",
|
|
@@ -1524,7 +1835,7 @@ async function validateTraceability(root, config) {
|
|
|
1524
1835
|
}
|
|
1525
1836
|
if (upstreamIds.size === 0) {
|
|
1526
1837
|
return [
|
|
1527
|
-
|
|
1838
|
+
issue6(
|
|
1528
1839
|
"QFAI-TRACE-000",
|
|
1529
1840
|
"\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1530
1841
|
"info",
|
|
@@ -1539,7 +1850,7 @@ async function validateTraceability(root, config) {
|
|
|
1539
1850
|
);
|
|
1540
1851
|
if (orphanBrIds.length > 0) {
|
|
1541
1852
|
issues.push(
|
|
1542
|
-
|
|
1853
|
+
issue6(
|
|
1543
1854
|
"QFAI_TRACE_BR_ORPHAN",
|
|
1544
1855
|
`BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
|
|
1545
1856
|
"error",
|
|
@@ -1556,13 +1867,13 @@ async function validateTraceability(root, config) {
|
|
|
1556
1867
|
);
|
|
1557
1868
|
if (scWithoutContracts.length > 0) {
|
|
1558
1869
|
issues.push(
|
|
1559
|
-
|
|
1870
|
+
issue6(
|
|
1560
1871
|
"QFAI_TRACE_SC_NO_CONTRACT",
|
|
1561
1872
|
`SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
|
|
1562
1873
|
", "
|
|
1563
1874
|
)}`,
|
|
1564
1875
|
"error",
|
|
1565
|
-
|
|
1876
|
+
specsRoot,
|
|
1566
1877
|
"traceability.scMustTouchContracts",
|
|
1567
1878
|
scWithoutContracts
|
|
1568
1879
|
)
|
|
@@ -1576,11 +1887,11 @@ async function validateTraceability(root, config) {
|
|
|
1576
1887
|
);
|
|
1577
1888
|
if (orphanContracts.length > 0) {
|
|
1578
1889
|
issues.push(
|
|
1579
|
-
|
|
1890
|
+
issue6(
|
|
1580
1891
|
"QFAI_CONTRACT_ORPHAN",
|
|
1581
1892
|
`\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
1582
1893
|
"error",
|
|
1583
|
-
|
|
1894
|
+
specsRoot,
|
|
1584
1895
|
"traceability.allowOrphanContracts",
|
|
1585
1896
|
orphanContracts
|
|
1586
1897
|
)
|
|
@@ -1604,7 +1915,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1604
1915
|
const targetFiles = [...codeFiles, ...testFiles];
|
|
1605
1916
|
if (targetFiles.length === 0) {
|
|
1606
1917
|
issues.push(
|
|
1607
|
-
|
|
1918
|
+
issue6(
|
|
1608
1919
|
"QFAI-TRACE-001",
|
|
1609
1920
|
"\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1610
1921
|
"info",
|
|
@@ -1617,7 +1928,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1617
1928
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
1618
1929
|
let found = false;
|
|
1619
1930
|
for (const file of targetFiles) {
|
|
1620
|
-
const text = await
|
|
1931
|
+
const text = await readFile9(file, "utf-8");
|
|
1621
1932
|
if (pattern.test(text)) {
|
|
1622
1933
|
found = true;
|
|
1623
1934
|
break;
|
|
@@ -1625,7 +1936,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1625
1936
|
}
|
|
1626
1937
|
if (!found) {
|
|
1627
1938
|
issues.push(
|
|
1628
|
-
|
|
1939
|
+
issue6(
|
|
1629
1940
|
"QFAI-TRACE-002",
|
|
1630
1941
|
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
1631
1942
|
"warning",
|
|
@@ -1640,22 +1951,22 @@ function buildIdPattern(ids) {
|
|
|
1640
1951
|
const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
1641
1952
|
return new RegExp(`\\b(${escaped.join("|")})\\b`);
|
|
1642
1953
|
}
|
|
1643
|
-
function
|
|
1644
|
-
const
|
|
1954
|
+
function issue6(code, message, severity, file, rule, refs) {
|
|
1955
|
+
const issue7 = {
|
|
1645
1956
|
code,
|
|
1646
1957
|
severity,
|
|
1647
1958
|
message
|
|
1648
1959
|
};
|
|
1649
1960
|
if (file) {
|
|
1650
|
-
|
|
1961
|
+
issue7.file = file;
|
|
1651
1962
|
}
|
|
1652
1963
|
if (rule) {
|
|
1653
|
-
|
|
1964
|
+
issue7.rule = rule;
|
|
1654
1965
|
}
|
|
1655
1966
|
if (refs && refs.length > 0) {
|
|
1656
|
-
|
|
1967
|
+
issue7.refs = refs;
|
|
1657
1968
|
}
|
|
1658
|
-
return
|
|
1969
|
+
return issue7;
|
|
1659
1970
|
}
|
|
1660
1971
|
|
|
1661
1972
|
// src/core/validate.ts
|
|
@@ -1665,6 +1976,7 @@ async function validateProject(root, configResult) {
|
|
|
1665
1976
|
const issues = [
|
|
1666
1977
|
...configIssues,
|
|
1667
1978
|
...await validateSpecs(root, config),
|
|
1979
|
+
...await validateDeltas(root, config),
|
|
1668
1980
|
...await validateScenarios(root, config),
|
|
1669
1981
|
...await validateContracts(root, config),
|
|
1670
1982
|
...await validateDefinedIds(root, config),
|
|
@@ -1680,8 +1992,8 @@ async function validateProject(root, configResult) {
|
|
|
1680
1992
|
}
|
|
1681
1993
|
function countIssues(issues) {
|
|
1682
1994
|
return issues.reduce(
|
|
1683
|
-
(acc,
|
|
1684
|
-
acc[
|
|
1995
|
+
(acc, issue7) => {
|
|
1996
|
+
acc[issue7.severity] += 1;
|
|
1685
1997
|
return acc;
|
|
1686
1998
|
},
|
|
1687
1999
|
{ info: 0, warning: 0, error: 0 }
|
|
@@ -1694,21 +2006,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
1694
2006
|
const resolved = configResult ?? await loadConfig(root);
|
|
1695
2007
|
const config = resolved.config;
|
|
1696
2008
|
const configPath = resolved.configPath;
|
|
1697
|
-
const
|
|
1698
|
-
const
|
|
1699
|
-
const
|
|
1700
|
-
const
|
|
1701
|
-
const
|
|
1702
|
-
const dbRoot = resolvePath(root, config, "dataContractsDir");
|
|
2009
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2010
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2011
|
+
const apiRoot = path13.join(contractsRoot, "api");
|
|
2012
|
+
const uiRoot = path13.join(contractsRoot, "ui");
|
|
2013
|
+
const dbRoot = path13.join(contractsRoot, "db");
|
|
1703
2014
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1704
2015
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1705
|
-
const specFiles = await collectSpecFiles(
|
|
1706
|
-
const scenarioFiles = await
|
|
1707
|
-
extensions: [".feature"]
|
|
1708
|
-
});
|
|
1709
|
-
const decisionFiles = await collectFiles(decisionsRoot, {
|
|
1710
|
-
extensions: [".md"]
|
|
1711
|
-
});
|
|
2016
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
2017
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1712
2018
|
const {
|
|
1713
2019
|
api: apiFiles,
|
|
1714
2020
|
ui: uiFiles,
|
|
@@ -1717,7 +2023,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1717
2023
|
const idsByPrefix = await collectIds([
|
|
1718
2024
|
...specFiles,
|
|
1719
2025
|
...scenarioFiles,
|
|
1720
|
-
...decisionFiles,
|
|
1721
2026
|
...apiFiles,
|
|
1722
2027
|
...uiFiles,
|
|
1723
2028
|
...dbFiles
|
|
@@ -1742,7 +2047,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1742
2047
|
summary: {
|
|
1743
2048
|
specs: specFiles.length,
|
|
1744
2049
|
scenarios: scenarioFiles.length,
|
|
1745
|
-
decisions: decisionFiles.length,
|
|
1746
2050
|
contracts: {
|
|
1747
2051
|
api: apiFiles.length,
|
|
1748
2052
|
ui: uiFiles.length,
|
|
@@ -1776,7 +2080,6 @@ function formatReportMarkdown(data) {
|
|
|
1776
2080
|
lines.push("## \u6982\u8981");
|
|
1777
2081
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
1778
2082
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
1779
|
-
lines.push(`- decisions: ${data.summary.decisions}`);
|
|
1780
2083
|
lines.push(
|
|
1781
2084
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
1782
2085
|
);
|
|
@@ -1852,7 +2155,7 @@ async function collectIds(files) {
|
|
|
1852
2155
|
DATA: /* @__PURE__ */ new Set()
|
|
1853
2156
|
};
|
|
1854
2157
|
for (const file of files) {
|
|
1855
|
-
const text = await
|
|
2158
|
+
const text = await readFile10(file, "utf-8");
|
|
1856
2159
|
for (const prefix of ID_PREFIXES2) {
|
|
1857
2160
|
const ids = extractIds(text, prefix);
|
|
1858
2161
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -1870,7 +2173,7 @@ async function collectIds(files) {
|
|
|
1870
2173
|
async function collectUpstreamIds(files) {
|
|
1871
2174
|
const ids = /* @__PURE__ */ new Set();
|
|
1872
2175
|
for (const file of files) {
|
|
1873
|
-
const text = await
|
|
2176
|
+
const text = await readFile10(file, "utf-8");
|
|
1874
2177
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
1875
2178
|
}
|
|
1876
2179
|
return ids;
|
|
@@ -1891,7 +2194,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
1891
2194
|
}
|
|
1892
2195
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
1893
2196
|
for (const file of targetFiles) {
|
|
1894
|
-
const text = await
|
|
2197
|
+
const text = await readFile10(file, "utf-8");
|
|
1895
2198
|
if (pattern.test(text)) {
|
|
1896
2199
|
return true;
|
|
1897
2200
|
}
|
|
@@ -1913,20 +2216,20 @@ function toSortedArray(values) {
|
|
|
1913
2216
|
}
|
|
1914
2217
|
function buildHotspots(issues) {
|
|
1915
2218
|
const map = /* @__PURE__ */ new Map();
|
|
1916
|
-
for (const
|
|
1917
|
-
if (!
|
|
2219
|
+
for (const issue7 of issues) {
|
|
2220
|
+
if (!issue7.file) {
|
|
1918
2221
|
continue;
|
|
1919
2222
|
}
|
|
1920
|
-
const current = map.get(
|
|
1921
|
-
file:
|
|
2223
|
+
const current = map.get(issue7.file) ?? {
|
|
2224
|
+
file: issue7.file,
|
|
1922
2225
|
total: 0,
|
|
1923
2226
|
error: 0,
|
|
1924
2227
|
warning: 0,
|
|
1925
2228
|
info: 0
|
|
1926
2229
|
};
|
|
1927
2230
|
current.total += 1;
|
|
1928
|
-
current[
|
|
1929
|
-
map.set(
|
|
2231
|
+
current[issue7.severity] += 1;
|
|
2232
|
+
map.set(issue7.file, current);
|
|
1930
2233
|
}
|
|
1931
2234
|
return Array.from(map.values()).sort(
|
|
1932
2235
|
(a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
|
|
@@ -1935,21 +2238,22 @@ function buildHotspots(issues) {
|
|
|
1935
2238
|
|
|
1936
2239
|
// src/cli/commands/report.ts
|
|
1937
2240
|
async function runReport(options) {
|
|
1938
|
-
const root =
|
|
2241
|
+
const root = path14.resolve(options.root);
|
|
1939
2242
|
const configResult = await loadConfig(root);
|
|
1940
|
-
const input =
|
|
1941
|
-
const inputPath =
|
|
2243
|
+
const input = configResult.config.output.validateJsonPath;
|
|
2244
|
+
const inputPath = path14.isAbsolute(input) ? input : path14.resolve(root, input);
|
|
1942
2245
|
let validation;
|
|
1943
2246
|
try {
|
|
1944
2247
|
validation = await readValidationResult(inputPath);
|
|
1945
2248
|
} catch (err) {
|
|
1946
|
-
if (
|
|
2249
|
+
if (isMissingFileError2(err)) {
|
|
1947
2250
|
error(
|
|
1948
2251
|
[
|
|
1949
2252
|
`qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
|
|
1950
2253
|
"",
|
|
1951
|
-
"\u307E\u305A validate
|
|
1952
|
-
|
|
2254
|
+
"\u307E\u305A qfai validate \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
|
|
2255
|
+
" qfai validate",
|
|
2256
|
+
"\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/out/validate.json\uFF09",
|
|
1953
2257
|
"",
|
|
1954
2258
|
"GitHub Actions \u30C6\u30F3\u30D7\u30EC\u3092\u4F7F\u3063\u3066\u3044\u308B\u5834\u5408\u306F\u3001workflow \u306E validate \u30B8\u30E7\u30D6\u3092\u5148\u306B\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
1955
2259
|
].join("\n")
|
|
@@ -1961,10 +2265,11 @@ async function runReport(options) {
|
|
|
1961
2265
|
}
|
|
1962
2266
|
const data = await createReportData(root, validation, configResult);
|
|
1963
2267
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
1964
|
-
const
|
|
2268
|
+
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
2269
|
+
const defaultOut = options.format === "json" ? path14.join(outRoot, "report.json") : path14.join(outRoot, "report.md");
|
|
1965
2270
|
const out = options.outPath ?? defaultOut;
|
|
1966
|
-
const outPath =
|
|
1967
|
-
await mkdir2(
|
|
2271
|
+
const outPath = path14.isAbsolute(out) ? out : path14.resolve(root, out);
|
|
2272
|
+
await mkdir2(path14.dirname(outPath), { recursive: true });
|
|
1968
2273
|
await writeFile(outPath, `${output}
|
|
1969
2274
|
`, "utf-8");
|
|
1970
2275
|
info(
|
|
@@ -1973,7 +2278,7 @@ async function runReport(options) {
|
|
|
1973
2278
|
info(`wrote report: ${outPath}`);
|
|
1974
2279
|
}
|
|
1975
2280
|
async function readValidationResult(inputPath) {
|
|
1976
|
-
const raw = await
|
|
2281
|
+
const raw = await readFile11(inputPath, "utf-8");
|
|
1977
2282
|
const parsed = JSON.parse(raw);
|
|
1978
2283
|
if (!isValidationResult(parsed)) {
|
|
1979
2284
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
@@ -2005,7 +2310,7 @@ function isValidationResult(value) {
|
|
|
2005
2310
|
}
|
|
2006
2311
|
return typeof counts.info === "number" && typeof counts.warning === "number" && typeof counts.error === "number";
|
|
2007
2312
|
}
|
|
2008
|
-
function
|
|
2313
|
+
function isMissingFileError2(error2) {
|
|
2009
2314
|
if (!error2 || typeof error2 !== "object") {
|
|
2010
2315
|
return false;
|
|
2011
2316
|
}
|
|
@@ -2015,7 +2320,7 @@ function isMissingFileError(error2) {
|
|
|
2015
2320
|
|
|
2016
2321
|
// src/cli/commands/validate.ts
|
|
2017
2322
|
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
2018
|
-
import
|
|
2323
|
+
import path15 from "path";
|
|
2019
2324
|
|
|
2020
2325
|
// src/cli/lib/failOn.ts
|
|
2021
2326
|
function shouldFail(result, failOn) {
|
|
@@ -2030,24 +2335,17 @@ function shouldFail(result, failOn) {
|
|
|
2030
2335
|
|
|
2031
2336
|
// src/cli/commands/validate.ts
|
|
2032
2337
|
async function runValidate(options) {
|
|
2033
|
-
const root =
|
|
2338
|
+
const root = path15.resolve(options.root);
|
|
2034
2339
|
const configResult = await loadConfig(root);
|
|
2035
2340
|
const result = await validateProject(root, configResult);
|
|
2036
|
-
const format = options.format ??
|
|
2037
|
-
const explicitJsonPath = options.jsonPath;
|
|
2341
|
+
const format = options.format ?? "text";
|
|
2038
2342
|
if (format === "text") {
|
|
2039
2343
|
emitText(result);
|
|
2040
2344
|
}
|
|
2041
2345
|
if (format === "github") {
|
|
2042
2346
|
result.issues.forEach(emitGitHub);
|
|
2043
2347
|
}
|
|
2044
|
-
|
|
2045
|
-
if (shouldWriteJson) {
|
|
2046
|
-
const jsonPath = format === "json" ? options.jsonPath ?? configResult.config.output.jsonPath : explicitJsonPath;
|
|
2047
|
-
if (jsonPath) {
|
|
2048
|
-
await emitJson(result, root, jsonPath);
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2348
|
+
await emitJson(result, root, configResult.config.output.validateJsonPath);
|
|
2051
2349
|
const failOn = resolveFailOn(options, configResult.config.validation.failOn);
|
|
2052
2350
|
return shouldFail(result, failOn) ? 1 : 0;
|
|
2053
2351
|
}
|
|
@@ -2074,20 +2372,20 @@ function emitText(result) {
|
|
|
2074
2372
|
`
|
|
2075
2373
|
);
|
|
2076
2374
|
}
|
|
2077
|
-
function emitGitHub(
|
|
2078
|
-
const level =
|
|
2079
|
-
const file =
|
|
2080
|
-
const line =
|
|
2081
|
-
const column =
|
|
2375
|
+
function emitGitHub(issue7) {
|
|
2376
|
+
const level = issue7.severity === "error" ? "error" : issue7.severity === "warning" ? "warning" : "notice";
|
|
2377
|
+
const file = issue7.file ? `file=${issue7.file}` : "";
|
|
2378
|
+
const line = issue7.loc?.line ? `,line=${issue7.loc.line}` : "";
|
|
2379
|
+
const column = issue7.loc?.column ? `,col=${issue7.loc.column}` : "";
|
|
2082
2380
|
const location = file ? ` ${file}${line}${column}` : "";
|
|
2083
2381
|
process.stdout.write(
|
|
2084
|
-
`::${level}${location}::${
|
|
2382
|
+
`::${level}${location}::${issue7.code}: ${issue7.message}
|
|
2085
2383
|
`
|
|
2086
2384
|
);
|
|
2087
2385
|
}
|
|
2088
2386
|
async function emitJson(result, root, jsonPath) {
|
|
2089
|
-
const abs =
|
|
2090
|
-
await mkdir3(
|
|
2387
|
+
const abs = path15.isAbsolute(jsonPath) ? jsonPath : path15.resolve(root, jsonPath);
|
|
2388
|
+
await mkdir3(path15.dirname(abs), { recursive: true });
|
|
2091
2389
|
await writeFile2(abs, `${JSON.stringify(result, null, 2)}
|
|
2092
2390
|
`, "utf-8");
|
|
2093
2391
|
}
|
|
@@ -2148,15 +2446,6 @@ function parseArgs(argv, cwd) {
|
|
|
2148
2446
|
i += 1;
|
|
2149
2447
|
break;
|
|
2150
2448
|
}
|
|
2151
|
-
case "--json-path":
|
|
2152
|
-
{
|
|
2153
|
-
const next = args[i + 1];
|
|
2154
|
-
if (next) {
|
|
2155
|
-
options.jsonPath = next;
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
i += 1;
|
|
2159
|
-
break;
|
|
2160
2449
|
case "--out":
|
|
2161
2450
|
{
|
|
2162
2451
|
const next = args[i + 1];
|
|
@@ -2187,7 +2476,7 @@ function applyFormatOption(command, value, options) {
|
|
|
2187
2476
|
return;
|
|
2188
2477
|
}
|
|
2189
2478
|
if (command === "validate") {
|
|
2190
|
-
if (value === "text" || value === "
|
|
2479
|
+
if (value === "text" || value === "github") {
|
|
2191
2480
|
options.validateFormat = value;
|
|
2192
2481
|
}
|
|
2193
2482
|
return;
|
|
@@ -2195,7 +2484,7 @@ function applyFormatOption(command, value, options) {
|
|
|
2195
2484
|
if (value === "md" || value === "json") {
|
|
2196
2485
|
options.reportFormat = value;
|
|
2197
2486
|
}
|
|
2198
|
-
if (value === "text" || value === "
|
|
2487
|
+
if (value === "text" || value === "github") {
|
|
2199
2488
|
options.validateFormat = value;
|
|
2200
2489
|
}
|
|
2201
2490
|
}
|
|
@@ -2221,15 +2510,13 @@ async function run(argv, cwd) {
|
|
|
2221
2510
|
root: options.root,
|
|
2222
2511
|
strict: options.strict,
|
|
2223
2512
|
format: options.validateFormat,
|
|
2224
|
-
...options.failOn !== void 0 ? { failOn: options.failOn } : {}
|
|
2225
|
-
...options.jsonPath !== void 0 ? { jsonPath: options.jsonPath } : {}
|
|
2513
|
+
...options.failOn !== void 0 ? { failOn: options.failOn } : {}
|
|
2226
2514
|
});
|
|
2227
2515
|
return;
|
|
2228
2516
|
case "report":
|
|
2229
2517
|
await runReport({
|
|
2230
2518
|
root: options.root,
|
|
2231
2519
|
format: options.reportFormat,
|
|
2232
|
-
...options.jsonPath !== void 0 ? { jsonPath: options.jsonPath } : {},
|
|
2233
2520
|
...options.reportOut !== void 0 ? { outPath: options.reportOut } : {}
|
|
2234
2521
|
});
|
|
2235
2522
|
return;
|
|
@@ -2251,14 +2538,13 @@ Options:
|
|
|
2251
2538
|
--root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
|
|
2252
2539
|
--dir <path> init \u306E\u51FA\u529B\u5148
|
|
2253
2540
|
--force \u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u3092\u4E0A\u66F8\u304D
|
|
2254
|
-
--yes init: \
|
|
2541
|
+
--yes init: \u4E88\u7D04\u30D5\u30E9\u30B0\uFF08\u73FE\u72B6\u306F\u975E\u5BFE\u8A71\u306E\u305F\u3081\u6319\u52D5\u5DEE\u306A\u3057\u3002\u5C06\u6765\u306E\u5BFE\u8A71\u5C0E\u5165\u6642\u306B\u81EA\u52D5Yes\uFF09
|
|
2255
2542
|
--dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
|
|
2256
|
-
--format <text|
|
|
2543
|
+
--format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
|
|
2257
2544
|
--format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
|
|
2258
|
-
--strict
|
|
2545
|
+
--strict validate: warning \u4EE5\u4E0A\u3067 exit 1
|
|
2259
2546
|
--fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
|
|
2260
|
-
--
|
|
2261
|
-
--out <path> report: \u51FA\u529B\u5148
|
|
2547
|
+
--out <path> report: \u51FA\u529B\u5148
|
|
2262
2548
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|
|
2263
2549
|
`;
|
|
2264
2550
|
}
|