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.mjs
CHANGED
|
@@ -7,7 +7,6 @@ var defaultConfig = {
|
|
|
7
7
|
specDir: ".qfai/spec",
|
|
8
8
|
decisionsDir: ".qfai/spec/decisions",
|
|
9
9
|
scenariosDir: ".qfai/spec/scenarios",
|
|
10
|
-
rulesDir: ".qfai/rules",
|
|
11
10
|
contractsDir: ".qfai/contracts",
|
|
12
11
|
uiContractsDir: ".qfai/contracts/ui",
|
|
13
12
|
apiContractsDir: ".qfai/contracts/api",
|
|
@@ -31,7 +30,8 @@ var defaultConfig = {
|
|
|
31
30
|
traceability: {
|
|
32
31
|
brMustHaveSc: true,
|
|
33
32
|
scMustTouchContracts: true,
|
|
34
|
-
allowOrphanContracts: false
|
|
33
|
+
allowOrphanContracts: false,
|
|
34
|
+
unknownContractIdSeverity: "error"
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
output: {
|
|
@@ -106,13 +106,6 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
106
106
|
configPath,
|
|
107
107
|
issues
|
|
108
108
|
),
|
|
109
|
-
rulesDir: readString(
|
|
110
|
-
raw.rulesDir,
|
|
111
|
-
base.rulesDir,
|
|
112
|
-
"paths.rulesDir",
|
|
113
|
-
configPath,
|
|
114
|
-
issues
|
|
115
|
-
),
|
|
116
109
|
contractsDir: readString(
|
|
117
110
|
raw.contractsDir,
|
|
118
111
|
base.contractsDir,
|
|
@@ -237,6 +230,13 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
237
230
|
"validation.traceability.allowOrphanContracts",
|
|
238
231
|
configPath,
|
|
239
232
|
issues
|
|
233
|
+
),
|
|
234
|
+
unknownContractIdSeverity: readTraceabilitySeverity(
|
|
235
|
+
traceabilityRaw?.unknownContractIdSeverity,
|
|
236
|
+
base.traceability.unknownContractIdSeverity,
|
|
237
|
+
"validation.traceability.unknownContractIdSeverity",
|
|
238
|
+
configPath,
|
|
239
|
+
issues
|
|
240
240
|
)
|
|
241
241
|
}
|
|
242
242
|
};
|
|
@@ -316,6 +316,20 @@ function readFailOn(value, fallback, label, configPath, issues) {
|
|
|
316
316
|
}
|
|
317
317
|
return fallback;
|
|
318
318
|
}
|
|
319
|
+
function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
320
|
+
if (value === "warning" || value === "error") {
|
|
321
|
+
return value;
|
|
322
|
+
}
|
|
323
|
+
if (value !== void 0) {
|
|
324
|
+
issues.push(
|
|
325
|
+
configIssue(
|
|
326
|
+
configPath,
|
|
327
|
+
`${label} \u306F warning|error \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
328
|
+
)
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return fallback;
|
|
332
|
+
}
|
|
319
333
|
function readOutputFormat(value, fallback, label, configPath, issues) {
|
|
320
334
|
if (value === "text" || value === "json" || value === "github") {
|
|
321
335
|
return value;
|
|
@@ -356,13 +370,15 @@ function isRecord(value) {
|
|
|
356
370
|
}
|
|
357
371
|
|
|
358
372
|
// src/core/ids.ts
|
|
359
|
-
var
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
373
|
+
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
|
|
374
|
+
var STRICT_ID_PATTERNS = {
|
|
375
|
+
SPEC: /\bSPEC-\d{4}\b/g,
|
|
376
|
+
BR: /\bBR-\d{4}\b/g,
|
|
377
|
+
SC: /\bSC-\d{4}\b/g,
|
|
378
|
+
UI: /\bUI-\d{4}\b/g,
|
|
379
|
+
API: /\bAPI-\d{4}\b/g,
|
|
380
|
+
DATA: /\bDATA-\d{4}\b/g,
|
|
381
|
+
ADR: /\bADR-\d{4}\b/g
|
|
366
382
|
};
|
|
367
383
|
var LOOSE_ID_PATTERNS = {
|
|
368
384
|
SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
|
|
@@ -370,16 +386,17 @@ var LOOSE_ID_PATTERNS = {
|
|
|
370
386
|
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
371
387
|
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
372
388
|
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
373
|
-
DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi
|
|
389
|
+
DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi,
|
|
390
|
+
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
374
391
|
};
|
|
375
392
|
function extractIds(text, prefix) {
|
|
376
|
-
const pattern =
|
|
393
|
+
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
377
394
|
const matches = text.match(pattern);
|
|
378
395
|
return unique(matches ?? []);
|
|
379
396
|
}
|
|
380
397
|
function extractAllIds(text) {
|
|
381
398
|
const all = [];
|
|
382
|
-
|
|
399
|
+
ID_PREFIXES.forEach((prefix) => {
|
|
383
400
|
all.push(...extractIds(text, prefix));
|
|
384
401
|
});
|
|
385
402
|
return unique(all);
|
|
@@ -400,13 +417,13 @@ function unique(values) {
|
|
|
400
417
|
return Array.from(new Set(values));
|
|
401
418
|
}
|
|
402
419
|
function isValidId(value, prefix) {
|
|
403
|
-
const pattern =
|
|
420
|
+
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
404
421
|
const strict = new RegExp(pattern.source);
|
|
405
422
|
return strict.test(value);
|
|
406
423
|
}
|
|
407
424
|
|
|
408
425
|
// src/core/report.ts
|
|
409
|
-
import { readFile as
|
|
426
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
410
427
|
|
|
411
428
|
// src/core/discovery.ts
|
|
412
429
|
import path3 from "path";
|
|
@@ -502,8 +519,8 @@ import { readFile as readFile2 } from "fs/promises";
|
|
|
502
519
|
import path4 from "path";
|
|
503
520
|
import { fileURLToPath } from "url";
|
|
504
521
|
async function resolveToolVersion() {
|
|
505
|
-
if ("0.2.
|
|
506
|
-
return "0.2.
|
|
522
|
+
if ("0.2.9".length > 0) {
|
|
523
|
+
return "0.2.9";
|
|
507
524
|
}
|
|
508
525
|
try {
|
|
509
526
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -523,8 +540,50 @@ function resolvePackageJsonPath() {
|
|
|
523
540
|
|
|
524
541
|
// src/core/validators/contracts.ts
|
|
525
542
|
import { readFile as readFile3 } from "fs/promises";
|
|
543
|
+
|
|
544
|
+
// src/core/contracts.ts
|
|
526
545
|
import path5 from "path";
|
|
527
546
|
import { parse as parseYaml2 } from "yaml";
|
|
547
|
+
function parseStructuredContract(file, text) {
|
|
548
|
+
const ext = path5.extname(file).toLowerCase();
|
|
549
|
+
if (ext === ".json") {
|
|
550
|
+
return JSON.parse(text);
|
|
551
|
+
}
|
|
552
|
+
return parseYaml2(text);
|
|
553
|
+
}
|
|
554
|
+
function extractUiContractIds(doc) {
|
|
555
|
+
const id = typeof doc.id === "string" ? doc.id : "";
|
|
556
|
+
return extractIds(id, "UI");
|
|
557
|
+
}
|
|
558
|
+
function extractApiContractIds(doc) {
|
|
559
|
+
const operationIds = /* @__PURE__ */ new Set();
|
|
560
|
+
collectOperationIds(doc, operationIds);
|
|
561
|
+
const ids = /* @__PURE__ */ new Set();
|
|
562
|
+
for (const operationId of operationIds) {
|
|
563
|
+
extractIds(operationId, "API").forEach((id) => ids.add(id));
|
|
564
|
+
}
|
|
565
|
+
return Array.from(ids);
|
|
566
|
+
}
|
|
567
|
+
function collectOperationIds(value, out) {
|
|
568
|
+
if (!value || typeof value !== "object") {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (Array.isArray(value)) {
|
|
572
|
+
for (const item of value) {
|
|
573
|
+
collectOperationIds(item, out);
|
|
574
|
+
}
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
578
|
+
if (key === "operationId" && typeof entry === "string") {
|
|
579
|
+
out.add(entry);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
collectOperationIds(entry, out);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/core/validators/contracts.ts
|
|
528
587
|
var SQL_DANGEROUS_PATTERNS = [
|
|
529
588
|
{ pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
|
|
530
589
|
{ pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
|
|
@@ -573,12 +632,13 @@ async function validateUiContracts(uiRoot) {
|
|
|
573
632
|
"SC",
|
|
574
633
|
"UI",
|
|
575
634
|
"API",
|
|
576
|
-
"DATA"
|
|
635
|
+
"DATA",
|
|
636
|
+
"ADR"
|
|
577
637
|
]);
|
|
578
638
|
if (invalidIds.length > 0) {
|
|
579
639
|
issues.push(
|
|
580
640
|
issue(
|
|
581
|
-
"
|
|
641
|
+
"QFAI-ID-002",
|
|
582
642
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
583
643
|
"error",
|
|
584
644
|
file,
|
|
@@ -587,30 +647,32 @@ async function validateUiContracts(uiRoot) {
|
|
|
587
647
|
)
|
|
588
648
|
);
|
|
589
649
|
}
|
|
650
|
+
let doc;
|
|
590
651
|
try {
|
|
591
|
-
|
|
592
|
-
const id = typeof doc.id === "string" ? doc.id : "";
|
|
593
|
-
if (!id.startsWith("UI-")) {
|
|
594
|
-
issues.push(
|
|
595
|
-
issue(
|
|
596
|
-
"QFAI-UI-001",
|
|
597
|
-
"UI \u5951\u7D04\u306E id \u306F UI- \u3067\u59CB\u307E\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
598
|
-
"error",
|
|
599
|
-
file,
|
|
600
|
-
"contracts.ui.id"
|
|
601
|
-
)
|
|
602
|
-
);
|
|
603
|
-
}
|
|
652
|
+
doc = parseStructuredContract(file, text);
|
|
604
653
|
} catch (error) {
|
|
605
654
|
issues.push(
|
|
606
655
|
issue(
|
|
607
|
-
"QFAI-
|
|
608
|
-
`UI
|
|
656
|
+
"QFAI-CONTRACT-001",
|
|
657
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error)})`,
|
|
609
658
|
"error",
|
|
610
659
|
file,
|
|
611
660
|
"contracts.ui.parse"
|
|
612
661
|
)
|
|
613
662
|
);
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
const uiIds = extractUiContractIds(doc);
|
|
666
|
+
if (uiIds.length === 0) {
|
|
667
|
+
issues.push(
|
|
668
|
+
issue(
|
|
669
|
+
"QFAI-CONTRACT-002",
|
|
670
|
+
`UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
671
|
+
"error",
|
|
672
|
+
file,
|
|
673
|
+
"contracts.ui.id"
|
|
674
|
+
)
|
|
675
|
+
);
|
|
614
676
|
}
|
|
615
677
|
}
|
|
616
678
|
return issues;
|
|
@@ -637,12 +699,13 @@ async function validateApiContracts(apiRoot) {
|
|
|
637
699
|
"SC",
|
|
638
700
|
"UI",
|
|
639
701
|
"API",
|
|
640
|
-
"DATA"
|
|
702
|
+
"DATA",
|
|
703
|
+
"ADR"
|
|
641
704
|
]);
|
|
642
705
|
if (invalidIds.length > 0) {
|
|
643
706
|
issues.push(
|
|
644
707
|
issue(
|
|
645
|
-
"
|
|
708
|
+
"QFAI-ID-002",
|
|
646
709
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
647
710
|
"error",
|
|
648
711
|
file,
|
|
@@ -651,29 +714,43 @@ async function validateApiContracts(apiRoot) {
|
|
|
651
714
|
)
|
|
652
715
|
);
|
|
653
716
|
}
|
|
717
|
+
let doc;
|
|
654
718
|
try {
|
|
655
|
-
|
|
656
|
-
if (!doc || !hasOpenApi(doc)) {
|
|
657
|
-
issues.push(
|
|
658
|
-
issue(
|
|
659
|
-
"QFAI-API-001",
|
|
660
|
-
"OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
661
|
-
"error",
|
|
662
|
-
file,
|
|
663
|
-
"contracts.api.openapi"
|
|
664
|
-
)
|
|
665
|
-
);
|
|
666
|
-
}
|
|
719
|
+
doc = parseStructuredContract(file, text);
|
|
667
720
|
} catch (error) {
|
|
668
721
|
issues.push(
|
|
669
722
|
issue(
|
|
670
|
-
"QFAI-
|
|
671
|
-
`API \
|
|
723
|
+
"QFAI-CONTRACT-001",
|
|
724
|
+
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error)})`,
|
|
672
725
|
"error",
|
|
673
726
|
file,
|
|
674
727
|
"contracts.api.parse"
|
|
675
728
|
)
|
|
676
729
|
);
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (!hasOpenApi(doc)) {
|
|
733
|
+
issues.push(
|
|
734
|
+
issue(
|
|
735
|
+
"QFAI-API-001",
|
|
736
|
+
"OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
737
|
+
"error",
|
|
738
|
+
file,
|
|
739
|
+
"contracts.api.openapi"
|
|
740
|
+
)
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
const apiIds = extractApiContractIds(doc);
|
|
744
|
+
if (apiIds.length === 0) {
|
|
745
|
+
issues.push(
|
|
746
|
+
issue(
|
|
747
|
+
"QFAI-CONTRACT-002",
|
|
748
|
+
`API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
749
|
+
"error",
|
|
750
|
+
file,
|
|
751
|
+
"contracts.api.id"
|
|
752
|
+
)
|
|
753
|
+
);
|
|
677
754
|
}
|
|
678
755
|
}
|
|
679
756
|
return issues;
|
|
@@ -700,12 +777,13 @@ async function validateDataContracts(dataRoot) {
|
|
|
700
777
|
"SC",
|
|
701
778
|
"UI",
|
|
702
779
|
"API",
|
|
703
|
-
"DATA"
|
|
780
|
+
"DATA",
|
|
781
|
+
"ADR"
|
|
704
782
|
]);
|
|
705
783
|
if (invalidIds.length > 0) {
|
|
706
784
|
issues.push(
|
|
707
785
|
issue(
|
|
708
|
-
"
|
|
786
|
+
"QFAI-ID-002",
|
|
709
787
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
710
788
|
"error",
|
|
711
789
|
file,
|
|
@@ -735,13 +813,6 @@ function lintSql(text, file) {
|
|
|
735
813
|
}
|
|
736
814
|
return issues;
|
|
737
815
|
}
|
|
738
|
-
function parseStructured(file, text) {
|
|
739
|
-
const ext = path5.extname(file).toLowerCase();
|
|
740
|
-
if (ext === ".json") {
|
|
741
|
-
return JSON.parse(text);
|
|
742
|
-
}
|
|
743
|
-
return parseYaml2(text);
|
|
744
|
-
}
|
|
745
816
|
function hasOpenApi(doc) {
|
|
746
817
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
747
818
|
}
|
|
@@ -752,25 +823,165 @@ function formatError2(error) {
|
|
|
752
823
|
return String(error);
|
|
753
824
|
}
|
|
754
825
|
function issue(code, message, severity, file, rule, refs) {
|
|
755
|
-
const
|
|
826
|
+
const issue6 = {
|
|
756
827
|
code,
|
|
757
828
|
severity,
|
|
758
829
|
message
|
|
759
830
|
};
|
|
760
831
|
if (file) {
|
|
761
|
-
|
|
832
|
+
issue6.file = file;
|
|
762
833
|
}
|
|
763
834
|
if (rule) {
|
|
764
|
-
|
|
835
|
+
issue6.rule = rule;
|
|
765
836
|
}
|
|
766
837
|
if (refs && refs.length > 0) {
|
|
767
|
-
|
|
838
|
+
issue6.refs = refs;
|
|
768
839
|
}
|
|
769
|
-
return
|
|
840
|
+
return issue6;
|
|
770
841
|
}
|
|
771
842
|
|
|
772
|
-
// src/core/validators/
|
|
843
|
+
// src/core/validators/ids.ts
|
|
844
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
845
|
+
import path6 from "path";
|
|
846
|
+
|
|
847
|
+
// src/core/contractIndex.ts
|
|
773
848
|
import { readFile as readFile4 } from "fs/promises";
|
|
849
|
+
async function buildContractIndex(root, config) {
|
|
850
|
+
const uiRoot = resolvePath(root, config, "uiContractsDir");
|
|
851
|
+
const apiRoot = resolvePath(root, config, "apiContractsDir");
|
|
852
|
+
const dataRoot = resolvePath(root, config, "dataContractsDir");
|
|
853
|
+
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
854
|
+
collectUiContractFiles(uiRoot),
|
|
855
|
+
collectApiContractFiles(apiRoot),
|
|
856
|
+
collectDataContractFiles(dataRoot)
|
|
857
|
+
]);
|
|
858
|
+
const index = {
|
|
859
|
+
ids: /* @__PURE__ */ new Set(),
|
|
860
|
+
idToFiles: /* @__PURE__ */ new Map(),
|
|
861
|
+
files: { ui: uiFiles, api: apiFiles, data: dataFiles },
|
|
862
|
+
structuredParseFailedFiles: /* @__PURE__ */ new Set()
|
|
863
|
+
};
|
|
864
|
+
await indexUiContracts(uiFiles, index);
|
|
865
|
+
await indexApiContracts(apiFiles, index);
|
|
866
|
+
await indexDataContracts(dataFiles, index);
|
|
867
|
+
return index;
|
|
868
|
+
}
|
|
869
|
+
async function indexUiContracts(files, index) {
|
|
870
|
+
for (const file of files) {
|
|
871
|
+
const text = await readFile4(file, "utf-8");
|
|
872
|
+
try {
|
|
873
|
+
const doc = parseStructuredContract(file, text);
|
|
874
|
+
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
875
|
+
} catch {
|
|
876
|
+
index.structuredParseFailedFiles.add(file);
|
|
877
|
+
extractIds(text, "UI").forEach((id) => record(index, id, file));
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async function indexApiContracts(files, index) {
|
|
882
|
+
for (const file of files) {
|
|
883
|
+
const text = await readFile4(file, "utf-8");
|
|
884
|
+
try {
|
|
885
|
+
const doc = parseStructuredContract(file, text);
|
|
886
|
+
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
887
|
+
} catch {
|
|
888
|
+
index.structuredParseFailedFiles.add(file);
|
|
889
|
+
extractIds(text, "API").forEach((id) => record(index, id, file));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
async function indexDataContracts(files, index) {
|
|
894
|
+
for (const file of files) {
|
|
895
|
+
const text = await readFile4(file, "utf-8");
|
|
896
|
+
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function record(index, id, file) {
|
|
900
|
+
index.ids.add(id);
|
|
901
|
+
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
902
|
+
current.add(file);
|
|
903
|
+
index.idToFiles.set(id, current);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// src/core/validators/ids.ts
|
|
907
|
+
async function validateDefinedIds(root, config) {
|
|
908
|
+
const issues = [];
|
|
909
|
+
const specRoot = resolvePath(root, config, "specDir");
|
|
910
|
+
const scenarioRoot = resolvePath(root, config, "scenariosDir");
|
|
911
|
+
const specFiles = await collectSpecFiles(specRoot);
|
|
912
|
+
const scenarioFiles = await collectFiles(scenarioRoot, {
|
|
913
|
+
extensions: [".feature"]
|
|
914
|
+
});
|
|
915
|
+
const defined = /* @__PURE__ */ new Map();
|
|
916
|
+
await collectSpecDefinitionIds(specFiles, defined);
|
|
917
|
+
await collectScenarioDefinitionIds(scenarioFiles, defined);
|
|
918
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
919
|
+
for (const [id, files] of contractIndex.idToFiles.entries()) {
|
|
920
|
+
for (const file of files) {
|
|
921
|
+
recordId(defined, id, file);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
for (const [id, files] of defined.entries()) {
|
|
925
|
+
if (files.size <= 1) {
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
const sorted = Array.from(files).sort();
|
|
929
|
+
issues.push(
|
|
930
|
+
issue2(
|
|
931
|
+
"QFAI-ID-001",
|
|
932
|
+
`ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
|
|
933
|
+
"error",
|
|
934
|
+
sorted[0],
|
|
935
|
+
"id.duplicate"
|
|
936
|
+
)
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
return issues;
|
|
940
|
+
}
|
|
941
|
+
async function collectSpecDefinitionIds(files, out) {
|
|
942
|
+
for (const file of files) {
|
|
943
|
+
const text = await readFile5(file, "utf-8");
|
|
944
|
+
extractIds(text, "SPEC").forEach((id) => recordId(out, id, file));
|
|
945
|
+
extractIds(text, "BR").forEach((id) => recordId(out, id, file));
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
async function collectScenarioDefinitionIds(files, out) {
|
|
949
|
+
for (const file of files) {
|
|
950
|
+
const text = await readFile5(file, "utf-8");
|
|
951
|
+
extractIds(text, "SC").forEach((id) => recordId(out, id, file));
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
function recordId(out, id, file) {
|
|
955
|
+
const current = out.get(id) ?? /* @__PURE__ */ new Set();
|
|
956
|
+
current.add(file);
|
|
957
|
+
out.set(id, current);
|
|
958
|
+
}
|
|
959
|
+
function formatFileList(files, root) {
|
|
960
|
+
return files.map((file) => {
|
|
961
|
+
const relative = path6.relative(root, file);
|
|
962
|
+
return relative.length > 0 ? relative : file;
|
|
963
|
+
}).join(", ");
|
|
964
|
+
}
|
|
965
|
+
function issue2(code, message, severity, file, rule, refs) {
|
|
966
|
+
const issue6 = {
|
|
967
|
+
code,
|
|
968
|
+
severity,
|
|
969
|
+
message
|
|
970
|
+
};
|
|
971
|
+
if (file) {
|
|
972
|
+
issue6.file = file;
|
|
973
|
+
}
|
|
974
|
+
if (rule) {
|
|
975
|
+
issue6.rule = rule;
|
|
976
|
+
}
|
|
977
|
+
if (refs && refs.length > 0) {
|
|
978
|
+
issue6.refs = refs;
|
|
979
|
+
}
|
|
980
|
+
return issue6;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// src/core/validators/scenario.ts
|
|
984
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
774
985
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
775
986
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
776
987
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -781,7 +992,7 @@ async function validateScenarios(root, config) {
|
|
|
781
992
|
});
|
|
782
993
|
if (files.length === 0) {
|
|
783
994
|
return [
|
|
784
|
-
|
|
995
|
+
issue3(
|
|
785
996
|
"QFAI-SC-000",
|
|
786
997
|
"Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
787
998
|
"info",
|
|
@@ -792,7 +1003,7 @@ async function validateScenarios(root, config) {
|
|
|
792
1003
|
}
|
|
793
1004
|
const issues = [];
|
|
794
1005
|
for (const file of files) {
|
|
795
|
-
const text = await
|
|
1006
|
+
const text = await readFile6(file, "utf-8");
|
|
796
1007
|
issues.push(...validateScenarioContent(text, file));
|
|
797
1008
|
}
|
|
798
1009
|
return issues;
|
|
@@ -805,12 +1016,13 @@ function validateScenarioContent(text, file) {
|
|
|
805
1016
|
"SC",
|
|
806
1017
|
"UI",
|
|
807
1018
|
"API",
|
|
808
|
-
"DATA"
|
|
1019
|
+
"DATA",
|
|
1020
|
+
"ADR"
|
|
809
1021
|
]);
|
|
810
1022
|
if (invalidIds.length > 0) {
|
|
811
1023
|
issues.push(
|
|
812
|
-
|
|
813
|
-
"
|
|
1024
|
+
issue3(
|
|
1025
|
+
"QFAI-ID-002",
|
|
814
1026
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
815
1027
|
"error",
|
|
816
1028
|
file,
|
|
@@ -822,7 +1034,7 @@ function validateScenarioContent(text, file) {
|
|
|
822
1034
|
const scIds = extractIds(text, "SC");
|
|
823
1035
|
if (scIds.length === 0) {
|
|
824
1036
|
issues.push(
|
|
825
|
-
|
|
1037
|
+
issue3(
|
|
826
1038
|
"QFAI-SC-001",
|
|
827
1039
|
"SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
828
1040
|
"error",
|
|
@@ -834,7 +1046,7 @@ function validateScenarioContent(text, file) {
|
|
|
834
1046
|
const specIds = extractIds(text, "SPEC");
|
|
835
1047
|
if (specIds.length === 0) {
|
|
836
1048
|
issues.push(
|
|
837
|
-
|
|
1049
|
+
issue3(
|
|
838
1050
|
"QFAI-SC-002",
|
|
839
1051
|
"SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
840
1052
|
"error",
|
|
@@ -846,7 +1058,7 @@ function validateScenarioContent(text, file) {
|
|
|
846
1058
|
const brIds = extractIds(text, "BR");
|
|
847
1059
|
if (brIds.length === 0) {
|
|
848
1060
|
issues.push(
|
|
849
|
-
|
|
1061
|
+
issue3(
|
|
850
1062
|
"QFAI-SC-003",
|
|
851
1063
|
"SC \u306F BR \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
852
1064
|
"error",
|
|
@@ -867,7 +1079,7 @@ function validateScenarioContent(text, file) {
|
|
|
867
1079
|
}
|
|
868
1080
|
if (missingSteps.length > 0) {
|
|
869
1081
|
issues.push(
|
|
870
|
-
|
|
1082
|
+
issue3(
|
|
871
1083
|
"QFAI-SC-005",
|
|
872
1084
|
`Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
|
|
873
1085
|
"warning",
|
|
@@ -878,33 +1090,33 @@ function validateScenarioContent(text, file) {
|
|
|
878
1090
|
}
|
|
879
1091
|
return issues;
|
|
880
1092
|
}
|
|
881
|
-
function
|
|
882
|
-
const
|
|
1093
|
+
function issue3(code, message, severity, file, rule, refs) {
|
|
1094
|
+
const issue6 = {
|
|
883
1095
|
code,
|
|
884
1096
|
severity,
|
|
885
1097
|
message
|
|
886
1098
|
};
|
|
887
1099
|
if (file) {
|
|
888
|
-
|
|
1100
|
+
issue6.file = file;
|
|
889
1101
|
}
|
|
890
1102
|
if (rule) {
|
|
891
|
-
|
|
1103
|
+
issue6.rule = rule;
|
|
892
1104
|
}
|
|
893
1105
|
if (refs && refs.length > 0) {
|
|
894
|
-
|
|
1106
|
+
issue6.refs = refs;
|
|
895
1107
|
}
|
|
896
|
-
return
|
|
1108
|
+
return issue6;
|
|
897
1109
|
}
|
|
898
1110
|
|
|
899
1111
|
// src/core/validators/spec.ts
|
|
900
|
-
import { readFile as
|
|
1112
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
901
1113
|
async function validateSpecs(root, config) {
|
|
902
1114
|
const specsRoot = resolvePath(root, config, "specDir");
|
|
903
1115
|
const files = await collectSpecFiles(specsRoot);
|
|
904
1116
|
if (files.length === 0) {
|
|
905
1117
|
const expected = "spec-0001-<slug>.md";
|
|
906
1118
|
return [
|
|
907
|
-
|
|
1119
|
+
issue4(
|
|
908
1120
|
"QFAI-SPEC-000",
|
|
909
1121
|
`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}`,
|
|
910
1122
|
"info",
|
|
@@ -915,7 +1127,7 @@ async function validateSpecs(root, config) {
|
|
|
915
1127
|
}
|
|
916
1128
|
const issues = [];
|
|
917
1129
|
for (const file of files) {
|
|
918
|
-
const text = await
|
|
1130
|
+
const text = await readFile7(file, "utf-8");
|
|
919
1131
|
issues.push(
|
|
920
1132
|
...validateSpecContent(
|
|
921
1133
|
text,
|
|
@@ -934,12 +1146,13 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
934
1146
|
"SC",
|
|
935
1147
|
"UI",
|
|
936
1148
|
"API",
|
|
937
|
-
"DATA"
|
|
1149
|
+
"DATA",
|
|
1150
|
+
"ADR"
|
|
938
1151
|
]);
|
|
939
1152
|
if (invalidIds.length > 0) {
|
|
940
1153
|
issues.push(
|
|
941
|
-
|
|
942
|
-
"
|
|
1154
|
+
issue4(
|
|
1155
|
+
"QFAI-ID-002",
|
|
943
1156
|
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
944
1157
|
"error",
|
|
945
1158
|
file,
|
|
@@ -951,7 +1164,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
951
1164
|
const specIds = extractIds(text, "SPEC");
|
|
952
1165
|
if (specIds.length === 0) {
|
|
953
1166
|
issues.push(
|
|
954
|
-
|
|
1167
|
+
issue4(
|
|
955
1168
|
"QFAI-SPEC-001",
|
|
956
1169
|
"SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
957
1170
|
"error",
|
|
@@ -963,7 +1176,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
963
1176
|
const brIds = extractIds(text, "BR");
|
|
964
1177
|
if (brIds.length === 0) {
|
|
965
1178
|
issues.push(
|
|
966
|
-
|
|
1179
|
+
issue4(
|
|
967
1180
|
"QFAI-SPEC-002",
|
|
968
1181
|
"BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
969
1182
|
"error",
|
|
@@ -975,7 +1188,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
975
1188
|
const scIds = extractIds(text, "SC");
|
|
976
1189
|
if (scIds.length > 0) {
|
|
977
1190
|
issues.push(
|
|
978
|
-
|
|
1191
|
+
issue4(
|
|
979
1192
|
"QFAI-SPEC-003",
|
|
980
1193
|
"Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
|
|
981
1194
|
"warning",
|
|
@@ -988,7 +1201,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
988
1201
|
for (const section of requiredSections) {
|
|
989
1202
|
if (!text.includes(section)) {
|
|
990
1203
|
issues.push(
|
|
991
|
-
|
|
1204
|
+
issue4(
|
|
992
1205
|
"QFAI-SPEC-004",
|
|
993
1206
|
`\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
|
|
994
1207
|
"error",
|
|
@@ -1000,26 +1213,26 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1000
1213
|
}
|
|
1001
1214
|
return issues;
|
|
1002
1215
|
}
|
|
1003
|
-
function
|
|
1004
|
-
const
|
|
1216
|
+
function issue4(code, message, severity, file, rule, refs) {
|
|
1217
|
+
const issue6 = {
|
|
1005
1218
|
code,
|
|
1006
1219
|
severity,
|
|
1007
1220
|
message
|
|
1008
1221
|
};
|
|
1009
1222
|
if (file) {
|
|
1010
|
-
|
|
1223
|
+
issue6.file = file;
|
|
1011
1224
|
}
|
|
1012
1225
|
if (rule) {
|
|
1013
|
-
|
|
1226
|
+
issue6.rule = rule;
|
|
1014
1227
|
}
|
|
1015
1228
|
if (refs && refs.length > 0) {
|
|
1016
|
-
|
|
1229
|
+
issue6.refs = refs;
|
|
1017
1230
|
}
|
|
1018
|
-
return
|
|
1231
|
+
return issue6;
|
|
1019
1232
|
}
|
|
1020
1233
|
|
|
1021
1234
|
// src/core/validators/traceability.ts
|
|
1022
|
-
import { readFile as
|
|
1235
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1023
1236
|
async function validateTraceability(root, config) {
|
|
1024
1237
|
const issues = [];
|
|
1025
1238
|
const specsRoot = resolvePath(root, config, "specDir");
|
|
@@ -1035,36 +1248,141 @@ async function validateTraceability(root, config) {
|
|
|
1035
1248
|
extensions: [".feature"]
|
|
1036
1249
|
});
|
|
1037
1250
|
const upstreamIds = /* @__PURE__ */ new Set();
|
|
1251
|
+
const specIds = /* @__PURE__ */ new Set();
|
|
1038
1252
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
1039
1253
|
const brIdsInScenarios = /* @__PURE__ */ new Set();
|
|
1040
1254
|
const scIdsInScenarios = /* @__PURE__ */ new Set();
|
|
1041
1255
|
const scenarioContractIds = /* @__PURE__ */ new Set();
|
|
1042
1256
|
const scWithContracts = /* @__PURE__ */ new Set();
|
|
1043
|
-
|
|
1044
|
-
|
|
1257
|
+
const specToBrIds = /* @__PURE__ */ new Map();
|
|
1258
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
1259
|
+
const contractIds = contractIndex.ids;
|
|
1260
|
+
for (const file of specFiles) {
|
|
1261
|
+
const text = await readFile8(file, "utf-8");
|
|
1262
|
+
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1263
|
+
const specIdsInFile = extractIds(text, "SPEC");
|
|
1264
|
+
specIdsInFile.forEach((id) => specIds.add(id));
|
|
1265
|
+
const brIds = extractIds(text, "BR");
|
|
1266
|
+
brIds.forEach((id) => brIdsInSpecs.add(id));
|
|
1267
|
+
const referencedContractIds = /* @__PURE__ */ new Set([
|
|
1268
|
+
...extractIds(text, "UI"),
|
|
1269
|
+
...extractIds(text, "API"),
|
|
1270
|
+
...extractIds(text, "DATA")
|
|
1271
|
+
]);
|
|
1272
|
+
const unknownContractIds = Array.from(referencedContractIds).filter(
|
|
1273
|
+
(id) => !contractIds.has(id)
|
|
1274
|
+
);
|
|
1275
|
+
if (unknownContractIds.length > 0) {
|
|
1276
|
+
issues.push(
|
|
1277
|
+
issue5(
|
|
1278
|
+
"QFAI-TRACE-009",
|
|
1279
|
+
`Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1280
|
+
", "
|
|
1281
|
+
)}`,
|
|
1282
|
+
"error",
|
|
1283
|
+
file,
|
|
1284
|
+
"traceability.specContractExists",
|
|
1285
|
+
unknownContractIds
|
|
1286
|
+
)
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
for (const specId of specIdsInFile) {
|
|
1290
|
+
const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
|
|
1291
|
+
brIds.forEach((id) => current.add(id));
|
|
1292
|
+
specToBrIds.set(specId, current);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
for (const file of decisionFiles) {
|
|
1296
|
+
const text = await readFile8(file, "utf-8");
|
|
1045
1297
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1046
|
-
extractIds(text, "BR").forEach((id) => brIdsInSpecs.add(id));
|
|
1047
1298
|
}
|
|
1048
1299
|
for (const file of scenarioFiles) {
|
|
1049
|
-
const text = await
|
|
1300
|
+
const text = await readFile8(file, "utf-8");
|
|
1050
1301
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1302
|
+
const specIdsInScenario = extractIds(text, "SPEC");
|
|
1051
1303
|
const brIds = extractIds(text, "BR");
|
|
1052
|
-
brIds.forEach((id) => brIdsInScenarios.add(id));
|
|
1053
1304
|
const scIds = extractIds(text, "SC");
|
|
1054
|
-
|
|
1055
|
-
const contractIds = [
|
|
1305
|
+
const scenarioIds = [
|
|
1056
1306
|
...extractIds(text, "UI"),
|
|
1057
1307
|
...extractIds(text, "API"),
|
|
1058
1308
|
...extractIds(text, "DATA")
|
|
1059
1309
|
];
|
|
1060
|
-
|
|
1061
|
-
|
|
1310
|
+
brIds.forEach((id) => brIdsInScenarios.add(id));
|
|
1311
|
+
scIds.forEach((id) => scIdsInScenarios.add(id));
|
|
1312
|
+
scenarioIds.forEach((id) => scenarioContractIds.add(id));
|
|
1313
|
+
if (scenarioIds.length > 0) {
|
|
1062
1314
|
scIds.forEach((id) => scWithContracts.add(id));
|
|
1063
1315
|
}
|
|
1316
|
+
const unknownSpecIds = specIdsInScenario.filter((id) => !specIds.has(id));
|
|
1317
|
+
if (unknownSpecIds.length > 0) {
|
|
1318
|
+
issues.push(
|
|
1319
|
+
issue5(
|
|
1320
|
+
"QFAI-TRACE-005",
|
|
1321
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1322
|
+
"error",
|
|
1323
|
+
file,
|
|
1324
|
+
"traceability.scenarioSpecExists",
|
|
1325
|
+
unknownSpecIds
|
|
1326
|
+
)
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
const unknownBrIds = brIds.filter((id) => !brIdsInSpecs.has(id));
|
|
1330
|
+
if (unknownBrIds.length > 0) {
|
|
1331
|
+
issues.push(
|
|
1332
|
+
issue5(
|
|
1333
|
+
"QFAI-TRACE-006",
|
|
1334
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1335
|
+
"error",
|
|
1336
|
+
file,
|
|
1337
|
+
"traceability.scenarioBrExists",
|
|
1338
|
+
unknownBrIds
|
|
1339
|
+
)
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
const unknownContractIds = scenarioIds.filter((id) => !contractIds.has(id));
|
|
1343
|
+
if (unknownContractIds.length > 0) {
|
|
1344
|
+
issues.push(
|
|
1345
|
+
issue5(
|
|
1346
|
+
"QFAI-TRACE-008",
|
|
1347
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1348
|
+
", "
|
|
1349
|
+
)}`,
|
|
1350
|
+
config.validation.traceability.unknownContractIdSeverity,
|
|
1351
|
+
file,
|
|
1352
|
+
"traceability.scenarioContractExists",
|
|
1353
|
+
unknownContractIds
|
|
1354
|
+
)
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
if (specIdsInScenario.length > 0) {
|
|
1358
|
+
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
1359
|
+
for (const specId of specIdsInScenario) {
|
|
1360
|
+
const brIdsForSpec = specToBrIds.get(specId);
|
|
1361
|
+
if (!brIdsForSpec) {
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
1365
|
+
}
|
|
1366
|
+
const invalidBrIds = brIds.filter((id) => !allowedBrIds.has(id));
|
|
1367
|
+
if (invalidBrIds.length > 0) {
|
|
1368
|
+
issues.push(
|
|
1369
|
+
issue5(
|
|
1370
|
+
"QFAI-TRACE-007",
|
|
1371
|
+
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
1372
|
+
", "
|
|
1373
|
+
)} (SPEC: ${specIdsInScenario.join(", ")})`,
|
|
1374
|
+
"error",
|
|
1375
|
+
file,
|
|
1376
|
+
"traceability.scenarioBrUnderSpec",
|
|
1377
|
+
invalidBrIds
|
|
1378
|
+
)
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1064
1382
|
}
|
|
1065
1383
|
if (upstreamIds.size === 0) {
|
|
1066
1384
|
return [
|
|
1067
|
-
|
|
1385
|
+
issue5(
|
|
1068
1386
|
"QFAI-TRACE-000",
|
|
1069
1387
|
"\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1070
1388
|
"info",
|
|
@@ -1079,7 +1397,7 @@ async function validateTraceability(root, config) {
|
|
|
1079
1397
|
);
|
|
1080
1398
|
if (orphanBrIds.length > 0) {
|
|
1081
1399
|
issues.push(
|
|
1082
|
-
|
|
1400
|
+
issue5(
|
|
1083
1401
|
"QFAI_TRACE_BR_ORPHAN",
|
|
1084
1402
|
`BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
|
|
1085
1403
|
"error",
|
|
@@ -1096,7 +1414,7 @@ async function validateTraceability(root, config) {
|
|
|
1096
1414
|
);
|
|
1097
1415
|
if (scWithoutContracts.length > 0) {
|
|
1098
1416
|
issues.push(
|
|
1099
|
-
|
|
1417
|
+
issue5(
|
|
1100
1418
|
"QFAI_TRACE_SC_NO_CONTRACT",
|
|
1101
1419
|
`SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
|
|
1102
1420
|
", "
|
|
@@ -1110,14 +1428,13 @@ async function validateTraceability(root, config) {
|
|
|
1110
1428
|
}
|
|
1111
1429
|
}
|
|
1112
1430
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
1113
|
-
const contractIds = await collectContractIds(root, config);
|
|
1114
1431
|
if (contractIds.size > 0) {
|
|
1115
1432
|
const orphanContracts = Array.from(contractIds).filter(
|
|
1116
1433
|
(id) => !scenarioContractIds.has(id)
|
|
1117
1434
|
);
|
|
1118
1435
|
if (orphanContracts.length > 0) {
|
|
1119
1436
|
issues.push(
|
|
1120
|
-
|
|
1437
|
+
issue5(
|
|
1121
1438
|
"QFAI_CONTRACT_ORPHAN",
|
|
1122
1439
|
`\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
1123
1440
|
"error",
|
|
@@ -1134,27 +1451,6 @@ async function validateTraceability(root, config) {
|
|
|
1134
1451
|
);
|
|
1135
1452
|
return issues;
|
|
1136
1453
|
}
|
|
1137
|
-
async function collectContractIds(root, config) {
|
|
1138
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
1139
|
-
const uiRoot = resolvePath(root, config, "uiContractsDir");
|
|
1140
|
-
const apiRoot = resolvePath(root, config, "apiContractsDir");
|
|
1141
|
-
const dataRoot = resolvePath(root, config, "dataContractsDir");
|
|
1142
|
-
const uiFiles = await collectUiContractFiles(uiRoot);
|
|
1143
|
-
const apiFiles = await collectApiContractFiles(apiRoot);
|
|
1144
|
-
const dataFiles = await collectDataContractFiles(dataRoot);
|
|
1145
|
-
await collectIdsFromFiles(uiFiles, ["UI"], contractIds);
|
|
1146
|
-
await collectIdsFromFiles(apiFiles, ["API"], contractIds);
|
|
1147
|
-
await collectIdsFromFiles(dataFiles, ["DATA"], contractIds);
|
|
1148
|
-
return contractIds;
|
|
1149
|
-
}
|
|
1150
|
-
async function collectIdsFromFiles(files, prefixes, out) {
|
|
1151
|
-
for (const file of files) {
|
|
1152
|
-
const text = await readFile6(file, "utf-8");
|
|
1153
|
-
for (const prefix of prefixes) {
|
|
1154
|
-
extractIds(text, prefix).forEach((id) => out.add(id));
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
1454
|
async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
1159
1455
|
const issues = [];
|
|
1160
1456
|
const codeFiles = await collectFiles(srcRoot, {
|
|
@@ -1166,7 +1462,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1166
1462
|
const targetFiles = [...codeFiles, ...testFiles];
|
|
1167
1463
|
if (targetFiles.length === 0) {
|
|
1168
1464
|
issues.push(
|
|
1169
|
-
|
|
1465
|
+
issue5(
|
|
1170
1466
|
"QFAI-TRACE-001",
|
|
1171
1467
|
"\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1172
1468
|
"info",
|
|
@@ -1179,7 +1475,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1179
1475
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
1180
1476
|
let found = false;
|
|
1181
1477
|
for (const file of targetFiles) {
|
|
1182
|
-
const text = await
|
|
1478
|
+
const text = await readFile8(file, "utf-8");
|
|
1183
1479
|
if (pattern.test(text)) {
|
|
1184
1480
|
found = true;
|
|
1185
1481
|
break;
|
|
@@ -1187,7 +1483,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1187
1483
|
}
|
|
1188
1484
|
if (!found) {
|
|
1189
1485
|
issues.push(
|
|
1190
|
-
|
|
1486
|
+
issue5(
|
|
1191
1487
|
"QFAI-TRACE-002",
|
|
1192
1488
|
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
1193
1489
|
"warning",
|
|
@@ -1202,22 +1498,22 @@ function buildIdPattern(ids) {
|
|
|
1202
1498
|
const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
1203
1499
|
return new RegExp(`\\b(${escaped.join("|")})\\b`);
|
|
1204
1500
|
}
|
|
1205
|
-
function
|
|
1206
|
-
const
|
|
1501
|
+
function issue5(code, message, severity, file, rule, refs) {
|
|
1502
|
+
const issue6 = {
|
|
1207
1503
|
code,
|
|
1208
1504
|
severity,
|
|
1209
1505
|
message
|
|
1210
1506
|
};
|
|
1211
1507
|
if (file) {
|
|
1212
|
-
|
|
1508
|
+
issue6.file = file;
|
|
1213
1509
|
}
|
|
1214
1510
|
if (rule) {
|
|
1215
|
-
|
|
1511
|
+
issue6.rule = rule;
|
|
1216
1512
|
}
|
|
1217
1513
|
if (refs && refs.length > 0) {
|
|
1218
|
-
|
|
1514
|
+
issue6.refs = refs;
|
|
1219
1515
|
}
|
|
1220
|
-
return
|
|
1516
|
+
return issue6;
|
|
1221
1517
|
}
|
|
1222
1518
|
|
|
1223
1519
|
// src/core/validate.ts
|
|
@@ -1229,6 +1525,7 @@ async function validateProject(root, configResult) {
|
|
|
1229
1525
|
...await validateSpecs(root, config),
|
|
1230
1526
|
...await validateScenarios(root, config),
|
|
1231
1527
|
...await validateContracts(root, config),
|
|
1528
|
+
...await validateDefinedIds(root, config),
|
|
1232
1529
|
...await validateTraceability(root, config)
|
|
1233
1530
|
];
|
|
1234
1531
|
const toolVersion = await resolveToolVersion();
|
|
@@ -1241,8 +1538,8 @@ async function validateProject(root, configResult) {
|
|
|
1241
1538
|
}
|
|
1242
1539
|
function countIssues(issues) {
|
|
1243
1540
|
return issues.reduce(
|
|
1244
|
-
(acc,
|
|
1245
|
-
acc[
|
|
1541
|
+
(acc, issue6) => {
|
|
1542
|
+
acc[issue6.severity] += 1;
|
|
1246
1543
|
return acc;
|
|
1247
1544
|
},
|
|
1248
1545
|
{ info: 0, warning: 0, error: 0 }
|
|
@@ -1250,7 +1547,7 @@ function countIssues(issues) {
|
|
|
1250
1547
|
}
|
|
1251
1548
|
|
|
1252
1549
|
// src/core/report.ts
|
|
1253
|
-
var
|
|
1550
|
+
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
|
|
1254
1551
|
async function createReportData(root, validation, configResult) {
|
|
1255
1552
|
const resolved = configResult ?? await loadConfig(root);
|
|
1256
1553
|
const config = resolved.config;
|
|
@@ -1258,7 +1555,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1258
1555
|
const specRoot = resolvePath(root, config, "specDir");
|
|
1259
1556
|
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1260
1557
|
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1261
|
-
const rulesRoot = resolvePath(root, config, "rulesDir");
|
|
1262
1558
|
const apiRoot = resolvePath(root, config, "apiContractsDir");
|
|
1263
1559
|
const uiRoot = resolvePath(root, config, "uiContractsDir");
|
|
1264
1560
|
const dbRoot = resolvePath(root, config, "dataContractsDir");
|
|
@@ -1271,7 +1567,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1271
1567
|
const decisionFiles = await collectFiles(decisionsRoot, {
|
|
1272
1568
|
extensions: [".md"]
|
|
1273
1569
|
});
|
|
1274
|
-
const ruleFiles = await collectFiles(rulesRoot, { extensions: [".md"] });
|
|
1275
1570
|
const {
|
|
1276
1571
|
api: apiFiles,
|
|
1277
1572
|
ui: uiFiles,
|
|
@@ -1281,7 +1576,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1281
1576
|
...specFiles,
|
|
1282
1577
|
...scenarioFiles,
|
|
1283
1578
|
...decisionFiles,
|
|
1284
|
-
...ruleFiles,
|
|
1285
1579
|
...apiFiles,
|
|
1286
1580
|
...uiFiles,
|
|
1287
1581
|
...dbFiles
|
|
@@ -1307,7 +1601,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1307
1601
|
specs: specFiles.length,
|
|
1308
1602
|
scenarios: scenarioFiles.length,
|
|
1309
1603
|
decisions: decisionFiles.length,
|
|
1310
|
-
rules: ruleFiles.length,
|
|
1311
1604
|
contracts: {
|
|
1312
1605
|
api: apiFiles.length,
|
|
1313
1606
|
ui: uiFiles.length,
|
|
@@ -1342,7 +1635,6 @@ function formatReportMarkdown(data) {
|
|
|
1342
1635
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
1343
1636
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
1344
1637
|
lines.push(`- decisions: ${data.summary.decisions}`);
|
|
1345
|
-
lines.push(`- rules: ${data.summary.rules}`);
|
|
1346
1638
|
lines.push(
|
|
1347
1639
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
1348
1640
|
);
|
|
@@ -1378,7 +1670,7 @@ function formatReportMarkdown(data) {
|
|
|
1378
1670
|
lines.push("");
|
|
1379
1671
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
1380
1672
|
const traceIssues = data.issues.filter(
|
|
1381
|
-
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code === "QFAI_CONTRACT_ORPHAN"
|
|
1673
|
+
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-") || item.code === "QFAI_CONTRACT_ORPHAN"
|
|
1382
1674
|
);
|
|
1383
1675
|
if (traceIssues.length === 0) {
|
|
1384
1676
|
lines.push("- (none)");
|
|
@@ -1418,8 +1710,8 @@ async function collectIds(files) {
|
|
|
1418
1710
|
DATA: /* @__PURE__ */ new Set()
|
|
1419
1711
|
};
|
|
1420
1712
|
for (const file of files) {
|
|
1421
|
-
const text = await
|
|
1422
|
-
for (const prefix of
|
|
1713
|
+
const text = await readFile9(file, "utf-8");
|
|
1714
|
+
for (const prefix of ID_PREFIXES2) {
|
|
1423
1715
|
const ids = extractIds(text, prefix);
|
|
1424
1716
|
ids.forEach((id) => result[prefix].add(id));
|
|
1425
1717
|
}
|
|
@@ -1436,7 +1728,7 @@ async function collectIds(files) {
|
|
|
1436
1728
|
async function collectUpstreamIds(files) {
|
|
1437
1729
|
const ids = /* @__PURE__ */ new Set();
|
|
1438
1730
|
for (const file of files) {
|
|
1439
|
-
const text = await
|
|
1731
|
+
const text = await readFile9(file, "utf-8");
|
|
1440
1732
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
1441
1733
|
}
|
|
1442
1734
|
return ids;
|
|
@@ -1457,7 +1749,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
1457
1749
|
}
|
|
1458
1750
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
1459
1751
|
for (const file of targetFiles) {
|
|
1460
|
-
const text = await
|
|
1752
|
+
const text = await readFile9(file, "utf-8");
|
|
1461
1753
|
if (pattern.test(text)) {
|
|
1462
1754
|
return true;
|
|
1463
1755
|
}
|
|
@@ -1479,20 +1771,20 @@ function toSortedArray(values) {
|
|
|
1479
1771
|
}
|
|
1480
1772
|
function buildHotspots(issues) {
|
|
1481
1773
|
const map = /* @__PURE__ */ new Map();
|
|
1482
|
-
for (const
|
|
1483
|
-
if (!
|
|
1774
|
+
for (const issue6 of issues) {
|
|
1775
|
+
if (!issue6.file) {
|
|
1484
1776
|
continue;
|
|
1485
1777
|
}
|
|
1486
|
-
const current = map.get(
|
|
1487
|
-
file:
|
|
1778
|
+
const current = map.get(issue6.file) ?? {
|
|
1779
|
+
file: issue6.file,
|
|
1488
1780
|
total: 0,
|
|
1489
1781
|
error: 0,
|
|
1490
1782
|
warning: 0,
|
|
1491
1783
|
info: 0
|
|
1492
1784
|
};
|
|
1493
1785
|
current.total += 1;
|
|
1494
|
-
current[
|
|
1495
|
-
map.set(
|
|
1786
|
+
current[issue6.severity] += 1;
|
|
1787
|
+
map.set(issue6.file, current);
|
|
1496
1788
|
}
|
|
1497
1789
|
return Array.from(map.values()).sort(
|
|
1498
1790
|
(a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
|
|
@@ -1513,6 +1805,7 @@ export {
|
|
|
1513
1805
|
resolvePath,
|
|
1514
1806
|
resolveToolVersion,
|
|
1515
1807
|
validateContracts,
|
|
1808
|
+
validateDefinedIds,
|
|
1516
1809
|
validateProject,
|
|
1517
1810
|
validateScenarioContent,
|
|
1518
1811
|
validateScenarios,
|