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.cjs
CHANGED
|
@@ -160,8 +160,8 @@ function report(copied, skipped, dryRun, label) {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
// src/cli/commands/report.ts
|
|
163
|
-
var
|
|
164
|
-
var
|
|
163
|
+
var import_promises13 = require("fs/promises");
|
|
164
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
165
165
|
|
|
166
166
|
// src/core/config.ts
|
|
167
167
|
var import_promises2 = require("fs/promises");
|
|
@@ -169,13 +169,11 @@ var import_node_path4 = __toESM(require("path"), 1);
|
|
|
169
169
|
var import_yaml = require("yaml");
|
|
170
170
|
var defaultConfig = {
|
|
171
171
|
paths: {
|
|
172
|
-
specDir: ".qfai/spec",
|
|
173
|
-
decisionsDir: ".qfai/spec/decisions",
|
|
174
|
-
scenariosDir: ".qfai/spec/scenarios",
|
|
175
172
|
contractsDir: ".qfai/contracts",
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
173
|
+
specsDir: ".qfai/specs",
|
|
174
|
+
rulesDir: ".qfai/rules",
|
|
175
|
+
outDir: ".qfai/out",
|
|
176
|
+
promptsDir: ".qfai/prompts",
|
|
179
177
|
srcDir: "src",
|
|
180
178
|
testsDir: "tests"
|
|
181
179
|
},
|
|
@@ -200,8 +198,7 @@ var defaultConfig = {
|
|
|
200
198
|
}
|
|
201
199
|
},
|
|
202
200
|
output: {
|
|
203
|
-
|
|
204
|
-
jsonPath: ".qfai/out/validate.json"
|
|
201
|
+
validateJsonPath: ".qfai/out/validate.json"
|
|
205
202
|
}
|
|
206
203
|
};
|
|
207
204
|
function getConfigPath(root) {
|
|
@@ -250,27 +247,6 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
250
247
|
return base;
|
|
251
248
|
}
|
|
252
249
|
return {
|
|
253
|
-
specDir: readString(
|
|
254
|
-
raw.specDir,
|
|
255
|
-
base.specDir,
|
|
256
|
-
"paths.specDir",
|
|
257
|
-
configPath,
|
|
258
|
-
issues
|
|
259
|
-
),
|
|
260
|
-
decisionsDir: readString(
|
|
261
|
-
raw.decisionsDir,
|
|
262
|
-
base.decisionsDir,
|
|
263
|
-
"paths.decisionsDir",
|
|
264
|
-
configPath,
|
|
265
|
-
issues
|
|
266
|
-
),
|
|
267
|
-
scenariosDir: readString(
|
|
268
|
-
raw.scenariosDir,
|
|
269
|
-
base.scenariosDir,
|
|
270
|
-
"paths.scenariosDir",
|
|
271
|
-
configPath,
|
|
272
|
-
issues
|
|
273
|
-
),
|
|
274
250
|
contractsDir: readString(
|
|
275
251
|
raw.contractsDir,
|
|
276
252
|
base.contractsDir,
|
|
@@ -278,24 +254,31 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
278
254
|
configPath,
|
|
279
255
|
issues
|
|
280
256
|
),
|
|
281
|
-
|
|
282
|
-
raw.
|
|
283
|
-
base.
|
|
284
|
-
"paths.
|
|
257
|
+
specsDir: readString(
|
|
258
|
+
raw.specsDir,
|
|
259
|
+
base.specsDir,
|
|
260
|
+
"paths.specsDir",
|
|
261
|
+
configPath,
|
|
262
|
+
issues
|
|
263
|
+
),
|
|
264
|
+
rulesDir: readString(
|
|
265
|
+
raw.rulesDir,
|
|
266
|
+
base.rulesDir,
|
|
267
|
+
"paths.rulesDir",
|
|
285
268
|
configPath,
|
|
286
269
|
issues
|
|
287
270
|
),
|
|
288
|
-
|
|
289
|
-
raw.
|
|
290
|
-
base.
|
|
291
|
-
"paths.
|
|
271
|
+
outDir: readString(
|
|
272
|
+
raw.outDir,
|
|
273
|
+
base.outDir,
|
|
274
|
+
"paths.outDir",
|
|
292
275
|
configPath,
|
|
293
276
|
issues
|
|
294
277
|
),
|
|
295
|
-
|
|
296
|
-
raw.
|
|
297
|
-
base.
|
|
298
|
-
"paths.
|
|
278
|
+
promptsDir: readString(
|
|
279
|
+
raw.promptsDir,
|
|
280
|
+
base.promptsDir,
|
|
281
|
+
"paths.promptsDir",
|
|
299
282
|
configPath,
|
|
300
283
|
issues
|
|
301
284
|
),
|
|
@@ -418,17 +401,10 @@ function normalizeOutput(raw, configPath, issues) {
|
|
|
418
401
|
return base;
|
|
419
402
|
}
|
|
420
403
|
return {
|
|
421
|
-
|
|
422
|
-
raw.
|
|
423
|
-
base.
|
|
424
|
-
"output.
|
|
425
|
-
configPath,
|
|
426
|
-
issues
|
|
427
|
-
),
|
|
428
|
-
jsonPath: readString(
|
|
429
|
-
raw.jsonPath,
|
|
430
|
-
base.jsonPath,
|
|
431
|
-
"output.jsonPath",
|
|
404
|
+
validateJsonPath: readString(
|
|
405
|
+
raw.validateJsonPath,
|
|
406
|
+
base.validateJsonPath,
|
|
407
|
+
"output.validateJsonPath",
|
|
432
408
|
configPath,
|
|
433
409
|
issues
|
|
434
410
|
)
|
|
@@ -495,20 +471,6 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
|
495
471
|
}
|
|
496
472
|
return fallback;
|
|
497
473
|
}
|
|
498
|
-
function readOutputFormat(value, fallback, label, configPath, issues) {
|
|
499
|
-
if (value === "text" || value === "json" || value === "github") {
|
|
500
|
-
return value;
|
|
501
|
-
}
|
|
502
|
-
if (value !== void 0) {
|
|
503
|
-
issues.push(
|
|
504
|
-
configIssue(
|
|
505
|
-
configPath,
|
|
506
|
-
`${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
507
|
-
)
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
return fallback;
|
|
511
|
-
}
|
|
512
474
|
function configIssue(file, message) {
|
|
513
475
|
return {
|
|
514
476
|
code: "QFAI_CONFIG_INVALID",
|
|
@@ -535,7 +497,8 @@ function isRecord(value) {
|
|
|
535
497
|
}
|
|
536
498
|
|
|
537
499
|
// src/core/report.ts
|
|
538
|
-
var
|
|
500
|
+
var import_promises12 = require("fs/promises");
|
|
501
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
539
502
|
|
|
540
503
|
// src/core/discovery.ts
|
|
541
504
|
var import_node_path6 = __toESM(require("path"), 1);
|
|
@@ -596,10 +559,24 @@ async function exists2(target) {
|
|
|
596
559
|
}
|
|
597
560
|
|
|
598
561
|
// src/core/discovery.ts
|
|
599
|
-
var
|
|
600
|
-
async function
|
|
601
|
-
const files = await collectFiles(
|
|
602
|
-
|
|
562
|
+
var SPEC_PACK_DIR_PATTERN = /^spec-\d{3}$/;
|
|
563
|
+
async function collectSpecPackDirs(specsRoot) {
|
|
564
|
+
const files = await collectFiles(specsRoot, { extensions: [".md"] });
|
|
565
|
+
const packs = /* @__PURE__ */ new Set();
|
|
566
|
+
for (const file of files) {
|
|
567
|
+
if (isSpecPackFile(file, "spec.md")) {
|
|
568
|
+
packs.add(import_node_path6.default.dirname(file));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return Array.from(packs).sort();
|
|
572
|
+
}
|
|
573
|
+
async function collectSpecFiles(specsRoot) {
|
|
574
|
+
const files = await collectFiles(specsRoot, { extensions: [".md"] });
|
|
575
|
+
return files.filter((file) => isSpecPackFile(file, "spec.md"));
|
|
576
|
+
}
|
|
577
|
+
async function collectScenarioFiles(specsRoot) {
|
|
578
|
+
const files = await collectFiles(specsRoot, { extensions: [".md"] });
|
|
579
|
+
return files.filter((file) => isSpecPackFile(file, "scenario.md"));
|
|
603
580
|
}
|
|
604
581
|
async function collectUiContractFiles(uiRoot) {
|
|
605
582
|
return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
@@ -618,9 +595,12 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
|
618
595
|
]);
|
|
619
596
|
return { ui, api, db };
|
|
620
597
|
}
|
|
621
|
-
function
|
|
622
|
-
|
|
623
|
-
|
|
598
|
+
function isSpecPackFile(filePath, baseName) {
|
|
599
|
+
if (import_node_path6.default.basename(filePath).toLowerCase() !== baseName) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
const dirName = import_node_path6.default.basename(import_node_path6.default.dirname(filePath)).toLowerCase();
|
|
603
|
+
return SPEC_PACK_DIR_PATTERN.test(dirName);
|
|
624
604
|
}
|
|
625
605
|
|
|
626
606
|
// src/core/ids.ts
|
|
@@ -684,8 +664,8 @@ var import_promises4 = require("fs/promises");
|
|
|
684
664
|
var import_node_path7 = __toESM(require("path"), 1);
|
|
685
665
|
var import_node_url2 = require("url");
|
|
686
666
|
async function resolveToolVersion() {
|
|
687
|
-
if ("0.
|
|
688
|
-
return "0.
|
|
667
|
+
if ("0.3.1".length > 0) {
|
|
668
|
+
return "0.3.1";
|
|
689
669
|
}
|
|
690
670
|
try {
|
|
691
671
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -705,6 +685,7 @@ function resolvePackageJsonPath() {
|
|
|
705
685
|
|
|
706
686
|
// src/core/validators/contracts.ts
|
|
707
687
|
var import_promises5 = require("fs/promises");
|
|
688
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
708
689
|
|
|
709
690
|
// src/core/contracts.ts
|
|
710
691
|
var import_node_path8 = __toESM(require("path"), 1);
|
|
@@ -760,19 +741,10 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
760
741
|
];
|
|
761
742
|
async function validateContracts(root, config) {
|
|
762
743
|
const issues = [];
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
);
|
|
766
|
-
issues.push(
|
|
767
|
-
...await validateApiContracts(
|
|
768
|
-
resolvePath(root, config, "apiContractsDir")
|
|
769
|
-
)
|
|
770
|
-
);
|
|
771
|
-
issues.push(
|
|
772
|
-
...await validateDataContracts(
|
|
773
|
-
resolvePath(root, config, "dataContractsDir")
|
|
774
|
-
)
|
|
775
|
-
);
|
|
744
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
745
|
+
issues.push(...await validateUiContracts(import_node_path9.default.join(contractsRoot, "ui")));
|
|
746
|
+
issues.push(...await validateApiContracts(import_node_path9.default.join(contractsRoot, "api")));
|
|
747
|
+
issues.push(...await validateDataContracts(import_node_path9.default.join(contractsRoot, "db")));
|
|
776
748
|
return issues;
|
|
777
749
|
}
|
|
778
750
|
async function validateUiContracts(uiRoot) {
|
|
@@ -988,33 +960,125 @@ function formatError2(error2) {
|
|
|
988
960
|
return String(error2);
|
|
989
961
|
}
|
|
990
962
|
function issue(code, message, severity, file, rule, refs) {
|
|
991
|
-
const
|
|
963
|
+
const issue7 = {
|
|
992
964
|
code,
|
|
993
965
|
severity,
|
|
994
966
|
message
|
|
995
967
|
};
|
|
996
968
|
if (file) {
|
|
997
|
-
|
|
969
|
+
issue7.file = file;
|
|
998
970
|
}
|
|
999
971
|
if (rule) {
|
|
1000
|
-
|
|
972
|
+
issue7.rule = rule;
|
|
1001
973
|
}
|
|
1002
974
|
if (refs && refs.length > 0) {
|
|
1003
|
-
|
|
975
|
+
issue7.refs = refs;
|
|
1004
976
|
}
|
|
1005
|
-
return
|
|
977
|
+
return issue7;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/core/validators/delta.ts
|
|
981
|
+
var import_promises6 = require("fs/promises");
|
|
982
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
983
|
+
var SECTION_RE = /^##\s+変更区分/m;
|
|
984
|
+
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
985
|
+
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
986
|
+
var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
|
|
987
|
+
var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
|
|
988
|
+
async function validateDeltas(root, config) {
|
|
989
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
990
|
+
const packs = await collectSpecPackDirs(specsRoot);
|
|
991
|
+
if (packs.length === 0) {
|
|
992
|
+
return [];
|
|
993
|
+
}
|
|
994
|
+
const issues = [];
|
|
995
|
+
for (const pack of packs) {
|
|
996
|
+
const deltaPath = import_node_path10.default.join(pack, "delta.md");
|
|
997
|
+
let text;
|
|
998
|
+
try {
|
|
999
|
+
text = await (0, import_promises6.readFile)(deltaPath, "utf-8");
|
|
1000
|
+
} catch (error2) {
|
|
1001
|
+
if (isMissingFileError(error2)) {
|
|
1002
|
+
issues.push(
|
|
1003
|
+
issue2(
|
|
1004
|
+
"QFAI-DELTA-001",
|
|
1005
|
+
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1006
|
+
"error",
|
|
1007
|
+
deltaPath,
|
|
1008
|
+
"delta.exists"
|
|
1009
|
+
)
|
|
1010
|
+
);
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
throw error2;
|
|
1014
|
+
}
|
|
1015
|
+
const hasSection = SECTION_RE.test(text);
|
|
1016
|
+
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
1017
|
+
const hasChange = CHANGE_LINE_RE.test(text);
|
|
1018
|
+
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
1019
|
+
issues.push(
|
|
1020
|
+
issue2(
|
|
1021
|
+
"QFAI-DELTA-002",
|
|
1022
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
1023
|
+
"error",
|
|
1024
|
+
deltaPath,
|
|
1025
|
+
"delta.section"
|
|
1026
|
+
)
|
|
1027
|
+
);
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
1031
|
+
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
1032
|
+
if (compatibilityChecked === changeChecked) {
|
|
1033
|
+
issues.push(
|
|
1034
|
+
issue2(
|
|
1035
|
+
"QFAI-DELTA-003",
|
|
1036
|
+
"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",
|
|
1037
|
+
"error",
|
|
1038
|
+
deltaPath,
|
|
1039
|
+
"delta.classification"
|
|
1040
|
+
)
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return issues;
|
|
1045
|
+
}
|
|
1046
|
+
function isMissingFileError(error2) {
|
|
1047
|
+
if (!error2 || typeof error2 !== "object") {
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
return error2.code === "ENOENT";
|
|
1051
|
+
}
|
|
1052
|
+
function issue2(code, message, severity, file, rule, refs) {
|
|
1053
|
+
const issue7 = {
|
|
1054
|
+
code,
|
|
1055
|
+
severity,
|
|
1056
|
+
message
|
|
1057
|
+
};
|
|
1058
|
+
if (file) {
|
|
1059
|
+
issue7.file = file;
|
|
1060
|
+
}
|
|
1061
|
+
if (rule) {
|
|
1062
|
+
issue7.rule = rule;
|
|
1063
|
+
}
|
|
1064
|
+
if (refs && refs.length > 0) {
|
|
1065
|
+
issue7.refs = refs;
|
|
1066
|
+
}
|
|
1067
|
+
return issue7;
|
|
1006
1068
|
}
|
|
1007
1069
|
|
|
1008
1070
|
// src/core/validators/ids.ts
|
|
1009
|
-
var
|
|
1010
|
-
var
|
|
1071
|
+
var import_promises8 = require("fs/promises");
|
|
1072
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
1011
1073
|
|
|
1012
1074
|
// src/core/contractIndex.ts
|
|
1013
|
-
var
|
|
1075
|
+
var import_promises7 = require("fs/promises");
|
|
1076
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
1014
1077
|
async function buildContractIndex(root, config) {
|
|
1015
|
-
const
|
|
1016
|
-
const
|
|
1017
|
-
const
|
|
1078
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1079
|
+
const uiRoot = import_node_path11.default.join(contractsRoot, "ui");
|
|
1080
|
+
const apiRoot = import_node_path11.default.join(contractsRoot, "api");
|
|
1081
|
+
const dataRoot = import_node_path11.default.join(contractsRoot, "db");
|
|
1018
1082
|
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
1019
1083
|
collectUiContractFiles(uiRoot),
|
|
1020
1084
|
collectApiContractFiles(apiRoot),
|
|
@@ -1033,7 +1097,7 @@ async function buildContractIndex(root, config) {
|
|
|
1033
1097
|
}
|
|
1034
1098
|
async function indexUiContracts(files, index) {
|
|
1035
1099
|
for (const file of files) {
|
|
1036
|
-
const text = await (0,
|
|
1100
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
1037
1101
|
try {
|
|
1038
1102
|
const doc = parseStructuredContract(file, text);
|
|
1039
1103
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1045,7 +1109,7 @@ async function indexUiContracts(files, index) {
|
|
|
1045
1109
|
}
|
|
1046
1110
|
async function indexApiContracts(files, index) {
|
|
1047
1111
|
for (const file of files) {
|
|
1048
|
-
const text = await (0,
|
|
1112
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
1049
1113
|
try {
|
|
1050
1114
|
const doc = parseStructuredContract(file, text);
|
|
1051
1115
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1057,7 +1121,7 @@ async function indexApiContracts(files, index) {
|
|
|
1057
1121
|
}
|
|
1058
1122
|
async function indexDataContracts(files, index) {
|
|
1059
1123
|
for (const file of files) {
|
|
1060
|
-
const text = await (0,
|
|
1124
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
1061
1125
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1062
1126
|
}
|
|
1063
1127
|
}
|
|
@@ -1068,15 +1132,191 @@ function record(index, id, file) {
|
|
|
1068
1132
|
index.idToFiles.set(id, current);
|
|
1069
1133
|
}
|
|
1070
1134
|
|
|
1135
|
+
// src/core/parse/gherkin.ts
|
|
1136
|
+
var FEATURE_RE = /^\s*Feature:\s+/;
|
|
1137
|
+
var SCENARIO_RE = /^\s*Scenario(?: Outline)?:\s*(.+)\s*$/;
|
|
1138
|
+
var TAG_LINE_RE = /^\s*@/;
|
|
1139
|
+
function parseTags(line) {
|
|
1140
|
+
return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
|
|
1141
|
+
}
|
|
1142
|
+
function parseGherkinFeature(text, file) {
|
|
1143
|
+
const lines = text.split(/\r?\n/);
|
|
1144
|
+
const scenarios = [];
|
|
1145
|
+
let featurePresent = false;
|
|
1146
|
+
let featureTags = [];
|
|
1147
|
+
let pendingTags = [];
|
|
1148
|
+
let current = null;
|
|
1149
|
+
const flush = () => {
|
|
1150
|
+
if (!current) return;
|
|
1151
|
+
scenarios.push({
|
|
1152
|
+
...current,
|
|
1153
|
+
body: current.body.trim()
|
|
1154
|
+
});
|
|
1155
|
+
current = null;
|
|
1156
|
+
};
|
|
1157
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1158
|
+
const line = lines[i] ?? "";
|
|
1159
|
+
const trimmed = line.trim();
|
|
1160
|
+
if (TAG_LINE_RE.test(trimmed)) {
|
|
1161
|
+
pendingTags.push(...parseTags(trimmed));
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
if (FEATURE_RE.test(trimmed)) {
|
|
1165
|
+
featurePresent = true;
|
|
1166
|
+
featureTags = [...pendingTags];
|
|
1167
|
+
pendingTags = [];
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
const match = trimmed.match(SCENARIO_RE);
|
|
1171
|
+
if (match) {
|
|
1172
|
+
const scenarioName = match[1]?.trim();
|
|
1173
|
+
if (!scenarioName) {
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
flush();
|
|
1177
|
+
current = {
|
|
1178
|
+
name: scenarioName,
|
|
1179
|
+
line: i + 1,
|
|
1180
|
+
tags: [...featureTags, ...pendingTags],
|
|
1181
|
+
body: ""
|
|
1182
|
+
};
|
|
1183
|
+
pendingTags = [];
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
if (current) {
|
|
1187
|
+
current.body += `${line}
|
|
1188
|
+
`;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
flush();
|
|
1192
|
+
return { file, featurePresent, scenarios };
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// src/core/parse/markdown.ts
|
|
1196
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1197
|
+
function parseHeadings(md) {
|
|
1198
|
+
const lines = md.split(/\r?\n/);
|
|
1199
|
+
const headings = [];
|
|
1200
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1201
|
+
const line = lines[i] ?? "";
|
|
1202
|
+
const match = line.match(HEADING_RE);
|
|
1203
|
+
if (!match) continue;
|
|
1204
|
+
const levelToken = match[1];
|
|
1205
|
+
const title = match[2];
|
|
1206
|
+
if (!levelToken || !title) continue;
|
|
1207
|
+
headings.push({
|
|
1208
|
+
level: levelToken.length,
|
|
1209
|
+
title: title.trim(),
|
|
1210
|
+
line: i + 1
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
return headings;
|
|
1214
|
+
}
|
|
1215
|
+
function extractH2Sections(md) {
|
|
1216
|
+
const lines = md.split(/\r?\n/);
|
|
1217
|
+
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1218
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1219
|
+
for (let i = 0; i < headings.length; i++) {
|
|
1220
|
+
const current = headings[i];
|
|
1221
|
+
if (!current) continue;
|
|
1222
|
+
const next = headings[i + 1];
|
|
1223
|
+
const startLine = current.line + 1;
|
|
1224
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1225
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1226
|
+
sections.set(current.title.trim(), {
|
|
1227
|
+
title: current.title.trim(),
|
|
1228
|
+
startLine,
|
|
1229
|
+
endLine,
|
|
1230
|
+
body
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
return sections;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// src/core/parse/spec.ts
|
|
1237
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1238
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1239
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1240
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1241
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1242
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1243
|
+
function parseSpec(md, file) {
|
|
1244
|
+
const headings = parseHeadings(md);
|
|
1245
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
1246
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1247
|
+
const sections = extractH2Sections(md);
|
|
1248
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1249
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1250
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1251
|
+
const startLine = brSection?.startLine ?? 1;
|
|
1252
|
+
const brs = [];
|
|
1253
|
+
const brsWithoutPriority = [];
|
|
1254
|
+
const brsWithInvalidPriority = [];
|
|
1255
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
1256
|
+
const lineText = brLines[i] ?? "";
|
|
1257
|
+
const lineNumber = startLine + i;
|
|
1258
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
1259
|
+
if (validMatch) {
|
|
1260
|
+
const id = validMatch[1];
|
|
1261
|
+
const priority = validMatch[2];
|
|
1262
|
+
const text = validMatch[3];
|
|
1263
|
+
if (!id || !priority || !text) continue;
|
|
1264
|
+
brs.push({
|
|
1265
|
+
id,
|
|
1266
|
+
priority,
|
|
1267
|
+
text: text.trim(),
|
|
1268
|
+
line: lineNumber
|
|
1269
|
+
});
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1273
|
+
if (anyPriorityMatch) {
|
|
1274
|
+
const id = anyPriorityMatch[1];
|
|
1275
|
+
const priority = anyPriorityMatch[2];
|
|
1276
|
+
const text = anyPriorityMatch[3];
|
|
1277
|
+
if (!id || !priority || !text) continue;
|
|
1278
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
1279
|
+
brsWithInvalidPriority.push({
|
|
1280
|
+
id,
|
|
1281
|
+
priority,
|
|
1282
|
+
text: text.trim(),
|
|
1283
|
+
line: lineNumber
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1289
|
+
if (noPriorityMatch) {
|
|
1290
|
+
const id = noPriorityMatch[1];
|
|
1291
|
+
const text = noPriorityMatch[2];
|
|
1292
|
+
if (!id || !text) continue;
|
|
1293
|
+
brsWithoutPriority.push({
|
|
1294
|
+
id,
|
|
1295
|
+
text: text.trim(),
|
|
1296
|
+
line: lineNumber
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
const parsed = {
|
|
1301
|
+
file,
|
|
1302
|
+
sections: sectionNames,
|
|
1303
|
+
brs,
|
|
1304
|
+
brsWithoutPriority,
|
|
1305
|
+
brsWithInvalidPriority
|
|
1306
|
+
};
|
|
1307
|
+
if (specId) {
|
|
1308
|
+
parsed.specId = specId;
|
|
1309
|
+
}
|
|
1310
|
+
return parsed;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1071
1313
|
// src/core/validators/ids.ts
|
|
1314
|
+
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1072
1315
|
async function validateDefinedIds(root, config) {
|
|
1073
1316
|
const issues = [];
|
|
1074
|
-
const
|
|
1075
|
-
const
|
|
1076
|
-
const
|
|
1077
|
-
const scenarioFiles = await collectFiles(scenarioRoot, {
|
|
1078
|
-
extensions: [".feature"]
|
|
1079
|
-
});
|
|
1317
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1318
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
1319
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1080
1320
|
const defined = /* @__PURE__ */ new Map();
|
|
1081
1321
|
await collectSpecDefinitionIds(specFiles, defined);
|
|
1082
1322
|
await collectScenarioDefinitionIds(scenarioFiles, defined);
|
|
@@ -1092,7 +1332,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1092
1332
|
}
|
|
1093
1333
|
const sorted = Array.from(files).sort();
|
|
1094
1334
|
issues.push(
|
|
1095
|
-
|
|
1335
|
+
issue3(
|
|
1096
1336
|
"QFAI-ID-001",
|
|
1097
1337
|
`ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
|
|
1098
1338
|
"error",
|
|
@@ -1105,15 +1345,25 @@ async function validateDefinedIds(root, config) {
|
|
|
1105
1345
|
}
|
|
1106
1346
|
async function collectSpecDefinitionIds(files, out) {
|
|
1107
1347
|
for (const file of files) {
|
|
1108
|
-
const text = await (0,
|
|
1109
|
-
|
|
1110
|
-
|
|
1348
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
1349
|
+
const parsed = parseSpec(text, file);
|
|
1350
|
+
if (parsed.specId) {
|
|
1351
|
+
recordId(out, parsed.specId, file);
|
|
1352
|
+
}
|
|
1353
|
+
parsed.brs.forEach((br) => recordId(out, br.id, file));
|
|
1111
1354
|
}
|
|
1112
1355
|
}
|
|
1113
1356
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1114
1357
|
for (const file of files) {
|
|
1115
|
-
const text = await (0,
|
|
1116
|
-
|
|
1358
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
1359
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1360
|
+
for (const scenario of parsed.scenarios) {
|
|
1361
|
+
for (const tag of scenario.tags) {
|
|
1362
|
+
if (SC_TAG_RE.test(tag)) {
|
|
1363
|
+
recordId(out, tag, file);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1117
1367
|
}
|
|
1118
1368
|
}
|
|
1119
1369
|
function recordId(out, id, file) {
|
|
@@ -1123,58 +1373,60 @@ function recordId(out, id, file) {
|
|
|
1123
1373
|
}
|
|
1124
1374
|
function formatFileList(files, root) {
|
|
1125
1375
|
return files.map((file) => {
|
|
1126
|
-
const relative =
|
|
1376
|
+
const relative = import_node_path12.default.relative(root, file);
|
|
1127
1377
|
return relative.length > 0 ? relative : file;
|
|
1128
1378
|
}).join(", ");
|
|
1129
1379
|
}
|
|
1130
|
-
function
|
|
1131
|
-
const
|
|
1380
|
+
function issue3(code, message, severity, file, rule, refs) {
|
|
1381
|
+
const issue7 = {
|
|
1132
1382
|
code,
|
|
1133
1383
|
severity,
|
|
1134
1384
|
message
|
|
1135
1385
|
};
|
|
1136
1386
|
if (file) {
|
|
1137
|
-
|
|
1387
|
+
issue7.file = file;
|
|
1138
1388
|
}
|
|
1139
1389
|
if (rule) {
|
|
1140
|
-
|
|
1390
|
+
issue7.rule = rule;
|
|
1141
1391
|
}
|
|
1142
1392
|
if (refs && refs.length > 0) {
|
|
1143
|
-
|
|
1393
|
+
issue7.refs = refs;
|
|
1144
1394
|
}
|
|
1145
|
-
return
|
|
1395
|
+
return issue7;
|
|
1146
1396
|
}
|
|
1147
1397
|
|
|
1148
1398
|
// src/core/validators/scenario.ts
|
|
1149
|
-
var
|
|
1399
|
+
var import_promises9 = require("fs/promises");
|
|
1150
1400
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1151
1401
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1152
1402
|
var THEN_PATTERN = /\bThen\b/;
|
|
1403
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1404
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1405
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1153
1406
|
async function validateScenarios(root, config) {
|
|
1154
|
-
const
|
|
1155
|
-
const files = await
|
|
1156
|
-
extensions: [".feature"]
|
|
1157
|
-
});
|
|
1407
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1408
|
+
const files = await collectScenarioFiles(specsRoot);
|
|
1158
1409
|
if (files.length === 0) {
|
|
1159
1410
|
return [
|
|
1160
|
-
|
|
1411
|
+
issue4(
|
|
1161
1412
|
"QFAI-SC-000",
|
|
1162
1413
|
"Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1163
1414
|
"info",
|
|
1164
|
-
|
|
1415
|
+
specsRoot,
|
|
1165
1416
|
"scenario.files"
|
|
1166
1417
|
)
|
|
1167
1418
|
];
|
|
1168
1419
|
}
|
|
1169
1420
|
const issues = [];
|
|
1170
1421
|
for (const file of files) {
|
|
1171
|
-
const text = await (0,
|
|
1422
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1172
1423
|
issues.push(...validateScenarioContent(text, file));
|
|
1173
1424
|
}
|
|
1174
1425
|
return issues;
|
|
1175
1426
|
}
|
|
1176
1427
|
function validateScenarioContent(text, file) {
|
|
1177
1428
|
const issues = [];
|
|
1429
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1178
1430
|
const invalidIds = extractInvalidIds(text, [
|
|
1179
1431
|
"SPEC",
|
|
1180
1432
|
"BR",
|
|
@@ -1186,7 +1438,7 @@ function validateScenarioContent(text, file) {
|
|
|
1186
1438
|
]);
|
|
1187
1439
|
if (invalidIds.length > 0) {
|
|
1188
1440
|
issues.push(
|
|
1189
|
-
|
|
1441
|
+
issue4(
|
|
1190
1442
|
"QFAI-ID-002",
|
|
1191
1443
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
1192
1444
|
"error",
|
|
@@ -1196,94 +1448,114 @@ function validateScenarioContent(text, file) {
|
|
|
1196
1448
|
)
|
|
1197
1449
|
);
|
|
1198
1450
|
}
|
|
1199
|
-
const
|
|
1200
|
-
if (
|
|
1451
|
+
const missingStructure = [];
|
|
1452
|
+
if (!parsed.featurePresent) missingStructure.push("Feature");
|
|
1453
|
+
if (parsed.scenarios.length === 0) missingStructure.push("Scenario");
|
|
1454
|
+
if (missingStructure.length > 0) {
|
|
1201
1455
|
issues.push(
|
|
1202
|
-
|
|
1203
|
-
"QFAI-SC-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
"scenario.id"
|
|
1208
|
-
)
|
|
1209
|
-
);
|
|
1210
|
-
}
|
|
1211
|
-
const specIds = extractIds(text, "SPEC");
|
|
1212
|
-
if (specIds.length === 0) {
|
|
1213
|
-
issues.push(
|
|
1214
|
-
issue3(
|
|
1215
|
-
"QFAI-SC-002",
|
|
1216
|
-
"SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
1217
|
-
"error",
|
|
1218
|
-
file,
|
|
1219
|
-
"scenario.spec"
|
|
1220
|
-
)
|
|
1221
|
-
);
|
|
1222
|
-
}
|
|
1223
|
-
const brIds = extractIds(text, "BR");
|
|
1224
|
-
if (brIds.length === 0) {
|
|
1225
|
-
issues.push(
|
|
1226
|
-
issue3(
|
|
1227
|
-
"QFAI-SC-003",
|
|
1228
|
-
"SC \u306F BR \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
1456
|
+
issue4(
|
|
1457
|
+
"QFAI-SC-006",
|
|
1458
|
+
`Scenario \u30D5\u30A1\u30A4\u30EB\u306B\u5FC5\u8981\u306A\u69CB\u9020\u304C\u3042\u308A\u307E\u305B\u3093: ${missingStructure.join(
|
|
1459
|
+
", "
|
|
1460
|
+
)}`,
|
|
1229
1461
|
"error",
|
|
1230
1462
|
file,
|
|
1231
|
-
"scenario.
|
|
1463
|
+
"scenario.structure"
|
|
1232
1464
|
)
|
|
1233
1465
|
);
|
|
1234
1466
|
}
|
|
1235
|
-
const
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1467
|
+
for (const scenario of parsed.scenarios) {
|
|
1468
|
+
if (scenario.tags.length === 0) {
|
|
1469
|
+
issues.push(
|
|
1470
|
+
issue4(
|
|
1471
|
+
"QFAI-SC-007",
|
|
1472
|
+
`Scenario \u30BF\u30B0\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${scenario.name}`,
|
|
1473
|
+
"error",
|
|
1474
|
+
file,
|
|
1475
|
+
"scenario.tags"
|
|
1476
|
+
)
|
|
1477
|
+
);
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
const missingTags = [];
|
|
1481
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
|
|
1482
|
+
if (scTags.length === 0) {
|
|
1483
|
+
missingTags.push("SC(0\u4EF6)");
|
|
1484
|
+
} else if (scTags.length > 1) {
|
|
1485
|
+
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1486
|
+
}
|
|
1487
|
+
if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
|
|
1488
|
+
missingTags.push("SPEC");
|
|
1489
|
+
}
|
|
1490
|
+
if (!scenario.tags.some((tag) => BR_TAG_RE.test(tag))) {
|
|
1491
|
+
missingTags.push("BR");
|
|
1492
|
+
}
|
|
1493
|
+
if (missingTags.length > 0) {
|
|
1494
|
+
issues.push(
|
|
1495
|
+
issue4(
|
|
1496
|
+
"QFAI-SC-008",
|
|
1497
|
+
`Scenario \u30BF\u30B0\u306B\u4E0D\u8DB3\u304C\u3042\u308A\u307E\u3059: ${missingTags.join(", ")} (${scenario.name})`,
|
|
1498
|
+
"error",
|
|
1499
|
+
file,
|
|
1500
|
+
"scenario.tagIds"
|
|
1501
|
+
)
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1244
1504
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1505
|
+
for (const scenario of parsed.scenarios) {
|
|
1506
|
+
const missingSteps = [];
|
|
1507
|
+
if (!GIVEN_PATTERN.test(scenario.body)) {
|
|
1508
|
+
missingSteps.push("Given");
|
|
1509
|
+
}
|
|
1510
|
+
if (!WHEN_PATTERN.test(scenario.body)) {
|
|
1511
|
+
missingSteps.push("When");
|
|
1512
|
+
}
|
|
1513
|
+
if (!THEN_PATTERN.test(scenario.body)) {
|
|
1514
|
+
missingSteps.push("Then");
|
|
1515
|
+
}
|
|
1516
|
+
if (missingSteps.length > 0) {
|
|
1517
|
+
issues.push(
|
|
1518
|
+
issue4(
|
|
1519
|
+
"QFAI-SC-005",
|
|
1520
|
+
`Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")} (${scenario.name})`,
|
|
1521
|
+
"warning",
|
|
1522
|
+
file,
|
|
1523
|
+
"scenario.steps"
|
|
1524
|
+
)
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1255
1527
|
}
|
|
1256
1528
|
return issues;
|
|
1257
1529
|
}
|
|
1258
|
-
function
|
|
1259
|
-
const
|
|
1530
|
+
function issue4(code, message, severity, file, rule, refs) {
|
|
1531
|
+
const issue7 = {
|
|
1260
1532
|
code,
|
|
1261
1533
|
severity,
|
|
1262
1534
|
message
|
|
1263
1535
|
};
|
|
1264
1536
|
if (file) {
|
|
1265
|
-
|
|
1537
|
+
issue7.file = file;
|
|
1266
1538
|
}
|
|
1267
1539
|
if (rule) {
|
|
1268
|
-
|
|
1540
|
+
issue7.rule = rule;
|
|
1269
1541
|
}
|
|
1270
1542
|
if (refs && refs.length > 0) {
|
|
1271
|
-
|
|
1543
|
+
issue7.refs = refs;
|
|
1272
1544
|
}
|
|
1273
|
-
return
|
|
1545
|
+
return issue7;
|
|
1274
1546
|
}
|
|
1275
1547
|
|
|
1276
1548
|
// src/core/validators/spec.ts
|
|
1277
|
-
var
|
|
1549
|
+
var import_promises10 = require("fs/promises");
|
|
1278
1550
|
async function validateSpecs(root, config) {
|
|
1279
|
-
const specsRoot = resolvePath(root, config, "
|
|
1551
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1280
1552
|
const files = await collectSpecFiles(specsRoot);
|
|
1281
1553
|
if (files.length === 0) {
|
|
1282
|
-
const expected = "spec-
|
|
1554
|
+
const expected = "spec-001/spec.md";
|
|
1283
1555
|
return [
|
|
1284
|
-
|
|
1556
|
+
issue5(
|
|
1285
1557
|
"QFAI-SPEC-000",
|
|
1286
|
-
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.
|
|
1558
|
+
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}`,
|
|
1287
1559
|
"info",
|
|
1288
1560
|
specsRoot,
|
|
1289
1561
|
"spec.files"
|
|
@@ -1292,7 +1564,7 @@ async function validateSpecs(root, config) {
|
|
|
1292
1564
|
}
|
|
1293
1565
|
const issues = [];
|
|
1294
1566
|
for (const file of files) {
|
|
1295
|
-
const text = await (0,
|
|
1567
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1296
1568
|
issues.push(
|
|
1297
1569
|
...validateSpecContent(
|
|
1298
1570
|
text,
|
|
@@ -1305,6 +1577,7 @@ async function validateSpecs(root, config) {
|
|
|
1305
1577
|
}
|
|
1306
1578
|
function validateSpecContent(text, file, requiredSections) {
|
|
1307
1579
|
const issues = [];
|
|
1580
|
+
const parsed = parseSpec(text, file);
|
|
1308
1581
|
const invalidIds = extractInvalidIds(text, [
|
|
1309
1582
|
"SPEC",
|
|
1310
1583
|
"BR",
|
|
@@ -1316,7 +1589,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1316
1589
|
]);
|
|
1317
1590
|
if (invalidIds.length > 0) {
|
|
1318
1591
|
issues.push(
|
|
1319
|
-
|
|
1592
|
+
issue5(
|
|
1320
1593
|
"QFAI-ID-002",
|
|
1321
1594
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
1322
1595
|
"error",
|
|
@@ -1326,10 +1599,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1326
1599
|
)
|
|
1327
1600
|
);
|
|
1328
1601
|
}
|
|
1329
|
-
|
|
1330
|
-
if (specIds.length === 0) {
|
|
1602
|
+
if (!parsed.specId) {
|
|
1331
1603
|
issues.push(
|
|
1332
|
-
|
|
1604
|
+
issue5(
|
|
1333
1605
|
"QFAI-SPEC-001",
|
|
1334
1606
|
"SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1335
1607
|
"error",
|
|
@@ -1338,10 +1610,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1338
1610
|
)
|
|
1339
1611
|
);
|
|
1340
1612
|
}
|
|
1341
|
-
|
|
1342
|
-
if (brIds.length === 0) {
|
|
1613
|
+
if (parsed.brs.length === 0) {
|
|
1343
1614
|
issues.push(
|
|
1344
|
-
|
|
1615
|
+
issue5(
|
|
1345
1616
|
"QFAI-SPEC-002",
|
|
1346
1617
|
"BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1347
1618
|
"error",
|
|
@@ -1350,10 +1621,34 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1350
1621
|
)
|
|
1351
1622
|
);
|
|
1352
1623
|
}
|
|
1624
|
+
for (const br of parsed.brsWithoutPriority) {
|
|
1625
|
+
issues.push(
|
|
1626
|
+
issue5(
|
|
1627
|
+
"QFAI-BR-001",
|
|
1628
|
+
`BR \u884C\u306B Priority \u304C\u3042\u308A\u307E\u305B\u3093: ${br.id}`,
|
|
1629
|
+
"error",
|
|
1630
|
+
file,
|
|
1631
|
+
"spec.brPriority",
|
|
1632
|
+
[br.id]
|
|
1633
|
+
)
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
for (const br of parsed.brsWithInvalidPriority) {
|
|
1637
|
+
issues.push(
|
|
1638
|
+
issue5(
|
|
1639
|
+
"QFAI-BR-002",
|
|
1640
|
+
`BR Priority \u304C\u4E0D\u6B63\u3067\u3059: ${br.id} (${br.priority})`,
|
|
1641
|
+
"error",
|
|
1642
|
+
file,
|
|
1643
|
+
"spec.brPriority",
|
|
1644
|
+
[br.id]
|
|
1645
|
+
)
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1353
1648
|
const scIds = extractIds(text, "SC");
|
|
1354
1649
|
if (scIds.length > 0) {
|
|
1355
1650
|
issues.push(
|
|
1356
|
-
|
|
1651
|
+
issue5(
|
|
1357
1652
|
"QFAI-SPEC-003",
|
|
1358
1653
|
"Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
|
|
1359
1654
|
"warning",
|
|
@@ -1364,9 +1659,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1364
1659
|
);
|
|
1365
1660
|
}
|
|
1366
1661
|
for (const section of requiredSections) {
|
|
1367
|
-
if (!
|
|
1662
|
+
if (!parsed.sections.has(section)) {
|
|
1368
1663
|
issues.push(
|
|
1369
|
-
|
|
1664
|
+
issue5(
|
|
1370
1665
|
"QFAI-SPEC-004",
|
|
1371
1666
|
`\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
|
|
1372
1667
|
"error",
|
|
@@ -1378,40 +1673,39 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1378
1673
|
}
|
|
1379
1674
|
return issues;
|
|
1380
1675
|
}
|
|
1381
|
-
function
|
|
1382
|
-
const
|
|
1676
|
+
function issue5(code, message, severity, file, rule, refs) {
|
|
1677
|
+
const issue7 = {
|
|
1383
1678
|
code,
|
|
1384
1679
|
severity,
|
|
1385
1680
|
message
|
|
1386
1681
|
};
|
|
1387
1682
|
if (file) {
|
|
1388
|
-
|
|
1683
|
+
issue7.file = file;
|
|
1389
1684
|
}
|
|
1390
1685
|
if (rule) {
|
|
1391
|
-
|
|
1686
|
+
issue7.rule = rule;
|
|
1392
1687
|
}
|
|
1393
1688
|
if (refs && refs.length > 0) {
|
|
1394
|
-
|
|
1689
|
+
issue7.refs = refs;
|
|
1395
1690
|
}
|
|
1396
|
-
return
|
|
1691
|
+
return issue7;
|
|
1397
1692
|
}
|
|
1398
1693
|
|
|
1399
1694
|
// src/core/validators/traceability.ts
|
|
1400
|
-
var
|
|
1695
|
+
var import_promises11 = require("fs/promises");
|
|
1696
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1697
|
+
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1698
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1699
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1700
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
1701
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1401
1702
|
async function validateTraceability(root, config) {
|
|
1402
1703
|
const issues = [];
|
|
1403
|
-
const specsRoot = resolvePath(root, config, "
|
|
1404
|
-
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1405
|
-
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1704
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1406
1705
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1407
1706
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1408
1707
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
1409
|
-
const
|
|
1410
|
-
extensions: [".md"]
|
|
1411
|
-
});
|
|
1412
|
-
const scenarioFiles = await collectFiles(scenariosRoot, {
|
|
1413
|
-
extensions: [".feature"]
|
|
1414
|
-
});
|
|
1708
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1415
1709
|
const upstreamIds = /* @__PURE__ */ new Set();
|
|
1416
1710
|
const specIds = /* @__PURE__ */ new Set();
|
|
1417
1711
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
@@ -1423,11 +1717,13 @@ async function validateTraceability(root, config) {
|
|
|
1423
1717
|
const contractIndex = await buildContractIndex(root, config);
|
|
1424
1718
|
const contractIds = contractIndex.ids;
|
|
1425
1719
|
for (const file of specFiles) {
|
|
1426
|
-
const text = await (0,
|
|
1720
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1427
1721
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
|
|
1722
|
+
const parsed = parseSpec(text, file);
|
|
1723
|
+
if (parsed.specId) {
|
|
1724
|
+
specIds.add(parsed.specId);
|
|
1725
|
+
}
|
|
1726
|
+
const brIds = parsed.brs.map((br) => br.id);
|
|
1431
1727
|
brIds.forEach((id) => brIdsInSpecs.add(id));
|
|
1432
1728
|
const referencedContractIds = /* @__PURE__ */ new Set([
|
|
1433
1729
|
...extractIds(text, "UI"),
|
|
@@ -1439,7 +1735,7 @@ async function validateTraceability(root, config) {
|
|
|
1439
1735
|
);
|
|
1440
1736
|
if (unknownContractIds.length > 0) {
|
|
1441
1737
|
issues.push(
|
|
1442
|
-
|
|
1738
|
+
issue6(
|
|
1443
1739
|
"QFAI-TRACE-009",
|
|
1444
1740
|
`Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1445
1741
|
", "
|
|
@@ -1451,37 +1747,50 @@ async function validateTraceability(root, config) {
|
|
|
1451
1747
|
)
|
|
1452
1748
|
);
|
|
1453
1749
|
}
|
|
1454
|
-
|
|
1455
|
-
const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
|
|
1750
|
+
if (parsed.specId) {
|
|
1751
|
+
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
1456
1752
|
brIds.forEach((id) => current.add(id));
|
|
1457
|
-
specToBrIds.set(specId, current);
|
|
1753
|
+
specToBrIds.set(parsed.specId, current);
|
|
1458
1754
|
}
|
|
1459
1755
|
}
|
|
1460
|
-
for (const file of decisionFiles) {
|
|
1461
|
-
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1462
|
-
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1463
|
-
}
|
|
1464
1756
|
for (const file of scenarioFiles) {
|
|
1465
|
-
const text = await (0,
|
|
1757
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1466
1758
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1467
|
-
const
|
|
1468
|
-
const
|
|
1469
|
-
const
|
|
1470
|
-
const
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1759
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1760
|
+
const specIdsInScenario = /* @__PURE__ */ new Set();
|
|
1761
|
+
const brIds = /* @__PURE__ */ new Set();
|
|
1762
|
+
const scIds = /* @__PURE__ */ new Set();
|
|
1763
|
+
const scenarioIds = /* @__PURE__ */ new Set();
|
|
1764
|
+
for (const scenario of parsed.scenarios) {
|
|
1765
|
+
for (const tag of scenario.tags) {
|
|
1766
|
+
if (SPEC_TAG_RE2.test(tag)) {
|
|
1767
|
+
specIdsInScenario.add(tag);
|
|
1768
|
+
}
|
|
1769
|
+
if (BR_TAG_RE2.test(tag)) {
|
|
1770
|
+
brIds.add(tag);
|
|
1771
|
+
}
|
|
1772
|
+
if (SC_TAG_RE3.test(tag)) {
|
|
1773
|
+
scIds.add(tag);
|
|
1774
|
+
}
|
|
1775
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1776
|
+
scenarioIds.add(tag);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
const specIdsList = Array.from(specIdsInScenario);
|
|
1781
|
+
const brIdsList = Array.from(brIds);
|
|
1782
|
+
const scIdsList = Array.from(scIds);
|
|
1783
|
+
const scenarioIdsList = Array.from(scenarioIds);
|
|
1784
|
+
brIdsList.forEach((id) => brIdsInScenarios.add(id));
|
|
1785
|
+
scIdsList.forEach((id) => scIdsInScenarios.add(id));
|
|
1786
|
+
scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
|
|
1787
|
+
if (scenarioIdsList.length > 0) {
|
|
1788
|
+
scIdsList.forEach((id) => scWithContracts.add(id));
|
|
1480
1789
|
}
|
|
1481
|
-
const unknownSpecIds =
|
|
1790
|
+
const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
|
|
1482
1791
|
if (unknownSpecIds.length > 0) {
|
|
1483
1792
|
issues.push(
|
|
1484
|
-
|
|
1793
|
+
issue6(
|
|
1485
1794
|
"QFAI-TRACE-005",
|
|
1486
1795
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1487
1796
|
"error",
|
|
@@ -1491,10 +1800,10 @@ async function validateTraceability(root, config) {
|
|
|
1491
1800
|
)
|
|
1492
1801
|
);
|
|
1493
1802
|
}
|
|
1494
|
-
const unknownBrIds =
|
|
1803
|
+
const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
|
|
1495
1804
|
if (unknownBrIds.length > 0) {
|
|
1496
1805
|
issues.push(
|
|
1497
|
-
|
|
1806
|
+
issue6(
|
|
1498
1807
|
"QFAI-TRACE-006",
|
|
1499
1808
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1500
1809
|
"error",
|
|
@@ -1504,10 +1813,12 @@ async function validateTraceability(root, config) {
|
|
|
1504
1813
|
)
|
|
1505
1814
|
);
|
|
1506
1815
|
}
|
|
1507
|
-
const unknownContractIds =
|
|
1816
|
+
const unknownContractIds = scenarioIdsList.filter(
|
|
1817
|
+
(id) => !contractIds.has(id)
|
|
1818
|
+
);
|
|
1508
1819
|
if (unknownContractIds.length > 0) {
|
|
1509
1820
|
issues.push(
|
|
1510
|
-
|
|
1821
|
+
issue6(
|
|
1511
1822
|
"QFAI-TRACE-008",
|
|
1512
1823
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1513
1824
|
", "
|
|
@@ -1519,23 +1830,23 @@ async function validateTraceability(root, config) {
|
|
|
1519
1830
|
)
|
|
1520
1831
|
);
|
|
1521
1832
|
}
|
|
1522
|
-
if (
|
|
1833
|
+
if (specIdsList.length > 0) {
|
|
1523
1834
|
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
1524
|
-
for (const specId of
|
|
1835
|
+
for (const specId of specIdsList) {
|
|
1525
1836
|
const brIdsForSpec = specToBrIds.get(specId);
|
|
1526
1837
|
if (!brIdsForSpec) {
|
|
1527
1838
|
continue;
|
|
1528
1839
|
}
|
|
1529
1840
|
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
1530
1841
|
}
|
|
1531
|
-
const invalidBrIds =
|
|
1842
|
+
const invalidBrIds = brIdsList.filter((id) => !allowedBrIds.has(id));
|
|
1532
1843
|
if (invalidBrIds.length > 0) {
|
|
1533
1844
|
issues.push(
|
|
1534
|
-
|
|
1845
|
+
issue6(
|
|
1535
1846
|
"QFAI-TRACE-007",
|
|
1536
1847
|
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
1537
1848
|
", "
|
|
1538
|
-
)} (SPEC: ${
|
|
1849
|
+
)} (SPEC: ${specIdsList.join(", ")})`,
|
|
1539
1850
|
"error",
|
|
1540
1851
|
file,
|
|
1541
1852
|
"traceability.scenarioBrUnderSpec",
|
|
@@ -1547,7 +1858,7 @@ async function validateTraceability(root, config) {
|
|
|
1547
1858
|
}
|
|
1548
1859
|
if (upstreamIds.size === 0) {
|
|
1549
1860
|
return [
|
|
1550
|
-
|
|
1861
|
+
issue6(
|
|
1551
1862
|
"QFAI-TRACE-000",
|
|
1552
1863
|
"\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1553
1864
|
"info",
|
|
@@ -1562,7 +1873,7 @@ async function validateTraceability(root, config) {
|
|
|
1562
1873
|
);
|
|
1563
1874
|
if (orphanBrIds.length > 0) {
|
|
1564
1875
|
issues.push(
|
|
1565
|
-
|
|
1876
|
+
issue6(
|
|
1566
1877
|
"QFAI_TRACE_BR_ORPHAN",
|
|
1567
1878
|
`BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
|
|
1568
1879
|
"error",
|
|
@@ -1579,13 +1890,13 @@ async function validateTraceability(root, config) {
|
|
|
1579
1890
|
);
|
|
1580
1891
|
if (scWithoutContracts.length > 0) {
|
|
1581
1892
|
issues.push(
|
|
1582
|
-
|
|
1893
|
+
issue6(
|
|
1583
1894
|
"QFAI_TRACE_SC_NO_CONTRACT",
|
|
1584
1895
|
`SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
|
|
1585
1896
|
", "
|
|
1586
1897
|
)}`,
|
|
1587
1898
|
"error",
|
|
1588
|
-
|
|
1899
|
+
specsRoot,
|
|
1589
1900
|
"traceability.scMustTouchContracts",
|
|
1590
1901
|
scWithoutContracts
|
|
1591
1902
|
)
|
|
@@ -1599,11 +1910,11 @@ async function validateTraceability(root, config) {
|
|
|
1599
1910
|
);
|
|
1600
1911
|
if (orphanContracts.length > 0) {
|
|
1601
1912
|
issues.push(
|
|
1602
|
-
|
|
1913
|
+
issue6(
|
|
1603
1914
|
"QFAI_CONTRACT_ORPHAN",
|
|
1604
1915
|
`\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
1605
1916
|
"error",
|
|
1606
|
-
|
|
1917
|
+
specsRoot,
|
|
1607
1918
|
"traceability.allowOrphanContracts",
|
|
1608
1919
|
orphanContracts
|
|
1609
1920
|
)
|
|
@@ -1627,7 +1938,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1627
1938
|
const targetFiles = [...codeFiles, ...testFiles];
|
|
1628
1939
|
if (targetFiles.length === 0) {
|
|
1629
1940
|
issues.push(
|
|
1630
|
-
|
|
1941
|
+
issue6(
|
|
1631
1942
|
"QFAI-TRACE-001",
|
|
1632
1943
|
"\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1633
1944
|
"info",
|
|
@@ -1640,7 +1951,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1640
1951
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
1641
1952
|
let found = false;
|
|
1642
1953
|
for (const file of targetFiles) {
|
|
1643
|
-
const text = await (0,
|
|
1954
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1644
1955
|
if (pattern.test(text)) {
|
|
1645
1956
|
found = true;
|
|
1646
1957
|
break;
|
|
@@ -1648,7 +1959,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1648
1959
|
}
|
|
1649
1960
|
if (!found) {
|
|
1650
1961
|
issues.push(
|
|
1651
|
-
|
|
1962
|
+
issue6(
|
|
1652
1963
|
"QFAI-TRACE-002",
|
|
1653
1964
|
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
1654
1965
|
"warning",
|
|
@@ -1663,22 +1974,22 @@ function buildIdPattern(ids) {
|
|
|
1663
1974
|
const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
1664
1975
|
return new RegExp(`\\b(${escaped.join("|")})\\b`);
|
|
1665
1976
|
}
|
|
1666
|
-
function
|
|
1667
|
-
const
|
|
1977
|
+
function issue6(code, message, severity, file, rule, refs) {
|
|
1978
|
+
const issue7 = {
|
|
1668
1979
|
code,
|
|
1669
1980
|
severity,
|
|
1670
1981
|
message
|
|
1671
1982
|
};
|
|
1672
1983
|
if (file) {
|
|
1673
|
-
|
|
1984
|
+
issue7.file = file;
|
|
1674
1985
|
}
|
|
1675
1986
|
if (rule) {
|
|
1676
|
-
|
|
1987
|
+
issue7.rule = rule;
|
|
1677
1988
|
}
|
|
1678
1989
|
if (refs && refs.length > 0) {
|
|
1679
|
-
|
|
1990
|
+
issue7.refs = refs;
|
|
1680
1991
|
}
|
|
1681
|
-
return
|
|
1992
|
+
return issue7;
|
|
1682
1993
|
}
|
|
1683
1994
|
|
|
1684
1995
|
// src/core/validate.ts
|
|
@@ -1688,6 +1999,7 @@ async function validateProject(root, configResult) {
|
|
|
1688
1999
|
const issues = [
|
|
1689
2000
|
...configIssues,
|
|
1690
2001
|
...await validateSpecs(root, config),
|
|
2002
|
+
...await validateDeltas(root, config),
|
|
1691
2003
|
...await validateScenarios(root, config),
|
|
1692
2004
|
...await validateContracts(root, config),
|
|
1693
2005
|
...await validateDefinedIds(root, config),
|
|
@@ -1703,8 +2015,8 @@ async function validateProject(root, configResult) {
|
|
|
1703
2015
|
}
|
|
1704
2016
|
function countIssues(issues) {
|
|
1705
2017
|
return issues.reduce(
|
|
1706
|
-
(acc,
|
|
1707
|
-
acc[
|
|
2018
|
+
(acc, issue7) => {
|
|
2019
|
+
acc[issue7.severity] += 1;
|
|
1708
2020
|
return acc;
|
|
1709
2021
|
},
|
|
1710
2022
|
{ info: 0, warning: 0, error: 0 }
|
|
@@ -1717,21 +2029,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
1717
2029
|
const resolved = configResult ?? await loadConfig(root);
|
|
1718
2030
|
const config = resolved.config;
|
|
1719
2031
|
const configPath = resolved.configPath;
|
|
1720
|
-
const
|
|
1721
|
-
const
|
|
1722
|
-
const
|
|
1723
|
-
const
|
|
1724
|
-
const
|
|
1725
|
-
const dbRoot = resolvePath(root, config, "dataContractsDir");
|
|
2032
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2033
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2034
|
+
const apiRoot = import_node_path13.default.join(contractsRoot, "api");
|
|
2035
|
+
const uiRoot = import_node_path13.default.join(contractsRoot, "ui");
|
|
2036
|
+
const dbRoot = import_node_path13.default.join(contractsRoot, "db");
|
|
1726
2037
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1727
2038
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1728
|
-
const specFiles = await collectSpecFiles(
|
|
1729
|
-
const scenarioFiles = await
|
|
1730
|
-
extensions: [".feature"]
|
|
1731
|
-
});
|
|
1732
|
-
const decisionFiles = await collectFiles(decisionsRoot, {
|
|
1733
|
-
extensions: [".md"]
|
|
1734
|
-
});
|
|
2039
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
2040
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1735
2041
|
const {
|
|
1736
2042
|
api: apiFiles,
|
|
1737
2043
|
ui: uiFiles,
|
|
@@ -1740,7 +2046,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1740
2046
|
const idsByPrefix = await collectIds([
|
|
1741
2047
|
...specFiles,
|
|
1742
2048
|
...scenarioFiles,
|
|
1743
|
-
...decisionFiles,
|
|
1744
2049
|
...apiFiles,
|
|
1745
2050
|
...uiFiles,
|
|
1746
2051
|
...dbFiles
|
|
@@ -1765,7 +2070,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1765
2070
|
summary: {
|
|
1766
2071
|
specs: specFiles.length,
|
|
1767
2072
|
scenarios: scenarioFiles.length,
|
|
1768
|
-
decisions: decisionFiles.length,
|
|
1769
2073
|
contracts: {
|
|
1770
2074
|
api: apiFiles.length,
|
|
1771
2075
|
ui: uiFiles.length,
|
|
@@ -1799,7 +2103,6 @@ function formatReportMarkdown(data) {
|
|
|
1799
2103
|
lines.push("## \u6982\u8981");
|
|
1800
2104
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
1801
2105
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
1802
|
-
lines.push(`- decisions: ${data.summary.decisions}`);
|
|
1803
2106
|
lines.push(
|
|
1804
2107
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
1805
2108
|
);
|
|
@@ -1875,7 +2178,7 @@ async function collectIds(files) {
|
|
|
1875
2178
|
DATA: /* @__PURE__ */ new Set()
|
|
1876
2179
|
};
|
|
1877
2180
|
for (const file of files) {
|
|
1878
|
-
const text = await (0,
|
|
2181
|
+
const text = await (0, import_promises12.readFile)(file, "utf-8");
|
|
1879
2182
|
for (const prefix of ID_PREFIXES2) {
|
|
1880
2183
|
const ids = extractIds(text, prefix);
|
|
1881
2184
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -1893,7 +2196,7 @@ async function collectIds(files) {
|
|
|
1893
2196
|
async function collectUpstreamIds(files) {
|
|
1894
2197
|
const ids = /* @__PURE__ */ new Set();
|
|
1895
2198
|
for (const file of files) {
|
|
1896
|
-
const text = await (0,
|
|
2199
|
+
const text = await (0, import_promises12.readFile)(file, "utf-8");
|
|
1897
2200
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
1898
2201
|
}
|
|
1899
2202
|
return ids;
|
|
@@ -1914,7 +2217,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
1914
2217
|
}
|
|
1915
2218
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
1916
2219
|
for (const file of targetFiles) {
|
|
1917
|
-
const text = await (0,
|
|
2220
|
+
const text = await (0, import_promises12.readFile)(file, "utf-8");
|
|
1918
2221
|
if (pattern.test(text)) {
|
|
1919
2222
|
return true;
|
|
1920
2223
|
}
|
|
@@ -1936,20 +2239,20 @@ function toSortedArray(values) {
|
|
|
1936
2239
|
}
|
|
1937
2240
|
function buildHotspots(issues) {
|
|
1938
2241
|
const map = /* @__PURE__ */ new Map();
|
|
1939
|
-
for (const
|
|
1940
|
-
if (!
|
|
2242
|
+
for (const issue7 of issues) {
|
|
2243
|
+
if (!issue7.file) {
|
|
1941
2244
|
continue;
|
|
1942
2245
|
}
|
|
1943
|
-
const current = map.get(
|
|
1944
|
-
file:
|
|
2246
|
+
const current = map.get(issue7.file) ?? {
|
|
2247
|
+
file: issue7.file,
|
|
1945
2248
|
total: 0,
|
|
1946
2249
|
error: 0,
|
|
1947
2250
|
warning: 0,
|
|
1948
2251
|
info: 0
|
|
1949
2252
|
};
|
|
1950
2253
|
current.total += 1;
|
|
1951
|
-
current[
|
|
1952
|
-
map.set(
|
|
2254
|
+
current[issue7.severity] += 1;
|
|
2255
|
+
map.set(issue7.file, current);
|
|
1953
2256
|
}
|
|
1954
2257
|
return Array.from(map.values()).sort(
|
|
1955
2258
|
(a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
|
|
@@ -1958,21 +2261,22 @@ function buildHotspots(issues) {
|
|
|
1958
2261
|
|
|
1959
2262
|
// src/cli/commands/report.ts
|
|
1960
2263
|
async function runReport(options) {
|
|
1961
|
-
const root =
|
|
2264
|
+
const root = import_node_path14.default.resolve(options.root);
|
|
1962
2265
|
const configResult = await loadConfig(root);
|
|
1963
|
-
const input =
|
|
1964
|
-
const inputPath =
|
|
2266
|
+
const input = configResult.config.output.validateJsonPath;
|
|
2267
|
+
const inputPath = import_node_path14.default.isAbsolute(input) ? input : import_node_path14.default.resolve(root, input);
|
|
1965
2268
|
let validation;
|
|
1966
2269
|
try {
|
|
1967
2270
|
validation = await readValidationResult(inputPath);
|
|
1968
2271
|
} catch (err) {
|
|
1969
|
-
if (
|
|
2272
|
+
if (isMissingFileError2(err)) {
|
|
1970
2273
|
error(
|
|
1971
2274
|
[
|
|
1972
2275
|
`qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
|
|
1973
2276
|
"",
|
|
1974
|
-
"\u307E\u305A validate
|
|
1975
|
-
|
|
2277
|
+
"\u307E\u305A qfai validate \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
|
|
2278
|
+
" qfai validate",
|
|
2279
|
+
"\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/out/validate.json\uFF09",
|
|
1976
2280
|
"",
|
|
1977
2281
|
"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"
|
|
1978
2282
|
].join("\n")
|
|
@@ -1984,11 +2288,12 @@ async function runReport(options) {
|
|
|
1984
2288
|
}
|
|
1985
2289
|
const data = await createReportData(root, validation, configResult);
|
|
1986
2290
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
1987
|
-
const
|
|
2291
|
+
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
2292
|
+
const defaultOut = options.format === "json" ? import_node_path14.default.join(outRoot, "report.json") : import_node_path14.default.join(outRoot, "report.md");
|
|
1988
2293
|
const out = options.outPath ?? defaultOut;
|
|
1989
|
-
const outPath =
|
|
1990
|
-
await (0,
|
|
1991
|
-
await (0,
|
|
2294
|
+
const outPath = import_node_path14.default.isAbsolute(out) ? out : import_node_path14.default.resolve(root, out);
|
|
2295
|
+
await (0, import_promises13.mkdir)(import_node_path14.default.dirname(outPath), { recursive: true });
|
|
2296
|
+
await (0, import_promises13.writeFile)(outPath, `${output}
|
|
1992
2297
|
`, "utf-8");
|
|
1993
2298
|
info(
|
|
1994
2299
|
`report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
|
|
@@ -1996,7 +2301,7 @@ async function runReport(options) {
|
|
|
1996
2301
|
info(`wrote report: ${outPath}`);
|
|
1997
2302
|
}
|
|
1998
2303
|
async function readValidationResult(inputPath) {
|
|
1999
|
-
const raw = await (0,
|
|
2304
|
+
const raw = await (0, import_promises13.readFile)(inputPath, "utf-8");
|
|
2000
2305
|
const parsed = JSON.parse(raw);
|
|
2001
2306
|
if (!isValidationResult(parsed)) {
|
|
2002
2307
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
@@ -2028,7 +2333,7 @@ function isValidationResult(value) {
|
|
|
2028
2333
|
}
|
|
2029
2334
|
return typeof counts.info === "number" && typeof counts.warning === "number" && typeof counts.error === "number";
|
|
2030
2335
|
}
|
|
2031
|
-
function
|
|
2336
|
+
function isMissingFileError2(error2) {
|
|
2032
2337
|
if (!error2 || typeof error2 !== "object") {
|
|
2033
2338
|
return false;
|
|
2034
2339
|
}
|
|
@@ -2037,8 +2342,8 @@ function isMissingFileError(error2) {
|
|
|
2037
2342
|
}
|
|
2038
2343
|
|
|
2039
2344
|
// src/cli/commands/validate.ts
|
|
2040
|
-
var
|
|
2041
|
-
var
|
|
2345
|
+
var import_promises14 = require("fs/promises");
|
|
2346
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
2042
2347
|
|
|
2043
2348
|
// src/cli/lib/failOn.ts
|
|
2044
2349
|
function shouldFail(result, failOn) {
|
|
@@ -2053,24 +2358,17 @@ function shouldFail(result, failOn) {
|
|
|
2053
2358
|
|
|
2054
2359
|
// src/cli/commands/validate.ts
|
|
2055
2360
|
async function runValidate(options) {
|
|
2056
|
-
const root =
|
|
2361
|
+
const root = import_node_path15.default.resolve(options.root);
|
|
2057
2362
|
const configResult = await loadConfig(root);
|
|
2058
2363
|
const result = await validateProject(root, configResult);
|
|
2059
|
-
const format = options.format ??
|
|
2060
|
-
const explicitJsonPath = options.jsonPath;
|
|
2364
|
+
const format = options.format ?? "text";
|
|
2061
2365
|
if (format === "text") {
|
|
2062
2366
|
emitText(result);
|
|
2063
2367
|
}
|
|
2064
2368
|
if (format === "github") {
|
|
2065
2369
|
result.issues.forEach(emitGitHub);
|
|
2066
2370
|
}
|
|
2067
|
-
|
|
2068
|
-
if (shouldWriteJson) {
|
|
2069
|
-
const jsonPath = format === "json" ? options.jsonPath ?? configResult.config.output.jsonPath : explicitJsonPath;
|
|
2070
|
-
if (jsonPath) {
|
|
2071
|
-
await emitJson(result, root, jsonPath);
|
|
2072
|
-
}
|
|
2073
|
-
}
|
|
2371
|
+
await emitJson(result, root, configResult.config.output.validateJsonPath);
|
|
2074
2372
|
const failOn = resolveFailOn(options, configResult.config.validation.failOn);
|
|
2075
2373
|
return shouldFail(result, failOn) ? 1 : 0;
|
|
2076
2374
|
}
|
|
@@ -2097,21 +2395,21 @@ function emitText(result) {
|
|
|
2097
2395
|
`
|
|
2098
2396
|
);
|
|
2099
2397
|
}
|
|
2100
|
-
function emitGitHub(
|
|
2101
|
-
const level =
|
|
2102
|
-
const file =
|
|
2103
|
-
const line =
|
|
2104
|
-
const column =
|
|
2398
|
+
function emitGitHub(issue7) {
|
|
2399
|
+
const level = issue7.severity === "error" ? "error" : issue7.severity === "warning" ? "warning" : "notice";
|
|
2400
|
+
const file = issue7.file ? `file=${issue7.file}` : "";
|
|
2401
|
+
const line = issue7.loc?.line ? `,line=${issue7.loc.line}` : "";
|
|
2402
|
+
const column = issue7.loc?.column ? `,col=${issue7.loc.column}` : "";
|
|
2105
2403
|
const location = file ? ` ${file}${line}${column}` : "";
|
|
2106
2404
|
process.stdout.write(
|
|
2107
|
-
`::${level}${location}::${
|
|
2405
|
+
`::${level}${location}::${issue7.code}: ${issue7.message}
|
|
2108
2406
|
`
|
|
2109
2407
|
);
|
|
2110
2408
|
}
|
|
2111
2409
|
async function emitJson(result, root, jsonPath) {
|
|
2112
|
-
const abs =
|
|
2113
|
-
await (0,
|
|
2114
|
-
await (0,
|
|
2410
|
+
const abs = import_node_path15.default.isAbsolute(jsonPath) ? jsonPath : import_node_path15.default.resolve(root, jsonPath);
|
|
2411
|
+
await (0, import_promises14.mkdir)(import_node_path15.default.dirname(abs), { recursive: true });
|
|
2412
|
+
await (0, import_promises14.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
2115
2413
|
`, "utf-8");
|
|
2116
2414
|
}
|
|
2117
2415
|
|
|
@@ -2171,15 +2469,6 @@ function parseArgs(argv, cwd) {
|
|
|
2171
2469
|
i += 1;
|
|
2172
2470
|
break;
|
|
2173
2471
|
}
|
|
2174
|
-
case "--json-path":
|
|
2175
|
-
{
|
|
2176
|
-
const next = args[i + 1];
|
|
2177
|
-
if (next) {
|
|
2178
|
-
options.jsonPath = next;
|
|
2179
|
-
}
|
|
2180
|
-
}
|
|
2181
|
-
i += 1;
|
|
2182
|
-
break;
|
|
2183
2472
|
case "--out":
|
|
2184
2473
|
{
|
|
2185
2474
|
const next = args[i + 1];
|
|
@@ -2210,7 +2499,7 @@ function applyFormatOption(command, value, options) {
|
|
|
2210
2499
|
return;
|
|
2211
2500
|
}
|
|
2212
2501
|
if (command === "validate") {
|
|
2213
|
-
if (value === "text" || value === "
|
|
2502
|
+
if (value === "text" || value === "github") {
|
|
2214
2503
|
options.validateFormat = value;
|
|
2215
2504
|
}
|
|
2216
2505
|
return;
|
|
@@ -2218,7 +2507,7 @@ function applyFormatOption(command, value, options) {
|
|
|
2218
2507
|
if (value === "md" || value === "json") {
|
|
2219
2508
|
options.reportFormat = value;
|
|
2220
2509
|
}
|
|
2221
|
-
if (value === "text" || value === "
|
|
2510
|
+
if (value === "text" || value === "github") {
|
|
2222
2511
|
options.validateFormat = value;
|
|
2223
2512
|
}
|
|
2224
2513
|
}
|
|
@@ -2244,15 +2533,13 @@ async function run(argv, cwd) {
|
|
|
2244
2533
|
root: options.root,
|
|
2245
2534
|
strict: options.strict,
|
|
2246
2535
|
format: options.validateFormat,
|
|
2247
|
-
...options.failOn !== void 0 ? { failOn: options.failOn } : {}
|
|
2248
|
-
...options.jsonPath !== void 0 ? { jsonPath: options.jsonPath } : {}
|
|
2536
|
+
...options.failOn !== void 0 ? { failOn: options.failOn } : {}
|
|
2249
2537
|
});
|
|
2250
2538
|
return;
|
|
2251
2539
|
case "report":
|
|
2252
2540
|
await runReport({
|
|
2253
2541
|
root: options.root,
|
|
2254
2542
|
format: options.reportFormat,
|
|
2255
|
-
...options.jsonPath !== void 0 ? { jsonPath: options.jsonPath } : {},
|
|
2256
2543
|
...options.reportOut !== void 0 ? { outPath: options.reportOut } : {}
|
|
2257
2544
|
});
|
|
2258
2545
|
return;
|
|
@@ -2274,14 +2561,13 @@ Options:
|
|
|
2274
2561
|
--root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
|
|
2275
2562
|
--dir <path> init \u306E\u51FA\u529B\u5148
|
|
2276
2563
|
--force \u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u3092\u4E0A\u66F8\u304D
|
|
2277
|
-
--yes init: \
|
|
2564
|
+
--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
|
|
2278
2565
|
--dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
|
|
2279
|
-
--format <text|
|
|
2566
|
+
--format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
|
|
2280
2567
|
--format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
|
|
2281
|
-
--strict
|
|
2568
|
+
--strict validate: warning \u4EE5\u4E0A\u3067 exit 1
|
|
2282
2569
|
--fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
|
|
2283
|
-
--
|
|
2284
|
-
--out <path> report: \u51FA\u529B\u5148
|
|
2570
|
+
--out <path> report: \u51FA\u529B\u5148
|
|
2285
2571
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|
|
2286
2572
|
`;
|
|
2287
2573
|
}
|