qfai 0.3.0 → 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 -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 +244 -273
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +244 -273
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +220 -232
- 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 +219 -231
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -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,6 +386,7 @@ 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
392
|
import path3 from "path";
|
|
@@ -484,10 +447,24 @@ async function exists(target) {
|
|
|
484
447
|
}
|
|
485
448
|
|
|
486
449
|
// src/core/discovery.ts
|
|
487
|
-
var
|
|
488
|
-
async function
|
|
489
|
-
const files = await collectFiles(
|
|
490
|
-
|
|
450
|
+
var SPEC_PACK_DIR_PATTERN = /^spec-\d{3}$/;
|
|
451
|
+
async function collectSpecPackDirs(specsRoot) {
|
|
452
|
+
const files = await collectFiles(specsRoot, { extensions: [".md"] });
|
|
453
|
+
const packs = /* @__PURE__ */ new Set();
|
|
454
|
+
for (const file of files) {
|
|
455
|
+
if (isSpecPackFile(file, "spec.md")) {
|
|
456
|
+
packs.add(path3.dirname(file));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return Array.from(packs).sort();
|
|
460
|
+
}
|
|
461
|
+
async function collectSpecFiles(specsRoot) {
|
|
462
|
+
const files = await collectFiles(specsRoot, { extensions: [".md"] });
|
|
463
|
+
return files.filter((file) => isSpecPackFile(file, "spec.md"));
|
|
464
|
+
}
|
|
465
|
+
async function collectScenarioFiles(specsRoot) {
|
|
466
|
+
const files = await collectFiles(specsRoot, { extensions: [".md"] });
|
|
467
|
+
return files.filter((file) => isSpecPackFile(file, "scenario.md"));
|
|
491
468
|
}
|
|
492
469
|
async function collectUiContractFiles(uiRoot) {
|
|
493
470
|
return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
@@ -506,9 +483,12 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
|
506
483
|
]);
|
|
507
484
|
return { ui, api, db };
|
|
508
485
|
}
|
|
509
|
-
function
|
|
510
|
-
|
|
511
|
-
|
|
486
|
+
function isSpecPackFile(filePath, baseName) {
|
|
487
|
+
if (path3.basename(filePath).toLowerCase() !== baseName) {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
const dirName = path3.basename(path3.dirname(filePath)).toLowerCase();
|
|
491
|
+
return SPEC_PACK_DIR_PATTERN.test(dirName);
|
|
512
492
|
}
|
|
513
493
|
|
|
514
494
|
// src/core/types.ts
|
|
@@ -519,8 +499,8 @@ import { readFile as readFile2 } from "fs/promises";
|
|
|
519
499
|
import path4 from "path";
|
|
520
500
|
import { fileURLToPath } from "url";
|
|
521
501
|
async function resolveToolVersion() {
|
|
522
|
-
if ("0.3.
|
|
523
|
-
return "0.3.
|
|
502
|
+
if ("0.3.1".length > 0) {
|
|
503
|
+
return "0.3.1";
|
|
524
504
|
}
|
|
525
505
|
try {
|
|
526
506
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -540,6 +520,7 @@ function resolvePackageJsonPath() {
|
|
|
540
520
|
|
|
541
521
|
// src/core/validators/contracts.ts
|
|
542
522
|
import { readFile as readFile3 } from "fs/promises";
|
|
523
|
+
import path6 from "path";
|
|
543
524
|
|
|
544
525
|
// src/core/contracts.ts
|
|
545
526
|
import path5 from "path";
|
|
@@ -595,19 +576,10 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
595
576
|
];
|
|
596
577
|
async function validateContracts(root, config) {
|
|
597
578
|
const issues = [];
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
);
|
|
601
|
-
issues.push(
|
|
602
|
-
...await validateApiContracts(
|
|
603
|
-
resolvePath(root, config, "apiContractsDir")
|
|
604
|
-
)
|
|
605
|
-
);
|
|
606
|
-
issues.push(
|
|
607
|
-
...await validateDataContracts(
|
|
608
|
-
resolvePath(root, config, "dataContractsDir")
|
|
609
|
-
)
|
|
610
|
-
);
|
|
579
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
580
|
+
issues.push(...await validateUiContracts(path6.join(contractsRoot, "ui")));
|
|
581
|
+
issues.push(...await validateApiContracts(path6.join(contractsRoot, "api")));
|
|
582
|
+
issues.push(...await validateDataContracts(path6.join(contractsRoot, "db")));
|
|
611
583
|
return issues;
|
|
612
584
|
}
|
|
613
585
|
async function validateUiContracts(uiRoot) {
|
|
@@ -840,72 +812,78 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
840
812
|
return issue7;
|
|
841
813
|
}
|
|
842
814
|
|
|
843
|
-
// src/core/validators/
|
|
815
|
+
// src/core/validators/delta.ts
|
|
844
816
|
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) {
|
|
817
|
+
import path7 from "path";
|
|
818
|
+
var SECTION_RE = /^##\s+変更区分/m;
|
|
819
|
+
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
820
|
+
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
821
|
+
var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
|
|
822
|
+
var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
|
|
823
|
+
async function validateDeltas(root, config) {
|
|
824
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
825
|
+
const packs = await collectSpecPackDirs(specsRoot);
|
|
826
|
+
if (packs.length === 0) {
|
|
886
827
|
return [];
|
|
887
828
|
}
|
|
888
829
|
const issues = [];
|
|
889
|
-
for (const
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
(
|
|
894
|
-
)
|
|
895
|
-
|
|
830
|
+
for (const pack of packs) {
|
|
831
|
+
const deltaPath = path7.join(pack, "delta.md");
|
|
832
|
+
let text;
|
|
833
|
+
try {
|
|
834
|
+
text = await readFile4(deltaPath, "utf-8");
|
|
835
|
+
} catch (error) {
|
|
836
|
+
if (isMissingFileError(error)) {
|
|
837
|
+
issues.push(
|
|
838
|
+
issue2(
|
|
839
|
+
"QFAI-DELTA-001",
|
|
840
|
+
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
841
|
+
"error",
|
|
842
|
+
deltaPath,
|
|
843
|
+
"delta.exists"
|
|
844
|
+
)
|
|
845
|
+
);
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
throw error;
|
|
849
|
+
}
|
|
850
|
+
const hasSection = SECTION_RE.test(text);
|
|
851
|
+
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
852
|
+
const hasChange = CHANGE_LINE_RE.test(text);
|
|
853
|
+
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
896
854
|
issues.push(
|
|
897
855
|
issue2(
|
|
898
|
-
"QFAI-
|
|
899
|
-
|
|
856
|
+
"QFAI-DELTA-002",
|
|
857
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
900
858
|
"error",
|
|
901
|
-
|
|
902
|
-
"
|
|
859
|
+
deltaPath,
|
|
860
|
+
"delta.section"
|
|
861
|
+
)
|
|
862
|
+
);
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
866
|
+
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
867
|
+
if (compatibilityChecked === changeChecked) {
|
|
868
|
+
issues.push(
|
|
869
|
+
issue2(
|
|
870
|
+
"QFAI-DELTA-003",
|
|
871
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u306F\u3069\u3061\u3089\u304B1\u3064\u3060\u3051\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4E21\u65B9ON/\u4E21\u65B9OFF\u306F\u7121\u52B9\u3067\u3059\uFF09\u3002",
|
|
872
|
+
"error",
|
|
873
|
+
deltaPath,
|
|
874
|
+
"delta.classification"
|
|
903
875
|
)
|
|
904
876
|
);
|
|
905
877
|
}
|
|
906
878
|
}
|
|
907
879
|
return issues;
|
|
908
880
|
}
|
|
881
|
+
function isMissingFileError(error) {
|
|
882
|
+
if (!error || typeof error !== "object") {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
return error.code === "ENOENT";
|
|
886
|
+
}
|
|
909
887
|
function issue2(code, message, severity, file, rule, refs) {
|
|
910
888
|
const issue7 = {
|
|
911
889
|
code,
|
|
@@ -926,14 +904,16 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
926
904
|
|
|
927
905
|
// src/core/validators/ids.ts
|
|
928
906
|
import { readFile as readFile6 } from "fs/promises";
|
|
929
|
-
import
|
|
907
|
+
import path9 from "path";
|
|
930
908
|
|
|
931
909
|
// src/core/contractIndex.ts
|
|
932
910
|
import { readFile as readFile5 } from "fs/promises";
|
|
911
|
+
import path8 from "path";
|
|
933
912
|
async function buildContractIndex(root, config) {
|
|
934
|
-
const
|
|
935
|
-
const
|
|
936
|
-
const
|
|
913
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
914
|
+
const uiRoot = path8.join(contractsRoot, "ui");
|
|
915
|
+
const apiRoot = path8.join(contractsRoot, "api");
|
|
916
|
+
const dataRoot = path8.join(contractsRoot, "db");
|
|
937
917
|
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
938
918
|
collectUiContractFiles(uiRoot),
|
|
939
919
|
collectApiContractFiles(apiRoot),
|
|
@@ -989,7 +969,7 @@ function record(index, id, file) {
|
|
|
989
969
|
|
|
990
970
|
// src/core/parse/gherkin.ts
|
|
991
971
|
var FEATURE_RE = /^\s*Feature:\s+/;
|
|
992
|
-
var SCENARIO_RE = /^\s*Scenario
|
|
972
|
+
var SCENARIO_RE = /^\s*Scenario(?: Outline)?:\s*(.+)\s*$/;
|
|
993
973
|
var TAG_LINE_RE = /^\s*@/;
|
|
994
974
|
function parseTags(line) {
|
|
995
975
|
return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
|
|
@@ -998,24 +978,52 @@ function parseGherkinFeature(text, file) {
|
|
|
998
978
|
const lines = text.split(/\r?\n/);
|
|
999
979
|
const scenarios = [];
|
|
1000
980
|
let featurePresent = false;
|
|
981
|
+
let featureTags = [];
|
|
982
|
+
let pendingTags = [];
|
|
983
|
+
let current = null;
|
|
984
|
+
const flush = () => {
|
|
985
|
+
if (!current) return;
|
|
986
|
+
scenarios.push({
|
|
987
|
+
...current,
|
|
988
|
+
body: current.body.trim()
|
|
989
|
+
});
|
|
990
|
+
current = null;
|
|
991
|
+
};
|
|
1001
992
|
for (let i = 0; i < lines.length; i++) {
|
|
1002
993
|
const line = lines[i] ?? "";
|
|
1003
|
-
|
|
994
|
+
const trimmed = line.trim();
|
|
995
|
+
if (TAG_LINE_RE.test(trimmed)) {
|
|
996
|
+
pendingTags.push(...parseTags(trimmed));
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (FEATURE_RE.test(trimmed)) {
|
|
1004
1000
|
featurePresent = true;
|
|
1001
|
+
featureTags = [...pendingTags];
|
|
1002
|
+
pendingTags = [];
|
|
1003
|
+
continue;
|
|
1005
1004
|
}
|
|
1006
|
-
const match =
|
|
1007
|
-
if (
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1005
|
+
const match = trimmed.match(SCENARIO_RE);
|
|
1006
|
+
if (match) {
|
|
1007
|
+
const scenarioName = match[1]?.trim();
|
|
1008
|
+
if (!scenarioName) {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
flush();
|
|
1012
|
+
current = {
|
|
1013
|
+
name: scenarioName,
|
|
1014
|
+
line: i + 1,
|
|
1015
|
+
tags: [...featureTags, ...pendingTags],
|
|
1016
|
+
body: ""
|
|
1017
|
+
};
|
|
1018
|
+
pendingTags = [];
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
if (current) {
|
|
1022
|
+
current.body += `${line}
|
|
1023
|
+
`;
|
|
1016
1024
|
}
|
|
1017
|
-
scenarios.push({ name: scenarioName, line: i + 1, tags });
|
|
1018
1025
|
}
|
|
1026
|
+
flush();
|
|
1019
1027
|
return { file, featurePresent, scenarios };
|
|
1020
1028
|
}
|
|
1021
1029
|
|
|
@@ -1062,9 +1070,9 @@ function extractH2Sections(md) {
|
|
|
1062
1070
|
|
|
1063
1071
|
// src/core/parse/spec.ts
|
|
1064
1072
|
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
|
|
1073
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1074
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1075
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1068
1076
|
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1069
1077
|
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1070
1078
|
function parseSpec(md, file) {
|
|
@@ -1141,12 +1149,9 @@ function parseSpec(md, file) {
|
|
|
1141
1149
|
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1142
1150
|
async function validateDefinedIds(root, config) {
|
|
1143
1151
|
const issues = [];
|
|
1144
|
-
const
|
|
1145
|
-
const
|
|
1146
|
-
const
|
|
1147
|
-
const scenarioFiles = await collectFiles(scenarioRoot, {
|
|
1148
|
-
extensions: [".feature"]
|
|
1149
|
-
});
|
|
1152
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1153
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
1154
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1150
1155
|
const defined = /* @__PURE__ */ new Map();
|
|
1151
1156
|
await collectSpecDefinitionIds(specFiles, defined);
|
|
1152
1157
|
await collectScenarioDefinitionIds(scenarioFiles, defined);
|
|
@@ -1203,7 +1208,7 @@ function recordId(out, id, file) {
|
|
|
1203
1208
|
}
|
|
1204
1209
|
function formatFileList(files, root) {
|
|
1205
1210
|
return files.map((file) => {
|
|
1206
|
-
const relative =
|
|
1211
|
+
const relative = path9.relative(root, file);
|
|
1207
1212
|
return relative.length > 0 ? relative : file;
|
|
1208
1213
|
}).join(", ");
|
|
1209
1214
|
}
|
|
@@ -1234,17 +1239,15 @@ var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
|
1234
1239
|
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1235
1240
|
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1236
1241
|
async function validateScenarios(root, config) {
|
|
1237
|
-
const
|
|
1238
|
-
const files = await
|
|
1239
|
-
extensions: [".feature"]
|
|
1240
|
-
});
|
|
1242
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1243
|
+
const files = await collectScenarioFiles(specsRoot);
|
|
1241
1244
|
if (files.length === 0) {
|
|
1242
1245
|
return [
|
|
1243
1246
|
issue4(
|
|
1244
1247
|
"QFAI-SC-000",
|
|
1245
1248
|
"Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1246
1249
|
"info",
|
|
1247
|
-
|
|
1250
|
+
specsRoot,
|
|
1248
1251
|
"scenario.files"
|
|
1249
1252
|
)
|
|
1250
1253
|
];
|
|
@@ -1310,8 +1313,11 @@ function validateScenarioContent(text, file) {
|
|
|
1310
1313
|
continue;
|
|
1311
1314
|
}
|
|
1312
1315
|
const missingTags = [];
|
|
1313
|
-
|
|
1314
|
-
|
|
1316
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
|
|
1317
|
+
if (scTags.length === 0) {
|
|
1318
|
+
missingTags.push("SC(0\u4EF6)");
|
|
1319
|
+
} else if (scTags.length > 1) {
|
|
1320
|
+
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1315
1321
|
}
|
|
1316
1322
|
if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
|
|
1317
1323
|
missingTags.push("SPEC");
|
|
@@ -1331,26 +1337,28 @@ function validateScenarioContent(text, file) {
|
|
|
1331
1337
|
);
|
|
1332
1338
|
}
|
|
1333
1339
|
}
|
|
1334
|
-
const
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1340
|
+
for (const scenario of parsed.scenarios) {
|
|
1341
|
+
const missingSteps = [];
|
|
1342
|
+
if (!GIVEN_PATTERN.test(scenario.body)) {
|
|
1343
|
+
missingSteps.push("Given");
|
|
1344
|
+
}
|
|
1345
|
+
if (!WHEN_PATTERN.test(scenario.body)) {
|
|
1346
|
+
missingSteps.push("When");
|
|
1347
|
+
}
|
|
1348
|
+
if (!THEN_PATTERN.test(scenario.body)) {
|
|
1349
|
+
missingSteps.push("Then");
|
|
1350
|
+
}
|
|
1351
|
+
if (missingSteps.length > 0) {
|
|
1352
|
+
issues.push(
|
|
1353
|
+
issue4(
|
|
1354
|
+
"QFAI-SC-005",
|
|
1355
|
+
`Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")} (${scenario.name})`,
|
|
1356
|
+
"warning",
|
|
1357
|
+
file,
|
|
1358
|
+
"scenario.steps"
|
|
1359
|
+
)
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1354
1362
|
}
|
|
1355
1363
|
return issues;
|
|
1356
1364
|
}
|
|
@@ -1375,14 +1383,14 @@ function issue4(code, message, severity, file, rule, refs) {
|
|
|
1375
1383
|
// src/core/validators/spec.ts
|
|
1376
1384
|
import { readFile as readFile8 } from "fs/promises";
|
|
1377
1385
|
async function validateSpecs(root, config) {
|
|
1378
|
-
const specsRoot = resolvePath(root, config, "
|
|
1386
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1379
1387
|
const files = await collectSpecFiles(specsRoot);
|
|
1380
1388
|
if (files.length === 0) {
|
|
1381
|
-
const expected = "spec-
|
|
1389
|
+
const expected = "spec-001/spec.md";
|
|
1382
1390
|
return [
|
|
1383
1391
|
issue5(
|
|
1384
1392
|
"QFAI-SPEC-000",
|
|
1385
|
-
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.
|
|
1393
|
+
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}`,
|
|
1386
1394
|
"info",
|
|
1387
1395
|
specsRoot,
|
|
1388
1396
|
"spec.files"
|
|
@@ -1528,18 +1536,11 @@ var API_TAG_RE = /^API-\d{4}$/;
|
|
|
1528
1536
|
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1529
1537
|
async function validateTraceability(root, config) {
|
|
1530
1538
|
const issues = [];
|
|
1531
|
-
const specsRoot = resolvePath(root, config, "
|
|
1532
|
-
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1533
|
-
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1539
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1534
1540
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1535
1541
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1536
1542
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
1537
|
-
const
|
|
1538
|
-
extensions: [".md"]
|
|
1539
|
-
});
|
|
1540
|
-
const scenarioFiles = await collectFiles(scenariosRoot, {
|
|
1541
|
-
extensions: [".feature"]
|
|
1542
|
-
});
|
|
1543
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1543
1544
|
const upstreamIds = /* @__PURE__ */ new Set();
|
|
1544
1545
|
const specIds = /* @__PURE__ */ new Set();
|
|
1545
1546
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
@@ -1587,10 +1588,6 @@ async function validateTraceability(root, config) {
|
|
|
1587
1588
|
specToBrIds.set(parsed.specId, current);
|
|
1588
1589
|
}
|
|
1589
1590
|
}
|
|
1590
|
-
for (const file of decisionFiles) {
|
|
1591
|
-
const text = await readFile9(file, "utf-8");
|
|
1592
|
-
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1593
|
-
}
|
|
1594
1591
|
for (const file of scenarioFiles) {
|
|
1595
1592
|
const text = await readFile9(file, "utf-8");
|
|
1596
1593
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
@@ -1734,7 +1731,7 @@ async function validateTraceability(root, config) {
|
|
|
1734
1731
|
", "
|
|
1735
1732
|
)}`,
|
|
1736
1733
|
"error",
|
|
1737
|
-
|
|
1734
|
+
specsRoot,
|
|
1738
1735
|
"traceability.scMustTouchContracts",
|
|
1739
1736
|
scWithoutContracts
|
|
1740
1737
|
)
|
|
@@ -1752,7 +1749,7 @@ async function validateTraceability(root, config) {
|
|
|
1752
1749
|
"QFAI_CONTRACT_ORPHAN",
|
|
1753
1750
|
`\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
1754
1751
|
"error",
|
|
1755
|
-
|
|
1752
|
+
specsRoot,
|
|
1756
1753
|
"traceability.allowOrphanContracts",
|
|
1757
1754
|
orphanContracts
|
|
1758
1755
|
)
|
|
@@ -1837,8 +1834,8 @@ async function validateProject(root, configResult) {
|
|
|
1837
1834
|
const issues = [
|
|
1838
1835
|
...configIssues,
|
|
1839
1836
|
...await validateSpecs(root, config),
|
|
1837
|
+
...await validateDeltas(root, config),
|
|
1840
1838
|
...await validateScenarios(root, config),
|
|
1841
|
-
...await validateDecisions(root, config),
|
|
1842
1839
|
...await validateContracts(root, config),
|
|
1843
1840
|
...await validateDefinedIds(root, config),
|
|
1844
1841
|
...await validateTraceability(root, config)
|
|
@@ -1867,21 +1864,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
1867
1864
|
const resolved = configResult ?? await loadConfig(root);
|
|
1868
1865
|
const config = resolved.config;
|
|
1869
1866
|
const configPath = resolved.configPath;
|
|
1870
|
-
const
|
|
1871
|
-
const
|
|
1872
|
-
const
|
|
1873
|
-
const
|
|
1874
|
-
const
|
|
1875
|
-
const dbRoot = resolvePath(root, config, "dataContractsDir");
|
|
1867
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1868
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1869
|
+
const apiRoot = path10.join(contractsRoot, "api");
|
|
1870
|
+
const uiRoot = path10.join(contractsRoot, "ui");
|
|
1871
|
+
const dbRoot = path10.join(contractsRoot, "db");
|
|
1876
1872
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1877
1873
|
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
|
-
});
|
|
1874
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
1875
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1885
1876
|
const {
|
|
1886
1877
|
api: apiFiles,
|
|
1887
1878
|
ui: uiFiles,
|
|
@@ -1890,7 +1881,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1890
1881
|
const idsByPrefix = await collectIds([
|
|
1891
1882
|
...specFiles,
|
|
1892
1883
|
...scenarioFiles,
|
|
1893
|
-
...decisionFiles,
|
|
1894
1884
|
...apiFiles,
|
|
1895
1885
|
...uiFiles,
|
|
1896
1886
|
...dbFiles
|
|
@@ -1915,7 +1905,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1915
1905
|
summary: {
|
|
1916
1906
|
specs: specFiles.length,
|
|
1917
1907
|
scenarios: scenarioFiles.length,
|
|
1918
|
-
decisions: decisionFiles.length,
|
|
1919
1908
|
contracts: {
|
|
1920
1909
|
api: apiFiles.length,
|
|
1921
1910
|
ui: uiFiles.length,
|
|
@@ -1949,7 +1938,6 @@ function formatReportMarkdown(data) {
|
|
|
1949
1938
|
lines.push("## \u6982\u8981");
|
|
1950
1939
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
1951
1940
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
1952
|
-
lines.push(`- decisions: ${data.summary.decisions}`);
|
|
1953
1941
|
lines.push(
|
|
1954
1942
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
1955
1943
|
);
|
|
@@ -2120,8 +2108,8 @@ export {
|
|
|
2120
2108
|
resolvePath,
|
|
2121
2109
|
resolveToolVersion,
|
|
2122
2110
|
validateContracts,
|
|
2123
|
-
validateDecisions,
|
|
2124
2111
|
validateDefinedIds,
|
|
2112
|
+
validateDeltas,
|
|
2125
2113
|
validateProject,
|
|
2126
2114
|
validateScenarioContent,
|
|
2127
2115
|
validateScenarios,
|