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.cjs
CHANGED
|
@@ -45,6 +45,7 @@ __export(src_exports, {
|
|
|
45
45
|
resolveToolVersion: () => resolveToolVersion,
|
|
46
46
|
validateContracts: () => validateContracts,
|
|
47
47
|
validateDefinedIds: () => validateDefinedIds,
|
|
48
|
+
validateDeltas: () => validateDeltas,
|
|
48
49
|
validateProject: () => validateProject,
|
|
49
50
|
validateScenarioContent: () => validateScenarioContent,
|
|
50
51
|
validateScenarios: () => validateScenarios,
|
|
@@ -60,13 +61,11 @@ var import_node_path = __toESM(require("path"), 1);
|
|
|
60
61
|
var import_yaml = require("yaml");
|
|
61
62
|
var defaultConfig = {
|
|
62
63
|
paths: {
|
|
63
|
-
specDir: ".qfai/spec",
|
|
64
|
-
decisionsDir: ".qfai/spec/decisions",
|
|
65
|
-
scenariosDir: ".qfai/spec/scenarios",
|
|
66
64
|
contractsDir: ".qfai/contracts",
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
specsDir: ".qfai/specs",
|
|
66
|
+
rulesDir: ".qfai/rules",
|
|
67
|
+
outDir: ".qfai/out",
|
|
68
|
+
promptsDir: ".qfai/prompts",
|
|
70
69
|
srcDir: "src",
|
|
71
70
|
testsDir: "tests"
|
|
72
71
|
},
|
|
@@ -91,8 +90,7 @@ var defaultConfig = {
|
|
|
91
90
|
}
|
|
92
91
|
},
|
|
93
92
|
output: {
|
|
94
|
-
|
|
95
|
-
jsonPath: ".qfai/out/validate.json"
|
|
93
|
+
validateJsonPath: ".qfai/out/validate.json"
|
|
96
94
|
}
|
|
97
95
|
};
|
|
98
96
|
function getConfigPath(root) {
|
|
@@ -141,27 +139,6 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
141
139
|
return base;
|
|
142
140
|
}
|
|
143
141
|
return {
|
|
144
|
-
specDir: readString(
|
|
145
|
-
raw.specDir,
|
|
146
|
-
base.specDir,
|
|
147
|
-
"paths.specDir",
|
|
148
|
-
configPath,
|
|
149
|
-
issues
|
|
150
|
-
),
|
|
151
|
-
decisionsDir: readString(
|
|
152
|
-
raw.decisionsDir,
|
|
153
|
-
base.decisionsDir,
|
|
154
|
-
"paths.decisionsDir",
|
|
155
|
-
configPath,
|
|
156
|
-
issues
|
|
157
|
-
),
|
|
158
|
-
scenariosDir: readString(
|
|
159
|
-
raw.scenariosDir,
|
|
160
|
-
base.scenariosDir,
|
|
161
|
-
"paths.scenariosDir",
|
|
162
|
-
configPath,
|
|
163
|
-
issues
|
|
164
|
-
),
|
|
165
142
|
contractsDir: readString(
|
|
166
143
|
raw.contractsDir,
|
|
167
144
|
base.contractsDir,
|
|
@@ -169,24 +146,31 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
169
146
|
configPath,
|
|
170
147
|
issues
|
|
171
148
|
),
|
|
172
|
-
|
|
173
|
-
raw.
|
|
174
|
-
base.
|
|
175
|
-
"paths.
|
|
149
|
+
specsDir: readString(
|
|
150
|
+
raw.specsDir,
|
|
151
|
+
base.specsDir,
|
|
152
|
+
"paths.specsDir",
|
|
176
153
|
configPath,
|
|
177
154
|
issues
|
|
178
155
|
),
|
|
179
|
-
|
|
180
|
-
raw.
|
|
181
|
-
base.
|
|
182
|
-
"paths.
|
|
156
|
+
rulesDir: readString(
|
|
157
|
+
raw.rulesDir,
|
|
158
|
+
base.rulesDir,
|
|
159
|
+
"paths.rulesDir",
|
|
183
160
|
configPath,
|
|
184
161
|
issues
|
|
185
162
|
),
|
|
186
|
-
|
|
187
|
-
raw.
|
|
188
|
-
base.
|
|
189
|
-
"paths.
|
|
163
|
+
outDir: readString(
|
|
164
|
+
raw.outDir,
|
|
165
|
+
base.outDir,
|
|
166
|
+
"paths.outDir",
|
|
167
|
+
configPath,
|
|
168
|
+
issues
|
|
169
|
+
),
|
|
170
|
+
promptsDir: readString(
|
|
171
|
+
raw.promptsDir,
|
|
172
|
+
base.promptsDir,
|
|
173
|
+
"paths.promptsDir",
|
|
190
174
|
configPath,
|
|
191
175
|
issues
|
|
192
176
|
),
|
|
@@ -309,17 +293,10 @@ function normalizeOutput(raw, configPath, issues) {
|
|
|
309
293
|
return base;
|
|
310
294
|
}
|
|
311
295
|
return {
|
|
312
|
-
|
|
313
|
-
raw.
|
|
314
|
-
base.
|
|
315
|
-
"output.
|
|
316
|
-
configPath,
|
|
317
|
-
issues
|
|
318
|
-
),
|
|
319
|
-
jsonPath: readString(
|
|
320
|
-
raw.jsonPath,
|
|
321
|
-
base.jsonPath,
|
|
322
|
-
"output.jsonPath",
|
|
296
|
+
validateJsonPath: readString(
|
|
297
|
+
raw.validateJsonPath,
|
|
298
|
+
base.validateJsonPath,
|
|
299
|
+
"output.validateJsonPath",
|
|
323
300
|
configPath,
|
|
324
301
|
issues
|
|
325
302
|
)
|
|
@@ -386,20 +363,6 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
|
386
363
|
}
|
|
387
364
|
return fallback;
|
|
388
365
|
}
|
|
389
|
-
function readOutputFormat(value, fallback, label, configPath, issues) {
|
|
390
|
-
if (value === "text" || value === "json" || value === "github") {
|
|
391
|
-
return value;
|
|
392
|
-
}
|
|
393
|
-
if (value !== void 0) {
|
|
394
|
-
issues.push(
|
|
395
|
-
configIssue(
|
|
396
|
-
configPath,
|
|
397
|
-
`${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
398
|
-
)
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
return fallback;
|
|
402
|
-
}
|
|
403
366
|
function configIssue(file, message) {
|
|
404
367
|
return {
|
|
405
368
|
code: "QFAI_CONFIG_INVALID",
|
|
@@ -479,7 +442,8 @@ function isValidId(value, prefix) {
|
|
|
479
442
|
}
|
|
480
443
|
|
|
481
444
|
// src/core/report.ts
|
|
482
|
-
var
|
|
445
|
+
var import_promises11 = require("fs/promises");
|
|
446
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
483
447
|
|
|
484
448
|
// src/core/discovery.ts
|
|
485
449
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
@@ -540,10 +504,24 @@ async function exists(target) {
|
|
|
540
504
|
}
|
|
541
505
|
|
|
542
506
|
// src/core/discovery.ts
|
|
543
|
-
var
|
|
544
|
-
async function
|
|
545
|
-
const files = await collectFiles(
|
|
546
|
-
|
|
507
|
+
var SPEC_PACK_DIR_PATTERN = /^spec-\d{3}$/;
|
|
508
|
+
async function collectSpecPackDirs(specsRoot) {
|
|
509
|
+
const files = await collectFiles(specsRoot, { extensions: [".md"] });
|
|
510
|
+
const packs = /* @__PURE__ */ new Set();
|
|
511
|
+
for (const file of files) {
|
|
512
|
+
if (isSpecPackFile(file, "spec.md")) {
|
|
513
|
+
packs.add(import_node_path3.default.dirname(file));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return Array.from(packs).sort();
|
|
517
|
+
}
|
|
518
|
+
async function collectSpecFiles(specsRoot) {
|
|
519
|
+
const files = await collectFiles(specsRoot, { extensions: [".md"] });
|
|
520
|
+
return files.filter((file) => isSpecPackFile(file, "spec.md"));
|
|
521
|
+
}
|
|
522
|
+
async function collectScenarioFiles(specsRoot) {
|
|
523
|
+
const files = await collectFiles(specsRoot, { extensions: [".md"] });
|
|
524
|
+
return files.filter((file) => isSpecPackFile(file, "scenario.md"));
|
|
547
525
|
}
|
|
548
526
|
async function collectUiContractFiles(uiRoot) {
|
|
549
527
|
return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
@@ -562,9 +540,12 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
|
562
540
|
]);
|
|
563
541
|
return { ui, api, db };
|
|
564
542
|
}
|
|
565
|
-
function
|
|
566
|
-
|
|
567
|
-
|
|
543
|
+
function isSpecPackFile(filePath, baseName) {
|
|
544
|
+
if (import_node_path3.default.basename(filePath).toLowerCase() !== baseName) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
const dirName = import_node_path3.default.basename(import_node_path3.default.dirname(filePath)).toLowerCase();
|
|
548
|
+
return SPEC_PACK_DIR_PATTERN.test(dirName);
|
|
568
549
|
}
|
|
569
550
|
|
|
570
551
|
// src/core/types.ts
|
|
@@ -575,8 +556,8 @@ var import_promises3 = require("fs/promises");
|
|
|
575
556
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
576
557
|
var import_node_url = require("url");
|
|
577
558
|
async function resolveToolVersion() {
|
|
578
|
-
if ("0.
|
|
579
|
-
return "0.
|
|
559
|
+
if ("0.3.1".length > 0) {
|
|
560
|
+
return "0.3.1";
|
|
580
561
|
}
|
|
581
562
|
try {
|
|
582
563
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -596,6 +577,7 @@ function resolvePackageJsonPath() {
|
|
|
596
577
|
|
|
597
578
|
// src/core/validators/contracts.ts
|
|
598
579
|
var import_promises4 = require("fs/promises");
|
|
580
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
599
581
|
|
|
600
582
|
// src/core/contracts.ts
|
|
601
583
|
var import_node_path5 = __toESM(require("path"), 1);
|
|
@@ -651,19 +633,10 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
651
633
|
];
|
|
652
634
|
async function validateContracts(root, config) {
|
|
653
635
|
const issues = [];
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
);
|
|
657
|
-
issues.push(
|
|
658
|
-
...await validateApiContracts(
|
|
659
|
-
resolvePath(root, config, "apiContractsDir")
|
|
660
|
-
)
|
|
661
|
-
);
|
|
662
|
-
issues.push(
|
|
663
|
-
...await validateDataContracts(
|
|
664
|
-
resolvePath(root, config, "dataContractsDir")
|
|
665
|
-
)
|
|
666
|
-
);
|
|
636
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
637
|
+
issues.push(...await validateUiContracts(import_node_path6.default.join(contractsRoot, "ui")));
|
|
638
|
+
issues.push(...await validateApiContracts(import_node_path6.default.join(contractsRoot, "api")));
|
|
639
|
+
issues.push(...await validateDataContracts(import_node_path6.default.join(contractsRoot, "db")));
|
|
667
640
|
return issues;
|
|
668
641
|
}
|
|
669
642
|
async function validateUiContracts(uiRoot) {
|
|
@@ -879,33 +852,125 @@ function formatError2(error) {
|
|
|
879
852
|
return String(error);
|
|
880
853
|
}
|
|
881
854
|
function issue(code, message, severity, file, rule, refs) {
|
|
882
|
-
const
|
|
855
|
+
const issue7 = {
|
|
883
856
|
code,
|
|
884
857
|
severity,
|
|
885
858
|
message
|
|
886
859
|
};
|
|
887
860
|
if (file) {
|
|
888
|
-
|
|
861
|
+
issue7.file = file;
|
|
889
862
|
}
|
|
890
863
|
if (rule) {
|
|
891
|
-
|
|
864
|
+
issue7.rule = rule;
|
|
892
865
|
}
|
|
893
866
|
if (refs && refs.length > 0) {
|
|
894
|
-
|
|
867
|
+
issue7.refs = refs;
|
|
895
868
|
}
|
|
896
|
-
return
|
|
869
|
+
return issue7;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/core/validators/delta.ts
|
|
873
|
+
var import_promises5 = require("fs/promises");
|
|
874
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
875
|
+
var SECTION_RE = /^##\s+変更区分/m;
|
|
876
|
+
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
877
|
+
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
878
|
+
var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
|
|
879
|
+
var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
|
|
880
|
+
async function validateDeltas(root, config) {
|
|
881
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
882
|
+
const packs = await collectSpecPackDirs(specsRoot);
|
|
883
|
+
if (packs.length === 0) {
|
|
884
|
+
return [];
|
|
885
|
+
}
|
|
886
|
+
const issues = [];
|
|
887
|
+
for (const pack of packs) {
|
|
888
|
+
const deltaPath = import_node_path7.default.join(pack, "delta.md");
|
|
889
|
+
let text;
|
|
890
|
+
try {
|
|
891
|
+
text = await (0, import_promises5.readFile)(deltaPath, "utf-8");
|
|
892
|
+
} catch (error) {
|
|
893
|
+
if (isMissingFileError(error)) {
|
|
894
|
+
issues.push(
|
|
895
|
+
issue2(
|
|
896
|
+
"QFAI-DELTA-001",
|
|
897
|
+
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
898
|
+
"error",
|
|
899
|
+
deltaPath,
|
|
900
|
+
"delta.exists"
|
|
901
|
+
)
|
|
902
|
+
);
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
throw error;
|
|
906
|
+
}
|
|
907
|
+
const hasSection = SECTION_RE.test(text);
|
|
908
|
+
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
909
|
+
const hasChange = CHANGE_LINE_RE.test(text);
|
|
910
|
+
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
911
|
+
issues.push(
|
|
912
|
+
issue2(
|
|
913
|
+
"QFAI-DELTA-002",
|
|
914
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
915
|
+
"error",
|
|
916
|
+
deltaPath,
|
|
917
|
+
"delta.section"
|
|
918
|
+
)
|
|
919
|
+
);
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
923
|
+
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
924
|
+
if (compatibilityChecked === changeChecked) {
|
|
925
|
+
issues.push(
|
|
926
|
+
issue2(
|
|
927
|
+
"QFAI-DELTA-003",
|
|
928
|
+
"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",
|
|
929
|
+
"error",
|
|
930
|
+
deltaPath,
|
|
931
|
+
"delta.classification"
|
|
932
|
+
)
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return issues;
|
|
937
|
+
}
|
|
938
|
+
function isMissingFileError(error) {
|
|
939
|
+
if (!error || typeof error !== "object") {
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
return error.code === "ENOENT";
|
|
943
|
+
}
|
|
944
|
+
function issue2(code, message, severity, file, rule, refs) {
|
|
945
|
+
const issue7 = {
|
|
946
|
+
code,
|
|
947
|
+
severity,
|
|
948
|
+
message
|
|
949
|
+
};
|
|
950
|
+
if (file) {
|
|
951
|
+
issue7.file = file;
|
|
952
|
+
}
|
|
953
|
+
if (rule) {
|
|
954
|
+
issue7.rule = rule;
|
|
955
|
+
}
|
|
956
|
+
if (refs && refs.length > 0) {
|
|
957
|
+
issue7.refs = refs;
|
|
958
|
+
}
|
|
959
|
+
return issue7;
|
|
897
960
|
}
|
|
898
961
|
|
|
899
962
|
// src/core/validators/ids.ts
|
|
900
|
-
var
|
|
901
|
-
var
|
|
963
|
+
var import_promises7 = require("fs/promises");
|
|
964
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
902
965
|
|
|
903
966
|
// src/core/contractIndex.ts
|
|
904
|
-
var
|
|
967
|
+
var import_promises6 = require("fs/promises");
|
|
968
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
905
969
|
async function buildContractIndex(root, config) {
|
|
906
|
-
const
|
|
907
|
-
const
|
|
908
|
-
const
|
|
970
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
971
|
+
const uiRoot = import_node_path8.default.join(contractsRoot, "ui");
|
|
972
|
+
const apiRoot = import_node_path8.default.join(contractsRoot, "api");
|
|
973
|
+
const dataRoot = import_node_path8.default.join(contractsRoot, "db");
|
|
909
974
|
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
910
975
|
collectUiContractFiles(uiRoot),
|
|
911
976
|
collectApiContractFiles(apiRoot),
|
|
@@ -924,7 +989,7 @@ async function buildContractIndex(root, config) {
|
|
|
924
989
|
}
|
|
925
990
|
async function indexUiContracts(files, index) {
|
|
926
991
|
for (const file of files) {
|
|
927
|
-
const text = await (0,
|
|
992
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
928
993
|
try {
|
|
929
994
|
const doc = parseStructuredContract(file, text);
|
|
930
995
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -936,7 +1001,7 @@ async function indexUiContracts(files, index) {
|
|
|
936
1001
|
}
|
|
937
1002
|
async function indexApiContracts(files, index) {
|
|
938
1003
|
for (const file of files) {
|
|
939
|
-
const text = await (0,
|
|
1004
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
940
1005
|
try {
|
|
941
1006
|
const doc = parseStructuredContract(file, text);
|
|
942
1007
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -948,7 +1013,7 @@ async function indexApiContracts(files, index) {
|
|
|
948
1013
|
}
|
|
949
1014
|
async function indexDataContracts(files, index) {
|
|
950
1015
|
for (const file of files) {
|
|
951
|
-
const text = await (0,
|
|
1016
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
952
1017
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
953
1018
|
}
|
|
954
1019
|
}
|
|
@@ -959,15 +1024,191 @@ function record(index, id, file) {
|
|
|
959
1024
|
index.idToFiles.set(id, current);
|
|
960
1025
|
}
|
|
961
1026
|
|
|
1027
|
+
// src/core/parse/gherkin.ts
|
|
1028
|
+
var FEATURE_RE = /^\s*Feature:\s+/;
|
|
1029
|
+
var SCENARIO_RE = /^\s*Scenario(?: Outline)?:\s*(.+)\s*$/;
|
|
1030
|
+
var TAG_LINE_RE = /^\s*@/;
|
|
1031
|
+
function parseTags(line) {
|
|
1032
|
+
return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
|
|
1033
|
+
}
|
|
1034
|
+
function parseGherkinFeature(text, file) {
|
|
1035
|
+
const lines = text.split(/\r?\n/);
|
|
1036
|
+
const scenarios = [];
|
|
1037
|
+
let featurePresent = false;
|
|
1038
|
+
let featureTags = [];
|
|
1039
|
+
let pendingTags = [];
|
|
1040
|
+
let current = null;
|
|
1041
|
+
const flush = () => {
|
|
1042
|
+
if (!current) return;
|
|
1043
|
+
scenarios.push({
|
|
1044
|
+
...current,
|
|
1045
|
+
body: current.body.trim()
|
|
1046
|
+
});
|
|
1047
|
+
current = null;
|
|
1048
|
+
};
|
|
1049
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1050
|
+
const line = lines[i] ?? "";
|
|
1051
|
+
const trimmed = line.trim();
|
|
1052
|
+
if (TAG_LINE_RE.test(trimmed)) {
|
|
1053
|
+
pendingTags.push(...parseTags(trimmed));
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
if (FEATURE_RE.test(trimmed)) {
|
|
1057
|
+
featurePresent = true;
|
|
1058
|
+
featureTags = [...pendingTags];
|
|
1059
|
+
pendingTags = [];
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
const match = trimmed.match(SCENARIO_RE);
|
|
1063
|
+
if (match) {
|
|
1064
|
+
const scenarioName = match[1]?.trim();
|
|
1065
|
+
if (!scenarioName) {
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
flush();
|
|
1069
|
+
current = {
|
|
1070
|
+
name: scenarioName,
|
|
1071
|
+
line: i + 1,
|
|
1072
|
+
tags: [...featureTags, ...pendingTags],
|
|
1073
|
+
body: ""
|
|
1074
|
+
};
|
|
1075
|
+
pendingTags = [];
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
if (current) {
|
|
1079
|
+
current.body += `${line}
|
|
1080
|
+
`;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
flush();
|
|
1084
|
+
return { file, featurePresent, scenarios };
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// src/core/parse/markdown.ts
|
|
1088
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1089
|
+
function parseHeadings(md) {
|
|
1090
|
+
const lines = md.split(/\r?\n/);
|
|
1091
|
+
const headings = [];
|
|
1092
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1093
|
+
const line = lines[i] ?? "";
|
|
1094
|
+
const match = line.match(HEADING_RE);
|
|
1095
|
+
if (!match) continue;
|
|
1096
|
+
const levelToken = match[1];
|
|
1097
|
+
const title = match[2];
|
|
1098
|
+
if (!levelToken || !title) continue;
|
|
1099
|
+
headings.push({
|
|
1100
|
+
level: levelToken.length,
|
|
1101
|
+
title: title.trim(),
|
|
1102
|
+
line: i + 1
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
return headings;
|
|
1106
|
+
}
|
|
1107
|
+
function extractH2Sections(md) {
|
|
1108
|
+
const lines = md.split(/\r?\n/);
|
|
1109
|
+
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1110
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1111
|
+
for (let i = 0; i < headings.length; i++) {
|
|
1112
|
+
const current = headings[i];
|
|
1113
|
+
if (!current) continue;
|
|
1114
|
+
const next = headings[i + 1];
|
|
1115
|
+
const startLine = current.line + 1;
|
|
1116
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1117
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1118
|
+
sections.set(current.title.trim(), {
|
|
1119
|
+
title: current.title.trim(),
|
|
1120
|
+
startLine,
|
|
1121
|
+
endLine,
|
|
1122
|
+
body
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
return sections;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// src/core/parse/spec.ts
|
|
1129
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1130
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1131
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1132
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1133
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1134
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1135
|
+
function parseSpec(md, file) {
|
|
1136
|
+
const headings = parseHeadings(md);
|
|
1137
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
1138
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1139
|
+
const sections = extractH2Sections(md);
|
|
1140
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1141
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1142
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1143
|
+
const startLine = brSection?.startLine ?? 1;
|
|
1144
|
+
const brs = [];
|
|
1145
|
+
const brsWithoutPriority = [];
|
|
1146
|
+
const brsWithInvalidPriority = [];
|
|
1147
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
1148
|
+
const lineText = brLines[i] ?? "";
|
|
1149
|
+
const lineNumber = startLine + i;
|
|
1150
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
1151
|
+
if (validMatch) {
|
|
1152
|
+
const id = validMatch[1];
|
|
1153
|
+
const priority = validMatch[2];
|
|
1154
|
+
const text = validMatch[3];
|
|
1155
|
+
if (!id || !priority || !text) continue;
|
|
1156
|
+
brs.push({
|
|
1157
|
+
id,
|
|
1158
|
+
priority,
|
|
1159
|
+
text: text.trim(),
|
|
1160
|
+
line: lineNumber
|
|
1161
|
+
});
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1165
|
+
if (anyPriorityMatch) {
|
|
1166
|
+
const id = anyPriorityMatch[1];
|
|
1167
|
+
const priority = anyPriorityMatch[2];
|
|
1168
|
+
const text = anyPriorityMatch[3];
|
|
1169
|
+
if (!id || !priority || !text) continue;
|
|
1170
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
1171
|
+
brsWithInvalidPriority.push({
|
|
1172
|
+
id,
|
|
1173
|
+
priority,
|
|
1174
|
+
text: text.trim(),
|
|
1175
|
+
line: lineNumber
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1181
|
+
if (noPriorityMatch) {
|
|
1182
|
+
const id = noPriorityMatch[1];
|
|
1183
|
+
const text = noPriorityMatch[2];
|
|
1184
|
+
if (!id || !text) continue;
|
|
1185
|
+
brsWithoutPriority.push({
|
|
1186
|
+
id,
|
|
1187
|
+
text: text.trim(),
|
|
1188
|
+
line: lineNumber
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
const parsed = {
|
|
1193
|
+
file,
|
|
1194
|
+
sections: sectionNames,
|
|
1195
|
+
brs,
|
|
1196
|
+
brsWithoutPriority,
|
|
1197
|
+
brsWithInvalidPriority
|
|
1198
|
+
};
|
|
1199
|
+
if (specId) {
|
|
1200
|
+
parsed.specId = specId;
|
|
1201
|
+
}
|
|
1202
|
+
return parsed;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
962
1205
|
// src/core/validators/ids.ts
|
|
1206
|
+
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
963
1207
|
async function validateDefinedIds(root, config) {
|
|
964
1208
|
const issues = [];
|
|
965
|
-
const
|
|
966
|
-
const
|
|
967
|
-
const
|
|
968
|
-
const scenarioFiles = await collectFiles(scenarioRoot, {
|
|
969
|
-
extensions: [".feature"]
|
|
970
|
-
});
|
|
1209
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1210
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
1211
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
971
1212
|
const defined = /* @__PURE__ */ new Map();
|
|
972
1213
|
await collectSpecDefinitionIds(specFiles, defined);
|
|
973
1214
|
await collectScenarioDefinitionIds(scenarioFiles, defined);
|
|
@@ -983,7 +1224,7 @@ async function validateDefinedIds(root, config) {
|
|
|
983
1224
|
}
|
|
984
1225
|
const sorted = Array.from(files).sort();
|
|
985
1226
|
issues.push(
|
|
986
|
-
|
|
1227
|
+
issue3(
|
|
987
1228
|
"QFAI-ID-001",
|
|
988
1229
|
`ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
|
|
989
1230
|
"error",
|
|
@@ -996,15 +1237,25 @@ async function validateDefinedIds(root, config) {
|
|
|
996
1237
|
}
|
|
997
1238
|
async function collectSpecDefinitionIds(files, out) {
|
|
998
1239
|
for (const file of files) {
|
|
999
|
-
const text = await (0,
|
|
1000
|
-
|
|
1001
|
-
|
|
1240
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
1241
|
+
const parsed = parseSpec(text, file);
|
|
1242
|
+
if (parsed.specId) {
|
|
1243
|
+
recordId(out, parsed.specId, file);
|
|
1244
|
+
}
|
|
1245
|
+
parsed.brs.forEach((br) => recordId(out, br.id, file));
|
|
1002
1246
|
}
|
|
1003
1247
|
}
|
|
1004
1248
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1005
1249
|
for (const file of files) {
|
|
1006
|
-
const text = await (0,
|
|
1007
|
-
|
|
1250
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
1251
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1252
|
+
for (const scenario of parsed.scenarios) {
|
|
1253
|
+
for (const tag of scenario.tags) {
|
|
1254
|
+
if (SC_TAG_RE.test(tag)) {
|
|
1255
|
+
recordId(out, tag, file);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1008
1259
|
}
|
|
1009
1260
|
}
|
|
1010
1261
|
function recordId(out, id, file) {
|
|
@@ -1014,58 +1265,60 @@ function recordId(out, id, file) {
|
|
|
1014
1265
|
}
|
|
1015
1266
|
function formatFileList(files, root) {
|
|
1016
1267
|
return files.map((file) => {
|
|
1017
|
-
const relative =
|
|
1268
|
+
const relative = import_node_path9.default.relative(root, file);
|
|
1018
1269
|
return relative.length > 0 ? relative : file;
|
|
1019
1270
|
}).join(", ");
|
|
1020
1271
|
}
|
|
1021
|
-
function
|
|
1022
|
-
const
|
|
1272
|
+
function issue3(code, message, severity, file, rule, refs) {
|
|
1273
|
+
const issue7 = {
|
|
1023
1274
|
code,
|
|
1024
1275
|
severity,
|
|
1025
1276
|
message
|
|
1026
1277
|
};
|
|
1027
1278
|
if (file) {
|
|
1028
|
-
|
|
1279
|
+
issue7.file = file;
|
|
1029
1280
|
}
|
|
1030
1281
|
if (rule) {
|
|
1031
|
-
|
|
1282
|
+
issue7.rule = rule;
|
|
1032
1283
|
}
|
|
1033
1284
|
if (refs && refs.length > 0) {
|
|
1034
|
-
|
|
1285
|
+
issue7.refs = refs;
|
|
1035
1286
|
}
|
|
1036
|
-
return
|
|
1287
|
+
return issue7;
|
|
1037
1288
|
}
|
|
1038
1289
|
|
|
1039
1290
|
// src/core/validators/scenario.ts
|
|
1040
|
-
var
|
|
1291
|
+
var import_promises8 = require("fs/promises");
|
|
1041
1292
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1042
1293
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1043
1294
|
var THEN_PATTERN = /\bThen\b/;
|
|
1295
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1296
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1297
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1044
1298
|
async function validateScenarios(root, config) {
|
|
1045
|
-
const
|
|
1046
|
-
const files = await
|
|
1047
|
-
extensions: [".feature"]
|
|
1048
|
-
});
|
|
1299
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1300
|
+
const files = await collectScenarioFiles(specsRoot);
|
|
1049
1301
|
if (files.length === 0) {
|
|
1050
1302
|
return [
|
|
1051
|
-
|
|
1303
|
+
issue4(
|
|
1052
1304
|
"QFAI-SC-000",
|
|
1053
1305
|
"Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1054
1306
|
"info",
|
|
1055
|
-
|
|
1307
|
+
specsRoot,
|
|
1056
1308
|
"scenario.files"
|
|
1057
1309
|
)
|
|
1058
1310
|
];
|
|
1059
1311
|
}
|
|
1060
1312
|
const issues = [];
|
|
1061
1313
|
for (const file of files) {
|
|
1062
|
-
const text = await (0,
|
|
1314
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
1063
1315
|
issues.push(...validateScenarioContent(text, file));
|
|
1064
1316
|
}
|
|
1065
1317
|
return issues;
|
|
1066
1318
|
}
|
|
1067
1319
|
function validateScenarioContent(text, file) {
|
|
1068
1320
|
const issues = [];
|
|
1321
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1069
1322
|
const invalidIds = extractInvalidIds(text, [
|
|
1070
1323
|
"SPEC",
|
|
1071
1324
|
"BR",
|
|
@@ -1077,7 +1330,7 @@ function validateScenarioContent(text, file) {
|
|
|
1077
1330
|
]);
|
|
1078
1331
|
if (invalidIds.length > 0) {
|
|
1079
1332
|
issues.push(
|
|
1080
|
-
|
|
1333
|
+
issue4(
|
|
1081
1334
|
"QFAI-ID-002",
|
|
1082
1335
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
1083
1336
|
"error",
|
|
@@ -1087,94 +1340,114 @@ function validateScenarioContent(text, file) {
|
|
|
1087
1340
|
)
|
|
1088
1341
|
);
|
|
1089
1342
|
}
|
|
1090
|
-
const
|
|
1091
|
-
if (
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
"QFAI-SC-001",
|
|
1095
|
-
"SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1096
|
-
"error",
|
|
1097
|
-
file,
|
|
1098
|
-
"scenario.id"
|
|
1099
|
-
)
|
|
1100
|
-
);
|
|
1101
|
-
}
|
|
1102
|
-
const specIds = extractIds(text, "SPEC");
|
|
1103
|
-
if (specIds.length === 0) {
|
|
1104
|
-
issues.push(
|
|
1105
|
-
issue3(
|
|
1106
|
-
"QFAI-SC-002",
|
|
1107
|
-
"SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
1108
|
-
"error",
|
|
1109
|
-
file,
|
|
1110
|
-
"scenario.spec"
|
|
1111
|
-
)
|
|
1112
|
-
);
|
|
1113
|
-
}
|
|
1114
|
-
const brIds = extractIds(text, "BR");
|
|
1115
|
-
if (brIds.length === 0) {
|
|
1343
|
+
const missingStructure = [];
|
|
1344
|
+
if (!parsed.featurePresent) missingStructure.push("Feature");
|
|
1345
|
+
if (parsed.scenarios.length === 0) missingStructure.push("Scenario");
|
|
1346
|
+
if (missingStructure.length > 0) {
|
|
1116
1347
|
issues.push(
|
|
1117
|
-
|
|
1118
|
-
"QFAI-SC-
|
|
1119
|
-
|
|
1348
|
+
issue4(
|
|
1349
|
+
"QFAI-SC-006",
|
|
1350
|
+
`Scenario \u30D5\u30A1\u30A4\u30EB\u306B\u5FC5\u8981\u306A\u69CB\u9020\u304C\u3042\u308A\u307E\u305B\u3093: ${missingStructure.join(
|
|
1351
|
+
", "
|
|
1352
|
+
)}`,
|
|
1120
1353
|
"error",
|
|
1121
1354
|
file,
|
|
1122
|
-
"scenario.
|
|
1355
|
+
"scenario.structure"
|
|
1123
1356
|
)
|
|
1124
1357
|
);
|
|
1125
1358
|
}
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1359
|
+
for (const scenario of parsed.scenarios) {
|
|
1360
|
+
if (scenario.tags.length === 0) {
|
|
1361
|
+
issues.push(
|
|
1362
|
+
issue4(
|
|
1363
|
+
"QFAI-SC-007",
|
|
1364
|
+
`Scenario \u30BF\u30B0\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${scenario.name}`,
|
|
1365
|
+
"error",
|
|
1366
|
+
file,
|
|
1367
|
+
"scenario.tags"
|
|
1368
|
+
)
|
|
1369
|
+
);
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
const missingTags = [];
|
|
1373
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
|
|
1374
|
+
if (scTags.length === 0) {
|
|
1375
|
+
missingTags.push("SC(0\u4EF6)");
|
|
1376
|
+
} else if (scTags.length > 1) {
|
|
1377
|
+
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1378
|
+
}
|
|
1379
|
+
if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
|
|
1380
|
+
missingTags.push("SPEC");
|
|
1381
|
+
}
|
|
1382
|
+
if (!scenario.tags.some((tag) => BR_TAG_RE.test(tag))) {
|
|
1383
|
+
missingTags.push("BR");
|
|
1384
|
+
}
|
|
1385
|
+
if (missingTags.length > 0) {
|
|
1386
|
+
issues.push(
|
|
1387
|
+
issue4(
|
|
1388
|
+
"QFAI-SC-008",
|
|
1389
|
+
`Scenario \u30BF\u30B0\u306B\u4E0D\u8DB3\u304C\u3042\u308A\u307E\u3059: ${missingTags.join(", ")} (${scenario.name})`,
|
|
1390
|
+
"error",
|
|
1391
|
+
file,
|
|
1392
|
+
"scenario.tagIds"
|
|
1393
|
+
)
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1135
1396
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1397
|
+
for (const scenario of parsed.scenarios) {
|
|
1398
|
+
const missingSteps = [];
|
|
1399
|
+
if (!GIVEN_PATTERN.test(scenario.body)) {
|
|
1400
|
+
missingSteps.push("Given");
|
|
1401
|
+
}
|
|
1402
|
+
if (!WHEN_PATTERN.test(scenario.body)) {
|
|
1403
|
+
missingSteps.push("When");
|
|
1404
|
+
}
|
|
1405
|
+
if (!THEN_PATTERN.test(scenario.body)) {
|
|
1406
|
+
missingSteps.push("Then");
|
|
1407
|
+
}
|
|
1408
|
+
if (missingSteps.length > 0) {
|
|
1409
|
+
issues.push(
|
|
1410
|
+
issue4(
|
|
1411
|
+
"QFAI-SC-005",
|
|
1412
|
+
`Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")} (${scenario.name})`,
|
|
1413
|
+
"warning",
|
|
1414
|
+
file,
|
|
1415
|
+
"scenario.steps"
|
|
1416
|
+
)
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1146
1419
|
}
|
|
1147
1420
|
return issues;
|
|
1148
1421
|
}
|
|
1149
|
-
function
|
|
1150
|
-
const
|
|
1422
|
+
function issue4(code, message, severity, file, rule, refs) {
|
|
1423
|
+
const issue7 = {
|
|
1151
1424
|
code,
|
|
1152
1425
|
severity,
|
|
1153
1426
|
message
|
|
1154
1427
|
};
|
|
1155
1428
|
if (file) {
|
|
1156
|
-
|
|
1429
|
+
issue7.file = file;
|
|
1157
1430
|
}
|
|
1158
1431
|
if (rule) {
|
|
1159
|
-
|
|
1432
|
+
issue7.rule = rule;
|
|
1160
1433
|
}
|
|
1161
1434
|
if (refs && refs.length > 0) {
|
|
1162
|
-
|
|
1435
|
+
issue7.refs = refs;
|
|
1163
1436
|
}
|
|
1164
|
-
return
|
|
1437
|
+
return issue7;
|
|
1165
1438
|
}
|
|
1166
1439
|
|
|
1167
1440
|
// src/core/validators/spec.ts
|
|
1168
|
-
var
|
|
1441
|
+
var import_promises9 = require("fs/promises");
|
|
1169
1442
|
async function validateSpecs(root, config) {
|
|
1170
|
-
const specsRoot = resolvePath(root, config, "
|
|
1443
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1171
1444
|
const files = await collectSpecFiles(specsRoot);
|
|
1172
1445
|
if (files.length === 0) {
|
|
1173
|
-
const expected = "spec-
|
|
1446
|
+
const expected = "spec-001/spec.md";
|
|
1174
1447
|
return [
|
|
1175
|
-
|
|
1448
|
+
issue5(
|
|
1176
1449
|
"QFAI-SPEC-000",
|
|
1177
|
-
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.
|
|
1450
|
+
`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}`,
|
|
1178
1451
|
"info",
|
|
1179
1452
|
specsRoot,
|
|
1180
1453
|
"spec.files"
|
|
@@ -1183,7 +1456,7 @@ async function validateSpecs(root, config) {
|
|
|
1183
1456
|
}
|
|
1184
1457
|
const issues = [];
|
|
1185
1458
|
for (const file of files) {
|
|
1186
|
-
const text = await (0,
|
|
1459
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1187
1460
|
issues.push(
|
|
1188
1461
|
...validateSpecContent(
|
|
1189
1462
|
text,
|
|
@@ -1196,6 +1469,7 @@ async function validateSpecs(root, config) {
|
|
|
1196
1469
|
}
|
|
1197
1470
|
function validateSpecContent(text, file, requiredSections) {
|
|
1198
1471
|
const issues = [];
|
|
1472
|
+
const parsed = parseSpec(text, file);
|
|
1199
1473
|
const invalidIds = extractInvalidIds(text, [
|
|
1200
1474
|
"SPEC",
|
|
1201
1475
|
"BR",
|
|
@@ -1207,7 +1481,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1207
1481
|
]);
|
|
1208
1482
|
if (invalidIds.length > 0) {
|
|
1209
1483
|
issues.push(
|
|
1210
|
-
|
|
1484
|
+
issue5(
|
|
1211
1485
|
"QFAI-ID-002",
|
|
1212
1486
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
1213
1487
|
"error",
|
|
@@ -1217,10 +1491,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1217
1491
|
)
|
|
1218
1492
|
);
|
|
1219
1493
|
}
|
|
1220
|
-
|
|
1221
|
-
if (specIds.length === 0) {
|
|
1494
|
+
if (!parsed.specId) {
|
|
1222
1495
|
issues.push(
|
|
1223
|
-
|
|
1496
|
+
issue5(
|
|
1224
1497
|
"QFAI-SPEC-001",
|
|
1225
1498
|
"SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1226
1499
|
"error",
|
|
@@ -1229,10 +1502,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1229
1502
|
)
|
|
1230
1503
|
);
|
|
1231
1504
|
}
|
|
1232
|
-
|
|
1233
|
-
if (brIds.length === 0) {
|
|
1505
|
+
if (parsed.brs.length === 0) {
|
|
1234
1506
|
issues.push(
|
|
1235
|
-
|
|
1507
|
+
issue5(
|
|
1236
1508
|
"QFAI-SPEC-002",
|
|
1237
1509
|
"BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1238
1510
|
"error",
|
|
@@ -1241,10 +1513,34 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1241
1513
|
)
|
|
1242
1514
|
);
|
|
1243
1515
|
}
|
|
1516
|
+
for (const br of parsed.brsWithoutPriority) {
|
|
1517
|
+
issues.push(
|
|
1518
|
+
issue5(
|
|
1519
|
+
"QFAI-BR-001",
|
|
1520
|
+
`BR \u884C\u306B Priority \u304C\u3042\u308A\u307E\u305B\u3093: ${br.id}`,
|
|
1521
|
+
"error",
|
|
1522
|
+
file,
|
|
1523
|
+
"spec.brPriority",
|
|
1524
|
+
[br.id]
|
|
1525
|
+
)
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
for (const br of parsed.brsWithInvalidPriority) {
|
|
1529
|
+
issues.push(
|
|
1530
|
+
issue5(
|
|
1531
|
+
"QFAI-BR-002",
|
|
1532
|
+
`BR Priority \u304C\u4E0D\u6B63\u3067\u3059: ${br.id} (${br.priority})`,
|
|
1533
|
+
"error",
|
|
1534
|
+
file,
|
|
1535
|
+
"spec.brPriority",
|
|
1536
|
+
[br.id]
|
|
1537
|
+
)
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1244
1540
|
const scIds = extractIds(text, "SC");
|
|
1245
1541
|
if (scIds.length > 0) {
|
|
1246
1542
|
issues.push(
|
|
1247
|
-
|
|
1543
|
+
issue5(
|
|
1248
1544
|
"QFAI-SPEC-003",
|
|
1249
1545
|
"Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
|
|
1250
1546
|
"warning",
|
|
@@ -1255,9 +1551,9 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1255
1551
|
);
|
|
1256
1552
|
}
|
|
1257
1553
|
for (const section of requiredSections) {
|
|
1258
|
-
if (!
|
|
1554
|
+
if (!parsed.sections.has(section)) {
|
|
1259
1555
|
issues.push(
|
|
1260
|
-
|
|
1556
|
+
issue5(
|
|
1261
1557
|
"QFAI-SPEC-004",
|
|
1262
1558
|
`\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
|
|
1263
1559
|
"error",
|
|
@@ -1269,40 +1565,39 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1269
1565
|
}
|
|
1270
1566
|
return issues;
|
|
1271
1567
|
}
|
|
1272
|
-
function
|
|
1273
|
-
const
|
|
1568
|
+
function issue5(code, message, severity, file, rule, refs) {
|
|
1569
|
+
const issue7 = {
|
|
1274
1570
|
code,
|
|
1275
1571
|
severity,
|
|
1276
1572
|
message
|
|
1277
1573
|
};
|
|
1278
1574
|
if (file) {
|
|
1279
|
-
|
|
1575
|
+
issue7.file = file;
|
|
1280
1576
|
}
|
|
1281
1577
|
if (rule) {
|
|
1282
|
-
|
|
1578
|
+
issue7.rule = rule;
|
|
1283
1579
|
}
|
|
1284
1580
|
if (refs && refs.length > 0) {
|
|
1285
|
-
|
|
1581
|
+
issue7.refs = refs;
|
|
1286
1582
|
}
|
|
1287
|
-
return
|
|
1583
|
+
return issue7;
|
|
1288
1584
|
}
|
|
1289
1585
|
|
|
1290
1586
|
// src/core/validators/traceability.ts
|
|
1291
|
-
var
|
|
1587
|
+
var import_promises10 = require("fs/promises");
|
|
1588
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1589
|
+
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1590
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1591
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1592
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
1593
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1292
1594
|
async function validateTraceability(root, config) {
|
|
1293
1595
|
const issues = [];
|
|
1294
|
-
const specsRoot = resolvePath(root, config, "
|
|
1295
|
-
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1296
|
-
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1596
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1297
1597
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1298
1598
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1299
1599
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
1300
|
-
const
|
|
1301
|
-
extensions: [".md"]
|
|
1302
|
-
});
|
|
1303
|
-
const scenarioFiles = await collectFiles(scenariosRoot, {
|
|
1304
|
-
extensions: [".feature"]
|
|
1305
|
-
});
|
|
1600
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1306
1601
|
const upstreamIds = /* @__PURE__ */ new Set();
|
|
1307
1602
|
const specIds = /* @__PURE__ */ new Set();
|
|
1308
1603
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
@@ -1314,11 +1609,13 @@ async function validateTraceability(root, config) {
|
|
|
1314
1609
|
const contractIndex = await buildContractIndex(root, config);
|
|
1315
1610
|
const contractIds = contractIndex.ids;
|
|
1316
1611
|
for (const file of specFiles) {
|
|
1317
|
-
const text = await (0,
|
|
1612
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1318
1613
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1614
|
+
const parsed = parseSpec(text, file);
|
|
1615
|
+
if (parsed.specId) {
|
|
1616
|
+
specIds.add(parsed.specId);
|
|
1617
|
+
}
|
|
1618
|
+
const brIds = parsed.brs.map((br) => br.id);
|
|
1322
1619
|
brIds.forEach((id) => brIdsInSpecs.add(id));
|
|
1323
1620
|
const referencedContractIds = /* @__PURE__ */ new Set([
|
|
1324
1621
|
...extractIds(text, "UI"),
|
|
@@ -1330,7 +1627,7 @@ async function validateTraceability(root, config) {
|
|
|
1330
1627
|
);
|
|
1331
1628
|
if (unknownContractIds.length > 0) {
|
|
1332
1629
|
issues.push(
|
|
1333
|
-
|
|
1630
|
+
issue6(
|
|
1334
1631
|
"QFAI-TRACE-009",
|
|
1335
1632
|
`Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1336
1633
|
", "
|
|
@@ -1342,37 +1639,50 @@ async function validateTraceability(root, config) {
|
|
|
1342
1639
|
)
|
|
1343
1640
|
);
|
|
1344
1641
|
}
|
|
1345
|
-
|
|
1346
|
-
const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
|
|
1642
|
+
if (parsed.specId) {
|
|
1643
|
+
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
1347
1644
|
brIds.forEach((id) => current.add(id));
|
|
1348
|
-
specToBrIds.set(specId, current);
|
|
1645
|
+
specToBrIds.set(parsed.specId, current);
|
|
1349
1646
|
}
|
|
1350
1647
|
}
|
|
1351
|
-
for (const file of decisionFiles) {
|
|
1352
|
-
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1353
|
-
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1354
|
-
}
|
|
1355
1648
|
for (const file of scenarioFiles) {
|
|
1356
|
-
const text = await (0,
|
|
1649
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1357
1650
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1358
|
-
const
|
|
1359
|
-
const
|
|
1360
|
-
const
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1651
|
+
const parsed = parseGherkinFeature(text, file);
|
|
1652
|
+
const specIdsInScenario = /* @__PURE__ */ new Set();
|
|
1653
|
+
const brIds = /* @__PURE__ */ new Set();
|
|
1654
|
+
const scIds = /* @__PURE__ */ new Set();
|
|
1655
|
+
const scenarioIds = /* @__PURE__ */ new Set();
|
|
1656
|
+
for (const scenario of parsed.scenarios) {
|
|
1657
|
+
for (const tag of scenario.tags) {
|
|
1658
|
+
if (SPEC_TAG_RE2.test(tag)) {
|
|
1659
|
+
specIdsInScenario.add(tag);
|
|
1660
|
+
}
|
|
1661
|
+
if (BR_TAG_RE2.test(tag)) {
|
|
1662
|
+
brIds.add(tag);
|
|
1663
|
+
}
|
|
1664
|
+
if (SC_TAG_RE3.test(tag)) {
|
|
1665
|
+
scIds.add(tag);
|
|
1666
|
+
}
|
|
1667
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1668
|
+
scenarioIds.add(tag);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1371
1671
|
}
|
|
1372
|
-
const
|
|
1672
|
+
const specIdsList = Array.from(specIdsInScenario);
|
|
1673
|
+
const brIdsList = Array.from(brIds);
|
|
1674
|
+
const scIdsList = Array.from(scIds);
|
|
1675
|
+
const scenarioIdsList = Array.from(scenarioIds);
|
|
1676
|
+
brIdsList.forEach((id) => brIdsInScenarios.add(id));
|
|
1677
|
+
scIdsList.forEach((id) => scIdsInScenarios.add(id));
|
|
1678
|
+
scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
|
|
1679
|
+
if (scenarioIdsList.length > 0) {
|
|
1680
|
+
scIdsList.forEach((id) => scWithContracts.add(id));
|
|
1681
|
+
}
|
|
1682
|
+
const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
|
|
1373
1683
|
if (unknownSpecIds.length > 0) {
|
|
1374
1684
|
issues.push(
|
|
1375
|
-
|
|
1685
|
+
issue6(
|
|
1376
1686
|
"QFAI-TRACE-005",
|
|
1377
1687
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1378
1688
|
"error",
|
|
@@ -1382,10 +1692,10 @@ async function validateTraceability(root, config) {
|
|
|
1382
1692
|
)
|
|
1383
1693
|
);
|
|
1384
1694
|
}
|
|
1385
|
-
const unknownBrIds =
|
|
1695
|
+
const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
|
|
1386
1696
|
if (unknownBrIds.length > 0) {
|
|
1387
1697
|
issues.push(
|
|
1388
|
-
|
|
1698
|
+
issue6(
|
|
1389
1699
|
"QFAI-TRACE-006",
|
|
1390
1700
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1391
1701
|
"error",
|
|
@@ -1395,10 +1705,12 @@ async function validateTraceability(root, config) {
|
|
|
1395
1705
|
)
|
|
1396
1706
|
);
|
|
1397
1707
|
}
|
|
1398
|
-
const unknownContractIds =
|
|
1708
|
+
const unknownContractIds = scenarioIdsList.filter(
|
|
1709
|
+
(id) => !contractIds.has(id)
|
|
1710
|
+
);
|
|
1399
1711
|
if (unknownContractIds.length > 0) {
|
|
1400
1712
|
issues.push(
|
|
1401
|
-
|
|
1713
|
+
issue6(
|
|
1402
1714
|
"QFAI-TRACE-008",
|
|
1403
1715
|
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1404
1716
|
", "
|
|
@@ -1410,23 +1722,23 @@ async function validateTraceability(root, config) {
|
|
|
1410
1722
|
)
|
|
1411
1723
|
);
|
|
1412
1724
|
}
|
|
1413
|
-
if (
|
|
1725
|
+
if (specIdsList.length > 0) {
|
|
1414
1726
|
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
1415
|
-
for (const specId of
|
|
1727
|
+
for (const specId of specIdsList) {
|
|
1416
1728
|
const brIdsForSpec = specToBrIds.get(specId);
|
|
1417
1729
|
if (!brIdsForSpec) {
|
|
1418
1730
|
continue;
|
|
1419
1731
|
}
|
|
1420
1732
|
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
1421
1733
|
}
|
|
1422
|
-
const invalidBrIds =
|
|
1734
|
+
const invalidBrIds = brIdsList.filter((id) => !allowedBrIds.has(id));
|
|
1423
1735
|
if (invalidBrIds.length > 0) {
|
|
1424
1736
|
issues.push(
|
|
1425
|
-
|
|
1737
|
+
issue6(
|
|
1426
1738
|
"QFAI-TRACE-007",
|
|
1427
1739
|
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
1428
1740
|
", "
|
|
1429
|
-
)} (SPEC: ${
|
|
1741
|
+
)} (SPEC: ${specIdsList.join(", ")})`,
|
|
1430
1742
|
"error",
|
|
1431
1743
|
file,
|
|
1432
1744
|
"traceability.scenarioBrUnderSpec",
|
|
@@ -1438,7 +1750,7 @@ async function validateTraceability(root, config) {
|
|
|
1438
1750
|
}
|
|
1439
1751
|
if (upstreamIds.size === 0) {
|
|
1440
1752
|
return [
|
|
1441
|
-
|
|
1753
|
+
issue6(
|
|
1442
1754
|
"QFAI-TRACE-000",
|
|
1443
1755
|
"\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1444
1756
|
"info",
|
|
@@ -1453,7 +1765,7 @@ async function validateTraceability(root, config) {
|
|
|
1453
1765
|
);
|
|
1454
1766
|
if (orphanBrIds.length > 0) {
|
|
1455
1767
|
issues.push(
|
|
1456
|
-
|
|
1768
|
+
issue6(
|
|
1457
1769
|
"QFAI_TRACE_BR_ORPHAN",
|
|
1458
1770
|
`BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
|
|
1459
1771
|
"error",
|
|
@@ -1470,13 +1782,13 @@ async function validateTraceability(root, config) {
|
|
|
1470
1782
|
);
|
|
1471
1783
|
if (scWithoutContracts.length > 0) {
|
|
1472
1784
|
issues.push(
|
|
1473
|
-
|
|
1785
|
+
issue6(
|
|
1474
1786
|
"QFAI_TRACE_SC_NO_CONTRACT",
|
|
1475
1787
|
`SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
|
|
1476
1788
|
", "
|
|
1477
1789
|
)}`,
|
|
1478
1790
|
"error",
|
|
1479
|
-
|
|
1791
|
+
specsRoot,
|
|
1480
1792
|
"traceability.scMustTouchContracts",
|
|
1481
1793
|
scWithoutContracts
|
|
1482
1794
|
)
|
|
@@ -1490,11 +1802,11 @@ async function validateTraceability(root, config) {
|
|
|
1490
1802
|
);
|
|
1491
1803
|
if (orphanContracts.length > 0) {
|
|
1492
1804
|
issues.push(
|
|
1493
|
-
|
|
1805
|
+
issue6(
|
|
1494
1806
|
"QFAI_CONTRACT_ORPHAN",
|
|
1495
1807
|
`\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
1496
1808
|
"error",
|
|
1497
|
-
|
|
1809
|
+
specsRoot,
|
|
1498
1810
|
"traceability.allowOrphanContracts",
|
|
1499
1811
|
orphanContracts
|
|
1500
1812
|
)
|
|
@@ -1518,7 +1830,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1518
1830
|
const targetFiles = [...codeFiles, ...testFiles];
|
|
1519
1831
|
if (targetFiles.length === 0) {
|
|
1520
1832
|
issues.push(
|
|
1521
|
-
|
|
1833
|
+
issue6(
|
|
1522
1834
|
"QFAI-TRACE-001",
|
|
1523
1835
|
"\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1524
1836
|
"info",
|
|
@@ -1531,7 +1843,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1531
1843
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
1532
1844
|
let found = false;
|
|
1533
1845
|
for (const file of targetFiles) {
|
|
1534
|
-
const text = await (0,
|
|
1846
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1535
1847
|
if (pattern.test(text)) {
|
|
1536
1848
|
found = true;
|
|
1537
1849
|
break;
|
|
@@ -1539,7 +1851,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1539
1851
|
}
|
|
1540
1852
|
if (!found) {
|
|
1541
1853
|
issues.push(
|
|
1542
|
-
|
|
1854
|
+
issue6(
|
|
1543
1855
|
"QFAI-TRACE-002",
|
|
1544
1856
|
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
1545
1857
|
"warning",
|
|
@@ -1554,22 +1866,22 @@ function buildIdPattern(ids) {
|
|
|
1554
1866
|
const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
1555
1867
|
return new RegExp(`\\b(${escaped.join("|")})\\b`);
|
|
1556
1868
|
}
|
|
1557
|
-
function
|
|
1558
|
-
const
|
|
1869
|
+
function issue6(code, message, severity, file, rule, refs) {
|
|
1870
|
+
const issue7 = {
|
|
1559
1871
|
code,
|
|
1560
1872
|
severity,
|
|
1561
1873
|
message
|
|
1562
1874
|
};
|
|
1563
1875
|
if (file) {
|
|
1564
|
-
|
|
1876
|
+
issue7.file = file;
|
|
1565
1877
|
}
|
|
1566
1878
|
if (rule) {
|
|
1567
|
-
|
|
1879
|
+
issue7.rule = rule;
|
|
1568
1880
|
}
|
|
1569
1881
|
if (refs && refs.length > 0) {
|
|
1570
|
-
|
|
1882
|
+
issue7.refs = refs;
|
|
1571
1883
|
}
|
|
1572
|
-
return
|
|
1884
|
+
return issue7;
|
|
1573
1885
|
}
|
|
1574
1886
|
|
|
1575
1887
|
// src/core/validate.ts
|
|
@@ -1579,6 +1891,7 @@ async function validateProject(root, configResult) {
|
|
|
1579
1891
|
const issues = [
|
|
1580
1892
|
...configIssues,
|
|
1581
1893
|
...await validateSpecs(root, config),
|
|
1894
|
+
...await validateDeltas(root, config),
|
|
1582
1895
|
...await validateScenarios(root, config),
|
|
1583
1896
|
...await validateContracts(root, config),
|
|
1584
1897
|
...await validateDefinedIds(root, config),
|
|
@@ -1594,8 +1907,8 @@ async function validateProject(root, configResult) {
|
|
|
1594
1907
|
}
|
|
1595
1908
|
function countIssues(issues) {
|
|
1596
1909
|
return issues.reduce(
|
|
1597
|
-
(acc,
|
|
1598
|
-
acc[
|
|
1910
|
+
(acc, issue7) => {
|
|
1911
|
+
acc[issue7.severity] += 1;
|
|
1599
1912
|
return acc;
|
|
1600
1913
|
},
|
|
1601
1914
|
{ info: 0, warning: 0, error: 0 }
|
|
@@ -1608,21 +1921,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
1608
1921
|
const resolved = configResult ?? await loadConfig(root);
|
|
1609
1922
|
const config = resolved.config;
|
|
1610
1923
|
const configPath = resolved.configPath;
|
|
1611
|
-
const
|
|
1612
|
-
const
|
|
1613
|
-
const
|
|
1614
|
-
const
|
|
1615
|
-
const
|
|
1616
|
-
const dbRoot = resolvePath(root, config, "dataContractsDir");
|
|
1924
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1925
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1926
|
+
const apiRoot = import_node_path10.default.join(contractsRoot, "api");
|
|
1927
|
+
const uiRoot = import_node_path10.default.join(contractsRoot, "ui");
|
|
1928
|
+
const dbRoot = import_node_path10.default.join(contractsRoot, "db");
|
|
1617
1929
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1618
1930
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1619
|
-
const specFiles = await collectSpecFiles(
|
|
1620
|
-
const scenarioFiles = await
|
|
1621
|
-
extensions: [".feature"]
|
|
1622
|
-
});
|
|
1623
|
-
const decisionFiles = await collectFiles(decisionsRoot, {
|
|
1624
|
-
extensions: [".md"]
|
|
1625
|
-
});
|
|
1931
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
1932
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1626
1933
|
const {
|
|
1627
1934
|
api: apiFiles,
|
|
1628
1935
|
ui: uiFiles,
|
|
@@ -1631,7 +1938,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1631
1938
|
const idsByPrefix = await collectIds([
|
|
1632
1939
|
...specFiles,
|
|
1633
1940
|
...scenarioFiles,
|
|
1634
|
-
...decisionFiles,
|
|
1635
1941
|
...apiFiles,
|
|
1636
1942
|
...uiFiles,
|
|
1637
1943
|
...dbFiles
|
|
@@ -1656,7 +1962,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1656
1962
|
summary: {
|
|
1657
1963
|
specs: specFiles.length,
|
|
1658
1964
|
scenarios: scenarioFiles.length,
|
|
1659
|
-
decisions: decisionFiles.length,
|
|
1660
1965
|
contracts: {
|
|
1661
1966
|
api: apiFiles.length,
|
|
1662
1967
|
ui: uiFiles.length,
|
|
@@ -1690,7 +1995,6 @@ function formatReportMarkdown(data) {
|
|
|
1690
1995
|
lines.push("## \u6982\u8981");
|
|
1691
1996
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
1692
1997
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
1693
|
-
lines.push(`- decisions: ${data.summary.decisions}`);
|
|
1694
1998
|
lines.push(
|
|
1695
1999
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
1696
2000
|
);
|
|
@@ -1766,7 +2070,7 @@ async function collectIds(files) {
|
|
|
1766
2070
|
DATA: /* @__PURE__ */ new Set()
|
|
1767
2071
|
};
|
|
1768
2072
|
for (const file of files) {
|
|
1769
|
-
const text = await (0,
|
|
2073
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1770
2074
|
for (const prefix of ID_PREFIXES2) {
|
|
1771
2075
|
const ids = extractIds(text, prefix);
|
|
1772
2076
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -1784,7 +2088,7 @@ async function collectIds(files) {
|
|
|
1784
2088
|
async function collectUpstreamIds(files) {
|
|
1785
2089
|
const ids = /* @__PURE__ */ new Set();
|
|
1786
2090
|
for (const file of files) {
|
|
1787
|
-
const text = await (0,
|
|
2091
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1788
2092
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
1789
2093
|
}
|
|
1790
2094
|
return ids;
|
|
@@ -1805,7 +2109,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
1805
2109
|
}
|
|
1806
2110
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
1807
2111
|
for (const file of targetFiles) {
|
|
1808
|
-
const text = await (0,
|
|
2112
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1809
2113
|
if (pattern.test(text)) {
|
|
1810
2114
|
return true;
|
|
1811
2115
|
}
|
|
@@ -1827,20 +2131,20 @@ function toSortedArray(values) {
|
|
|
1827
2131
|
}
|
|
1828
2132
|
function buildHotspots(issues) {
|
|
1829
2133
|
const map = /* @__PURE__ */ new Map();
|
|
1830
|
-
for (const
|
|
1831
|
-
if (!
|
|
2134
|
+
for (const issue7 of issues) {
|
|
2135
|
+
if (!issue7.file) {
|
|
1832
2136
|
continue;
|
|
1833
2137
|
}
|
|
1834
|
-
const current = map.get(
|
|
1835
|
-
file:
|
|
2138
|
+
const current = map.get(issue7.file) ?? {
|
|
2139
|
+
file: issue7.file,
|
|
1836
2140
|
total: 0,
|
|
1837
2141
|
error: 0,
|
|
1838
2142
|
warning: 0,
|
|
1839
2143
|
info: 0
|
|
1840
2144
|
};
|
|
1841
2145
|
current.total += 1;
|
|
1842
|
-
current[
|
|
1843
|
-
map.set(
|
|
2146
|
+
current[issue7.severity] += 1;
|
|
2147
|
+
map.set(issue7.file, current);
|
|
1844
2148
|
}
|
|
1845
2149
|
return Array.from(map.values()).sort(
|
|
1846
2150
|
(a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
|
|
@@ -1863,6 +2167,7 @@ function buildHotspots(issues) {
|
|
|
1863
2167
|
resolveToolVersion,
|
|
1864
2168
|
validateContracts,
|
|
1865
2169
|
validateDefinedIds,
|
|
2170
|
+
validateDeltas,
|
|
1866
2171
|
validateProject,
|
|
1867
2172
|
validateScenarioContent,
|
|
1868
2173
|
validateScenarios,
|