qfai 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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-0001/delta.md +30 -0
- package/assets/init/.qfai/specs/spec-0001/scenario.md +10 -0
- package/assets/init/.qfai/{spec/spec-0001-sample.md → specs/spec-0001/spec.md} +3 -2
- package/assets/init/root/.github/workflows/qfai.yml +1 -1
- package/assets/init/root/qfai.config.yaml +5 -8
- package/dist/cli/index.cjs +619 -439
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +594 -410
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +590 -393
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -12
- package/dist/index.d.ts +8 -12
- package/dist/index.mjs +569 -368
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
- package/assets/init/.qfai/spec/README.md +0 -82
- package/assets/init/.qfai/spec/decisions/ADR-0001.md +0 -9
- package/assets/init/.qfai/spec/decisions/README.md +0 -37
- 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",
|
|
96
|
+
configPath,
|
|
97
|
+
issues
|
|
98
|
+
),
|
|
99
|
+
rulesDir: readString(
|
|
100
|
+
raw.rulesDir,
|
|
101
|
+
base.rulesDir,
|
|
102
|
+
"paths.rulesDir",
|
|
120
103
|
configPath,
|
|
121
104
|
issues
|
|
122
105
|
),
|
|
123
|
-
|
|
124
|
-
raw.
|
|
125
|
-
base.
|
|
126
|
-
"paths.
|
|
106
|
+
outDir: readString(
|
|
107
|
+
raw.outDir,
|
|
108
|
+
base.outDir,
|
|
109
|
+
"paths.outDir",
|
|
127
110
|
configPath,
|
|
128
111
|
issues
|
|
129
112
|
),
|
|
130
|
-
|
|
131
|
-
raw.
|
|
132
|
-
base.
|
|
133
|
-
"paths.
|
|
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",
|
|
@@ -424,9 +386,10 @@ function isValidId(value, prefix) {
|
|
|
424
386
|
|
|
425
387
|
// src/core/report.ts
|
|
426
388
|
import { readFile as readFile10 } from "fs/promises";
|
|
389
|
+
import path10 from "path";
|
|
427
390
|
|
|
428
391
|
// src/core/discovery.ts
|
|
429
|
-
import
|
|
392
|
+
import { access as access2 } from "fs/promises";
|
|
430
393
|
|
|
431
394
|
// src/core/fs.ts
|
|
432
395
|
import { access, readdir } from "fs/promises";
|
|
@@ -483,11 +446,50 @@ async function exists(target) {
|
|
|
483
446
|
}
|
|
484
447
|
}
|
|
485
448
|
|
|
449
|
+
// src/core/specLayout.ts
|
|
450
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
451
|
+
import path3 from "path";
|
|
452
|
+
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
453
|
+
async function collectSpecEntries(specsRoot) {
|
|
454
|
+
const dirs = await listSpecDirs(specsRoot);
|
|
455
|
+
const entries = dirs.map((dir) => ({
|
|
456
|
+
dir,
|
|
457
|
+
specPath: path3.join(dir, "spec.md"),
|
|
458
|
+
deltaPath: path3.join(dir, "delta.md"),
|
|
459
|
+
scenarioPath: path3.join(dir, "scenario.md")
|
|
460
|
+
}));
|
|
461
|
+
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
462
|
+
}
|
|
463
|
+
async function listSpecDirs(specsRoot) {
|
|
464
|
+
try {
|
|
465
|
+
const items = await readdir2(specsRoot, { withFileTypes: true });
|
|
466
|
+
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => path3.join(specsRoot, name));
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (isMissingFileError(error)) {
|
|
469
|
+
return [];
|
|
470
|
+
}
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function isMissingFileError(error) {
|
|
475
|
+
if (!error || typeof error !== "object") {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
return error.code === "ENOENT";
|
|
479
|
+
}
|
|
480
|
+
|
|
486
481
|
// src/core/discovery.ts
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
482
|
+
async function collectSpecPackDirs(specsRoot) {
|
|
483
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
484
|
+
return entries.map((entry) => entry.dir);
|
|
485
|
+
}
|
|
486
|
+
async function collectSpecFiles(specsRoot) {
|
|
487
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
488
|
+
return filterExisting(entries.map((entry) => entry.specPath));
|
|
489
|
+
}
|
|
490
|
+
async function collectScenarioFiles(specsRoot) {
|
|
491
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
492
|
+
return filterExisting(entries.map((entry) => entry.scenarioPath));
|
|
491
493
|
}
|
|
492
494
|
async function collectUiContractFiles(uiRoot) {
|
|
493
495
|
return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
@@ -506,9 +508,22 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
|
506
508
|
]);
|
|
507
509
|
return { ui, api, db };
|
|
508
510
|
}
|
|
509
|
-
function
|
|
510
|
-
const
|
|
511
|
-
|
|
511
|
+
async function filterExisting(files) {
|
|
512
|
+
const existing = [];
|
|
513
|
+
for (const file of files) {
|
|
514
|
+
if (await exists2(file)) {
|
|
515
|
+
existing.push(file);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return existing;
|
|
519
|
+
}
|
|
520
|
+
async function exists2(target) {
|
|
521
|
+
try {
|
|
522
|
+
await access2(target);
|
|
523
|
+
return true;
|
|
524
|
+
} catch {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
512
527
|
}
|
|
513
528
|
|
|
514
529
|
// src/core/types.ts
|
|
@@ -519,8 +534,8 @@ import { readFile as readFile2 } from "fs/promises";
|
|
|
519
534
|
import path4 from "path";
|
|
520
535
|
import { fileURLToPath } from "url";
|
|
521
536
|
async function resolveToolVersion() {
|
|
522
|
-
if ("0.3.
|
|
523
|
-
return "0.3.
|
|
537
|
+
if ("0.3.2".length > 0) {
|
|
538
|
+
return "0.3.2";
|
|
524
539
|
}
|
|
525
540
|
try {
|
|
526
541
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -540,6 +555,7 @@ function resolvePackageJsonPath() {
|
|
|
540
555
|
|
|
541
556
|
// src/core/validators/contracts.ts
|
|
542
557
|
import { readFile as readFile3 } from "fs/promises";
|
|
558
|
+
import path6 from "path";
|
|
543
559
|
|
|
544
560
|
// src/core/contracts.ts
|
|
545
561
|
import path5 from "path";
|
|
@@ -595,19 +611,10 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
595
611
|
];
|
|
596
612
|
async function validateContracts(root, config) {
|
|
597
613
|
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
|
-
);
|
|
614
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
615
|
+
issues.push(...await validateUiContracts(path6.join(contractsRoot, "ui")));
|
|
616
|
+
issues.push(...await validateApiContracts(path6.join(contractsRoot, "api")));
|
|
617
|
+
issues.push(...await validateDataContracts(path6.join(contractsRoot, "db")));
|
|
611
618
|
return issues;
|
|
612
619
|
}
|
|
613
620
|
async function validateUiContracts(uiRoot) {
|
|
@@ -840,72 +847,78 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
840
847
|
return issue7;
|
|
841
848
|
}
|
|
842
849
|
|
|
843
|
-
// src/core/validators/
|
|
850
|
+
// src/core/validators/delta.ts
|
|
844
851
|
import { readFile as readFile4 } from "fs/promises";
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
var
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
const status = extractField(md, "Status");
|
|
856
|
-
const context = extractField(md, "Context");
|
|
857
|
-
const decision = extractField(md, "Decision");
|
|
858
|
-
const consequences = extractField(md, "Consequences");
|
|
859
|
-
const related = extractField(md, "Related");
|
|
860
|
-
if (status) fields.status = status;
|
|
861
|
-
if (context) fields.context = context;
|
|
862
|
-
if (decision) fields.decision = decision;
|
|
863
|
-
if (consequences) fields.consequences = consequences;
|
|
864
|
-
if (related) fields.related = related;
|
|
865
|
-
const parsed = {
|
|
866
|
-
file,
|
|
867
|
-
fields
|
|
868
|
-
};
|
|
869
|
-
if (adrId) {
|
|
870
|
-
parsed.adrId = adrId;
|
|
871
|
-
}
|
|
872
|
-
return parsed;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// src/core/validators/decisions.ts
|
|
876
|
-
var REQUIRED_FIELDS = [
|
|
877
|
-
{ key: "status", label: "Status" },
|
|
878
|
-
{ key: "context", label: "Context" },
|
|
879
|
-
{ key: "decision", label: "Decision" },
|
|
880
|
-
{ key: "consequences", label: "Consequences" }
|
|
881
|
-
];
|
|
882
|
-
async function validateDecisions(root, config) {
|
|
883
|
-
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
884
|
-
const files = await collectFiles(decisionsRoot, { extensions: [".md"] });
|
|
885
|
-
if (files.length === 0) {
|
|
852
|
+
import path7 from "path";
|
|
853
|
+
var SECTION_RE = /^##\s+変更区分/m;
|
|
854
|
+
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
855
|
+
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
856
|
+
var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
|
|
857
|
+
var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
|
|
858
|
+
async function validateDeltas(root, config) {
|
|
859
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
860
|
+
const packs = await collectSpecPackDirs(specsRoot);
|
|
861
|
+
if (packs.length === 0) {
|
|
886
862
|
return [];
|
|
887
863
|
}
|
|
888
864
|
const issues = [];
|
|
889
|
-
for (const
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
(
|
|
894
|
-
)
|
|
895
|
-
|
|
865
|
+
for (const pack of packs) {
|
|
866
|
+
const deltaPath = path7.join(pack, "delta.md");
|
|
867
|
+
let text;
|
|
868
|
+
try {
|
|
869
|
+
text = await readFile4(deltaPath, "utf-8");
|
|
870
|
+
} catch (error) {
|
|
871
|
+
if (isMissingFileError2(error)) {
|
|
872
|
+
issues.push(
|
|
873
|
+
issue2(
|
|
874
|
+
"QFAI-DELTA-001",
|
|
875
|
+
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
876
|
+
"error",
|
|
877
|
+
deltaPath,
|
|
878
|
+
"delta.exists"
|
|
879
|
+
)
|
|
880
|
+
);
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
throw error;
|
|
884
|
+
}
|
|
885
|
+
const hasSection = SECTION_RE.test(text);
|
|
886
|
+
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
887
|
+
const hasChange = CHANGE_LINE_RE.test(text);
|
|
888
|
+
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
896
889
|
issues.push(
|
|
897
890
|
issue2(
|
|
898
|
-
"QFAI-
|
|
899
|
-
|
|
891
|
+
"QFAI-DELTA-002",
|
|
892
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
900
893
|
"error",
|
|
901
|
-
|
|
902
|
-
"
|
|
894
|
+
deltaPath,
|
|
895
|
+
"delta.section"
|
|
896
|
+
)
|
|
897
|
+
);
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
901
|
+
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
902
|
+
if (compatibilityChecked === changeChecked) {
|
|
903
|
+
issues.push(
|
|
904
|
+
issue2(
|
|
905
|
+
"QFAI-DELTA-003",
|
|
906
|
+
"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",
|
|
907
|
+
"error",
|
|
908
|
+
deltaPath,
|
|
909
|
+
"delta.classification"
|
|
903
910
|
)
|
|
904
911
|
);
|
|
905
912
|
}
|
|
906
913
|
}
|
|
907
914
|
return issues;
|
|
908
915
|
}
|
|
916
|
+
function isMissingFileError2(error) {
|
|
917
|
+
if (!error || typeof error !== "object") {
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
return error.code === "ENOENT";
|
|
921
|
+
}
|
|
909
922
|
function issue2(code, message, severity, file, rule, refs) {
|
|
910
923
|
const issue7 = {
|
|
911
924
|
code,
|
|
@@ -926,14 +939,16 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
926
939
|
|
|
927
940
|
// src/core/validators/ids.ts
|
|
928
941
|
import { readFile as readFile6 } from "fs/promises";
|
|
929
|
-
import
|
|
942
|
+
import path9 from "path";
|
|
930
943
|
|
|
931
944
|
// src/core/contractIndex.ts
|
|
932
945
|
import { readFile as readFile5 } from "fs/promises";
|
|
946
|
+
import path8 from "path";
|
|
933
947
|
async function buildContractIndex(root, config) {
|
|
934
|
-
const
|
|
935
|
-
const
|
|
936
|
-
const
|
|
948
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
949
|
+
const uiRoot = path8.join(contractsRoot, "ui");
|
|
950
|
+
const apiRoot = path8.join(contractsRoot, "api");
|
|
951
|
+
const dataRoot = path8.join(contractsRoot, "db");
|
|
937
952
|
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
938
953
|
collectUiContractFiles(uiRoot),
|
|
939
954
|
collectApiContractFiles(apiRoot),
|
|
@@ -987,38 +1002,6 @@ function record(index, id, file) {
|
|
|
987
1002
|
index.idToFiles.set(id, current);
|
|
988
1003
|
}
|
|
989
1004
|
|
|
990
|
-
// src/core/parse/gherkin.ts
|
|
991
|
-
var FEATURE_RE = /^\s*Feature:\s+/;
|
|
992
|
-
var SCENARIO_RE = /^\s*Scenario:\s*(.+)\s*$/;
|
|
993
|
-
var TAG_LINE_RE = /^\s*@/;
|
|
994
|
-
function parseTags(line) {
|
|
995
|
-
return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
|
|
996
|
-
}
|
|
997
|
-
function parseGherkinFeature(text, file) {
|
|
998
|
-
const lines = text.split(/\r?\n/);
|
|
999
|
-
const scenarios = [];
|
|
1000
|
-
let featurePresent = false;
|
|
1001
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1002
|
-
const line = lines[i] ?? "";
|
|
1003
|
-
if (FEATURE_RE.test(line)) {
|
|
1004
|
-
featurePresent = true;
|
|
1005
|
-
}
|
|
1006
|
-
const match = line.match(SCENARIO_RE);
|
|
1007
|
-
if (!match) continue;
|
|
1008
|
-
const scenarioName = match[1];
|
|
1009
|
-
if (!scenarioName) continue;
|
|
1010
|
-
const tags = [];
|
|
1011
|
-
for (let j = i - 1; j >= 0; j--) {
|
|
1012
|
-
const previous = lines[j] ?? "";
|
|
1013
|
-
if (previous.trim() === "") continue;
|
|
1014
|
-
if (!TAG_LINE_RE.test(previous)) break;
|
|
1015
|
-
tags.unshift(...parseTags(previous));
|
|
1016
|
-
}
|
|
1017
|
-
scenarios.push({ name: scenarioName, line: i + 1, tags });
|
|
1018
|
-
}
|
|
1019
|
-
return { file, featurePresent, scenarios };
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
1005
|
// src/core/parse/markdown.ts
|
|
1023
1006
|
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1024
1007
|
function parseHeadings(md) {
|
|
@@ -1062,9 +1045,9 @@ function extractH2Sections(md) {
|
|
|
1062
1045
|
|
|
1063
1046
|
// src/core/parse/spec.ts
|
|
1064
1047
|
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1065
|
-
var BR_LINE_RE = /^\s
|
|
1066
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s
|
|
1067
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s
|
|
1048
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1049
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1050
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1068
1051
|
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1069
1052
|
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1070
1053
|
function parseSpec(md, file) {
|
|
@@ -1137,16 +1120,171 @@ function parseSpec(md, file) {
|
|
|
1137
1120
|
return parsed;
|
|
1138
1121
|
}
|
|
1139
1122
|
|
|
1140
|
-
// src/core/
|
|
1123
|
+
// src/core/gherkin/parse.ts
|
|
1124
|
+
import {
|
|
1125
|
+
AstBuilder,
|
|
1126
|
+
GherkinClassicTokenMatcher,
|
|
1127
|
+
Parser
|
|
1128
|
+
} from "@cucumber/gherkin";
|
|
1129
|
+
import { randomUUID } from "crypto";
|
|
1130
|
+
function parseGherkin(source, uri) {
|
|
1131
|
+
const errors = [];
|
|
1132
|
+
const uuidFn = () => randomUUID();
|
|
1133
|
+
const builder = new AstBuilder(uuidFn);
|
|
1134
|
+
const matcher = new GherkinClassicTokenMatcher();
|
|
1135
|
+
const parser = new Parser(builder, matcher);
|
|
1136
|
+
try {
|
|
1137
|
+
const gherkinDocument = parser.parse(source);
|
|
1138
|
+
gherkinDocument.uri = uri;
|
|
1139
|
+
return { gherkinDocument, errors };
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
errors.push(formatError3(error));
|
|
1142
|
+
return { gherkinDocument: null, errors };
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
function formatError3(error) {
|
|
1146
|
+
if (error instanceof Error) {
|
|
1147
|
+
return error.message;
|
|
1148
|
+
}
|
|
1149
|
+
return String(error);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// src/core/scenarioModel.ts
|
|
1153
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1141
1154
|
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1155
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1156
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1157
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
1158
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1159
|
+
function parseScenarioDocument(text, uri) {
|
|
1160
|
+
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1161
|
+
if (!gherkinDocument) {
|
|
1162
|
+
return { document: null, errors };
|
|
1163
|
+
}
|
|
1164
|
+
const feature = gherkinDocument.feature;
|
|
1165
|
+
if (!feature) {
|
|
1166
|
+
return {
|
|
1167
|
+
document: { uri, featureTags: [], scenarios: [] },
|
|
1168
|
+
errors
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
const featureTags = collectTagNames(feature.tags);
|
|
1172
|
+
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
1173
|
+
return {
|
|
1174
|
+
document: {
|
|
1175
|
+
uri,
|
|
1176
|
+
featureName: feature.name,
|
|
1177
|
+
featureTags,
|
|
1178
|
+
scenarios
|
|
1179
|
+
},
|
|
1180
|
+
errors
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
function buildScenarioAtoms(document) {
|
|
1184
|
+
return document.scenarios.map((scenario) => {
|
|
1185
|
+
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1186
|
+
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1187
|
+
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1188
|
+
const contractIds = /* @__PURE__ */ new Set();
|
|
1189
|
+
scenario.tags.forEach((tag) => {
|
|
1190
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1191
|
+
contractIds.add(tag);
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
for (const step of scenario.steps) {
|
|
1195
|
+
for (const text of collectStepTexts(step)) {
|
|
1196
|
+
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1197
|
+
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1198
|
+
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
const atom = {
|
|
1202
|
+
uri: document.uri,
|
|
1203
|
+
featureName: document.featureName ?? "",
|
|
1204
|
+
scenarioName: scenario.name,
|
|
1205
|
+
kind: scenario.kind,
|
|
1206
|
+
brIds,
|
|
1207
|
+
contractIds: Array.from(contractIds).sort()
|
|
1208
|
+
};
|
|
1209
|
+
if (scenario.line !== void 0) {
|
|
1210
|
+
atom.line = scenario.line;
|
|
1211
|
+
}
|
|
1212
|
+
if (specIds.length === 1) {
|
|
1213
|
+
const specId = specIds[0];
|
|
1214
|
+
if (specId) {
|
|
1215
|
+
atom.specId = specId;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (scIds.length === 1) {
|
|
1219
|
+
const scId = scIds[0];
|
|
1220
|
+
if (scId) {
|
|
1221
|
+
atom.scId = scId;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return atom;
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
function collectScenarioNodes(feature, featureTags) {
|
|
1228
|
+
const scenarios = [];
|
|
1229
|
+
for (const child of feature.children) {
|
|
1230
|
+
if (child.scenario) {
|
|
1231
|
+
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
1232
|
+
}
|
|
1233
|
+
if (child.rule) {
|
|
1234
|
+
const ruleTags = collectTagNames(child.rule.tags);
|
|
1235
|
+
for (const ruleChild of child.rule.children) {
|
|
1236
|
+
if (ruleChild.scenario) {
|
|
1237
|
+
scenarios.push(
|
|
1238
|
+
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
return scenarios;
|
|
1245
|
+
}
|
|
1246
|
+
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
1247
|
+
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
1248
|
+
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
1249
|
+
return {
|
|
1250
|
+
name: scenario.name,
|
|
1251
|
+
kind,
|
|
1252
|
+
line: scenario.location?.line,
|
|
1253
|
+
tags,
|
|
1254
|
+
steps: scenario.steps
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
function collectTagNames(tags) {
|
|
1258
|
+
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1259
|
+
}
|
|
1260
|
+
function collectStepTexts(step) {
|
|
1261
|
+
const texts = [];
|
|
1262
|
+
if (step.text) {
|
|
1263
|
+
texts.push(step.text);
|
|
1264
|
+
}
|
|
1265
|
+
if (step.docString?.content) {
|
|
1266
|
+
texts.push(step.docString.content);
|
|
1267
|
+
}
|
|
1268
|
+
if (step.dataTable?.rows) {
|
|
1269
|
+
for (const row of step.dataTable.rows) {
|
|
1270
|
+
for (const cell of row.cells) {
|
|
1271
|
+
texts.push(cell.value);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
return texts;
|
|
1276
|
+
}
|
|
1277
|
+
function unique2(values) {
|
|
1278
|
+
return Array.from(new Set(values));
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/core/validators/ids.ts
|
|
1282
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1142
1283
|
async function validateDefinedIds(root, config) {
|
|
1143
1284
|
const issues = [];
|
|
1144
|
-
const
|
|
1145
|
-
const
|
|
1146
|
-
const
|
|
1147
|
-
const scenarioFiles = await collectFiles(scenarioRoot, {
|
|
1148
|
-
extensions: [".feature"]
|
|
1149
|
-
});
|
|
1285
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1286
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
1287
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1150
1288
|
const defined = /* @__PURE__ */ new Map();
|
|
1151
1289
|
await collectSpecDefinitionIds(specFiles, defined);
|
|
1152
1290
|
await collectScenarioDefinitionIds(scenarioFiles, defined);
|
|
@@ -1186,10 +1324,13 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1186
1324
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1187
1325
|
for (const file of files) {
|
|
1188
1326
|
const text = await readFile6(file, "utf-8");
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1327
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1328
|
+
if (!document || errors.length > 0) {
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
for (const scenario of document.scenarios) {
|
|
1191
1332
|
for (const tag of scenario.tags) {
|
|
1192
|
-
if (
|
|
1333
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
1193
1334
|
recordId(out, tag, file);
|
|
1194
1335
|
}
|
|
1195
1336
|
}
|
|
@@ -1203,7 +1344,7 @@ function recordId(out, id, file) {
|
|
|
1203
1344
|
}
|
|
1204
1345
|
function formatFileList(files, root) {
|
|
1205
1346
|
return files.map((file) => {
|
|
1206
|
-
const relative =
|
|
1347
|
+
const relative = path9.relative(root, file);
|
|
1207
1348
|
return relative.length > 0 ? relative : file;
|
|
1208
1349
|
}).join(", ");
|
|
1209
1350
|
}
|
|
@@ -1230,35 +1371,51 @@ import { readFile as readFile7 } from "fs/promises";
|
|
|
1230
1371
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1231
1372
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1232
1373
|
var THEN_PATTERN = /\bThen\b/;
|
|
1233
|
-
var
|
|
1234
|
-
var
|
|
1235
|
-
var
|
|
1374
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1375
|
+
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1376
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1236
1377
|
async function validateScenarios(root, config) {
|
|
1237
|
-
const
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1378
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1379
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1380
|
+
if (entries.length === 0) {
|
|
1381
|
+
const expected = "spec-0001/scenario.md";
|
|
1382
|
+
const legacy = "spec-001/scenario.md";
|
|
1242
1383
|
return [
|
|
1243
1384
|
issue4(
|
|
1244
1385
|
"QFAI-SC-000",
|
|
1245
|
-
|
|
1386
|
+
`Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected} (${legacy} \u306F\u975E\u5BFE\u5FDC)`,
|
|
1246
1387
|
"info",
|
|
1247
|
-
|
|
1388
|
+
specsRoot,
|
|
1248
1389
|
"scenario.files"
|
|
1249
1390
|
)
|
|
1250
1391
|
];
|
|
1251
1392
|
}
|
|
1252
1393
|
const issues = [];
|
|
1253
|
-
for (const
|
|
1254
|
-
|
|
1255
|
-
|
|
1394
|
+
for (const entry of entries) {
|
|
1395
|
+
let text;
|
|
1396
|
+
try {
|
|
1397
|
+
text = await readFile7(entry.scenarioPath, "utf-8");
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
if (isMissingFileError3(error)) {
|
|
1400
|
+
issues.push(
|
|
1401
|
+
issue4(
|
|
1402
|
+
"QFAI-SC-001",
|
|
1403
|
+
"scenario.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1404
|
+
"error",
|
|
1405
|
+
entry.scenarioPath,
|
|
1406
|
+
"scenario.exists"
|
|
1407
|
+
)
|
|
1408
|
+
);
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
throw error;
|
|
1412
|
+
}
|
|
1413
|
+
issues.push(...validateScenarioContent(text, entry.scenarioPath));
|
|
1256
1414
|
}
|
|
1257
1415
|
return issues;
|
|
1258
1416
|
}
|
|
1259
1417
|
function validateScenarioContent(text, file) {
|
|
1260
1418
|
const issues = [];
|
|
1261
|
-
const parsed = parseGherkinFeature(text, file);
|
|
1262
1419
|
const invalidIds = extractInvalidIds(text, [
|
|
1263
1420
|
"SPEC",
|
|
1264
1421
|
"BR",
|
|
@@ -1280,9 +1437,47 @@ function validateScenarioContent(text, file) {
|
|
|
1280
1437
|
)
|
|
1281
1438
|
);
|
|
1282
1439
|
}
|
|
1440
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1441
|
+
if (!document || errors.length > 0) {
|
|
1442
|
+
issues.push(
|
|
1443
|
+
issue4(
|
|
1444
|
+
"QFAI-SC-010",
|
|
1445
|
+
`Gherkin \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errors.join(", ") || "unknown"}`,
|
|
1446
|
+
"error",
|
|
1447
|
+
file,
|
|
1448
|
+
"scenario.parse"
|
|
1449
|
+
)
|
|
1450
|
+
);
|
|
1451
|
+
return issues;
|
|
1452
|
+
}
|
|
1453
|
+
const featureSpecTags = document.featureTags.filter(
|
|
1454
|
+
(tag) => SPEC_TAG_RE2.test(tag)
|
|
1455
|
+
);
|
|
1456
|
+
if (featureSpecTags.length === 0) {
|
|
1457
|
+
issues.push(
|
|
1458
|
+
issue4(
|
|
1459
|
+
"QFAI-SC-009",
|
|
1460
|
+
"Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1461
|
+
"error",
|
|
1462
|
+
file,
|
|
1463
|
+
"scenario.featureSpec"
|
|
1464
|
+
)
|
|
1465
|
+
);
|
|
1466
|
+
} else if (featureSpecTags.length > 1) {
|
|
1467
|
+
issues.push(
|
|
1468
|
+
issue4(
|
|
1469
|
+
"QFAI-SC-009",
|
|
1470
|
+
`Feature \u306E SPEC \u30BF\u30B0\u304C\u8907\u6570\u3042\u308A\u307E\u3059: ${featureSpecTags.join(", ")}`,
|
|
1471
|
+
"error",
|
|
1472
|
+
file,
|
|
1473
|
+
"scenario.featureSpec",
|
|
1474
|
+
featureSpecTags
|
|
1475
|
+
)
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1283
1478
|
const missingStructure = [];
|
|
1284
|
-
if (!
|
|
1285
|
-
if (
|
|
1479
|
+
if (!document.featureName) missingStructure.push("Feature");
|
|
1480
|
+
if (document.scenarios.length === 0) missingStructure.push("Scenario");
|
|
1286
1481
|
if (missingStructure.length > 0) {
|
|
1287
1482
|
issues.push(
|
|
1288
1483
|
issue4(
|
|
@@ -1296,7 +1491,7 @@ function validateScenarioContent(text, file) {
|
|
|
1296
1491
|
)
|
|
1297
1492
|
);
|
|
1298
1493
|
}
|
|
1299
|
-
for (const scenario of
|
|
1494
|
+
for (const scenario of document.scenarios) {
|
|
1300
1495
|
if (scenario.tags.length === 0) {
|
|
1301
1496
|
issues.push(
|
|
1302
1497
|
issue4(
|
|
@@ -1310,13 +1505,16 @@ function validateScenarioContent(text, file) {
|
|
|
1310
1505
|
continue;
|
|
1311
1506
|
}
|
|
1312
1507
|
const missingTags = [];
|
|
1313
|
-
|
|
1314
|
-
|
|
1508
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
|
|
1509
|
+
if (scTags.length === 0) {
|
|
1510
|
+
missingTags.push("SC(0\u4EF6)");
|
|
1511
|
+
} else if (scTags.length > 1) {
|
|
1512
|
+
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1315
1513
|
}
|
|
1316
|
-
if (!scenario.tags.some((tag) =>
|
|
1514
|
+
if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
|
|
1317
1515
|
missingTags.push("SPEC");
|
|
1318
1516
|
}
|
|
1319
|
-
if (!scenario.tags.some((tag) =>
|
|
1517
|
+
if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
|
|
1320
1518
|
missingTags.push("BR");
|
|
1321
1519
|
}
|
|
1322
1520
|
if (missingTags.length > 0) {
|
|
@@ -1331,26 +1529,29 @@ function validateScenarioContent(text, file) {
|
|
|
1331
1529
|
);
|
|
1332
1530
|
}
|
|
1333
1531
|
}
|
|
1334
|
-
const
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1532
|
+
for (const scenario of document.scenarios) {
|
|
1533
|
+
const missingSteps = [];
|
|
1534
|
+
const keywords = scenario.steps.map((step) => step.keyword.trim());
|
|
1535
|
+
if (!keywords.some((keyword) => GIVEN_PATTERN.test(keyword))) {
|
|
1536
|
+
missingSteps.push("Given");
|
|
1537
|
+
}
|
|
1538
|
+
if (!keywords.some((keyword) => WHEN_PATTERN.test(keyword))) {
|
|
1539
|
+
missingSteps.push("When");
|
|
1540
|
+
}
|
|
1541
|
+
if (!keywords.some((keyword) => THEN_PATTERN.test(keyword))) {
|
|
1542
|
+
missingSteps.push("Then");
|
|
1543
|
+
}
|
|
1544
|
+
if (missingSteps.length > 0) {
|
|
1545
|
+
issues.push(
|
|
1546
|
+
issue4(
|
|
1547
|
+
"QFAI-SC-005",
|
|
1548
|
+
`Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")} (${scenario.name})`,
|
|
1549
|
+
"warning",
|
|
1550
|
+
file,
|
|
1551
|
+
"scenario.steps"
|
|
1552
|
+
)
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1354
1555
|
}
|
|
1355
1556
|
return issues;
|
|
1356
1557
|
}
|
|
@@ -1371,18 +1572,25 @@ function issue4(code, message, severity, file, rule, refs) {
|
|
|
1371
1572
|
}
|
|
1372
1573
|
return issue7;
|
|
1373
1574
|
}
|
|
1575
|
+
function isMissingFileError3(error) {
|
|
1576
|
+
if (!error || typeof error !== "object") {
|
|
1577
|
+
return false;
|
|
1578
|
+
}
|
|
1579
|
+
return error.code === "ENOENT";
|
|
1580
|
+
}
|
|
1374
1581
|
|
|
1375
1582
|
// src/core/validators/spec.ts
|
|
1376
1583
|
import { readFile as readFile8 } from "fs/promises";
|
|
1377
1584
|
async function validateSpecs(root, config) {
|
|
1378
|
-
const specsRoot = resolvePath(root, config, "
|
|
1379
|
-
const
|
|
1380
|
-
if (
|
|
1381
|
-
const expected = "spec-0001
|
|
1585
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1586
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1587
|
+
if (entries.length === 0) {
|
|
1588
|
+
const expected = "spec-0001/spec.md";
|
|
1589
|
+
const legacy = "spec-001/spec.md";
|
|
1382
1590
|
return [
|
|
1383
1591
|
issue5(
|
|
1384
1592
|
"QFAI-SPEC-000",
|
|
1385
|
-
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.
|
|
1593
|
+
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected} (${legacy} \u306F\u975E\u5BFE\u5FDC)`,
|
|
1386
1594
|
"info",
|
|
1387
1595
|
specsRoot,
|
|
1388
1596
|
"spec.files"
|
|
@@ -1390,12 +1598,29 @@ async function validateSpecs(root, config) {
|
|
|
1390
1598
|
];
|
|
1391
1599
|
}
|
|
1392
1600
|
const issues = [];
|
|
1393
|
-
for (const
|
|
1394
|
-
|
|
1601
|
+
for (const entry of entries) {
|
|
1602
|
+
let text;
|
|
1603
|
+
try {
|
|
1604
|
+
text = await readFile8(entry.specPath, "utf-8");
|
|
1605
|
+
} catch (error) {
|
|
1606
|
+
if (isMissingFileError4(error)) {
|
|
1607
|
+
issues.push(
|
|
1608
|
+
issue5(
|
|
1609
|
+
"QFAI-SPEC-005",
|
|
1610
|
+
"spec.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1611
|
+
"error",
|
|
1612
|
+
entry.specPath,
|
|
1613
|
+
"spec.exists"
|
|
1614
|
+
)
|
|
1615
|
+
);
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
throw error;
|
|
1619
|
+
}
|
|
1395
1620
|
issues.push(
|
|
1396
1621
|
...validateSpecContent(
|
|
1397
1622
|
text,
|
|
1398
|
-
|
|
1623
|
+
entry.specPath,
|
|
1399
1624
|
config.validation.require.specSections
|
|
1400
1625
|
)
|
|
1401
1626
|
);
|
|
@@ -1517,29 +1742,25 @@ function issue5(code, message, severity, file, rule, refs) {
|
|
|
1517
1742
|
}
|
|
1518
1743
|
return issue7;
|
|
1519
1744
|
}
|
|
1745
|
+
function isMissingFileError4(error) {
|
|
1746
|
+
if (!error || typeof error !== "object") {
|
|
1747
|
+
return false;
|
|
1748
|
+
}
|
|
1749
|
+
return error.code === "ENOENT";
|
|
1750
|
+
}
|
|
1520
1751
|
|
|
1521
1752
|
// src/core/validators/traceability.ts
|
|
1522
1753
|
import { readFile as readFile9 } from "fs/promises";
|
|
1523
|
-
var
|
|
1524
|
-
var
|
|
1525
|
-
var
|
|
1526
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1527
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
1528
|
-
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1754
|
+
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1755
|
+
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1756
|
+
var BR_TAG_RE3 = /^BR-\d{4}$/;
|
|
1529
1757
|
async function validateTraceability(root, config) {
|
|
1530
1758
|
const issues = [];
|
|
1531
|
-
const specsRoot = resolvePath(root, config, "
|
|
1532
|
-
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1533
|
-
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1759
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1534
1760
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1535
1761
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1536
1762
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
1537
|
-
const
|
|
1538
|
-
extensions: [".md"]
|
|
1539
|
-
});
|
|
1540
|
-
const scenarioFiles = await collectFiles(scenariosRoot, {
|
|
1541
|
-
extensions: [".feature"]
|
|
1542
|
-
});
|
|
1763
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1543
1764
|
const upstreamIds = /* @__PURE__ */ new Set();
|
|
1544
1765
|
const specIds = /* @__PURE__ */ new Set();
|
|
1545
1766
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
@@ -1587,111 +1808,100 @@ async function validateTraceability(root, config) {
|
|
|
1587
1808
|
specToBrIds.set(parsed.specId, current);
|
|
1588
1809
|
}
|
|
1589
1810
|
}
|
|
1590
|
-
for (const file of decisionFiles) {
|
|
1591
|
-
const text = await readFile9(file, "utf-8");
|
|
1592
|
-
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1593
|
-
}
|
|
1594
1811
|
for (const file of scenarioFiles) {
|
|
1595
1812
|
const text = await readFile9(file, "utf-8");
|
|
1596
1813
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1597
|
-
const
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
const scIds = /* @__PURE__ */ new Set();
|
|
1601
|
-
const scenarioIds = /* @__PURE__ */ new Set();
|
|
1602
|
-
for (const scenario of parsed.scenarios) {
|
|
1603
|
-
for (const tag of scenario.tags) {
|
|
1604
|
-
if (SPEC_TAG_RE2.test(tag)) {
|
|
1605
|
-
specIdsInScenario.add(tag);
|
|
1606
|
-
}
|
|
1607
|
-
if (BR_TAG_RE2.test(tag)) {
|
|
1608
|
-
brIds.add(tag);
|
|
1609
|
-
}
|
|
1610
|
-
if (SC_TAG_RE3.test(tag)) {
|
|
1611
|
-
scIds.add(tag);
|
|
1612
|
-
}
|
|
1613
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1614
|
-
scenarioIds.add(tag);
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
const specIdsList = Array.from(specIdsInScenario);
|
|
1619
|
-
const brIdsList = Array.from(brIds);
|
|
1620
|
-
const scIdsList = Array.from(scIds);
|
|
1621
|
-
const scenarioIdsList = Array.from(scenarioIds);
|
|
1622
|
-
brIdsList.forEach((id) => brIdsInScenarios.add(id));
|
|
1623
|
-
scIdsList.forEach((id) => scIdsInScenarios.add(id));
|
|
1624
|
-
scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
|
|
1625
|
-
if (scenarioIdsList.length > 0) {
|
|
1626
|
-
scIdsList.forEach((id) => scWithContracts.add(id));
|
|
1627
|
-
}
|
|
1628
|
-
const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
|
|
1629
|
-
if (unknownSpecIds.length > 0) {
|
|
1630
|
-
issues.push(
|
|
1631
|
-
issue6(
|
|
1632
|
-
"QFAI-TRACE-005",
|
|
1633
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1634
|
-
"error",
|
|
1635
|
-
file,
|
|
1636
|
-
"traceability.scenarioSpecExists",
|
|
1637
|
-
unknownSpecIds
|
|
1638
|
-
)
|
|
1639
|
-
);
|
|
1640
|
-
}
|
|
1641
|
-
const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
|
|
1642
|
-
if (unknownBrIds.length > 0) {
|
|
1643
|
-
issues.push(
|
|
1644
|
-
issue6(
|
|
1645
|
-
"QFAI-TRACE-006",
|
|
1646
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1647
|
-
"error",
|
|
1648
|
-
file,
|
|
1649
|
-
"traceability.scenarioBrExists",
|
|
1650
|
-
unknownBrIds
|
|
1651
|
-
)
|
|
1652
|
-
);
|
|
1653
|
-
}
|
|
1654
|
-
const unknownContractIds = scenarioIdsList.filter(
|
|
1655
|
-
(id) => !contractIds.has(id)
|
|
1656
|
-
);
|
|
1657
|
-
if (unknownContractIds.length > 0) {
|
|
1658
|
-
issues.push(
|
|
1659
|
-
issue6(
|
|
1660
|
-
"QFAI-TRACE-008",
|
|
1661
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1662
|
-
", "
|
|
1663
|
-
)}`,
|
|
1664
|
-
config.validation.traceability.unknownContractIdSeverity,
|
|
1665
|
-
file,
|
|
1666
|
-
"traceability.scenarioContractExists",
|
|
1667
|
-
unknownContractIds
|
|
1668
|
-
)
|
|
1669
|
-
);
|
|
1814
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1815
|
+
if (!document || errors.length > 0) {
|
|
1816
|
+
continue;
|
|
1670
1817
|
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1818
|
+
const atoms = buildScenarioAtoms(document);
|
|
1819
|
+
for (const [index, scenario] of document.scenarios.entries()) {
|
|
1820
|
+
const atom = atoms[index];
|
|
1821
|
+
if (!atom) {
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
1825
|
+
const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
|
|
1826
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
|
|
1827
|
+
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
1828
|
+
scTags.forEach((id) => scIdsInScenarios.add(id));
|
|
1829
|
+
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
1830
|
+
if (atom.contractIds.length > 0) {
|
|
1831
|
+
scTags.forEach((id) => scWithContracts.add(id));
|
|
1832
|
+
}
|
|
1833
|
+
const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
|
|
1834
|
+
if (unknownSpecIds.length > 0) {
|
|
1835
|
+
issues.push(
|
|
1836
|
+
issue6(
|
|
1837
|
+
"QFAI-TRACE-005",
|
|
1838
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(
|
|
1839
|
+
", "
|
|
1840
|
+
)} (${scenario.name})`,
|
|
1841
|
+
"error",
|
|
1842
|
+
file,
|
|
1843
|
+
"traceability.scenarioSpecExists",
|
|
1844
|
+
unknownSpecIds
|
|
1845
|
+
)
|
|
1846
|
+
);
|
|
1679
1847
|
}
|
|
1680
|
-
const
|
|
1681
|
-
if (
|
|
1848
|
+
const unknownBrIds = brTags.filter((id) => !brIdsInSpecs.has(id));
|
|
1849
|
+
if (unknownBrIds.length > 0) {
|
|
1682
1850
|
issues.push(
|
|
1683
1851
|
issue6(
|
|
1684
|
-
"QFAI-TRACE-
|
|
1685
|
-
`Scenario \
|
|
1852
|
+
"QFAI-TRACE-006",
|
|
1853
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(
|
|
1686
1854
|
", "
|
|
1687
|
-
)} (
|
|
1855
|
+
)} (${scenario.name})`,
|
|
1688
1856
|
"error",
|
|
1689
1857
|
file,
|
|
1690
|
-
"traceability.
|
|
1691
|
-
|
|
1858
|
+
"traceability.scenarioBrExists",
|
|
1859
|
+
unknownBrIds
|
|
1692
1860
|
)
|
|
1693
1861
|
);
|
|
1694
1862
|
}
|
|
1863
|
+
const unknownContractIds = atom.contractIds.filter(
|
|
1864
|
+
(id) => !contractIds.has(id)
|
|
1865
|
+
);
|
|
1866
|
+
if (unknownContractIds.length > 0) {
|
|
1867
|
+
issues.push(
|
|
1868
|
+
issue6(
|
|
1869
|
+
"QFAI-TRACE-008",
|
|
1870
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1871
|
+
", "
|
|
1872
|
+
)} (${scenario.name})`,
|
|
1873
|
+
config.validation.traceability.unknownContractIdSeverity,
|
|
1874
|
+
file,
|
|
1875
|
+
"traceability.scenarioContractExists",
|
|
1876
|
+
unknownContractIds
|
|
1877
|
+
)
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
if (specTags.length > 0 && brTags.length > 0) {
|
|
1881
|
+
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
1882
|
+
for (const specId of specTags) {
|
|
1883
|
+
const brIdsForSpec = specToBrIds.get(specId);
|
|
1884
|
+
if (!brIdsForSpec) {
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
1888
|
+
}
|
|
1889
|
+
const invalidBrIds = brTags.filter((id) => !allowedBrIds.has(id));
|
|
1890
|
+
if (invalidBrIds.length > 0) {
|
|
1891
|
+
issues.push(
|
|
1892
|
+
issue6(
|
|
1893
|
+
"QFAI-TRACE-007",
|
|
1894
|
+
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
1895
|
+
", "
|
|
1896
|
+
)} (SPEC: ${specTags.join(", ")}) (${scenario.name})`,
|
|
1897
|
+
"error",
|
|
1898
|
+
file,
|
|
1899
|
+
"traceability.scenarioBrUnderSpec",
|
|
1900
|
+
invalidBrIds
|
|
1901
|
+
)
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1695
1905
|
}
|
|
1696
1906
|
}
|
|
1697
1907
|
if (upstreamIds.size === 0) {
|
|
@@ -1734,7 +1944,7 @@ async function validateTraceability(root, config) {
|
|
|
1734
1944
|
", "
|
|
1735
1945
|
)}`,
|
|
1736
1946
|
"error",
|
|
1737
|
-
|
|
1947
|
+
specsRoot,
|
|
1738
1948
|
"traceability.scMustTouchContracts",
|
|
1739
1949
|
scWithoutContracts
|
|
1740
1950
|
)
|
|
@@ -1752,7 +1962,7 @@ async function validateTraceability(root, config) {
|
|
|
1752
1962
|
"QFAI_CONTRACT_ORPHAN",
|
|
1753
1963
|
`\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
1754
1964
|
"error",
|
|
1755
|
-
|
|
1965
|
+
specsRoot,
|
|
1756
1966
|
"traceability.allowOrphanContracts",
|
|
1757
1967
|
orphanContracts
|
|
1758
1968
|
)
|
|
@@ -1837,8 +2047,8 @@ async function validateProject(root, configResult) {
|
|
|
1837
2047
|
const issues = [
|
|
1838
2048
|
...configIssues,
|
|
1839
2049
|
...await validateSpecs(root, config),
|
|
2050
|
+
...await validateDeltas(root, config),
|
|
1840
2051
|
...await validateScenarios(root, config),
|
|
1841
|
-
...await validateDecisions(root, config),
|
|
1842
2052
|
...await validateContracts(root, config),
|
|
1843
2053
|
...await validateDefinedIds(root, config),
|
|
1844
2054
|
...await validateTraceability(root, config)
|
|
@@ -1867,21 +2077,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
1867
2077
|
const resolved = configResult ?? await loadConfig(root);
|
|
1868
2078
|
const config = resolved.config;
|
|
1869
2079
|
const configPath = resolved.configPath;
|
|
1870
|
-
const
|
|
1871
|
-
const
|
|
1872
|
-
const
|
|
1873
|
-
const
|
|
1874
|
-
const
|
|
1875
|
-
const dbRoot = resolvePath(root, config, "dataContractsDir");
|
|
2080
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2081
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2082
|
+
const apiRoot = path10.join(contractsRoot, "api");
|
|
2083
|
+
const uiRoot = path10.join(contractsRoot, "ui");
|
|
2084
|
+
const dbRoot = path10.join(contractsRoot, "db");
|
|
1876
2085
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1877
2086
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1878
|
-
const specFiles = await collectSpecFiles(
|
|
1879
|
-
const scenarioFiles = await
|
|
1880
|
-
extensions: [".feature"]
|
|
1881
|
-
});
|
|
1882
|
-
const decisionFiles = await collectFiles(decisionsRoot, {
|
|
1883
|
-
extensions: [".md"]
|
|
1884
|
-
});
|
|
2087
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
2088
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1885
2089
|
const {
|
|
1886
2090
|
api: apiFiles,
|
|
1887
2091
|
ui: uiFiles,
|
|
@@ -1890,7 +2094,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1890
2094
|
const idsByPrefix = await collectIds([
|
|
1891
2095
|
...specFiles,
|
|
1892
2096
|
...scenarioFiles,
|
|
1893
|
-
...decisionFiles,
|
|
1894
2097
|
...apiFiles,
|
|
1895
2098
|
...uiFiles,
|
|
1896
2099
|
...dbFiles
|
|
@@ -1915,7 +2118,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1915
2118
|
summary: {
|
|
1916
2119
|
specs: specFiles.length,
|
|
1917
2120
|
scenarios: scenarioFiles.length,
|
|
1918
|
-
decisions: decisionFiles.length,
|
|
1919
2121
|
contracts: {
|
|
1920
2122
|
api: apiFiles.length,
|
|
1921
2123
|
ui: uiFiles.length,
|
|
@@ -1949,7 +2151,6 @@ function formatReportMarkdown(data) {
|
|
|
1949
2151
|
lines.push("## \u6982\u8981");
|
|
1950
2152
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
1951
2153
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
1952
|
-
lines.push(`- decisions: ${data.summary.decisions}`);
|
|
1953
2154
|
lines.push(
|
|
1954
2155
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
1955
2156
|
);
|
|
@@ -2120,8 +2321,8 @@ export {
|
|
|
2120
2321
|
resolvePath,
|
|
2121
2322
|
resolveToolVersion,
|
|
2122
2323
|
validateContracts,
|
|
2123
|
-
validateDecisions,
|
|
2124
2324
|
validateDefinedIds,
|
|
2325
|
+
validateDeltas,
|
|
2125
2326
|
validateProject,
|
|
2126
2327
|
validateScenarioContent,
|
|
2127
2328
|
validateScenarios,
|