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/cli/index.cjs
CHANGED
|
@@ -160,8 +160,8 @@ function report(copied, skipped, dryRun, label) {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
// src/cli/commands/report.ts
|
|
163
|
-
var
|
|
164
|
-
var
|
|
163
|
+
var import_promises15 = require("fs/promises");
|
|
164
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
165
165
|
|
|
166
166
|
// src/core/config.ts
|
|
167
167
|
var import_promises2 = require("fs/promises");
|
|
@@ -169,13 +169,11 @@ var import_node_path4 = __toESM(require("path"), 1);
|
|
|
169
169
|
var import_yaml = require("yaml");
|
|
170
170
|
var defaultConfig = {
|
|
171
171
|
paths: {
|
|
172
|
-
specDir: ".qfai/spec",
|
|
173
|
-
decisionsDir: ".qfai/spec/decisions",
|
|
174
|
-
scenariosDir: ".qfai/spec/scenarios",
|
|
175
172
|
contractsDir: ".qfai/contracts",
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
173
|
+
specsDir: ".qfai/specs",
|
|
174
|
+
rulesDir: ".qfai/rules",
|
|
175
|
+
outDir: ".qfai/out",
|
|
176
|
+
promptsDir: ".qfai/prompts",
|
|
179
177
|
srcDir: "src",
|
|
180
178
|
testsDir: "tests"
|
|
181
179
|
},
|
|
@@ -200,8 +198,7 @@ var defaultConfig = {
|
|
|
200
198
|
}
|
|
201
199
|
},
|
|
202
200
|
output: {
|
|
203
|
-
|
|
204
|
-
jsonPath: ".qfai/out/validate.json"
|
|
201
|
+
validateJsonPath: ".qfai/out/validate.json"
|
|
205
202
|
}
|
|
206
203
|
};
|
|
207
204
|
function getConfigPath(root) {
|
|
@@ -250,27 +247,6 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
250
247
|
return base;
|
|
251
248
|
}
|
|
252
249
|
return {
|
|
253
|
-
specDir: readString(
|
|
254
|
-
raw.specDir,
|
|
255
|
-
base.specDir,
|
|
256
|
-
"paths.specDir",
|
|
257
|
-
configPath,
|
|
258
|
-
issues
|
|
259
|
-
),
|
|
260
|
-
decisionsDir: readString(
|
|
261
|
-
raw.decisionsDir,
|
|
262
|
-
base.decisionsDir,
|
|
263
|
-
"paths.decisionsDir",
|
|
264
|
-
configPath,
|
|
265
|
-
issues
|
|
266
|
-
),
|
|
267
|
-
scenariosDir: readString(
|
|
268
|
-
raw.scenariosDir,
|
|
269
|
-
base.scenariosDir,
|
|
270
|
-
"paths.scenariosDir",
|
|
271
|
-
configPath,
|
|
272
|
-
issues
|
|
273
|
-
),
|
|
274
250
|
contractsDir: readString(
|
|
275
251
|
raw.contractsDir,
|
|
276
252
|
base.contractsDir,
|
|
@@ -278,24 +254,31 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
278
254
|
configPath,
|
|
279
255
|
issues
|
|
280
256
|
),
|
|
281
|
-
|
|
282
|
-
raw.
|
|
283
|
-
base.
|
|
284
|
-
"paths.
|
|
257
|
+
specsDir: readString(
|
|
258
|
+
raw.specsDir,
|
|
259
|
+
base.specsDir,
|
|
260
|
+
"paths.specsDir",
|
|
285
261
|
configPath,
|
|
286
262
|
issues
|
|
287
263
|
),
|
|
288
|
-
|
|
289
|
-
raw.
|
|
290
|
-
base.
|
|
291
|
-
"paths.
|
|
264
|
+
rulesDir: readString(
|
|
265
|
+
raw.rulesDir,
|
|
266
|
+
base.rulesDir,
|
|
267
|
+
"paths.rulesDir",
|
|
292
268
|
configPath,
|
|
293
269
|
issues
|
|
294
270
|
),
|
|
295
|
-
|
|
296
|
-
raw.
|
|
297
|
-
base.
|
|
298
|
-
"paths.
|
|
271
|
+
outDir: readString(
|
|
272
|
+
raw.outDir,
|
|
273
|
+
base.outDir,
|
|
274
|
+
"paths.outDir",
|
|
275
|
+
configPath,
|
|
276
|
+
issues
|
|
277
|
+
),
|
|
278
|
+
promptsDir: readString(
|
|
279
|
+
raw.promptsDir,
|
|
280
|
+
base.promptsDir,
|
|
281
|
+
"paths.promptsDir",
|
|
299
282
|
configPath,
|
|
300
283
|
issues
|
|
301
284
|
),
|
|
@@ -418,17 +401,10 @@ function normalizeOutput(raw, configPath, issues) {
|
|
|
418
401
|
return base;
|
|
419
402
|
}
|
|
420
403
|
return {
|
|
421
|
-
|
|
422
|
-
raw.
|
|
423
|
-
base.
|
|
424
|
-
"output.
|
|
425
|
-
configPath,
|
|
426
|
-
issues
|
|
427
|
-
),
|
|
428
|
-
jsonPath: readString(
|
|
429
|
-
raw.jsonPath,
|
|
430
|
-
base.jsonPath,
|
|
431
|
-
"output.jsonPath",
|
|
404
|
+
validateJsonPath: readString(
|
|
405
|
+
raw.validateJsonPath,
|
|
406
|
+
base.validateJsonPath,
|
|
407
|
+
"output.validateJsonPath",
|
|
432
408
|
configPath,
|
|
433
409
|
issues
|
|
434
410
|
)
|
|
@@ -495,20 +471,6 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
|
495
471
|
}
|
|
496
472
|
return fallback;
|
|
497
473
|
}
|
|
498
|
-
function readOutputFormat(value, fallback, label, configPath, issues) {
|
|
499
|
-
if (value === "text" || value === "json" || value === "github") {
|
|
500
|
-
return value;
|
|
501
|
-
}
|
|
502
|
-
if (value !== void 0) {
|
|
503
|
-
issues.push(
|
|
504
|
-
configIssue(
|
|
505
|
-
configPath,
|
|
506
|
-
`${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
507
|
-
)
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
return fallback;
|
|
511
|
-
}
|
|
512
474
|
function configIssue(file, message) {
|
|
513
475
|
return {
|
|
514
476
|
code: "QFAI_CONFIG_INVALID",
|
|
@@ -535,10 +497,11 @@ function isRecord(value) {
|
|
|
535
497
|
}
|
|
536
498
|
|
|
537
499
|
// src/core/report.ts
|
|
538
|
-
var
|
|
500
|
+
var import_promises14 = require("fs/promises");
|
|
501
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
539
502
|
|
|
540
503
|
// src/core/discovery.ts
|
|
541
|
-
var
|
|
504
|
+
var import_promises5 = require("fs/promises");
|
|
542
505
|
|
|
543
506
|
// src/core/fs.ts
|
|
544
507
|
var import_promises3 = require("fs/promises");
|
|
@@ -595,11 +558,50 @@ async function exists2(target) {
|
|
|
595
558
|
}
|
|
596
559
|
}
|
|
597
560
|
|
|
561
|
+
// src/core/specLayout.ts
|
|
562
|
+
var import_promises4 = require("fs/promises");
|
|
563
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
564
|
+
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
565
|
+
async function collectSpecEntries(specsRoot) {
|
|
566
|
+
const dirs = await listSpecDirs(specsRoot);
|
|
567
|
+
const entries = dirs.map((dir) => ({
|
|
568
|
+
dir,
|
|
569
|
+
specPath: import_node_path6.default.join(dir, "spec.md"),
|
|
570
|
+
deltaPath: import_node_path6.default.join(dir, "delta.md"),
|
|
571
|
+
scenarioPath: import_node_path6.default.join(dir, "scenario.md")
|
|
572
|
+
}));
|
|
573
|
+
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
574
|
+
}
|
|
575
|
+
async function listSpecDirs(specsRoot) {
|
|
576
|
+
try {
|
|
577
|
+
const items = await (0, import_promises4.readdir)(specsRoot, { withFileTypes: true });
|
|
578
|
+
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => import_node_path6.default.join(specsRoot, name));
|
|
579
|
+
} catch (error2) {
|
|
580
|
+
if (isMissingFileError(error2)) {
|
|
581
|
+
return [];
|
|
582
|
+
}
|
|
583
|
+
throw error2;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function isMissingFileError(error2) {
|
|
587
|
+
if (!error2 || typeof error2 !== "object") {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
return error2.code === "ENOENT";
|
|
591
|
+
}
|
|
592
|
+
|
|
598
593
|
// src/core/discovery.ts
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
594
|
+
async function collectSpecPackDirs(specsRoot) {
|
|
595
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
596
|
+
return entries.map((entry) => entry.dir);
|
|
597
|
+
}
|
|
598
|
+
async function collectSpecFiles(specsRoot) {
|
|
599
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
600
|
+
return filterExisting(entries.map((entry) => entry.specPath));
|
|
601
|
+
}
|
|
602
|
+
async function collectScenarioFiles(specsRoot) {
|
|
603
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
604
|
+
return filterExisting(entries.map((entry) => entry.scenarioPath));
|
|
603
605
|
}
|
|
604
606
|
async function collectUiContractFiles(uiRoot) {
|
|
605
607
|
return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
@@ -618,9 +620,22 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
|
618
620
|
]);
|
|
619
621
|
return { ui, api, db };
|
|
620
622
|
}
|
|
621
|
-
function
|
|
622
|
-
const
|
|
623
|
-
|
|
623
|
+
async function filterExisting(files) {
|
|
624
|
+
const existing = [];
|
|
625
|
+
for (const file of files) {
|
|
626
|
+
if (await exists3(file)) {
|
|
627
|
+
existing.push(file);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return existing;
|
|
631
|
+
}
|
|
632
|
+
async function exists3(target) {
|
|
633
|
+
try {
|
|
634
|
+
await (0, import_promises5.access)(target);
|
|
635
|
+
return true;
|
|
636
|
+
} catch {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
624
639
|
}
|
|
625
640
|
|
|
626
641
|
// src/core/ids.ts
|
|
@@ -680,16 +695,16 @@ function isValidId(value, prefix) {
|
|
|
680
695
|
var VALIDATION_SCHEMA_VERSION = "0.2";
|
|
681
696
|
|
|
682
697
|
// src/core/version.ts
|
|
683
|
-
var
|
|
698
|
+
var import_promises6 = require("fs/promises");
|
|
684
699
|
var import_node_path7 = __toESM(require("path"), 1);
|
|
685
700
|
var import_node_url2 = require("url");
|
|
686
701
|
async function resolveToolVersion() {
|
|
687
|
-
if ("0.3.
|
|
688
|
-
return "0.3.
|
|
702
|
+
if ("0.3.2".length > 0) {
|
|
703
|
+
return "0.3.2";
|
|
689
704
|
}
|
|
690
705
|
try {
|
|
691
706
|
const packagePath = resolvePackageJsonPath();
|
|
692
|
-
const raw = await (0,
|
|
707
|
+
const raw = await (0, import_promises6.readFile)(packagePath, "utf-8");
|
|
693
708
|
const parsed = JSON.parse(raw);
|
|
694
709
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
695
710
|
return version.length > 0 ? version : "unknown";
|
|
@@ -704,7 +719,8 @@ function resolvePackageJsonPath() {
|
|
|
704
719
|
}
|
|
705
720
|
|
|
706
721
|
// src/core/validators/contracts.ts
|
|
707
|
-
var
|
|
722
|
+
var import_promises7 = require("fs/promises");
|
|
723
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
708
724
|
|
|
709
725
|
// src/core/contracts.ts
|
|
710
726
|
var import_node_path8 = __toESM(require("path"), 1);
|
|
@@ -760,19 +776,10 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
760
776
|
];
|
|
761
777
|
async function validateContracts(root, config) {
|
|
762
778
|
const issues = [];
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
);
|
|
766
|
-
issues.push(
|
|
767
|
-
...await validateApiContracts(
|
|
768
|
-
resolvePath(root, config, "apiContractsDir")
|
|
769
|
-
)
|
|
770
|
-
);
|
|
771
|
-
issues.push(
|
|
772
|
-
...await validateDataContracts(
|
|
773
|
-
resolvePath(root, config, "dataContractsDir")
|
|
774
|
-
)
|
|
775
|
-
);
|
|
779
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
780
|
+
issues.push(...await validateUiContracts(import_node_path9.default.join(contractsRoot, "ui")));
|
|
781
|
+
issues.push(...await validateApiContracts(import_node_path9.default.join(contractsRoot, "api")));
|
|
782
|
+
issues.push(...await validateDataContracts(import_node_path9.default.join(contractsRoot, "db")));
|
|
776
783
|
return issues;
|
|
777
784
|
}
|
|
778
785
|
async function validateUiContracts(uiRoot) {
|
|
@@ -790,7 +797,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
790
797
|
}
|
|
791
798
|
const issues = [];
|
|
792
799
|
for (const file of files) {
|
|
793
|
-
const text = await (0,
|
|
800
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
794
801
|
const invalidIds = extractInvalidIds(text, [
|
|
795
802
|
"SPEC",
|
|
796
803
|
"BR",
|
|
@@ -857,7 +864,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
857
864
|
}
|
|
858
865
|
const issues = [];
|
|
859
866
|
for (const file of files) {
|
|
860
|
-
const text = await (0,
|
|
867
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
861
868
|
const invalidIds = extractInvalidIds(text, [
|
|
862
869
|
"SPEC",
|
|
863
870
|
"BR",
|
|
@@ -935,7 +942,7 @@ async function validateDataContracts(dataRoot) {
|
|
|
935
942
|
}
|
|
936
943
|
const issues = [];
|
|
937
944
|
for (const file of files) {
|
|
938
|
-
const text = await (0,
|
|
945
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
939
946
|
const invalidIds = extractInvalidIds(text, [
|
|
940
947
|
"SPEC",
|
|
941
948
|
"BR",
|
|
@@ -1005,72 +1012,78 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1005
1012
|
return issue7;
|
|
1006
1013
|
}
|
|
1007
1014
|
|
|
1008
|
-
// src/core/validators/
|
|
1009
|
-
var
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
var
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
const status = extractField(md, "Status");
|
|
1021
|
-
const context = extractField(md, "Context");
|
|
1022
|
-
const decision = extractField(md, "Decision");
|
|
1023
|
-
const consequences = extractField(md, "Consequences");
|
|
1024
|
-
const related = extractField(md, "Related");
|
|
1025
|
-
if (status) fields.status = status;
|
|
1026
|
-
if (context) fields.context = context;
|
|
1027
|
-
if (decision) fields.decision = decision;
|
|
1028
|
-
if (consequences) fields.consequences = consequences;
|
|
1029
|
-
if (related) fields.related = related;
|
|
1030
|
-
const parsed = {
|
|
1031
|
-
file,
|
|
1032
|
-
fields
|
|
1033
|
-
};
|
|
1034
|
-
if (adrId) {
|
|
1035
|
-
parsed.adrId = adrId;
|
|
1036
|
-
}
|
|
1037
|
-
return parsed;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
// src/core/validators/decisions.ts
|
|
1041
|
-
var REQUIRED_FIELDS = [
|
|
1042
|
-
{ key: "status", label: "Status" },
|
|
1043
|
-
{ key: "context", label: "Context" },
|
|
1044
|
-
{ key: "decision", label: "Decision" },
|
|
1045
|
-
{ key: "consequences", label: "Consequences" }
|
|
1046
|
-
];
|
|
1047
|
-
async function validateDecisions(root, config) {
|
|
1048
|
-
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1049
|
-
const files = await collectFiles(decisionsRoot, { extensions: [".md"] });
|
|
1050
|
-
if (files.length === 0) {
|
|
1015
|
+
// src/core/validators/delta.ts
|
|
1016
|
+
var import_promises8 = require("fs/promises");
|
|
1017
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
1018
|
+
var SECTION_RE = /^##\s+変更区分/m;
|
|
1019
|
+
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1020
|
+
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
1021
|
+
var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
|
|
1022
|
+
var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
|
|
1023
|
+
async function validateDeltas(root, config) {
|
|
1024
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1025
|
+
const packs = await collectSpecPackDirs(specsRoot);
|
|
1026
|
+
if (packs.length === 0) {
|
|
1051
1027
|
return [];
|
|
1052
1028
|
}
|
|
1053
1029
|
const issues = [];
|
|
1054
|
-
for (const
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
(
|
|
1059
|
-
)
|
|
1060
|
-
|
|
1030
|
+
for (const pack of packs) {
|
|
1031
|
+
const deltaPath = import_node_path10.default.join(pack, "delta.md");
|
|
1032
|
+
let text;
|
|
1033
|
+
try {
|
|
1034
|
+
text = await (0, import_promises8.readFile)(deltaPath, "utf-8");
|
|
1035
|
+
} catch (error2) {
|
|
1036
|
+
if (isMissingFileError2(error2)) {
|
|
1037
|
+
issues.push(
|
|
1038
|
+
issue2(
|
|
1039
|
+
"QFAI-DELTA-001",
|
|
1040
|
+
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1041
|
+
"error",
|
|
1042
|
+
deltaPath,
|
|
1043
|
+
"delta.exists"
|
|
1044
|
+
)
|
|
1045
|
+
);
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
throw error2;
|
|
1049
|
+
}
|
|
1050
|
+
const hasSection = SECTION_RE.test(text);
|
|
1051
|
+
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
1052
|
+
const hasChange = CHANGE_LINE_RE.test(text);
|
|
1053
|
+
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
1061
1054
|
issues.push(
|
|
1062
1055
|
issue2(
|
|
1063
|
-
"QFAI-
|
|
1064
|
-
|
|
1056
|
+
"QFAI-DELTA-002",
|
|
1057
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
1065
1058
|
"error",
|
|
1066
|
-
|
|
1067
|
-
"
|
|
1059
|
+
deltaPath,
|
|
1060
|
+
"delta.section"
|
|
1061
|
+
)
|
|
1062
|
+
);
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
1066
|
+
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
1067
|
+
if (compatibilityChecked === changeChecked) {
|
|
1068
|
+
issues.push(
|
|
1069
|
+
issue2(
|
|
1070
|
+
"QFAI-DELTA-003",
|
|
1071
|
+
"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",
|
|
1072
|
+
"error",
|
|
1073
|
+
deltaPath,
|
|
1074
|
+
"delta.classification"
|
|
1068
1075
|
)
|
|
1069
1076
|
);
|
|
1070
1077
|
}
|
|
1071
1078
|
}
|
|
1072
1079
|
return issues;
|
|
1073
1080
|
}
|
|
1081
|
+
function isMissingFileError2(error2) {
|
|
1082
|
+
if (!error2 || typeof error2 !== "object") {
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1085
|
+
return error2.code === "ENOENT";
|
|
1086
|
+
}
|
|
1074
1087
|
function issue2(code, message, severity, file, rule, refs) {
|
|
1075
1088
|
const issue7 = {
|
|
1076
1089
|
code,
|
|
@@ -1090,15 +1103,17 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1090
1103
|
}
|
|
1091
1104
|
|
|
1092
1105
|
// src/core/validators/ids.ts
|
|
1093
|
-
var
|
|
1094
|
-
var
|
|
1106
|
+
var import_promises10 = require("fs/promises");
|
|
1107
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
1095
1108
|
|
|
1096
1109
|
// src/core/contractIndex.ts
|
|
1097
|
-
var
|
|
1110
|
+
var import_promises9 = require("fs/promises");
|
|
1111
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
1098
1112
|
async function buildContractIndex(root, config) {
|
|
1099
|
-
const
|
|
1100
|
-
const
|
|
1101
|
-
const
|
|
1113
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1114
|
+
const uiRoot = import_node_path11.default.join(contractsRoot, "ui");
|
|
1115
|
+
const apiRoot = import_node_path11.default.join(contractsRoot, "api");
|
|
1116
|
+
const dataRoot = import_node_path11.default.join(contractsRoot, "db");
|
|
1102
1117
|
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
1103
1118
|
collectUiContractFiles(uiRoot),
|
|
1104
1119
|
collectApiContractFiles(apiRoot),
|
|
@@ -1117,7 +1132,7 @@ async function buildContractIndex(root, config) {
|
|
|
1117
1132
|
}
|
|
1118
1133
|
async function indexUiContracts(files, index) {
|
|
1119
1134
|
for (const file of files) {
|
|
1120
|
-
const text = await (0,
|
|
1135
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1121
1136
|
try {
|
|
1122
1137
|
const doc = parseStructuredContract(file, text);
|
|
1123
1138
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1129,7 +1144,7 @@ async function indexUiContracts(files, index) {
|
|
|
1129
1144
|
}
|
|
1130
1145
|
async function indexApiContracts(files, index) {
|
|
1131
1146
|
for (const file of files) {
|
|
1132
|
-
const text = await (0,
|
|
1147
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1133
1148
|
try {
|
|
1134
1149
|
const doc = parseStructuredContract(file, text);
|
|
1135
1150
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1141,7 +1156,7 @@ async function indexApiContracts(files, index) {
|
|
|
1141
1156
|
}
|
|
1142
1157
|
async function indexDataContracts(files, index) {
|
|
1143
1158
|
for (const file of files) {
|
|
1144
|
-
const text = await (0,
|
|
1159
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1145
1160
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1146
1161
|
}
|
|
1147
1162
|
}
|
|
@@ -1152,38 +1167,6 @@ function record(index, id, file) {
|
|
|
1152
1167
|
index.idToFiles.set(id, current);
|
|
1153
1168
|
}
|
|
1154
1169
|
|
|
1155
|
-
// src/core/parse/gherkin.ts
|
|
1156
|
-
var FEATURE_RE = /^\s*Feature:\s+/;
|
|
1157
|
-
var SCENARIO_RE = /^\s*Scenario:\s*(.+)\s*$/;
|
|
1158
|
-
var TAG_LINE_RE = /^\s*@/;
|
|
1159
|
-
function parseTags(line) {
|
|
1160
|
-
return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
|
|
1161
|
-
}
|
|
1162
|
-
function parseGherkinFeature(text, file) {
|
|
1163
|
-
const lines = text.split(/\r?\n/);
|
|
1164
|
-
const scenarios = [];
|
|
1165
|
-
let featurePresent = false;
|
|
1166
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1167
|
-
const line = lines[i] ?? "";
|
|
1168
|
-
if (FEATURE_RE.test(line)) {
|
|
1169
|
-
featurePresent = true;
|
|
1170
|
-
}
|
|
1171
|
-
const match = line.match(SCENARIO_RE);
|
|
1172
|
-
if (!match) continue;
|
|
1173
|
-
const scenarioName = match[1];
|
|
1174
|
-
if (!scenarioName) continue;
|
|
1175
|
-
const tags = [];
|
|
1176
|
-
for (let j = i - 1; j >= 0; j--) {
|
|
1177
|
-
const previous = lines[j] ?? "";
|
|
1178
|
-
if (previous.trim() === "") continue;
|
|
1179
|
-
if (!TAG_LINE_RE.test(previous)) break;
|
|
1180
|
-
tags.unshift(...parseTags(previous));
|
|
1181
|
-
}
|
|
1182
|
-
scenarios.push({ name: scenarioName, line: i + 1, tags });
|
|
1183
|
-
}
|
|
1184
|
-
return { file, featurePresent, scenarios };
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
1170
|
// src/core/parse/markdown.ts
|
|
1188
1171
|
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1189
1172
|
function parseHeadings(md) {
|
|
@@ -1227,9 +1210,9 @@ function extractH2Sections(md) {
|
|
|
1227
1210
|
|
|
1228
1211
|
// src/core/parse/spec.ts
|
|
1229
1212
|
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1230
|
-
var BR_LINE_RE = /^\s
|
|
1231
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s
|
|
1232
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s
|
|
1213
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1214
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1215
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1233
1216
|
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1234
1217
|
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1235
1218
|
function parseSpec(md, file) {
|
|
@@ -1302,16 +1285,167 @@ function parseSpec(md, file) {
|
|
|
1302
1285
|
return parsed;
|
|
1303
1286
|
}
|
|
1304
1287
|
|
|
1305
|
-
// src/core/
|
|
1288
|
+
// src/core/gherkin/parse.ts
|
|
1289
|
+
var import_gherkin = require("@cucumber/gherkin");
|
|
1290
|
+
var import_node_crypto = require("crypto");
|
|
1291
|
+
function parseGherkin(source, uri) {
|
|
1292
|
+
const errors = [];
|
|
1293
|
+
const uuidFn = () => (0, import_node_crypto.randomUUID)();
|
|
1294
|
+
const builder = new import_gherkin.AstBuilder(uuidFn);
|
|
1295
|
+
const matcher = new import_gherkin.GherkinClassicTokenMatcher();
|
|
1296
|
+
const parser = new import_gherkin.Parser(builder, matcher);
|
|
1297
|
+
try {
|
|
1298
|
+
const gherkinDocument = parser.parse(source);
|
|
1299
|
+
gherkinDocument.uri = uri;
|
|
1300
|
+
return { gherkinDocument, errors };
|
|
1301
|
+
} catch (error2) {
|
|
1302
|
+
errors.push(formatError3(error2));
|
|
1303
|
+
return { gherkinDocument: null, errors };
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
function formatError3(error2) {
|
|
1307
|
+
if (error2 instanceof Error) {
|
|
1308
|
+
return error2.message;
|
|
1309
|
+
}
|
|
1310
|
+
return String(error2);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/core/scenarioModel.ts
|
|
1314
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1306
1315
|
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1316
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1317
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1318
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
1319
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1320
|
+
function parseScenarioDocument(text, uri) {
|
|
1321
|
+
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1322
|
+
if (!gherkinDocument) {
|
|
1323
|
+
return { document: null, errors };
|
|
1324
|
+
}
|
|
1325
|
+
const feature = gherkinDocument.feature;
|
|
1326
|
+
if (!feature) {
|
|
1327
|
+
return {
|
|
1328
|
+
document: { uri, featureTags: [], scenarios: [] },
|
|
1329
|
+
errors
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
const featureTags = collectTagNames(feature.tags);
|
|
1333
|
+
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
1334
|
+
return {
|
|
1335
|
+
document: {
|
|
1336
|
+
uri,
|
|
1337
|
+
featureName: feature.name,
|
|
1338
|
+
featureTags,
|
|
1339
|
+
scenarios
|
|
1340
|
+
},
|
|
1341
|
+
errors
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
function buildScenarioAtoms(document) {
|
|
1345
|
+
return document.scenarios.map((scenario) => {
|
|
1346
|
+
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1347
|
+
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1348
|
+
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1349
|
+
const contractIds = /* @__PURE__ */ new Set();
|
|
1350
|
+
scenario.tags.forEach((tag) => {
|
|
1351
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1352
|
+
contractIds.add(tag);
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
for (const step of scenario.steps) {
|
|
1356
|
+
for (const text of collectStepTexts(step)) {
|
|
1357
|
+
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1358
|
+
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1359
|
+
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
const atom = {
|
|
1363
|
+
uri: document.uri,
|
|
1364
|
+
featureName: document.featureName ?? "",
|
|
1365
|
+
scenarioName: scenario.name,
|
|
1366
|
+
kind: scenario.kind,
|
|
1367
|
+
brIds,
|
|
1368
|
+
contractIds: Array.from(contractIds).sort()
|
|
1369
|
+
};
|
|
1370
|
+
if (scenario.line !== void 0) {
|
|
1371
|
+
atom.line = scenario.line;
|
|
1372
|
+
}
|
|
1373
|
+
if (specIds.length === 1) {
|
|
1374
|
+
const specId = specIds[0];
|
|
1375
|
+
if (specId) {
|
|
1376
|
+
atom.specId = specId;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
if (scIds.length === 1) {
|
|
1380
|
+
const scId = scIds[0];
|
|
1381
|
+
if (scId) {
|
|
1382
|
+
atom.scId = scId;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return atom;
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
function collectScenarioNodes(feature, featureTags) {
|
|
1389
|
+
const scenarios = [];
|
|
1390
|
+
for (const child of feature.children) {
|
|
1391
|
+
if (child.scenario) {
|
|
1392
|
+
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
1393
|
+
}
|
|
1394
|
+
if (child.rule) {
|
|
1395
|
+
const ruleTags = collectTagNames(child.rule.tags);
|
|
1396
|
+
for (const ruleChild of child.rule.children) {
|
|
1397
|
+
if (ruleChild.scenario) {
|
|
1398
|
+
scenarios.push(
|
|
1399
|
+
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return scenarios;
|
|
1406
|
+
}
|
|
1407
|
+
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
1408
|
+
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
1409
|
+
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
1410
|
+
return {
|
|
1411
|
+
name: scenario.name,
|
|
1412
|
+
kind,
|
|
1413
|
+
line: scenario.location?.line,
|
|
1414
|
+
tags,
|
|
1415
|
+
steps: scenario.steps
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
function collectTagNames(tags) {
|
|
1419
|
+
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1420
|
+
}
|
|
1421
|
+
function collectStepTexts(step) {
|
|
1422
|
+
const texts = [];
|
|
1423
|
+
if (step.text) {
|
|
1424
|
+
texts.push(step.text);
|
|
1425
|
+
}
|
|
1426
|
+
if (step.docString?.content) {
|
|
1427
|
+
texts.push(step.docString.content);
|
|
1428
|
+
}
|
|
1429
|
+
if (step.dataTable?.rows) {
|
|
1430
|
+
for (const row of step.dataTable.rows) {
|
|
1431
|
+
for (const cell of row.cells) {
|
|
1432
|
+
texts.push(cell.value);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
return texts;
|
|
1437
|
+
}
|
|
1438
|
+
function unique2(values) {
|
|
1439
|
+
return Array.from(new Set(values));
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// src/core/validators/ids.ts
|
|
1443
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1307
1444
|
async function validateDefinedIds(root, config) {
|
|
1308
1445
|
const issues = [];
|
|
1309
|
-
const
|
|
1310
|
-
const
|
|
1311
|
-
const
|
|
1312
|
-
const scenarioFiles = await collectFiles(scenarioRoot, {
|
|
1313
|
-
extensions: [".feature"]
|
|
1314
|
-
});
|
|
1446
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1447
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
1448
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1315
1449
|
const defined = /* @__PURE__ */ new Map();
|
|
1316
1450
|
await collectSpecDefinitionIds(specFiles, defined);
|
|
1317
1451
|
await collectScenarioDefinitionIds(scenarioFiles, defined);
|
|
@@ -1340,7 +1474,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1340
1474
|
}
|
|
1341
1475
|
async function collectSpecDefinitionIds(files, out) {
|
|
1342
1476
|
for (const file of files) {
|
|
1343
|
-
const text = await (0,
|
|
1477
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1344
1478
|
const parsed = parseSpec(text, file);
|
|
1345
1479
|
if (parsed.specId) {
|
|
1346
1480
|
recordId(out, parsed.specId, file);
|
|
@@ -1350,11 +1484,14 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1350
1484
|
}
|
|
1351
1485
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1352
1486
|
for (const file of files) {
|
|
1353
|
-
const text = await (0,
|
|
1354
|
-
const
|
|
1355
|
-
|
|
1487
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1488
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1489
|
+
if (!document || errors.length > 0) {
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
for (const scenario of document.scenarios) {
|
|
1356
1493
|
for (const tag of scenario.tags) {
|
|
1357
|
-
if (
|
|
1494
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
1358
1495
|
recordId(out, tag, file);
|
|
1359
1496
|
}
|
|
1360
1497
|
}
|
|
@@ -1368,7 +1505,7 @@ function recordId(out, id, file) {
|
|
|
1368
1505
|
}
|
|
1369
1506
|
function formatFileList(files, root) {
|
|
1370
1507
|
return files.map((file) => {
|
|
1371
|
-
const relative =
|
|
1508
|
+
const relative = import_node_path12.default.relative(root, file);
|
|
1372
1509
|
return relative.length > 0 ? relative : file;
|
|
1373
1510
|
}).join(", ");
|
|
1374
1511
|
}
|
|
@@ -1391,39 +1528,55 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1391
1528
|
}
|
|
1392
1529
|
|
|
1393
1530
|
// src/core/validators/scenario.ts
|
|
1394
|
-
var
|
|
1531
|
+
var import_promises11 = require("fs/promises");
|
|
1395
1532
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1396
1533
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1397
1534
|
var THEN_PATTERN = /\bThen\b/;
|
|
1398
|
-
var
|
|
1399
|
-
var
|
|
1400
|
-
var
|
|
1535
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1536
|
+
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1537
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1401
1538
|
async function validateScenarios(root, config) {
|
|
1402
|
-
const
|
|
1403
|
-
const
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1539
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1540
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1541
|
+
if (entries.length === 0) {
|
|
1542
|
+
const expected = "spec-0001/scenario.md";
|
|
1543
|
+
const legacy = "spec-001/scenario.md";
|
|
1407
1544
|
return [
|
|
1408
1545
|
issue4(
|
|
1409
1546
|
"QFAI-SC-000",
|
|
1410
|
-
|
|
1547
|
+
`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)`,
|
|
1411
1548
|
"info",
|
|
1412
|
-
|
|
1549
|
+
specsRoot,
|
|
1413
1550
|
"scenario.files"
|
|
1414
1551
|
)
|
|
1415
1552
|
];
|
|
1416
1553
|
}
|
|
1417
1554
|
const issues = [];
|
|
1418
|
-
for (const
|
|
1419
|
-
|
|
1420
|
-
|
|
1555
|
+
for (const entry of entries) {
|
|
1556
|
+
let text;
|
|
1557
|
+
try {
|
|
1558
|
+
text = await (0, import_promises11.readFile)(entry.scenarioPath, "utf-8");
|
|
1559
|
+
} catch (error2) {
|
|
1560
|
+
if (isMissingFileError3(error2)) {
|
|
1561
|
+
issues.push(
|
|
1562
|
+
issue4(
|
|
1563
|
+
"QFAI-SC-001",
|
|
1564
|
+
"scenario.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1565
|
+
"error",
|
|
1566
|
+
entry.scenarioPath,
|
|
1567
|
+
"scenario.exists"
|
|
1568
|
+
)
|
|
1569
|
+
);
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
throw error2;
|
|
1573
|
+
}
|
|
1574
|
+
issues.push(...validateScenarioContent(text, entry.scenarioPath));
|
|
1421
1575
|
}
|
|
1422
1576
|
return issues;
|
|
1423
1577
|
}
|
|
1424
1578
|
function validateScenarioContent(text, file) {
|
|
1425
1579
|
const issues = [];
|
|
1426
|
-
const parsed = parseGherkinFeature(text, file);
|
|
1427
1580
|
const invalidIds = extractInvalidIds(text, [
|
|
1428
1581
|
"SPEC",
|
|
1429
1582
|
"BR",
|
|
@@ -1445,9 +1598,47 @@ function validateScenarioContent(text, file) {
|
|
|
1445
1598
|
)
|
|
1446
1599
|
);
|
|
1447
1600
|
}
|
|
1601
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1602
|
+
if (!document || errors.length > 0) {
|
|
1603
|
+
issues.push(
|
|
1604
|
+
issue4(
|
|
1605
|
+
"QFAI-SC-010",
|
|
1606
|
+
`Gherkin \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errors.join(", ") || "unknown"}`,
|
|
1607
|
+
"error",
|
|
1608
|
+
file,
|
|
1609
|
+
"scenario.parse"
|
|
1610
|
+
)
|
|
1611
|
+
);
|
|
1612
|
+
return issues;
|
|
1613
|
+
}
|
|
1614
|
+
const featureSpecTags = document.featureTags.filter(
|
|
1615
|
+
(tag) => SPEC_TAG_RE2.test(tag)
|
|
1616
|
+
);
|
|
1617
|
+
if (featureSpecTags.length === 0) {
|
|
1618
|
+
issues.push(
|
|
1619
|
+
issue4(
|
|
1620
|
+
"QFAI-SC-009",
|
|
1621
|
+
"Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1622
|
+
"error",
|
|
1623
|
+
file,
|
|
1624
|
+
"scenario.featureSpec"
|
|
1625
|
+
)
|
|
1626
|
+
);
|
|
1627
|
+
} else if (featureSpecTags.length > 1) {
|
|
1628
|
+
issues.push(
|
|
1629
|
+
issue4(
|
|
1630
|
+
"QFAI-SC-009",
|
|
1631
|
+
`Feature \u306E SPEC \u30BF\u30B0\u304C\u8907\u6570\u3042\u308A\u307E\u3059: ${featureSpecTags.join(", ")}`,
|
|
1632
|
+
"error",
|
|
1633
|
+
file,
|
|
1634
|
+
"scenario.featureSpec",
|
|
1635
|
+
featureSpecTags
|
|
1636
|
+
)
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1448
1639
|
const missingStructure = [];
|
|
1449
|
-
if (!
|
|
1450
|
-
if (
|
|
1640
|
+
if (!document.featureName) missingStructure.push("Feature");
|
|
1641
|
+
if (document.scenarios.length === 0) missingStructure.push("Scenario");
|
|
1451
1642
|
if (missingStructure.length > 0) {
|
|
1452
1643
|
issues.push(
|
|
1453
1644
|
issue4(
|
|
@@ -1461,7 +1652,7 @@ function validateScenarioContent(text, file) {
|
|
|
1461
1652
|
)
|
|
1462
1653
|
);
|
|
1463
1654
|
}
|
|
1464
|
-
for (const scenario of
|
|
1655
|
+
for (const scenario of document.scenarios) {
|
|
1465
1656
|
if (scenario.tags.length === 0) {
|
|
1466
1657
|
issues.push(
|
|
1467
1658
|
issue4(
|
|
@@ -1475,13 +1666,16 @@ function validateScenarioContent(text, file) {
|
|
|
1475
1666
|
continue;
|
|
1476
1667
|
}
|
|
1477
1668
|
const missingTags = [];
|
|
1478
|
-
|
|
1479
|
-
|
|
1669
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
|
|
1670
|
+
if (scTags.length === 0) {
|
|
1671
|
+
missingTags.push("SC(0\u4EF6)");
|
|
1672
|
+
} else if (scTags.length > 1) {
|
|
1673
|
+
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1480
1674
|
}
|
|
1481
|
-
if (!scenario.tags.some((tag) =>
|
|
1675
|
+
if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
|
|
1482
1676
|
missingTags.push("SPEC");
|
|
1483
1677
|
}
|
|
1484
|
-
if (!scenario.tags.some((tag) =>
|
|
1678
|
+
if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
|
|
1485
1679
|
missingTags.push("BR");
|
|
1486
1680
|
}
|
|
1487
1681
|
if (missingTags.length > 0) {
|
|
@@ -1496,26 +1690,29 @@ function validateScenarioContent(text, file) {
|
|
|
1496
1690
|
);
|
|
1497
1691
|
}
|
|
1498
1692
|
}
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1693
|
+
for (const scenario of document.scenarios) {
|
|
1694
|
+
const missingSteps = [];
|
|
1695
|
+
const keywords = scenario.steps.map((step) => step.keyword.trim());
|
|
1696
|
+
if (!keywords.some((keyword) => GIVEN_PATTERN.test(keyword))) {
|
|
1697
|
+
missingSteps.push("Given");
|
|
1698
|
+
}
|
|
1699
|
+
if (!keywords.some((keyword) => WHEN_PATTERN.test(keyword))) {
|
|
1700
|
+
missingSteps.push("When");
|
|
1701
|
+
}
|
|
1702
|
+
if (!keywords.some((keyword) => THEN_PATTERN.test(keyword))) {
|
|
1703
|
+
missingSteps.push("Then");
|
|
1704
|
+
}
|
|
1705
|
+
if (missingSteps.length > 0) {
|
|
1706
|
+
issues.push(
|
|
1707
|
+
issue4(
|
|
1708
|
+
"QFAI-SC-005",
|
|
1709
|
+
`Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")} (${scenario.name})`,
|
|
1710
|
+
"warning",
|
|
1711
|
+
file,
|
|
1712
|
+
"scenario.steps"
|
|
1713
|
+
)
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1519
1716
|
}
|
|
1520
1717
|
return issues;
|
|
1521
1718
|
}
|
|
@@ -1536,18 +1733,25 @@ function issue4(code, message, severity, file, rule, refs) {
|
|
|
1536
1733
|
}
|
|
1537
1734
|
return issue7;
|
|
1538
1735
|
}
|
|
1736
|
+
function isMissingFileError3(error2) {
|
|
1737
|
+
if (!error2 || typeof error2 !== "object") {
|
|
1738
|
+
return false;
|
|
1739
|
+
}
|
|
1740
|
+
return error2.code === "ENOENT";
|
|
1741
|
+
}
|
|
1539
1742
|
|
|
1540
1743
|
// src/core/validators/spec.ts
|
|
1541
|
-
var
|
|
1744
|
+
var import_promises12 = require("fs/promises");
|
|
1542
1745
|
async function validateSpecs(root, config) {
|
|
1543
|
-
const specsRoot = resolvePath(root, config, "
|
|
1544
|
-
const
|
|
1545
|
-
if (
|
|
1546
|
-
const expected = "spec-0001
|
|
1746
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1747
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1748
|
+
if (entries.length === 0) {
|
|
1749
|
+
const expected = "spec-0001/spec.md";
|
|
1750
|
+
const legacy = "spec-001/spec.md";
|
|
1547
1751
|
return [
|
|
1548
1752
|
issue5(
|
|
1549
1753
|
"QFAI-SPEC-000",
|
|
1550
|
-
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.
|
|
1754
|
+
`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)`,
|
|
1551
1755
|
"info",
|
|
1552
1756
|
specsRoot,
|
|
1553
1757
|
"spec.files"
|
|
@@ -1555,12 +1759,29 @@ async function validateSpecs(root, config) {
|
|
|
1555
1759
|
];
|
|
1556
1760
|
}
|
|
1557
1761
|
const issues = [];
|
|
1558
|
-
for (const
|
|
1559
|
-
|
|
1762
|
+
for (const entry of entries) {
|
|
1763
|
+
let text;
|
|
1764
|
+
try {
|
|
1765
|
+
text = await (0, import_promises12.readFile)(entry.specPath, "utf-8");
|
|
1766
|
+
} catch (error2) {
|
|
1767
|
+
if (isMissingFileError4(error2)) {
|
|
1768
|
+
issues.push(
|
|
1769
|
+
issue5(
|
|
1770
|
+
"QFAI-SPEC-005",
|
|
1771
|
+
"spec.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1772
|
+
"error",
|
|
1773
|
+
entry.specPath,
|
|
1774
|
+
"spec.exists"
|
|
1775
|
+
)
|
|
1776
|
+
);
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
throw error2;
|
|
1780
|
+
}
|
|
1560
1781
|
issues.push(
|
|
1561
1782
|
...validateSpecContent(
|
|
1562
1783
|
text,
|
|
1563
|
-
|
|
1784
|
+
entry.specPath,
|
|
1564
1785
|
config.validation.require.specSections
|
|
1565
1786
|
)
|
|
1566
1787
|
);
|
|
@@ -1682,29 +1903,25 @@ function issue5(code, message, severity, file, rule, refs) {
|
|
|
1682
1903
|
}
|
|
1683
1904
|
return issue7;
|
|
1684
1905
|
}
|
|
1906
|
+
function isMissingFileError4(error2) {
|
|
1907
|
+
if (!error2 || typeof error2 !== "object") {
|
|
1908
|
+
return false;
|
|
1909
|
+
}
|
|
1910
|
+
return error2.code === "ENOENT";
|
|
1911
|
+
}
|
|
1685
1912
|
|
|
1686
1913
|
// src/core/validators/traceability.ts
|
|
1687
|
-
var
|
|
1688
|
-
var
|
|
1689
|
-
var
|
|
1690
|
-
var
|
|
1691
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1692
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
1693
|
-
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1914
|
+
var import_promises13 = require("fs/promises");
|
|
1915
|
+
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1916
|
+
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1917
|
+
var BR_TAG_RE3 = /^BR-\d{4}$/;
|
|
1694
1918
|
async function validateTraceability(root, config) {
|
|
1695
1919
|
const issues = [];
|
|
1696
|
-
const specsRoot = resolvePath(root, config, "
|
|
1697
|
-
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1698
|
-
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1920
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1699
1921
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1700
1922
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1701
1923
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
1702
|
-
const
|
|
1703
|
-
extensions: [".md"]
|
|
1704
|
-
});
|
|
1705
|
-
const scenarioFiles = await collectFiles(scenariosRoot, {
|
|
1706
|
-
extensions: [".feature"]
|
|
1707
|
-
});
|
|
1924
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1708
1925
|
const upstreamIds = /* @__PURE__ */ new Set();
|
|
1709
1926
|
const specIds = /* @__PURE__ */ new Set();
|
|
1710
1927
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
@@ -1716,7 +1933,7 @@ async function validateTraceability(root, config) {
|
|
|
1716
1933
|
const contractIndex = await buildContractIndex(root, config);
|
|
1717
1934
|
const contractIds = contractIndex.ids;
|
|
1718
1935
|
for (const file of specFiles) {
|
|
1719
|
-
const text = await (0,
|
|
1936
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1720
1937
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1721
1938
|
const parsed = parseSpec(text, file);
|
|
1722
1939
|
if (parsed.specId) {
|
|
@@ -1752,111 +1969,100 @@ async function validateTraceability(root, config) {
|
|
|
1752
1969
|
specToBrIds.set(parsed.specId, current);
|
|
1753
1970
|
}
|
|
1754
1971
|
}
|
|
1755
|
-
for (const file of decisionFiles) {
|
|
1756
|
-
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1757
|
-
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1758
|
-
}
|
|
1759
1972
|
for (const file of scenarioFiles) {
|
|
1760
|
-
const text = await (0,
|
|
1973
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1761
1974
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
const scIds = /* @__PURE__ */ new Set();
|
|
1766
|
-
const scenarioIds = /* @__PURE__ */ new Set();
|
|
1767
|
-
for (const scenario of parsed.scenarios) {
|
|
1768
|
-
for (const tag of scenario.tags) {
|
|
1769
|
-
if (SPEC_TAG_RE2.test(tag)) {
|
|
1770
|
-
specIdsInScenario.add(tag);
|
|
1771
|
-
}
|
|
1772
|
-
if (BR_TAG_RE2.test(tag)) {
|
|
1773
|
-
brIds.add(tag);
|
|
1774
|
-
}
|
|
1775
|
-
if (SC_TAG_RE3.test(tag)) {
|
|
1776
|
-
scIds.add(tag);
|
|
1777
|
-
}
|
|
1778
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1779
|
-
scenarioIds.add(tag);
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
const specIdsList = Array.from(specIdsInScenario);
|
|
1784
|
-
const brIdsList = Array.from(brIds);
|
|
1785
|
-
const scIdsList = Array.from(scIds);
|
|
1786
|
-
const scenarioIdsList = Array.from(scenarioIds);
|
|
1787
|
-
brIdsList.forEach((id) => brIdsInScenarios.add(id));
|
|
1788
|
-
scIdsList.forEach((id) => scIdsInScenarios.add(id));
|
|
1789
|
-
scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
|
|
1790
|
-
if (scenarioIdsList.length > 0) {
|
|
1791
|
-
scIdsList.forEach((id) => scWithContracts.add(id));
|
|
1792
|
-
}
|
|
1793
|
-
const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
|
|
1794
|
-
if (unknownSpecIds.length > 0) {
|
|
1795
|
-
issues.push(
|
|
1796
|
-
issue6(
|
|
1797
|
-
"QFAI-TRACE-005",
|
|
1798
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1799
|
-
"error",
|
|
1800
|
-
file,
|
|
1801
|
-
"traceability.scenarioSpecExists",
|
|
1802
|
-
unknownSpecIds
|
|
1803
|
-
)
|
|
1804
|
-
);
|
|
1805
|
-
}
|
|
1806
|
-
const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
|
|
1807
|
-
if (unknownBrIds.length > 0) {
|
|
1808
|
-
issues.push(
|
|
1809
|
-
issue6(
|
|
1810
|
-
"QFAI-TRACE-006",
|
|
1811
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1812
|
-
"error",
|
|
1813
|
-
file,
|
|
1814
|
-
"traceability.scenarioBrExists",
|
|
1815
|
-
unknownBrIds
|
|
1816
|
-
)
|
|
1817
|
-
);
|
|
1818
|
-
}
|
|
1819
|
-
const unknownContractIds = scenarioIdsList.filter(
|
|
1820
|
-
(id) => !contractIds.has(id)
|
|
1821
|
-
);
|
|
1822
|
-
if (unknownContractIds.length > 0) {
|
|
1823
|
-
issues.push(
|
|
1824
|
-
issue6(
|
|
1825
|
-
"QFAI-TRACE-008",
|
|
1826
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1827
|
-
", "
|
|
1828
|
-
)}`,
|
|
1829
|
-
config.validation.traceability.unknownContractIdSeverity,
|
|
1830
|
-
file,
|
|
1831
|
-
"traceability.scenarioContractExists",
|
|
1832
|
-
unknownContractIds
|
|
1833
|
-
)
|
|
1834
|
-
);
|
|
1975
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1976
|
+
if (!document || errors.length > 0) {
|
|
1977
|
+
continue;
|
|
1835
1978
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1979
|
+
const atoms = buildScenarioAtoms(document);
|
|
1980
|
+
for (const [index, scenario] of document.scenarios.entries()) {
|
|
1981
|
+
const atom = atoms[index];
|
|
1982
|
+
if (!atom) {
|
|
1983
|
+
continue;
|
|
1984
|
+
}
|
|
1985
|
+
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
1986
|
+
const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
|
|
1987
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
|
|
1988
|
+
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
1989
|
+
scTags.forEach((id) => scIdsInScenarios.add(id));
|
|
1990
|
+
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
1991
|
+
if (atom.contractIds.length > 0) {
|
|
1992
|
+
scTags.forEach((id) => scWithContracts.add(id));
|
|
1844
1993
|
}
|
|
1845
|
-
const
|
|
1846
|
-
if (
|
|
1994
|
+
const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
|
|
1995
|
+
if (unknownSpecIds.length > 0) {
|
|
1847
1996
|
issues.push(
|
|
1848
1997
|
issue6(
|
|
1849
|
-
"QFAI-TRACE-
|
|
1850
|
-
`Scenario \
|
|
1998
|
+
"QFAI-TRACE-005",
|
|
1999
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(
|
|
1851
2000
|
", "
|
|
1852
|
-
)} (
|
|
2001
|
+
)} (${scenario.name})`,
|
|
1853
2002
|
"error",
|
|
1854
2003
|
file,
|
|
1855
|
-
"traceability.
|
|
1856
|
-
|
|
2004
|
+
"traceability.scenarioSpecExists",
|
|
2005
|
+
unknownSpecIds
|
|
1857
2006
|
)
|
|
1858
2007
|
);
|
|
1859
2008
|
}
|
|
2009
|
+
const unknownBrIds = brTags.filter((id) => !brIdsInSpecs.has(id));
|
|
2010
|
+
if (unknownBrIds.length > 0) {
|
|
2011
|
+
issues.push(
|
|
2012
|
+
issue6(
|
|
2013
|
+
"QFAI-TRACE-006",
|
|
2014
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(
|
|
2015
|
+
", "
|
|
2016
|
+
)} (${scenario.name})`,
|
|
2017
|
+
"error",
|
|
2018
|
+
file,
|
|
2019
|
+
"traceability.scenarioBrExists",
|
|
2020
|
+
unknownBrIds
|
|
2021
|
+
)
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
const unknownContractIds = atom.contractIds.filter(
|
|
2025
|
+
(id) => !contractIds.has(id)
|
|
2026
|
+
);
|
|
2027
|
+
if (unknownContractIds.length > 0) {
|
|
2028
|
+
issues.push(
|
|
2029
|
+
issue6(
|
|
2030
|
+
"QFAI-TRACE-008",
|
|
2031
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
2032
|
+
", "
|
|
2033
|
+
)} (${scenario.name})`,
|
|
2034
|
+
config.validation.traceability.unknownContractIdSeverity,
|
|
2035
|
+
file,
|
|
2036
|
+
"traceability.scenarioContractExists",
|
|
2037
|
+
unknownContractIds
|
|
2038
|
+
)
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
if (specTags.length > 0 && brTags.length > 0) {
|
|
2042
|
+
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
2043
|
+
for (const specId of specTags) {
|
|
2044
|
+
const brIdsForSpec = specToBrIds.get(specId);
|
|
2045
|
+
if (!brIdsForSpec) {
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
2049
|
+
}
|
|
2050
|
+
const invalidBrIds = brTags.filter((id) => !allowedBrIds.has(id));
|
|
2051
|
+
if (invalidBrIds.length > 0) {
|
|
2052
|
+
issues.push(
|
|
2053
|
+
issue6(
|
|
2054
|
+
"QFAI-TRACE-007",
|
|
2055
|
+
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
2056
|
+
", "
|
|
2057
|
+
)} (SPEC: ${specTags.join(", ")}) (${scenario.name})`,
|
|
2058
|
+
"error",
|
|
2059
|
+
file,
|
|
2060
|
+
"traceability.scenarioBrUnderSpec",
|
|
2061
|
+
invalidBrIds
|
|
2062
|
+
)
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
1860
2066
|
}
|
|
1861
2067
|
}
|
|
1862
2068
|
if (upstreamIds.size === 0) {
|
|
@@ -1899,7 +2105,7 @@ async function validateTraceability(root, config) {
|
|
|
1899
2105
|
", "
|
|
1900
2106
|
)}`,
|
|
1901
2107
|
"error",
|
|
1902
|
-
|
|
2108
|
+
specsRoot,
|
|
1903
2109
|
"traceability.scMustTouchContracts",
|
|
1904
2110
|
scWithoutContracts
|
|
1905
2111
|
)
|
|
@@ -1917,7 +2123,7 @@ async function validateTraceability(root, config) {
|
|
|
1917
2123
|
"QFAI_CONTRACT_ORPHAN",
|
|
1918
2124
|
`\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
1919
2125
|
"error",
|
|
1920
|
-
|
|
2126
|
+
specsRoot,
|
|
1921
2127
|
"traceability.allowOrphanContracts",
|
|
1922
2128
|
orphanContracts
|
|
1923
2129
|
)
|
|
@@ -1954,7 +2160,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1954
2160
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
1955
2161
|
let found = false;
|
|
1956
2162
|
for (const file of targetFiles) {
|
|
1957
|
-
const text = await (0,
|
|
2163
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1958
2164
|
if (pattern.test(text)) {
|
|
1959
2165
|
found = true;
|
|
1960
2166
|
break;
|
|
@@ -2002,8 +2208,8 @@ async function validateProject(root, configResult) {
|
|
|
2002
2208
|
const issues = [
|
|
2003
2209
|
...configIssues,
|
|
2004
2210
|
...await validateSpecs(root, config),
|
|
2211
|
+
...await validateDeltas(root, config),
|
|
2005
2212
|
...await validateScenarios(root, config),
|
|
2006
|
-
...await validateDecisions(root, config),
|
|
2007
2213
|
...await validateContracts(root, config),
|
|
2008
2214
|
...await validateDefinedIds(root, config),
|
|
2009
2215
|
...await validateTraceability(root, config)
|
|
@@ -2032,21 +2238,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
2032
2238
|
const resolved = configResult ?? await loadConfig(root);
|
|
2033
2239
|
const config = resolved.config;
|
|
2034
2240
|
const configPath = resolved.configPath;
|
|
2035
|
-
const
|
|
2036
|
-
const
|
|
2037
|
-
const
|
|
2038
|
-
const
|
|
2039
|
-
const
|
|
2040
|
-
const dbRoot = resolvePath(root, config, "dataContractsDir");
|
|
2241
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2242
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2243
|
+
const apiRoot = import_node_path13.default.join(contractsRoot, "api");
|
|
2244
|
+
const uiRoot = import_node_path13.default.join(contractsRoot, "ui");
|
|
2245
|
+
const dbRoot = import_node_path13.default.join(contractsRoot, "db");
|
|
2041
2246
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
2042
2247
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
2043
|
-
const specFiles = await collectSpecFiles(
|
|
2044
|
-
const scenarioFiles = await
|
|
2045
|
-
extensions: [".feature"]
|
|
2046
|
-
});
|
|
2047
|
-
const decisionFiles = await collectFiles(decisionsRoot, {
|
|
2048
|
-
extensions: [".md"]
|
|
2049
|
-
});
|
|
2248
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
2249
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
2050
2250
|
const {
|
|
2051
2251
|
api: apiFiles,
|
|
2052
2252
|
ui: uiFiles,
|
|
@@ -2055,7 +2255,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
2055
2255
|
const idsByPrefix = await collectIds([
|
|
2056
2256
|
...specFiles,
|
|
2057
2257
|
...scenarioFiles,
|
|
2058
|
-
...decisionFiles,
|
|
2059
2258
|
...apiFiles,
|
|
2060
2259
|
...uiFiles,
|
|
2061
2260
|
...dbFiles
|
|
@@ -2080,7 +2279,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
2080
2279
|
summary: {
|
|
2081
2280
|
specs: specFiles.length,
|
|
2082
2281
|
scenarios: scenarioFiles.length,
|
|
2083
|
-
decisions: decisionFiles.length,
|
|
2084
2282
|
contracts: {
|
|
2085
2283
|
api: apiFiles.length,
|
|
2086
2284
|
ui: uiFiles.length,
|
|
@@ -2114,7 +2312,6 @@ function formatReportMarkdown(data) {
|
|
|
2114
2312
|
lines.push("## \u6982\u8981");
|
|
2115
2313
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2116
2314
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
2117
|
-
lines.push(`- decisions: ${data.summary.decisions}`);
|
|
2118
2315
|
lines.push(
|
|
2119
2316
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
2120
2317
|
);
|
|
@@ -2190,7 +2387,7 @@ async function collectIds(files) {
|
|
|
2190
2387
|
DATA: /* @__PURE__ */ new Set()
|
|
2191
2388
|
};
|
|
2192
2389
|
for (const file of files) {
|
|
2193
|
-
const text = await (0,
|
|
2390
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2194
2391
|
for (const prefix of ID_PREFIXES2) {
|
|
2195
2392
|
const ids = extractIds(text, prefix);
|
|
2196
2393
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -2208,7 +2405,7 @@ async function collectIds(files) {
|
|
|
2208
2405
|
async function collectUpstreamIds(files) {
|
|
2209
2406
|
const ids = /* @__PURE__ */ new Set();
|
|
2210
2407
|
for (const file of files) {
|
|
2211
|
-
const text = await (0,
|
|
2408
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2212
2409
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
2213
2410
|
}
|
|
2214
2411
|
return ids;
|
|
@@ -2229,7 +2426,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
2229
2426
|
}
|
|
2230
2427
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
2231
2428
|
for (const file of targetFiles) {
|
|
2232
|
-
const text = await (0,
|
|
2429
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2233
2430
|
if (pattern.test(text)) {
|
|
2234
2431
|
return true;
|
|
2235
2432
|
}
|
|
@@ -2273,21 +2470,22 @@ function buildHotspots(issues) {
|
|
|
2273
2470
|
|
|
2274
2471
|
// src/cli/commands/report.ts
|
|
2275
2472
|
async function runReport(options) {
|
|
2276
|
-
const root =
|
|
2473
|
+
const root = import_node_path14.default.resolve(options.root);
|
|
2277
2474
|
const configResult = await loadConfig(root);
|
|
2278
|
-
const input =
|
|
2279
|
-
const inputPath =
|
|
2475
|
+
const input = configResult.config.output.validateJsonPath;
|
|
2476
|
+
const inputPath = import_node_path14.default.isAbsolute(input) ? input : import_node_path14.default.resolve(root, input);
|
|
2280
2477
|
let validation;
|
|
2281
2478
|
try {
|
|
2282
2479
|
validation = await readValidationResult(inputPath);
|
|
2283
2480
|
} catch (err) {
|
|
2284
|
-
if (
|
|
2481
|
+
if (isMissingFileError5(err)) {
|
|
2285
2482
|
error(
|
|
2286
2483
|
[
|
|
2287
2484
|
`qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
|
|
2288
2485
|
"",
|
|
2289
|
-
"\u307E\u305A validate
|
|
2290
|
-
|
|
2486
|
+
"\u307E\u305A qfai validate \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
|
|
2487
|
+
" qfai validate",
|
|
2488
|
+
"\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/out/validate.json\uFF09",
|
|
2291
2489
|
"",
|
|
2292
2490
|
"GitHub Actions \u30C6\u30F3\u30D7\u30EC\u3092\u4F7F\u3063\u3066\u3044\u308B\u5834\u5408\u306F\u3001workflow \u306E validate \u30B8\u30E7\u30D6\u3092\u5148\u306B\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
2293
2491
|
].join("\n")
|
|
@@ -2299,11 +2497,12 @@ async function runReport(options) {
|
|
|
2299
2497
|
}
|
|
2300
2498
|
const data = await createReportData(root, validation, configResult);
|
|
2301
2499
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
2302
|
-
const
|
|
2500
|
+
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
2501
|
+
const defaultOut = options.format === "json" ? import_node_path14.default.join(outRoot, "report.json") : import_node_path14.default.join(outRoot, "report.md");
|
|
2303
2502
|
const out = options.outPath ?? defaultOut;
|
|
2304
|
-
const outPath =
|
|
2305
|
-
await (0,
|
|
2306
|
-
await (0,
|
|
2503
|
+
const outPath = import_node_path14.default.isAbsolute(out) ? out : import_node_path14.default.resolve(root, out);
|
|
2504
|
+
await (0, import_promises15.mkdir)(import_node_path14.default.dirname(outPath), { recursive: true });
|
|
2505
|
+
await (0, import_promises15.writeFile)(outPath, `${output}
|
|
2307
2506
|
`, "utf-8");
|
|
2308
2507
|
info(
|
|
2309
2508
|
`report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
|
|
@@ -2311,7 +2510,7 @@ async function runReport(options) {
|
|
|
2311
2510
|
info(`wrote report: ${outPath}`);
|
|
2312
2511
|
}
|
|
2313
2512
|
async function readValidationResult(inputPath) {
|
|
2314
|
-
const raw = await (0,
|
|
2513
|
+
const raw = await (0, import_promises15.readFile)(inputPath, "utf-8");
|
|
2315
2514
|
const parsed = JSON.parse(raw);
|
|
2316
2515
|
if (!isValidationResult(parsed)) {
|
|
2317
2516
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
@@ -2343,7 +2542,7 @@ function isValidationResult(value) {
|
|
|
2343
2542
|
}
|
|
2344
2543
|
return typeof counts.info === "number" && typeof counts.warning === "number" && typeof counts.error === "number";
|
|
2345
2544
|
}
|
|
2346
|
-
function
|
|
2545
|
+
function isMissingFileError5(error2) {
|
|
2347
2546
|
if (!error2 || typeof error2 !== "object") {
|
|
2348
2547
|
return false;
|
|
2349
2548
|
}
|
|
@@ -2352,8 +2551,8 @@ function isMissingFileError(error2) {
|
|
|
2352
2551
|
}
|
|
2353
2552
|
|
|
2354
2553
|
// src/cli/commands/validate.ts
|
|
2355
|
-
var
|
|
2356
|
-
var
|
|
2554
|
+
var import_promises16 = require("fs/promises");
|
|
2555
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
2357
2556
|
|
|
2358
2557
|
// src/cli/lib/failOn.ts
|
|
2359
2558
|
function shouldFail(result, failOn) {
|
|
@@ -2368,24 +2567,17 @@ function shouldFail(result, failOn) {
|
|
|
2368
2567
|
|
|
2369
2568
|
// src/cli/commands/validate.ts
|
|
2370
2569
|
async function runValidate(options) {
|
|
2371
|
-
const root =
|
|
2570
|
+
const root = import_node_path15.default.resolve(options.root);
|
|
2372
2571
|
const configResult = await loadConfig(root);
|
|
2373
2572
|
const result = await validateProject(root, configResult);
|
|
2374
|
-
const format = options.format ??
|
|
2375
|
-
const explicitJsonPath = options.jsonPath;
|
|
2573
|
+
const format = options.format ?? "text";
|
|
2376
2574
|
if (format === "text") {
|
|
2377
2575
|
emitText(result);
|
|
2378
2576
|
}
|
|
2379
2577
|
if (format === "github") {
|
|
2380
2578
|
result.issues.forEach(emitGitHub);
|
|
2381
2579
|
}
|
|
2382
|
-
|
|
2383
|
-
if (shouldWriteJson) {
|
|
2384
|
-
const jsonPath = format === "json" ? options.jsonPath ?? configResult.config.output.jsonPath : explicitJsonPath;
|
|
2385
|
-
if (jsonPath) {
|
|
2386
|
-
await emitJson(result, root, jsonPath);
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2580
|
+
await emitJson(result, root, configResult.config.output.validateJsonPath);
|
|
2389
2581
|
const failOn = resolveFailOn(options, configResult.config.validation.failOn);
|
|
2390
2582
|
return shouldFail(result, failOn) ? 1 : 0;
|
|
2391
2583
|
}
|
|
@@ -2424,9 +2616,9 @@ function emitGitHub(issue7) {
|
|
|
2424
2616
|
);
|
|
2425
2617
|
}
|
|
2426
2618
|
async function emitJson(result, root, jsonPath) {
|
|
2427
|
-
const abs =
|
|
2428
|
-
await (0,
|
|
2429
|
-
await (0,
|
|
2619
|
+
const abs = import_node_path15.default.isAbsolute(jsonPath) ? jsonPath : import_node_path15.default.resolve(root, jsonPath);
|
|
2620
|
+
await (0, import_promises16.mkdir)(import_node_path15.default.dirname(abs), { recursive: true });
|
|
2621
|
+
await (0, import_promises16.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
2430
2622
|
`, "utf-8");
|
|
2431
2623
|
}
|
|
2432
2624
|
|
|
@@ -2486,15 +2678,6 @@ function parseArgs(argv, cwd) {
|
|
|
2486
2678
|
i += 1;
|
|
2487
2679
|
break;
|
|
2488
2680
|
}
|
|
2489
|
-
case "--json-path":
|
|
2490
|
-
{
|
|
2491
|
-
const next = args[i + 1];
|
|
2492
|
-
if (next) {
|
|
2493
|
-
options.jsonPath = next;
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
2496
|
-
i += 1;
|
|
2497
|
-
break;
|
|
2498
2681
|
case "--out":
|
|
2499
2682
|
{
|
|
2500
2683
|
const next = args[i + 1];
|
|
@@ -2525,7 +2708,7 @@ function applyFormatOption(command, value, options) {
|
|
|
2525
2708
|
return;
|
|
2526
2709
|
}
|
|
2527
2710
|
if (command === "validate") {
|
|
2528
|
-
if (value === "text" || value === "
|
|
2711
|
+
if (value === "text" || value === "github") {
|
|
2529
2712
|
options.validateFormat = value;
|
|
2530
2713
|
}
|
|
2531
2714
|
return;
|
|
@@ -2533,7 +2716,7 @@ function applyFormatOption(command, value, options) {
|
|
|
2533
2716
|
if (value === "md" || value === "json") {
|
|
2534
2717
|
options.reportFormat = value;
|
|
2535
2718
|
}
|
|
2536
|
-
if (value === "text" || value === "
|
|
2719
|
+
if (value === "text" || value === "github") {
|
|
2537
2720
|
options.validateFormat = value;
|
|
2538
2721
|
}
|
|
2539
2722
|
}
|
|
@@ -2559,15 +2742,13 @@ async function run(argv, cwd) {
|
|
|
2559
2742
|
root: options.root,
|
|
2560
2743
|
strict: options.strict,
|
|
2561
2744
|
format: options.validateFormat,
|
|
2562
|
-
...options.failOn !== void 0 ? { failOn: options.failOn } : {}
|
|
2563
|
-
...options.jsonPath !== void 0 ? { jsonPath: options.jsonPath } : {}
|
|
2745
|
+
...options.failOn !== void 0 ? { failOn: options.failOn } : {}
|
|
2564
2746
|
});
|
|
2565
2747
|
return;
|
|
2566
2748
|
case "report":
|
|
2567
2749
|
await runReport({
|
|
2568
2750
|
root: options.root,
|
|
2569
2751
|
format: options.reportFormat,
|
|
2570
|
-
...options.jsonPath !== void 0 ? { jsonPath: options.jsonPath } : {},
|
|
2571
2752
|
...options.reportOut !== void 0 ? { outPath: options.reportOut } : {}
|
|
2572
2753
|
});
|
|
2573
2754
|
return;
|
|
@@ -2589,14 +2770,13 @@ Options:
|
|
|
2589
2770
|
--root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
|
|
2590
2771
|
--dir <path> init \u306E\u51FA\u529B\u5148
|
|
2591
2772
|
--force \u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u3092\u4E0A\u66F8\u304D
|
|
2592
|
-
--yes init: \
|
|
2773
|
+
--yes init: \u4E88\u7D04\u30D5\u30E9\u30B0\uFF08\u73FE\u72B6\u306F\u975E\u5BFE\u8A71\u306E\u305F\u3081\u6319\u52D5\u5DEE\u306A\u3057\u3002\u5C06\u6765\u306E\u5BFE\u8A71\u5C0E\u5165\u6642\u306B\u81EA\u52D5Yes\uFF09
|
|
2593
2774
|
--dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
|
|
2594
|
-
--format <text|
|
|
2775
|
+
--format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
|
|
2595
2776
|
--format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
|
|
2596
|
-
--strict
|
|
2777
|
+
--strict validate: warning \u4EE5\u4E0A\u3067 exit 1
|
|
2597
2778
|
--fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
|
|
2598
|
-
--
|
|
2599
|
-
--out <path> report: \u51FA\u529B\u5148
|
|
2779
|
+
--out <path> report: \u51FA\u529B\u5148
|
|
2600
2780
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|
|
2601
2781
|
`;
|
|
2602
2782
|
}
|