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.cjs
CHANGED
|
@@ -44,8 +44,8 @@ __export(src_exports, {
|
|
|
44
44
|
resolvePath: () => resolvePath,
|
|
45
45
|
resolveToolVersion: () => resolveToolVersion,
|
|
46
46
|
validateContracts: () => validateContracts,
|
|
47
|
-
validateDecisions: () => validateDecisions,
|
|
48
47
|
validateDefinedIds: () => validateDefinedIds,
|
|
48
|
+
validateDeltas: () => validateDeltas,
|
|
49
49
|
validateProject: () => validateProject,
|
|
50
50
|
validateScenarioContent: () => validateScenarioContent,
|
|
51
51
|
validateScenarios: () => validateScenarios,
|
|
@@ -61,13 +61,11 @@ var import_node_path = __toESM(require("path"), 1);
|
|
|
61
61
|
var import_yaml = require("yaml");
|
|
62
62
|
var defaultConfig = {
|
|
63
63
|
paths: {
|
|
64
|
-
specDir: ".qfai/spec",
|
|
65
|
-
decisionsDir: ".qfai/spec/decisions",
|
|
66
|
-
scenariosDir: ".qfai/spec/scenarios",
|
|
67
64
|
contractsDir: ".qfai/contracts",
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
specsDir: ".qfai/specs",
|
|
66
|
+
rulesDir: ".qfai/rules",
|
|
67
|
+
outDir: ".qfai/out",
|
|
68
|
+
promptsDir: ".qfai/prompts",
|
|
71
69
|
srcDir: "src",
|
|
72
70
|
testsDir: "tests"
|
|
73
71
|
},
|
|
@@ -92,8 +90,7 @@ var defaultConfig = {
|
|
|
92
90
|
}
|
|
93
91
|
},
|
|
94
92
|
output: {
|
|
95
|
-
|
|
96
|
-
jsonPath: ".qfai/out/validate.json"
|
|
93
|
+
validateJsonPath: ".qfai/out/validate.json"
|
|
97
94
|
}
|
|
98
95
|
};
|
|
99
96
|
function getConfigPath(root) {
|
|
@@ -142,27 +139,6 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
142
139
|
return base;
|
|
143
140
|
}
|
|
144
141
|
return {
|
|
145
|
-
specDir: readString(
|
|
146
|
-
raw.specDir,
|
|
147
|
-
base.specDir,
|
|
148
|
-
"paths.specDir",
|
|
149
|
-
configPath,
|
|
150
|
-
issues
|
|
151
|
-
),
|
|
152
|
-
decisionsDir: readString(
|
|
153
|
-
raw.decisionsDir,
|
|
154
|
-
base.decisionsDir,
|
|
155
|
-
"paths.decisionsDir",
|
|
156
|
-
configPath,
|
|
157
|
-
issues
|
|
158
|
-
),
|
|
159
|
-
scenariosDir: readString(
|
|
160
|
-
raw.scenariosDir,
|
|
161
|
-
base.scenariosDir,
|
|
162
|
-
"paths.scenariosDir",
|
|
163
|
-
configPath,
|
|
164
|
-
issues
|
|
165
|
-
),
|
|
166
142
|
contractsDir: readString(
|
|
167
143
|
raw.contractsDir,
|
|
168
144
|
base.contractsDir,
|
|
@@ -170,24 +146,31 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
170
146
|
configPath,
|
|
171
147
|
issues
|
|
172
148
|
),
|
|
173
|
-
|
|
174
|
-
raw.
|
|
175
|
-
base.
|
|
176
|
-
"paths.
|
|
149
|
+
specsDir: readString(
|
|
150
|
+
raw.specsDir,
|
|
151
|
+
base.specsDir,
|
|
152
|
+
"paths.specsDir",
|
|
153
|
+
configPath,
|
|
154
|
+
issues
|
|
155
|
+
),
|
|
156
|
+
rulesDir: readString(
|
|
157
|
+
raw.rulesDir,
|
|
158
|
+
base.rulesDir,
|
|
159
|
+
"paths.rulesDir",
|
|
177
160
|
configPath,
|
|
178
161
|
issues
|
|
179
162
|
),
|
|
180
|
-
|
|
181
|
-
raw.
|
|
182
|
-
base.
|
|
183
|
-
"paths.
|
|
163
|
+
outDir: readString(
|
|
164
|
+
raw.outDir,
|
|
165
|
+
base.outDir,
|
|
166
|
+
"paths.outDir",
|
|
184
167
|
configPath,
|
|
185
168
|
issues
|
|
186
169
|
),
|
|
187
|
-
|
|
188
|
-
raw.
|
|
189
|
-
base.
|
|
190
|
-
"paths.
|
|
170
|
+
promptsDir: readString(
|
|
171
|
+
raw.promptsDir,
|
|
172
|
+
base.promptsDir,
|
|
173
|
+
"paths.promptsDir",
|
|
191
174
|
configPath,
|
|
192
175
|
issues
|
|
193
176
|
),
|
|
@@ -310,17 +293,10 @@ function normalizeOutput(raw, configPath, issues) {
|
|
|
310
293
|
return base;
|
|
311
294
|
}
|
|
312
295
|
return {
|
|
313
|
-
|
|
314
|
-
raw.
|
|
315
|
-
base.
|
|
316
|
-
"output.
|
|
317
|
-
configPath,
|
|
318
|
-
issues
|
|
319
|
-
),
|
|
320
|
-
jsonPath: readString(
|
|
321
|
-
raw.jsonPath,
|
|
322
|
-
base.jsonPath,
|
|
323
|
-
"output.jsonPath",
|
|
296
|
+
validateJsonPath: readString(
|
|
297
|
+
raw.validateJsonPath,
|
|
298
|
+
base.validateJsonPath,
|
|
299
|
+
"output.validateJsonPath",
|
|
324
300
|
configPath,
|
|
325
301
|
issues
|
|
326
302
|
)
|
|
@@ -387,20 +363,6 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
|
387
363
|
}
|
|
388
364
|
return fallback;
|
|
389
365
|
}
|
|
390
|
-
function readOutputFormat(value, fallback, label, configPath, issues) {
|
|
391
|
-
if (value === "text" || value === "json" || value === "github") {
|
|
392
|
-
return value;
|
|
393
|
-
}
|
|
394
|
-
if (value !== void 0) {
|
|
395
|
-
issues.push(
|
|
396
|
-
configIssue(
|
|
397
|
-
configPath,
|
|
398
|
-
`${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
399
|
-
)
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
return fallback;
|
|
403
|
-
}
|
|
404
366
|
function configIssue(file, message) {
|
|
405
367
|
return {
|
|
406
368
|
code: "QFAI_CONFIG_INVALID",
|
|
@@ -481,6 +443,7 @@ function isValidId(value, prefix) {
|
|
|
481
443
|
|
|
482
444
|
// src/core/report.ts
|
|
483
445
|
var import_promises11 = require("fs/promises");
|
|
446
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
484
447
|
|
|
485
448
|
// src/core/discovery.ts
|
|
486
449
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
@@ -541,10 +504,24 @@ async function exists(target) {
|
|
|
541
504
|
}
|
|
542
505
|
|
|
543
506
|
// src/core/discovery.ts
|
|
544
|
-
var
|
|
545
|
-
async function
|
|
546
|
-
const files = await collectFiles(
|
|
547
|
-
|
|
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"));
|
|
548
525
|
}
|
|
549
526
|
async function collectUiContractFiles(uiRoot) {
|
|
550
527
|
return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
@@ -563,9 +540,12 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
|
563
540
|
]);
|
|
564
541
|
return { ui, api, db };
|
|
565
542
|
}
|
|
566
|
-
function
|
|
567
|
-
|
|
568
|
-
|
|
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);
|
|
569
549
|
}
|
|
570
550
|
|
|
571
551
|
// src/core/types.ts
|
|
@@ -576,8 +556,8 @@ var import_promises3 = require("fs/promises");
|
|
|
576
556
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
577
557
|
var import_node_url = require("url");
|
|
578
558
|
async function resolveToolVersion() {
|
|
579
|
-
if ("0.3.
|
|
580
|
-
return "0.3.
|
|
559
|
+
if ("0.3.1".length > 0) {
|
|
560
|
+
return "0.3.1";
|
|
581
561
|
}
|
|
582
562
|
try {
|
|
583
563
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -597,6 +577,7 @@ function resolvePackageJsonPath() {
|
|
|
597
577
|
|
|
598
578
|
// src/core/validators/contracts.ts
|
|
599
579
|
var import_promises4 = require("fs/promises");
|
|
580
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
600
581
|
|
|
601
582
|
// src/core/contracts.ts
|
|
602
583
|
var import_node_path5 = __toESM(require("path"), 1);
|
|
@@ -652,19 +633,10 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
652
633
|
];
|
|
653
634
|
async function validateContracts(root, config) {
|
|
654
635
|
const issues = [];
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
);
|
|
658
|
-
issues.push(
|
|
659
|
-
...await validateApiContracts(
|
|
660
|
-
resolvePath(root, config, "apiContractsDir")
|
|
661
|
-
)
|
|
662
|
-
);
|
|
663
|
-
issues.push(
|
|
664
|
-
...await validateDataContracts(
|
|
665
|
-
resolvePath(root, config, "dataContractsDir")
|
|
666
|
-
)
|
|
667
|
-
);
|
|
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")));
|
|
668
640
|
return issues;
|
|
669
641
|
}
|
|
670
642
|
async function validateUiContracts(uiRoot) {
|
|
@@ -897,72 +869,78 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
897
869
|
return issue7;
|
|
898
870
|
}
|
|
899
871
|
|
|
900
|
-
// src/core/validators/
|
|
872
|
+
// src/core/validators/delta.ts
|
|
901
873
|
var import_promises5 = require("fs/promises");
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
var
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
const status = extractField(md, "Status");
|
|
913
|
-
const context = extractField(md, "Context");
|
|
914
|
-
const decision = extractField(md, "Decision");
|
|
915
|
-
const consequences = extractField(md, "Consequences");
|
|
916
|
-
const related = extractField(md, "Related");
|
|
917
|
-
if (status) fields.status = status;
|
|
918
|
-
if (context) fields.context = context;
|
|
919
|
-
if (decision) fields.decision = decision;
|
|
920
|
-
if (consequences) fields.consequences = consequences;
|
|
921
|
-
if (related) fields.related = related;
|
|
922
|
-
const parsed = {
|
|
923
|
-
file,
|
|
924
|
-
fields
|
|
925
|
-
};
|
|
926
|
-
if (adrId) {
|
|
927
|
-
parsed.adrId = adrId;
|
|
928
|
-
}
|
|
929
|
-
return parsed;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// src/core/validators/decisions.ts
|
|
933
|
-
var REQUIRED_FIELDS = [
|
|
934
|
-
{ key: "status", label: "Status" },
|
|
935
|
-
{ key: "context", label: "Context" },
|
|
936
|
-
{ key: "decision", label: "Decision" },
|
|
937
|
-
{ key: "consequences", label: "Consequences" }
|
|
938
|
-
];
|
|
939
|
-
async function validateDecisions(root, config) {
|
|
940
|
-
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
941
|
-
const files = await collectFiles(decisionsRoot, { extensions: [".md"] });
|
|
942
|
-
if (files.length === 0) {
|
|
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) {
|
|
943
884
|
return [];
|
|
944
885
|
}
|
|
945
886
|
const issues = [];
|
|
946
|
-
for (const
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
(
|
|
951
|
-
)
|
|
952
|
-
|
|
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) {
|
|
953
911
|
issues.push(
|
|
954
912
|
issue2(
|
|
955
|
-
"QFAI-
|
|
956
|
-
|
|
913
|
+
"QFAI-DELTA-002",
|
|
914
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
957
915
|
"error",
|
|
958
|
-
|
|
959
|
-
"
|
|
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"
|
|
960
932
|
)
|
|
961
933
|
);
|
|
962
934
|
}
|
|
963
935
|
}
|
|
964
936
|
return issues;
|
|
965
937
|
}
|
|
938
|
+
function isMissingFileError(error) {
|
|
939
|
+
if (!error || typeof error !== "object") {
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
return error.code === "ENOENT";
|
|
943
|
+
}
|
|
966
944
|
function issue2(code, message, severity, file, rule, refs) {
|
|
967
945
|
const issue7 = {
|
|
968
946
|
code,
|
|
@@ -983,14 +961,16 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
983
961
|
|
|
984
962
|
// src/core/validators/ids.ts
|
|
985
963
|
var import_promises7 = require("fs/promises");
|
|
986
|
-
var
|
|
964
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
987
965
|
|
|
988
966
|
// src/core/contractIndex.ts
|
|
989
967
|
var import_promises6 = require("fs/promises");
|
|
968
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
990
969
|
async function buildContractIndex(root, config) {
|
|
991
|
-
const
|
|
992
|
-
const
|
|
993
|
-
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");
|
|
994
974
|
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
995
975
|
collectUiContractFiles(uiRoot),
|
|
996
976
|
collectApiContractFiles(apiRoot),
|
|
@@ -1046,7 +1026,7 @@ function record(index, id, file) {
|
|
|
1046
1026
|
|
|
1047
1027
|
// src/core/parse/gherkin.ts
|
|
1048
1028
|
var FEATURE_RE = /^\s*Feature:\s+/;
|
|
1049
|
-
var SCENARIO_RE = /^\s*Scenario
|
|
1029
|
+
var SCENARIO_RE = /^\s*Scenario(?: Outline)?:\s*(.+)\s*$/;
|
|
1050
1030
|
var TAG_LINE_RE = /^\s*@/;
|
|
1051
1031
|
function parseTags(line) {
|
|
1052
1032
|
return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
|
|
@@ -1055,24 +1035,52 @@ function parseGherkinFeature(text, file) {
|
|
|
1055
1035
|
const lines = text.split(/\r?\n/);
|
|
1056
1036
|
const scenarios = [];
|
|
1057
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
|
+
};
|
|
1058
1049
|
for (let i = 0; i < lines.length; i++) {
|
|
1059
1050
|
const line = lines[i] ?? "";
|
|
1060
|
-
|
|
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)) {
|
|
1061
1057
|
featurePresent = true;
|
|
1058
|
+
featureTags = [...pendingTags];
|
|
1059
|
+
pendingTags = [];
|
|
1060
|
+
continue;
|
|
1062
1061
|
}
|
|
1063
|
-
const match =
|
|
1064
|
-
if (
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
+
`;
|
|
1073
1081
|
}
|
|
1074
|
-
scenarios.push({ name: scenarioName, line: i + 1, tags });
|
|
1075
1082
|
}
|
|
1083
|
+
flush();
|
|
1076
1084
|
return { file, featurePresent, scenarios };
|
|
1077
1085
|
}
|
|
1078
1086
|
|
|
@@ -1119,9 +1127,9 @@ function extractH2Sections(md) {
|
|
|
1119
1127
|
|
|
1120
1128
|
// src/core/parse/spec.ts
|
|
1121
1129
|
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1122
|
-
var BR_LINE_RE = /^\s
|
|
1123
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s
|
|
1124
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s
|
|
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.*)$/;
|
|
1125
1133
|
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1126
1134
|
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1127
1135
|
function parseSpec(md, file) {
|
|
@@ -1198,12 +1206,9 @@ function parseSpec(md, file) {
|
|
|
1198
1206
|
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1199
1207
|
async function validateDefinedIds(root, config) {
|
|
1200
1208
|
const issues = [];
|
|
1201
|
-
const
|
|
1202
|
-
const
|
|
1203
|
-
const
|
|
1204
|
-
const scenarioFiles = await collectFiles(scenarioRoot, {
|
|
1205
|
-
extensions: [".feature"]
|
|
1206
|
-
});
|
|
1209
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1210
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
1211
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1207
1212
|
const defined = /* @__PURE__ */ new Map();
|
|
1208
1213
|
await collectSpecDefinitionIds(specFiles, defined);
|
|
1209
1214
|
await collectScenarioDefinitionIds(scenarioFiles, defined);
|
|
@@ -1260,7 +1265,7 @@ function recordId(out, id, file) {
|
|
|
1260
1265
|
}
|
|
1261
1266
|
function formatFileList(files, root) {
|
|
1262
1267
|
return files.map((file) => {
|
|
1263
|
-
const relative =
|
|
1268
|
+
const relative = import_node_path9.default.relative(root, file);
|
|
1264
1269
|
return relative.length > 0 ? relative : file;
|
|
1265
1270
|
}).join(", ");
|
|
1266
1271
|
}
|
|
@@ -1291,17 +1296,15 @@ var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
|
1291
1296
|
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1292
1297
|
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1293
1298
|
async function validateScenarios(root, config) {
|
|
1294
|
-
const
|
|
1295
|
-
const files = await
|
|
1296
|
-
extensions: [".feature"]
|
|
1297
|
-
});
|
|
1299
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1300
|
+
const files = await collectScenarioFiles(specsRoot);
|
|
1298
1301
|
if (files.length === 0) {
|
|
1299
1302
|
return [
|
|
1300
1303
|
issue4(
|
|
1301
1304
|
"QFAI-SC-000",
|
|
1302
1305
|
"Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1303
1306
|
"info",
|
|
1304
|
-
|
|
1307
|
+
specsRoot,
|
|
1305
1308
|
"scenario.files"
|
|
1306
1309
|
)
|
|
1307
1310
|
];
|
|
@@ -1367,8 +1370,11 @@ function validateScenarioContent(text, file) {
|
|
|
1367
1370
|
continue;
|
|
1368
1371
|
}
|
|
1369
1372
|
const missingTags = [];
|
|
1370
|
-
|
|
1371
|
-
|
|
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)`);
|
|
1372
1378
|
}
|
|
1373
1379
|
if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
|
|
1374
1380
|
missingTags.push("SPEC");
|
|
@@ -1388,26 +1394,28 @@ function validateScenarioContent(text, file) {
|
|
|
1388
1394
|
);
|
|
1389
1395
|
}
|
|
1390
1396
|
}
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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
|
+
}
|
|
1411
1419
|
}
|
|
1412
1420
|
return issues;
|
|
1413
1421
|
}
|
|
@@ -1432,14 +1440,14 @@ function issue4(code, message, severity, file, rule, refs) {
|
|
|
1432
1440
|
// src/core/validators/spec.ts
|
|
1433
1441
|
var import_promises9 = require("fs/promises");
|
|
1434
1442
|
async function validateSpecs(root, config) {
|
|
1435
|
-
const specsRoot = resolvePath(root, config, "
|
|
1443
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1436
1444
|
const files = await collectSpecFiles(specsRoot);
|
|
1437
1445
|
if (files.length === 0) {
|
|
1438
|
-
const expected = "spec-
|
|
1446
|
+
const expected = "spec-001/spec.md";
|
|
1439
1447
|
return [
|
|
1440
1448
|
issue5(
|
|
1441
1449
|
"QFAI-SPEC-000",
|
|
1442
|
-
`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}`,
|
|
1443
1451
|
"info",
|
|
1444
1452
|
specsRoot,
|
|
1445
1453
|
"spec.files"
|
|
@@ -1585,18 +1593,11 @@ var API_TAG_RE = /^API-\d{4}$/;
|
|
|
1585
1593
|
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1586
1594
|
async function validateTraceability(root, config) {
|
|
1587
1595
|
const issues = [];
|
|
1588
|
-
const specsRoot = resolvePath(root, config, "
|
|
1589
|
-
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1590
|
-
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1596
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1591
1597
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1592
1598
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1593
1599
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
1594
|
-
const
|
|
1595
|
-
extensions: [".md"]
|
|
1596
|
-
});
|
|
1597
|
-
const scenarioFiles = await collectFiles(scenariosRoot, {
|
|
1598
|
-
extensions: [".feature"]
|
|
1599
|
-
});
|
|
1600
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1600
1601
|
const upstreamIds = /* @__PURE__ */ new Set();
|
|
1601
1602
|
const specIds = /* @__PURE__ */ new Set();
|
|
1602
1603
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
@@ -1644,10 +1645,6 @@ async function validateTraceability(root, config) {
|
|
|
1644
1645
|
specToBrIds.set(parsed.specId, current);
|
|
1645
1646
|
}
|
|
1646
1647
|
}
|
|
1647
|
-
for (const file of decisionFiles) {
|
|
1648
|
-
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1649
|
-
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1650
|
-
}
|
|
1651
1648
|
for (const file of scenarioFiles) {
|
|
1652
1649
|
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1653
1650
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
@@ -1791,7 +1788,7 @@ async function validateTraceability(root, config) {
|
|
|
1791
1788
|
", "
|
|
1792
1789
|
)}`,
|
|
1793
1790
|
"error",
|
|
1794
|
-
|
|
1791
|
+
specsRoot,
|
|
1795
1792
|
"traceability.scMustTouchContracts",
|
|
1796
1793
|
scWithoutContracts
|
|
1797
1794
|
)
|
|
@@ -1809,7 +1806,7 @@ async function validateTraceability(root, config) {
|
|
|
1809
1806
|
"QFAI_CONTRACT_ORPHAN",
|
|
1810
1807
|
`\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
1811
1808
|
"error",
|
|
1812
|
-
|
|
1809
|
+
specsRoot,
|
|
1813
1810
|
"traceability.allowOrphanContracts",
|
|
1814
1811
|
orphanContracts
|
|
1815
1812
|
)
|
|
@@ -1894,8 +1891,8 @@ async function validateProject(root, configResult) {
|
|
|
1894
1891
|
const issues = [
|
|
1895
1892
|
...configIssues,
|
|
1896
1893
|
...await validateSpecs(root, config),
|
|
1894
|
+
...await validateDeltas(root, config),
|
|
1897
1895
|
...await validateScenarios(root, config),
|
|
1898
|
-
...await validateDecisions(root, config),
|
|
1899
1896
|
...await validateContracts(root, config),
|
|
1900
1897
|
...await validateDefinedIds(root, config),
|
|
1901
1898
|
...await validateTraceability(root, config)
|
|
@@ -1924,21 +1921,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
1924
1921
|
const resolved = configResult ?? await loadConfig(root);
|
|
1925
1922
|
const config = resolved.config;
|
|
1926
1923
|
const configPath = resolved.configPath;
|
|
1927
|
-
const
|
|
1928
|
-
const
|
|
1929
|
-
const
|
|
1930
|
-
const
|
|
1931
|
-
const
|
|
1932
|
-
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");
|
|
1933
1929
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1934
1930
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1935
|
-
const specFiles = await collectSpecFiles(
|
|
1936
|
-
const scenarioFiles = await
|
|
1937
|
-
extensions: [".feature"]
|
|
1938
|
-
});
|
|
1939
|
-
const decisionFiles = await collectFiles(decisionsRoot, {
|
|
1940
|
-
extensions: [".md"]
|
|
1941
|
-
});
|
|
1931
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
1932
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1942
1933
|
const {
|
|
1943
1934
|
api: apiFiles,
|
|
1944
1935
|
ui: uiFiles,
|
|
@@ -1947,7 +1938,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1947
1938
|
const idsByPrefix = await collectIds([
|
|
1948
1939
|
...specFiles,
|
|
1949
1940
|
...scenarioFiles,
|
|
1950
|
-
...decisionFiles,
|
|
1951
1941
|
...apiFiles,
|
|
1952
1942
|
...uiFiles,
|
|
1953
1943
|
...dbFiles
|
|
@@ -1972,7 +1962,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1972
1962
|
summary: {
|
|
1973
1963
|
specs: specFiles.length,
|
|
1974
1964
|
scenarios: scenarioFiles.length,
|
|
1975
|
-
decisions: decisionFiles.length,
|
|
1976
1965
|
contracts: {
|
|
1977
1966
|
api: apiFiles.length,
|
|
1978
1967
|
ui: uiFiles.length,
|
|
@@ -2006,7 +1995,6 @@ function formatReportMarkdown(data) {
|
|
|
2006
1995
|
lines.push("## \u6982\u8981");
|
|
2007
1996
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2008
1997
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
2009
|
-
lines.push(`- decisions: ${data.summary.decisions}`);
|
|
2010
1998
|
lines.push(
|
|
2011
1999
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
2012
2000
|
);
|
|
@@ -2178,8 +2166,8 @@ function buildHotspots(issues) {
|
|
|
2178
2166
|
resolvePath,
|
|
2179
2167
|
resolveToolVersion,
|
|
2180
2168
|
validateContracts,
|
|
2181
|
-
validateDecisions,
|
|
2182
2169
|
validateDefinedIds,
|
|
2170
|
+
validateDeltas,
|
|
2183
2171
|
validateProject,
|
|
2184
2172
|
validateScenarioContent,
|
|
2185
2173
|
validateScenarios,
|