qfai 0.2.6 → 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/contracts/README.md +6 -4
- package/assets/init/.qfai/spec/README.md +6 -3
- package/assets/init/.qfai/spec/decisions/README.md +1 -1
- package/assets/init/root/qfai.config.yaml +1 -1
- package/dist/cli/index.cjs +485 -193
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +482 -190
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +460 -166
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -4
- package/dist/index.d.ts +7 -4
- package/dist/index.mjs +459 -166
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -44,6 +44,7 @@ __export(src_exports, {
|
|
|
44
44
|
resolvePath: () => resolvePath,
|
|
45
45
|
resolveToolVersion: () => resolveToolVersion,
|
|
46
46
|
validateContracts: () => validateContracts,
|
|
47
|
+
validateDefinedIds: () => validateDefinedIds,
|
|
47
48
|
validateProject: () => validateProject,
|
|
48
49
|
validateScenarioContent: () => validateScenarioContent,
|
|
49
50
|
validateScenarios: () => validateScenarios,
|
|
@@ -62,7 +63,6 @@ var defaultConfig = {
|
|
|
62
63
|
specDir: ".qfai/spec",
|
|
63
64
|
decisionsDir: ".qfai/spec/decisions",
|
|
64
65
|
scenariosDir: ".qfai/spec/scenarios",
|
|
65
|
-
rulesDir: ".qfai/rules",
|
|
66
66
|
contractsDir: ".qfai/contracts",
|
|
67
67
|
uiContractsDir: ".qfai/contracts/ui",
|
|
68
68
|
apiContractsDir: ".qfai/contracts/api",
|
|
@@ -86,7 +86,8 @@ var defaultConfig = {
|
|
|
86
86
|
traceability: {
|
|
87
87
|
brMustHaveSc: true,
|
|
88
88
|
scMustTouchContracts: true,
|
|
89
|
-
allowOrphanContracts: false
|
|
89
|
+
allowOrphanContracts: false,
|
|
90
|
+
unknownContractIdSeverity: "error"
|
|
90
91
|
}
|
|
91
92
|
},
|
|
92
93
|
output: {
|
|
@@ -161,13 +162,6 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
161
162
|
configPath,
|
|
162
163
|
issues
|
|
163
164
|
),
|
|
164
|
-
rulesDir: readString(
|
|
165
|
-
raw.rulesDir,
|
|
166
|
-
base.rulesDir,
|
|
167
|
-
"paths.rulesDir",
|
|
168
|
-
configPath,
|
|
169
|
-
issues
|
|
170
|
-
),
|
|
171
165
|
contractsDir: readString(
|
|
172
166
|
raw.contractsDir,
|
|
173
167
|
base.contractsDir,
|
|
@@ -292,6 +286,13 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
292
286
|
"validation.traceability.allowOrphanContracts",
|
|
293
287
|
configPath,
|
|
294
288
|
issues
|
|
289
|
+
),
|
|
290
|
+
unknownContractIdSeverity: readTraceabilitySeverity(
|
|
291
|
+
traceabilityRaw?.unknownContractIdSeverity,
|
|
292
|
+
base.traceability.unknownContractIdSeverity,
|
|
293
|
+
"validation.traceability.unknownContractIdSeverity",
|
|
294
|
+
configPath,
|
|
295
|
+
issues
|
|
295
296
|
)
|
|
296
297
|
}
|
|
297
298
|
};
|
|
@@ -371,6 +372,20 @@ function readFailOn(value, fallback, label, configPath, issues) {
|
|
|
371
372
|
}
|
|
372
373
|
return fallback;
|
|
373
374
|
}
|
|
375
|
+
function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
376
|
+
if (value === "warning" || value === "error") {
|
|
377
|
+
return value;
|
|
378
|
+
}
|
|
379
|
+
if (value !== void 0) {
|
|
380
|
+
issues.push(
|
|
381
|
+
configIssue(
|
|
382
|
+
configPath,
|
|
383
|
+
`${label} \u306F warning|error \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
384
|
+
)
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
return fallback;
|
|
388
|
+
}
|
|
374
389
|
function readOutputFormat(value, fallback, label, configPath, issues) {
|
|
375
390
|
if (value === "text" || value === "json" || value === "github") {
|
|
376
391
|
return value;
|
|
@@ -411,13 +426,15 @@ function isRecord(value) {
|
|
|
411
426
|
}
|
|
412
427
|
|
|
413
428
|
// src/core/ids.ts
|
|
414
|
-
var
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
429
|
+
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
|
|
430
|
+
var STRICT_ID_PATTERNS = {
|
|
431
|
+
SPEC: /\bSPEC-\d{4}\b/g,
|
|
432
|
+
BR: /\bBR-\d{4}\b/g,
|
|
433
|
+
SC: /\bSC-\d{4}\b/g,
|
|
434
|
+
UI: /\bUI-\d{4}\b/g,
|
|
435
|
+
API: /\bAPI-\d{4}\b/g,
|
|
436
|
+
DATA: /\bDATA-\d{4}\b/g,
|
|
437
|
+
ADR: /\bADR-\d{4}\b/g
|
|
421
438
|
};
|
|
422
439
|
var LOOSE_ID_PATTERNS = {
|
|
423
440
|
SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
|
|
@@ -425,16 +442,17 @@ var LOOSE_ID_PATTERNS = {
|
|
|
425
442
|
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
426
443
|
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
427
444
|
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
428
|
-
DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi
|
|
445
|
+
DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi,
|
|
446
|
+
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
429
447
|
};
|
|
430
448
|
function extractIds(text, prefix) {
|
|
431
|
-
const pattern =
|
|
449
|
+
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
432
450
|
const matches = text.match(pattern);
|
|
433
451
|
return unique(matches ?? []);
|
|
434
452
|
}
|
|
435
453
|
function extractAllIds(text) {
|
|
436
454
|
const all = [];
|
|
437
|
-
|
|
455
|
+
ID_PREFIXES.forEach((prefix) => {
|
|
438
456
|
all.push(...extractIds(text, prefix));
|
|
439
457
|
});
|
|
440
458
|
return unique(all);
|
|
@@ -455,13 +473,13 @@ function unique(values) {
|
|
|
455
473
|
return Array.from(new Set(values));
|
|
456
474
|
}
|
|
457
475
|
function isValidId(value, prefix) {
|
|
458
|
-
const pattern =
|
|
476
|
+
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
459
477
|
const strict = new RegExp(pattern.source);
|
|
460
478
|
return strict.test(value);
|
|
461
479
|
}
|
|
462
480
|
|
|
463
481
|
// src/core/report.ts
|
|
464
|
-
var
|
|
482
|
+
var import_promises10 = require("fs/promises");
|
|
465
483
|
|
|
466
484
|
// src/core/discovery.ts
|
|
467
485
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
@@ -557,8 +575,8 @@ var import_promises3 = require("fs/promises");
|
|
|
557
575
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
558
576
|
var import_node_url = require("url");
|
|
559
577
|
async function resolveToolVersion() {
|
|
560
|
-
if ("0.2.
|
|
561
|
-
return "0.2.
|
|
578
|
+
if ("0.2.9".length > 0) {
|
|
579
|
+
return "0.2.9";
|
|
562
580
|
}
|
|
563
581
|
try {
|
|
564
582
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -578,8 +596,50 @@ function resolvePackageJsonPath() {
|
|
|
578
596
|
|
|
579
597
|
// src/core/validators/contracts.ts
|
|
580
598
|
var import_promises4 = require("fs/promises");
|
|
599
|
+
|
|
600
|
+
// src/core/contracts.ts
|
|
581
601
|
var import_node_path5 = __toESM(require("path"), 1);
|
|
582
602
|
var import_yaml2 = require("yaml");
|
|
603
|
+
function parseStructuredContract(file, text) {
|
|
604
|
+
const ext = import_node_path5.default.extname(file).toLowerCase();
|
|
605
|
+
if (ext === ".json") {
|
|
606
|
+
return JSON.parse(text);
|
|
607
|
+
}
|
|
608
|
+
return (0, import_yaml2.parse)(text);
|
|
609
|
+
}
|
|
610
|
+
function extractUiContractIds(doc) {
|
|
611
|
+
const id = typeof doc.id === "string" ? doc.id : "";
|
|
612
|
+
return extractIds(id, "UI");
|
|
613
|
+
}
|
|
614
|
+
function extractApiContractIds(doc) {
|
|
615
|
+
const operationIds = /* @__PURE__ */ new Set();
|
|
616
|
+
collectOperationIds(doc, operationIds);
|
|
617
|
+
const ids = /* @__PURE__ */ new Set();
|
|
618
|
+
for (const operationId of operationIds) {
|
|
619
|
+
extractIds(operationId, "API").forEach((id) => ids.add(id));
|
|
620
|
+
}
|
|
621
|
+
return Array.from(ids);
|
|
622
|
+
}
|
|
623
|
+
function collectOperationIds(value, out) {
|
|
624
|
+
if (!value || typeof value !== "object") {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (Array.isArray(value)) {
|
|
628
|
+
for (const item of value) {
|
|
629
|
+
collectOperationIds(item, out);
|
|
630
|
+
}
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
634
|
+
if (key === "operationId" && typeof entry === "string") {
|
|
635
|
+
out.add(entry);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
collectOperationIds(entry, out);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/core/validators/contracts.ts
|
|
583
643
|
var SQL_DANGEROUS_PATTERNS = [
|
|
584
644
|
{ pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
|
|
585
645
|
{ pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
|
|
@@ -628,12 +688,13 @@ async function validateUiContracts(uiRoot) {
|
|
|
628
688
|
"SC",
|
|
629
689
|
"UI",
|
|
630
690
|
"API",
|
|
631
|
-
"DATA"
|
|
691
|
+
"DATA",
|
|
692
|
+
"ADR"
|
|
632
693
|
]);
|
|
633
694
|
if (invalidIds.length > 0) {
|
|
634
695
|
issues.push(
|
|
635
696
|
issue(
|
|
636
|
-
"
|
|
697
|
+
"QFAI-ID-002",
|
|
637
698
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
638
699
|
"error",
|
|
639
700
|
file,
|
|
@@ -642,30 +703,32 @@ async function validateUiContracts(uiRoot) {
|
|
|
642
703
|
)
|
|
643
704
|
);
|
|
644
705
|
}
|
|
706
|
+
let doc;
|
|
645
707
|
try {
|
|
646
|
-
|
|
647
|
-
const id = typeof doc.id === "string" ? doc.id : "";
|
|
648
|
-
if (!id.startsWith("UI-")) {
|
|
649
|
-
issues.push(
|
|
650
|
-
issue(
|
|
651
|
-
"QFAI-UI-001",
|
|
652
|
-
"UI \u5951\u7D04\u306E id \u306F UI- \u3067\u59CB\u307E\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
653
|
-
"error",
|
|
654
|
-
file,
|
|
655
|
-
"contracts.ui.id"
|
|
656
|
-
)
|
|
657
|
-
);
|
|
658
|
-
}
|
|
708
|
+
doc = parseStructuredContract(file, text);
|
|
659
709
|
} catch (error) {
|
|
660
710
|
issues.push(
|
|
661
711
|
issue(
|
|
662
|
-
"QFAI-
|
|
663
|
-
`UI
|
|
712
|
+
"QFAI-CONTRACT-001",
|
|
713
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error)})`,
|
|
664
714
|
"error",
|
|
665
715
|
file,
|
|
666
716
|
"contracts.ui.parse"
|
|
667
717
|
)
|
|
668
718
|
);
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const uiIds = extractUiContractIds(doc);
|
|
722
|
+
if (uiIds.length === 0) {
|
|
723
|
+
issues.push(
|
|
724
|
+
issue(
|
|
725
|
+
"QFAI-CONTRACT-002",
|
|
726
|
+
`UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
727
|
+
"error",
|
|
728
|
+
file,
|
|
729
|
+
"contracts.ui.id"
|
|
730
|
+
)
|
|
731
|
+
);
|
|
669
732
|
}
|
|
670
733
|
}
|
|
671
734
|
return issues;
|
|
@@ -692,12 +755,13 @@ async function validateApiContracts(apiRoot) {
|
|
|
692
755
|
"SC",
|
|
693
756
|
"UI",
|
|
694
757
|
"API",
|
|
695
|
-
"DATA"
|
|
758
|
+
"DATA",
|
|
759
|
+
"ADR"
|
|
696
760
|
]);
|
|
697
761
|
if (invalidIds.length > 0) {
|
|
698
762
|
issues.push(
|
|
699
763
|
issue(
|
|
700
|
-
"
|
|
764
|
+
"QFAI-ID-002",
|
|
701
765
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
702
766
|
"error",
|
|
703
767
|
file,
|
|
@@ -706,29 +770,43 @@ async function validateApiContracts(apiRoot) {
|
|
|
706
770
|
)
|
|
707
771
|
);
|
|
708
772
|
}
|
|
773
|
+
let doc;
|
|
709
774
|
try {
|
|
710
|
-
|
|
711
|
-
if (!doc || !hasOpenApi(doc)) {
|
|
712
|
-
issues.push(
|
|
713
|
-
issue(
|
|
714
|
-
"QFAI-API-001",
|
|
715
|
-
"OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
716
|
-
"error",
|
|
717
|
-
file,
|
|
718
|
-
"contracts.api.openapi"
|
|
719
|
-
)
|
|
720
|
-
);
|
|
721
|
-
}
|
|
775
|
+
doc = parseStructuredContract(file, text);
|
|
722
776
|
} catch (error) {
|
|
723
777
|
issues.push(
|
|
724
778
|
issue(
|
|
725
|
-
"QFAI-
|
|
726
|
-
`API \
|
|
779
|
+
"QFAI-CONTRACT-001",
|
|
780
|
+
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error)})`,
|
|
727
781
|
"error",
|
|
728
782
|
file,
|
|
729
783
|
"contracts.api.parse"
|
|
730
784
|
)
|
|
731
785
|
);
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
if (!hasOpenApi(doc)) {
|
|
789
|
+
issues.push(
|
|
790
|
+
issue(
|
|
791
|
+
"QFAI-API-001",
|
|
792
|
+
"OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
793
|
+
"error",
|
|
794
|
+
file,
|
|
795
|
+
"contracts.api.openapi"
|
|
796
|
+
)
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
const apiIds = extractApiContractIds(doc);
|
|
800
|
+
if (apiIds.length === 0) {
|
|
801
|
+
issues.push(
|
|
802
|
+
issue(
|
|
803
|
+
"QFAI-CONTRACT-002",
|
|
804
|
+
`API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
805
|
+
"error",
|
|
806
|
+
file,
|
|
807
|
+
"contracts.api.id"
|
|
808
|
+
)
|
|
809
|
+
);
|
|
732
810
|
}
|
|
733
811
|
}
|
|
734
812
|
return issues;
|
|
@@ -755,12 +833,13 @@ async function validateDataContracts(dataRoot) {
|
|
|
755
833
|
"SC",
|
|
756
834
|
"UI",
|
|
757
835
|
"API",
|
|
758
|
-
"DATA"
|
|
836
|
+
"DATA",
|
|
837
|
+
"ADR"
|
|
759
838
|
]);
|
|
760
839
|
if (invalidIds.length > 0) {
|
|
761
840
|
issues.push(
|
|
762
841
|
issue(
|
|
763
|
-
"
|
|
842
|
+
"QFAI-ID-002",
|
|
764
843
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
765
844
|
"error",
|
|
766
845
|
file,
|
|
@@ -790,13 +869,6 @@ function lintSql(text, file) {
|
|
|
790
869
|
}
|
|
791
870
|
return issues;
|
|
792
871
|
}
|
|
793
|
-
function parseStructured(file, text) {
|
|
794
|
-
const ext = import_node_path5.default.extname(file).toLowerCase();
|
|
795
|
-
if (ext === ".json") {
|
|
796
|
-
return JSON.parse(text);
|
|
797
|
-
}
|
|
798
|
-
return (0, import_yaml2.parse)(text);
|
|
799
|
-
}
|
|
800
872
|
function hasOpenApi(doc) {
|
|
801
873
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
802
874
|
}
|
|
@@ -807,25 +879,165 @@ function formatError2(error) {
|
|
|
807
879
|
return String(error);
|
|
808
880
|
}
|
|
809
881
|
function issue(code, message, severity, file, rule, refs) {
|
|
810
|
-
const
|
|
882
|
+
const issue6 = {
|
|
811
883
|
code,
|
|
812
884
|
severity,
|
|
813
885
|
message
|
|
814
886
|
};
|
|
815
887
|
if (file) {
|
|
816
|
-
|
|
888
|
+
issue6.file = file;
|
|
817
889
|
}
|
|
818
890
|
if (rule) {
|
|
819
|
-
|
|
891
|
+
issue6.rule = rule;
|
|
820
892
|
}
|
|
821
893
|
if (refs && refs.length > 0) {
|
|
822
|
-
|
|
894
|
+
issue6.refs = refs;
|
|
823
895
|
}
|
|
824
|
-
return
|
|
896
|
+
return issue6;
|
|
825
897
|
}
|
|
826
898
|
|
|
827
|
-
// src/core/validators/
|
|
899
|
+
// src/core/validators/ids.ts
|
|
900
|
+
var import_promises6 = require("fs/promises");
|
|
901
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
902
|
+
|
|
903
|
+
// src/core/contractIndex.ts
|
|
828
904
|
var import_promises5 = require("fs/promises");
|
|
905
|
+
async function buildContractIndex(root, config) {
|
|
906
|
+
const uiRoot = resolvePath(root, config, "uiContractsDir");
|
|
907
|
+
const apiRoot = resolvePath(root, config, "apiContractsDir");
|
|
908
|
+
const dataRoot = resolvePath(root, config, "dataContractsDir");
|
|
909
|
+
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
910
|
+
collectUiContractFiles(uiRoot),
|
|
911
|
+
collectApiContractFiles(apiRoot),
|
|
912
|
+
collectDataContractFiles(dataRoot)
|
|
913
|
+
]);
|
|
914
|
+
const index = {
|
|
915
|
+
ids: /* @__PURE__ */ new Set(),
|
|
916
|
+
idToFiles: /* @__PURE__ */ new Map(),
|
|
917
|
+
files: { ui: uiFiles, api: apiFiles, data: dataFiles },
|
|
918
|
+
structuredParseFailedFiles: /* @__PURE__ */ new Set()
|
|
919
|
+
};
|
|
920
|
+
await indexUiContracts(uiFiles, index);
|
|
921
|
+
await indexApiContracts(apiFiles, index);
|
|
922
|
+
await indexDataContracts(dataFiles, index);
|
|
923
|
+
return index;
|
|
924
|
+
}
|
|
925
|
+
async function indexUiContracts(files, index) {
|
|
926
|
+
for (const file of files) {
|
|
927
|
+
const text = await (0, import_promises5.readFile)(file, "utf-8");
|
|
928
|
+
try {
|
|
929
|
+
const doc = parseStructuredContract(file, text);
|
|
930
|
+
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
931
|
+
} catch {
|
|
932
|
+
index.structuredParseFailedFiles.add(file);
|
|
933
|
+
extractIds(text, "UI").forEach((id) => record(index, id, file));
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
async function indexApiContracts(files, index) {
|
|
938
|
+
for (const file of files) {
|
|
939
|
+
const text = await (0, import_promises5.readFile)(file, "utf-8");
|
|
940
|
+
try {
|
|
941
|
+
const doc = parseStructuredContract(file, text);
|
|
942
|
+
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
943
|
+
} catch {
|
|
944
|
+
index.structuredParseFailedFiles.add(file);
|
|
945
|
+
extractIds(text, "API").forEach((id) => record(index, id, file));
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
async function indexDataContracts(files, index) {
|
|
950
|
+
for (const file of files) {
|
|
951
|
+
const text = await (0, import_promises5.readFile)(file, "utf-8");
|
|
952
|
+
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function record(index, id, file) {
|
|
956
|
+
index.ids.add(id);
|
|
957
|
+
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
958
|
+
current.add(file);
|
|
959
|
+
index.idToFiles.set(id, current);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// src/core/validators/ids.ts
|
|
963
|
+
async function validateDefinedIds(root, config) {
|
|
964
|
+
const issues = [];
|
|
965
|
+
const specRoot = resolvePath(root, config, "specDir");
|
|
966
|
+
const scenarioRoot = resolvePath(root, config, "scenariosDir");
|
|
967
|
+
const specFiles = await collectSpecFiles(specRoot);
|
|
968
|
+
const scenarioFiles = await collectFiles(scenarioRoot, {
|
|
969
|
+
extensions: [".feature"]
|
|
970
|
+
});
|
|
971
|
+
const defined = /* @__PURE__ */ new Map();
|
|
972
|
+
await collectSpecDefinitionIds(specFiles, defined);
|
|
973
|
+
await collectScenarioDefinitionIds(scenarioFiles, defined);
|
|
974
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
975
|
+
for (const [id, files] of contractIndex.idToFiles.entries()) {
|
|
976
|
+
for (const file of files) {
|
|
977
|
+
recordId(defined, id, file);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
for (const [id, files] of defined.entries()) {
|
|
981
|
+
if (files.size <= 1) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
const sorted = Array.from(files).sort();
|
|
985
|
+
issues.push(
|
|
986
|
+
issue2(
|
|
987
|
+
"QFAI-ID-001",
|
|
988
|
+
`ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
|
|
989
|
+
"error",
|
|
990
|
+
sorted[0],
|
|
991
|
+
"id.duplicate"
|
|
992
|
+
)
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
return issues;
|
|
996
|
+
}
|
|
997
|
+
async function collectSpecDefinitionIds(files, out) {
|
|
998
|
+
for (const file of files) {
|
|
999
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
1000
|
+
extractIds(text, "SPEC").forEach((id) => recordId(out, id, file));
|
|
1001
|
+
extractIds(text, "BR").forEach((id) => recordId(out, id, file));
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
async function collectScenarioDefinitionIds(files, out) {
|
|
1005
|
+
for (const file of files) {
|
|
1006
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
1007
|
+
extractIds(text, "SC").forEach((id) => recordId(out, id, file));
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function recordId(out, id, file) {
|
|
1011
|
+
const current = out.get(id) ?? /* @__PURE__ */ new Set();
|
|
1012
|
+
current.add(file);
|
|
1013
|
+
out.set(id, current);
|
|
1014
|
+
}
|
|
1015
|
+
function formatFileList(files, root) {
|
|
1016
|
+
return files.map((file) => {
|
|
1017
|
+
const relative = import_node_path6.default.relative(root, file);
|
|
1018
|
+
return relative.length > 0 ? relative : file;
|
|
1019
|
+
}).join(", ");
|
|
1020
|
+
}
|
|
1021
|
+
function issue2(code, message, severity, file, rule, refs) {
|
|
1022
|
+
const issue6 = {
|
|
1023
|
+
code,
|
|
1024
|
+
severity,
|
|
1025
|
+
message
|
|
1026
|
+
};
|
|
1027
|
+
if (file) {
|
|
1028
|
+
issue6.file = file;
|
|
1029
|
+
}
|
|
1030
|
+
if (rule) {
|
|
1031
|
+
issue6.rule = rule;
|
|
1032
|
+
}
|
|
1033
|
+
if (refs && refs.length > 0) {
|
|
1034
|
+
issue6.refs = refs;
|
|
1035
|
+
}
|
|
1036
|
+
return issue6;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// src/core/validators/scenario.ts
|
|
1040
|
+
var import_promises7 = require("fs/promises");
|
|
829
1041
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
830
1042
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
831
1043
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -836,7 +1048,7 @@ async function validateScenarios(root, config) {
|
|
|
836
1048
|
});
|
|
837
1049
|
if (files.length === 0) {
|
|
838
1050
|
return [
|
|
839
|
-
|
|
1051
|
+
issue3(
|
|
840
1052
|
"QFAI-SC-000",
|
|
841
1053
|
"Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
842
1054
|
"info",
|
|
@@ -847,7 +1059,7 @@ async function validateScenarios(root, config) {
|
|
|
847
1059
|
}
|
|
848
1060
|
const issues = [];
|
|
849
1061
|
for (const file of files) {
|
|
850
|
-
const text = await (0,
|
|
1062
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
851
1063
|
issues.push(...validateScenarioContent(text, file));
|
|
852
1064
|
}
|
|
853
1065
|
return issues;
|
|
@@ -860,12 +1072,13 @@ function validateScenarioContent(text, file) {
|
|
|
860
1072
|
"SC",
|
|
861
1073
|
"UI",
|
|
862
1074
|
"API",
|
|
863
|
-
"DATA"
|
|
1075
|
+
"DATA",
|
|
1076
|
+
"ADR"
|
|
864
1077
|
]);
|
|
865
1078
|
if (invalidIds.length > 0) {
|
|
866
1079
|
issues.push(
|
|
867
|
-
|
|
868
|
-
"
|
|
1080
|
+
issue3(
|
|
1081
|
+
"QFAI-ID-002",
|
|
869
1082
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
870
1083
|
"error",
|
|
871
1084
|
file,
|
|
@@ -877,7 +1090,7 @@ function validateScenarioContent(text, file) {
|
|
|
877
1090
|
const scIds = extractIds(text, "SC");
|
|
878
1091
|
if (scIds.length === 0) {
|
|
879
1092
|
issues.push(
|
|
880
|
-
|
|
1093
|
+
issue3(
|
|
881
1094
|
"QFAI-SC-001",
|
|
882
1095
|
"SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
883
1096
|
"error",
|
|
@@ -889,7 +1102,7 @@ function validateScenarioContent(text, file) {
|
|
|
889
1102
|
const specIds = extractIds(text, "SPEC");
|
|
890
1103
|
if (specIds.length === 0) {
|
|
891
1104
|
issues.push(
|
|
892
|
-
|
|
1105
|
+
issue3(
|
|
893
1106
|
"QFAI-SC-002",
|
|
894
1107
|
"SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
895
1108
|
"error",
|
|
@@ -901,7 +1114,7 @@ function validateScenarioContent(text, file) {
|
|
|
901
1114
|
const brIds = extractIds(text, "BR");
|
|
902
1115
|
if (brIds.length === 0) {
|
|
903
1116
|
issues.push(
|
|
904
|
-
|
|
1117
|
+
issue3(
|
|
905
1118
|
"QFAI-SC-003",
|
|
906
1119
|
"SC \u306F BR \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
907
1120
|
"error",
|
|
@@ -922,7 +1135,7 @@ function validateScenarioContent(text, file) {
|
|
|
922
1135
|
}
|
|
923
1136
|
if (missingSteps.length > 0) {
|
|
924
1137
|
issues.push(
|
|
925
|
-
|
|
1138
|
+
issue3(
|
|
926
1139
|
"QFAI-SC-005",
|
|
927
1140
|
`Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
|
|
928
1141
|
"warning",
|
|
@@ -933,33 +1146,33 @@ function validateScenarioContent(text, file) {
|
|
|
933
1146
|
}
|
|
934
1147
|
return issues;
|
|
935
1148
|
}
|
|
936
|
-
function
|
|
937
|
-
const
|
|
1149
|
+
function issue3(code, message, severity, file, rule, refs) {
|
|
1150
|
+
const issue6 = {
|
|
938
1151
|
code,
|
|
939
1152
|
severity,
|
|
940
1153
|
message
|
|
941
1154
|
};
|
|
942
1155
|
if (file) {
|
|
943
|
-
|
|
1156
|
+
issue6.file = file;
|
|
944
1157
|
}
|
|
945
1158
|
if (rule) {
|
|
946
|
-
|
|
1159
|
+
issue6.rule = rule;
|
|
947
1160
|
}
|
|
948
1161
|
if (refs && refs.length > 0) {
|
|
949
|
-
|
|
1162
|
+
issue6.refs = refs;
|
|
950
1163
|
}
|
|
951
|
-
return
|
|
1164
|
+
return issue6;
|
|
952
1165
|
}
|
|
953
1166
|
|
|
954
1167
|
// src/core/validators/spec.ts
|
|
955
|
-
var
|
|
1168
|
+
var import_promises8 = require("fs/promises");
|
|
956
1169
|
async function validateSpecs(root, config) {
|
|
957
1170
|
const specsRoot = resolvePath(root, config, "specDir");
|
|
958
1171
|
const files = await collectSpecFiles(specsRoot);
|
|
959
1172
|
if (files.length === 0) {
|
|
960
1173
|
const expected = "spec-0001-<slug>.md";
|
|
961
1174
|
return [
|
|
962
|
-
|
|
1175
|
+
issue4(
|
|
963
1176
|
"QFAI-SPEC-000",
|
|
964
1177
|
`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}`,
|
|
965
1178
|
"info",
|
|
@@ -970,7 +1183,7 @@ async function validateSpecs(root, config) {
|
|
|
970
1183
|
}
|
|
971
1184
|
const issues = [];
|
|
972
1185
|
for (const file of files) {
|
|
973
|
-
const text = await (0,
|
|
1186
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
974
1187
|
issues.push(
|
|
975
1188
|
...validateSpecContent(
|
|
976
1189
|
text,
|
|
@@ -989,12 +1202,13 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
989
1202
|
"SC",
|
|
990
1203
|
"UI",
|
|
991
1204
|
"API",
|
|
992
|
-
"DATA"
|
|
1205
|
+
"DATA",
|
|
1206
|
+
"ADR"
|
|
993
1207
|
]);
|
|
994
1208
|
if (invalidIds.length > 0) {
|
|
995
1209
|
issues.push(
|
|
996
|
-
|
|
997
|
-
"
|
|
1210
|
+
issue4(
|
|
1211
|
+
"QFAI-ID-002",
|
|
998
1212
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
999
1213
|
"error",
|
|
1000
1214
|
file,
|
|
@@ -1006,7 +1220,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1006
1220
|
const specIds = extractIds(text, "SPEC");
|
|
1007
1221
|
if (specIds.length === 0) {
|
|
1008
1222
|
issues.push(
|
|
1009
|
-
|
|
1223
|
+
issue4(
|
|
1010
1224
|
"QFAI-SPEC-001",
|
|
1011
1225
|
"SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1012
1226
|
"error",
|
|
@@ -1018,7 +1232,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1018
1232
|
const brIds = extractIds(text, "BR");
|
|
1019
1233
|
if (brIds.length === 0) {
|
|
1020
1234
|
issues.push(
|
|
1021
|
-
|
|
1235
|
+
issue4(
|
|
1022
1236
|
"QFAI-SPEC-002",
|
|
1023
1237
|
"BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1024
1238
|
"error",
|
|
@@ -1030,7 +1244,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1030
1244
|
const scIds = extractIds(text, "SC");
|
|
1031
1245
|
if (scIds.length > 0) {
|
|
1032
1246
|
issues.push(
|
|
1033
|
-
|
|
1247
|
+
issue4(
|
|
1034
1248
|
"QFAI-SPEC-003",
|
|
1035
1249
|
"Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
|
|
1036
1250
|
"warning",
|
|
@@ -1043,7 +1257,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1043
1257
|
for (const section of requiredSections) {
|
|
1044
1258
|
if (!text.includes(section)) {
|
|
1045
1259
|
issues.push(
|
|
1046
|
-
|
|
1260
|
+
issue4(
|
|
1047
1261
|
"QFAI-SPEC-004",
|
|
1048
1262
|
`\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
|
|
1049
1263
|
"error",
|
|
@@ -1055,26 +1269,26 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1055
1269
|
}
|
|
1056
1270
|
return issues;
|
|
1057
1271
|
}
|
|
1058
|
-
function
|
|
1059
|
-
const
|
|
1272
|
+
function issue4(code, message, severity, file, rule, refs) {
|
|
1273
|
+
const issue6 = {
|
|
1060
1274
|
code,
|
|
1061
1275
|
severity,
|
|
1062
1276
|
message
|
|
1063
1277
|
};
|
|
1064
1278
|
if (file) {
|
|
1065
|
-
|
|
1279
|
+
issue6.file = file;
|
|
1066
1280
|
}
|
|
1067
1281
|
if (rule) {
|
|
1068
|
-
|
|
1282
|
+
issue6.rule = rule;
|
|
1069
1283
|
}
|
|
1070
1284
|
if (refs && refs.length > 0) {
|
|
1071
|
-
|
|
1285
|
+
issue6.refs = refs;
|
|
1072
1286
|
}
|
|
1073
|
-
return
|
|
1287
|
+
return issue6;
|
|
1074
1288
|
}
|
|
1075
1289
|
|
|
1076
1290
|
// src/core/validators/traceability.ts
|
|
1077
|
-
var
|
|
1291
|
+
var import_promises9 = require("fs/promises");
|
|
1078
1292
|
async function validateTraceability(root, config) {
|
|
1079
1293
|
const issues = [];
|
|
1080
1294
|
const specsRoot = resolvePath(root, config, "specDir");
|
|
@@ -1090,36 +1304,141 @@ async function validateTraceability(root, config) {
|
|
|
1090
1304
|
extensions: [".feature"]
|
|
1091
1305
|
});
|
|
1092
1306
|
const upstreamIds = /* @__PURE__ */ new Set();
|
|
1307
|
+
const specIds = /* @__PURE__ */ new Set();
|
|
1093
1308
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
1094
1309
|
const brIdsInScenarios = /* @__PURE__ */ new Set();
|
|
1095
1310
|
const scIdsInScenarios = /* @__PURE__ */ new Set();
|
|
1096
1311
|
const scenarioContractIds = /* @__PURE__ */ new Set();
|
|
1097
1312
|
const scWithContracts = /* @__PURE__ */ new Set();
|
|
1098
|
-
|
|
1099
|
-
|
|
1313
|
+
const specToBrIds = /* @__PURE__ */ new Map();
|
|
1314
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
1315
|
+
const contractIds = contractIndex.ids;
|
|
1316
|
+
for (const file of specFiles) {
|
|
1317
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1318
|
+
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1319
|
+
const specIdsInFile = extractIds(text, "SPEC");
|
|
1320
|
+
specIdsInFile.forEach((id) => specIds.add(id));
|
|
1321
|
+
const brIds = extractIds(text, "BR");
|
|
1322
|
+
brIds.forEach((id) => brIdsInSpecs.add(id));
|
|
1323
|
+
const referencedContractIds = /* @__PURE__ */ new Set([
|
|
1324
|
+
...extractIds(text, "UI"),
|
|
1325
|
+
...extractIds(text, "API"),
|
|
1326
|
+
...extractIds(text, "DATA")
|
|
1327
|
+
]);
|
|
1328
|
+
const unknownContractIds = Array.from(referencedContractIds).filter(
|
|
1329
|
+
(id) => !contractIds.has(id)
|
|
1330
|
+
);
|
|
1331
|
+
if (unknownContractIds.length > 0) {
|
|
1332
|
+
issues.push(
|
|
1333
|
+
issue5(
|
|
1334
|
+
"QFAI-TRACE-009",
|
|
1335
|
+
`Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1336
|
+
", "
|
|
1337
|
+
)}`,
|
|
1338
|
+
"error",
|
|
1339
|
+
file,
|
|
1340
|
+
"traceability.specContractExists",
|
|
1341
|
+
unknownContractIds
|
|
1342
|
+
)
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
for (const specId of specIdsInFile) {
|
|
1346
|
+
const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
|
|
1347
|
+
brIds.forEach((id) => current.add(id));
|
|
1348
|
+
specToBrIds.set(specId, current);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
for (const file of decisionFiles) {
|
|
1352
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1100
1353
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1101
|
-
extractIds(text, "BR").forEach((id) => brIdsInSpecs.add(id));
|
|
1102
1354
|
}
|
|
1103
1355
|
for (const file of scenarioFiles) {
|
|
1104
|
-
const text = await (0,
|
|
1356
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1105
1357
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1358
|
+
const specIdsInScenario = extractIds(text, "SPEC");
|
|
1106
1359
|
const brIds = extractIds(text, "BR");
|
|
1107
|
-
brIds.forEach((id) => brIdsInScenarios.add(id));
|
|
1108
1360
|
const scIds = extractIds(text, "SC");
|
|
1109
|
-
|
|
1110
|
-
const contractIds = [
|
|
1361
|
+
const scenarioIds = [
|
|
1111
1362
|
...extractIds(text, "UI"),
|
|
1112
1363
|
...extractIds(text, "API"),
|
|
1113
1364
|
...extractIds(text, "DATA")
|
|
1114
1365
|
];
|
|
1115
|
-
|
|
1116
|
-
|
|
1366
|
+
brIds.forEach((id) => brIdsInScenarios.add(id));
|
|
1367
|
+
scIds.forEach((id) => scIdsInScenarios.add(id));
|
|
1368
|
+
scenarioIds.forEach((id) => scenarioContractIds.add(id));
|
|
1369
|
+
if (scenarioIds.length > 0) {
|
|
1117
1370
|
scIds.forEach((id) => scWithContracts.add(id));
|
|
1118
1371
|
}
|
|
1372
|
+
const unknownSpecIds = specIdsInScenario.filter((id) => !specIds.has(id));
|
|
1373
|
+
if (unknownSpecIds.length > 0) {
|
|
1374
|
+
issues.push(
|
|
1375
|
+
issue5(
|
|
1376
|
+
"QFAI-TRACE-005",
|
|
1377
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1378
|
+
"error",
|
|
1379
|
+
file,
|
|
1380
|
+
"traceability.scenarioSpecExists",
|
|
1381
|
+
unknownSpecIds
|
|
1382
|
+
)
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
const unknownBrIds = brIds.filter((id) => !brIdsInSpecs.has(id));
|
|
1386
|
+
if (unknownBrIds.length > 0) {
|
|
1387
|
+
issues.push(
|
|
1388
|
+
issue5(
|
|
1389
|
+
"QFAI-TRACE-006",
|
|
1390
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1391
|
+
"error",
|
|
1392
|
+
file,
|
|
1393
|
+
"traceability.scenarioBrExists",
|
|
1394
|
+
unknownBrIds
|
|
1395
|
+
)
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
const unknownContractIds = scenarioIds.filter((id) => !contractIds.has(id));
|
|
1399
|
+
if (unknownContractIds.length > 0) {
|
|
1400
|
+
issues.push(
|
|
1401
|
+
issue5(
|
|
1402
|
+
"QFAI-TRACE-008",
|
|
1403
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1404
|
+
", "
|
|
1405
|
+
)}`,
|
|
1406
|
+
config.validation.traceability.unknownContractIdSeverity,
|
|
1407
|
+
file,
|
|
1408
|
+
"traceability.scenarioContractExists",
|
|
1409
|
+
unknownContractIds
|
|
1410
|
+
)
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
if (specIdsInScenario.length > 0) {
|
|
1414
|
+
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
1415
|
+
for (const specId of specIdsInScenario) {
|
|
1416
|
+
const brIdsForSpec = specToBrIds.get(specId);
|
|
1417
|
+
if (!brIdsForSpec) {
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
1421
|
+
}
|
|
1422
|
+
const invalidBrIds = brIds.filter((id) => !allowedBrIds.has(id));
|
|
1423
|
+
if (invalidBrIds.length > 0) {
|
|
1424
|
+
issues.push(
|
|
1425
|
+
issue5(
|
|
1426
|
+
"QFAI-TRACE-007",
|
|
1427
|
+
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
1428
|
+
", "
|
|
1429
|
+
)} (SPEC: ${specIdsInScenario.join(", ")})`,
|
|
1430
|
+
"error",
|
|
1431
|
+
file,
|
|
1432
|
+
"traceability.scenarioBrUnderSpec",
|
|
1433
|
+
invalidBrIds
|
|
1434
|
+
)
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1119
1438
|
}
|
|
1120
1439
|
if (upstreamIds.size === 0) {
|
|
1121
1440
|
return [
|
|
1122
|
-
|
|
1441
|
+
issue5(
|
|
1123
1442
|
"QFAI-TRACE-000",
|
|
1124
1443
|
"\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1125
1444
|
"info",
|
|
@@ -1134,7 +1453,7 @@ async function validateTraceability(root, config) {
|
|
|
1134
1453
|
);
|
|
1135
1454
|
if (orphanBrIds.length > 0) {
|
|
1136
1455
|
issues.push(
|
|
1137
|
-
|
|
1456
|
+
issue5(
|
|
1138
1457
|
"QFAI_TRACE_BR_ORPHAN",
|
|
1139
1458
|
`BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
|
|
1140
1459
|
"error",
|
|
@@ -1151,7 +1470,7 @@ async function validateTraceability(root, config) {
|
|
|
1151
1470
|
);
|
|
1152
1471
|
if (scWithoutContracts.length > 0) {
|
|
1153
1472
|
issues.push(
|
|
1154
|
-
|
|
1473
|
+
issue5(
|
|
1155
1474
|
"QFAI_TRACE_SC_NO_CONTRACT",
|
|
1156
1475
|
`SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
|
|
1157
1476
|
", "
|
|
@@ -1165,14 +1484,13 @@ async function validateTraceability(root, config) {
|
|
|
1165
1484
|
}
|
|
1166
1485
|
}
|
|
1167
1486
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
1168
|
-
const contractIds = await collectContractIds(root, config);
|
|
1169
1487
|
if (contractIds.size > 0) {
|
|
1170
1488
|
const orphanContracts = Array.from(contractIds).filter(
|
|
1171
1489
|
(id) => !scenarioContractIds.has(id)
|
|
1172
1490
|
);
|
|
1173
1491
|
if (orphanContracts.length > 0) {
|
|
1174
1492
|
issues.push(
|
|
1175
|
-
|
|
1493
|
+
issue5(
|
|
1176
1494
|
"QFAI_CONTRACT_ORPHAN",
|
|
1177
1495
|
`\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
1178
1496
|
"error",
|
|
@@ -1189,27 +1507,6 @@ async function validateTraceability(root, config) {
|
|
|
1189
1507
|
);
|
|
1190
1508
|
return issues;
|
|
1191
1509
|
}
|
|
1192
|
-
async function collectContractIds(root, config) {
|
|
1193
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
1194
|
-
const uiRoot = resolvePath(root, config, "uiContractsDir");
|
|
1195
|
-
const apiRoot = resolvePath(root, config, "apiContractsDir");
|
|
1196
|
-
const dataRoot = resolvePath(root, config, "dataContractsDir");
|
|
1197
|
-
const uiFiles = await collectUiContractFiles(uiRoot);
|
|
1198
|
-
const apiFiles = await collectApiContractFiles(apiRoot);
|
|
1199
|
-
const dataFiles = await collectDataContractFiles(dataRoot);
|
|
1200
|
-
await collectIdsFromFiles(uiFiles, ["UI"], contractIds);
|
|
1201
|
-
await collectIdsFromFiles(apiFiles, ["API"], contractIds);
|
|
1202
|
-
await collectIdsFromFiles(dataFiles, ["DATA"], contractIds);
|
|
1203
|
-
return contractIds;
|
|
1204
|
-
}
|
|
1205
|
-
async function collectIdsFromFiles(files, prefixes, out) {
|
|
1206
|
-
for (const file of files) {
|
|
1207
|
-
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
1208
|
-
for (const prefix of prefixes) {
|
|
1209
|
-
extractIds(text, prefix).forEach((id) => out.add(id));
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
1510
|
async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
1214
1511
|
const issues = [];
|
|
1215
1512
|
const codeFiles = await collectFiles(srcRoot, {
|
|
@@ -1221,7 +1518,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1221
1518
|
const targetFiles = [...codeFiles, ...testFiles];
|
|
1222
1519
|
if (targetFiles.length === 0) {
|
|
1223
1520
|
issues.push(
|
|
1224
|
-
|
|
1521
|
+
issue5(
|
|
1225
1522
|
"QFAI-TRACE-001",
|
|
1226
1523
|
"\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1227
1524
|
"info",
|
|
@@ -1234,7 +1531,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1234
1531
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
1235
1532
|
let found = false;
|
|
1236
1533
|
for (const file of targetFiles) {
|
|
1237
|
-
const text = await (0,
|
|
1534
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1238
1535
|
if (pattern.test(text)) {
|
|
1239
1536
|
found = true;
|
|
1240
1537
|
break;
|
|
@@ -1242,7 +1539,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1242
1539
|
}
|
|
1243
1540
|
if (!found) {
|
|
1244
1541
|
issues.push(
|
|
1245
|
-
|
|
1542
|
+
issue5(
|
|
1246
1543
|
"QFAI-TRACE-002",
|
|
1247
1544
|
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
1248
1545
|
"warning",
|
|
@@ -1257,22 +1554,22 @@ function buildIdPattern(ids) {
|
|
|
1257
1554
|
const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
1258
1555
|
return new RegExp(`\\b(${escaped.join("|")})\\b`);
|
|
1259
1556
|
}
|
|
1260
|
-
function
|
|
1261
|
-
const
|
|
1557
|
+
function issue5(code, message, severity, file, rule, refs) {
|
|
1558
|
+
const issue6 = {
|
|
1262
1559
|
code,
|
|
1263
1560
|
severity,
|
|
1264
1561
|
message
|
|
1265
1562
|
};
|
|
1266
1563
|
if (file) {
|
|
1267
|
-
|
|
1564
|
+
issue6.file = file;
|
|
1268
1565
|
}
|
|
1269
1566
|
if (rule) {
|
|
1270
|
-
|
|
1567
|
+
issue6.rule = rule;
|
|
1271
1568
|
}
|
|
1272
1569
|
if (refs && refs.length > 0) {
|
|
1273
|
-
|
|
1570
|
+
issue6.refs = refs;
|
|
1274
1571
|
}
|
|
1275
|
-
return
|
|
1572
|
+
return issue6;
|
|
1276
1573
|
}
|
|
1277
1574
|
|
|
1278
1575
|
// src/core/validate.ts
|
|
@@ -1284,6 +1581,7 @@ async function validateProject(root, configResult) {
|
|
|
1284
1581
|
...await validateSpecs(root, config),
|
|
1285
1582
|
...await validateScenarios(root, config),
|
|
1286
1583
|
...await validateContracts(root, config),
|
|
1584
|
+
...await validateDefinedIds(root, config),
|
|
1287
1585
|
...await validateTraceability(root, config)
|
|
1288
1586
|
];
|
|
1289
1587
|
const toolVersion = await resolveToolVersion();
|
|
@@ -1296,8 +1594,8 @@ async function validateProject(root, configResult) {
|
|
|
1296
1594
|
}
|
|
1297
1595
|
function countIssues(issues) {
|
|
1298
1596
|
return issues.reduce(
|
|
1299
|
-
(acc,
|
|
1300
|
-
acc[
|
|
1597
|
+
(acc, issue6) => {
|
|
1598
|
+
acc[issue6.severity] += 1;
|
|
1301
1599
|
return acc;
|
|
1302
1600
|
},
|
|
1303
1601
|
{ info: 0, warning: 0, error: 0 }
|
|
@@ -1305,7 +1603,7 @@ function countIssues(issues) {
|
|
|
1305
1603
|
}
|
|
1306
1604
|
|
|
1307
1605
|
// src/core/report.ts
|
|
1308
|
-
var
|
|
1606
|
+
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
|
|
1309
1607
|
async function createReportData(root, validation, configResult) {
|
|
1310
1608
|
const resolved = configResult ?? await loadConfig(root);
|
|
1311
1609
|
const config = resolved.config;
|
|
@@ -1313,7 +1611,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1313
1611
|
const specRoot = resolvePath(root, config, "specDir");
|
|
1314
1612
|
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1315
1613
|
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1316
|
-
const rulesRoot = resolvePath(root, config, "rulesDir");
|
|
1317
1614
|
const apiRoot = resolvePath(root, config, "apiContractsDir");
|
|
1318
1615
|
const uiRoot = resolvePath(root, config, "uiContractsDir");
|
|
1319
1616
|
const dbRoot = resolvePath(root, config, "dataContractsDir");
|
|
@@ -1326,7 +1623,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1326
1623
|
const decisionFiles = await collectFiles(decisionsRoot, {
|
|
1327
1624
|
extensions: [".md"]
|
|
1328
1625
|
});
|
|
1329
|
-
const ruleFiles = await collectFiles(rulesRoot, { extensions: [".md"] });
|
|
1330
1626
|
const {
|
|
1331
1627
|
api: apiFiles,
|
|
1332
1628
|
ui: uiFiles,
|
|
@@ -1336,7 +1632,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1336
1632
|
...specFiles,
|
|
1337
1633
|
...scenarioFiles,
|
|
1338
1634
|
...decisionFiles,
|
|
1339
|
-
...ruleFiles,
|
|
1340
1635
|
...apiFiles,
|
|
1341
1636
|
...uiFiles,
|
|
1342
1637
|
...dbFiles
|
|
@@ -1362,7 +1657,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1362
1657
|
specs: specFiles.length,
|
|
1363
1658
|
scenarios: scenarioFiles.length,
|
|
1364
1659
|
decisions: decisionFiles.length,
|
|
1365
|
-
rules: ruleFiles.length,
|
|
1366
1660
|
contracts: {
|
|
1367
1661
|
api: apiFiles.length,
|
|
1368
1662
|
ui: uiFiles.length,
|
|
@@ -1397,7 +1691,6 @@ function formatReportMarkdown(data) {
|
|
|
1397
1691
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
1398
1692
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
1399
1693
|
lines.push(`- decisions: ${data.summary.decisions}`);
|
|
1400
|
-
lines.push(`- rules: ${data.summary.rules}`);
|
|
1401
1694
|
lines.push(
|
|
1402
1695
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
1403
1696
|
);
|
|
@@ -1433,7 +1726,7 @@ function formatReportMarkdown(data) {
|
|
|
1433
1726
|
lines.push("");
|
|
1434
1727
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
1435
1728
|
const traceIssues = data.issues.filter(
|
|
1436
|
-
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code === "QFAI_CONTRACT_ORPHAN"
|
|
1729
|
+
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-") || item.code === "QFAI_CONTRACT_ORPHAN"
|
|
1437
1730
|
);
|
|
1438
1731
|
if (traceIssues.length === 0) {
|
|
1439
1732
|
lines.push("- (none)");
|
|
@@ -1473,8 +1766,8 @@ async function collectIds(files) {
|
|
|
1473
1766
|
DATA: /* @__PURE__ */ new Set()
|
|
1474
1767
|
};
|
|
1475
1768
|
for (const file of files) {
|
|
1476
|
-
const text = await (0,
|
|
1477
|
-
for (const prefix of
|
|
1769
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1770
|
+
for (const prefix of ID_PREFIXES2) {
|
|
1478
1771
|
const ids = extractIds(text, prefix);
|
|
1479
1772
|
ids.forEach((id) => result[prefix].add(id));
|
|
1480
1773
|
}
|
|
@@ -1491,7 +1784,7 @@ async function collectIds(files) {
|
|
|
1491
1784
|
async function collectUpstreamIds(files) {
|
|
1492
1785
|
const ids = /* @__PURE__ */ new Set();
|
|
1493
1786
|
for (const file of files) {
|
|
1494
|
-
const text = await (0,
|
|
1787
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1495
1788
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
1496
1789
|
}
|
|
1497
1790
|
return ids;
|
|
@@ -1512,7 +1805,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
1512
1805
|
}
|
|
1513
1806
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
1514
1807
|
for (const file of targetFiles) {
|
|
1515
|
-
const text = await (0,
|
|
1808
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1516
1809
|
if (pattern.test(text)) {
|
|
1517
1810
|
return true;
|
|
1518
1811
|
}
|
|
@@ -1534,20 +1827,20 @@ function toSortedArray(values) {
|
|
|
1534
1827
|
}
|
|
1535
1828
|
function buildHotspots(issues) {
|
|
1536
1829
|
const map = /* @__PURE__ */ new Map();
|
|
1537
|
-
for (const
|
|
1538
|
-
if (!
|
|
1830
|
+
for (const issue6 of issues) {
|
|
1831
|
+
if (!issue6.file) {
|
|
1539
1832
|
continue;
|
|
1540
1833
|
}
|
|
1541
|
-
const current = map.get(
|
|
1542
|
-
file:
|
|
1834
|
+
const current = map.get(issue6.file) ?? {
|
|
1835
|
+
file: issue6.file,
|
|
1543
1836
|
total: 0,
|
|
1544
1837
|
error: 0,
|
|
1545
1838
|
warning: 0,
|
|
1546
1839
|
info: 0
|
|
1547
1840
|
};
|
|
1548
1841
|
current.total += 1;
|
|
1549
|
-
current[
|
|
1550
|
-
map.set(
|
|
1842
|
+
current[issue6.severity] += 1;
|
|
1843
|
+
map.set(issue6.file, current);
|
|
1551
1844
|
}
|
|
1552
1845
|
return Array.from(map.values()).sort(
|
|
1553
1846
|
(a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
|
|
@@ -1569,6 +1862,7 @@ function buildHotspots(issues) {
|
|
|
1569
1862
|
resolvePath,
|
|
1570
1863
|
resolveToolVersion,
|
|
1571
1864
|
validateContracts,
|
|
1865
|
+
validateDefinedIds,
|
|
1572
1866
|
validateProject,
|
|
1573
1867
|
validateScenarioContent,
|
|
1574
1868
|
validateScenarios,
|