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.mjs
CHANGED
|
@@ -138,7 +138,7 @@ function report(copied, skipped, dryRun, label) {
|
|
|
138
138
|
|
|
139
139
|
// src/cli/commands/report.ts
|
|
140
140
|
import { mkdir as mkdir2, readFile as readFile11, writeFile } from "fs/promises";
|
|
141
|
-
import
|
|
141
|
+
import path14 from "path";
|
|
142
142
|
|
|
143
143
|
// src/core/config.ts
|
|
144
144
|
import { readFile } from "fs/promises";
|
|
@@ -146,13 +146,11 @@ import path4 from "path";
|
|
|
146
146
|
import { parse as parseYaml } from "yaml";
|
|
147
147
|
var defaultConfig = {
|
|
148
148
|
paths: {
|
|
149
|
-
specDir: ".qfai/spec",
|
|
150
|
-
decisionsDir: ".qfai/spec/decisions",
|
|
151
|
-
scenariosDir: ".qfai/spec/scenarios",
|
|
152
149
|
contractsDir: ".qfai/contracts",
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
specsDir: ".qfai/specs",
|
|
151
|
+
rulesDir: ".qfai/rules",
|
|
152
|
+
outDir: ".qfai/out",
|
|
153
|
+
promptsDir: ".qfai/prompts",
|
|
156
154
|
srcDir: "src",
|
|
157
155
|
testsDir: "tests"
|
|
158
156
|
},
|
|
@@ -177,8 +175,7 @@ var defaultConfig = {
|
|
|
177
175
|
}
|
|
178
176
|
},
|
|
179
177
|
output: {
|
|
180
|
-
|
|
181
|
-
jsonPath: ".qfai/out/validate.json"
|
|
178
|
+
validateJsonPath: ".qfai/out/validate.json"
|
|
182
179
|
}
|
|
183
180
|
};
|
|
184
181
|
function getConfigPath(root) {
|
|
@@ -227,27 +224,6 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
227
224
|
return base;
|
|
228
225
|
}
|
|
229
226
|
return {
|
|
230
|
-
specDir: readString(
|
|
231
|
-
raw.specDir,
|
|
232
|
-
base.specDir,
|
|
233
|
-
"paths.specDir",
|
|
234
|
-
configPath,
|
|
235
|
-
issues
|
|
236
|
-
),
|
|
237
|
-
decisionsDir: readString(
|
|
238
|
-
raw.decisionsDir,
|
|
239
|
-
base.decisionsDir,
|
|
240
|
-
"paths.decisionsDir",
|
|
241
|
-
configPath,
|
|
242
|
-
issues
|
|
243
|
-
),
|
|
244
|
-
scenariosDir: readString(
|
|
245
|
-
raw.scenariosDir,
|
|
246
|
-
base.scenariosDir,
|
|
247
|
-
"paths.scenariosDir",
|
|
248
|
-
configPath,
|
|
249
|
-
issues
|
|
250
|
-
),
|
|
251
227
|
contractsDir: readString(
|
|
252
228
|
raw.contractsDir,
|
|
253
229
|
base.contractsDir,
|
|
@@ -255,24 +231,31 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
255
231
|
configPath,
|
|
256
232
|
issues
|
|
257
233
|
),
|
|
258
|
-
|
|
259
|
-
raw.
|
|
260
|
-
base.
|
|
261
|
-
"paths.
|
|
234
|
+
specsDir: readString(
|
|
235
|
+
raw.specsDir,
|
|
236
|
+
base.specsDir,
|
|
237
|
+
"paths.specsDir",
|
|
262
238
|
configPath,
|
|
263
239
|
issues
|
|
264
240
|
),
|
|
265
|
-
|
|
266
|
-
raw.
|
|
267
|
-
base.
|
|
268
|
-
"paths.
|
|
241
|
+
rulesDir: readString(
|
|
242
|
+
raw.rulesDir,
|
|
243
|
+
base.rulesDir,
|
|
244
|
+
"paths.rulesDir",
|
|
269
245
|
configPath,
|
|
270
246
|
issues
|
|
271
247
|
),
|
|
272
|
-
|
|
273
|
-
raw.
|
|
274
|
-
base.
|
|
275
|
-
"paths.
|
|
248
|
+
outDir: readString(
|
|
249
|
+
raw.outDir,
|
|
250
|
+
base.outDir,
|
|
251
|
+
"paths.outDir",
|
|
252
|
+
configPath,
|
|
253
|
+
issues
|
|
254
|
+
),
|
|
255
|
+
promptsDir: readString(
|
|
256
|
+
raw.promptsDir,
|
|
257
|
+
base.promptsDir,
|
|
258
|
+
"paths.promptsDir",
|
|
276
259
|
configPath,
|
|
277
260
|
issues
|
|
278
261
|
),
|
|
@@ -395,17 +378,10 @@ function normalizeOutput(raw, configPath, issues) {
|
|
|
395
378
|
return base;
|
|
396
379
|
}
|
|
397
380
|
return {
|
|
398
|
-
|
|
399
|
-
raw.
|
|
400
|
-
base.
|
|
401
|
-
"output.
|
|
402
|
-
configPath,
|
|
403
|
-
issues
|
|
404
|
-
),
|
|
405
|
-
jsonPath: readString(
|
|
406
|
-
raw.jsonPath,
|
|
407
|
-
base.jsonPath,
|
|
408
|
-
"output.jsonPath",
|
|
381
|
+
validateJsonPath: readString(
|
|
382
|
+
raw.validateJsonPath,
|
|
383
|
+
base.validateJsonPath,
|
|
384
|
+
"output.validateJsonPath",
|
|
409
385
|
configPath,
|
|
410
386
|
issues
|
|
411
387
|
)
|
|
@@ -472,20 +448,6 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
|
472
448
|
}
|
|
473
449
|
return fallback;
|
|
474
450
|
}
|
|
475
|
-
function readOutputFormat(value, fallback, label, configPath, issues) {
|
|
476
|
-
if (value === "text" || value === "json" || value === "github") {
|
|
477
|
-
return value;
|
|
478
|
-
}
|
|
479
|
-
if (value !== void 0) {
|
|
480
|
-
issues.push(
|
|
481
|
-
configIssue(
|
|
482
|
-
configPath,
|
|
483
|
-
`${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
484
|
-
)
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
return fallback;
|
|
488
|
-
}
|
|
489
451
|
function configIssue(file, message) {
|
|
490
452
|
return {
|
|
491
453
|
code: "QFAI_CONFIG_INVALID",
|
|
@@ -513,9 +475,10 @@ function isRecord(value) {
|
|
|
513
475
|
|
|
514
476
|
// src/core/report.ts
|
|
515
477
|
import { readFile as readFile10 } from "fs/promises";
|
|
478
|
+
import path13 from "path";
|
|
516
479
|
|
|
517
480
|
// src/core/discovery.ts
|
|
518
|
-
import
|
|
481
|
+
import { access as access3 } from "fs/promises";
|
|
519
482
|
|
|
520
483
|
// src/core/fs.ts
|
|
521
484
|
import { access as access2, readdir as readdir2 } from "fs/promises";
|
|
@@ -572,11 +535,50 @@ async function exists2(target) {
|
|
|
572
535
|
}
|
|
573
536
|
}
|
|
574
537
|
|
|
538
|
+
// src/core/specLayout.ts
|
|
539
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
540
|
+
import path6 from "path";
|
|
541
|
+
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
542
|
+
async function collectSpecEntries(specsRoot) {
|
|
543
|
+
const dirs = await listSpecDirs(specsRoot);
|
|
544
|
+
const entries = dirs.map((dir) => ({
|
|
545
|
+
dir,
|
|
546
|
+
specPath: path6.join(dir, "spec.md"),
|
|
547
|
+
deltaPath: path6.join(dir, "delta.md"),
|
|
548
|
+
scenarioPath: path6.join(dir, "scenario.md")
|
|
549
|
+
}));
|
|
550
|
+
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
551
|
+
}
|
|
552
|
+
async function listSpecDirs(specsRoot) {
|
|
553
|
+
try {
|
|
554
|
+
const items = await readdir3(specsRoot, { withFileTypes: true });
|
|
555
|
+
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => path6.join(specsRoot, name));
|
|
556
|
+
} catch (error2) {
|
|
557
|
+
if (isMissingFileError(error2)) {
|
|
558
|
+
return [];
|
|
559
|
+
}
|
|
560
|
+
throw error2;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function isMissingFileError(error2) {
|
|
564
|
+
if (!error2 || typeof error2 !== "object") {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
return error2.code === "ENOENT";
|
|
568
|
+
}
|
|
569
|
+
|
|
575
570
|
// src/core/discovery.ts
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
571
|
+
async function collectSpecPackDirs(specsRoot) {
|
|
572
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
573
|
+
return entries.map((entry) => entry.dir);
|
|
574
|
+
}
|
|
575
|
+
async function collectSpecFiles(specsRoot) {
|
|
576
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
577
|
+
return filterExisting(entries.map((entry) => entry.specPath));
|
|
578
|
+
}
|
|
579
|
+
async function collectScenarioFiles(specsRoot) {
|
|
580
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
581
|
+
return filterExisting(entries.map((entry) => entry.scenarioPath));
|
|
580
582
|
}
|
|
581
583
|
async function collectUiContractFiles(uiRoot) {
|
|
582
584
|
return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
@@ -595,9 +597,22 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
|
595
597
|
]);
|
|
596
598
|
return { ui, api, db };
|
|
597
599
|
}
|
|
598
|
-
function
|
|
599
|
-
const
|
|
600
|
-
|
|
600
|
+
async function filterExisting(files) {
|
|
601
|
+
const existing = [];
|
|
602
|
+
for (const file of files) {
|
|
603
|
+
if (await exists3(file)) {
|
|
604
|
+
existing.push(file);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return existing;
|
|
608
|
+
}
|
|
609
|
+
async function exists3(target) {
|
|
610
|
+
try {
|
|
611
|
+
await access3(target);
|
|
612
|
+
return true;
|
|
613
|
+
} catch {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
601
616
|
}
|
|
602
617
|
|
|
603
618
|
// src/core/ids.ts
|
|
@@ -661,8 +676,8 @@ import { readFile as readFile2 } from "fs/promises";
|
|
|
661
676
|
import path7 from "path";
|
|
662
677
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
663
678
|
async function resolveToolVersion() {
|
|
664
|
-
if ("0.3.
|
|
665
|
-
return "0.3.
|
|
679
|
+
if ("0.3.2".length > 0) {
|
|
680
|
+
return "0.3.2";
|
|
666
681
|
}
|
|
667
682
|
try {
|
|
668
683
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -682,6 +697,7 @@ function resolvePackageJsonPath() {
|
|
|
682
697
|
|
|
683
698
|
// src/core/validators/contracts.ts
|
|
684
699
|
import { readFile as readFile3 } from "fs/promises";
|
|
700
|
+
import path9 from "path";
|
|
685
701
|
|
|
686
702
|
// src/core/contracts.ts
|
|
687
703
|
import path8 from "path";
|
|
@@ -737,19 +753,10 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
737
753
|
];
|
|
738
754
|
async function validateContracts(root, config) {
|
|
739
755
|
const issues = [];
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
);
|
|
743
|
-
issues.push(
|
|
744
|
-
...await validateApiContracts(
|
|
745
|
-
resolvePath(root, config, "apiContractsDir")
|
|
746
|
-
)
|
|
747
|
-
);
|
|
748
|
-
issues.push(
|
|
749
|
-
...await validateDataContracts(
|
|
750
|
-
resolvePath(root, config, "dataContractsDir")
|
|
751
|
-
)
|
|
752
|
-
);
|
|
756
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
757
|
+
issues.push(...await validateUiContracts(path9.join(contractsRoot, "ui")));
|
|
758
|
+
issues.push(...await validateApiContracts(path9.join(contractsRoot, "api")));
|
|
759
|
+
issues.push(...await validateDataContracts(path9.join(contractsRoot, "db")));
|
|
753
760
|
return issues;
|
|
754
761
|
}
|
|
755
762
|
async function validateUiContracts(uiRoot) {
|
|
@@ -982,72 +989,78 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
982
989
|
return issue7;
|
|
983
990
|
}
|
|
984
991
|
|
|
985
|
-
// src/core/validators/
|
|
992
|
+
// src/core/validators/delta.ts
|
|
986
993
|
import { readFile as readFile4 } from "fs/promises";
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
var
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
const status = extractField(md, "Status");
|
|
998
|
-
const context = extractField(md, "Context");
|
|
999
|
-
const decision = extractField(md, "Decision");
|
|
1000
|
-
const consequences = extractField(md, "Consequences");
|
|
1001
|
-
const related = extractField(md, "Related");
|
|
1002
|
-
if (status) fields.status = status;
|
|
1003
|
-
if (context) fields.context = context;
|
|
1004
|
-
if (decision) fields.decision = decision;
|
|
1005
|
-
if (consequences) fields.consequences = consequences;
|
|
1006
|
-
if (related) fields.related = related;
|
|
1007
|
-
const parsed = {
|
|
1008
|
-
file,
|
|
1009
|
-
fields
|
|
1010
|
-
};
|
|
1011
|
-
if (adrId) {
|
|
1012
|
-
parsed.adrId = adrId;
|
|
1013
|
-
}
|
|
1014
|
-
return parsed;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
// src/core/validators/decisions.ts
|
|
1018
|
-
var REQUIRED_FIELDS = [
|
|
1019
|
-
{ key: "status", label: "Status" },
|
|
1020
|
-
{ key: "context", label: "Context" },
|
|
1021
|
-
{ key: "decision", label: "Decision" },
|
|
1022
|
-
{ key: "consequences", label: "Consequences" }
|
|
1023
|
-
];
|
|
1024
|
-
async function validateDecisions(root, config) {
|
|
1025
|
-
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1026
|
-
const files = await collectFiles(decisionsRoot, { extensions: [".md"] });
|
|
1027
|
-
if (files.length === 0) {
|
|
994
|
+
import path10 from "path";
|
|
995
|
+
var SECTION_RE = /^##\s+変更区分/m;
|
|
996
|
+
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
997
|
+
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
998
|
+
var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
|
|
999
|
+
var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
|
|
1000
|
+
async function validateDeltas(root, config) {
|
|
1001
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1002
|
+
const packs = await collectSpecPackDirs(specsRoot);
|
|
1003
|
+
if (packs.length === 0) {
|
|
1028
1004
|
return [];
|
|
1029
1005
|
}
|
|
1030
1006
|
const issues = [];
|
|
1031
|
-
for (const
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
(
|
|
1036
|
-
)
|
|
1037
|
-
|
|
1007
|
+
for (const pack of packs) {
|
|
1008
|
+
const deltaPath = path10.join(pack, "delta.md");
|
|
1009
|
+
let text;
|
|
1010
|
+
try {
|
|
1011
|
+
text = await readFile4(deltaPath, "utf-8");
|
|
1012
|
+
} catch (error2) {
|
|
1013
|
+
if (isMissingFileError2(error2)) {
|
|
1014
|
+
issues.push(
|
|
1015
|
+
issue2(
|
|
1016
|
+
"QFAI-DELTA-001",
|
|
1017
|
+
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1018
|
+
"error",
|
|
1019
|
+
deltaPath,
|
|
1020
|
+
"delta.exists"
|
|
1021
|
+
)
|
|
1022
|
+
);
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
throw error2;
|
|
1026
|
+
}
|
|
1027
|
+
const hasSection = SECTION_RE.test(text);
|
|
1028
|
+
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
1029
|
+
const hasChange = CHANGE_LINE_RE.test(text);
|
|
1030
|
+
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
1038
1031
|
issues.push(
|
|
1039
1032
|
issue2(
|
|
1040
|
-
"QFAI-
|
|
1041
|
-
|
|
1033
|
+
"QFAI-DELTA-002",
|
|
1034
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
1042
1035
|
"error",
|
|
1043
|
-
|
|
1044
|
-
"
|
|
1036
|
+
deltaPath,
|
|
1037
|
+
"delta.section"
|
|
1038
|
+
)
|
|
1039
|
+
);
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
1043
|
+
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
1044
|
+
if (compatibilityChecked === changeChecked) {
|
|
1045
|
+
issues.push(
|
|
1046
|
+
issue2(
|
|
1047
|
+
"QFAI-DELTA-003",
|
|
1048
|
+
"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",
|
|
1049
|
+
"error",
|
|
1050
|
+
deltaPath,
|
|
1051
|
+
"delta.classification"
|
|
1045
1052
|
)
|
|
1046
1053
|
);
|
|
1047
1054
|
}
|
|
1048
1055
|
}
|
|
1049
1056
|
return issues;
|
|
1050
1057
|
}
|
|
1058
|
+
function isMissingFileError2(error2) {
|
|
1059
|
+
if (!error2 || typeof error2 !== "object") {
|
|
1060
|
+
return false;
|
|
1061
|
+
}
|
|
1062
|
+
return error2.code === "ENOENT";
|
|
1063
|
+
}
|
|
1051
1064
|
function issue2(code, message, severity, file, rule, refs) {
|
|
1052
1065
|
const issue7 = {
|
|
1053
1066
|
code,
|
|
@@ -1068,14 +1081,16 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1068
1081
|
|
|
1069
1082
|
// src/core/validators/ids.ts
|
|
1070
1083
|
import { readFile as readFile6 } from "fs/promises";
|
|
1071
|
-
import
|
|
1084
|
+
import path12 from "path";
|
|
1072
1085
|
|
|
1073
1086
|
// src/core/contractIndex.ts
|
|
1074
1087
|
import { readFile as readFile5 } from "fs/promises";
|
|
1088
|
+
import path11 from "path";
|
|
1075
1089
|
async function buildContractIndex(root, config) {
|
|
1076
|
-
const
|
|
1077
|
-
const
|
|
1078
|
-
const
|
|
1090
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1091
|
+
const uiRoot = path11.join(contractsRoot, "ui");
|
|
1092
|
+
const apiRoot = path11.join(contractsRoot, "api");
|
|
1093
|
+
const dataRoot = path11.join(contractsRoot, "db");
|
|
1079
1094
|
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
1080
1095
|
collectUiContractFiles(uiRoot),
|
|
1081
1096
|
collectApiContractFiles(apiRoot),
|
|
@@ -1129,38 +1144,6 @@ function record(index, id, file) {
|
|
|
1129
1144
|
index.idToFiles.set(id, current);
|
|
1130
1145
|
}
|
|
1131
1146
|
|
|
1132
|
-
// src/core/parse/gherkin.ts
|
|
1133
|
-
var FEATURE_RE = /^\s*Feature:\s+/;
|
|
1134
|
-
var SCENARIO_RE = /^\s*Scenario:\s*(.+)\s*$/;
|
|
1135
|
-
var TAG_LINE_RE = /^\s*@/;
|
|
1136
|
-
function parseTags(line) {
|
|
1137
|
-
return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
|
|
1138
|
-
}
|
|
1139
|
-
function parseGherkinFeature(text, file) {
|
|
1140
|
-
const lines = text.split(/\r?\n/);
|
|
1141
|
-
const scenarios = [];
|
|
1142
|
-
let featurePresent = false;
|
|
1143
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1144
|
-
const line = lines[i] ?? "";
|
|
1145
|
-
if (FEATURE_RE.test(line)) {
|
|
1146
|
-
featurePresent = true;
|
|
1147
|
-
}
|
|
1148
|
-
const match = line.match(SCENARIO_RE);
|
|
1149
|
-
if (!match) continue;
|
|
1150
|
-
const scenarioName = match[1];
|
|
1151
|
-
if (!scenarioName) continue;
|
|
1152
|
-
const tags = [];
|
|
1153
|
-
for (let j = i - 1; j >= 0; j--) {
|
|
1154
|
-
const previous = lines[j] ?? "";
|
|
1155
|
-
if (previous.trim() === "") continue;
|
|
1156
|
-
if (!TAG_LINE_RE.test(previous)) break;
|
|
1157
|
-
tags.unshift(...parseTags(previous));
|
|
1158
|
-
}
|
|
1159
|
-
scenarios.push({ name: scenarioName, line: i + 1, tags });
|
|
1160
|
-
}
|
|
1161
|
-
return { file, featurePresent, scenarios };
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
1147
|
// src/core/parse/markdown.ts
|
|
1165
1148
|
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1166
1149
|
function parseHeadings(md) {
|
|
@@ -1204,9 +1187,9 @@ function extractH2Sections(md) {
|
|
|
1204
1187
|
|
|
1205
1188
|
// src/core/parse/spec.ts
|
|
1206
1189
|
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1207
|
-
var BR_LINE_RE = /^\s
|
|
1208
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s
|
|
1209
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s
|
|
1190
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1191
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1192
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1210
1193
|
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1211
1194
|
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1212
1195
|
function parseSpec(md, file) {
|
|
@@ -1279,16 +1262,171 @@ function parseSpec(md, file) {
|
|
|
1279
1262
|
return parsed;
|
|
1280
1263
|
}
|
|
1281
1264
|
|
|
1282
|
-
// src/core/
|
|
1265
|
+
// src/core/gherkin/parse.ts
|
|
1266
|
+
import {
|
|
1267
|
+
AstBuilder,
|
|
1268
|
+
GherkinClassicTokenMatcher,
|
|
1269
|
+
Parser
|
|
1270
|
+
} from "@cucumber/gherkin";
|
|
1271
|
+
import { randomUUID } from "crypto";
|
|
1272
|
+
function parseGherkin(source, uri) {
|
|
1273
|
+
const errors = [];
|
|
1274
|
+
const uuidFn = () => randomUUID();
|
|
1275
|
+
const builder = new AstBuilder(uuidFn);
|
|
1276
|
+
const matcher = new GherkinClassicTokenMatcher();
|
|
1277
|
+
const parser = new Parser(builder, matcher);
|
|
1278
|
+
try {
|
|
1279
|
+
const gherkinDocument = parser.parse(source);
|
|
1280
|
+
gherkinDocument.uri = uri;
|
|
1281
|
+
return { gherkinDocument, errors };
|
|
1282
|
+
} catch (error2) {
|
|
1283
|
+
errors.push(formatError3(error2));
|
|
1284
|
+
return { gherkinDocument: null, errors };
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
function formatError3(error2) {
|
|
1288
|
+
if (error2 instanceof Error) {
|
|
1289
|
+
return error2.message;
|
|
1290
|
+
}
|
|
1291
|
+
return String(error2);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// src/core/scenarioModel.ts
|
|
1295
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1283
1296
|
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1297
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1298
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1299
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
1300
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1301
|
+
function parseScenarioDocument(text, uri) {
|
|
1302
|
+
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1303
|
+
if (!gherkinDocument) {
|
|
1304
|
+
return { document: null, errors };
|
|
1305
|
+
}
|
|
1306
|
+
const feature = gherkinDocument.feature;
|
|
1307
|
+
if (!feature) {
|
|
1308
|
+
return {
|
|
1309
|
+
document: { uri, featureTags: [], scenarios: [] },
|
|
1310
|
+
errors
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
const featureTags = collectTagNames(feature.tags);
|
|
1314
|
+
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
1315
|
+
return {
|
|
1316
|
+
document: {
|
|
1317
|
+
uri,
|
|
1318
|
+
featureName: feature.name,
|
|
1319
|
+
featureTags,
|
|
1320
|
+
scenarios
|
|
1321
|
+
},
|
|
1322
|
+
errors
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
function buildScenarioAtoms(document) {
|
|
1326
|
+
return document.scenarios.map((scenario) => {
|
|
1327
|
+
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1328
|
+
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1329
|
+
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1330
|
+
const contractIds = /* @__PURE__ */ new Set();
|
|
1331
|
+
scenario.tags.forEach((tag) => {
|
|
1332
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1333
|
+
contractIds.add(tag);
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
for (const step of scenario.steps) {
|
|
1337
|
+
for (const text of collectStepTexts(step)) {
|
|
1338
|
+
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1339
|
+
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1340
|
+
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const atom = {
|
|
1344
|
+
uri: document.uri,
|
|
1345
|
+
featureName: document.featureName ?? "",
|
|
1346
|
+
scenarioName: scenario.name,
|
|
1347
|
+
kind: scenario.kind,
|
|
1348
|
+
brIds,
|
|
1349
|
+
contractIds: Array.from(contractIds).sort()
|
|
1350
|
+
};
|
|
1351
|
+
if (scenario.line !== void 0) {
|
|
1352
|
+
atom.line = scenario.line;
|
|
1353
|
+
}
|
|
1354
|
+
if (specIds.length === 1) {
|
|
1355
|
+
const specId = specIds[0];
|
|
1356
|
+
if (specId) {
|
|
1357
|
+
atom.specId = specId;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
if (scIds.length === 1) {
|
|
1361
|
+
const scId = scIds[0];
|
|
1362
|
+
if (scId) {
|
|
1363
|
+
atom.scId = scId;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return atom;
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
function collectScenarioNodes(feature, featureTags) {
|
|
1370
|
+
const scenarios = [];
|
|
1371
|
+
for (const child of feature.children) {
|
|
1372
|
+
if (child.scenario) {
|
|
1373
|
+
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
1374
|
+
}
|
|
1375
|
+
if (child.rule) {
|
|
1376
|
+
const ruleTags = collectTagNames(child.rule.tags);
|
|
1377
|
+
for (const ruleChild of child.rule.children) {
|
|
1378
|
+
if (ruleChild.scenario) {
|
|
1379
|
+
scenarios.push(
|
|
1380
|
+
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return scenarios;
|
|
1387
|
+
}
|
|
1388
|
+
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
1389
|
+
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
1390
|
+
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
1391
|
+
return {
|
|
1392
|
+
name: scenario.name,
|
|
1393
|
+
kind,
|
|
1394
|
+
line: scenario.location?.line,
|
|
1395
|
+
tags,
|
|
1396
|
+
steps: scenario.steps
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
function collectTagNames(tags) {
|
|
1400
|
+
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1401
|
+
}
|
|
1402
|
+
function collectStepTexts(step) {
|
|
1403
|
+
const texts = [];
|
|
1404
|
+
if (step.text) {
|
|
1405
|
+
texts.push(step.text);
|
|
1406
|
+
}
|
|
1407
|
+
if (step.docString?.content) {
|
|
1408
|
+
texts.push(step.docString.content);
|
|
1409
|
+
}
|
|
1410
|
+
if (step.dataTable?.rows) {
|
|
1411
|
+
for (const row of step.dataTable.rows) {
|
|
1412
|
+
for (const cell of row.cells) {
|
|
1413
|
+
texts.push(cell.value);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return texts;
|
|
1418
|
+
}
|
|
1419
|
+
function unique2(values) {
|
|
1420
|
+
return Array.from(new Set(values));
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// src/core/validators/ids.ts
|
|
1424
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1284
1425
|
async function validateDefinedIds(root, config) {
|
|
1285
1426
|
const issues = [];
|
|
1286
|
-
const
|
|
1287
|
-
const
|
|
1288
|
-
const
|
|
1289
|
-
const scenarioFiles = await collectFiles(scenarioRoot, {
|
|
1290
|
-
extensions: [".feature"]
|
|
1291
|
-
});
|
|
1427
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1428
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
1429
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1292
1430
|
const defined = /* @__PURE__ */ new Map();
|
|
1293
1431
|
await collectSpecDefinitionIds(specFiles, defined);
|
|
1294
1432
|
await collectScenarioDefinitionIds(scenarioFiles, defined);
|
|
@@ -1328,10 +1466,13 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1328
1466
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1329
1467
|
for (const file of files) {
|
|
1330
1468
|
const text = await readFile6(file, "utf-8");
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1469
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1470
|
+
if (!document || errors.length > 0) {
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
for (const scenario of document.scenarios) {
|
|
1333
1474
|
for (const tag of scenario.tags) {
|
|
1334
|
-
if (
|
|
1475
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
1335
1476
|
recordId(out, tag, file);
|
|
1336
1477
|
}
|
|
1337
1478
|
}
|
|
@@ -1345,7 +1486,7 @@ function recordId(out, id, file) {
|
|
|
1345
1486
|
}
|
|
1346
1487
|
function formatFileList(files, root) {
|
|
1347
1488
|
return files.map((file) => {
|
|
1348
|
-
const relative =
|
|
1489
|
+
const relative = path12.relative(root, file);
|
|
1349
1490
|
return relative.length > 0 ? relative : file;
|
|
1350
1491
|
}).join(", ");
|
|
1351
1492
|
}
|
|
@@ -1372,35 +1513,51 @@ import { readFile as readFile7 } from "fs/promises";
|
|
|
1372
1513
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1373
1514
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1374
1515
|
var THEN_PATTERN = /\bThen\b/;
|
|
1375
|
-
var
|
|
1376
|
-
var
|
|
1377
|
-
var
|
|
1516
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1517
|
+
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1518
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1378
1519
|
async function validateScenarios(root, config) {
|
|
1379
|
-
const
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1520
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1521
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1522
|
+
if (entries.length === 0) {
|
|
1523
|
+
const expected = "spec-0001/scenario.md";
|
|
1524
|
+
const legacy = "spec-001/scenario.md";
|
|
1384
1525
|
return [
|
|
1385
1526
|
issue4(
|
|
1386
1527
|
"QFAI-SC-000",
|
|
1387
|
-
|
|
1528
|
+
`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)`,
|
|
1388
1529
|
"info",
|
|
1389
|
-
|
|
1530
|
+
specsRoot,
|
|
1390
1531
|
"scenario.files"
|
|
1391
1532
|
)
|
|
1392
1533
|
];
|
|
1393
1534
|
}
|
|
1394
1535
|
const issues = [];
|
|
1395
|
-
for (const
|
|
1396
|
-
|
|
1397
|
-
|
|
1536
|
+
for (const entry of entries) {
|
|
1537
|
+
let text;
|
|
1538
|
+
try {
|
|
1539
|
+
text = await readFile7(entry.scenarioPath, "utf-8");
|
|
1540
|
+
} catch (error2) {
|
|
1541
|
+
if (isMissingFileError3(error2)) {
|
|
1542
|
+
issues.push(
|
|
1543
|
+
issue4(
|
|
1544
|
+
"QFAI-SC-001",
|
|
1545
|
+
"scenario.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1546
|
+
"error",
|
|
1547
|
+
entry.scenarioPath,
|
|
1548
|
+
"scenario.exists"
|
|
1549
|
+
)
|
|
1550
|
+
);
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1553
|
+
throw error2;
|
|
1554
|
+
}
|
|
1555
|
+
issues.push(...validateScenarioContent(text, entry.scenarioPath));
|
|
1398
1556
|
}
|
|
1399
1557
|
return issues;
|
|
1400
1558
|
}
|
|
1401
1559
|
function validateScenarioContent(text, file) {
|
|
1402
1560
|
const issues = [];
|
|
1403
|
-
const parsed = parseGherkinFeature(text, file);
|
|
1404
1561
|
const invalidIds = extractInvalidIds(text, [
|
|
1405
1562
|
"SPEC",
|
|
1406
1563
|
"BR",
|
|
@@ -1422,9 +1579,47 @@ function validateScenarioContent(text, file) {
|
|
|
1422
1579
|
)
|
|
1423
1580
|
);
|
|
1424
1581
|
}
|
|
1582
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1583
|
+
if (!document || errors.length > 0) {
|
|
1584
|
+
issues.push(
|
|
1585
|
+
issue4(
|
|
1586
|
+
"QFAI-SC-010",
|
|
1587
|
+
`Gherkin \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errors.join(", ") || "unknown"}`,
|
|
1588
|
+
"error",
|
|
1589
|
+
file,
|
|
1590
|
+
"scenario.parse"
|
|
1591
|
+
)
|
|
1592
|
+
);
|
|
1593
|
+
return issues;
|
|
1594
|
+
}
|
|
1595
|
+
const featureSpecTags = document.featureTags.filter(
|
|
1596
|
+
(tag) => SPEC_TAG_RE2.test(tag)
|
|
1597
|
+
);
|
|
1598
|
+
if (featureSpecTags.length === 0) {
|
|
1599
|
+
issues.push(
|
|
1600
|
+
issue4(
|
|
1601
|
+
"QFAI-SC-009",
|
|
1602
|
+
"Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1603
|
+
"error",
|
|
1604
|
+
file,
|
|
1605
|
+
"scenario.featureSpec"
|
|
1606
|
+
)
|
|
1607
|
+
);
|
|
1608
|
+
} else if (featureSpecTags.length > 1) {
|
|
1609
|
+
issues.push(
|
|
1610
|
+
issue4(
|
|
1611
|
+
"QFAI-SC-009",
|
|
1612
|
+
`Feature \u306E SPEC \u30BF\u30B0\u304C\u8907\u6570\u3042\u308A\u307E\u3059: ${featureSpecTags.join(", ")}`,
|
|
1613
|
+
"error",
|
|
1614
|
+
file,
|
|
1615
|
+
"scenario.featureSpec",
|
|
1616
|
+
featureSpecTags
|
|
1617
|
+
)
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1425
1620
|
const missingStructure = [];
|
|
1426
|
-
if (!
|
|
1427
|
-
if (
|
|
1621
|
+
if (!document.featureName) missingStructure.push("Feature");
|
|
1622
|
+
if (document.scenarios.length === 0) missingStructure.push("Scenario");
|
|
1428
1623
|
if (missingStructure.length > 0) {
|
|
1429
1624
|
issues.push(
|
|
1430
1625
|
issue4(
|
|
@@ -1438,7 +1633,7 @@ function validateScenarioContent(text, file) {
|
|
|
1438
1633
|
)
|
|
1439
1634
|
);
|
|
1440
1635
|
}
|
|
1441
|
-
for (const scenario of
|
|
1636
|
+
for (const scenario of document.scenarios) {
|
|
1442
1637
|
if (scenario.tags.length === 0) {
|
|
1443
1638
|
issues.push(
|
|
1444
1639
|
issue4(
|
|
@@ -1452,13 +1647,16 @@ function validateScenarioContent(text, file) {
|
|
|
1452
1647
|
continue;
|
|
1453
1648
|
}
|
|
1454
1649
|
const missingTags = [];
|
|
1455
|
-
|
|
1456
|
-
|
|
1650
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
|
|
1651
|
+
if (scTags.length === 0) {
|
|
1652
|
+
missingTags.push("SC(0\u4EF6)");
|
|
1653
|
+
} else if (scTags.length > 1) {
|
|
1654
|
+
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1457
1655
|
}
|
|
1458
|
-
if (!scenario.tags.some((tag) =>
|
|
1656
|
+
if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
|
|
1459
1657
|
missingTags.push("SPEC");
|
|
1460
1658
|
}
|
|
1461
|
-
if (!scenario.tags.some((tag) =>
|
|
1659
|
+
if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
|
|
1462
1660
|
missingTags.push("BR");
|
|
1463
1661
|
}
|
|
1464
1662
|
if (missingTags.length > 0) {
|
|
@@ -1473,26 +1671,29 @@ function validateScenarioContent(text, file) {
|
|
|
1473
1671
|
);
|
|
1474
1672
|
}
|
|
1475
1673
|
}
|
|
1476
|
-
const
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1674
|
+
for (const scenario of document.scenarios) {
|
|
1675
|
+
const missingSteps = [];
|
|
1676
|
+
const keywords = scenario.steps.map((step) => step.keyword.trim());
|
|
1677
|
+
if (!keywords.some((keyword) => GIVEN_PATTERN.test(keyword))) {
|
|
1678
|
+
missingSteps.push("Given");
|
|
1679
|
+
}
|
|
1680
|
+
if (!keywords.some((keyword) => WHEN_PATTERN.test(keyword))) {
|
|
1681
|
+
missingSteps.push("When");
|
|
1682
|
+
}
|
|
1683
|
+
if (!keywords.some((keyword) => THEN_PATTERN.test(keyword))) {
|
|
1684
|
+
missingSteps.push("Then");
|
|
1685
|
+
}
|
|
1686
|
+
if (missingSteps.length > 0) {
|
|
1687
|
+
issues.push(
|
|
1688
|
+
issue4(
|
|
1689
|
+
"QFAI-SC-005",
|
|
1690
|
+
`Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")} (${scenario.name})`,
|
|
1691
|
+
"warning",
|
|
1692
|
+
file,
|
|
1693
|
+
"scenario.steps"
|
|
1694
|
+
)
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1496
1697
|
}
|
|
1497
1698
|
return issues;
|
|
1498
1699
|
}
|
|
@@ -1513,18 +1714,25 @@ function issue4(code, message, severity, file, rule, refs) {
|
|
|
1513
1714
|
}
|
|
1514
1715
|
return issue7;
|
|
1515
1716
|
}
|
|
1717
|
+
function isMissingFileError3(error2) {
|
|
1718
|
+
if (!error2 || typeof error2 !== "object") {
|
|
1719
|
+
return false;
|
|
1720
|
+
}
|
|
1721
|
+
return error2.code === "ENOENT";
|
|
1722
|
+
}
|
|
1516
1723
|
|
|
1517
1724
|
// src/core/validators/spec.ts
|
|
1518
1725
|
import { readFile as readFile8 } from "fs/promises";
|
|
1519
1726
|
async function validateSpecs(root, config) {
|
|
1520
|
-
const specsRoot = resolvePath(root, config, "
|
|
1521
|
-
const
|
|
1522
|
-
if (
|
|
1523
|
-
const expected = "spec-0001
|
|
1727
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1728
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1729
|
+
if (entries.length === 0) {
|
|
1730
|
+
const expected = "spec-0001/spec.md";
|
|
1731
|
+
const legacy = "spec-001/spec.md";
|
|
1524
1732
|
return [
|
|
1525
1733
|
issue5(
|
|
1526
1734
|
"QFAI-SPEC-000",
|
|
1527
|
-
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.
|
|
1735
|
+
`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)`,
|
|
1528
1736
|
"info",
|
|
1529
1737
|
specsRoot,
|
|
1530
1738
|
"spec.files"
|
|
@@ -1532,12 +1740,29 @@ async function validateSpecs(root, config) {
|
|
|
1532
1740
|
];
|
|
1533
1741
|
}
|
|
1534
1742
|
const issues = [];
|
|
1535
|
-
for (const
|
|
1536
|
-
|
|
1743
|
+
for (const entry of entries) {
|
|
1744
|
+
let text;
|
|
1745
|
+
try {
|
|
1746
|
+
text = await readFile8(entry.specPath, "utf-8");
|
|
1747
|
+
} catch (error2) {
|
|
1748
|
+
if (isMissingFileError4(error2)) {
|
|
1749
|
+
issues.push(
|
|
1750
|
+
issue5(
|
|
1751
|
+
"QFAI-SPEC-005",
|
|
1752
|
+
"spec.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1753
|
+
"error",
|
|
1754
|
+
entry.specPath,
|
|
1755
|
+
"spec.exists"
|
|
1756
|
+
)
|
|
1757
|
+
);
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
throw error2;
|
|
1761
|
+
}
|
|
1537
1762
|
issues.push(
|
|
1538
1763
|
...validateSpecContent(
|
|
1539
1764
|
text,
|
|
1540
|
-
|
|
1765
|
+
entry.specPath,
|
|
1541
1766
|
config.validation.require.specSections
|
|
1542
1767
|
)
|
|
1543
1768
|
);
|
|
@@ -1659,29 +1884,25 @@ function issue5(code, message, severity, file, rule, refs) {
|
|
|
1659
1884
|
}
|
|
1660
1885
|
return issue7;
|
|
1661
1886
|
}
|
|
1887
|
+
function isMissingFileError4(error2) {
|
|
1888
|
+
if (!error2 || typeof error2 !== "object") {
|
|
1889
|
+
return false;
|
|
1890
|
+
}
|
|
1891
|
+
return error2.code === "ENOENT";
|
|
1892
|
+
}
|
|
1662
1893
|
|
|
1663
1894
|
// src/core/validators/traceability.ts
|
|
1664
1895
|
import { readFile as readFile9 } from "fs/promises";
|
|
1665
|
-
var
|
|
1666
|
-
var
|
|
1667
|
-
var
|
|
1668
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1669
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
1670
|
-
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1896
|
+
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1897
|
+
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1898
|
+
var BR_TAG_RE3 = /^BR-\d{4}$/;
|
|
1671
1899
|
async function validateTraceability(root, config) {
|
|
1672
1900
|
const issues = [];
|
|
1673
|
-
const specsRoot = resolvePath(root, config, "
|
|
1674
|
-
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1675
|
-
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1901
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1676
1902
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1677
1903
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1678
1904
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
1679
|
-
const
|
|
1680
|
-
extensions: [".md"]
|
|
1681
|
-
});
|
|
1682
|
-
const scenarioFiles = await collectFiles(scenariosRoot, {
|
|
1683
|
-
extensions: [".feature"]
|
|
1684
|
-
});
|
|
1905
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1685
1906
|
const upstreamIds = /* @__PURE__ */ new Set();
|
|
1686
1907
|
const specIds = /* @__PURE__ */ new Set();
|
|
1687
1908
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
@@ -1729,111 +1950,100 @@ async function validateTraceability(root, config) {
|
|
|
1729
1950
|
specToBrIds.set(parsed.specId, current);
|
|
1730
1951
|
}
|
|
1731
1952
|
}
|
|
1732
|
-
for (const file of decisionFiles) {
|
|
1733
|
-
const text = await readFile9(file, "utf-8");
|
|
1734
|
-
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1735
|
-
}
|
|
1736
1953
|
for (const file of scenarioFiles) {
|
|
1737
1954
|
const text = await readFile9(file, "utf-8");
|
|
1738
1955
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1739
|
-
const
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
const scIds = /* @__PURE__ */ new Set();
|
|
1743
|
-
const scenarioIds = /* @__PURE__ */ new Set();
|
|
1744
|
-
for (const scenario of parsed.scenarios) {
|
|
1745
|
-
for (const tag of scenario.tags) {
|
|
1746
|
-
if (SPEC_TAG_RE2.test(tag)) {
|
|
1747
|
-
specIdsInScenario.add(tag);
|
|
1748
|
-
}
|
|
1749
|
-
if (BR_TAG_RE2.test(tag)) {
|
|
1750
|
-
brIds.add(tag);
|
|
1751
|
-
}
|
|
1752
|
-
if (SC_TAG_RE3.test(tag)) {
|
|
1753
|
-
scIds.add(tag);
|
|
1754
|
-
}
|
|
1755
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1756
|
-
scenarioIds.add(tag);
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
const specIdsList = Array.from(specIdsInScenario);
|
|
1761
|
-
const brIdsList = Array.from(brIds);
|
|
1762
|
-
const scIdsList = Array.from(scIds);
|
|
1763
|
-
const scenarioIdsList = Array.from(scenarioIds);
|
|
1764
|
-
brIdsList.forEach((id) => brIdsInScenarios.add(id));
|
|
1765
|
-
scIdsList.forEach((id) => scIdsInScenarios.add(id));
|
|
1766
|
-
scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
|
|
1767
|
-
if (scenarioIdsList.length > 0) {
|
|
1768
|
-
scIdsList.forEach((id) => scWithContracts.add(id));
|
|
1769
|
-
}
|
|
1770
|
-
const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
|
|
1771
|
-
if (unknownSpecIds.length > 0) {
|
|
1772
|
-
issues.push(
|
|
1773
|
-
issue6(
|
|
1774
|
-
"QFAI-TRACE-005",
|
|
1775
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1776
|
-
"error",
|
|
1777
|
-
file,
|
|
1778
|
-
"traceability.scenarioSpecExists",
|
|
1779
|
-
unknownSpecIds
|
|
1780
|
-
)
|
|
1781
|
-
);
|
|
1782
|
-
}
|
|
1783
|
-
const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
|
|
1784
|
-
if (unknownBrIds.length > 0) {
|
|
1785
|
-
issues.push(
|
|
1786
|
-
issue6(
|
|
1787
|
-
"QFAI-TRACE-006",
|
|
1788
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1789
|
-
"error",
|
|
1790
|
-
file,
|
|
1791
|
-
"traceability.scenarioBrExists",
|
|
1792
|
-
unknownBrIds
|
|
1793
|
-
)
|
|
1794
|
-
);
|
|
1795
|
-
}
|
|
1796
|
-
const unknownContractIds = scenarioIdsList.filter(
|
|
1797
|
-
(id) => !contractIds.has(id)
|
|
1798
|
-
);
|
|
1799
|
-
if (unknownContractIds.length > 0) {
|
|
1800
|
-
issues.push(
|
|
1801
|
-
issue6(
|
|
1802
|
-
"QFAI-TRACE-008",
|
|
1803
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1804
|
-
", "
|
|
1805
|
-
)}`,
|
|
1806
|
-
config.validation.traceability.unknownContractIdSeverity,
|
|
1807
|
-
file,
|
|
1808
|
-
"traceability.scenarioContractExists",
|
|
1809
|
-
unknownContractIds
|
|
1810
|
-
)
|
|
1811
|
-
);
|
|
1956
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1957
|
+
if (!document || errors.length > 0) {
|
|
1958
|
+
continue;
|
|
1812
1959
|
}
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1960
|
+
const atoms = buildScenarioAtoms(document);
|
|
1961
|
+
for (const [index, scenario] of document.scenarios.entries()) {
|
|
1962
|
+
const atom = atoms[index];
|
|
1963
|
+
if (!atom) {
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
1967
|
+
const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
|
|
1968
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
|
|
1969
|
+
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
1970
|
+
scTags.forEach((id) => scIdsInScenarios.add(id));
|
|
1971
|
+
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
1972
|
+
if (atom.contractIds.length > 0) {
|
|
1973
|
+
scTags.forEach((id) => scWithContracts.add(id));
|
|
1974
|
+
}
|
|
1975
|
+
const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
|
|
1976
|
+
if (unknownSpecIds.length > 0) {
|
|
1977
|
+
issues.push(
|
|
1978
|
+
issue6(
|
|
1979
|
+
"QFAI-TRACE-005",
|
|
1980
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(
|
|
1981
|
+
", "
|
|
1982
|
+
)} (${scenario.name})`,
|
|
1983
|
+
"error",
|
|
1984
|
+
file,
|
|
1985
|
+
"traceability.scenarioSpecExists",
|
|
1986
|
+
unknownSpecIds
|
|
1987
|
+
)
|
|
1988
|
+
);
|
|
1821
1989
|
}
|
|
1822
|
-
const
|
|
1823
|
-
if (
|
|
1990
|
+
const unknownBrIds = brTags.filter((id) => !brIdsInSpecs.has(id));
|
|
1991
|
+
if (unknownBrIds.length > 0) {
|
|
1824
1992
|
issues.push(
|
|
1825
1993
|
issue6(
|
|
1826
|
-
"QFAI-TRACE-
|
|
1827
|
-
`Scenario \
|
|
1994
|
+
"QFAI-TRACE-006",
|
|
1995
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(
|
|
1828
1996
|
", "
|
|
1829
|
-
)} (
|
|
1997
|
+
)} (${scenario.name})`,
|
|
1830
1998
|
"error",
|
|
1831
1999
|
file,
|
|
1832
|
-
"traceability.
|
|
1833
|
-
|
|
2000
|
+
"traceability.scenarioBrExists",
|
|
2001
|
+
unknownBrIds
|
|
2002
|
+
)
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
const unknownContractIds = atom.contractIds.filter(
|
|
2006
|
+
(id) => !contractIds.has(id)
|
|
2007
|
+
);
|
|
2008
|
+
if (unknownContractIds.length > 0) {
|
|
2009
|
+
issues.push(
|
|
2010
|
+
issue6(
|
|
2011
|
+
"QFAI-TRACE-008",
|
|
2012
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
2013
|
+
", "
|
|
2014
|
+
)} (${scenario.name})`,
|
|
2015
|
+
config.validation.traceability.unknownContractIdSeverity,
|
|
2016
|
+
file,
|
|
2017
|
+
"traceability.scenarioContractExists",
|
|
2018
|
+
unknownContractIds
|
|
1834
2019
|
)
|
|
1835
2020
|
);
|
|
1836
2021
|
}
|
|
2022
|
+
if (specTags.length > 0 && brTags.length > 0) {
|
|
2023
|
+
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
2024
|
+
for (const specId of specTags) {
|
|
2025
|
+
const brIdsForSpec = specToBrIds.get(specId);
|
|
2026
|
+
if (!brIdsForSpec) {
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
2030
|
+
}
|
|
2031
|
+
const invalidBrIds = brTags.filter((id) => !allowedBrIds.has(id));
|
|
2032
|
+
if (invalidBrIds.length > 0) {
|
|
2033
|
+
issues.push(
|
|
2034
|
+
issue6(
|
|
2035
|
+
"QFAI-TRACE-007",
|
|
2036
|
+
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
2037
|
+
", "
|
|
2038
|
+
)} (SPEC: ${specTags.join(", ")}) (${scenario.name})`,
|
|
2039
|
+
"error",
|
|
2040
|
+
file,
|
|
2041
|
+
"traceability.scenarioBrUnderSpec",
|
|
2042
|
+
invalidBrIds
|
|
2043
|
+
)
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
1837
2047
|
}
|
|
1838
2048
|
}
|
|
1839
2049
|
if (upstreamIds.size === 0) {
|
|
@@ -1876,7 +2086,7 @@ async function validateTraceability(root, config) {
|
|
|
1876
2086
|
", "
|
|
1877
2087
|
)}`,
|
|
1878
2088
|
"error",
|
|
1879
|
-
|
|
2089
|
+
specsRoot,
|
|
1880
2090
|
"traceability.scMustTouchContracts",
|
|
1881
2091
|
scWithoutContracts
|
|
1882
2092
|
)
|
|
@@ -1894,7 +2104,7 @@ async function validateTraceability(root, config) {
|
|
|
1894
2104
|
"QFAI_CONTRACT_ORPHAN",
|
|
1895
2105
|
`\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
1896
2106
|
"error",
|
|
1897
|
-
|
|
2107
|
+
specsRoot,
|
|
1898
2108
|
"traceability.allowOrphanContracts",
|
|
1899
2109
|
orphanContracts
|
|
1900
2110
|
)
|
|
@@ -1979,8 +2189,8 @@ async function validateProject(root, configResult) {
|
|
|
1979
2189
|
const issues = [
|
|
1980
2190
|
...configIssues,
|
|
1981
2191
|
...await validateSpecs(root, config),
|
|
2192
|
+
...await validateDeltas(root, config),
|
|
1982
2193
|
...await validateScenarios(root, config),
|
|
1983
|
-
...await validateDecisions(root, config),
|
|
1984
2194
|
...await validateContracts(root, config),
|
|
1985
2195
|
...await validateDefinedIds(root, config),
|
|
1986
2196
|
...await validateTraceability(root, config)
|
|
@@ -2009,21 +2219,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
2009
2219
|
const resolved = configResult ?? await loadConfig(root);
|
|
2010
2220
|
const config = resolved.config;
|
|
2011
2221
|
const configPath = resolved.configPath;
|
|
2012
|
-
const
|
|
2013
|
-
const
|
|
2014
|
-
const
|
|
2015
|
-
const
|
|
2016
|
-
const
|
|
2017
|
-
const dbRoot = resolvePath(root, config, "dataContractsDir");
|
|
2222
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2223
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2224
|
+
const apiRoot = path13.join(contractsRoot, "api");
|
|
2225
|
+
const uiRoot = path13.join(contractsRoot, "ui");
|
|
2226
|
+
const dbRoot = path13.join(contractsRoot, "db");
|
|
2018
2227
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
2019
2228
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
2020
|
-
const specFiles = await collectSpecFiles(
|
|
2021
|
-
const scenarioFiles = await
|
|
2022
|
-
extensions: [".feature"]
|
|
2023
|
-
});
|
|
2024
|
-
const decisionFiles = await collectFiles(decisionsRoot, {
|
|
2025
|
-
extensions: [".md"]
|
|
2026
|
-
});
|
|
2229
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
2230
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
2027
2231
|
const {
|
|
2028
2232
|
api: apiFiles,
|
|
2029
2233
|
ui: uiFiles,
|
|
@@ -2032,7 +2236,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
2032
2236
|
const idsByPrefix = await collectIds([
|
|
2033
2237
|
...specFiles,
|
|
2034
2238
|
...scenarioFiles,
|
|
2035
|
-
...decisionFiles,
|
|
2036
2239
|
...apiFiles,
|
|
2037
2240
|
...uiFiles,
|
|
2038
2241
|
...dbFiles
|
|
@@ -2057,7 +2260,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
2057
2260
|
summary: {
|
|
2058
2261
|
specs: specFiles.length,
|
|
2059
2262
|
scenarios: scenarioFiles.length,
|
|
2060
|
-
decisions: decisionFiles.length,
|
|
2061
2263
|
contracts: {
|
|
2062
2264
|
api: apiFiles.length,
|
|
2063
2265
|
ui: uiFiles.length,
|
|
@@ -2091,7 +2293,6 @@ function formatReportMarkdown(data) {
|
|
|
2091
2293
|
lines.push("## \u6982\u8981");
|
|
2092
2294
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2093
2295
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
2094
|
-
lines.push(`- decisions: ${data.summary.decisions}`);
|
|
2095
2296
|
lines.push(
|
|
2096
2297
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
2097
2298
|
);
|
|
@@ -2250,21 +2451,22 @@ function buildHotspots(issues) {
|
|
|
2250
2451
|
|
|
2251
2452
|
// src/cli/commands/report.ts
|
|
2252
2453
|
async function runReport(options) {
|
|
2253
|
-
const root =
|
|
2454
|
+
const root = path14.resolve(options.root);
|
|
2254
2455
|
const configResult = await loadConfig(root);
|
|
2255
|
-
const input =
|
|
2256
|
-
const inputPath =
|
|
2456
|
+
const input = configResult.config.output.validateJsonPath;
|
|
2457
|
+
const inputPath = path14.isAbsolute(input) ? input : path14.resolve(root, input);
|
|
2257
2458
|
let validation;
|
|
2258
2459
|
try {
|
|
2259
2460
|
validation = await readValidationResult(inputPath);
|
|
2260
2461
|
} catch (err) {
|
|
2261
|
-
if (
|
|
2462
|
+
if (isMissingFileError5(err)) {
|
|
2262
2463
|
error(
|
|
2263
2464
|
[
|
|
2264
2465
|
`qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
|
|
2265
2466
|
"",
|
|
2266
|
-
"\u307E\u305A validate
|
|
2267
|
-
|
|
2467
|
+
"\u307E\u305A qfai validate \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
|
|
2468
|
+
" qfai validate",
|
|
2469
|
+
"\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/out/validate.json\uFF09",
|
|
2268
2470
|
"",
|
|
2269
2471
|
"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"
|
|
2270
2472
|
].join("\n")
|
|
@@ -2276,10 +2478,11 @@ async function runReport(options) {
|
|
|
2276
2478
|
}
|
|
2277
2479
|
const data = await createReportData(root, validation, configResult);
|
|
2278
2480
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
2279
|
-
const
|
|
2481
|
+
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
2482
|
+
const defaultOut = options.format === "json" ? path14.join(outRoot, "report.json") : path14.join(outRoot, "report.md");
|
|
2280
2483
|
const out = options.outPath ?? defaultOut;
|
|
2281
|
-
const outPath =
|
|
2282
|
-
await mkdir2(
|
|
2484
|
+
const outPath = path14.isAbsolute(out) ? out : path14.resolve(root, out);
|
|
2485
|
+
await mkdir2(path14.dirname(outPath), { recursive: true });
|
|
2283
2486
|
await writeFile(outPath, `${output}
|
|
2284
2487
|
`, "utf-8");
|
|
2285
2488
|
info(
|
|
@@ -2320,7 +2523,7 @@ function isValidationResult(value) {
|
|
|
2320
2523
|
}
|
|
2321
2524
|
return typeof counts.info === "number" && typeof counts.warning === "number" && typeof counts.error === "number";
|
|
2322
2525
|
}
|
|
2323
|
-
function
|
|
2526
|
+
function isMissingFileError5(error2) {
|
|
2324
2527
|
if (!error2 || typeof error2 !== "object") {
|
|
2325
2528
|
return false;
|
|
2326
2529
|
}
|
|
@@ -2330,7 +2533,7 @@ function isMissingFileError(error2) {
|
|
|
2330
2533
|
|
|
2331
2534
|
// src/cli/commands/validate.ts
|
|
2332
2535
|
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
2333
|
-
import
|
|
2536
|
+
import path15 from "path";
|
|
2334
2537
|
|
|
2335
2538
|
// src/cli/lib/failOn.ts
|
|
2336
2539
|
function shouldFail(result, failOn) {
|
|
@@ -2345,24 +2548,17 @@ function shouldFail(result, failOn) {
|
|
|
2345
2548
|
|
|
2346
2549
|
// src/cli/commands/validate.ts
|
|
2347
2550
|
async function runValidate(options) {
|
|
2348
|
-
const root =
|
|
2551
|
+
const root = path15.resolve(options.root);
|
|
2349
2552
|
const configResult = await loadConfig(root);
|
|
2350
2553
|
const result = await validateProject(root, configResult);
|
|
2351
|
-
const format = options.format ??
|
|
2352
|
-
const explicitJsonPath = options.jsonPath;
|
|
2554
|
+
const format = options.format ?? "text";
|
|
2353
2555
|
if (format === "text") {
|
|
2354
2556
|
emitText(result);
|
|
2355
2557
|
}
|
|
2356
2558
|
if (format === "github") {
|
|
2357
2559
|
result.issues.forEach(emitGitHub);
|
|
2358
2560
|
}
|
|
2359
|
-
|
|
2360
|
-
if (shouldWriteJson) {
|
|
2361
|
-
const jsonPath = format === "json" ? options.jsonPath ?? configResult.config.output.jsonPath : explicitJsonPath;
|
|
2362
|
-
if (jsonPath) {
|
|
2363
|
-
await emitJson(result, root, jsonPath);
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
2561
|
+
await emitJson(result, root, configResult.config.output.validateJsonPath);
|
|
2366
2562
|
const failOn = resolveFailOn(options, configResult.config.validation.failOn);
|
|
2367
2563
|
return shouldFail(result, failOn) ? 1 : 0;
|
|
2368
2564
|
}
|
|
@@ -2401,8 +2597,8 @@ function emitGitHub(issue7) {
|
|
|
2401
2597
|
);
|
|
2402
2598
|
}
|
|
2403
2599
|
async function emitJson(result, root, jsonPath) {
|
|
2404
|
-
const abs =
|
|
2405
|
-
await mkdir3(
|
|
2600
|
+
const abs = path15.isAbsolute(jsonPath) ? jsonPath : path15.resolve(root, jsonPath);
|
|
2601
|
+
await mkdir3(path15.dirname(abs), { recursive: true });
|
|
2406
2602
|
await writeFile2(abs, `${JSON.stringify(result, null, 2)}
|
|
2407
2603
|
`, "utf-8");
|
|
2408
2604
|
}
|
|
@@ -2463,15 +2659,6 @@ function parseArgs(argv, cwd) {
|
|
|
2463
2659
|
i += 1;
|
|
2464
2660
|
break;
|
|
2465
2661
|
}
|
|
2466
|
-
case "--json-path":
|
|
2467
|
-
{
|
|
2468
|
-
const next = args[i + 1];
|
|
2469
|
-
if (next) {
|
|
2470
|
-
options.jsonPath = next;
|
|
2471
|
-
}
|
|
2472
|
-
}
|
|
2473
|
-
i += 1;
|
|
2474
|
-
break;
|
|
2475
2662
|
case "--out":
|
|
2476
2663
|
{
|
|
2477
2664
|
const next = args[i + 1];
|
|
@@ -2502,7 +2689,7 @@ function applyFormatOption(command, value, options) {
|
|
|
2502
2689
|
return;
|
|
2503
2690
|
}
|
|
2504
2691
|
if (command === "validate") {
|
|
2505
|
-
if (value === "text" || value === "
|
|
2692
|
+
if (value === "text" || value === "github") {
|
|
2506
2693
|
options.validateFormat = value;
|
|
2507
2694
|
}
|
|
2508
2695
|
return;
|
|
@@ -2510,7 +2697,7 @@ function applyFormatOption(command, value, options) {
|
|
|
2510
2697
|
if (value === "md" || value === "json") {
|
|
2511
2698
|
options.reportFormat = value;
|
|
2512
2699
|
}
|
|
2513
|
-
if (value === "text" || value === "
|
|
2700
|
+
if (value === "text" || value === "github") {
|
|
2514
2701
|
options.validateFormat = value;
|
|
2515
2702
|
}
|
|
2516
2703
|
}
|
|
@@ -2536,15 +2723,13 @@ async function run(argv, cwd) {
|
|
|
2536
2723
|
root: options.root,
|
|
2537
2724
|
strict: options.strict,
|
|
2538
2725
|
format: options.validateFormat,
|
|
2539
|
-
...options.failOn !== void 0 ? { failOn: options.failOn } : {}
|
|
2540
|
-
...options.jsonPath !== void 0 ? { jsonPath: options.jsonPath } : {}
|
|
2726
|
+
...options.failOn !== void 0 ? { failOn: options.failOn } : {}
|
|
2541
2727
|
});
|
|
2542
2728
|
return;
|
|
2543
2729
|
case "report":
|
|
2544
2730
|
await runReport({
|
|
2545
2731
|
root: options.root,
|
|
2546
2732
|
format: options.reportFormat,
|
|
2547
|
-
...options.jsonPath !== void 0 ? { jsonPath: options.jsonPath } : {},
|
|
2548
2733
|
...options.reportOut !== void 0 ? { outPath: options.reportOut } : {}
|
|
2549
2734
|
});
|
|
2550
2735
|
return;
|
|
@@ -2566,14 +2751,13 @@ Options:
|
|
|
2566
2751
|
--root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
|
|
2567
2752
|
--dir <path> init \u306E\u51FA\u529B\u5148
|
|
2568
2753
|
--force \u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u3092\u4E0A\u66F8\u304D
|
|
2569
|
-
--yes init: \
|
|
2754
|
+
--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
|
|
2570
2755
|
--dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
|
|
2571
|
-
--format <text|
|
|
2756
|
+
--format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
|
|
2572
2757
|
--format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
|
|
2573
|
-
--strict
|
|
2758
|
+
--strict validate: warning \u4EE5\u4E0A\u3067 exit 1
|
|
2574
2759
|
--fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
|
|
2575
|
-
--
|
|
2576
|
-
--out <path> report: \u51FA\u529B\u5148
|
|
2760
|
+
--out <path> report: \u51FA\u529B\u5148
|
|
2577
2761
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|
|
2578
2762
|
`;
|
|
2579
2763
|
}
|