qfai 0.4.0 → 0.4.4
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 +20 -1
- package/assets/init/.qfai/README.md +3 -1
- package/assets/init/.qfai/contracts/README.md +17 -8
- package/assets/init/.qfai/contracts/api/api-0001-sample.yaml +3 -2
- package/assets/init/.qfai/contracts/db/db-0001-sample.sql +2 -1
- package/assets/init/.qfai/contracts/ui/ui-0001-sample.yaml +3 -1
- package/assets/init/.qfai/promptpack/modes/change.md +3 -2
- package/assets/init/.qfai/promptpack/modes/compatibility.md +2 -0
- package/assets/init/.qfai/prompts/README.md +1 -0
- package/assets/init/.qfai/prompts/qfai-generate-test-globs.md +29 -0
- package/assets/init/.qfai/prompts/require-to-spec.md +4 -2
- package/assets/init/.qfai/specs/README.md +9 -2
- package/assets/init/.qfai/specs/spec-0001/spec.md +2 -0
- package/assets/init/root/qfai.config.yaml +6 -1
- package/assets/init/root/tests/qfai-traceability.sample.test.ts +2 -2
- package/dist/cli/index.cjs +885 -489
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +885 -489
- package/dist/cli/index.mjs.map +1 -1
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +4 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/contractIndex.d.ts +1 -2
- package/dist/core/contractIndex.d.ts.map +1 -1
- package/dist/core/contractIndex.js +10 -38
- package/dist/core/contractIndex.js.map +1 -1
- package/dist/core/contractsDecl.d.ts +3 -0
- package/dist/core/contractsDecl.d.ts.map +1 -0
- package/dist/core/contractsDecl.js +19 -0
- package/dist/core/contractsDecl.js.map +1 -0
- package/dist/core/fs.d.ts +5 -0
- package/dist/core/fs.d.ts.map +1 -1
- package/dist/core/fs.js +13 -0
- package/dist/core/fs.js.map +1 -1
- package/dist/core/ids.d.ts +1 -1
- package/dist/core/ids.d.ts.map +1 -1
- package/dist/core/ids.js +3 -3
- package/dist/core/ids.js.map +1 -1
- package/dist/core/parse/spec.d.ts +8 -0
- package/dist/core/parse/spec.d.ts.map +1 -1
- package/dist/core/parse/spec.js +43 -0
- package/dist/core/parse/spec.js.map +1 -1
- package/dist/core/report.d.ts +16 -2
- package/dist/core/report.d.ts.map +1 -1
- package/dist/core/report.js +144 -11
- package/dist/core/report.js.map +1 -1
- package/dist/core/scenarioModel.d.ts.map +1 -1
- package/dist/core/scenarioModel.js +3 -5
- package/dist/core/scenarioModel.js.map +1 -1
- package/dist/core/traceability.d.ts +15 -1
- package/dist/core/traceability.d.ts.map +1 -1
- package/dist/core/traceability.js +96 -9
- package/dist/core/traceability.js.map +1 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/validate.d.ts.map +1 -1
- package/dist/core/validate.js +12 -1
- package/dist/core/validate.js.map +1 -1
- package/dist/core/validators/contracts.d.ts.map +1 -1
- package/dist/core/validators/contracts.js +45 -18
- package/dist/core/validators/contracts.js.map +1 -1
- package/dist/core/validators/scenario.d.ts.map +1 -1
- package/dist/core/validators/scenario.js +2 -15
- package/dist/core/validators/scenario.js.map +1 -1
- package/dist/core/validators/spec.js +1 -1
- package/dist/core/validators/spec.js.map +1 -1
- package/dist/core/validators/traceability.d.ts.map +1 -1
- package/dist/core/validators/traceability.js +66 -34
- package/dist/core/validators/traceability.js.map +1 -1
- package/dist/index.cjs +869 -473
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -12
- package/dist/index.mjs +869 -473
- package/dist/index.mjs.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
package/dist/cli/index.cjs
CHANGED
|
@@ -178,7 +178,7 @@ function report(copied, skipped, dryRun, label) {
|
|
|
178
178
|
|
|
179
179
|
// src/cli/commands/report.ts
|
|
180
180
|
var import_promises16 = require("fs/promises");
|
|
181
|
-
var
|
|
181
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
182
182
|
|
|
183
183
|
// src/core/config.ts
|
|
184
184
|
var import_promises2 = require("fs/promises");
|
|
@@ -209,8 +209,9 @@ var defaultConfig = {
|
|
|
209
209
|
},
|
|
210
210
|
traceability: {
|
|
211
211
|
brMustHaveSc: true,
|
|
212
|
-
scMustTouchContracts: true,
|
|
213
212
|
scMustHaveTest: true,
|
|
213
|
+
testFileGlobs: [],
|
|
214
|
+
testFileExcludeGlobs: [],
|
|
214
215
|
scNoTestSeverity: "error",
|
|
215
216
|
allowOrphanContracts: false,
|
|
216
217
|
unknownContractIdSeverity: "error"
|
|
@@ -384,13 +385,6 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
384
385
|
configPath,
|
|
385
386
|
issues
|
|
386
387
|
),
|
|
387
|
-
scMustTouchContracts: readBoolean(
|
|
388
|
-
traceabilityRaw?.scMustTouchContracts,
|
|
389
|
-
base.traceability.scMustTouchContracts,
|
|
390
|
-
"validation.traceability.scMustTouchContracts",
|
|
391
|
-
configPath,
|
|
392
|
-
issues
|
|
393
|
-
),
|
|
394
388
|
scMustHaveTest: readBoolean(
|
|
395
389
|
traceabilityRaw?.scMustHaveTest,
|
|
396
390
|
base.traceability.scMustHaveTest,
|
|
@@ -398,6 +392,20 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
398
392
|
configPath,
|
|
399
393
|
issues
|
|
400
394
|
),
|
|
395
|
+
testFileGlobs: readStringArray(
|
|
396
|
+
traceabilityRaw?.testFileGlobs,
|
|
397
|
+
base.traceability.testFileGlobs,
|
|
398
|
+
"validation.traceability.testFileGlobs",
|
|
399
|
+
configPath,
|
|
400
|
+
issues
|
|
401
|
+
),
|
|
402
|
+
testFileExcludeGlobs: readStringArray(
|
|
403
|
+
traceabilityRaw?.testFileExcludeGlobs,
|
|
404
|
+
base.traceability.testFileExcludeGlobs,
|
|
405
|
+
"validation.traceability.testFileExcludeGlobs",
|
|
406
|
+
configPath,
|
|
407
|
+
issues
|
|
408
|
+
),
|
|
401
409
|
scNoTestSeverity: readTraceabilitySeverity(
|
|
402
410
|
traceabilityRaw?.scNoTestSeverity,
|
|
403
411
|
base.traceability.scNoTestSeverity,
|
|
@@ -531,7 +539,11 @@ function isRecord(value) {
|
|
|
531
539
|
|
|
532
540
|
// src/core/report.ts
|
|
533
541
|
var import_promises15 = require("fs/promises");
|
|
534
|
-
var
|
|
542
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
543
|
+
|
|
544
|
+
// src/core/contractIndex.ts
|
|
545
|
+
var import_promises6 = require("fs/promises");
|
|
546
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
535
547
|
|
|
536
548
|
// src/core/discovery.ts
|
|
537
549
|
var import_promises5 = require("fs/promises");
|
|
@@ -539,6 +551,7 @@ var import_promises5 = require("fs/promises");
|
|
|
539
551
|
// src/core/fs.ts
|
|
540
552
|
var import_promises3 = require("fs/promises");
|
|
541
553
|
var import_node_path5 = __toESM(require("path"), 1);
|
|
554
|
+
var import_fast_glob = __toESM(require("fast-glob"), 1);
|
|
542
555
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
543
556
|
"node_modules",
|
|
544
557
|
".git",
|
|
@@ -560,6 +573,18 @@ async function collectFiles(root, options = {}) {
|
|
|
560
573
|
await walk(root, root, ignoreDirs, extensions, entries);
|
|
561
574
|
return entries;
|
|
562
575
|
}
|
|
576
|
+
async function collectFilesByGlobs(root, options) {
|
|
577
|
+
if (options.globs.length === 0) {
|
|
578
|
+
return [];
|
|
579
|
+
}
|
|
580
|
+
return (0, import_fast_glob.default)(options.globs, {
|
|
581
|
+
cwd: root,
|
|
582
|
+
ignore: options.ignore ?? [],
|
|
583
|
+
onlyFiles: true,
|
|
584
|
+
absolute: true,
|
|
585
|
+
unique: true
|
|
586
|
+
});
|
|
587
|
+
}
|
|
563
588
|
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
564
589
|
const items = await (0, import_promises3.readdir)(current, { withFileTypes: true });
|
|
565
590
|
for (const item of items) {
|
|
@@ -671,15 +696,66 @@ async function exists3(target) {
|
|
|
671
696
|
}
|
|
672
697
|
}
|
|
673
698
|
|
|
699
|
+
// src/core/contractsDecl.ts
|
|
700
|
+
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
|
|
701
|
+
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
|
|
702
|
+
function extractDeclaredContractIds(text) {
|
|
703
|
+
const ids = [];
|
|
704
|
+
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
705
|
+
const id = match[1];
|
|
706
|
+
if (id) {
|
|
707
|
+
ids.push(id);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return ids;
|
|
711
|
+
}
|
|
712
|
+
function stripContractDeclarationLines(text) {
|
|
713
|
+
return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/core/contractIndex.ts
|
|
717
|
+
async function buildContractIndex(root, config) {
|
|
718
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
719
|
+
const uiRoot = import_node_path7.default.join(contractsRoot, "ui");
|
|
720
|
+
const apiRoot = import_node_path7.default.join(contractsRoot, "api");
|
|
721
|
+
const dbRoot = import_node_path7.default.join(contractsRoot, "db");
|
|
722
|
+
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
723
|
+
collectUiContractFiles(uiRoot),
|
|
724
|
+
collectApiContractFiles(apiRoot),
|
|
725
|
+
collectDataContractFiles(dbRoot)
|
|
726
|
+
]);
|
|
727
|
+
const index = {
|
|
728
|
+
ids: /* @__PURE__ */ new Set(),
|
|
729
|
+
idToFiles: /* @__PURE__ */ new Map(),
|
|
730
|
+
files: { ui: uiFiles, api: apiFiles, db: dbFiles }
|
|
731
|
+
};
|
|
732
|
+
await indexContractFiles(uiFiles, index);
|
|
733
|
+
await indexContractFiles(apiFiles, index);
|
|
734
|
+
await indexContractFiles(dbFiles, index);
|
|
735
|
+
return index;
|
|
736
|
+
}
|
|
737
|
+
async function indexContractFiles(files, index) {
|
|
738
|
+
for (const file of files) {
|
|
739
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
740
|
+
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function record(index, id, file) {
|
|
744
|
+
index.ids.add(id);
|
|
745
|
+
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
746
|
+
current.add(file);
|
|
747
|
+
index.idToFiles.set(id, current);
|
|
748
|
+
}
|
|
749
|
+
|
|
674
750
|
// src/core/ids.ts
|
|
675
|
-
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "
|
|
751
|
+
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
676
752
|
var STRICT_ID_PATTERNS = {
|
|
677
753
|
SPEC: /\bSPEC-\d{4}\b/g,
|
|
678
754
|
BR: /\bBR-\d{4}\b/g,
|
|
679
755
|
SC: /\bSC-\d{4}\b/g,
|
|
680
756
|
UI: /\bUI-\d{4}\b/g,
|
|
681
757
|
API: /\bAPI-\d{4}\b/g,
|
|
682
|
-
|
|
758
|
+
DB: /\bDB-\d{4}\b/g,
|
|
683
759
|
ADR: /\bADR-\d{4}\b/g
|
|
684
760
|
};
|
|
685
761
|
var LOOSE_ID_PATTERNS = {
|
|
@@ -688,7 +764,7 @@ var LOOSE_ID_PATTERNS = {
|
|
|
688
764
|
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
689
765
|
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
690
766
|
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
691
|
-
|
|
767
|
+
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
692
768
|
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
693
769
|
};
|
|
694
770
|
function extractIds(text, prefix) {
|
|
@@ -724,8 +800,170 @@ function isValidId(value, prefix) {
|
|
|
724
800
|
return strict.test(value);
|
|
725
801
|
}
|
|
726
802
|
|
|
803
|
+
// src/core/parse/markdown.ts
|
|
804
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
805
|
+
function parseHeadings(md) {
|
|
806
|
+
const lines = md.split(/\r?\n/);
|
|
807
|
+
const headings = [];
|
|
808
|
+
for (let i = 0; i < lines.length; i++) {
|
|
809
|
+
const line = lines[i] ?? "";
|
|
810
|
+
const match = line.match(HEADING_RE);
|
|
811
|
+
if (!match) continue;
|
|
812
|
+
const levelToken = match[1];
|
|
813
|
+
const title = match[2];
|
|
814
|
+
if (!levelToken || !title) continue;
|
|
815
|
+
headings.push({
|
|
816
|
+
level: levelToken.length,
|
|
817
|
+
title: title.trim(),
|
|
818
|
+
line: i + 1
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
return headings;
|
|
822
|
+
}
|
|
823
|
+
function extractH2Sections(md) {
|
|
824
|
+
const lines = md.split(/\r?\n/);
|
|
825
|
+
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
826
|
+
const sections = /* @__PURE__ */ new Map();
|
|
827
|
+
for (let i = 0; i < headings.length; i++) {
|
|
828
|
+
const current = headings[i];
|
|
829
|
+
if (!current) continue;
|
|
830
|
+
const next = headings[i + 1];
|
|
831
|
+
const startLine = current.line + 1;
|
|
832
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
833
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
834
|
+
sections.set(current.title.trim(), {
|
|
835
|
+
title: current.title.trim(),
|
|
836
|
+
startLine,
|
|
837
|
+
endLine,
|
|
838
|
+
body
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
return sections;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// src/core/parse/spec.ts
|
|
845
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
846
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
847
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
848
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
849
|
+
var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
|
|
850
|
+
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
851
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
852
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
853
|
+
function parseSpec(md, file) {
|
|
854
|
+
const headings = parseHeadings(md);
|
|
855
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
856
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
857
|
+
const sections = extractH2Sections(md);
|
|
858
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
859
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
860
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
861
|
+
const startLine = brSection?.startLine ?? 1;
|
|
862
|
+
const brs = [];
|
|
863
|
+
const brsWithoutPriority = [];
|
|
864
|
+
const brsWithInvalidPriority = [];
|
|
865
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
866
|
+
const lineText = brLines[i] ?? "";
|
|
867
|
+
const lineNumber = startLine + i;
|
|
868
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
869
|
+
if (validMatch) {
|
|
870
|
+
const id = validMatch[1];
|
|
871
|
+
const priority = validMatch[2];
|
|
872
|
+
const text = validMatch[3];
|
|
873
|
+
if (!id || !priority || !text) continue;
|
|
874
|
+
brs.push({
|
|
875
|
+
id,
|
|
876
|
+
priority,
|
|
877
|
+
text: text.trim(),
|
|
878
|
+
line: lineNumber
|
|
879
|
+
});
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
883
|
+
if (anyPriorityMatch) {
|
|
884
|
+
const id = anyPriorityMatch[1];
|
|
885
|
+
const priority = anyPriorityMatch[2];
|
|
886
|
+
const text = anyPriorityMatch[3];
|
|
887
|
+
if (!id || !priority || !text) continue;
|
|
888
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
889
|
+
brsWithInvalidPriority.push({
|
|
890
|
+
id,
|
|
891
|
+
priority,
|
|
892
|
+
text: text.trim(),
|
|
893
|
+
line: lineNumber
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
899
|
+
if (noPriorityMatch) {
|
|
900
|
+
const id = noPriorityMatch[1];
|
|
901
|
+
const text = noPriorityMatch[2];
|
|
902
|
+
if (!id || !text) continue;
|
|
903
|
+
brsWithoutPriority.push({
|
|
904
|
+
id,
|
|
905
|
+
text: text.trim(),
|
|
906
|
+
line: lineNumber
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
const parsed = {
|
|
911
|
+
file,
|
|
912
|
+
sections: sectionNames,
|
|
913
|
+
brs,
|
|
914
|
+
brsWithoutPriority,
|
|
915
|
+
brsWithInvalidPriority,
|
|
916
|
+
contractRefs: parseContractRefs(md)
|
|
917
|
+
};
|
|
918
|
+
if (specId) {
|
|
919
|
+
parsed.specId = specId;
|
|
920
|
+
}
|
|
921
|
+
return parsed;
|
|
922
|
+
}
|
|
923
|
+
function parseContractRefs(md) {
|
|
924
|
+
const lines = [];
|
|
925
|
+
for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
|
|
926
|
+
lines.push((match[1] ?? "").trim());
|
|
927
|
+
}
|
|
928
|
+
const ids = [];
|
|
929
|
+
const invalidTokens = [];
|
|
930
|
+
let hasNone = false;
|
|
931
|
+
for (const line of lines) {
|
|
932
|
+
if (line.length === 0) {
|
|
933
|
+
invalidTokens.push("(empty)");
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
const tokens = line.split(",").map((token) => token.trim());
|
|
937
|
+
for (const token of tokens) {
|
|
938
|
+
if (token.length === 0) {
|
|
939
|
+
invalidTokens.push("(empty)");
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
if (token === "none") {
|
|
943
|
+
hasNone = true;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
947
|
+
ids.push(token);
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
invalidTokens.push(token);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return {
|
|
954
|
+
lines,
|
|
955
|
+
ids: unique2(ids),
|
|
956
|
+
invalidTokens: unique2(invalidTokens),
|
|
957
|
+
hasNone
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function unique2(values) {
|
|
961
|
+
return Array.from(new Set(values));
|
|
962
|
+
}
|
|
963
|
+
|
|
727
964
|
// src/core/traceability.ts
|
|
728
|
-
var
|
|
965
|
+
var import_promises7 = require("fs/promises");
|
|
966
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
729
967
|
|
|
730
968
|
// src/core/gherkin/parse.ts
|
|
731
969
|
var import_gherkin = require("@cucumber/gherkin");
|
|
@@ -758,7 +996,7 @@ var SC_TAG_RE = /^SC-\d{4}$/;
|
|
|
758
996
|
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
759
997
|
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
760
998
|
var API_TAG_RE = /^API-\d{4}$/;
|
|
761
|
-
var
|
|
999
|
+
var DB_TAG_RE = /^DB-\d{4}$/;
|
|
762
1000
|
function parseScenarioDocument(text, uri) {
|
|
763
1001
|
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
764
1002
|
if (!gherkinDocument) {
|
|
@@ -787,10 +1025,10 @@ function buildScenarioAtoms(document) {
|
|
|
787
1025
|
return document.scenarios.map((scenario) => {
|
|
788
1026
|
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
789
1027
|
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
790
|
-
const brIds =
|
|
1028
|
+
const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
791
1029
|
const contractIds = /* @__PURE__ */ new Set();
|
|
792
1030
|
scenario.tags.forEach((tag) => {
|
|
793
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) ||
|
|
1031
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
|
|
794
1032
|
contractIds.add(tag);
|
|
795
1033
|
}
|
|
796
1034
|
});
|
|
@@ -798,7 +1036,7 @@ function buildScenarioAtoms(document) {
|
|
|
798
1036
|
for (const text of collectStepTexts(step)) {
|
|
799
1037
|
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
800
1038
|
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
801
|
-
extractIds(text, "
|
|
1039
|
+
extractIds(text, "DB").forEach((id) => contractIds.add(id));
|
|
802
1040
|
}
|
|
803
1041
|
}
|
|
804
1042
|
const atom = {
|
|
@@ -877,16 +1115,37 @@ function collectStepTexts(step) {
|
|
|
877
1115
|
}
|
|
878
1116
|
return texts;
|
|
879
1117
|
}
|
|
880
|
-
function
|
|
1118
|
+
function unique3(values) {
|
|
881
1119
|
return Array.from(new Set(values));
|
|
882
1120
|
}
|
|
883
1121
|
|
|
884
1122
|
// src/core/traceability.ts
|
|
885
1123
|
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1124
|
+
var SC_TEST_ANNOTATION_RE = /\bQFAI:SC-(\d{4})\b/g;
|
|
1125
|
+
var DEFAULT_TEST_FILE_EXCLUDE_GLOBS = [
|
|
1126
|
+
"**/node_modules/**",
|
|
1127
|
+
"**/.git/**",
|
|
1128
|
+
"**/.qfai/**",
|
|
1129
|
+
"**/dist/**",
|
|
1130
|
+
"**/build/**",
|
|
1131
|
+
"**/coverage/**",
|
|
1132
|
+
"**/.next/**",
|
|
1133
|
+
"**/out/**"
|
|
1134
|
+
];
|
|
1135
|
+
function extractAnnotatedScIds(text) {
|
|
1136
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1137
|
+
for (const match of text.matchAll(SC_TEST_ANNOTATION_RE)) {
|
|
1138
|
+
const suffix = match[1];
|
|
1139
|
+
if (suffix) {
|
|
1140
|
+
ids.add(`SC-${suffix}`);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return Array.from(ids);
|
|
1144
|
+
}
|
|
886
1145
|
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
887
1146
|
const scIds = /* @__PURE__ */ new Set();
|
|
888
1147
|
for (const file of scenarioFiles) {
|
|
889
|
-
const text = await (0,
|
|
1148
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
890
1149
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
891
1150
|
if (!document || errors.length > 0) {
|
|
892
1151
|
continue;
|
|
@@ -901,14 +1160,67 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
|
901
1160
|
}
|
|
902
1161
|
return scIds;
|
|
903
1162
|
}
|
|
904
|
-
async function
|
|
1163
|
+
async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
1164
|
+
const sources = /* @__PURE__ */ new Map();
|
|
1165
|
+
for (const file of scenarioFiles) {
|
|
1166
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
1167
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1168
|
+
if (!document || errors.length > 0) {
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
for (const scenario of document.scenarios) {
|
|
1172
|
+
for (const tag of scenario.tags) {
|
|
1173
|
+
if (!SC_TAG_RE2.test(tag)) {
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
const current = sources.get(tag) ?? /* @__PURE__ */ new Set();
|
|
1177
|
+
current.add(file);
|
|
1178
|
+
sources.set(tag, current);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return sources;
|
|
1183
|
+
}
|
|
1184
|
+
async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
905
1185
|
const refs = /* @__PURE__ */ new Map();
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1186
|
+
const normalizedGlobs = normalizeGlobs(globs);
|
|
1187
|
+
const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
|
|
1188
|
+
const mergedExcludeGlobs = Array.from(
|
|
1189
|
+
/* @__PURE__ */ new Set([...DEFAULT_TEST_FILE_EXCLUDE_GLOBS, ...normalizedExcludeGlobs])
|
|
1190
|
+
);
|
|
1191
|
+
if (normalizedGlobs.length === 0) {
|
|
1192
|
+
return {
|
|
1193
|
+
refs,
|
|
1194
|
+
scan: {
|
|
1195
|
+
globs: normalizedGlobs,
|
|
1196
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
1197
|
+
matchedFileCount: 0
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
let files = [];
|
|
1202
|
+
try {
|
|
1203
|
+
files = await collectFilesByGlobs(root, {
|
|
1204
|
+
globs: normalizedGlobs,
|
|
1205
|
+
ignore: mergedExcludeGlobs
|
|
1206
|
+
});
|
|
1207
|
+
} catch (error2) {
|
|
1208
|
+
return {
|
|
1209
|
+
refs,
|
|
1210
|
+
scan: {
|
|
1211
|
+
globs: normalizedGlobs,
|
|
1212
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
1213
|
+
matchedFileCount: 0
|
|
1214
|
+
},
|
|
1215
|
+
error: formatError3(error2)
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
const normalizedFiles = Array.from(
|
|
1219
|
+
new Set(files.map((file) => import_node_path8.default.normalize(file)))
|
|
1220
|
+
);
|
|
1221
|
+
for (const file of normalizedFiles) {
|
|
1222
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
1223
|
+
const scIds = extractAnnotatedScIds(text);
|
|
912
1224
|
if (scIds.length === 0) {
|
|
913
1225
|
continue;
|
|
914
1226
|
}
|
|
@@ -918,7 +1230,14 @@ async function collectScTestReferences(testsRoot) {
|
|
|
918
1230
|
refs.set(scId, current);
|
|
919
1231
|
}
|
|
920
1232
|
}
|
|
921
|
-
return
|
|
1233
|
+
return {
|
|
1234
|
+
refs,
|
|
1235
|
+
scan: {
|
|
1236
|
+
globs: normalizedGlobs,
|
|
1237
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
1238
|
+
matchedFileCount: normalizedFiles.length
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
922
1241
|
}
|
|
923
1242
|
function buildScCoverage(scIds, refs) {
|
|
924
1243
|
const sortedScIds = toSortedArray(scIds);
|
|
@@ -946,18 +1265,27 @@ function buildScCoverage(scIds, refs) {
|
|
|
946
1265
|
function toSortedArray(values) {
|
|
947
1266
|
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
948
1267
|
}
|
|
1268
|
+
function normalizeGlobs(globs) {
|
|
1269
|
+
return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
1270
|
+
}
|
|
1271
|
+
function formatError3(error2) {
|
|
1272
|
+
if (error2 instanceof Error) {
|
|
1273
|
+
return error2.message;
|
|
1274
|
+
}
|
|
1275
|
+
return String(error2);
|
|
1276
|
+
}
|
|
949
1277
|
|
|
950
1278
|
// src/core/version.ts
|
|
951
|
-
var
|
|
952
|
-
var
|
|
1279
|
+
var import_promises8 = require("fs/promises");
|
|
1280
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
953
1281
|
var import_node_url2 = require("url");
|
|
954
1282
|
async function resolveToolVersion() {
|
|
955
|
-
if ("0.4.
|
|
956
|
-
return "0.4.
|
|
1283
|
+
if ("0.4.4".length > 0) {
|
|
1284
|
+
return "0.4.4";
|
|
957
1285
|
}
|
|
958
1286
|
try {
|
|
959
1287
|
const packagePath = resolvePackageJsonPath();
|
|
960
|
-
const raw = await (0,
|
|
1288
|
+
const raw = await (0, import_promises8.readFile)(packagePath, "utf-8");
|
|
961
1289
|
const parsed = JSON.parse(raw);
|
|
962
1290
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
963
1291
|
return version.length > 0 ? version : "unknown";
|
|
@@ -968,71 +1296,42 @@ async function resolveToolVersion() {
|
|
|
968
1296
|
function resolvePackageJsonPath() {
|
|
969
1297
|
const base = __filename;
|
|
970
1298
|
const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
|
|
971
|
-
return
|
|
1299
|
+
return import_node_path9.default.resolve(import_node_path9.default.dirname(basePath), "../../package.json");
|
|
972
1300
|
}
|
|
973
1301
|
|
|
974
1302
|
// src/core/validators/contracts.ts
|
|
975
|
-
var
|
|
976
|
-
var
|
|
1303
|
+
var import_promises9 = require("fs/promises");
|
|
1304
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
977
1305
|
|
|
978
1306
|
// src/core/contracts.ts
|
|
979
|
-
var
|
|
1307
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
980
1308
|
var import_yaml2 = require("yaml");
|
|
981
1309
|
function parseStructuredContract(file, text) {
|
|
982
|
-
const ext =
|
|
1310
|
+
const ext = import_node_path10.default.extname(file).toLowerCase();
|
|
983
1311
|
if (ext === ".json") {
|
|
984
1312
|
return JSON.parse(text);
|
|
985
1313
|
}
|
|
986
1314
|
return (0, import_yaml2.parse)(text);
|
|
987
1315
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
extractIds(operationId, "API").forEach((id) => ids.add(id));
|
|
998
|
-
}
|
|
999
|
-
return Array.from(ids);
|
|
1000
|
-
}
|
|
1001
|
-
function collectOperationIds(value, out) {
|
|
1002
|
-
if (!value || typeof value !== "object") {
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
if (Array.isArray(value)) {
|
|
1006
|
-
for (const item of value) {
|
|
1007
|
-
collectOperationIds(item, out);
|
|
1008
|
-
}
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
1012
|
-
if (key === "operationId" && typeof entry === "string") {
|
|
1013
|
-
out.add(entry);
|
|
1014
|
-
continue;
|
|
1015
|
-
}
|
|
1016
|
-
collectOperationIds(entry, out);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
// src/core/validators/contracts.ts
|
|
1021
|
-
var SQL_DANGEROUS_PATTERNS = [
|
|
1022
|
-
{ pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
|
|
1023
|
-
{ pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
|
|
1024
|
-
{ pattern: /\bTRUNCATE\b/i, label: "TRUNCATE" },
|
|
1025
|
-
{
|
|
1026
|
-
pattern: /\bALTER\s+TABLE\b[\s\S]*\bDROP\b/i,
|
|
1027
|
-
label: "ALTER TABLE ... DROP"
|
|
1316
|
+
|
|
1317
|
+
// src/core/validators/contracts.ts
|
|
1318
|
+
var SQL_DANGEROUS_PATTERNS = [
|
|
1319
|
+
{ pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
|
|
1320
|
+
{ pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
|
|
1321
|
+
{ pattern: /\bTRUNCATE\b/i, label: "TRUNCATE" },
|
|
1322
|
+
{
|
|
1323
|
+
pattern: /\bALTER\s+TABLE\b[\s\S]*\bDROP\b/i,
|
|
1324
|
+
label: "ALTER TABLE ... DROP"
|
|
1028
1325
|
}
|
|
1029
1326
|
];
|
|
1030
1327
|
async function validateContracts(root, config) {
|
|
1031
1328
|
const issues = [];
|
|
1032
1329
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1033
|
-
issues.push(...await validateUiContracts(
|
|
1034
|
-
issues.push(...await validateApiContracts(
|
|
1035
|
-
issues.push(...await validateDataContracts(
|
|
1330
|
+
issues.push(...await validateUiContracts(import_node_path11.default.join(contractsRoot, "ui")));
|
|
1331
|
+
issues.push(...await validateApiContracts(import_node_path11.default.join(contractsRoot, "api")));
|
|
1332
|
+
issues.push(...await validateDataContracts(import_node_path11.default.join(contractsRoot, "db")));
|
|
1333
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
1334
|
+
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1036
1335
|
return issues;
|
|
1037
1336
|
}
|
|
1038
1337
|
async function validateUiContracts(uiRoot) {
|
|
@@ -1050,14 +1349,14 @@ async function validateUiContracts(uiRoot) {
|
|
|
1050
1349
|
}
|
|
1051
1350
|
const issues = [];
|
|
1052
1351
|
for (const file of files) {
|
|
1053
|
-
const text = await (0,
|
|
1352
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1054
1353
|
const invalidIds = extractInvalidIds(text, [
|
|
1055
1354
|
"SPEC",
|
|
1056
1355
|
"BR",
|
|
1057
1356
|
"SC",
|
|
1058
1357
|
"UI",
|
|
1059
1358
|
"API",
|
|
1060
|
-
"
|
|
1359
|
+
"DB",
|
|
1061
1360
|
"ADR"
|
|
1062
1361
|
]);
|
|
1063
1362
|
if (invalidIds.length > 0) {
|
|
@@ -1072,32 +1371,20 @@ async function validateUiContracts(uiRoot) {
|
|
|
1072
1371
|
)
|
|
1073
1372
|
);
|
|
1074
1373
|
}
|
|
1075
|
-
|
|
1374
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1375
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
|
|
1076
1376
|
try {
|
|
1077
|
-
|
|
1377
|
+
parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
1078
1378
|
} catch (error2) {
|
|
1079
1379
|
issues.push(
|
|
1080
1380
|
issue(
|
|
1081
1381
|
"QFAI-CONTRACT-001",
|
|
1082
|
-
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1382
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
|
|
1083
1383
|
"error",
|
|
1084
1384
|
file,
|
|
1085
1385
|
"contracts.ui.parse"
|
|
1086
1386
|
)
|
|
1087
1387
|
);
|
|
1088
|
-
continue;
|
|
1089
|
-
}
|
|
1090
|
-
const uiIds = extractUiContractIds(doc);
|
|
1091
|
-
if (uiIds.length === 0) {
|
|
1092
|
-
issues.push(
|
|
1093
|
-
issue(
|
|
1094
|
-
"QFAI-CONTRACT-002",
|
|
1095
|
-
`UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
1096
|
-
"error",
|
|
1097
|
-
file,
|
|
1098
|
-
"contracts.ui.id"
|
|
1099
|
-
)
|
|
1100
|
-
);
|
|
1101
1388
|
}
|
|
1102
1389
|
}
|
|
1103
1390
|
return issues;
|
|
@@ -1117,14 +1404,14 @@ async function validateApiContracts(apiRoot) {
|
|
|
1117
1404
|
}
|
|
1118
1405
|
const issues = [];
|
|
1119
1406
|
for (const file of files) {
|
|
1120
|
-
const text = await (0,
|
|
1407
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1121
1408
|
const invalidIds = extractInvalidIds(text, [
|
|
1122
1409
|
"SPEC",
|
|
1123
1410
|
"BR",
|
|
1124
1411
|
"SC",
|
|
1125
1412
|
"UI",
|
|
1126
1413
|
"API",
|
|
1127
|
-
"
|
|
1414
|
+
"DB",
|
|
1128
1415
|
"ADR"
|
|
1129
1416
|
]);
|
|
1130
1417
|
if (invalidIds.length > 0) {
|
|
@@ -1139,14 +1426,16 @@ async function validateApiContracts(apiRoot) {
|
|
|
1139
1426
|
)
|
|
1140
1427
|
);
|
|
1141
1428
|
}
|
|
1429
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1430
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "API"));
|
|
1142
1431
|
let doc;
|
|
1143
1432
|
try {
|
|
1144
|
-
doc = parseStructuredContract(file, text);
|
|
1433
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
1145
1434
|
} catch (error2) {
|
|
1146
1435
|
issues.push(
|
|
1147
1436
|
issue(
|
|
1148
1437
|
"QFAI-CONTRACT-001",
|
|
1149
|
-
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1438
|
+
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
|
|
1150
1439
|
"error",
|
|
1151
1440
|
file,
|
|
1152
1441
|
"contracts.api.parse"
|
|
@@ -1165,18 +1454,6 @@ async function validateApiContracts(apiRoot) {
|
|
|
1165
1454
|
)
|
|
1166
1455
|
);
|
|
1167
1456
|
}
|
|
1168
|
-
const apiIds = extractApiContractIds(doc);
|
|
1169
|
-
if (apiIds.length === 0) {
|
|
1170
|
-
issues.push(
|
|
1171
|
-
issue(
|
|
1172
|
-
"QFAI-CONTRACT-002",
|
|
1173
|
-
`API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
1174
|
-
"error",
|
|
1175
|
-
file,
|
|
1176
|
-
"contracts.api.id"
|
|
1177
|
-
)
|
|
1178
|
-
);
|
|
1179
|
-
}
|
|
1180
1457
|
}
|
|
1181
1458
|
return issues;
|
|
1182
1459
|
}
|
|
@@ -1185,24 +1462,24 @@ async function validateDataContracts(dataRoot) {
|
|
|
1185
1462
|
if (files.length === 0) {
|
|
1186
1463
|
return [
|
|
1187
1464
|
issue(
|
|
1188
|
-
"QFAI-
|
|
1189
|
-
"
|
|
1465
|
+
"QFAI-DB-000",
|
|
1466
|
+
"DB \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1190
1467
|
"info",
|
|
1191
1468
|
dataRoot,
|
|
1192
|
-
"contracts.
|
|
1469
|
+
"contracts.db.files"
|
|
1193
1470
|
)
|
|
1194
1471
|
];
|
|
1195
1472
|
}
|
|
1196
1473
|
const issues = [];
|
|
1197
1474
|
for (const file of files) {
|
|
1198
|
-
const text = await (0,
|
|
1475
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1199
1476
|
const invalidIds = extractInvalidIds(text, [
|
|
1200
1477
|
"SPEC",
|
|
1201
1478
|
"BR",
|
|
1202
1479
|
"SC",
|
|
1203
1480
|
"UI",
|
|
1204
1481
|
"API",
|
|
1205
|
-
"
|
|
1482
|
+
"DB",
|
|
1206
1483
|
"ADR"
|
|
1207
1484
|
]);
|
|
1208
1485
|
if (invalidIds.length > 0) {
|
|
@@ -1217,6 +1494,8 @@ async function validateDataContracts(dataRoot) {
|
|
|
1217
1494
|
)
|
|
1218
1495
|
);
|
|
1219
1496
|
}
|
|
1497
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1498
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "DB"));
|
|
1220
1499
|
issues.push(...lintSql(text, file));
|
|
1221
1500
|
}
|
|
1222
1501
|
return issues;
|
|
@@ -1227,21 +1506,87 @@ function lintSql(text, file) {
|
|
|
1227
1506
|
if (pattern.test(text)) {
|
|
1228
1507
|
issues.push(
|
|
1229
1508
|
issue(
|
|
1230
|
-
"QFAI-
|
|
1509
|
+
"QFAI-DB-001",
|
|
1231
1510
|
`\u5371\u967A\u306A SQL \u64CD\u4F5C\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059: ${label}`,
|
|
1232
1511
|
"warning",
|
|
1233
1512
|
file,
|
|
1234
|
-
"contracts.
|
|
1513
|
+
"contracts.db.sql"
|
|
1235
1514
|
)
|
|
1236
1515
|
);
|
|
1237
1516
|
}
|
|
1238
1517
|
}
|
|
1239
1518
|
return issues;
|
|
1240
1519
|
}
|
|
1520
|
+
function validateDeclaredContractIds(ids, file, kind) {
|
|
1521
|
+
const issues = [];
|
|
1522
|
+
if (ids.length === 0) {
|
|
1523
|
+
issues.push(
|
|
1524
|
+
issue(
|
|
1525
|
+
"QFAI-CONTRACT-010",
|
|
1526
|
+
`\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
|
|
1527
|
+
"error",
|
|
1528
|
+
file,
|
|
1529
|
+
"contracts.declaration"
|
|
1530
|
+
)
|
|
1531
|
+
);
|
|
1532
|
+
return issues;
|
|
1533
|
+
}
|
|
1534
|
+
if (ids.length > 1) {
|
|
1535
|
+
issues.push(
|
|
1536
|
+
issue(
|
|
1537
|
+
"QFAI-CONTRACT-011",
|
|
1538
|
+
`\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B\u8907\u6570\u306E QFAI-CONTRACT-ID \u304C\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${ids.join(
|
|
1539
|
+
", "
|
|
1540
|
+
)}`,
|
|
1541
|
+
"error",
|
|
1542
|
+
file,
|
|
1543
|
+
"contracts.declaration",
|
|
1544
|
+
ids
|
|
1545
|
+
)
|
|
1546
|
+
);
|
|
1547
|
+
return issues;
|
|
1548
|
+
}
|
|
1549
|
+
const [id] = ids;
|
|
1550
|
+
if (id && !id.startsWith(`${kind}-`)) {
|
|
1551
|
+
issues.push(
|
|
1552
|
+
issue(
|
|
1553
|
+
"QFAI-CONTRACT-013",
|
|
1554
|
+
`\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E QFAI-CONTRACT-ID \u304C ${kind}- \u3067\u306F\u3042\u308A\u307E\u305B\u3093: ${id}`,
|
|
1555
|
+
"error",
|
|
1556
|
+
file,
|
|
1557
|
+
"contracts.declarationPrefix",
|
|
1558
|
+
[id]
|
|
1559
|
+
)
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
return issues;
|
|
1563
|
+
}
|
|
1564
|
+
function validateDuplicateContractIds(contractIndex) {
|
|
1565
|
+
const issues = [];
|
|
1566
|
+
for (const [id, files] of contractIndex.idToFiles.entries()) {
|
|
1567
|
+
if (files.size <= 1) {
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
const sortedFiles = Array.from(files).sort((a, b) => a.localeCompare(b));
|
|
1571
|
+
issues.push(
|
|
1572
|
+
issue(
|
|
1573
|
+
"QFAI-CONTRACT-012",
|
|
1574
|
+
`\u5951\u7D04 ID \u304C\u8907\u6570\u30D5\u30A1\u30A4\u30EB\u3067\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${id} (${sortedFiles.join(
|
|
1575
|
+
", "
|
|
1576
|
+
)})`,
|
|
1577
|
+
"error",
|
|
1578
|
+
sortedFiles[0],
|
|
1579
|
+
"contracts.idDuplicate",
|
|
1580
|
+
[id]
|
|
1581
|
+
)
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
return issues;
|
|
1585
|
+
}
|
|
1241
1586
|
function hasOpenApi(doc) {
|
|
1242
1587
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
1243
1588
|
}
|
|
1244
|
-
function
|
|
1589
|
+
function formatError4(error2) {
|
|
1245
1590
|
if (error2 instanceof Error) {
|
|
1246
1591
|
return error2.message;
|
|
1247
1592
|
}
|
|
@@ -1266,8 +1611,8 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1266
1611
|
}
|
|
1267
1612
|
|
|
1268
1613
|
// src/core/validators/delta.ts
|
|
1269
|
-
var
|
|
1270
|
-
var
|
|
1614
|
+
var import_promises10 = require("fs/promises");
|
|
1615
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
1271
1616
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1272
1617
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1273
1618
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1277,268 +1622,87 @@ async function validateDeltas(root, config) {
|
|
|
1277
1622
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1278
1623
|
const packs = await collectSpecPackDirs(specsRoot);
|
|
1279
1624
|
if (packs.length === 0) {
|
|
1280
|
-
return [];
|
|
1281
|
-
}
|
|
1282
|
-
const issues = [];
|
|
1283
|
-
for (const pack of packs) {
|
|
1284
|
-
const deltaPath =
|
|
1285
|
-
let text;
|
|
1286
|
-
try {
|
|
1287
|
-
text = await (0,
|
|
1288
|
-
} catch (error2) {
|
|
1289
|
-
if (isMissingFileError2(error2)) {
|
|
1290
|
-
issues.push(
|
|
1291
|
-
issue2(
|
|
1292
|
-
"QFAI-DELTA-001",
|
|
1293
|
-
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1294
|
-
"error",
|
|
1295
|
-
deltaPath,
|
|
1296
|
-
"delta.exists"
|
|
1297
|
-
)
|
|
1298
|
-
);
|
|
1299
|
-
continue;
|
|
1300
|
-
}
|
|
1301
|
-
throw error2;
|
|
1302
|
-
}
|
|
1303
|
-
const hasSection = SECTION_RE.test(text);
|
|
1304
|
-
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
1305
|
-
const hasChange = CHANGE_LINE_RE.test(text);
|
|
1306
|
-
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
1307
|
-
issues.push(
|
|
1308
|
-
issue2(
|
|
1309
|
-
"QFAI-DELTA-002",
|
|
1310
|
-
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
1311
|
-
"error",
|
|
1312
|
-
deltaPath,
|
|
1313
|
-
"delta.section"
|
|
1314
|
-
)
|
|
1315
|
-
);
|
|
1316
|
-
continue;
|
|
1317
|
-
}
|
|
1318
|
-
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
1319
|
-
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
1320
|
-
if (compatibilityChecked === changeChecked) {
|
|
1321
|
-
issues.push(
|
|
1322
|
-
issue2(
|
|
1323
|
-
"QFAI-DELTA-003",
|
|
1324
|
-
"delta.md \u306E\u5909\u66F4\u533A\u5206\u306F\u3069\u3061\u3089\u304B1\u3064\u3060\u3051\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4E21\u65B9ON/\u4E21\u65B9OFF\u306F\u7121\u52B9\u3067\u3059\uFF09\u3002",
|
|
1325
|
-
"error",
|
|
1326
|
-
deltaPath,
|
|
1327
|
-
"delta.classification"
|
|
1328
|
-
)
|
|
1329
|
-
);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
return issues;
|
|
1333
|
-
}
|
|
1334
|
-
function isMissingFileError2(error2) {
|
|
1335
|
-
if (!error2 || typeof error2 !== "object") {
|
|
1336
|
-
return false;
|
|
1337
|
-
}
|
|
1338
|
-
return error2.code === "ENOENT";
|
|
1339
|
-
}
|
|
1340
|
-
function issue2(code, message, severity, file, rule, refs) {
|
|
1341
|
-
const issue7 = {
|
|
1342
|
-
code,
|
|
1343
|
-
severity,
|
|
1344
|
-
message
|
|
1345
|
-
};
|
|
1346
|
-
if (file) {
|
|
1347
|
-
issue7.file = file;
|
|
1348
|
-
}
|
|
1349
|
-
if (rule) {
|
|
1350
|
-
issue7.rule = rule;
|
|
1351
|
-
}
|
|
1352
|
-
if (refs && refs.length > 0) {
|
|
1353
|
-
issue7.refs = refs;
|
|
1354
|
-
}
|
|
1355
|
-
return issue7;
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
// src/core/validators/ids.ts
|
|
1359
|
-
var import_promises11 = require("fs/promises");
|
|
1360
|
-
var import_node_path12 = __toESM(require("path"), 1);
|
|
1361
|
-
|
|
1362
|
-
// src/core/contractIndex.ts
|
|
1363
|
-
var import_promises10 = require("fs/promises");
|
|
1364
|
-
var import_node_path11 = __toESM(require("path"), 1);
|
|
1365
|
-
async function buildContractIndex(root, config) {
|
|
1366
|
-
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1367
|
-
const uiRoot = import_node_path11.default.join(contractsRoot, "ui");
|
|
1368
|
-
const apiRoot = import_node_path11.default.join(contractsRoot, "api");
|
|
1369
|
-
const dataRoot = import_node_path11.default.join(contractsRoot, "db");
|
|
1370
|
-
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
1371
|
-
collectUiContractFiles(uiRoot),
|
|
1372
|
-
collectApiContractFiles(apiRoot),
|
|
1373
|
-
collectDataContractFiles(dataRoot)
|
|
1374
|
-
]);
|
|
1375
|
-
const index = {
|
|
1376
|
-
ids: /* @__PURE__ */ new Set(),
|
|
1377
|
-
idToFiles: /* @__PURE__ */ new Map(),
|
|
1378
|
-
files: { ui: uiFiles, api: apiFiles, data: dataFiles },
|
|
1379
|
-
structuredParseFailedFiles: /* @__PURE__ */ new Set()
|
|
1380
|
-
};
|
|
1381
|
-
await indexUiContracts(uiFiles, index);
|
|
1382
|
-
await indexApiContracts(apiFiles, index);
|
|
1383
|
-
await indexDataContracts(dataFiles, index);
|
|
1384
|
-
return index;
|
|
1385
|
-
}
|
|
1386
|
-
async function indexUiContracts(files, index) {
|
|
1387
|
-
for (const file of files) {
|
|
1388
|
-
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1389
|
-
try {
|
|
1390
|
-
const doc = parseStructuredContract(file, text);
|
|
1391
|
-
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
1392
|
-
} catch {
|
|
1393
|
-
index.structuredParseFailedFiles.add(file);
|
|
1394
|
-
extractIds(text, "UI").forEach((id) => record(index, id, file));
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
async function indexApiContracts(files, index) {
|
|
1399
|
-
for (const file of files) {
|
|
1400
|
-
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1401
|
-
try {
|
|
1402
|
-
const doc = parseStructuredContract(file, text);
|
|
1403
|
-
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
1404
|
-
} catch {
|
|
1405
|
-
index.structuredParseFailedFiles.add(file);
|
|
1406
|
-
extractIds(text, "API").forEach((id) => record(index, id, file));
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
async function indexDataContracts(files, index) {
|
|
1411
|
-
for (const file of files) {
|
|
1412
|
-
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1413
|
-
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
function record(index, id, file) {
|
|
1417
|
-
index.ids.add(id);
|
|
1418
|
-
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
1419
|
-
current.add(file);
|
|
1420
|
-
index.idToFiles.set(id, current);
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
// src/core/parse/markdown.ts
|
|
1424
|
-
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1425
|
-
function parseHeadings(md) {
|
|
1426
|
-
const lines = md.split(/\r?\n/);
|
|
1427
|
-
const headings = [];
|
|
1428
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1429
|
-
const line = lines[i] ?? "";
|
|
1430
|
-
const match = line.match(HEADING_RE);
|
|
1431
|
-
if (!match) continue;
|
|
1432
|
-
const levelToken = match[1];
|
|
1433
|
-
const title = match[2];
|
|
1434
|
-
if (!levelToken || !title) continue;
|
|
1435
|
-
headings.push({
|
|
1436
|
-
level: levelToken.length,
|
|
1437
|
-
title: title.trim(),
|
|
1438
|
-
line: i + 1
|
|
1439
|
-
});
|
|
1440
|
-
}
|
|
1441
|
-
return headings;
|
|
1442
|
-
}
|
|
1443
|
-
function extractH2Sections(md) {
|
|
1444
|
-
const lines = md.split(/\r?\n/);
|
|
1445
|
-
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1446
|
-
const sections = /* @__PURE__ */ new Map();
|
|
1447
|
-
for (let i = 0; i < headings.length; i++) {
|
|
1448
|
-
const current = headings[i];
|
|
1449
|
-
if (!current) continue;
|
|
1450
|
-
const next = headings[i + 1];
|
|
1451
|
-
const startLine = current.line + 1;
|
|
1452
|
-
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1453
|
-
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1454
|
-
sections.set(current.title.trim(), {
|
|
1455
|
-
title: current.title.trim(),
|
|
1456
|
-
startLine,
|
|
1457
|
-
endLine,
|
|
1458
|
-
body
|
|
1459
|
-
});
|
|
1460
|
-
}
|
|
1461
|
-
return sections;
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
// src/core/parse/spec.ts
|
|
1465
|
-
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1466
|
-
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1467
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1468
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1469
|
-
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1470
|
-
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1471
|
-
function parseSpec(md, file) {
|
|
1472
|
-
const headings = parseHeadings(md);
|
|
1473
|
-
const h1 = headings.find((heading) => heading.level === 1);
|
|
1474
|
-
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1475
|
-
const sections = extractH2Sections(md);
|
|
1476
|
-
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1477
|
-
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1478
|
-
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1479
|
-
const startLine = brSection?.startLine ?? 1;
|
|
1480
|
-
const brs = [];
|
|
1481
|
-
const brsWithoutPriority = [];
|
|
1482
|
-
const brsWithInvalidPriority = [];
|
|
1483
|
-
for (let i = 0; i < brLines.length; i++) {
|
|
1484
|
-
const lineText = brLines[i] ?? "";
|
|
1485
|
-
const lineNumber = startLine + i;
|
|
1486
|
-
const validMatch = lineText.match(BR_LINE_RE);
|
|
1487
|
-
if (validMatch) {
|
|
1488
|
-
const id = validMatch[1];
|
|
1489
|
-
const priority = validMatch[2];
|
|
1490
|
-
const text = validMatch[3];
|
|
1491
|
-
if (!id || !priority || !text) continue;
|
|
1492
|
-
brs.push({
|
|
1493
|
-
id,
|
|
1494
|
-
priority,
|
|
1495
|
-
text: text.trim(),
|
|
1496
|
-
line: lineNumber
|
|
1497
|
-
});
|
|
1498
|
-
continue;
|
|
1499
|
-
}
|
|
1500
|
-
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1501
|
-
if (anyPriorityMatch) {
|
|
1502
|
-
const id = anyPriorityMatch[1];
|
|
1503
|
-
const priority = anyPriorityMatch[2];
|
|
1504
|
-
const text = anyPriorityMatch[3];
|
|
1505
|
-
if (!id || !priority || !text) continue;
|
|
1506
|
-
if (!VALID_PRIORITIES.has(priority)) {
|
|
1507
|
-
brsWithInvalidPriority.push({
|
|
1508
|
-
id,
|
|
1509
|
-
priority,
|
|
1510
|
-
text: text.trim(),
|
|
1511
|
-
line: lineNumber
|
|
1512
|
-
});
|
|
1625
|
+
return [];
|
|
1626
|
+
}
|
|
1627
|
+
const issues = [];
|
|
1628
|
+
for (const pack of packs) {
|
|
1629
|
+
const deltaPath = import_node_path12.default.join(pack, "delta.md");
|
|
1630
|
+
let text;
|
|
1631
|
+
try {
|
|
1632
|
+
text = await (0, import_promises10.readFile)(deltaPath, "utf-8");
|
|
1633
|
+
} catch (error2) {
|
|
1634
|
+
if (isMissingFileError2(error2)) {
|
|
1635
|
+
issues.push(
|
|
1636
|
+
issue2(
|
|
1637
|
+
"QFAI-DELTA-001",
|
|
1638
|
+
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1639
|
+
"error",
|
|
1640
|
+
deltaPath,
|
|
1641
|
+
"delta.exists"
|
|
1642
|
+
)
|
|
1643
|
+
);
|
|
1644
|
+
continue;
|
|
1513
1645
|
}
|
|
1646
|
+
throw error2;
|
|
1647
|
+
}
|
|
1648
|
+
const hasSection = SECTION_RE.test(text);
|
|
1649
|
+
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
1650
|
+
const hasChange = CHANGE_LINE_RE.test(text);
|
|
1651
|
+
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
1652
|
+
issues.push(
|
|
1653
|
+
issue2(
|
|
1654
|
+
"QFAI-DELTA-002",
|
|
1655
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
1656
|
+
"error",
|
|
1657
|
+
deltaPath,
|
|
1658
|
+
"delta.section"
|
|
1659
|
+
)
|
|
1660
|
+
);
|
|
1514
1661
|
continue;
|
|
1515
1662
|
}
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1663
|
+
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
1664
|
+
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
1665
|
+
if (compatibilityChecked === changeChecked) {
|
|
1666
|
+
issues.push(
|
|
1667
|
+
issue2(
|
|
1668
|
+
"QFAI-DELTA-003",
|
|
1669
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u306F\u3069\u3061\u3089\u304B1\u3064\u3060\u3051\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4E21\u65B9ON/\u4E21\u65B9OFF\u306F\u7121\u52B9\u3067\u3059\uFF09\u3002",
|
|
1670
|
+
"error",
|
|
1671
|
+
deltaPath,
|
|
1672
|
+
"delta.classification"
|
|
1673
|
+
)
|
|
1674
|
+
);
|
|
1526
1675
|
}
|
|
1527
1676
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1677
|
+
return issues;
|
|
1678
|
+
}
|
|
1679
|
+
function isMissingFileError2(error2) {
|
|
1680
|
+
if (!error2 || typeof error2 !== "object") {
|
|
1681
|
+
return false;
|
|
1682
|
+
}
|
|
1683
|
+
return error2.code === "ENOENT";
|
|
1684
|
+
}
|
|
1685
|
+
function issue2(code, message, severity, file, rule, refs) {
|
|
1686
|
+
const issue7 = {
|
|
1687
|
+
code,
|
|
1688
|
+
severity,
|
|
1689
|
+
message
|
|
1534
1690
|
};
|
|
1535
|
-
if (
|
|
1536
|
-
|
|
1691
|
+
if (file) {
|
|
1692
|
+
issue7.file = file;
|
|
1537
1693
|
}
|
|
1538
|
-
|
|
1694
|
+
if (rule) {
|
|
1695
|
+
issue7.rule = rule;
|
|
1696
|
+
}
|
|
1697
|
+
if (refs && refs.length > 0) {
|
|
1698
|
+
issue7.refs = refs;
|
|
1699
|
+
}
|
|
1700
|
+
return issue7;
|
|
1539
1701
|
}
|
|
1540
1702
|
|
|
1541
1703
|
// src/core/validators/ids.ts
|
|
1704
|
+
var import_promises11 = require("fs/promises");
|
|
1705
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
1542
1706
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1543
1707
|
async function validateDefinedIds(root, config) {
|
|
1544
1708
|
const issues = [];
|
|
@@ -1604,7 +1768,7 @@ function recordId(out, id, file) {
|
|
|
1604
1768
|
}
|
|
1605
1769
|
function formatFileList(files, root) {
|
|
1606
1770
|
return files.map((file) => {
|
|
1607
|
-
const relative =
|
|
1771
|
+
const relative = import_node_path13.default.relative(root, file);
|
|
1608
1772
|
return relative.length > 0 ? relative : file;
|
|
1609
1773
|
}).join(", ");
|
|
1610
1774
|
}
|
|
@@ -1633,7 +1797,6 @@ var WHEN_PATTERN = /\bWhen\b/;
|
|
|
1633
1797
|
var THEN_PATTERN = /\bThen\b/;
|
|
1634
1798
|
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1635
1799
|
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1636
|
-
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1637
1800
|
async function validateScenarios(root, config) {
|
|
1638
1801
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1639
1802
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -1682,7 +1845,7 @@ function validateScenarioContent(text, file) {
|
|
|
1682
1845
|
"SC",
|
|
1683
1846
|
"UI",
|
|
1684
1847
|
"API",
|
|
1685
|
-
"
|
|
1848
|
+
"DB",
|
|
1686
1849
|
"ADR"
|
|
1687
1850
|
]);
|
|
1688
1851
|
if (invalidIds.length > 0) {
|
|
@@ -1713,17 +1876,7 @@ function validateScenarioContent(text, file) {
|
|
|
1713
1876
|
const featureSpecTags = document.featureTags.filter(
|
|
1714
1877
|
(tag) => SPEC_TAG_RE2.test(tag)
|
|
1715
1878
|
);
|
|
1716
|
-
if (featureSpecTags.length
|
|
1717
|
-
issues.push(
|
|
1718
|
-
issue4(
|
|
1719
|
-
"QFAI-SC-009",
|
|
1720
|
-
"Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1721
|
-
"error",
|
|
1722
|
-
file,
|
|
1723
|
-
"scenario.featureSpec"
|
|
1724
|
-
)
|
|
1725
|
-
);
|
|
1726
|
-
} else if (featureSpecTags.length > 1) {
|
|
1879
|
+
if (featureSpecTags.length > 1) {
|
|
1727
1880
|
issues.push(
|
|
1728
1881
|
issue4(
|
|
1729
1882
|
"QFAI-SC-009",
|
|
@@ -1751,17 +1904,6 @@ function validateScenarioContent(text, file) {
|
|
|
1751
1904
|
)
|
|
1752
1905
|
);
|
|
1753
1906
|
}
|
|
1754
|
-
if (document.scenarios.length > 1) {
|
|
1755
|
-
issues.push(
|
|
1756
|
-
issue4(
|
|
1757
|
-
"QFAI-SC-011",
|
|
1758
|
-
`Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
|
|
1759
|
-
"error",
|
|
1760
|
-
file,
|
|
1761
|
-
"scenario.single"
|
|
1762
|
-
)
|
|
1763
|
-
);
|
|
1764
|
-
}
|
|
1765
1907
|
for (const scenario of document.scenarios) {
|
|
1766
1908
|
if (scenario.tags.length === 0) {
|
|
1767
1909
|
issues.push(
|
|
@@ -1782,12 +1924,6 @@ function validateScenarioContent(text, file) {
|
|
|
1782
1924
|
} else if (scTags.length > 1) {
|
|
1783
1925
|
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1784
1926
|
}
|
|
1785
|
-
if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
|
|
1786
|
-
missingTags.push("SPEC");
|
|
1787
|
-
}
|
|
1788
|
-
if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
|
|
1789
|
-
missingTags.push("BR");
|
|
1790
|
-
}
|
|
1791
1927
|
if (missingTags.length > 0) {
|
|
1792
1928
|
issues.push(
|
|
1793
1929
|
issue4(
|
|
@@ -1907,7 +2043,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1907
2043
|
"SC",
|
|
1908
2044
|
"UI",
|
|
1909
2045
|
"API",
|
|
1910
|
-
"
|
|
2046
|
+
"DB",
|
|
1911
2047
|
"ADR"
|
|
1912
2048
|
]);
|
|
1913
2049
|
if (invalidIds.length > 0) {
|
|
@@ -2022,9 +2158,8 @@ function isMissingFileError4(error2) {
|
|
|
2022
2158
|
|
|
2023
2159
|
// src/core/validators/traceability.ts
|
|
2024
2160
|
var import_promises14 = require("fs/promises");
|
|
2025
|
-
var SC_TAG_RE5 = /^SC-\d{4}$/;
|
|
2026
2161
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
2027
|
-
var
|
|
2162
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
2028
2163
|
async function validateTraceability(root, config) {
|
|
2029
2164
|
const issues = [];
|
|
2030
2165
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -2037,8 +2172,7 @@ async function validateTraceability(root, config) {
|
|
|
2037
2172
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
2038
2173
|
const brIdsInScenarios = /* @__PURE__ */ new Set();
|
|
2039
2174
|
const scIdsInScenarios = /* @__PURE__ */ new Set();
|
|
2040
|
-
const
|
|
2041
|
-
const scWithContracts = /* @__PURE__ */ new Set();
|
|
2175
|
+
const specContractIds = /* @__PURE__ */ new Set();
|
|
2042
2176
|
const specToBrIds = /* @__PURE__ */ new Map();
|
|
2043
2177
|
const contractIndex = await buildContractIndex(root, config);
|
|
2044
2178
|
const contractIds = contractIndex.ids;
|
|
@@ -2051,19 +2185,60 @@ async function validateTraceability(root, config) {
|
|
|
2051
2185
|
}
|
|
2052
2186
|
const brIds = parsed.brs.map((br) => br.id);
|
|
2053
2187
|
brIds.forEach((id) => brIdsInSpecs.add(id));
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
const
|
|
2188
|
+
if (parsed.specId) {
|
|
2189
|
+
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
2190
|
+
brIds.forEach((id) => current.add(id));
|
|
2191
|
+
specToBrIds.set(parsed.specId, current);
|
|
2192
|
+
}
|
|
2193
|
+
const contractRefs = parsed.contractRefs;
|
|
2194
|
+
if (contractRefs.lines.length === 0) {
|
|
2195
|
+
issues.push(
|
|
2196
|
+
issue6(
|
|
2197
|
+
"QFAI-TRACE-020",
|
|
2198
|
+
"Spec \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
2199
|
+
"error",
|
|
2200
|
+
file,
|
|
2201
|
+
"traceability.specContractRefRequired"
|
|
2202
|
+
)
|
|
2203
|
+
);
|
|
2204
|
+
} else {
|
|
2205
|
+
if (contractRefs.hasNone && contractRefs.ids.length > 0) {
|
|
2206
|
+
issues.push(
|
|
2207
|
+
issue6(
|
|
2208
|
+
"QFAI-TRACE-021",
|
|
2209
|
+
"Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2210
|
+
"error",
|
|
2211
|
+
file,
|
|
2212
|
+
"traceability.specContractRefFormat"
|
|
2213
|
+
)
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
if (contractRefs.invalidTokens.length > 0) {
|
|
2217
|
+
issues.push(
|
|
2218
|
+
issue6(
|
|
2219
|
+
"QFAI-TRACE-021",
|
|
2220
|
+
`Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
|
|
2221
|
+
", "
|
|
2222
|
+
)}`,
|
|
2223
|
+
"error",
|
|
2224
|
+
file,
|
|
2225
|
+
"traceability.specContractRefFormat",
|
|
2226
|
+
contractRefs.invalidTokens
|
|
2227
|
+
)
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
contractRefs.ids.forEach((id) => {
|
|
2232
|
+
specContractIds.add(id);
|
|
2233
|
+
});
|
|
2234
|
+
const unknownContractIds = contractRefs.ids.filter(
|
|
2060
2235
|
(id) => !contractIds.has(id)
|
|
2061
2236
|
);
|
|
2062
2237
|
if (unknownContractIds.length > 0) {
|
|
2063
2238
|
issues.push(
|
|
2064
2239
|
issue6(
|
|
2065
|
-
"QFAI-TRACE-
|
|
2066
|
-
`Spec \u304C\
|
|
2240
|
+
"QFAI-TRACE-021",
|
|
2241
|
+
`Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
2067
2242
|
", "
|
|
2068
2243
|
)}`,
|
|
2069
2244
|
"error",
|
|
@@ -2073,11 +2248,6 @@ async function validateTraceability(root, config) {
|
|
|
2073
2248
|
)
|
|
2074
2249
|
);
|
|
2075
2250
|
}
|
|
2076
|
-
if (parsed.specId) {
|
|
2077
|
-
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
2078
|
-
brIds.forEach((id) => current.add(id));
|
|
2079
|
-
specToBrIds.set(parsed.specId, current);
|
|
2080
|
-
}
|
|
2081
2251
|
}
|
|
2082
2252
|
for (const file of scenarioFiles) {
|
|
2083
2253
|
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
@@ -2087,20 +2257,42 @@ async function validateTraceability(root, config) {
|
|
|
2087
2257
|
continue;
|
|
2088
2258
|
}
|
|
2089
2259
|
const atoms = buildScenarioAtoms(document);
|
|
2260
|
+
const scIdsInFile = /* @__PURE__ */ new Set();
|
|
2090
2261
|
for (const [index, scenario] of document.scenarios.entries()) {
|
|
2091
2262
|
const atom = atoms[index];
|
|
2092
2263
|
if (!atom) {
|
|
2093
2264
|
continue;
|
|
2094
2265
|
}
|
|
2095
2266
|
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
2096
|
-
const brTags = scenario.tags.filter((tag) =>
|
|
2097
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2267
|
+
const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
|
|
2268
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
|
|
2269
|
+
if (specTags.length === 0) {
|
|
2270
|
+
issues.push(
|
|
2271
|
+
issue6(
|
|
2272
|
+
"QFAI-TRACE-014",
|
|
2273
|
+
`Scenario \u304C SPEC \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
|
|
2274
|
+
"error",
|
|
2275
|
+
file,
|
|
2276
|
+
"traceability.scenarioSpecRequired"
|
|
2277
|
+
)
|
|
2278
|
+
);
|
|
2279
|
+
}
|
|
2280
|
+
if (brTags.length === 0) {
|
|
2281
|
+
issues.push(
|
|
2282
|
+
issue6(
|
|
2283
|
+
"QFAI-TRACE-015",
|
|
2284
|
+
`Scenario \u304C BR \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
|
|
2285
|
+
"error",
|
|
2286
|
+
file,
|
|
2287
|
+
"traceability.scenarioBrRequired"
|
|
2288
|
+
)
|
|
2289
|
+
);
|
|
2103
2290
|
}
|
|
2291
|
+
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
2292
|
+
scTags.forEach((id) => {
|
|
2293
|
+
scIdsInScenarios.add(id);
|
|
2294
|
+
scIdsInFile.add(id);
|
|
2295
|
+
});
|
|
2104
2296
|
const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
|
|
2105
2297
|
if (unknownSpecIds.length > 0) {
|
|
2106
2298
|
issues.push(
|
|
@@ -2174,6 +2366,22 @@ async function validateTraceability(root, config) {
|
|
|
2174
2366
|
}
|
|
2175
2367
|
}
|
|
2176
2368
|
}
|
|
2369
|
+
if (scIdsInFile.size !== 1) {
|
|
2370
|
+
const invalidScIds = Array.from(scIdsInFile).sort(
|
|
2371
|
+
(a, b) => a.localeCompare(b)
|
|
2372
|
+
);
|
|
2373
|
+
const detail = invalidScIds.length === 0 ? "SC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" : `\u8907\u6570\u306E SC \u304C\u5B58\u5728\u3057\u307E\u3059: ${invalidScIds.join(", ")}`;
|
|
2374
|
+
issues.push(
|
|
2375
|
+
issue6(
|
|
2376
|
+
"QFAI-TRACE-012",
|
|
2377
|
+
`Spec entry \u304C Spec:SC=1:1 \u3092\u6E80\u305F\u3057\u3066\u3044\u307E\u305B\u3093: ${detail}`,
|
|
2378
|
+
"error",
|
|
2379
|
+
file,
|
|
2380
|
+
"traceability.specScOneToOne",
|
|
2381
|
+
invalidScIds
|
|
2382
|
+
)
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2177
2385
|
}
|
|
2178
2386
|
if (upstreamIds.size === 0) {
|
|
2179
2387
|
return [
|
|
@@ -2203,40 +2411,62 @@ async function validateTraceability(root, config) {
|
|
|
2203
2411
|
);
|
|
2204
2412
|
}
|
|
2205
2413
|
}
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2414
|
+
const scRefsResult = await collectScTestReferences(
|
|
2415
|
+
root,
|
|
2416
|
+
config.validation.traceability.testFileGlobs,
|
|
2417
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2418
|
+
);
|
|
2419
|
+
const scTestRefs = scRefsResult.refs;
|
|
2420
|
+
const testFileScan = scRefsResult.scan;
|
|
2421
|
+
const hasScenarios = scIdsInScenarios.size > 0;
|
|
2422
|
+
const hasGlobConfig = testFileScan.globs.length > 0;
|
|
2423
|
+
const hasMatchedTests = testFileScan.matchedFileCount > 0;
|
|
2424
|
+
if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
|
|
2425
|
+
const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
|
|
2426
|
+
issues.push(
|
|
2427
|
+
issue6(
|
|
2428
|
+
"QFAI-TRACE-013",
|
|
2429
|
+
`\u30C6\u30B9\u30C8\u63A2\u7D22 glob \u304C\u672A\u8A2D\u5B9A/\u4E0D\u6B63/\u4E00\u81F4\u30D5\u30A1\u30A4\u30EB0\u306E\u305F\u3081 SC\u2192Test \u3092\u5224\u5B9A\u3067\u304D\u307E\u305B\u3093\u3002${detail}`,
|
|
2430
|
+
"error",
|
|
2431
|
+
testsRoot,
|
|
2432
|
+
"traceability.testFileGlobs"
|
|
2433
|
+
)
|
|
2434
|
+
);
|
|
2435
|
+
} else {
|
|
2436
|
+
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
2437
|
+
const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
|
|
2438
|
+
const refs = scTestRefs.get(id);
|
|
2439
|
+
return !refs || refs.size === 0;
|
|
2440
|
+
});
|
|
2441
|
+
if (scWithoutTests.length > 0) {
|
|
2442
|
+
issues.push(
|
|
2443
|
+
issue6(
|
|
2444
|
+
"QFAI-TRACE-010",
|
|
2445
|
+
`SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(
|
|
2446
|
+
", "
|
|
2447
|
+
)}\u3002testFileGlobs \u306B\u4E00\u81F4\u3059\u308B\u30C6\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\u3078 QFAI:SC-xxxx \u3092\u8A18\u8F09\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
|
|
2448
|
+
config.validation.traceability.scNoTestSeverity,
|
|
2449
|
+
testsRoot,
|
|
2450
|
+
"traceability.scMustHaveTest",
|
|
2451
|
+
scWithoutTests
|
|
2452
|
+
)
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
const unknownScIds = Array.from(scTestRefs.keys()).filter(
|
|
2457
|
+
(id) => !scIdsInScenarios.has(id)
|
|
2209
2458
|
);
|
|
2210
|
-
if (
|
|
2459
|
+
if (unknownScIds.length > 0) {
|
|
2211
2460
|
issues.push(
|
|
2212
2461
|
issue6(
|
|
2213
|
-
"
|
|
2214
|
-
|
|
2462
|
+
"QFAI-TRACE-011",
|
|
2463
|
+
`\u30C6\u30B9\u30C8\u304C\u672A\u77E5\u306E SC \u3092\u30A2\u30CE\u30C6\u30FC\u30B7\u30E7\u30F3\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownScIds.join(
|
|
2215
2464
|
", "
|
|
2216
2465
|
)}`,
|
|
2217
2466
|
"error",
|
|
2218
|
-
specsRoot,
|
|
2219
|
-
"traceability.scMustTouchContracts",
|
|
2220
|
-
scWithoutContracts
|
|
2221
|
-
)
|
|
2222
|
-
);
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
2226
|
-
const scTestRefs = await collectScTestReferences(testsRoot);
|
|
2227
|
-
const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
|
|
2228
|
-
const refs = scTestRefs.get(id);
|
|
2229
|
-
return !refs || refs.size === 0;
|
|
2230
|
-
});
|
|
2231
|
-
if (scWithoutTests.length > 0) {
|
|
2232
|
-
issues.push(
|
|
2233
|
-
issue6(
|
|
2234
|
-
"QFAI-TRACE-010",
|
|
2235
|
-
`SC \u304C tests \u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(", ")}\u3002tests/ \u914D\u4E0B\u306E\u30C6\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\uFF08.ts/.tsx/.js/.jsx\uFF09\u306B SC ID \u3092\u30B3\u30E1\u30F3\u30C8\u307E\u305F\u306F\u30B3\u30FC\u30C9\u3067\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
|
|
2236
|
-
config.validation.traceability.scNoTestSeverity,
|
|
2237
2467
|
testsRoot,
|
|
2238
|
-
"traceability.
|
|
2239
|
-
|
|
2468
|
+
"traceability.scUnknownInTests",
|
|
2469
|
+
unknownScIds
|
|
2240
2470
|
)
|
|
2241
2471
|
);
|
|
2242
2472
|
}
|
|
@@ -2244,16 +2474,16 @@ async function validateTraceability(root, config) {
|
|
|
2244
2474
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
2245
2475
|
if (contractIds.size > 0) {
|
|
2246
2476
|
const orphanContracts = Array.from(contractIds).filter(
|
|
2247
|
-
(id) => !
|
|
2477
|
+
(id) => !specContractIds.has(id)
|
|
2248
2478
|
);
|
|
2249
2479
|
if (orphanContracts.length > 0) {
|
|
2250
2480
|
issues.push(
|
|
2251
2481
|
issue6(
|
|
2252
|
-
"
|
|
2253
|
-
`\u5951\u7D04\u304C
|
|
2482
|
+
"QFAI-TRACE-022",
|
|
2483
|
+
`\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
2254
2484
|
"error",
|
|
2255
2485
|
specsRoot,
|
|
2256
|
-
"traceability.
|
|
2486
|
+
"traceability.contractCoverage",
|
|
2257
2487
|
orphanContracts
|
|
2258
2488
|
)
|
|
2259
2489
|
);
|
|
@@ -2299,8 +2529,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2299
2529
|
issues.push(
|
|
2300
2530
|
issue6(
|
|
2301
2531
|
"QFAI-TRACE-002",
|
|
2302
|
-
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
2303
|
-
"
|
|
2532
|
+
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\uFF08\u53C2\u8003\u60C5\u5831\uFF09\u3002",
|
|
2533
|
+
"info",
|
|
2304
2534
|
srcRoot,
|
|
2305
2535
|
"traceability.codeReferences"
|
|
2306
2536
|
)
|
|
@@ -2343,11 +2573,24 @@ async function validateProject(root, configResult) {
|
|
|
2343
2573
|
...await validateDefinedIds(root, config),
|
|
2344
2574
|
...await validateTraceability(root, config)
|
|
2345
2575
|
];
|
|
2576
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2577
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
2578
|
+
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2579
|
+
const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
|
|
2580
|
+
root,
|
|
2581
|
+
config.validation.traceability.testFileGlobs,
|
|
2582
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2583
|
+
);
|
|
2584
|
+
const scCoverage = buildScCoverage(scIds, scTestRefs);
|
|
2346
2585
|
const toolVersion = await resolveToolVersion();
|
|
2347
2586
|
return {
|
|
2348
2587
|
toolVersion,
|
|
2349
2588
|
issues,
|
|
2350
|
-
counts: countIssues(issues)
|
|
2589
|
+
counts: countIssues(issues),
|
|
2590
|
+
traceability: {
|
|
2591
|
+
sc: scCoverage,
|
|
2592
|
+
testFiles
|
|
2593
|
+
}
|
|
2351
2594
|
};
|
|
2352
2595
|
}
|
|
2353
2596
|
function countIssues(issues) {
|
|
@@ -2361,16 +2604,16 @@ function countIssues(issues) {
|
|
|
2361
2604
|
}
|
|
2362
2605
|
|
|
2363
2606
|
// src/core/report.ts
|
|
2364
|
-
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "
|
|
2607
|
+
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2365
2608
|
async function createReportData(root, validation, configResult) {
|
|
2366
2609
|
const resolved = configResult ?? await loadConfig(root);
|
|
2367
2610
|
const config = resolved.config;
|
|
2368
2611
|
const configPath = resolved.configPath;
|
|
2369
2612
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2370
2613
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2371
|
-
const apiRoot =
|
|
2372
|
-
const uiRoot =
|
|
2373
|
-
const dbRoot =
|
|
2614
|
+
const apiRoot = import_node_path14.default.join(contractsRoot, "api");
|
|
2615
|
+
const uiRoot = import_node_path14.default.join(contractsRoot, "ui");
|
|
2616
|
+
const dbRoot = import_node_path14.default.join(contractsRoot, "db");
|
|
2374
2617
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
2375
2618
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
2376
2619
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -2380,6 +2623,23 @@ async function createReportData(root, validation, configResult) {
|
|
|
2380
2623
|
ui: uiFiles,
|
|
2381
2624
|
db: dbFiles
|
|
2382
2625
|
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
2626
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
2627
|
+
const specContractRefs = await collectSpecContractRefs(specFiles);
|
|
2628
|
+
const contractIdList = Array.from(contractIndex.ids);
|
|
2629
|
+
const referencedContracts = /* @__PURE__ */ new Set();
|
|
2630
|
+
for (const ids of specContractRefs.specToContractIds.values()) {
|
|
2631
|
+
ids.forEach((id) => referencedContracts.add(id));
|
|
2632
|
+
}
|
|
2633
|
+
const referencedContractCount = contractIdList.filter(
|
|
2634
|
+
(id) => referencedContracts.has(id)
|
|
2635
|
+
).length;
|
|
2636
|
+
const orphanContractCount = contractIdList.filter(
|
|
2637
|
+
(id) => !referencedContracts.has(id)
|
|
2638
|
+
).length;
|
|
2639
|
+
const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
|
|
2640
|
+
const specToContractIdsRecord = mapToSortedRecord(
|
|
2641
|
+
specContractRefs.specToContractIds
|
|
2642
|
+
);
|
|
2383
2643
|
const idsByPrefix = await collectIds([
|
|
2384
2644
|
...specFiles,
|
|
2385
2645
|
...scenarioFiles,
|
|
@@ -2397,8 +2657,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
2397
2657
|
testsRoot
|
|
2398
2658
|
);
|
|
2399
2659
|
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2400
|
-
const
|
|
2401
|
-
|
|
2660
|
+
const scRefsResult = await collectScTestReferences(
|
|
2661
|
+
root,
|
|
2662
|
+
config.validation.traceability.testFileGlobs,
|
|
2663
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2664
|
+
);
|
|
2665
|
+
const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
|
|
2666
|
+
const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
|
|
2667
|
+
const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
|
|
2668
|
+
const scSourceRecord = mapToSortedRecord(scSources);
|
|
2402
2669
|
const resolvedValidation = validation ?? await validateProject(root, resolved);
|
|
2403
2670
|
const version = await resolveToolVersion();
|
|
2404
2671
|
return {
|
|
@@ -2423,12 +2690,24 @@ async function createReportData(root, validation, configResult) {
|
|
|
2423
2690
|
sc: idsByPrefix.SC,
|
|
2424
2691
|
ui: idsByPrefix.UI,
|
|
2425
2692
|
api: idsByPrefix.API,
|
|
2426
|
-
|
|
2693
|
+
db: idsByPrefix.DB
|
|
2427
2694
|
},
|
|
2428
2695
|
traceability: {
|
|
2429
2696
|
upstreamIdsFound: upstreamIds.size,
|
|
2430
2697
|
referencedInCodeOrTests: traceability,
|
|
2431
|
-
sc: scCoverage
|
|
2698
|
+
sc: scCoverage,
|
|
2699
|
+
scSources: scSourceRecord,
|
|
2700
|
+
testFiles,
|
|
2701
|
+
contracts: {
|
|
2702
|
+
total: contractIdList.length,
|
|
2703
|
+
referenced: referencedContractCount,
|
|
2704
|
+
orphan: orphanContractCount,
|
|
2705
|
+
idToSpecs: contractIdToSpecsRecord
|
|
2706
|
+
},
|
|
2707
|
+
specs: {
|
|
2708
|
+
contractRefMissing: specContractRefs.missingRefSpecs.size,
|
|
2709
|
+
specToContractIds: specToContractIdsRecord
|
|
2710
|
+
}
|
|
2432
2711
|
},
|
|
2433
2712
|
issues: resolvedValidation.issues
|
|
2434
2713
|
};
|
|
@@ -2457,7 +2736,7 @@ function formatReportMarkdown(data) {
|
|
|
2457
2736
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
2458
2737
|
lines.push(formatIdLine("UI", data.ids.ui));
|
|
2459
2738
|
lines.push(formatIdLine("API", data.ids.api));
|
|
2460
|
-
lines.push(formatIdLine("
|
|
2739
|
+
lines.push(formatIdLine("DB", data.ids.db));
|
|
2461
2740
|
lines.push("");
|
|
2462
2741
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
2463
2742
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
@@ -2465,14 +2744,77 @@ function formatReportMarkdown(data) {
|
|
|
2465
2744
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2466
2745
|
);
|
|
2467
2746
|
lines.push("");
|
|
2747
|
+
lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2748
|
+
lines.push(`- total: ${data.traceability.contracts.total}`);
|
|
2749
|
+
lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
|
|
2750
|
+
lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
|
|
2751
|
+
lines.push(
|
|
2752
|
+
`- specContractRefMissing: ${data.traceability.specs.contractRefMissing}`
|
|
2753
|
+
);
|
|
2754
|
+
lines.push("");
|
|
2755
|
+
lines.push("## \u5951\u7D04\u2192Spec");
|
|
2756
|
+
const contractToSpecs = data.traceability.contracts.idToSpecs;
|
|
2757
|
+
const contractIds = Object.keys(contractToSpecs).sort(
|
|
2758
|
+
(a, b) => a.localeCompare(b)
|
|
2759
|
+
);
|
|
2760
|
+
if (contractIds.length === 0) {
|
|
2761
|
+
lines.push("- (none)");
|
|
2762
|
+
} else {
|
|
2763
|
+
for (const contractId of contractIds) {
|
|
2764
|
+
const specs = contractToSpecs[contractId] ?? [];
|
|
2765
|
+
if (specs.length === 0) {
|
|
2766
|
+
lines.push(`- ${contractId}: (none)`);
|
|
2767
|
+
} else {
|
|
2768
|
+
lines.push(`- ${contractId}: ${specs.join(", ")}`);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
lines.push("");
|
|
2773
|
+
lines.push("## Spec\u2192\u5951\u7D04");
|
|
2774
|
+
const specToContracts = data.traceability.specs.specToContractIds;
|
|
2775
|
+
const specIds = Object.keys(specToContracts).sort(
|
|
2776
|
+
(a, b) => a.localeCompare(b)
|
|
2777
|
+
);
|
|
2778
|
+
if (specIds.length === 0) {
|
|
2779
|
+
lines.push("- (none)");
|
|
2780
|
+
} else {
|
|
2781
|
+
for (const specId of specIds) {
|
|
2782
|
+
const contractIds2 = specToContracts[specId] ?? [];
|
|
2783
|
+
if (contractIds2.length === 0) {
|
|
2784
|
+
lines.push(`- ${specId}: (none)`);
|
|
2785
|
+
} else {
|
|
2786
|
+
lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
lines.push("");
|
|
2468
2791
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2469
2792
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2470
2793
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2471
2794
|
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
2795
|
+
lines.push(
|
|
2796
|
+
`- testFileGlobs: ${formatList(data.traceability.testFiles.globs)}`
|
|
2797
|
+
);
|
|
2798
|
+
lines.push(
|
|
2799
|
+
`- testFileExcludeGlobs: ${formatList(
|
|
2800
|
+
data.traceability.testFiles.excludeGlobs
|
|
2801
|
+
)}`
|
|
2802
|
+
);
|
|
2803
|
+
lines.push(
|
|
2804
|
+
`- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
|
|
2805
|
+
);
|
|
2472
2806
|
if (data.traceability.sc.missingIds.length === 0) {
|
|
2473
2807
|
lines.push("- missingIds: (none)");
|
|
2474
2808
|
} else {
|
|
2475
|
-
|
|
2809
|
+
const sources = data.traceability.scSources;
|
|
2810
|
+
const missingWithSources = data.traceability.sc.missingIds.map((id) => {
|
|
2811
|
+
const files = sources[id] ?? [];
|
|
2812
|
+
if (files.length === 0) {
|
|
2813
|
+
return id;
|
|
2814
|
+
}
|
|
2815
|
+
return `${id} (${files.join(", ")})`;
|
|
2816
|
+
});
|
|
2817
|
+
lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
|
|
2476
2818
|
}
|
|
2477
2819
|
lines.push("");
|
|
2478
2820
|
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
@@ -2491,6 +2833,20 @@ function formatReportMarkdown(data) {
|
|
|
2491
2833
|
}
|
|
2492
2834
|
}
|
|
2493
2835
|
lines.push("");
|
|
2836
|
+
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
2837
|
+
const specScIssues = data.issues.filter(
|
|
2838
|
+
(item) => item.code === "QFAI-TRACE-012"
|
|
2839
|
+
);
|
|
2840
|
+
if (specScIssues.length === 0) {
|
|
2841
|
+
lines.push("- (none)");
|
|
2842
|
+
} else {
|
|
2843
|
+
for (const item of specScIssues) {
|
|
2844
|
+
const location = item.file ?? "(unknown)";
|
|
2845
|
+
const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
|
|
2846
|
+
lines.push(`- ${location}: ${refs}`);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
lines.push("");
|
|
2494
2850
|
lines.push("## Hotspots");
|
|
2495
2851
|
const hotspots = buildHotspots(data.issues);
|
|
2496
2852
|
if (hotspots.length === 0) {
|
|
@@ -2505,7 +2861,7 @@ function formatReportMarkdown(data) {
|
|
|
2505
2861
|
lines.push("");
|
|
2506
2862
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
2507
2863
|
const traceIssues = data.issues.filter(
|
|
2508
|
-
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2864
|
+
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2509
2865
|
);
|
|
2510
2866
|
if (traceIssues.length === 0) {
|
|
2511
2867
|
lines.push("- (none)");
|
|
@@ -2535,6 +2891,33 @@ function formatReportMarkdown(data) {
|
|
|
2535
2891
|
function formatReportJson(data) {
|
|
2536
2892
|
return JSON.stringify(data, null, 2);
|
|
2537
2893
|
}
|
|
2894
|
+
async function collectSpecContractRefs(specFiles) {
|
|
2895
|
+
const specToContractIds = /* @__PURE__ */ new Map();
|
|
2896
|
+
const idToSpecs = /* @__PURE__ */ new Map();
|
|
2897
|
+
const missingRefSpecs = /* @__PURE__ */ new Set();
|
|
2898
|
+
for (const file of specFiles) {
|
|
2899
|
+
const text = await (0, import_promises15.readFile)(file, "utf-8");
|
|
2900
|
+
const parsed = parseSpec(text, file);
|
|
2901
|
+
const specKey = parsed.specId ?? file;
|
|
2902
|
+
const refs = parsed.contractRefs;
|
|
2903
|
+
if (refs.lines.length === 0) {
|
|
2904
|
+
missingRefSpecs.add(specKey);
|
|
2905
|
+
}
|
|
2906
|
+
const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
|
|
2907
|
+
for (const id of refs.ids) {
|
|
2908
|
+
currentContracts.add(id);
|
|
2909
|
+
const specs = idToSpecs.get(id) ?? /* @__PURE__ */ new Set();
|
|
2910
|
+
specs.add(specKey);
|
|
2911
|
+
idToSpecs.set(id, specs);
|
|
2912
|
+
}
|
|
2913
|
+
specToContractIds.set(specKey, currentContracts);
|
|
2914
|
+
}
|
|
2915
|
+
return {
|
|
2916
|
+
specToContractIds,
|
|
2917
|
+
idToSpecs,
|
|
2918
|
+
missingRefSpecs
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2538
2921
|
async function collectIds(files) {
|
|
2539
2922
|
const result = {
|
|
2540
2923
|
SPEC: /* @__PURE__ */ new Set(),
|
|
@@ -2542,7 +2925,7 @@ async function collectIds(files) {
|
|
|
2542
2925
|
SC: /* @__PURE__ */ new Set(),
|
|
2543
2926
|
UI: /* @__PURE__ */ new Set(),
|
|
2544
2927
|
API: /* @__PURE__ */ new Set(),
|
|
2545
|
-
|
|
2928
|
+
DB: /* @__PURE__ */ new Set()
|
|
2546
2929
|
};
|
|
2547
2930
|
for (const file of files) {
|
|
2548
2931
|
const text = await (0, import_promises15.readFile)(file, "utf-8");
|
|
@@ -2557,7 +2940,7 @@ async function collectIds(files) {
|
|
|
2557
2940
|
SC: toSortedArray2(result.SC),
|
|
2558
2941
|
UI: toSortedArray2(result.UI),
|
|
2559
2942
|
API: toSortedArray2(result.API),
|
|
2560
|
-
|
|
2943
|
+
DB: toSortedArray2(result.DB)
|
|
2561
2944
|
};
|
|
2562
2945
|
}
|
|
2563
2946
|
async function collectUpstreamIds(files) {
|
|
@@ -2601,9 +2984,22 @@ function formatIdLine(label, values) {
|
|
|
2601
2984
|
}
|
|
2602
2985
|
return `- ${label}: ${values.join(", ")}`;
|
|
2603
2986
|
}
|
|
2987
|
+
function formatList(values) {
|
|
2988
|
+
if (values.length === 0) {
|
|
2989
|
+
return "(none)";
|
|
2990
|
+
}
|
|
2991
|
+
return values.join(", ");
|
|
2992
|
+
}
|
|
2604
2993
|
function toSortedArray2(values) {
|
|
2605
2994
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2606
2995
|
}
|
|
2996
|
+
function mapToSortedRecord(values) {
|
|
2997
|
+
const record2 = {};
|
|
2998
|
+
for (const [key, files] of values.entries()) {
|
|
2999
|
+
record2[key] = Array.from(files).sort((a, b) => a.localeCompare(b));
|
|
3000
|
+
}
|
|
3001
|
+
return record2;
|
|
3002
|
+
}
|
|
2607
3003
|
function buildHotspots(issues) {
|
|
2608
3004
|
const map = /* @__PURE__ */ new Map();
|
|
2609
3005
|
for (const issue7 of issues) {
|
|
@@ -2628,10 +3024,10 @@ function buildHotspots(issues) {
|
|
|
2628
3024
|
|
|
2629
3025
|
// src/cli/commands/report.ts
|
|
2630
3026
|
async function runReport(options) {
|
|
2631
|
-
const root =
|
|
3027
|
+
const root = import_node_path15.default.resolve(options.root);
|
|
2632
3028
|
const configResult = await loadConfig(root);
|
|
2633
3029
|
const input = configResult.config.output.validateJsonPath;
|
|
2634
|
-
const inputPath =
|
|
3030
|
+
const inputPath = import_node_path15.default.isAbsolute(input) ? input : import_node_path15.default.resolve(root, input);
|
|
2635
3031
|
let validation;
|
|
2636
3032
|
try {
|
|
2637
3033
|
validation = await readValidationResult(inputPath);
|
|
@@ -2656,10 +3052,10 @@ async function runReport(options) {
|
|
|
2656
3052
|
const data = await createReportData(root, validation, configResult);
|
|
2657
3053
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
2658
3054
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
2659
|
-
const defaultOut = options.format === "json" ?
|
|
3055
|
+
const defaultOut = options.format === "json" ? import_node_path15.default.join(outRoot, "report.json") : import_node_path15.default.join(outRoot, "report.md");
|
|
2660
3056
|
const out = options.outPath ?? defaultOut;
|
|
2661
|
-
const outPath =
|
|
2662
|
-
await (0, import_promises16.mkdir)(
|
|
3057
|
+
const outPath = import_node_path15.default.isAbsolute(out) ? out : import_node_path15.default.resolve(root, out);
|
|
3058
|
+
await (0, import_promises16.mkdir)(import_node_path15.default.dirname(outPath), { recursive: true });
|
|
2663
3059
|
await (0, import_promises16.writeFile)(outPath, `${output}
|
|
2664
3060
|
`, "utf-8");
|
|
2665
3061
|
info(
|
|
@@ -2702,7 +3098,7 @@ function isMissingFileError5(error2) {
|
|
|
2702
3098
|
|
|
2703
3099
|
// src/cli/commands/validate.ts
|
|
2704
3100
|
var import_promises17 = require("fs/promises");
|
|
2705
|
-
var
|
|
3101
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
2706
3102
|
|
|
2707
3103
|
// src/cli/lib/failOn.ts
|
|
2708
3104
|
function shouldFail(result, failOn) {
|
|
@@ -2717,7 +3113,7 @@ function shouldFail(result, failOn) {
|
|
|
2717
3113
|
|
|
2718
3114
|
// src/cli/commands/validate.ts
|
|
2719
3115
|
async function runValidate(options) {
|
|
2720
|
-
const root =
|
|
3116
|
+
const root = import_node_path16.default.resolve(options.root);
|
|
2721
3117
|
const configResult = await loadConfig(root);
|
|
2722
3118
|
const result = await validateProject(root, configResult);
|
|
2723
3119
|
const format = options.format ?? "text";
|
|
@@ -2766,8 +3162,8 @@ function emitGitHub(issue7) {
|
|
|
2766
3162
|
);
|
|
2767
3163
|
}
|
|
2768
3164
|
async function emitJson(result, root, jsonPath) {
|
|
2769
|
-
const abs =
|
|
2770
|
-
await (0, import_promises17.mkdir)(
|
|
3165
|
+
const abs = import_node_path16.default.isAbsolute(jsonPath) ? jsonPath : import_node_path16.default.resolve(root, jsonPath);
|
|
3166
|
+
await (0, import_promises17.mkdir)(import_node_path16.default.dirname(abs), { recursive: true });
|
|
2771
3167
|
await (0, import_promises17.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
2772
3168
|
`, "utf-8");
|
|
2773
3169
|
}
|