qfai 0.2.5 → 0.2.8
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 +1 -1
- package/assets/init/.qfai/README.md +42 -0
- package/assets/init/.qfai/contracts/README.md +61 -0
- package/assets/init/{qfai → .qfai}/contracts/api/api-0001-sample.yaml +3 -0
- package/assets/init/{qfai → .qfai}/contracts/db/db-0001-sample.sql +3 -2
- package/assets/init/.qfai/contracts/ui/ui-0001-sample.yaml +6 -0
- package/assets/init/.qfai/out/README.md +17 -0
- package/assets/init/.qfai/prompts/README.md +32 -0
- package/assets/init/{qfai → .qfai}/prompts/makeBusinessFlow.md +1 -1
- package/assets/init/{qfai → .qfai}/prompts/makeOverview.md +1 -1
- package/assets/init/.qfai/spec/README.md +80 -0
- package/assets/init/.qfai/spec/decisions/ADR-0001.md +9 -0
- package/assets/init/.qfai/spec/decisions/README.md +36 -0
- package/assets/init/.qfai/spec/scenarios/scenarios.feature +6 -0
- package/assets/init/.qfai/spec/spec-0001-sample.md +36 -0
- package/assets/init/root/qfai.config.yaml +8 -8
- package/dist/cli/index.cjs +498 -206
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.mjs +495 -203
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +471 -177
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -4
- package/dist/index.d.ts +135 -2
- package/dist/index.mjs +470 -177
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/assets/init/qfai/README.md +0 -6
- package/assets/init/qfai/contracts/ui/ui-0001-sample.yaml +0 -4
- package/assets/init/qfai/spec/decisions/ADR-0001.md +0 -7
- package/assets/init/qfai/spec/scenarios.feature +0 -6
- package/assets/init/qfai/spec/spec-0001-sample.md +0 -29
- package/dist/cli/commands/init.d.ts +0 -8
- package/dist/cli/commands/init.d.ts.map +0 -1
- package/dist/cli/commands/init.js +0 -30
- package/dist/cli/commands/init.js.map +0 -1
- package/dist/cli/commands/report.d.ts +0 -8
- package/dist/cli/commands/report.d.ts.map +0 -1
- package/dist/cli/commands/report.js +0 -83
- package/dist/cli/commands/report.js.map +0 -1
- package/dist/cli/commands/validate.d.ts +0 -10
- package/dist/cli/commands/validate.d.ts.map +0 -1
- package/dist/cli/commands/validate.js +0 -66
- package/dist/cli/commands/validate.js.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -7
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/lib/args.d.ts +0 -19
- package/dist/cli/lib/args.d.ts.map +0 -1
- package/dist/cli/lib/args.js +0 -107
- package/dist/cli/lib/args.js.map +0 -1
- package/dist/cli/lib/assets.d.ts +0 -2
- package/dist/cli/lib/assets.d.ts.map +0 -1
- package/dist/cli/lib/assets.js +0 -8
- package/dist/cli/lib/assets.js.map +0 -1
- package/dist/cli/lib/failOn.d.ts +0 -5
- package/dist/cli/lib/failOn.d.ts.map +0 -1
- package/dist/cli/lib/failOn.js +0 -10
- package/dist/cli/lib/failOn.js.map +0 -1
- package/dist/cli/lib/fs.d.ts +0 -11
- package/dist/cli/lib/fs.d.ts.map +0 -1
- package/dist/cli/lib/fs.js +0 -91
- package/dist/cli/lib/fs.js.map +0 -1
- package/dist/cli/lib/logger.d.ts +0 -4
- package/dist/cli/lib/logger.d.ts.map +0 -1
- package/dist/cli/lib/logger.js +0 -10
- package/dist/cli/lib/logger.js.map +0 -1
- package/dist/cli/main.d.ts +0 -2
- package/dist/cli/main.d.ts.map +0 -1
- package/dist/cli/main.js +0 -73
- package/dist/cli/main.js.map +0 -1
- package/dist/core/config.d.ts +0 -46
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js +0 -224
- package/dist/core/config.js.map +0 -1
- package/dist/core/discovery.d.ts +0 -11
- package/dist/core/discovery.d.ts.map +0 -1
- package/dist/core/discovery.js +0 -31
- package/dist/core/discovery.js.map +0 -1
- package/dist/core/fs.d.ts +0 -6
- package/dist/core/fs.d.ts.map +0 -1
- package/dist/core/fs.js +0 -55
- package/dist/core/fs.js.map +0 -1
- package/dist/core/ids.d.ts +0 -5
- package/dist/core/ids.d.ts.map +0 -1
- package/dist/core/ids.js +0 -49
- package/dist/core/ids.js.map +0 -1
- package/dist/core/index.d.ts +0 -11
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -11
- package/dist/core/index.js.map +0 -1
- package/dist/core/report.d.ts +0 -41
- package/dist/core/report.d.ts.map +0 -1
- package/dist/core/report.js +0 -238
- package/dist/core/report.js.map +0 -1
- package/dist/core/types.d.ts +0 -27
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js +0 -2
- package/dist/core/types.js.map +0 -1
- package/dist/core/validate.d.ts +0 -4
- package/dist/core/validate.d.ts.map +0 -1
- package/dist/core/validate.js +0 -32
- package/dist/core/validate.js.map +0 -1
- package/dist/core/validators/contracts.d.ts +0 -5
- package/dist/core/validators/contracts.d.ts.map +0 -1
- package/dist/core/validators/contracts.js +0 -157
- package/dist/core/validators/contracts.js.map +0 -1
- package/dist/core/validators/scenario.d.ts +0 -5
- package/dist/core/validators/scenario.d.ts.map +0 -1
- package/dist/core/validators/scenario.js +0 -82
- package/dist/core/validators/scenario.js.map +0 -1
- package/dist/core/validators/spec.d.ts +0 -5
- package/dist/core/validators/spec.d.ts.map +0 -1
- package/dist/core/validators/spec.js +0 -69
- package/dist/core/validators/spec.js.map +0 -1
- package/dist/core/validators/traceability.d.ts +0 -4
- package/dist/core/validators/traceability.d.ts.map +0 -1
- package/dist/core/validators/traceability.js +0 -148
- package/dist/core/validators/traceability.js.map +0 -1
- package/dist/core/version.d.ts +0 -2
- package/dist/core/version.d.ts.map +0 -1
- package/dist/core/version.js +0 -25
- package/dist/core/version.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -108,9 +108,9 @@ function error(message) {
|
|
|
108
108
|
async function runInit(options) {
|
|
109
109
|
const assetsRoot = getInitAssetsDir();
|
|
110
110
|
const rootAssets = path3.join(assetsRoot, "root");
|
|
111
|
-
const qfaiAssets = path3.join(assetsRoot, "qfai");
|
|
111
|
+
const qfaiAssets = path3.join(assetsRoot, ".qfai");
|
|
112
112
|
const destRoot = path3.resolve(options.dir);
|
|
113
|
-
const destQfai = path3.join(destRoot, "qfai");
|
|
113
|
+
const destQfai = path3.join(destRoot, ".qfai");
|
|
114
114
|
const rootResult = await copyTemplateTree(rootAssets, destRoot, {
|
|
115
115
|
force: options.force,
|
|
116
116
|
dryRun: options.dryRun
|
|
@@ -137,8 +137,8 @@ function report(copied, skipped, dryRun, label) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// src/cli/commands/report.ts
|
|
140
|
-
import { mkdir as mkdir2, readFile as
|
|
141
|
-
import
|
|
140
|
+
import { mkdir as mkdir2, readFile as readFile10, writeFile } from "fs/promises";
|
|
141
|
+
import path10 from "path";
|
|
142
142
|
|
|
143
143
|
// src/core/config.ts
|
|
144
144
|
import { readFile } from "fs/promises";
|
|
@@ -146,14 +146,13 @@ 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",
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
dataContractsDir: "qfai/contracts/db",
|
|
149
|
+
specDir: ".qfai/spec",
|
|
150
|
+
decisionsDir: ".qfai/spec/decisions",
|
|
151
|
+
scenariosDir: ".qfai/spec/scenarios",
|
|
152
|
+
contractsDir: ".qfai/contracts",
|
|
153
|
+
uiContractsDir: ".qfai/contracts/ui",
|
|
154
|
+
apiContractsDir: ".qfai/contracts/api",
|
|
155
|
+
dataContractsDir: ".qfai/contracts/db",
|
|
157
156
|
srcDir: "src",
|
|
158
157
|
testsDir: "tests"
|
|
159
158
|
},
|
|
@@ -173,7 +172,8 @@ var defaultConfig = {
|
|
|
173
172
|
traceability: {
|
|
174
173
|
brMustHaveSc: true,
|
|
175
174
|
scMustTouchContracts: true,
|
|
176
|
-
allowOrphanContracts: false
|
|
175
|
+
allowOrphanContracts: false,
|
|
176
|
+
unknownContractIdSeverity: "error"
|
|
177
177
|
}
|
|
178
178
|
},
|
|
179
179
|
output: {
|
|
@@ -248,13 +248,6 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
248
248
|
configPath,
|
|
249
249
|
issues
|
|
250
250
|
),
|
|
251
|
-
rulesDir: readString(
|
|
252
|
-
raw.rulesDir,
|
|
253
|
-
base.rulesDir,
|
|
254
|
-
"paths.rulesDir",
|
|
255
|
-
configPath,
|
|
256
|
-
issues
|
|
257
|
-
),
|
|
258
251
|
contractsDir: readString(
|
|
259
252
|
raw.contractsDir,
|
|
260
253
|
base.contractsDir,
|
|
@@ -379,6 +372,13 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
379
372
|
"validation.traceability.allowOrphanContracts",
|
|
380
373
|
configPath,
|
|
381
374
|
issues
|
|
375
|
+
),
|
|
376
|
+
unknownContractIdSeverity: readTraceabilitySeverity(
|
|
377
|
+
traceabilityRaw?.unknownContractIdSeverity,
|
|
378
|
+
base.traceability.unknownContractIdSeverity,
|
|
379
|
+
"validation.traceability.unknownContractIdSeverity",
|
|
380
|
+
configPath,
|
|
381
|
+
issues
|
|
382
382
|
)
|
|
383
383
|
}
|
|
384
384
|
};
|
|
@@ -458,6 +458,20 @@ function readFailOn(value, fallback, label, configPath, issues) {
|
|
|
458
458
|
}
|
|
459
459
|
return fallback;
|
|
460
460
|
}
|
|
461
|
+
function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
462
|
+
if (value === "warning" || value === "error") {
|
|
463
|
+
return value;
|
|
464
|
+
}
|
|
465
|
+
if (value !== void 0) {
|
|
466
|
+
issues.push(
|
|
467
|
+
configIssue(
|
|
468
|
+
configPath,
|
|
469
|
+
`${label} \u306F warning|error \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
470
|
+
)
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
return fallback;
|
|
474
|
+
}
|
|
461
475
|
function readOutputFormat(value, fallback, label, configPath, issues) {
|
|
462
476
|
if (value === "text" || value === "json" || value === "github") {
|
|
463
477
|
return value;
|
|
@@ -498,7 +512,7 @@ function isRecord(value) {
|
|
|
498
512
|
}
|
|
499
513
|
|
|
500
514
|
// src/core/report.ts
|
|
501
|
-
import { readFile as
|
|
515
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
502
516
|
|
|
503
517
|
// src/core/discovery.ts
|
|
504
518
|
import path6 from "path";
|
|
@@ -559,8 +573,7 @@ async function exists2(target) {
|
|
|
559
573
|
}
|
|
560
574
|
|
|
561
575
|
// src/core/discovery.ts
|
|
562
|
-
var
|
|
563
|
-
var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/i;
|
|
576
|
+
var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/;
|
|
564
577
|
async function collectSpecFiles(specRoot) {
|
|
565
578
|
const files = await collectFiles(specRoot, { extensions: [".md"] });
|
|
566
579
|
return files.filter((file) => isSpecFile(file));
|
|
@@ -584,17 +597,19 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
|
584
597
|
}
|
|
585
598
|
function isSpecFile(filePath) {
|
|
586
599
|
const name = path6.basename(filePath).toLowerCase();
|
|
587
|
-
return
|
|
600
|
+
return SPEC_NAMED_PATTERN.test(name);
|
|
588
601
|
}
|
|
589
602
|
|
|
590
603
|
// src/core/ids.ts
|
|
591
|
-
var
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
604
|
+
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
|
|
605
|
+
var STRICT_ID_PATTERNS = {
|
|
606
|
+
SPEC: /\bSPEC-\d{4}\b/g,
|
|
607
|
+
BR: /\bBR-\d{4}\b/g,
|
|
608
|
+
SC: /\bSC-\d{4}\b/g,
|
|
609
|
+
UI: /\bUI-\d{4}\b/g,
|
|
610
|
+
API: /\bAPI-\d{4}\b/g,
|
|
611
|
+
DATA: /\bDATA-\d{4}\b/g,
|
|
612
|
+
ADR: /\bADR-\d{4}\b/g
|
|
598
613
|
};
|
|
599
614
|
var LOOSE_ID_PATTERNS = {
|
|
600
615
|
SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
|
|
@@ -602,16 +617,17 @@ var LOOSE_ID_PATTERNS = {
|
|
|
602
617
|
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
603
618
|
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
604
619
|
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
605
|
-
DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi
|
|
620
|
+
DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi,
|
|
621
|
+
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
606
622
|
};
|
|
607
623
|
function extractIds(text, prefix) {
|
|
608
|
-
const pattern =
|
|
624
|
+
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
609
625
|
const matches = text.match(pattern);
|
|
610
626
|
return unique(matches ?? []);
|
|
611
627
|
}
|
|
612
628
|
function extractAllIds(text) {
|
|
613
629
|
const all = [];
|
|
614
|
-
|
|
630
|
+
ID_PREFIXES.forEach((prefix) => {
|
|
615
631
|
all.push(...extractIds(text, prefix));
|
|
616
632
|
});
|
|
617
633
|
return unique(all);
|
|
@@ -632,7 +648,7 @@ function unique(values) {
|
|
|
632
648
|
return Array.from(new Set(values));
|
|
633
649
|
}
|
|
634
650
|
function isValidId(value, prefix) {
|
|
635
|
-
const pattern =
|
|
651
|
+
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
636
652
|
const strict = new RegExp(pattern.source);
|
|
637
653
|
return strict.test(value);
|
|
638
654
|
}
|
|
@@ -645,8 +661,8 @@ import { readFile as readFile2 } from "fs/promises";
|
|
|
645
661
|
import path7 from "path";
|
|
646
662
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
647
663
|
async function resolveToolVersion() {
|
|
648
|
-
if ("0.2.
|
|
649
|
-
return "0.2.
|
|
664
|
+
if ("0.2.9".length > 0) {
|
|
665
|
+
return "0.2.9";
|
|
650
666
|
}
|
|
651
667
|
try {
|
|
652
668
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -666,8 +682,50 @@ function resolvePackageJsonPath() {
|
|
|
666
682
|
|
|
667
683
|
// src/core/validators/contracts.ts
|
|
668
684
|
import { readFile as readFile3 } from "fs/promises";
|
|
685
|
+
|
|
686
|
+
// src/core/contracts.ts
|
|
669
687
|
import path8 from "path";
|
|
670
688
|
import { parse as parseYaml2 } from "yaml";
|
|
689
|
+
function parseStructuredContract(file, text) {
|
|
690
|
+
const ext = path8.extname(file).toLowerCase();
|
|
691
|
+
if (ext === ".json") {
|
|
692
|
+
return JSON.parse(text);
|
|
693
|
+
}
|
|
694
|
+
return parseYaml2(text);
|
|
695
|
+
}
|
|
696
|
+
function extractUiContractIds(doc) {
|
|
697
|
+
const id = typeof doc.id === "string" ? doc.id : "";
|
|
698
|
+
return extractIds(id, "UI");
|
|
699
|
+
}
|
|
700
|
+
function extractApiContractIds(doc) {
|
|
701
|
+
const operationIds = /* @__PURE__ */ new Set();
|
|
702
|
+
collectOperationIds(doc, operationIds);
|
|
703
|
+
const ids = /* @__PURE__ */ new Set();
|
|
704
|
+
for (const operationId of operationIds) {
|
|
705
|
+
extractIds(operationId, "API").forEach((id) => ids.add(id));
|
|
706
|
+
}
|
|
707
|
+
return Array.from(ids);
|
|
708
|
+
}
|
|
709
|
+
function collectOperationIds(value, out) {
|
|
710
|
+
if (!value || typeof value !== "object") {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (Array.isArray(value)) {
|
|
714
|
+
for (const item of value) {
|
|
715
|
+
collectOperationIds(item, out);
|
|
716
|
+
}
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
720
|
+
if (key === "operationId" && typeof entry === "string") {
|
|
721
|
+
out.add(entry);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
collectOperationIds(entry, out);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/core/validators/contracts.ts
|
|
671
729
|
var SQL_DANGEROUS_PATTERNS = [
|
|
672
730
|
{ pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
|
|
673
731
|
{ pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
|
|
@@ -716,12 +774,13 @@ async function validateUiContracts(uiRoot) {
|
|
|
716
774
|
"SC",
|
|
717
775
|
"UI",
|
|
718
776
|
"API",
|
|
719
|
-
"DATA"
|
|
777
|
+
"DATA",
|
|
778
|
+
"ADR"
|
|
720
779
|
]);
|
|
721
780
|
if (invalidIds.length > 0) {
|
|
722
781
|
issues.push(
|
|
723
782
|
issue(
|
|
724
|
-
"
|
|
783
|
+
"QFAI-ID-002",
|
|
725
784
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
726
785
|
"error",
|
|
727
786
|
file,
|
|
@@ -730,30 +789,32 @@ async function validateUiContracts(uiRoot) {
|
|
|
730
789
|
)
|
|
731
790
|
);
|
|
732
791
|
}
|
|
792
|
+
let doc;
|
|
733
793
|
try {
|
|
734
|
-
|
|
735
|
-
const id = typeof doc.id === "string" ? doc.id : "";
|
|
736
|
-
if (!id.startsWith("UI-")) {
|
|
737
|
-
issues.push(
|
|
738
|
-
issue(
|
|
739
|
-
"QFAI-UI-001",
|
|
740
|
-
"UI \u5951\u7D04\u306E id \u306F UI- \u3067\u59CB\u307E\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
741
|
-
"error",
|
|
742
|
-
file,
|
|
743
|
-
"contracts.ui.id"
|
|
744
|
-
)
|
|
745
|
-
);
|
|
746
|
-
}
|
|
794
|
+
doc = parseStructuredContract(file, text);
|
|
747
795
|
} catch (error2) {
|
|
748
796
|
issues.push(
|
|
749
797
|
issue(
|
|
750
|
-
"QFAI-
|
|
751
|
-
`UI
|
|
798
|
+
"QFAI-CONTRACT-001",
|
|
799
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error2)})`,
|
|
752
800
|
"error",
|
|
753
801
|
file,
|
|
754
802
|
"contracts.ui.parse"
|
|
755
803
|
)
|
|
756
804
|
);
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const uiIds = extractUiContractIds(doc);
|
|
808
|
+
if (uiIds.length === 0) {
|
|
809
|
+
issues.push(
|
|
810
|
+
issue(
|
|
811
|
+
"QFAI-CONTRACT-002",
|
|
812
|
+
`UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
813
|
+
"error",
|
|
814
|
+
file,
|
|
815
|
+
"contracts.ui.id"
|
|
816
|
+
)
|
|
817
|
+
);
|
|
757
818
|
}
|
|
758
819
|
}
|
|
759
820
|
return issues;
|
|
@@ -780,12 +841,13 @@ async function validateApiContracts(apiRoot) {
|
|
|
780
841
|
"SC",
|
|
781
842
|
"UI",
|
|
782
843
|
"API",
|
|
783
|
-
"DATA"
|
|
844
|
+
"DATA",
|
|
845
|
+
"ADR"
|
|
784
846
|
]);
|
|
785
847
|
if (invalidIds.length > 0) {
|
|
786
848
|
issues.push(
|
|
787
849
|
issue(
|
|
788
|
-
"
|
|
850
|
+
"QFAI-ID-002",
|
|
789
851
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
790
852
|
"error",
|
|
791
853
|
file,
|
|
@@ -794,29 +856,43 @@ async function validateApiContracts(apiRoot) {
|
|
|
794
856
|
)
|
|
795
857
|
);
|
|
796
858
|
}
|
|
859
|
+
let doc;
|
|
797
860
|
try {
|
|
798
|
-
|
|
799
|
-
if (!doc || !hasOpenApi(doc)) {
|
|
800
|
-
issues.push(
|
|
801
|
-
issue(
|
|
802
|
-
"QFAI-API-001",
|
|
803
|
-
"OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
804
|
-
"error",
|
|
805
|
-
file,
|
|
806
|
-
"contracts.api.openapi"
|
|
807
|
-
)
|
|
808
|
-
);
|
|
809
|
-
}
|
|
861
|
+
doc = parseStructuredContract(file, text);
|
|
810
862
|
} catch (error2) {
|
|
811
863
|
issues.push(
|
|
812
864
|
issue(
|
|
813
|
-
"QFAI-
|
|
814
|
-
`API \
|
|
865
|
+
"QFAI-CONTRACT-001",
|
|
866
|
+
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error2)})`,
|
|
815
867
|
"error",
|
|
816
868
|
file,
|
|
817
869
|
"contracts.api.parse"
|
|
818
870
|
)
|
|
819
871
|
);
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (!hasOpenApi(doc)) {
|
|
875
|
+
issues.push(
|
|
876
|
+
issue(
|
|
877
|
+
"QFAI-API-001",
|
|
878
|
+
"OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
879
|
+
"error",
|
|
880
|
+
file,
|
|
881
|
+
"contracts.api.openapi"
|
|
882
|
+
)
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
const apiIds = extractApiContractIds(doc);
|
|
886
|
+
if (apiIds.length === 0) {
|
|
887
|
+
issues.push(
|
|
888
|
+
issue(
|
|
889
|
+
"QFAI-CONTRACT-002",
|
|
890
|
+
`API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
891
|
+
"error",
|
|
892
|
+
file,
|
|
893
|
+
"contracts.api.id"
|
|
894
|
+
)
|
|
895
|
+
);
|
|
820
896
|
}
|
|
821
897
|
}
|
|
822
898
|
return issues;
|
|
@@ -843,12 +919,13 @@ async function validateDataContracts(dataRoot) {
|
|
|
843
919
|
"SC",
|
|
844
920
|
"UI",
|
|
845
921
|
"API",
|
|
846
|
-
"DATA"
|
|
922
|
+
"DATA",
|
|
923
|
+
"ADR"
|
|
847
924
|
]);
|
|
848
925
|
if (invalidIds.length > 0) {
|
|
849
926
|
issues.push(
|
|
850
927
|
issue(
|
|
851
|
-
"
|
|
928
|
+
"QFAI-ID-002",
|
|
852
929
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
853
930
|
"error",
|
|
854
931
|
file,
|
|
@@ -878,13 +955,6 @@ function lintSql(text, file) {
|
|
|
878
955
|
}
|
|
879
956
|
return issues;
|
|
880
957
|
}
|
|
881
|
-
function parseStructured(file, text) {
|
|
882
|
-
const ext = path8.extname(file).toLowerCase();
|
|
883
|
-
if (ext === ".json") {
|
|
884
|
-
return JSON.parse(text);
|
|
885
|
-
}
|
|
886
|
-
return parseYaml2(text);
|
|
887
|
-
}
|
|
888
958
|
function hasOpenApi(doc) {
|
|
889
959
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
890
960
|
}
|
|
@@ -895,25 +965,165 @@ function formatError2(error2) {
|
|
|
895
965
|
return String(error2);
|
|
896
966
|
}
|
|
897
967
|
function issue(code, message, severity, file, rule, refs) {
|
|
898
|
-
const
|
|
968
|
+
const issue6 = {
|
|
899
969
|
code,
|
|
900
970
|
severity,
|
|
901
971
|
message
|
|
902
972
|
};
|
|
903
973
|
if (file) {
|
|
904
|
-
|
|
974
|
+
issue6.file = file;
|
|
905
975
|
}
|
|
906
976
|
if (rule) {
|
|
907
|
-
|
|
977
|
+
issue6.rule = rule;
|
|
908
978
|
}
|
|
909
979
|
if (refs && refs.length > 0) {
|
|
910
|
-
|
|
980
|
+
issue6.refs = refs;
|
|
911
981
|
}
|
|
912
|
-
return
|
|
982
|
+
return issue6;
|
|
913
983
|
}
|
|
914
984
|
|
|
915
|
-
// src/core/validators/
|
|
985
|
+
// src/core/validators/ids.ts
|
|
986
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
987
|
+
import path9 from "path";
|
|
988
|
+
|
|
989
|
+
// src/core/contractIndex.ts
|
|
916
990
|
import { readFile as readFile4 } from "fs/promises";
|
|
991
|
+
async function buildContractIndex(root, config) {
|
|
992
|
+
const uiRoot = resolvePath(root, config, "uiContractsDir");
|
|
993
|
+
const apiRoot = resolvePath(root, config, "apiContractsDir");
|
|
994
|
+
const dataRoot = resolvePath(root, config, "dataContractsDir");
|
|
995
|
+
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
996
|
+
collectUiContractFiles(uiRoot),
|
|
997
|
+
collectApiContractFiles(apiRoot),
|
|
998
|
+
collectDataContractFiles(dataRoot)
|
|
999
|
+
]);
|
|
1000
|
+
const index = {
|
|
1001
|
+
ids: /* @__PURE__ */ new Set(),
|
|
1002
|
+
idToFiles: /* @__PURE__ */ new Map(),
|
|
1003
|
+
files: { ui: uiFiles, api: apiFiles, data: dataFiles },
|
|
1004
|
+
structuredParseFailedFiles: /* @__PURE__ */ new Set()
|
|
1005
|
+
};
|
|
1006
|
+
await indexUiContracts(uiFiles, index);
|
|
1007
|
+
await indexApiContracts(apiFiles, index);
|
|
1008
|
+
await indexDataContracts(dataFiles, index);
|
|
1009
|
+
return index;
|
|
1010
|
+
}
|
|
1011
|
+
async function indexUiContracts(files, index) {
|
|
1012
|
+
for (const file of files) {
|
|
1013
|
+
const text = await readFile4(file, "utf-8");
|
|
1014
|
+
try {
|
|
1015
|
+
const doc = parseStructuredContract(file, text);
|
|
1016
|
+
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
1017
|
+
} catch {
|
|
1018
|
+
index.structuredParseFailedFiles.add(file);
|
|
1019
|
+
extractIds(text, "UI").forEach((id) => record(index, id, file));
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
async function indexApiContracts(files, index) {
|
|
1024
|
+
for (const file of files) {
|
|
1025
|
+
const text = await readFile4(file, "utf-8");
|
|
1026
|
+
try {
|
|
1027
|
+
const doc = parseStructuredContract(file, text);
|
|
1028
|
+
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
1029
|
+
} catch {
|
|
1030
|
+
index.structuredParseFailedFiles.add(file);
|
|
1031
|
+
extractIds(text, "API").forEach((id) => record(index, id, file));
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
async function indexDataContracts(files, index) {
|
|
1036
|
+
for (const file of files) {
|
|
1037
|
+
const text = await readFile4(file, "utf-8");
|
|
1038
|
+
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
function record(index, id, file) {
|
|
1042
|
+
index.ids.add(id);
|
|
1043
|
+
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
1044
|
+
current.add(file);
|
|
1045
|
+
index.idToFiles.set(id, current);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/core/validators/ids.ts
|
|
1049
|
+
async function validateDefinedIds(root, config) {
|
|
1050
|
+
const issues = [];
|
|
1051
|
+
const specRoot = resolvePath(root, config, "specDir");
|
|
1052
|
+
const scenarioRoot = resolvePath(root, config, "scenariosDir");
|
|
1053
|
+
const specFiles = await collectSpecFiles(specRoot);
|
|
1054
|
+
const scenarioFiles = await collectFiles(scenarioRoot, {
|
|
1055
|
+
extensions: [".feature"]
|
|
1056
|
+
});
|
|
1057
|
+
const defined = /* @__PURE__ */ new Map();
|
|
1058
|
+
await collectSpecDefinitionIds(specFiles, defined);
|
|
1059
|
+
await collectScenarioDefinitionIds(scenarioFiles, defined);
|
|
1060
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
1061
|
+
for (const [id, files] of contractIndex.idToFiles.entries()) {
|
|
1062
|
+
for (const file of files) {
|
|
1063
|
+
recordId(defined, id, file);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
for (const [id, files] of defined.entries()) {
|
|
1067
|
+
if (files.size <= 1) {
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
const sorted = Array.from(files).sort();
|
|
1071
|
+
issues.push(
|
|
1072
|
+
issue2(
|
|
1073
|
+
"QFAI-ID-001",
|
|
1074
|
+
`ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
|
|
1075
|
+
"error",
|
|
1076
|
+
sorted[0],
|
|
1077
|
+
"id.duplicate"
|
|
1078
|
+
)
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
return issues;
|
|
1082
|
+
}
|
|
1083
|
+
async function collectSpecDefinitionIds(files, out) {
|
|
1084
|
+
for (const file of files) {
|
|
1085
|
+
const text = await readFile5(file, "utf-8");
|
|
1086
|
+
extractIds(text, "SPEC").forEach((id) => recordId(out, id, file));
|
|
1087
|
+
extractIds(text, "BR").forEach((id) => recordId(out, id, file));
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
async function collectScenarioDefinitionIds(files, out) {
|
|
1091
|
+
for (const file of files) {
|
|
1092
|
+
const text = await readFile5(file, "utf-8");
|
|
1093
|
+
extractIds(text, "SC").forEach((id) => recordId(out, id, file));
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
function recordId(out, id, file) {
|
|
1097
|
+
const current = out.get(id) ?? /* @__PURE__ */ new Set();
|
|
1098
|
+
current.add(file);
|
|
1099
|
+
out.set(id, current);
|
|
1100
|
+
}
|
|
1101
|
+
function formatFileList(files, root) {
|
|
1102
|
+
return files.map((file) => {
|
|
1103
|
+
const relative = path9.relative(root, file);
|
|
1104
|
+
return relative.length > 0 ? relative : file;
|
|
1105
|
+
}).join(", ");
|
|
1106
|
+
}
|
|
1107
|
+
function issue2(code, message, severity, file, rule, refs) {
|
|
1108
|
+
const issue6 = {
|
|
1109
|
+
code,
|
|
1110
|
+
severity,
|
|
1111
|
+
message
|
|
1112
|
+
};
|
|
1113
|
+
if (file) {
|
|
1114
|
+
issue6.file = file;
|
|
1115
|
+
}
|
|
1116
|
+
if (rule) {
|
|
1117
|
+
issue6.rule = rule;
|
|
1118
|
+
}
|
|
1119
|
+
if (refs && refs.length > 0) {
|
|
1120
|
+
issue6.refs = refs;
|
|
1121
|
+
}
|
|
1122
|
+
return issue6;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// src/core/validators/scenario.ts
|
|
1126
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
917
1127
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
918
1128
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
919
1129
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -924,7 +1134,7 @@ async function validateScenarios(root, config) {
|
|
|
924
1134
|
});
|
|
925
1135
|
if (files.length === 0) {
|
|
926
1136
|
return [
|
|
927
|
-
|
|
1137
|
+
issue3(
|
|
928
1138
|
"QFAI-SC-000",
|
|
929
1139
|
"Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
930
1140
|
"info",
|
|
@@ -935,7 +1145,7 @@ async function validateScenarios(root, config) {
|
|
|
935
1145
|
}
|
|
936
1146
|
const issues = [];
|
|
937
1147
|
for (const file of files) {
|
|
938
|
-
const text = await
|
|
1148
|
+
const text = await readFile6(file, "utf-8");
|
|
939
1149
|
issues.push(...validateScenarioContent(text, file));
|
|
940
1150
|
}
|
|
941
1151
|
return issues;
|
|
@@ -948,12 +1158,13 @@ function validateScenarioContent(text, file) {
|
|
|
948
1158
|
"SC",
|
|
949
1159
|
"UI",
|
|
950
1160
|
"API",
|
|
951
|
-
"DATA"
|
|
1161
|
+
"DATA",
|
|
1162
|
+
"ADR"
|
|
952
1163
|
]);
|
|
953
1164
|
if (invalidIds.length > 0) {
|
|
954
1165
|
issues.push(
|
|
955
|
-
|
|
956
|
-
"
|
|
1166
|
+
issue3(
|
|
1167
|
+
"QFAI-ID-002",
|
|
957
1168
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
958
1169
|
"error",
|
|
959
1170
|
file,
|
|
@@ -965,7 +1176,7 @@ function validateScenarioContent(text, file) {
|
|
|
965
1176
|
const scIds = extractIds(text, "SC");
|
|
966
1177
|
if (scIds.length === 0) {
|
|
967
1178
|
issues.push(
|
|
968
|
-
|
|
1179
|
+
issue3(
|
|
969
1180
|
"QFAI-SC-001",
|
|
970
1181
|
"SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
971
1182
|
"error",
|
|
@@ -977,7 +1188,7 @@ function validateScenarioContent(text, file) {
|
|
|
977
1188
|
const specIds = extractIds(text, "SPEC");
|
|
978
1189
|
if (specIds.length === 0) {
|
|
979
1190
|
issues.push(
|
|
980
|
-
|
|
1191
|
+
issue3(
|
|
981
1192
|
"QFAI-SC-002",
|
|
982
1193
|
"SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
983
1194
|
"error",
|
|
@@ -989,7 +1200,7 @@ function validateScenarioContent(text, file) {
|
|
|
989
1200
|
const brIds = extractIds(text, "BR");
|
|
990
1201
|
if (brIds.length === 0) {
|
|
991
1202
|
issues.push(
|
|
992
|
-
|
|
1203
|
+
issue3(
|
|
993
1204
|
"QFAI-SC-003",
|
|
994
1205
|
"SC \u306F BR \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
995
1206
|
"error",
|
|
@@ -1010,7 +1221,7 @@ function validateScenarioContent(text, file) {
|
|
|
1010
1221
|
}
|
|
1011
1222
|
if (missingSteps.length > 0) {
|
|
1012
1223
|
issues.push(
|
|
1013
|
-
|
|
1224
|
+
issue3(
|
|
1014
1225
|
"QFAI-SC-005",
|
|
1015
1226
|
`Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
|
|
1016
1227
|
"warning",
|
|
@@ -1021,34 +1232,35 @@ function validateScenarioContent(text, file) {
|
|
|
1021
1232
|
}
|
|
1022
1233
|
return issues;
|
|
1023
1234
|
}
|
|
1024
|
-
function
|
|
1025
|
-
const
|
|
1235
|
+
function issue3(code, message, severity, file, rule, refs) {
|
|
1236
|
+
const issue6 = {
|
|
1026
1237
|
code,
|
|
1027
1238
|
severity,
|
|
1028
1239
|
message
|
|
1029
1240
|
};
|
|
1030
1241
|
if (file) {
|
|
1031
|
-
|
|
1242
|
+
issue6.file = file;
|
|
1032
1243
|
}
|
|
1033
1244
|
if (rule) {
|
|
1034
|
-
|
|
1245
|
+
issue6.rule = rule;
|
|
1035
1246
|
}
|
|
1036
1247
|
if (refs && refs.length > 0) {
|
|
1037
|
-
|
|
1248
|
+
issue6.refs = refs;
|
|
1038
1249
|
}
|
|
1039
|
-
return
|
|
1250
|
+
return issue6;
|
|
1040
1251
|
}
|
|
1041
1252
|
|
|
1042
1253
|
// src/core/validators/spec.ts
|
|
1043
|
-
import { readFile as
|
|
1254
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1044
1255
|
async function validateSpecs(root, config) {
|
|
1045
1256
|
const specsRoot = resolvePath(root, config, "specDir");
|
|
1046
1257
|
const files = await collectSpecFiles(specsRoot);
|
|
1047
1258
|
if (files.length === 0) {
|
|
1259
|
+
const expected = "spec-0001-<slug>.md";
|
|
1048
1260
|
return [
|
|
1049
|
-
|
|
1261
|
+
issue4(
|
|
1050
1262
|
"QFAI-SPEC-000",
|
|
1051
|
-
|
|
1263
|
+
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}`,
|
|
1052
1264
|
"info",
|
|
1053
1265
|
specsRoot,
|
|
1054
1266
|
"spec.files"
|
|
@@ -1057,7 +1269,7 @@ async function validateSpecs(root, config) {
|
|
|
1057
1269
|
}
|
|
1058
1270
|
const issues = [];
|
|
1059
1271
|
for (const file of files) {
|
|
1060
|
-
const text = await
|
|
1272
|
+
const text = await readFile7(file, "utf-8");
|
|
1061
1273
|
issues.push(
|
|
1062
1274
|
...validateSpecContent(
|
|
1063
1275
|
text,
|
|
@@ -1076,12 +1288,13 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1076
1288
|
"SC",
|
|
1077
1289
|
"UI",
|
|
1078
1290
|
"API",
|
|
1079
|
-
"DATA"
|
|
1291
|
+
"DATA",
|
|
1292
|
+
"ADR"
|
|
1080
1293
|
]);
|
|
1081
1294
|
if (invalidIds.length > 0) {
|
|
1082
1295
|
issues.push(
|
|
1083
|
-
|
|
1084
|
-
"
|
|
1296
|
+
issue4(
|
|
1297
|
+
"QFAI-ID-002",
|
|
1085
1298
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
1086
1299
|
"error",
|
|
1087
1300
|
file,
|
|
@@ -1093,7 +1306,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1093
1306
|
const specIds = extractIds(text, "SPEC");
|
|
1094
1307
|
if (specIds.length === 0) {
|
|
1095
1308
|
issues.push(
|
|
1096
|
-
|
|
1309
|
+
issue4(
|
|
1097
1310
|
"QFAI-SPEC-001",
|
|
1098
1311
|
"SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1099
1312
|
"error",
|
|
@@ -1105,7 +1318,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1105
1318
|
const brIds = extractIds(text, "BR");
|
|
1106
1319
|
if (brIds.length === 0) {
|
|
1107
1320
|
issues.push(
|
|
1108
|
-
|
|
1321
|
+
issue4(
|
|
1109
1322
|
"QFAI-SPEC-002",
|
|
1110
1323
|
"BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1111
1324
|
"error",
|
|
@@ -1117,7 +1330,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1117
1330
|
const scIds = extractIds(text, "SC");
|
|
1118
1331
|
if (scIds.length > 0) {
|
|
1119
1332
|
issues.push(
|
|
1120
|
-
|
|
1333
|
+
issue4(
|
|
1121
1334
|
"QFAI-SPEC-003",
|
|
1122
1335
|
"Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
|
|
1123
1336
|
"warning",
|
|
@@ -1130,7 +1343,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1130
1343
|
for (const section of requiredSections) {
|
|
1131
1344
|
if (!text.includes(section)) {
|
|
1132
1345
|
issues.push(
|
|
1133
|
-
|
|
1346
|
+
issue4(
|
|
1134
1347
|
"QFAI-SPEC-004",
|
|
1135
1348
|
`\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
|
|
1136
1349
|
"error",
|
|
@@ -1142,26 +1355,26 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1142
1355
|
}
|
|
1143
1356
|
return issues;
|
|
1144
1357
|
}
|
|
1145
|
-
function
|
|
1146
|
-
const
|
|
1358
|
+
function issue4(code, message, severity, file, rule, refs) {
|
|
1359
|
+
const issue6 = {
|
|
1147
1360
|
code,
|
|
1148
1361
|
severity,
|
|
1149
1362
|
message
|
|
1150
1363
|
};
|
|
1151
1364
|
if (file) {
|
|
1152
|
-
|
|
1365
|
+
issue6.file = file;
|
|
1153
1366
|
}
|
|
1154
1367
|
if (rule) {
|
|
1155
|
-
|
|
1368
|
+
issue6.rule = rule;
|
|
1156
1369
|
}
|
|
1157
1370
|
if (refs && refs.length > 0) {
|
|
1158
|
-
|
|
1371
|
+
issue6.refs = refs;
|
|
1159
1372
|
}
|
|
1160
|
-
return
|
|
1373
|
+
return issue6;
|
|
1161
1374
|
}
|
|
1162
1375
|
|
|
1163
1376
|
// src/core/validators/traceability.ts
|
|
1164
|
-
import { readFile as
|
|
1377
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1165
1378
|
async function validateTraceability(root, config) {
|
|
1166
1379
|
const issues = [];
|
|
1167
1380
|
const specsRoot = resolvePath(root, config, "specDir");
|
|
@@ -1177,36 +1390,141 @@ async function validateTraceability(root, config) {
|
|
|
1177
1390
|
extensions: [".feature"]
|
|
1178
1391
|
});
|
|
1179
1392
|
const upstreamIds = /* @__PURE__ */ new Set();
|
|
1393
|
+
const specIds = /* @__PURE__ */ new Set();
|
|
1180
1394
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
1181
1395
|
const brIdsInScenarios = /* @__PURE__ */ new Set();
|
|
1182
1396
|
const scIdsInScenarios = /* @__PURE__ */ new Set();
|
|
1183
1397
|
const scenarioContractIds = /* @__PURE__ */ new Set();
|
|
1184
1398
|
const scWithContracts = /* @__PURE__ */ new Set();
|
|
1185
|
-
|
|
1186
|
-
|
|
1399
|
+
const specToBrIds = /* @__PURE__ */ new Map();
|
|
1400
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
1401
|
+
const contractIds = contractIndex.ids;
|
|
1402
|
+
for (const file of specFiles) {
|
|
1403
|
+
const text = await readFile8(file, "utf-8");
|
|
1404
|
+
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1405
|
+
const specIdsInFile = extractIds(text, "SPEC");
|
|
1406
|
+
specIdsInFile.forEach((id) => specIds.add(id));
|
|
1407
|
+
const brIds = extractIds(text, "BR");
|
|
1408
|
+
brIds.forEach((id) => brIdsInSpecs.add(id));
|
|
1409
|
+
const referencedContractIds = /* @__PURE__ */ new Set([
|
|
1410
|
+
...extractIds(text, "UI"),
|
|
1411
|
+
...extractIds(text, "API"),
|
|
1412
|
+
...extractIds(text, "DATA")
|
|
1413
|
+
]);
|
|
1414
|
+
const unknownContractIds = Array.from(referencedContractIds).filter(
|
|
1415
|
+
(id) => !contractIds.has(id)
|
|
1416
|
+
);
|
|
1417
|
+
if (unknownContractIds.length > 0) {
|
|
1418
|
+
issues.push(
|
|
1419
|
+
issue5(
|
|
1420
|
+
"QFAI-TRACE-009",
|
|
1421
|
+
`Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1422
|
+
", "
|
|
1423
|
+
)}`,
|
|
1424
|
+
"error",
|
|
1425
|
+
file,
|
|
1426
|
+
"traceability.specContractExists",
|
|
1427
|
+
unknownContractIds
|
|
1428
|
+
)
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
for (const specId of specIdsInFile) {
|
|
1432
|
+
const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
|
|
1433
|
+
brIds.forEach((id) => current.add(id));
|
|
1434
|
+
specToBrIds.set(specId, current);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
for (const file of decisionFiles) {
|
|
1438
|
+
const text = await readFile8(file, "utf-8");
|
|
1187
1439
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1188
|
-
extractIds(text, "BR").forEach((id) => brIdsInSpecs.add(id));
|
|
1189
1440
|
}
|
|
1190
1441
|
for (const file of scenarioFiles) {
|
|
1191
|
-
const text = await
|
|
1442
|
+
const text = await readFile8(file, "utf-8");
|
|
1192
1443
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1444
|
+
const specIdsInScenario = extractIds(text, "SPEC");
|
|
1193
1445
|
const brIds = extractIds(text, "BR");
|
|
1194
|
-
brIds.forEach((id) => brIdsInScenarios.add(id));
|
|
1195
1446
|
const scIds = extractIds(text, "SC");
|
|
1196
|
-
|
|
1197
|
-
const contractIds = [
|
|
1447
|
+
const scenarioIds = [
|
|
1198
1448
|
...extractIds(text, "UI"),
|
|
1199
1449
|
...extractIds(text, "API"),
|
|
1200
1450
|
...extractIds(text, "DATA")
|
|
1201
1451
|
];
|
|
1202
|
-
|
|
1203
|
-
|
|
1452
|
+
brIds.forEach((id) => brIdsInScenarios.add(id));
|
|
1453
|
+
scIds.forEach((id) => scIdsInScenarios.add(id));
|
|
1454
|
+
scenarioIds.forEach((id) => scenarioContractIds.add(id));
|
|
1455
|
+
if (scenarioIds.length > 0) {
|
|
1204
1456
|
scIds.forEach((id) => scWithContracts.add(id));
|
|
1205
1457
|
}
|
|
1458
|
+
const unknownSpecIds = specIdsInScenario.filter((id) => !specIds.has(id));
|
|
1459
|
+
if (unknownSpecIds.length > 0) {
|
|
1460
|
+
issues.push(
|
|
1461
|
+
issue5(
|
|
1462
|
+
"QFAI-TRACE-005",
|
|
1463
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1464
|
+
"error",
|
|
1465
|
+
file,
|
|
1466
|
+
"traceability.scenarioSpecExists",
|
|
1467
|
+
unknownSpecIds
|
|
1468
|
+
)
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
const unknownBrIds = brIds.filter((id) => !brIdsInSpecs.has(id));
|
|
1472
|
+
if (unknownBrIds.length > 0) {
|
|
1473
|
+
issues.push(
|
|
1474
|
+
issue5(
|
|
1475
|
+
"QFAI-TRACE-006",
|
|
1476
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1477
|
+
"error",
|
|
1478
|
+
file,
|
|
1479
|
+
"traceability.scenarioBrExists",
|
|
1480
|
+
unknownBrIds
|
|
1481
|
+
)
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
const unknownContractIds = scenarioIds.filter((id) => !contractIds.has(id));
|
|
1485
|
+
if (unknownContractIds.length > 0) {
|
|
1486
|
+
issues.push(
|
|
1487
|
+
issue5(
|
|
1488
|
+
"QFAI-TRACE-008",
|
|
1489
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1490
|
+
", "
|
|
1491
|
+
)}`,
|
|
1492
|
+
config.validation.traceability.unknownContractIdSeverity,
|
|
1493
|
+
file,
|
|
1494
|
+
"traceability.scenarioContractExists",
|
|
1495
|
+
unknownContractIds
|
|
1496
|
+
)
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
if (specIdsInScenario.length > 0) {
|
|
1500
|
+
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
1501
|
+
for (const specId of specIdsInScenario) {
|
|
1502
|
+
const brIdsForSpec = specToBrIds.get(specId);
|
|
1503
|
+
if (!brIdsForSpec) {
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
1507
|
+
}
|
|
1508
|
+
const invalidBrIds = brIds.filter((id) => !allowedBrIds.has(id));
|
|
1509
|
+
if (invalidBrIds.length > 0) {
|
|
1510
|
+
issues.push(
|
|
1511
|
+
issue5(
|
|
1512
|
+
"QFAI-TRACE-007",
|
|
1513
|
+
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
1514
|
+
", "
|
|
1515
|
+
)} (SPEC: ${specIdsInScenario.join(", ")})`,
|
|
1516
|
+
"error",
|
|
1517
|
+
file,
|
|
1518
|
+
"traceability.scenarioBrUnderSpec",
|
|
1519
|
+
invalidBrIds
|
|
1520
|
+
)
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1206
1524
|
}
|
|
1207
1525
|
if (upstreamIds.size === 0) {
|
|
1208
1526
|
return [
|
|
1209
|
-
|
|
1527
|
+
issue5(
|
|
1210
1528
|
"QFAI-TRACE-000",
|
|
1211
1529
|
"\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1212
1530
|
"info",
|
|
@@ -1221,7 +1539,7 @@ async function validateTraceability(root, config) {
|
|
|
1221
1539
|
);
|
|
1222
1540
|
if (orphanBrIds.length > 0) {
|
|
1223
1541
|
issues.push(
|
|
1224
|
-
|
|
1542
|
+
issue5(
|
|
1225
1543
|
"QFAI_TRACE_BR_ORPHAN",
|
|
1226
1544
|
`BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
|
|
1227
1545
|
"error",
|
|
@@ -1238,7 +1556,7 @@ async function validateTraceability(root, config) {
|
|
|
1238
1556
|
);
|
|
1239
1557
|
if (scWithoutContracts.length > 0) {
|
|
1240
1558
|
issues.push(
|
|
1241
|
-
|
|
1559
|
+
issue5(
|
|
1242
1560
|
"QFAI_TRACE_SC_NO_CONTRACT",
|
|
1243
1561
|
`SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
|
|
1244
1562
|
", "
|
|
@@ -1252,14 +1570,13 @@ async function validateTraceability(root, config) {
|
|
|
1252
1570
|
}
|
|
1253
1571
|
}
|
|
1254
1572
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
1255
|
-
const contractIds = await collectContractIds(root, config);
|
|
1256
1573
|
if (contractIds.size > 0) {
|
|
1257
1574
|
const orphanContracts = Array.from(contractIds).filter(
|
|
1258
1575
|
(id) => !scenarioContractIds.has(id)
|
|
1259
1576
|
);
|
|
1260
1577
|
if (orphanContracts.length > 0) {
|
|
1261
1578
|
issues.push(
|
|
1262
|
-
|
|
1579
|
+
issue5(
|
|
1263
1580
|
"QFAI_CONTRACT_ORPHAN",
|
|
1264
1581
|
`\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
1265
1582
|
"error",
|
|
@@ -1276,27 +1593,6 @@ async function validateTraceability(root, config) {
|
|
|
1276
1593
|
);
|
|
1277
1594
|
return issues;
|
|
1278
1595
|
}
|
|
1279
|
-
async function collectContractIds(root, config) {
|
|
1280
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
1281
|
-
const uiRoot = resolvePath(root, config, "uiContractsDir");
|
|
1282
|
-
const apiRoot = resolvePath(root, config, "apiContractsDir");
|
|
1283
|
-
const dataRoot = resolvePath(root, config, "dataContractsDir");
|
|
1284
|
-
const uiFiles = await collectUiContractFiles(uiRoot);
|
|
1285
|
-
const apiFiles = await collectApiContractFiles(apiRoot);
|
|
1286
|
-
const dataFiles = await collectDataContractFiles(dataRoot);
|
|
1287
|
-
await collectIdsFromFiles(uiFiles, ["UI"], contractIds);
|
|
1288
|
-
await collectIdsFromFiles(apiFiles, ["API"], contractIds);
|
|
1289
|
-
await collectIdsFromFiles(dataFiles, ["DATA"], contractIds);
|
|
1290
|
-
return contractIds;
|
|
1291
|
-
}
|
|
1292
|
-
async function collectIdsFromFiles(files, prefixes, out) {
|
|
1293
|
-
for (const file of files) {
|
|
1294
|
-
const text = await readFile6(file, "utf-8");
|
|
1295
|
-
for (const prefix of prefixes) {
|
|
1296
|
-
extractIds(text, prefix).forEach((id) => out.add(id));
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
1596
|
async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
1301
1597
|
const issues = [];
|
|
1302
1598
|
const codeFiles = await collectFiles(srcRoot, {
|
|
@@ -1308,7 +1604,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1308
1604
|
const targetFiles = [...codeFiles, ...testFiles];
|
|
1309
1605
|
if (targetFiles.length === 0) {
|
|
1310
1606
|
issues.push(
|
|
1311
|
-
|
|
1607
|
+
issue5(
|
|
1312
1608
|
"QFAI-TRACE-001",
|
|
1313
1609
|
"\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1314
1610
|
"info",
|
|
@@ -1321,7 +1617,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1321
1617
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
1322
1618
|
let found = false;
|
|
1323
1619
|
for (const file of targetFiles) {
|
|
1324
|
-
const text = await
|
|
1620
|
+
const text = await readFile8(file, "utf-8");
|
|
1325
1621
|
if (pattern.test(text)) {
|
|
1326
1622
|
found = true;
|
|
1327
1623
|
break;
|
|
@@ -1329,7 +1625,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1329
1625
|
}
|
|
1330
1626
|
if (!found) {
|
|
1331
1627
|
issues.push(
|
|
1332
|
-
|
|
1628
|
+
issue5(
|
|
1333
1629
|
"QFAI-TRACE-002",
|
|
1334
1630
|
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
1335
1631
|
"warning",
|
|
@@ -1344,22 +1640,22 @@ function buildIdPattern(ids) {
|
|
|
1344
1640
|
const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
1345
1641
|
return new RegExp(`\\b(${escaped.join("|")})\\b`);
|
|
1346
1642
|
}
|
|
1347
|
-
function
|
|
1348
|
-
const
|
|
1643
|
+
function issue5(code, message, severity, file, rule, refs) {
|
|
1644
|
+
const issue6 = {
|
|
1349
1645
|
code,
|
|
1350
1646
|
severity,
|
|
1351
1647
|
message
|
|
1352
1648
|
};
|
|
1353
1649
|
if (file) {
|
|
1354
|
-
|
|
1650
|
+
issue6.file = file;
|
|
1355
1651
|
}
|
|
1356
1652
|
if (rule) {
|
|
1357
|
-
|
|
1653
|
+
issue6.rule = rule;
|
|
1358
1654
|
}
|
|
1359
1655
|
if (refs && refs.length > 0) {
|
|
1360
|
-
|
|
1656
|
+
issue6.refs = refs;
|
|
1361
1657
|
}
|
|
1362
|
-
return
|
|
1658
|
+
return issue6;
|
|
1363
1659
|
}
|
|
1364
1660
|
|
|
1365
1661
|
// src/core/validate.ts
|
|
@@ -1371,6 +1667,7 @@ async function validateProject(root, configResult) {
|
|
|
1371
1667
|
...await validateSpecs(root, config),
|
|
1372
1668
|
...await validateScenarios(root, config),
|
|
1373
1669
|
...await validateContracts(root, config),
|
|
1670
|
+
...await validateDefinedIds(root, config),
|
|
1374
1671
|
...await validateTraceability(root, config)
|
|
1375
1672
|
];
|
|
1376
1673
|
const toolVersion = await resolveToolVersion();
|
|
@@ -1383,8 +1680,8 @@ async function validateProject(root, configResult) {
|
|
|
1383
1680
|
}
|
|
1384
1681
|
function countIssues(issues) {
|
|
1385
1682
|
return issues.reduce(
|
|
1386
|
-
(acc,
|
|
1387
|
-
acc[
|
|
1683
|
+
(acc, issue6) => {
|
|
1684
|
+
acc[issue6.severity] += 1;
|
|
1388
1685
|
return acc;
|
|
1389
1686
|
},
|
|
1390
1687
|
{ info: 0, warning: 0, error: 0 }
|
|
@@ -1392,7 +1689,7 @@ function countIssues(issues) {
|
|
|
1392
1689
|
}
|
|
1393
1690
|
|
|
1394
1691
|
// src/core/report.ts
|
|
1395
|
-
var
|
|
1692
|
+
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
|
|
1396
1693
|
async function createReportData(root, validation, configResult) {
|
|
1397
1694
|
const resolved = configResult ?? await loadConfig(root);
|
|
1398
1695
|
const config = resolved.config;
|
|
@@ -1400,7 +1697,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1400
1697
|
const specRoot = resolvePath(root, config, "specDir");
|
|
1401
1698
|
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1402
1699
|
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1403
|
-
const rulesRoot = resolvePath(root, config, "rulesDir");
|
|
1404
1700
|
const apiRoot = resolvePath(root, config, "apiContractsDir");
|
|
1405
1701
|
const uiRoot = resolvePath(root, config, "uiContractsDir");
|
|
1406
1702
|
const dbRoot = resolvePath(root, config, "dataContractsDir");
|
|
@@ -1413,7 +1709,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1413
1709
|
const decisionFiles = await collectFiles(decisionsRoot, {
|
|
1414
1710
|
extensions: [".md"]
|
|
1415
1711
|
});
|
|
1416
|
-
const ruleFiles = await collectFiles(rulesRoot, { extensions: [".md"] });
|
|
1417
1712
|
const {
|
|
1418
1713
|
api: apiFiles,
|
|
1419
1714
|
ui: uiFiles,
|
|
@@ -1423,7 +1718,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1423
1718
|
...specFiles,
|
|
1424
1719
|
...scenarioFiles,
|
|
1425
1720
|
...decisionFiles,
|
|
1426
|
-
...ruleFiles,
|
|
1427
1721
|
...apiFiles,
|
|
1428
1722
|
...uiFiles,
|
|
1429
1723
|
...dbFiles
|
|
@@ -1449,7 +1743,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1449
1743
|
specs: specFiles.length,
|
|
1450
1744
|
scenarios: scenarioFiles.length,
|
|
1451
1745
|
decisions: decisionFiles.length,
|
|
1452
|
-
rules: ruleFiles.length,
|
|
1453
1746
|
contracts: {
|
|
1454
1747
|
api: apiFiles.length,
|
|
1455
1748
|
ui: uiFiles.length,
|
|
@@ -1484,7 +1777,6 @@ function formatReportMarkdown(data) {
|
|
|
1484
1777
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
1485
1778
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
1486
1779
|
lines.push(`- decisions: ${data.summary.decisions}`);
|
|
1487
|
-
lines.push(`- rules: ${data.summary.rules}`);
|
|
1488
1780
|
lines.push(
|
|
1489
1781
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
1490
1782
|
);
|
|
@@ -1520,7 +1812,7 @@ function formatReportMarkdown(data) {
|
|
|
1520
1812
|
lines.push("");
|
|
1521
1813
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
1522
1814
|
const traceIssues = data.issues.filter(
|
|
1523
|
-
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code === "QFAI_CONTRACT_ORPHAN"
|
|
1815
|
+
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-") || item.code === "QFAI_CONTRACT_ORPHAN"
|
|
1524
1816
|
);
|
|
1525
1817
|
if (traceIssues.length === 0) {
|
|
1526
1818
|
lines.push("- (none)");
|
|
@@ -1560,8 +1852,8 @@ async function collectIds(files) {
|
|
|
1560
1852
|
DATA: /* @__PURE__ */ new Set()
|
|
1561
1853
|
};
|
|
1562
1854
|
for (const file of files) {
|
|
1563
|
-
const text = await
|
|
1564
|
-
for (const prefix of
|
|
1855
|
+
const text = await readFile9(file, "utf-8");
|
|
1856
|
+
for (const prefix of ID_PREFIXES2) {
|
|
1565
1857
|
const ids = extractIds(text, prefix);
|
|
1566
1858
|
ids.forEach((id) => result[prefix].add(id));
|
|
1567
1859
|
}
|
|
@@ -1578,7 +1870,7 @@ async function collectIds(files) {
|
|
|
1578
1870
|
async function collectUpstreamIds(files) {
|
|
1579
1871
|
const ids = /* @__PURE__ */ new Set();
|
|
1580
1872
|
for (const file of files) {
|
|
1581
|
-
const text = await
|
|
1873
|
+
const text = await readFile9(file, "utf-8");
|
|
1582
1874
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
1583
1875
|
}
|
|
1584
1876
|
return ids;
|
|
@@ -1599,7 +1891,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
1599
1891
|
}
|
|
1600
1892
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
1601
1893
|
for (const file of targetFiles) {
|
|
1602
|
-
const text = await
|
|
1894
|
+
const text = await readFile9(file, "utf-8");
|
|
1603
1895
|
if (pattern.test(text)) {
|
|
1604
1896
|
return true;
|
|
1605
1897
|
}
|
|
@@ -1621,20 +1913,20 @@ function toSortedArray(values) {
|
|
|
1621
1913
|
}
|
|
1622
1914
|
function buildHotspots(issues) {
|
|
1623
1915
|
const map = /* @__PURE__ */ new Map();
|
|
1624
|
-
for (const
|
|
1625
|
-
if (!
|
|
1916
|
+
for (const issue6 of issues) {
|
|
1917
|
+
if (!issue6.file) {
|
|
1626
1918
|
continue;
|
|
1627
1919
|
}
|
|
1628
|
-
const current = map.get(
|
|
1629
|
-
file:
|
|
1920
|
+
const current = map.get(issue6.file) ?? {
|
|
1921
|
+
file: issue6.file,
|
|
1630
1922
|
total: 0,
|
|
1631
1923
|
error: 0,
|
|
1632
1924
|
warning: 0,
|
|
1633
1925
|
info: 0
|
|
1634
1926
|
};
|
|
1635
1927
|
current.total += 1;
|
|
1636
|
-
current[
|
|
1637
|
-
map.set(
|
|
1928
|
+
current[issue6.severity] += 1;
|
|
1929
|
+
map.set(issue6.file, current);
|
|
1638
1930
|
}
|
|
1639
1931
|
return Array.from(map.values()).sort(
|
|
1640
1932
|
(a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
|
|
@@ -1643,10 +1935,10 @@ function buildHotspots(issues) {
|
|
|
1643
1935
|
|
|
1644
1936
|
// src/cli/commands/report.ts
|
|
1645
1937
|
async function runReport(options) {
|
|
1646
|
-
const root =
|
|
1938
|
+
const root = path10.resolve(options.root);
|
|
1647
1939
|
const configResult = await loadConfig(root);
|
|
1648
1940
|
const input = options.jsonPath ?? configResult.config.output.jsonPath;
|
|
1649
|
-
const inputPath =
|
|
1941
|
+
const inputPath = path10.isAbsolute(input) ? input : path10.resolve(root, input);
|
|
1650
1942
|
let validation;
|
|
1651
1943
|
try {
|
|
1652
1944
|
validation = await readValidationResult(inputPath);
|
|
@@ -1671,8 +1963,8 @@ async function runReport(options) {
|
|
|
1671
1963
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
1672
1964
|
const defaultOut = options.format === "json" ? ".qfai/out/report.json" : ".qfai/out/report.md";
|
|
1673
1965
|
const out = options.outPath ?? defaultOut;
|
|
1674
|
-
const outPath =
|
|
1675
|
-
await mkdir2(
|
|
1966
|
+
const outPath = path10.isAbsolute(out) ? out : path10.resolve(root, out);
|
|
1967
|
+
await mkdir2(path10.dirname(outPath), { recursive: true });
|
|
1676
1968
|
await writeFile(outPath, `${output}
|
|
1677
1969
|
`, "utf-8");
|
|
1678
1970
|
info(
|
|
@@ -1681,7 +1973,7 @@ async function runReport(options) {
|
|
|
1681
1973
|
info(`wrote report: ${outPath}`);
|
|
1682
1974
|
}
|
|
1683
1975
|
async function readValidationResult(inputPath) {
|
|
1684
|
-
const raw = await
|
|
1976
|
+
const raw = await readFile10(inputPath, "utf-8");
|
|
1685
1977
|
const parsed = JSON.parse(raw);
|
|
1686
1978
|
if (!isValidationResult(parsed)) {
|
|
1687
1979
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
@@ -1697,17 +1989,17 @@ function isValidationResult(value) {
|
|
|
1697
1989
|
if (!value || typeof value !== "object") {
|
|
1698
1990
|
return false;
|
|
1699
1991
|
}
|
|
1700
|
-
const
|
|
1701
|
-
if (typeof
|
|
1992
|
+
const record2 = value;
|
|
1993
|
+
if (typeof record2.schemaVersion !== "string") {
|
|
1702
1994
|
return false;
|
|
1703
1995
|
}
|
|
1704
|
-
if (typeof
|
|
1996
|
+
if (typeof record2.toolVersion !== "string") {
|
|
1705
1997
|
return false;
|
|
1706
1998
|
}
|
|
1707
|
-
if (!Array.isArray(
|
|
1999
|
+
if (!Array.isArray(record2.issues)) {
|
|
1708
2000
|
return false;
|
|
1709
2001
|
}
|
|
1710
|
-
const counts =
|
|
2002
|
+
const counts = record2.counts;
|
|
1711
2003
|
if (!counts) {
|
|
1712
2004
|
return false;
|
|
1713
2005
|
}
|
|
@@ -1717,13 +2009,13 @@ function isMissingFileError(error2) {
|
|
|
1717
2009
|
if (!error2 || typeof error2 !== "object") {
|
|
1718
2010
|
return false;
|
|
1719
2011
|
}
|
|
1720
|
-
const
|
|
1721
|
-
return
|
|
2012
|
+
const record2 = error2;
|
|
2013
|
+
return record2.code === "ENOENT";
|
|
1722
2014
|
}
|
|
1723
2015
|
|
|
1724
2016
|
// src/cli/commands/validate.ts
|
|
1725
2017
|
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
1726
|
-
import
|
|
2018
|
+
import path11 from "path";
|
|
1727
2019
|
|
|
1728
2020
|
// src/cli/lib/failOn.ts
|
|
1729
2021
|
function shouldFail(result, failOn) {
|
|
@@ -1738,7 +2030,7 @@ function shouldFail(result, failOn) {
|
|
|
1738
2030
|
|
|
1739
2031
|
// src/cli/commands/validate.ts
|
|
1740
2032
|
async function runValidate(options) {
|
|
1741
|
-
const root =
|
|
2033
|
+
const root = path11.resolve(options.root);
|
|
1742
2034
|
const configResult = await loadConfig(root);
|
|
1743
2035
|
const result = await validateProject(root, configResult);
|
|
1744
2036
|
const format = options.format ?? configResult.config.output.format;
|
|
@@ -1782,20 +2074,20 @@ function emitText(result) {
|
|
|
1782
2074
|
`
|
|
1783
2075
|
);
|
|
1784
2076
|
}
|
|
1785
|
-
function emitGitHub(
|
|
1786
|
-
const level =
|
|
1787
|
-
const file =
|
|
1788
|
-
const line =
|
|
1789
|
-
const column =
|
|
2077
|
+
function emitGitHub(issue6) {
|
|
2078
|
+
const level = issue6.severity === "error" ? "error" : issue6.severity === "warning" ? "warning" : "notice";
|
|
2079
|
+
const file = issue6.file ? `file=${issue6.file}` : "";
|
|
2080
|
+
const line = issue6.loc?.line ? `,line=${issue6.loc.line}` : "";
|
|
2081
|
+
const column = issue6.loc?.column ? `,col=${issue6.loc.column}` : "";
|
|
1790
2082
|
const location = file ? ` ${file}${line}${column}` : "";
|
|
1791
2083
|
process.stdout.write(
|
|
1792
|
-
`::${level}${location}::${
|
|
2084
|
+
`::${level}${location}::${issue6.code}: ${issue6.message}
|
|
1793
2085
|
`
|
|
1794
2086
|
);
|
|
1795
2087
|
}
|
|
1796
2088
|
async function emitJson(result, root, jsonPath) {
|
|
1797
|
-
const abs =
|
|
1798
|
-
await mkdir3(
|
|
2089
|
+
const abs = path11.isAbsolute(jsonPath) ? jsonPath : path11.resolve(root, jsonPath);
|
|
2090
|
+
await mkdir3(path11.dirname(abs), { recursive: true });
|
|
1799
2091
|
await writeFile2(abs, `${JSON.stringify(result, null, 2)}
|
|
1800
2092
|
`, "utf-8");
|
|
1801
2093
|
}
|