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/index.mjs
CHANGED
|
@@ -4,13 +4,11 @@ import path from "path";
|
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
5
|
var defaultConfig = {
|
|
6
6
|
paths: {
|
|
7
|
-
specDir: ".qfai/spec",
|
|
8
|
-
decisionsDir: ".qfai/spec/decisions",
|
|
9
|
-
scenariosDir: ".qfai/spec/scenarios",
|
|
10
7
|
contractsDir: ".qfai/contracts",
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
specsDir: ".qfai/specs",
|
|
9
|
+
rulesDir: ".qfai/rules",
|
|
10
|
+
outDir: ".qfai/out",
|
|
11
|
+
promptsDir: ".qfai/prompts",
|
|
14
12
|
srcDir: "src",
|
|
15
13
|
testsDir: "tests"
|
|
16
14
|
},
|
|
@@ -35,8 +33,7 @@ var defaultConfig = {
|
|
|
35
33
|
}
|
|
36
34
|
},
|
|
37
35
|
output: {
|
|
38
|
-
|
|
39
|
-
jsonPath: ".qfai/out/validate.json"
|
|
36
|
+
validateJsonPath: ".qfai/out/validate.json"
|
|
40
37
|
}
|
|
41
38
|
};
|
|
42
39
|
function getConfigPath(root) {
|
|
@@ -85,27 +82,6 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
85
82
|
return base;
|
|
86
83
|
}
|
|
87
84
|
return {
|
|
88
|
-
specDir: readString(
|
|
89
|
-
raw.specDir,
|
|
90
|
-
base.specDir,
|
|
91
|
-
"paths.specDir",
|
|
92
|
-
configPath,
|
|
93
|
-
issues
|
|
94
|
-
),
|
|
95
|
-
decisionsDir: readString(
|
|
96
|
-
raw.decisionsDir,
|
|
97
|
-
base.decisionsDir,
|
|
98
|
-
"paths.decisionsDir",
|
|
99
|
-
configPath,
|
|
100
|
-
issues
|
|
101
|
-
),
|
|
102
|
-
scenariosDir: readString(
|
|
103
|
-
raw.scenariosDir,
|
|
104
|
-
base.scenariosDir,
|
|
105
|
-
"paths.scenariosDir",
|
|
106
|
-
configPath,
|
|
107
|
-
issues
|
|
108
|
-
),
|
|
109
85
|
contractsDir: readString(
|
|
110
86
|
raw.contractsDir,
|
|
111
87
|
base.contractsDir,
|
|
@@ -113,24 +89,31 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
113
89
|
configPath,
|
|
114
90
|
issues
|
|
115
91
|
),
|
|
116
|
-
|
|
117
|
-
raw.
|
|
118
|
-
base.
|
|
119
|
-
"paths.
|
|
92
|
+
specsDir: readString(
|
|
93
|
+
raw.specsDir,
|
|
94
|
+
base.specsDir,
|
|
95
|
+
"paths.specsDir",
|
|
120
96
|
configPath,
|
|
121
97
|
issues
|
|
122
98
|
),
|
|
123
|
-
|
|
124
|
-
raw.
|
|
125
|
-
base.
|
|
126
|
-
"paths.
|
|
99
|
+
rulesDir: readString(
|
|
100
|
+
raw.rulesDir,
|
|
101
|
+
base.rulesDir,
|
|
102
|
+
"paths.rulesDir",
|
|
127
103
|
configPath,
|
|
128
104
|
issues
|
|
129
105
|
),
|
|
130
|
-
|
|
131
|
-
raw.
|
|
132
|
-
base.
|
|
133
|
-
"paths.
|
|
106
|
+
outDir: readString(
|
|
107
|
+
raw.outDir,
|
|
108
|
+
base.outDir,
|
|
109
|
+
"paths.outDir",
|
|
110
|
+
configPath,
|
|
111
|
+
issues
|
|
112
|
+
),
|
|
113
|
+
promptsDir: readString(
|
|
114
|
+
raw.promptsDir,
|
|
115
|
+
base.promptsDir,
|
|
116
|
+
"paths.promptsDir",
|
|
134
117
|
configPath,
|
|
135
118
|
issues
|
|
136
119
|
),
|
|
@@ -253,17 +236,10 @@ function normalizeOutput(raw, configPath, issues) {
|
|
|
253
236
|
return base;
|
|
254
237
|
}
|
|
255
238
|
return {
|
|
256
|
-
|
|
257
|
-
raw.
|
|
258
|
-
base.
|
|
259
|
-
"output.
|
|
260
|
-
configPath,
|
|
261
|
-
issues
|
|
262
|
-
),
|
|
263
|
-
jsonPath: readString(
|
|
264
|
-
raw.jsonPath,
|
|
265
|
-
base.jsonPath,
|
|
266
|
-
"output.jsonPath",
|
|
239
|
+
validateJsonPath: readString(
|
|
240
|
+
raw.validateJsonPath,
|
|
241
|
+
base.validateJsonPath,
|
|
242
|
+
"output.validateJsonPath",
|
|
267
243
|
configPath,
|
|
268
244
|
issues
|
|
269
245
|
)
|
|
@@ -330,20 +306,6 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
|
330
306
|
}
|
|
331
307
|
return fallback;
|
|
332
308
|
}
|
|
333
|
-
function readOutputFormat(value, fallback, label, configPath, issues) {
|
|
334
|
-
if (value === "text" || value === "json" || value === "github") {
|
|
335
|
-
return value;
|
|
336
|
-
}
|
|
337
|
-
if (value !== void 0) {
|
|
338
|
-
issues.push(
|
|
339
|
-
configIssue(
|
|
340
|
-
configPath,
|
|
341
|
-
`${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
342
|
-
)
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
return fallback;
|
|
346
|
-
}
|
|
347
309
|
function configIssue(file, message) {
|
|
348
310
|
return {
|
|
349
311
|
code: "QFAI_CONFIG_INVALID",
|
|
@@ -423,7 +385,8 @@ function isValidId(value, prefix) {
|
|
|
423
385
|
}
|
|
424
386
|
|
|
425
387
|
// src/core/report.ts
|
|
426
|
-
import { readFile as
|
|
388
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
389
|
+
import path10 from "path";
|
|
427
390
|
|
|
428
391
|
// src/core/discovery.ts
|
|
429
392
|
import path3 from "path";
|
|
@@ -484,10 +447,24 @@ async function exists(target) {
|
|
|
484
447
|
}
|
|
485
448
|
|
|
486
449
|
// src/core/discovery.ts
|
|
487
|
-
var
|
|
488
|
-
async function
|
|
489
|
-
const files = await collectFiles(
|
|
490
|
-
|
|
450
|
+
var SPEC_PACK_DIR_PATTERN = /^spec-\d{3}$/;
|
|
451
|
+
async function collectSpecPackDirs(specsRoot) {
|
|
452
|
+
const files = await collectFiles(specsRoot, { extensions: [".md"] });
|
|
453
|
+
const packs = /* @__PURE__ */ new Set();
|
|
454
|
+
for (const file of files) {
|
|
455
|
+
if (isSpecPackFile(file, "spec.md")) {
|
|
456
|
+
packs.add(path3.dirname(file));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return Array.from(packs).sort();
|
|
460
|
+
}
|
|
461
|
+
async function collectSpecFiles(specsRoot) {
|
|
462
|
+
const files = await collectFiles(specsRoot, { extensions: [".md"] });
|
|
463
|
+
return files.filter((file) => isSpecPackFile(file, "spec.md"));
|
|
464
|
+
}
|
|
465
|
+
async function collectScenarioFiles(specsRoot) {
|
|
466
|
+
const files = await collectFiles(specsRoot, { extensions: [".md"] });
|
|
467
|
+
return files.filter((file) => isSpecPackFile(file, "scenario.md"));
|
|
491
468
|
}
|
|
492
469
|
async function collectUiContractFiles(uiRoot) {
|
|
493
470
|
return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
@@ -506,9 +483,12 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
|
506
483
|
]);
|
|
507
484
|
return { ui, api, db };
|
|
508
485
|
}
|
|
509
|
-
function
|
|
510
|
-
|
|
511
|
-
|
|
486
|
+
function isSpecPackFile(filePath, baseName) {
|
|
487
|
+
if (path3.basename(filePath).toLowerCase() !== baseName) {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
const dirName = path3.basename(path3.dirname(filePath)).toLowerCase();
|
|
491
|
+
return SPEC_PACK_DIR_PATTERN.test(dirName);
|
|
512
492
|
}
|
|
513
493
|
|
|
514
494
|
// src/core/types.ts
|
|
@@ -519,8 +499,8 @@ import { readFile as readFile2 } from "fs/promises";
|
|
|
519
499
|
import path4 from "path";
|
|
520
500
|
import { fileURLToPath } from "url";
|
|
521
501
|
async function resolveToolVersion() {
|
|
522
|
-
if ("0.
|
|
523
|
-
return "0.
|
|
502
|
+
if ("0.3.1".length > 0) {
|
|
503
|
+
return "0.3.1";
|
|
524
504
|
}
|
|
525
505
|
try {
|
|
526
506
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -540,6 +520,7 @@ function resolvePackageJsonPath() {
|
|
|
540
520
|
|
|
541
521
|
// src/core/validators/contracts.ts
|
|
542
522
|
import { readFile as readFile3 } from "fs/promises";
|
|
523
|
+
import path6 from "path";
|
|
543
524
|
|
|
544
525
|
// src/core/contracts.ts
|
|
545
526
|
import path5 from "path";
|
|
@@ -595,19 +576,10 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
595
576
|
];
|
|
596
577
|
async function validateContracts(root, config) {
|
|
597
578
|
const issues = [];
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
);
|
|
601
|
-
issues.push(
|
|
602
|
-
...await validateApiContracts(
|
|
603
|
-
resolvePath(root, config, "apiContractsDir")
|
|
604
|
-
)
|
|
605
|
-
);
|
|
606
|
-
issues.push(
|
|
607
|
-
...await validateDataContracts(
|
|
608
|
-
resolvePath(root, config, "dataContractsDir")
|
|
609
|
-
)
|
|
610
|
-
);
|
|
579
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
580
|
+
issues.push(...await validateUiContracts(path6.join(contractsRoot, "ui")));
|
|
581
|
+
issues.push(...await validateApiContracts(path6.join(contractsRoot, "api")));
|
|
582
|
+
issues.push(...await validateDataContracts(path6.join(contractsRoot, "db")));
|
|
611
583
|
return issues;
|
|
612
584
|
}
|
|
613
585
|
async function validateUiContracts(uiRoot) {
|
|
@@ -823,33 +795,125 @@ function formatError2(error) {
|
|
|
823
795
|
return String(error);
|
|
824
796
|
}
|
|
825
797
|
function issue(code, message, severity, file, rule, refs) {
|
|
826
|
-
const
|
|
798
|
+
const issue7 = {
|
|
827
799
|
code,
|
|
828
800
|
severity,
|
|
829
801
|
message
|
|
830
802
|
};
|
|
831
803
|
if (file) {
|
|
832
|
-
|
|
804
|
+
issue7.file = file;
|
|
833
805
|
}
|
|
834
806
|
if (rule) {
|
|
835
|
-
|
|
807
|
+
issue7.rule = rule;
|
|
836
808
|
}
|
|
837
809
|
if (refs && refs.length > 0) {
|
|
838
|
-
|
|
810
|
+
issue7.refs = refs;
|
|
839
811
|
}
|
|
840
|
-
return
|
|
812
|
+
return issue7;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// src/core/validators/delta.ts
|
|
816
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
817
|
+
import path7 from "path";
|
|
818
|
+
var SECTION_RE = /^##\s+変更区分/m;
|
|
819
|
+
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
820
|
+
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
821
|
+
var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
|
|
822
|
+
var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
|
|
823
|
+
async function validateDeltas(root, config) {
|
|
824
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
825
|
+
const packs = await collectSpecPackDirs(specsRoot);
|
|
826
|
+
if (packs.length === 0) {
|
|
827
|
+
return [];
|
|
828
|
+
}
|
|
829
|
+
const issues = [];
|
|
830
|
+
for (const pack of packs) {
|
|
831
|
+
const deltaPath = path7.join(pack, "delta.md");
|
|
832
|
+
let text;
|
|
833
|
+
try {
|
|
834
|
+
text = await readFile4(deltaPath, "utf-8");
|
|
835
|
+
} catch (error) {
|
|
836
|
+
if (isMissingFileError(error)) {
|
|
837
|
+
issues.push(
|
|
838
|
+
issue2(
|
|
839
|
+
"QFAI-DELTA-001",
|
|
840
|
+
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
841
|
+
"error",
|
|
842
|
+
deltaPath,
|
|
843
|
+
"delta.exists"
|
|
844
|
+
)
|
|
845
|
+
);
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
throw error;
|
|
849
|
+
}
|
|
850
|
+
const hasSection = SECTION_RE.test(text);
|
|
851
|
+
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
852
|
+
const hasChange = CHANGE_LINE_RE.test(text);
|
|
853
|
+
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
854
|
+
issues.push(
|
|
855
|
+
issue2(
|
|
856
|
+
"QFAI-DELTA-002",
|
|
857
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
858
|
+
"error",
|
|
859
|
+
deltaPath,
|
|
860
|
+
"delta.section"
|
|
861
|
+
)
|
|
862
|
+
);
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
866
|
+
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
867
|
+
if (compatibilityChecked === changeChecked) {
|
|
868
|
+
issues.push(
|
|
869
|
+
issue2(
|
|
870
|
+
"QFAI-DELTA-003",
|
|
871
|
+
"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",
|
|
872
|
+
"error",
|
|
873
|
+
deltaPath,
|
|
874
|
+
"delta.classification"
|
|
875
|
+
)
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return issues;
|
|
880
|
+
}
|
|
881
|
+
function isMissingFileError(error) {
|
|
882
|
+
if (!error || typeof error !== "object") {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
return error.code === "ENOENT";
|
|
886
|
+
}
|
|
887
|
+
function issue2(code, message, severity, file, rule, refs) {
|
|
888
|
+
const issue7 = {
|
|
889
|
+
code,
|
|
890
|
+
severity,
|
|
891
|
+
message
|
|
892
|
+
};
|
|
893
|
+
if (file) {
|
|
894
|
+
issue7.file = file;
|
|
895
|
+
}
|
|
896
|
+
if (rule) {
|
|
897
|
+
issue7.rule = rule;
|
|
898
|
+
}
|
|
899
|
+
if (refs && refs.length > 0) {
|
|
900
|
+
issue7.refs = refs;
|
|
901
|
+
}
|
|
902
|
+
return issue7;
|
|
841
903
|
}
|
|
842
904
|
|
|
843
905
|
// src/core/validators/ids.ts
|
|
844
|
-
import { readFile as
|
|
845
|
-
import
|
|
906
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
907
|
+
import path9 from "path";
|
|
846
908
|
|
|
847
909
|
// src/core/contractIndex.ts
|
|
848
|
-
import { readFile as
|
|
910
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
911
|
+
import path8 from "path";
|
|
849
912
|
async function buildContractIndex(root, config) {
|
|
850
|
-
const
|
|
851
|
-
const
|
|
852
|
-
const
|
|
913
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
914
|
+
const uiRoot = path8.join(contractsRoot, "ui");
|
|
915
|
+
const apiRoot = path8.join(contractsRoot, "api");
|
|
916
|
+
const dataRoot = path8.join(contractsRoot, "db");
|
|
853
917
|
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
854
918
|
collectUiContractFiles(uiRoot),
|
|
855
919
|
collectApiContractFiles(apiRoot),
|
|
@@ -868,7 +932,7 @@ async function buildContractIndex(root, config) {
|
|
|
868
932
|
}
|
|
869
933
|
async function indexUiContracts(files, index) {
|
|
870
934
|
for (const file of files) {
|
|
871
|
-
const text = await
|
|
935
|
+
const text = await readFile5(file, "utf-8");
|
|
872
936
|
try {
|
|
873
937
|
const doc = parseStructuredContract(file, text);
|
|
874
938
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -880,7 +944,7 @@ async function indexUiContracts(files, index) {
|
|
|
880
944
|
}
|
|
881
945
|
async function indexApiContracts(files, index) {
|
|
882
946
|
for (const file of files) {
|
|
883
|
-
const text = await
|
|
947
|
+
const text = await readFile5(file, "utf-8");
|
|
884
948
|
try {
|
|
885
949
|
const doc = parseStructuredContract(file, text);
|
|
886
950
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -892,7 +956,7 @@ async function indexApiContracts(files, index) {
|
|
|
892
956
|
}
|
|
893
957
|
async function indexDataContracts(files, index) {
|
|
894
958
|
for (const file of files) {
|
|
895
|
-
const text = await
|
|
959
|
+
const text = await readFile5(file, "utf-8");
|
|
896
960
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
897
961
|
}
|
|
898
962
|
}
|
|
@@ -903,15 +967,191 @@ function record(index, id, file) {
|
|
|
903
967
|
index.idToFiles.set(id, current);
|
|
904
968
|
}
|
|
905
969
|
|
|
970
|
+
// src/core/parse/gherkin.ts
|
|
971
|
+
var FEATURE_RE = /^\s*Feature:\s+/;
|
|
972
|
+
var SCENARIO_RE = /^\s*Scenario(?: Outline)?:\s*(.+)\s*$/;
|
|
973
|
+
var TAG_LINE_RE = /^\s*@/;
|
|
974
|
+
function parseTags(line) {
|
|
975
|
+
return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
|
|
976
|
+
}
|
|
977
|
+
function parseGherkinFeature(text, file) {
|
|
978
|
+
const lines = text.split(/\r?\n/);
|
|
979
|
+
const scenarios = [];
|
|
980
|
+
let featurePresent = false;
|
|
981
|
+
let featureTags = [];
|
|
982
|
+
let pendingTags = [];
|
|
983
|
+
let current = null;
|
|
984
|
+
const flush = () => {
|
|
985
|
+
if (!current) return;
|
|
986
|
+
scenarios.push({
|
|
987
|
+
...current,
|
|
988
|
+
body: current.body.trim()
|
|
989
|
+
});
|
|
990
|
+
current = null;
|
|
991
|
+
};
|
|
992
|
+
for (let i = 0; i < lines.length; i++) {
|
|
993
|
+
const line = lines[i] ?? "";
|
|
994
|
+
const trimmed = line.trim();
|
|
995
|
+
if (TAG_LINE_RE.test(trimmed)) {
|
|
996
|
+
pendingTags.push(...parseTags(trimmed));
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (FEATURE_RE.test(trimmed)) {
|
|
1000
|
+
featurePresent = true;
|
|
1001
|
+
featureTags = [...pendingTags];
|
|
1002
|
+
pendingTags = [];
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
const match = trimmed.match(SCENARIO_RE);
|
|
1006
|
+
if (match) {
|
|
1007
|
+
const scenarioName = match[1]?.trim();
|
|
1008
|
+
if (!scenarioName) {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
flush();
|
|
1012
|
+
current = {
|
|
1013
|
+
name: scenarioName,
|
|
1014
|
+
line: i + 1,
|
|
1015
|
+
tags: [...featureTags, ...pendingTags],
|
|
1016
|
+
body: ""
|
|
1017
|
+
};
|
|
1018
|
+
pendingTags = [];
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
if (current) {
|
|
1022
|
+
current.body += `${line}
|
|
1023
|
+
`;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
flush();
|
|
1027
|
+
return { file, featurePresent, scenarios };
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/core/parse/markdown.ts
|
|
1031
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1032
|
+
function parseHeadings(md) {
|
|
1033
|
+
const lines = md.split(/\r?\n/);
|
|
1034
|
+
const headings = [];
|
|
1035
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1036
|
+
const line = lines[i] ?? "";
|
|
1037
|
+
const match = line.match(HEADING_RE);
|
|
1038
|
+
if (!match) continue;
|
|
1039
|
+
const levelToken = match[1];
|
|
1040
|
+
const title = match[2];
|
|
1041
|
+
if (!levelToken || !title) continue;
|
|
1042
|
+
headings.push({
|
|
1043
|
+
level: levelToken.length,
|
|
1044
|
+
title: title.trim(),
|
|
1045
|
+
line: i + 1
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
return headings;
|
|
1049
|
+
}
|
|
1050
|
+
function extractH2Sections(md) {
|
|
1051
|
+
const lines = md.split(/\r?\n/);
|
|
1052
|
+
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1053
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1054
|
+
for (let i = 0; i < headings.length; i++) {
|
|
1055
|
+
const current = headings[i];
|
|
1056
|
+
if (!current) continue;
|
|
1057
|
+
const next = headings[i + 1];
|
|
1058
|
+
const startLine = current.line + 1;
|
|
1059
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1060
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1061
|
+
sections.set(current.title.trim(), {
|
|
1062
|
+
title: current.title.trim(),
|
|
1063
|
+
startLine,
|
|
1064
|
+
endLine,
|
|
1065
|
+
body
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
return sections;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// src/core/parse/spec.ts
|
|
1072
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1073
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1074
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1075
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1076
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1077
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1078
|
+
function parseSpec(md, file) {
|
|
1079
|
+
const headings = parseHeadings(md);
|
|
1080
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
1081
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1082
|
+
const sections = extractH2Sections(md);
|
|
1083
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1084
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1085
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1086
|
+
const startLine = brSection?.startLine ?? 1;
|
|
1087
|
+
const brs = [];
|
|
1088
|
+
const brsWithoutPriority = [];
|
|
1089
|
+
const brsWithInvalidPriority = [];
|
|
1090
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
1091
|
+
const lineText = brLines[i] ?? "";
|
|
1092
|
+
const lineNumber = startLine + i;
|
|
1093
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
1094
|
+
if (validMatch) {
|
|
1095
|
+
const id = validMatch[1];
|
|
1096
|
+
const priority = validMatch[2];
|
|
1097
|
+
const text = validMatch[3];
|
|
1098
|
+
if (!id || !priority || !text) continue;
|
|
1099
|
+
brs.push({
|
|
1100
|
+
id,
|
|
1101
|
+
priority,
|
|
1102
|
+
text: text.trim(),
|
|
1103
|
+
line: lineNumber
|
|
1104
|
+
});
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1108
|
+
if (anyPriorityMatch) {
|
|
1109
|
+
const id = anyPriorityMatch[1];
|
|
1110
|
+
const priority = anyPriorityMatch[2];
|
|
1111
|
+
const text = anyPriorityMatch[3];
|
|
1112
|
+
if (!id || !priority || !text) continue;
|
|
1113
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
1114
|
+
brsWithInvalidPriority.push({
|
|
1115
|
+
id,
|
|
1116
|
+
priority,
|
|
1117
|
+
text: text.trim(),
|
|
1118
|
+
line: lineNumber
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1124
|
+
if (noPriorityMatch) {
|
|
1125
|
+
const id = noPriorityMatch[1];
|
|
1126
|
+
const text = noPriorityMatch[2];
|
|
1127
|
+
if (!id || !text) continue;
|
|
1128
|
+
brsWithoutPriority.push({
|
|
1129
|
+
id,
|
|
1130
|
+
text: text.trim(),
|
|
1131
|
+
line: lineNumber
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const parsed = {
|
|
1136
|
+
file,
|
|
1137
|
+
sections: sectionNames,
|
|
1138
|
+
brs,
|
|
1139
|
+
brsWithoutPriority,
|
|
1140
|
+
brsWithInvalidPriority
|
|
1141
|
+
};
|
|
1142
|
+
if (specId) {
|
|
1143
|
+
parsed.specId = specId;
|
|
1144
|
+
}
|
|
1145
|
+
return parsed;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
906
1148
|
// src/core/validators/ids.ts
|
|
1149
|
+
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
907
1150
|
async function validateDefinedIds(root, config) {
|
|
908
1151
|
const issues = [];
|
|
909
|
-
const
|
|
910
|
-
const
|
|
911
|
-
const
|
|
912
|
-
const scenarioFiles = await collectFiles(scenarioRoot, {
|
|
913
|
-
extensions: [".feature"]
|
|
914
|
-
});
|
|
1152
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1153
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
1154
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
915
1155
|
const defined = /* @__PURE__ */ new Map();
|
|
916
1156
|
await collectSpecDefinitionIds(specFiles, defined);
|
|
917
1157
|
await collectScenarioDefinitionIds(scenarioFiles, defined);
|
|
@@ -927,7 +1167,7 @@ async function validateDefinedIds(root, config) {
|
|
|
927
1167
|
}
|
|
928
1168
|
const sorted = Array.from(files).sort();
|
|
929
1169
|
issues.push(
|
|
930
|
-
|
|
1170
|
+
issue3(
|
|
931
1171
|
"QFAI-ID-001",
|
|
932
1172
|
`ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
|
|
933
1173
|
"error",
|
|
@@ -940,15 +1180,25 @@ async function validateDefinedIds(root, config) {
|
|
|
940
1180
|
}
|
|
941
1181
|
async function collectSpecDefinitionIds(files, out) {
|
|
942
1182
|
for (const file of files) {
|
|
943
|
-
const text = await
|
|
944
|
-
|
|
945
|
-
|
|
1183
|
+
const text = await readFile6(file, "utf-8");
|
|
1184
|
+
const parsed = parseSpec(text, file);
|
|
1185
|
+
if (parsed.specId) {
|
|
1186
|
+
recordId(out, parsed.specId, file);
|
|
1187
|
+
}
|
|
1188
|
+
parsed.brs.forEach((br) => recordId(out, br.id, file));
|
|
946
1189
|
}
|
|
947
1190
|
}
|
|
948
1191
|
async function collectScenarioDefinitionIds(files, out) {
|
|
949
1192
|
for (const file of files) {
|
|
950
|
-
const text = await
|
|
951
|
-
|
|
1193
|
+
const text = await readFile6(file, "utf-8");
|
|
1194
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1195
|
+
for (const scenario of parsed.scenarios) {
|
|
1196
|
+
for (const tag of scenario.tags) {
|
|
1197
|
+
if (SC_TAG_RE.test(tag)) {
|
|
1198
|
+
recordId(out, tag, file);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
952
1202
|
}
|
|
953
1203
|
}
|
|
954
1204
|
function recordId(out, id, file) {
|
|
@@ -958,58 +1208,60 @@ function recordId(out, id, file) {
|
|
|
958
1208
|
}
|
|
959
1209
|
function formatFileList(files, root) {
|
|
960
1210
|
return files.map((file) => {
|
|
961
|
-
const relative =
|
|
1211
|
+
const relative = path9.relative(root, file);
|
|
962
1212
|
return relative.length > 0 ? relative : file;
|
|
963
1213
|
}).join(", ");
|
|
964
1214
|
}
|
|
965
|
-
function
|
|
966
|
-
const
|
|
1215
|
+
function issue3(code, message, severity, file, rule, refs) {
|
|
1216
|
+
const issue7 = {
|
|
967
1217
|
code,
|
|
968
1218
|
severity,
|
|
969
1219
|
message
|
|
970
1220
|
};
|
|
971
1221
|
if (file) {
|
|
972
|
-
|
|
1222
|
+
issue7.file = file;
|
|
973
1223
|
}
|
|
974
1224
|
if (rule) {
|
|
975
|
-
|
|
1225
|
+
issue7.rule = rule;
|
|
976
1226
|
}
|
|
977
1227
|
if (refs && refs.length > 0) {
|
|
978
|
-
|
|
1228
|
+
issue7.refs = refs;
|
|
979
1229
|
}
|
|
980
|
-
return
|
|
1230
|
+
return issue7;
|
|
981
1231
|
}
|
|
982
1232
|
|
|
983
1233
|
// src/core/validators/scenario.ts
|
|
984
|
-
import { readFile as
|
|
1234
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
985
1235
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
986
1236
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
987
1237
|
var THEN_PATTERN = /\bThen\b/;
|
|
1238
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1239
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1240
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
988
1241
|
async function validateScenarios(root, config) {
|
|
989
|
-
const
|
|
990
|
-
const files = await
|
|
991
|
-
extensions: [".feature"]
|
|
992
|
-
});
|
|
1242
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1243
|
+
const files = await collectScenarioFiles(specsRoot);
|
|
993
1244
|
if (files.length === 0) {
|
|
994
1245
|
return [
|
|
995
|
-
|
|
1246
|
+
issue4(
|
|
996
1247
|
"QFAI-SC-000",
|
|
997
1248
|
"Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
998
1249
|
"info",
|
|
999
|
-
|
|
1250
|
+
specsRoot,
|
|
1000
1251
|
"scenario.files"
|
|
1001
1252
|
)
|
|
1002
1253
|
];
|
|
1003
1254
|
}
|
|
1004
1255
|
const issues = [];
|
|
1005
1256
|
for (const file of files) {
|
|
1006
|
-
const text = await
|
|
1257
|
+
const text = await readFile7(file, "utf-8");
|
|
1007
1258
|
issues.push(...validateScenarioContent(text, file));
|
|
1008
1259
|
}
|
|
1009
1260
|
return issues;
|
|
1010
1261
|
}
|
|
1011
1262
|
function validateScenarioContent(text, file) {
|
|
1012
1263
|
const issues = [];
|
|
1264
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1013
1265
|
const invalidIds = extractInvalidIds(text, [
|
|
1014
1266
|
"SPEC",
|
|
1015
1267
|
"BR",
|
|
@@ -1021,7 +1273,7 @@ function validateScenarioContent(text, file) {
|
|
|
1021
1273
|
]);
|
|
1022
1274
|
if (invalidIds.length > 0) {
|
|
1023
1275
|
issues.push(
|
|
1024
|
-
|
|
1276
|
+
issue4(
|
|
1025
1277
|
"QFAI-ID-002",
|
|
1026
1278
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
1027
1279
|
"error",
|
|
@@ -1031,94 +1283,114 @@ function validateScenarioContent(text, file) {
|
|
|
1031
1283
|
)
|
|
1032
1284
|
);
|
|
1033
1285
|
}
|
|
1034
|
-
const
|
|
1035
|
-
if (
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
"QFAI-SC-001",
|
|
1039
|
-
"SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1040
|
-
"error",
|
|
1041
|
-
file,
|
|
1042
|
-
"scenario.id"
|
|
1043
|
-
)
|
|
1044
|
-
);
|
|
1045
|
-
}
|
|
1046
|
-
const specIds = extractIds(text, "SPEC");
|
|
1047
|
-
if (specIds.length === 0) {
|
|
1048
|
-
issues.push(
|
|
1049
|
-
issue3(
|
|
1050
|
-
"QFAI-SC-002",
|
|
1051
|
-
"SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
1052
|
-
"error",
|
|
1053
|
-
file,
|
|
1054
|
-
"scenario.spec"
|
|
1055
|
-
)
|
|
1056
|
-
);
|
|
1057
|
-
}
|
|
1058
|
-
const brIds = extractIds(text, "BR");
|
|
1059
|
-
if (brIds.length === 0) {
|
|
1286
|
+
const missingStructure = [];
|
|
1287
|
+
if (!parsed.featurePresent) missingStructure.push("Feature");
|
|
1288
|
+
if (parsed.scenarios.length === 0) missingStructure.push("Scenario");
|
|
1289
|
+
if (missingStructure.length > 0) {
|
|
1060
1290
|
issues.push(
|
|
1061
|
-
|
|
1062
|
-
"QFAI-SC-
|
|
1063
|
-
|
|
1291
|
+
issue4(
|
|
1292
|
+
"QFAI-SC-006",
|
|
1293
|
+
`Scenario \u30D5\u30A1\u30A4\u30EB\u306B\u5FC5\u8981\u306A\u69CB\u9020\u304C\u3042\u308A\u307E\u305B\u3093: ${missingStructure.join(
|
|
1294
|
+
", "
|
|
1295
|
+
)}`,
|
|
1064
1296
|
"error",
|
|
1065
1297
|
file,
|
|
1066
|
-
"scenario.
|
|
1298
|
+
"scenario.structure"
|
|
1067
1299
|
)
|
|
1068
1300
|
);
|
|
1069
1301
|
}
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1302
|
+
for (const scenario of parsed.scenarios) {
|
|
1303
|
+
if (scenario.tags.length === 0) {
|
|
1304
|
+
issues.push(
|
|
1305
|
+
issue4(
|
|
1306
|
+
"QFAI-SC-007",
|
|
1307
|
+
`Scenario \u30BF\u30B0\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${scenario.name}`,
|
|
1308
|
+
"error",
|
|
1309
|
+
file,
|
|
1310
|
+
"scenario.tags"
|
|
1311
|
+
)
|
|
1312
|
+
);
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
const missingTags = [];
|
|
1316
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
|
|
1317
|
+
if (scTags.length === 0) {
|
|
1318
|
+
missingTags.push("SC(0\u4EF6)");
|
|
1319
|
+
} else if (scTags.length > 1) {
|
|
1320
|
+
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1321
|
+
}
|
|
1322
|
+
if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
|
|
1323
|
+
missingTags.push("SPEC");
|
|
1324
|
+
}
|
|
1325
|
+
if (!scenario.tags.some((tag) => BR_TAG_RE.test(tag))) {
|
|
1326
|
+
missingTags.push("BR");
|
|
1327
|
+
}
|
|
1328
|
+
if (missingTags.length > 0) {
|
|
1329
|
+
issues.push(
|
|
1330
|
+
issue4(
|
|
1331
|
+
"QFAI-SC-008",
|
|
1332
|
+
`Scenario \u30BF\u30B0\u306B\u4E0D\u8DB3\u304C\u3042\u308A\u307E\u3059: ${missingTags.join(", ")} (${scenario.name})`,
|
|
1333
|
+
"error",
|
|
1334
|
+
file,
|
|
1335
|
+
"scenario.tagIds"
|
|
1336
|
+
)
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1079
1339
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1340
|
+
for (const scenario of parsed.scenarios) {
|
|
1341
|
+
const missingSteps = [];
|
|
1342
|
+
if (!GIVEN_PATTERN.test(scenario.body)) {
|
|
1343
|
+
missingSteps.push("Given");
|
|
1344
|
+
}
|
|
1345
|
+
if (!WHEN_PATTERN.test(scenario.body)) {
|
|
1346
|
+
missingSteps.push("When");
|
|
1347
|
+
}
|
|
1348
|
+
if (!THEN_PATTERN.test(scenario.body)) {
|
|
1349
|
+
missingSteps.push("Then");
|
|
1350
|
+
}
|
|
1351
|
+
if (missingSteps.length > 0) {
|
|
1352
|
+
issues.push(
|
|
1353
|
+
issue4(
|
|
1354
|
+
"QFAI-SC-005",
|
|
1355
|
+
`Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")} (${scenario.name})`,
|
|
1356
|
+
"warning",
|
|
1357
|
+
file,
|
|
1358
|
+
"scenario.steps"
|
|
1359
|
+
)
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1090
1362
|
}
|
|
1091
1363
|
return issues;
|
|
1092
1364
|
}
|
|
1093
|
-
function
|
|
1094
|
-
const
|
|
1365
|
+
function issue4(code, message, severity, file, rule, refs) {
|
|
1366
|
+
const issue7 = {
|
|
1095
1367
|
code,
|
|
1096
1368
|
severity,
|
|
1097
1369
|
message
|
|
1098
1370
|
};
|
|
1099
1371
|
if (file) {
|
|
1100
|
-
|
|
1372
|
+
issue7.file = file;
|
|
1101
1373
|
}
|
|
1102
1374
|
if (rule) {
|
|
1103
|
-
|
|
1375
|
+
issue7.rule = rule;
|
|
1104
1376
|
}
|
|
1105
1377
|
if (refs && refs.length > 0) {
|
|
1106
|
-
|
|
1378
|
+
issue7.refs = refs;
|
|
1107
1379
|
}
|
|
1108
|
-
return
|
|
1380
|
+
return issue7;
|
|
1109
1381
|
}
|
|
1110
1382
|
|
|
1111
1383
|
// src/core/validators/spec.ts
|
|
1112
|
-
import { readFile as
|
|
1384
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1113
1385
|
async function validateSpecs(root, config) {
|
|
1114
|
-
const specsRoot = resolvePath(root, config, "
|
|
1386
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1115
1387
|
const files = await collectSpecFiles(specsRoot);
|
|
1116
1388
|
if (files.length === 0) {
|
|
1117
|
-
const expected = "spec-
|
|
1389
|
+
const expected = "spec-001/spec.md";
|
|
1118
1390
|
return [
|
|
1119
|
-
|
|
1391
|
+
issue5(
|
|
1120
1392
|
"QFAI-SPEC-000",
|
|
1121
|
-
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.
|
|
1393
|
+
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}`,
|
|
1122
1394
|
"info",
|
|
1123
1395
|
specsRoot,
|
|
1124
1396
|
"spec.files"
|
|
@@ -1127,7 +1399,7 @@ async function validateSpecs(root, config) {
|
|
|
1127
1399
|
}
|
|
1128
1400
|
const issues = [];
|
|
1129
1401
|
for (const file of files) {
|
|
1130
|
-
const text = await
|
|
1402
|
+
const text = await readFile8(file, "utf-8");
|
|
1131
1403
|
issues.push(
|
|
1132
1404
|
...validateSpecContent(
|
|
1133
1405
|
text,
|
|
@@ -1140,6 +1412,7 @@ async function validateSpecs(root, config) {
|
|
|
1140
1412
|
}
|
|
1141
1413
|
function validateSpecContent(text, file, requiredSections) {
|
|
1142
1414
|
const issues = [];
|
|
1415
|
+
const parsed = parseSpec(text, file);
|
|
1143
1416
|
const invalidIds = extractInvalidIds(text, [
|
|
1144
1417
|
"SPEC",
|
|
1145
1418
|
"BR",
|
|
@@ -1151,7 +1424,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1151
1424
|
]);
|
|
1152
1425
|
if (invalidIds.length > 0) {
|
|
1153
1426
|
issues.push(
|
|
1154
|
-
|
|
1427
|
+
issue5(
|
|
1155
1428
|
"QFAI-ID-002",
|
|
1156
1429
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
1157
1430
|
"error",
|
|
@@ -1161,10 +1434,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1161
1434
|
)
|
|
1162
1435
|
);
|
|
1163
1436
|
}
|
|
1164
|
-
|
|
1165
|
-
if (specIds.length === 0) {
|
|
1437
|
+
if (!parsed.specId) {
|
|
1166
1438
|
issues.push(
|
|
1167
|
-
|
|
1439
|
+
issue5(
|
|
1168
1440
|
"QFAI-SPEC-001",
|
|
1169
1441
|
"SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1170
1442
|
"error",
|
|
@@ -1173,10 +1445,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1173
1445
|
)
|
|
1174
1446
|
);
|
|
1175
1447
|
}
|
|
1176
|
-
|
|
1177
|
-
if (brIds.length === 0) {
|
|
1448
|
+
if (parsed.brs.length === 0) {
|
|
1178
1449
|
issues.push(
|
|
1179
|
-
|
|
1450
|
+
issue5(
|
|
1180
1451
|
"QFAI-SPEC-002",
|
|
1181
1452
|
"BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1182
1453
|
"error",
|
|
@@ -1185,10 +1456,34 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1185
1456
|
)
|
|
1186
1457
|
);
|
|
1187
1458
|
}
|
|
1459
|
+
for (const br of parsed.brsWithoutPriority) {
|
|
1460
|
+
issues.push(
|
|
1461
|
+
issue5(
|
|
1462
|
+
"QFAI-BR-001",
|
|
1463
|
+
`BR \u884C\u306B Priority \u304C\u3042\u308A\u307E\u305B\u3093: ${br.id}`,
|
|
1464
|
+
"error",
|
|
1465
|
+
file,
|
|
1466
|
+
"spec.brPriority",
|
|
1467
|
+
[br.id]
|
|
1468
|
+
)
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
for (const br of parsed.brsWithInvalidPriority) {
|
|
1472
|
+
issues.push(
|
|
1473
|
+
issue5(
|
|
1474
|
+
"QFAI-BR-002",
|
|
1475
|
+
`BR Priority \u304C\u4E0D\u6B63\u3067\u3059: ${br.id} (${br.priority})`,
|
|
1476
|
+
"error",
|
|
1477
|
+
file,
|
|
1478
|
+
"spec.brPriority",
|
|
1479
|
+
[br.id]
|
|
1480
|
+
)
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1188
1483
|
const scIds = extractIds(text, "SC");
|
|
1189
1484
|
if (scIds.length > 0) {
|
|
1190
1485
|
issues.push(
|
|
1191
|
-
|
|
1486
|
+
issue5(
|
|
1192
1487
|
"QFAI-SPEC-003",
|
|
1193
1488
|
"Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
|
|
1194
1489
|
"warning",
|
|
@@ -1199,9 +1494,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1199
1494
|
);
|
|
1200
1495
|
}
|
|
1201
1496
|
for (const section of requiredSections) {
|
|
1202
|
-
if (!
|
|
1497
|
+
if (!parsed.sections.has(section)) {
|
|
1203
1498
|
issues.push(
|
|
1204
|
-
|
|
1499
|
+
issue5(
|
|
1205
1500
|
"QFAI-SPEC-004",
|
|
1206
1501
|
`\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
|
|
1207
1502
|
"error",
|
|
@@ -1213,40 +1508,39 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1213
1508
|
}
|
|
1214
1509
|
return issues;
|
|
1215
1510
|
}
|
|
1216
|
-
function
|
|
1217
|
-
const
|
|
1511
|
+
function issue5(code, message, severity, file, rule, refs) {
|
|
1512
|
+
const issue7 = {
|
|
1218
1513
|
code,
|
|
1219
1514
|
severity,
|
|
1220
1515
|
message
|
|
1221
1516
|
};
|
|
1222
1517
|
if (file) {
|
|
1223
|
-
|
|
1518
|
+
issue7.file = file;
|
|
1224
1519
|
}
|
|
1225
1520
|
if (rule) {
|
|
1226
|
-
|
|
1521
|
+
issue7.rule = rule;
|
|
1227
1522
|
}
|
|
1228
1523
|
if (refs && refs.length > 0) {
|
|
1229
|
-
|
|
1524
|
+
issue7.refs = refs;
|
|
1230
1525
|
}
|
|
1231
|
-
return
|
|
1526
|
+
return issue7;
|
|
1232
1527
|
}
|
|
1233
1528
|
|
|
1234
1529
|
// src/core/validators/traceability.ts
|
|
1235
|
-
import { readFile as
|
|
1530
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1531
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1532
|
+
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1533
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1534
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1535
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
1536
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1236
1537
|
async function validateTraceability(root, config) {
|
|
1237
1538
|
const issues = [];
|
|
1238
|
-
const specsRoot = resolvePath(root, config, "
|
|
1239
|
-
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1240
|
-
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1539
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1241
1540
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1242
1541
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1243
1542
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
1244
|
-
const
|
|
1245
|
-
extensions: [".md"]
|
|
1246
|
-
});
|
|
1247
|
-
const scenarioFiles = await collectFiles(scenariosRoot, {
|
|
1248
|
-
extensions: [".feature"]
|
|
1249
|
-
});
|
|
1543
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1250
1544
|
const upstreamIds = /* @__PURE__ */ new Set();
|
|
1251
1545
|
const specIds = /* @__PURE__ */ new Set();
|
|
1252
1546
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
@@ -1258,11 +1552,13 @@ async function validateTraceability(root, config) {
|
|
|
1258
1552
|
const contractIndex = await buildContractIndex(root, config);
|
|
1259
1553
|
const contractIds = contractIndex.ids;
|
|
1260
1554
|
for (const file of specFiles) {
|
|
1261
|
-
const text = await
|
|
1555
|
+
const text = await readFile9(file, "utf-8");
|
|
1262
1556
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
|
|
1557
|
+
const parsed = parseSpec(text, file);
|
|
1558
|
+
if (parsed.specId) {
|
|
1559
|
+
specIds.add(parsed.specId);
|
|
1560
|
+
}
|
|
1561
|
+
const brIds = parsed.brs.map((br) => br.id);
|
|
1266
1562
|
brIds.forEach((id) => brIdsInSpecs.add(id));
|
|
1267
1563
|
const referencedContractIds = /* @__PURE__ */ new Set([
|
|
1268
1564
|
...extractIds(text, "UI"),
|
|
@@ -1274,7 +1570,7 @@ async function validateTraceability(root, config) {
|
|
|
1274
1570
|
);
|
|
1275
1571
|
if (unknownContractIds.length > 0) {
|
|
1276
1572
|
issues.push(
|
|
1277
|
-
|
|
1573
|
+
issue6(
|
|
1278
1574
|
"QFAI-TRACE-009",
|
|
1279
1575
|
`Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1280
1576
|
", "
|
|
@@ -1286,37 +1582,50 @@ async function validateTraceability(root, config) {
|
|
|
1286
1582
|
)
|
|
1287
1583
|
);
|
|
1288
1584
|
}
|
|
1289
|
-
|
|
1290
|
-
const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
|
|
1585
|
+
if (parsed.specId) {
|
|
1586
|
+
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
1291
1587
|
brIds.forEach((id) => current.add(id));
|
|
1292
|
-
specToBrIds.set(specId, current);
|
|
1588
|
+
specToBrIds.set(parsed.specId, current);
|
|
1293
1589
|
}
|
|
1294
1590
|
}
|
|
1295
|
-
for (const file of decisionFiles) {
|
|
1296
|
-
const text = await readFile8(file, "utf-8");
|
|
1297
|
-
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1298
|
-
}
|
|
1299
1591
|
for (const file of scenarioFiles) {
|
|
1300
|
-
const text = await
|
|
1592
|
+
const text = await readFile9(file, "utf-8");
|
|
1301
1593
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1302
|
-
const
|
|
1303
|
-
const
|
|
1304
|
-
const
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1594
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1595
|
+
const specIdsInScenario = /* @__PURE__ */ new Set();
|
|
1596
|
+
const brIds = /* @__PURE__ */ new Set();
|
|
1597
|
+
const scIds = /* @__PURE__ */ new Set();
|
|
1598
|
+
const scenarioIds = /* @__PURE__ */ new Set();
|
|
1599
|
+
for (const scenario of parsed.scenarios) {
|
|
1600
|
+
for (const tag of scenario.tags) {
|
|
1601
|
+
if (SPEC_TAG_RE2.test(tag)) {
|
|
1602
|
+
specIdsInScenario.add(tag);
|
|
1603
|
+
}
|
|
1604
|
+
if (BR_TAG_RE2.test(tag)) {
|
|
1605
|
+
brIds.add(tag);
|
|
1606
|
+
}
|
|
1607
|
+
if (SC_TAG_RE3.test(tag)) {
|
|
1608
|
+
scIds.add(tag);
|
|
1609
|
+
}
|
|
1610
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1611
|
+
scenarioIds.add(tag);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1315
1614
|
}
|
|
1316
|
-
const
|
|
1615
|
+
const specIdsList = Array.from(specIdsInScenario);
|
|
1616
|
+
const brIdsList = Array.from(brIds);
|
|
1617
|
+
const scIdsList = Array.from(scIds);
|
|
1618
|
+
const scenarioIdsList = Array.from(scenarioIds);
|
|
1619
|
+
brIdsList.forEach((id) => brIdsInScenarios.add(id));
|
|
1620
|
+
scIdsList.forEach((id) => scIdsInScenarios.add(id));
|
|
1621
|
+
scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
|
|
1622
|
+
if (scenarioIdsList.length > 0) {
|
|
1623
|
+
scIdsList.forEach((id) => scWithContracts.add(id));
|
|
1624
|
+
}
|
|
1625
|
+
const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
|
|
1317
1626
|
if (unknownSpecIds.length > 0) {
|
|
1318
1627
|
issues.push(
|
|
1319
|
-
|
|
1628
|
+
issue6(
|
|
1320
1629
|
"QFAI-TRACE-005",
|
|
1321
1630
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1322
1631
|
"error",
|
|
@@ -1326,10 +1635,10 @@ async function validateTraceability(root, config) {
|
|
|
1326
1635
|
)
|
|
1327
1636
|
);
|
|
1328
1637
|
}
|
|
1329
|
-
const unknownBrIds =
|
|
1638
|
+
const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
|
|
1330
1639
|
if (unknownBrIds.length > 0) {
|
|
1331
1640
|
issues.push(
|
|
1332
|
-
|
|
1641
|
+
issue6(
|
|
1333
1642
|
"QFAI-TRACE-006",
|
|
1334
1643
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1335
1644
|
"error",
|
|
@@ -1339,10 +1648,12 @@ async function validateTraceability(root, config) {
|
|
|
1339
1648
|
)
|
|
1340
1649
|
);
|
|
1341
1650
|
}
|
|
1342
|
-
const unknownContractIds =
|
|
1651
|
+
const unknownContractIds = scenarioIdsList.filter(
|
|
1652
|
+
(id) => !contractIds.has(id)
|
|
1653
|
+
);
|
|
1343
1654
|
if (unknownContractIds.length > 0) {
|
|
1344
1655
|
issues.push(
|
|
1345
|
-
|
|
1656
|
+
issue6(
|
|
1346
1657
|
"QFAI-TRACE-008",
|
|
1347
1658
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1348
1659
|
", "
|
|
@@ -1354,23 +1665,23 @@ async function validateTraceability(root, config) {
|
|
|
1354
1665
|
)
|
|
1355
1666
|
);
|
|
1356
1667
|
}
|
|
1357
|
-
if (
|
|
1668
|
+
if (specIdsList.length > 0) {
|
|
1358
1669
|
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
1359
|
-
for (const specId of
|
|
1670
|
+
for (const specId of specIdsList) {
|
|
1360
1671
|
const brIdsForSpec = specToBrIds.get(specId);
|
|
1361
1672
|
if (!brIdsForSpec) {
|
|
1362
1673
|
continue;
|
|
1363
1674
|
}
|
|
1364
1675
|
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
1365
1676
|
}
|
|
1366
|
-
const invalidBrIds =
|
|
1677
|
+
const invalidBrIds = brIdsList.filter((id) => !allowedBrIds.has(id));
|
|
1367
1678
|
if (invalidBrIds.length > 0) {
|
|
1368
1679
|
issues.push(
|
|
1369
|
-
|
|
1680
|
+
issue6(
|
|
1370
1681
|
"QFAI-TRACE-007",
|
|
1371
1682
|
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
1372
1683
|
", "
|
|
1373
|
-
)} (SPEC: ${
|
|
1684
|
+
)} (SPEC: ${specIdsList.join(", ")})`,
|
|
1374
1685
|
"error",
|
|
1375
1686
|
file,
|
|
1376
1687
|
"traceability.scenarioBrUnderSpec",
|
|
@@ -1382,7 +1693,7 @@ async function validateTraceability(root, config) {
|
|
|
1382
1693
|
}
|
|
1383
1694
|
if (upstreamIds.size === 0) {
|
|
1384
1695
|
return [
|
|
1385
|
-
|
|
1696
|
+
issue6(
|
|
1386
1697
|
"QFAI-TRACE-000",
|
|
1387
1698
|
"\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1388
1699
|
"info",
|
|
@@ -1397,7 +1708,7 @@ async function validateTraceability(root, config) {
|
|
|
1397
1708
|
);
|
|
1398
1709
|
if (orphanBrIds.length > 0) {
|
|
1399
1710
|
issues.push(
|
|
1400
|
-
|
|
1711
|
+
issue6(
|
|
1401
1712
|
"QFAI_TRACE_BR_ORPHAN",
|
|
1402
1713
|
`BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
|
|
1403
1714
|
"error",
|
|
@@ -1414,13 +1725,13 @@ async function validateTraceability(root, config) {
|
|
|
1414
1725
|
);
|
|
1415
1726
|
if (scWithoutContracts.length > 0) {
|
|
1416
1727
|
issues.push(
|
|
1417
|
-
|
|
1728
|
+
issue6(
|
|
1418
1729
|
"QFAI_TRACE_SC_NO_CONTRACT",
|
|
1419
1730
|
`SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
|
|
1420
1731
|
", "
|
|
1421
1732
|
)}`,
|
|
1422
1733
|
"error",
|
|
1423
|
-
|
|
1734
|
+
specsRoot,
|
|
1424
1735
|
"traceability.scMustTouchContracts",
|
|
1425
1736
|
scWithoutContracts
|
|
1426
1737
|
)
|
|
@@ -1434,11 +1745,11 @@ async function validateTraceability(root, config) {
|
|
|
1434
1745
|
);
|
|
1435
1746
|
if (orphanContracts.length > 0) {
|
|
1436
1747
|
issues.push(
|
|
1437
|
-
|
|
1748
|
+
issue6(
|
|
1438
1749
|
"QFAI_CONTRACT_ORPHAN",
|
|
1439
1750
|
`\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
1440
1751
|
"error",
|
|
1441
|
-
|
|
1752
|
+
specsRoot,
|
|
1442
1753
|
"traceability.allowOrphanContracts",
|
|
1443
1754
|
orphanContracts
|
|
1444
1755
|
)
|
|
@@ -1462,7 +1773,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1462
1773
|
const targetFiles = [...codeFiles, ...testFiles];
|
|
1463
1774
|
if (targetFiles.length === 0) {
|
|
1464
1775
|
issues.push(
|
|
1465
|
-
|
|
1776
|
+
issue6(
|
|
1466
1777
|
"QFAI-TRACE-001",
|
|
1467
1778
|
"\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1468
1779
|
"info",
|
|
@@ -1475,7 +1786,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1475
1786
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
1476
1787
|
let found = false;
|
|
1477
1788
|
for (const file of targetFiles) {
|
|
1478
|
-
const text = await
|
|
1789
|
+
const text = await readFile9(file, "utf-8");
|
|
1479
1790
|
if (pattern.test(text)) {
|
|
1480
1791
|
found = true;
|
|
1481
1792
|
break;
|
|
@@ -1483,7 +1794,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1483
1794
|
}
|
|
1484
1795
|
if (!found) {
|
|
1485
1796
|
issues.push(
|
|
1486
|
-
|
|
1797
|
+
issue6(
|
|
1487
1798
|
"QFAI-TRACE-002",
|
|
1488
1799
|
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
1489
1800
|
"warning",
|
|
@@ -1498,22 +1809,22 @@ function buildIdPattern(ids) {
|
|
|
1498
1809
|
const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
1499
1810
|
return new RegExp(`\\b(${escaped.join("|")})\\b`);
|
|
1500
1811
|
}
|
|
1501
|
-
function
|
|
1502
|
-
const
|
|
1812
|
+
function issue6(code, message, severity, file, rule, refs) {
|
|
1813
|
+
const issue7 = {
|
|
1503
1814
|
code,
|
|
1504
1815
|
severity,
|
|
1505
1816
|
message
|
|
1506
1817
|
};
|
|
1507
1818
|
if (file) {
|
|
1508
|
-
|
|
1819
|
+
issue7.file = file;
|
|
1509
1820
|
}
|
|
1510
1821
|
if (rule) {
|
|
1511
|
-
|
|
1822
|
+
issue7.rule = rule;
|
|
1512
1823
|
}
|
|
1513
1824
|
if (refs && refs.length > 0) {
|
|
1514
|
-
|
|
1825
|
+
issue7.refs = refs;
|
|
1515
1826
|
}
|
|
1516
|
-
return
|
|
1827
|
+
return issue7;
|
|
1517
1828
|
}
|
|
1518
1829
|
|
|
1519
1830
|
// src/core/validate.ts
|
|
@@ -1523,6 +1834,7 @@ async function validateProject(root, configResult) {
|
|
|
1523
1834
|
const issues = [
|
|
1524
1835
|
...configIssues,
|
|
1525
1836
|
...await validateSpecs(root, config),
|
|
1837
|
+
...await validateDeltas(root, config),
|
|
1526
1838
|
...await validateScenarios(root, config),
|
|
1527
1839
|
...await validateContracts(root, config),
|
|
1528
1840
|
...await validateDefinedIds(root, config),
|
|
@@ -1538,8 +1850,8 @@ async function validateProject(root, configResult) {
|
|
|
1538
1850
|
}
|
|
1539
1851
|
function countIssues(issues) {
|
|
1540
1852
|
return issues.reduce(
|
|
1541
|
-
(acc,
|
|
1542
|
-
acc[
|
|
1853
|
+
(acc, issue7) => {
|
|
1854
|
+
acc[issue7.severity] += 1;
|
|
1543
1855
|
return acc;
|
|
1544
1856
|
},
|
|
1545
1857
|
{ info: 0, warning: 0, error: 0 }
|
|
@@ -1552,21 +1864,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
1552
1864
|
const resolved = configResult ?? await loadConfig(root);
|
|
1553
1865
|
const config = resolved.config;
|
|
1554
1866
|
const configPath = resolved.configPath;
|
|
1555
|
-
const
|
|
1556
|
-
const
|
|
1557
|
-
const
|
|
1558
|
-
const
|
|
1559
|
-
const
|
|
1560
|
-
const dbRoot = resolvePath(root, config, "dataContractsDir");
|
|
1867
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1868
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1869
|
+
const apiRoot = path10.join(contractsRoot, "api");
|
|
1870
|
+
const uiRoot = path10.join(contractsRoot, "ui");
|
|
1871
|
+
const dbRoot = path10.join(contractsRoot, "db");
|
|
1561
1872
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1562
1873
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1563
|
-
const specFiles = await collectSpecFiles(
|
|
1564
|
-
const scenarioFiles = await
|
|
1565
|
-
extensions: [".feature"]
|
|
1566
|
-
});
|
|
1567
|
-
const decisionFiles = await collectFiles(decisionsRoot, {
|
|
1568
|
-
extensions: [".md"]
|
|
1569
|
-
});
|
|
1874
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
1875
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1570
1876
|
const {
|
|
1571
1877
|
api: apiFiles,
|
|
1572
1878
|
ui: uiFiles,
|
|
@@ -1575,7 +1881,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1575
1881
|
const idsByPrefix = await collectIds([
|
|
1576
1882
|
...specFiles,
|
|
1577
1883
|
...scenarioFiles,
|
|
1578
|
-
...decisionFiles,
|
|
1579
1884
|
...apiFiles,
|
|
1580
1885
|
...uiFiles,
|
|
1581
1886
|
...dbFiles
|
|
@@ -1600,7 +1905,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1600
1905
|
summary: {
|
|
1601
1906
|
specs: specFiles.length,
|
|
1602
1907
|
scenarios: scenarioFiles.length,
|
|
1603
|
-
decisions: decisionFiles.length,
|
|
1604
1908
|
contracts: {
|
|
1605
1909
|
api: apiFiles.length,
|
|
1606
1910
|
ui: uiFiles.length,
|
|
@@ -1634,7 +1938,6 @@ function formatReportMarkdown(data) {
|
|
|
1634
1938
|
lines.push("## \u6982\u8981");
|
|
1635
1939
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
1636
1940
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
1637
|
-
lines.push(`- decisions: ${data.summary.decisions}`);
|
|
1638
1941
|
lines.push(
|
|
1639
1942
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
1640
1943
|
);
|
|
@@ -1710,7 +2013,7 @@ async function collectIds(files) {
|
|
|
1710
2013
|
DATA: /* @__PURE__ */ new Set()
|
|
1711
2014
|
};
|
|
1712
2015
|
for (const file of files) {
|
|
1713
|
-
const text = await
|
|
2016
|
+
const text = await readFile10(file, "utf-8");
|
|
1714
2017
|
for (const prefix of ID_PREFIXES2) {
|
|
1715
2018
|
const ids = extractIds(text, prefix);
|
|
1716
2019
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -1728,7 +2031,7 @@ async function collectIds(files) {
|
|
|
1728
2031
|
async function collectUpstreamIds(files) {
|
|
1729
2032
|
const ids = /* @__PURE__ */ new Set();
|
|
1730
2033
|
for (const file of files) {
|
|
1731
|
-
const text = await
|
|
2034
|
+
const text = await readFile10(file, "utf-8");
|
|
1732
2035
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
1733
2036
|
}
|
|
1734
2037
|
return ids;
|
|
@@ -1749,7 +2052,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
1749
2052
|
}
|
|
1750
2053
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
1751
2054
|
for (const file of targetFiles) {
|
|
1752
|
-
const text = await
|
|
2055
|
+
const text = await readFile10(file, "utf-8");
|
|
1753
2056
|
if (pattern.test(text)) {
|
|
1754
2057
|
return true;
|
|
1755
2058
|
}
|
|
@@ -1771,20 +2074,20 @@ function toSortedArray(values) {
|
|
|
1771
2074
|
}
|
|
1772
2075
|
function buildHotspots(issues) {
|
|
1773
2076
|
const map = /* @__PURE__ */ new Map();
|
|
1774
|
-
for (const
|
|
1775
|
-
if (!
|
|
2077
|
+
for (const issue7 of issues) {
|
|
2078
|
+
if (!issue7.file) {
|
|
1776
2079
|
continue;
|
|
1777
2080
|
}
|
|
1778
|
-
const current = map.get(
|
|
1779
|
-
file:
|
|
2081
|
+
const current = map.get(issue7.file) ?? {
|
|
2082
|
+
file: issue7.file,
|
|
1780
2083
|
total: 0,
|
|
1781
2084
|
error: 0,
|
|
1782
2085
|
warning: 0,
|
|
1783
2086
|
info: 0
|
|
1784
2087
|
};
|
|
1785
2088
|
current.total += 1;
|
|
1786
|
-
current[
|
|
1787
|
-
map.set(
|
|
2089
|
+
current[issue7.severity] += 1;
|
|
2090
|
+
map.set(issue7.file, current);
|
|
1788
2091
|
}
|
|
1789
2092
|
return Array.from(map.values()).sort(
|
|
1790
2093
|
(a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
|
|
@@ -1806,6 +2109,7 @@ export {
|
|
|
1806
2109
|
resolveToolVersion,
|
|
1807
2110
|
validateContracts,
|
|
1808
2111
|
validateDefinedIds,
|
|
2112
|
+
validateDeltas,
|
|
1809
2113
|
validateProject,
|
|
1810
2114
|
validateScenarioContent,
|
|
1811
2115
|
validateScenarios,
|