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