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/index.cjs
CHANGED
|
@@ -83,8 +83,9 @@ var defaultConfig = {
|
|
|
83
83
|
},
|
|
84
84
|
traceability: {
|
|
85
85
|
brMustHaveSc: true,
|
|
86
|
-
scMustTouchContracts: true,
|
|
87
86
|
scMustHaveTest: true,
|
|
87
|
+
testFileGlobs: [],
|
|
88
|
+
testFileExcludeGlobs: [],
|
|
88
89
|
scNoTestSeverity: "error",
|
|
89
90
|
allowOrphanContracts: false,
|
|
90
91
|
unknownContractIdSeverity: "error"
|
|
@@ -258,13 +259,6 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
258
259
|
configPath,
|
|
259
260
|
issues
|
|
260
261
|
),
|
|
261
|
-
scMustTouchContracts: readBoolean(
|
|
262
|
-
traceabilityRaw?.scMustTouchContracts,
|
|
263
|
-
base.traceability.scMustTouchContracts,
|
|
264
|
-
"validation.traceability.scMustTouchContracts",
|
|
265
|
-
configPath,
|
|
266
|
-
issues
|
|
267
|
-
),
|
|
268
262
|
scMustHaveTest: readBoolean(
|
|
269
263
|
traceabilityRaw?.scMustHaveTest,
|
|
270
264
|
base.traceability.scMustHaveTest,
|
|
@@ -272,6 +266,20 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
272
266
|
configPath,
|
|
273
267
|
issues
|
|
274
268
|
),
|
|
269
|
+
testFileGlobs: readStringArray(
|
|
270
|
+
traceabilityRaw?.testFileGlobs,
|
|
271
|
+
base.traceability.testFileGlobs,
|
|
272
|
+
"validation.traceability.testFileGlobs",
|
|
273
|
+
configPath,
|
|
274
|
+
issues
|
|
275
|
+
),
|
|
276
|
+
testFileExcludeGlobs: readStringArray(
|
|
277
|
+
traceabilityRaw?.testFileExcludeGlobs,
|
|
278
|
+
base.traceability.testFileExcludeGlobs,
|
|
279
|
+
"validation.traceability.testFileExcludeGlobs",
|
|
280
|
+
configPath,
|
|
281
|
+
issues
|
|
282
|
+
),
|
|
275
283
|
scNoTestSeverity: readTraceabilitySeverity(
|
|
276
284
|
traceabilityRaw?.scNoTestSeverity,
|
|
277
285
|
base.traceability.scNoTestSeverity,
|
|
@@ -404,14 +412,14 @@ function isRecord(value) {
|
|
|
404
412
|
}
|
|
405
413
|
|
|
406
414
|
// src/core/ids.ts
|
|
407
|
-
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "
|
|
415
|
+
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
408
416
|
var STRICT_ID_PATTERNS = {
|
|
409
417
|
SPEC: /\bSPEC-\d{4}\b/g,
|
|
410
418
|
BR: /\bBR-\d{4}\b/g,
|
|
411
419
|
SC: /\bSC-\d{4}\b/g,
|
|
412
420
|
UI: /\bUI-\d{4}\b/g,
|
|
413
421
|
API: /\bAPI-\d{4}\b/g,
|
|
414
|
-
|
|
422
|
+
DB: /\bDB-\d{4}\b/g,
|
|
415
423
|
ADR: /\bADR-\d{4}\b/g
|
|
416
424
|
};
|
|
417
425
|
var LOOSE_ID_PATTERNS = {
|
|
@@ -420,7 +428,7 @@ var LOOSE_ID_PATTERNS = {
|
|
|
420
428
|
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
421
429
|
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
422
430
|
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
423
|
-
|
|
431
|
+
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
424
432
|
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
425
433
|
};
|
|
426
434
|
function extractIds(text, prefix) {
|
|
@@ -458,7 +466,11 @@ function isValidId(value, prefix) {
|
|
|
458
466
|
|
|
459
467
|
// src/core/report.ts
|
|
460
468
|
var import_promises14 = require("fs/promises");
|
|
461
|
-
var
|
|
469
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
470
|
+
|
|
471
|
+
// src/core/contractIndex.ts
|
|
472
|
+
var import_promises5 = require("fs/promises");
|
|
473
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
462
474
|
|
|
463
475
|
// src/core/discovery.ts
|
|
464
476
|
var import_promises4 = require("fs/promises");
|
|
@@ -466,6 +478,7 @@ var import_promises4 = require("fs/promises");
|
|
|
466
478
|
// src/core/fs.ts
|
|
467
479
|
var import_promises2 = require("fs/promises");
|
|
468
480
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
481
|
+
var import_fast_glob = __toESM(require("fast-glob"), 1);
|
|
469
482
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
470
483
|
"node_modules",
|
|
471
484
|
".git",
|
|
@@ -487,6 +500,18 @@ async function collectFiles(root, options = {}) {
|
|
|
487
500
|
await walk(root, root, ignoreDirs, extensions, entries);
|
|
488
501
|
return entries;
|
|
489
502
|
}
|
|
503
|
+
async function collectFilesByGlobs(root, options) {
|
|
504
|
+
if (options.globs.length === 0) {
|
|
505
|
+
return [];
|
|
506
|
+
}
|
|
507
|
+
return (0, import_fast_glob.default)(options.globs, {
|
|
508
|
+
cwd: root,
|
|
509
|
+
ignore: options.ignore ?? [],
|
|
510
|
+
onlyFiles: true,
|
|
511
|
+
absolute: true,
|
|
512
|
+
unique: true
|
|
513
|
+
});
|
|
514
|
+
}
|
|
490
515
|
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
491
516
|
const items = await (0, import_promises2.readdir)(current, { withFileTypes: true });
|
|
492
517
|
for (const item of items) {
|
|
@@ -598,8 +623,221 @@ async function exists2(target) {
|
|
|
598
623
|
}
|
|
599
624
|
}
|
|
600
625
|
|
|
626
|
+
// src/core/contractsDecl.ts
|
|
627
|
+
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
|
|
628
|
+
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
|
|
629
|
+
function extractDeclaredContractIds(text) {
|
|
630
|
+
const ids = [];
|
|
631
|
+
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
632
|
+
const id = match[1];
|
|
633
|
+
if (id) {
|
|
634
|
+
ids.push(id);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return ids;
|
|
638
|
+
}
|
|
639
|
+
function stripContractDeclarationLines(text) {
|
|
640
|
+
return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/core/contractIndex.ts
|
|
644
|
+
async function buildContractIndex(root, config) {
|
|
645
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
646
|
+
const uiRoot = import_node_path4.default.join(contractsRoot, "ui");
|
|
647
|
+
const apiRoot = import_node_path4.default.join(contractsRoot, "api");
|
|
648
|
+
const dbRoot = import_node_path4.default.join(contractsRoot, "db");
|
|
649
|
+
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
650
|
+
collectUiContractFiles(uiRoot),
|
|
651
|
+
collectApiContractFiles(apiRoot),
|
|
652
|
+
collectDataContractFiles(dbRoot)
|
|
653
|
+
]);
|
|
654
|
+
const index = {
|
|
655
|
+
ids: /* @__PURE__ */ new Set(),
|
|
656
|
+
idToFiles: /* @__PURE__ */ new Map(),
|
|
657
|
+
files: { ui: uiFiles, api: apiFiles, db: dbFiles }
|
|
658
|
+
};
|
|
659
|
+
await indexContractFiles(uiFiles, index);
|
|
660
|
+
await indexContractFiles(apiFiles, index);
|
|
661
|
+
await indexContractFiles(dbFiles, index);
|
|
662
|
+
return index;
|
|
663
|
+
}
|
|
664
|
+
async function indexContractFiles(files, index) {
|
|
665
|
+
for (const file of files) {
|
|
666
|
+
const text = await (0, import_promises5.readFile)(file, "utf-8");
|
|
667
|
+
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function record(index, id, file) {
|
|
671
|
+
index.ids.add(id);
|
|
672
|
+
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
673
|
+
current.add(file);
|
|
674
|
+
index.idToFiles.set(id, current);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/core/parse/markdown.ts
|
|
678
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
679
|
+
function parseHeadings(md) {
|
|
680
|
+
const lines = md.split(/\r?\n/);
|
|
681
|
+
const headings = [];
|
|
682
|
+
for (let i = 0; i < lines.length; i++) {
|
|
683
|
+
const line = lines[i] ?? "";
|
|
684
|
+
const match = line.match(HEADING_RE);
|
|
685
|
+
if (!match) continue;
|
|
686
|
+
const levelToken = match[1];
|
|
687
|
+
const title = match[2];
|
|
688
|
+
if (!levelToken || !title) continue;
|
|
689
|
+
headings.push({
|
|
690
|
+
level: levelToken.length,
|
|
691
|
+
title: title.trim(),
|
|
692
|
+
line: i + 1
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
return headings;
|
|
696
|
+
}
|
|
697
|
+
function extractH2Sections(md) {
|
|
698
|
+
const lines = md.split(/\r?\n/);
|
|
699
|
+
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
700
|
+
const sections = /* @__PURE__ */ new Map();
|
|
701
|
+
for (let i = 0; i < headings.length; i++) {
|
|
702
|
+
const current = headings[i];
|
|
703
|
+
if (!current) continue;
|
|
704
|
+
const next = headings[i + 1];
|
|
705
|
+
const startLine = current.line + 1;
|
|
706
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
707
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
708
|
+
sections.set(current.title.trim(), {
|
|
709
|
+
title: current.title.trim(),
|
|
710
|
+
startLine,
|
|
711
|
+
endLine,
|
|
712
|
+
body
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
return sections;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/core/parse/spec.ts
|
|
719
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
720
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
721
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
722
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
723
|
+
var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
|
|
724
|
+
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
725
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
726
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
727
|
+
function parseSpec(md, file) {
|
|
728
|
+
const headings = parseHeadings(md);
|
|
729
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
730
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
731
|
+
const sections = extractH2Sections(md);
|
|
732
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
733
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
734
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
735
|
+
const startLine = brSection?.startLine ?? 1;
|
|
736
|
+
const brs = [];
|
|
737
|
+
const brsWithoutPriority = [];
|
|
738
|
+
const brsWithInvalidPriority = [];
|
|
739
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
740
|
+
const lineText = brLines[i] ?? "";
|
|
741
|
+
const lineNumber = startLine + i;
|
|
742
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
743
|
+
if (validMatch) {
|
|
744
|
+
const id = validMatch[1];
|
|
745
|
+
const priority = validMatch[2];
|
|
746
|
+
const text = validMatch[3];
|
|
747
|
+
if (!id || !priority || !text) continue;
|
|
748
|
+
brs.push({
|
|
749
|
+
id,
|
|
750
|
+
priority,
|
|
751
|
+
text: text.trim(),
|
|
752
|
+
line: lineNumber
|
|
753
|
+
});
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
757
|
+
if (anyPriorityMatch) {
|
|
758
|
+
const id = anyPriorityMatch[1];
|
|
759
|
+
const priority = anyPriorityMatch[2];
|
|
760
|
+
const text = anyPriorityMatch[3];
|
|
761
|
+
if (!id || !priority || !text) continue;
|
|
762
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
763
|
+
brsWithInvalidPriority.push({
|
|
764
|
+
id,
|
|
765
|
+
priority,
|
|
766
|
+
text: text.trim(),
|
|
767
|
+
line: lineNumber
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
773
|
+
if (noPriorityMatch) {
|
|
774
|
+
const id = noPriorityMatch[1];
|
|
775
|
+
const text = noPriorityMatch[2];
|
|
776
|
+
if (!id || !text) continue;
|
|
777
|
+
brsWithoutPriority.push({
|
|
778
|
+
id,
|
|
779
|
+
text: text.trim(),
|
|
780
|
+
line: lineNumber
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const parsed = {
|
|
785
|
+
file,
|
|
786
|
+
sections: sectionNames,
|
|
787
|
+
brs,
|
|
788
|
+
brsWithoutPriority,
|
|
789
|
+
brsWithInvalidPriority,
|
|
790
|
+
contractRefs: parseContractRefs(md)
|
|
791
|
+
};
|
|
792
|
+
if (specId) {
|
|
793
|
+
parsed.specId = specId;
|
|
794
|
+
}
|
|
795
|
+
return parsed;
|
|
796
|
+
}
|
|
797
|
+
function parseContractRefs(md) {
|
|
798
|
+
const lines = [];
|
|
799
|
+
for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
|
|
800
|
+
lines.push((match[1] ?? "").trim());
|
|
801
|
+
}
|
|
802
|
+
const ids = [];
|
|
803
|
+
const invalidTokens = [];
|
|
804
|
+
let hasNone = false;
|
|
805
|
+
for (const line of lines) {
|
|
806
|
+
if (line.length === 0) {
|
|
807
|
+
invalidTokens.push("(empty)");
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
const tokens = line.split(",").map((token) => token.trim());
|
|
811
|
+
for (const token of tokens) {
|
|
812
|
+
if (token.length === 0) {
|
|
813
|
+
invalidTokens.push("(empty)");
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (token === "none") {
|
|
817
|
+
hasNone = true;
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
821
|
+
ids.push(token);
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
invalidTokens.push(token);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
lines,
|
|
829
|
+
ids: unique2(ids),
|
|
830
|
+
invalidTokens: unique2(invalidTokens),
|
|
831
|
+
hasNone
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function unique2(values) {
|
|
835
|
+
return Array.from(new Set(values));
|
|
836
|
+
}
|
|
837
|
+
|
|
601
838
|
// src/core/traceability.ts
|
|
602
|
-
var
|
|
839
|
+
var import_promises6 = require("fs/promises");
|
|
840
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
603
841
|
|
|
604
842
|
// src/core/gherkin/parse.ts
|
|
605
843
|
var import_gherkin = require("@cucumber/gherkin");
|
|
@@ -632,7 +870,7 @@ var SC_TAG_RE = /^SC-\d{4}$/;
|
|
|
632
870
|
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
633
871
|
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
634
872
|
var API_TAG_RE = /^API-\d{4}$/;
|
|
635
|
-
var
|
|
873
|
+
var DB_TAG_RE = /^DB-\d{4}$/;
|
|
636
874
|
function parseScenarioDocument(text, uri) {
|
|
637
875
|
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
638
876
|
if (!gherkinDocument) {
|
|
@@ -661,10 +899,10 @@ function buildScenarioAtoms(document) {
|
|
|
661
899
|
return document.scenarios.map((scenario) => {
|
|
662
900
|
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
663
901
|
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
664
|
-
const brIds =
|
|
902
|
+
const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
665
903
|
const contractIds = /* @__PURE__ */ new Set();
|
|
666
904
|
scenario.tags.forEach((tag) => {
|
|
667
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) ||
|
|
905
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
|
|
668
906
|
contractIds.add(tag);
|
|
669
907
|
}
|
|
670
908
|
});
|
|
@@ -672,7 +910,7 @@ function buildScenarioAtoms(document) {
|
|
|
672
910
|
for (const text of collectStepTexts(step)) {
|
|
673
911
|
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
674
912
|
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
675
|
-
extractIds(text, "
|
|
913
|
+
extractIds(text, "DB").forEach((id) => contractIds.add(id));
|
|
676
914
|
}
|
|
677
915
|
}
|
|
678
916
|
const atom = {
|
|
@@ -751,16 +989,37 @@ function collectStepTexts(step) {
|
|
|
751
989
|
}
|
|
752
990
|
return texts;
|
|
753
991
|
}
|
|
754
|
-
function
|
|
992
|
+
function unique3(values) {
|
|
755
993
|
return Array.from(new Set(values));
|
|
756
994
|
}
|
|
757
995
|
|
|
758
996
|
// src/core/traceability.ts
|
|
759
997
|
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
998
|
+
var SC_TEST_ANNOTATION_RE = /\bQFAI:SC-(\d{4})\b/g;
|
|
999
|
+
var DEFAULT_TEST_FILE_EXCLUDE_GLOBS = [
|
|
1000
|
+
"**/node_modules/**",
|
|
1001
|
+
"**/.git/**",
|
|
1002
|
+
"**/.qfai/**",
|
|
1003
|
+
"**/dist/**",
|
|
1004
|
+
"**/build/**",
|
|
1005
|
+
"**/coverage/**",
|
|
1006
|
+
"**/.next/**",
|
|
1007
|
+
"**/out/**"
|
|
1008
|
+
];
|
|
1009
|
+
function extractAnnotatedScIds(text) {
|
|
1010
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1011
|
+
for (const match of text.matchAll(SC_TEST_ANNOTATION_RE)) {
|
|
1012
|
+
const suffix = match[1];
|
|
1013
|
+
if (suffix) {
|
|
1014
|
+
ids.add(`SC-${suffix}`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return Array.from(ids);
|
|
1018
|
+
}
|
|
760
1019
|
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
761
1020
|
const scIds = /* @__PURE__ */ new Set();
|
|
762
1021
|
for (const file of scenarioFiles) {
|
|
763
|
-
const text = await (0,
|
|
1022
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
764
1023
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
765
1024
|
if (!document || errors.length > 0) {
|
|
766
1025
|
continue;
|
|
@@ -775,14 +1034,67 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
|
775
1034
|
}
|
|
776
1035
|
return scIds;
|
|
777
1036
|
}
|
|
778
|
-
async function
|
|
1037
|
+
async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
1038
|
+
const sources = /* @__PURE__ */ new Map();
|
|
1039
|
+
for (const file of scenarioFiles) {
|
|
1040
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
1041
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1042
|
+
if (!document || errors.length > 0) {
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
for (const scenario of document.scenarios) {
|
|
1046
|
+
for (const tag of scenario.tags) {
|
|
1047
|
+
if (!SC_TAG_RE2.test(tag)) {
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
const current = sources.get(tag) ?? /* @__PURE__ */ new Set();
|
|
1051
|
+
current.add(file);
|
|
1052
|
+
sources.set(tag, current);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return sources;
|
|
1057
|
+
}
|
|
1058
|
+
async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
779
1059
|
const refs = /* @__PURE__ */ new Map();
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1060
|
+
const normalizedGlobs = normalizeGlobs(globs);
|
|
1061
|
+
const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
|
|
1062
|
+
const mergedExcludeGlobs = Array.from(
|
|
1063
|
+
/* @__PURE__ */ new Set([...DEFAULT_TEST_FILE_EXCLUDE_GLOBS, ...normalizedExcludeGlobs])
|
|
1064
|
+
);
|
|
1065
|
+
if (normalizedGlobs.length === 0) {
|
|
1066
|
+
return {
|
|
1067
|
+
refs,
|
|
1068
|
+
scan: {
|
|
1069
|
+
globs: normalizedGlobs,
|
|
1070
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
1071
|
+
matchedFileCount: 0
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
let files = [];
|
|
1076
|
+
try {
|
|
1077
|
+
files = await collectFilesByGlobs(root, {
|
|
1078
|
+
globs: normalizedGlobs,
|
|
1079
|
+
ignore: mergedExcludeGlobs
|
|
1080
|
+
});
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
return {
|
|
1083
|
+
refs,
|
|
1084
|
+
scan: {
|
|
1085
|
+
globs: normalizedGlobs,
|
|
1086
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
1087
|
+
matchedFileCount: 0
|
|
1088
|
+
},
|
|
1089
|
+
error: formatError3(error)
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
const normalizedFiles = Array.from(
|
|
1093
|
+
new Set(files.map((file) => import_node_path5.default.normalize(file)))
|
|
1094
|
+
);
|
|
1095
|
+
for (const file of normalizedFiles) {
|
|
1096
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
1097
|
+
const scIds = extractAnnotatedScIds(text);
|
|
786
1098
|
if (scIds.length === 0) {
|
|
787
1099
|
continue;
|
|
788
1100
|
}
|
|
@@ -792,7 +1104,14 @@ async function collectScTestReferences(testsRoot) {
|
|
|
792
1104
|
refs.set(scId, current);
|
|
793
1105
|
}
|
|
794
1106
|
}
|
|
795
|
-
return
|
|
1107
|
+
return {
|
|
1108
|
+
refs,
|
|
1109
|
+
scan: {
|
|
1110
|
+
globs: normalizedGlobs,
|
|
1111
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
1112
|
+
matchedFileCount: normalizedFiles.length
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
796
1115
|
}
|
|
797
1116
|
function buildScCoverage(scIds, refs) {
|
|
798
1117
|
const sortedScIds = toSortedArray(scIds);
|
|
@@ -820,18 +1139,27 @@ function buildScCoverage(scIds, refs) {
|
|
|
820
1139
|
function toSortedArray(values) {
|
|
821
1140
|
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
822
1141
|
}
|
|
1142
|
+
function normalizeGlobs(globs) {
|
|
1143
|
+
return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
1144
|
+
}
|
|
1145
|
+
function formatError3(error) {
|
|
1146
|
+
if (error instanceof Error) {
|
|
1147
|
+
return error.message;
|
|
1148
|
+
}
|
|
1149
|
+
return String(error);
|
|
1150
|
+
}
|
|
823
1151
|
|
|
824
1152
|
// src/core/version.ts
|
|
825
|
-
var
|
|
826
|
-
var
|
|
1153
|
+
var import_promises7 = require("fs/promises");
|
|
1154
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
827
1155
|
var import_node_url = require("url");
|
|
828
1156
|
async function resolveToolVersion() {
|
|
829
|
-
if ("0.4.
|
|
830
|
-
return "0.4.
|
|
1157
|
+
if ("0.4.4".length > 0) {
|
|
1158
|
+
return "0.4.4";
|
|
831
1159
|
}
|
|
832
1160
|
try {
|
|
833
1161
|
const packagePath = resolvePackageJsonPath();
|
|
834
|
-
const raw = await (0,
|
|
1162
|
+
const raw = await (0, import_promises7.readFile)(packagePath, "utf-8");
|
|
835
1163
|
const parsed = JSON.parse(raw);
|
|
836
1164
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
837
1165
|
return version.length > 0 ? version : "unknown";
|
|
@@ -842,54 +1170,23 @@ async function resolveToolVersion() {
|
|
|
842
1170
|
function resolvePackageJsonPath() {
|
|
843
1171
|
const base = __filename;
|
|
844
1172
|
const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
|
|
845
|
-
return
|
|
1173
|
+
return import_node_path6.default.resolve(import_node_path6.default.dirname(basePath), "../../package.json");
|
|
846
1174
|
}
|
|
847
1175
|
|
|
848
1176
|
// src/core/validators/contracts.ts
|
|
849
|
-
var
|
|
850
|
-
var
|
|
1177
|
+
var import_promises8 = require("fs/promises");
|
|
1178
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
851
1179
|
|
|
852
1180
|
// src/core/contracts.ts
|
|
853
|
-
var
|
|
1181
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
854
1182
|
var import_yaml2 = require("yaml");
|
|
855
1183
|
function parseStructuredContract(file, text) {
|
|
856
|
-
const ext =
|
|
1184
|
+
const ext = import_node_path7.default.extname(file).toLowerCase();
|
|
857
1185
|
if (ext === ".json") {
|
|
858
1186
|
return JSON.parse(text);
|
|
859
1187
|
}
|
|
860
1188
|
return (0, import_yaml2.parse)(text);
|
|
861
1189
|
}
|
|
862
|
-
function extractUiContractIds(doc) {
|
|
863
|
-
const id = typeof doc.id === "string" ? doc.id : "";
|
|
864
|
-
return extractIds(id, "UI");
|
|
865
|
-
}
|
|
866
|
-
function extractApiContractIds(doc) {
|
|
867
|
-
const operationIds = /* @__PURE__ */ new Set();
|
|
868
|
-
collectOperationIds(doc, operationIds);
|
|
869
|
-
const ids = /* @__PURE__ */ new Set();
|
|
870
|
-
for (const operationId of operationIds) {
|
|
871
|
-
extractIds(operationId, "API").forEach((id) => ids.add(id));
|
|
872
|
-
}
|
|
873
|
-
return Array.from(ids);
|
|
874
|
-
}
|
|
875
|
-
function collectOperationIds(value, out) {
|
|
876
|
-
if (!value || typeof value !== "object") {
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
if (Array.isArray(value)) {
|
|
880
|
-
for (const item of value) {
|
|
881
|
-
collectOperationIds(item, out);
|
|
882
|
-
}
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
886
|
-
if (key === "operationId" && typeof entry === "string") {
|
|
887
|
-
out.add(entry);
|
|
888
|
-
continue;
|
|
889
|
-
}
|
|
890
|
-
collectOperationIds(entry, out);
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
1190
|
|
|
894
1191
|
// src/core/validators/contracts.ts
|
|
895
1192
|
var SQL_DANGEROUS_PATTERNS = [
|
|
@@ -904,9 +1201,11 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
904
1201
|
async function validateContracts(root, config) {
|
|
905
1202
|
const issues = [];
|
|
906
1203
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
907
|
-
issues.push(...await validateUiContracts(
|
|
908
|
-
issues.push(...await validateApiContracts(
|
|
909
|
-
issues.push(...await validateDataContracts(
|
|
1204
|
+
issues.push(...await validateUiContracts(import_node_path8.default.join(contractsRoot, "ui")));
|
|
1205
|
+
issues.push(...await validateApiContracts(import_node_path8.default.join(contractsRoot, "api")));
|
|
1206
|
+
issues.push(...await validateDataContracts(import_node_path8.default.join(contractsRoot, "db")));
|
|
1207
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
1208
|
+
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
910
1209
|
return issues;
|
|
911
1210
|
}
|
|
912
1211
|
async function validateUiContracts(uiRoot) {
|
|
@@ -924,14 +1223,14 @@ async function validateUiContracts(uiRoot) {
|
|
|
924
1223
|
}
|
|
925
1224
|
const issues = [];
|
|
926
1225
|
for (const file of files) {
|
|
927
|
-
const text = await (0,
|
|
1226
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
928
1227
|
const invalidIds = extractInvalidIds(text, [
|
|
929
1228
|
"SPEC",
|
|
930
1229
|
"BR",
|
|
931
1230
|
"SC",
|
|
932
1231
|
"UI",
|
|
933
1232
|
"API",
|
|
934
|
-
"
|
|
1233
|
+
"DB",
|
|
935
1234
|
"ADR"
|
|
936
1235
|
]);
|
|
937
1236
|
if (invalidIds.length > 0) {
|
|
@@ -946,32 +1245,20 @@ async function validateUiContracts(uiRoot) {
|
|
|
946
1245
|
)
|
|
947
1246
|
);
|
|
948
1247
|
}
|
|
949
|
-
|
|
1248
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1249
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
|
|
950
1250
|
try {
|
|
951
|
-
|
|
1251
|
+
parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
952
1252
|
} catch (error) {
|
|
953
1253
|
issues.push(
|
|
954
1254
|
issue(
|
|
955
1255
|
"QFAI-CONTRACT-001",
|
|
956
|
-
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1256
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
|
|
957
1257
|
"error",
|
|
958
1258
|
file,
|
|
959
1259
|
"contracts.ui.parse"
|
|
960
1260
|
)
|
|
961
1261
|
);
|
|
962
|
-
continue;
|
|
963
|
-
}
|
|
964
|
-
const uiIds = extractUiContractIds(doc);
|
|
965
|
-
if (uiIds.length === 0) {
|
|
966
|
-
issues.push(
|
|
967
|
-
issue(
|
|
968
|
-
"QFAI-CONTRACT-002",
|
|
969
|
-
`UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
970
|
-
"error",
|
|
971
|
-
file,
|
|
972
|
-
"contracts.ui.id"
|
|
973
|
-
)
|
|
974
|
-
);
|
|
975
1262
|
}
|
|
976
1263
|
}
|
|
977
1264
|
return issues;
|
|
@@ -991,14 +1278,14 @@ async function validateApiContracts(apiRoot) {
|
|
|
991
1278
|
}
|
|
992
1279
|
const issues = [];
|
|
993
1280
|
for (const file of files) {
|
|
994
|
-
const text = await (0,
|
|
1281
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
995
1282
|
const invalidIds = extractInvalidIds(text, [
|
|
996
1283
|
"SPEC",
|
|
997
1284
|
"BR",
|
|
998
1285
|
"SC",
|
|
999
1286
|
"UI",
|
|
1000
1287
|
"API",
|
|
1001
|
-
"
|
|
1288
|
+
"DB",
|
|
1002
1289
|
"ADR"
|
|
1003
1290
|
]);
|
|
1004
1291
|
if (invalidIds.length > 0) {
|
|
@@ -1013,14 +1300,16 @@ async function validateApiContracts(apiRoot) {
|
|
|
1013
1300
|
)
|
|
1014
1301
|
);
|
|
1015
1302
|
}
|
|
1303
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1304
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "API"));
|
|
1016
1305
|
let doc;
|
|
1017
1306
|
try {
|
|
1018
|
-
doc = parseStructuredContract(file, text);
|
|
1307
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
1019
1308
|
} catch (error) {
|
|
1020
1309
|
issues.push(
|
|
1021
1310
|
issue(
|
|
1022
1311
|
"QFAI-CONTRACT-001",
|
|
1023
|
-
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1312
|
+
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
|
|
1024
1313
|
"error",
|
|
1025
1314
|
file,
|
|
1026
1315
|
"contracts.api.parse"
|
|
@@ -1039,18 +1328,6 @@ async function validateApiContracts(apiRoot) {
|
|
|
1039
1328
|
)
|
|
1040
1329
|
);
|
|
1041
1330
|
}
|
|
1042
|
-
const apiIds = extractApiContractIds(doc);
|
|
1043
|
-
if (apiIds.length === 0) {
|
|
1044
|
-
issues.push(
|
|
1045
|
-
issue(
|
|
1046
|
-
"QFAI-CONTRACT-002",
|
|
1047
|
-
`API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
1048
|
-
"error",
|
|
1049
|
-
file,
|
|
1050
|
-
"contracts.api.id"
|
|
1051
|
-
)
|
|
1052
|
-
);
|
|
1053
|
-
}
|
|
1054
1331
|
}
|
|
1055
1332
|
return issues;
|
|
1056
1333
|
}
|
|
@@ -1059,24 +1336,24 @@ async function validateDataContracts(dataRoot) {
|
|
|
1059
1336
|
if (files.length === 0) {
|
|
1060
1337
|
return [
|
|
1061
1338
|
issue(
|
|
1062
|
-
"QFAI-
|
|
1063
|
-
"
|
|
1339
|
+
"QFAI-DB-000",
|
|
1340
|
+
"DB \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1064
1341
|
"info",
|
|
1065
1342
|
dataRoot,
|
|
1066
|
-
"contracts.
|
|
1343
|
+
"contracts.db.files"
|
|
1067
1344
|
)
|
|
1068
1345
|
];
|
|
1069
1346
|
}
|
|
1070
1347
|
const issues = [];
|
|
1071
1348
|
for (const file of files) {
|
|
1072
|
-
const text = await (0,
|
|
1349
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
1073
1350
|
const invalidIds = extractInvalidIds(text, [
|
|
1074
1351
|
"SPEC",
|
|
1075
1352
|
"BR",
|
|
1076
1353
|
"SC",
|
|
1077
1354
|
"UI",
|
|
1078
1355
|
"API",
|
|
1079
|
-
"
|
|
1356
|
+
"DB",
|
|
1080
1357
|
"ADR"
|
|
1081
1358
|
]);
|
|
1082
1359
|
if (invalidIds.length > 0) {
|
|
@@ -1091,6 +1368,8 @@ async function validateDataContracts(dataRoot) {
|
|
|
1091
1368
|
)
|
|
1092
1369
|
);
|
|
1093
1370
|
}
|
|
1371
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1372
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "DB"));
|
|
1094
1373
|
issues.push(...lintSql(text, file));
|
|
1095
1374
|
}
|
|
1096
1375
|
return issues;
|
|
@@ -1101,21 +1380,87 @@ function lintSql(text, file) {
|
|
|
1101
1380
|
if (pattern.test(text)) {
|
|
1102
1381
|
issues.push(
|
|
1103
1382
|
issue(
|
|
1104
|
-
"QFAI-
|
|
1383
|
+
"QFAI-DB-001",
|
|
1105
1384
|
`\u5371\u967A\u306A SQL \u64CD\u4F5C\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059: ${label}`,
|
|
1106
1385
|
"warning",
|
|
1107
1386
|
file,
|
|
1108
|
-
"contracts.
|
|
1387
|
+
"contracts.db.sql"
|
|
1109
1388
|
)
|
|
1110
1389
|
);
|
|
1111
1390
|
}
|
|
1112
1391
|
}
|
|
1113
1392
|
return issues;
|
|
1114
1393
|
}
|
|
1394
|
+
function validateDeclaredContractIds(ids, file, kind) {
|
|
1395
|
+
const issues = [];
|
|
1396
|
+
if (ids.length === 0) {
|
|
1397
|
+
issues.push(
|
|
1398
|
+
issue(
|
|
1399
|
+
"QFAI-CONTRACT-010",
|
|
1400
|
+
`\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
|
|
1401
|
+
"error",
|
|
1402
|
+
file,
|
|
1403
|
+
"contracts.declaration"
|
|
1404
|
+
)
|
|
1405
|
+
);
|
|
1406
|
+
return issues;
|
|
1407
|
+
}
|
|
1408
|
+
if (ids.length > 1) {
|
|
1409
|
+
issues.push(
|
|
1410
|
+
issue(
|
|
1411
|
+
"QFAI-CONTRACT-011",
|
|
1412
|
+
`\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B\u8907\u6570\u306E QFAI-CONTRACT-ID \u304C\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${ids.join(
|
|
1413
|
+
", "
|
|
1414
|
+
)}`,
|
|
1415
|
+
"error",
|
|
1416
|
+
file,
|
|
1417
|
+
"contracts.declaration",
|
|
1418
|
+
ids
|
|
1419
|
+
)
|
|
1420
|
+
);
|
|
1421
|
+
return issues;
|
|
1422
|
+
}
|
|
1423
|
+
const [id] = ids;
|
|
1424
|
+
if (id && !id.startsWith(`${kind}-`)) {
|
|
1425
|
+
issues.push(
|
|
1426
|
+
issue(
|
|
1427
|
+
"QFAI-CONTRACT-013",
|
|
1428
|
+
`\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E QFAI-CONTRACT-ID \u304C ${kind}- \u3067\u306F\u3042\u308A\u307E\u305B\u3093: ${id}`,
|
|
1429
|
+
"error",
|
|
1430
|
+
file,
|
|
1431
|
+
"contracts.declarationPrefix",
|
|
1432
|
+
[id]
|
|
1433
|
+
)
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
return issues;
|
|
1437
|
+
}
|
|
1438
|
+
function validateDuplicateContractIds(contractIndex) {
|
|
1439
|
+
const issues = [];
|
|
1440
|
+
for (const [id, files] of contractIndex.idToFiles.entries()) {
|
|
1441
|
+
if (files.size <= 1) {
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
const sortedFiles = Array.from(files).sort((a, b) => a.localeCompare(b));
|
|
1445
|
+
issues.push(
|
|
1446
|
+
issue(
|
|
1447
|
+
"QFAI-CONTRACT-012",
|
|
1448
|
+
`\u5951\u7D04 ID \u304C\u8907\u6570\u30D5\u30A1\u30A4\u30EB\u3067\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${id} (${sortedFiles.join(
|
|
1449
|
+
", "
|
|
1450
|
+
)})`,
|
|
1451
|
+
"error",
|
|
1452
|
+
sortedFiles[0],
|
|
1453
|
+
"contracts.idDuplicate",
|
|
1454
|
+
[id]
|
|
1455
|
+
)
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
return issues;
|
|
1459
|
+
}
|
|
1115
1460
|
function hasOpenApi(doc) {
|
|
1116
1461
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
1117
1462
|
}
|
|
1118
|
-
function
|
|
1463
|
+
function formatError4(error) {
|
|
1119
1464
|
if (error instanceof Error) {
|
|
1120
1465
|
return error.message;
|
|
1121
1466
|
}
|
|
@@ -1140,279 +1485,98 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1140
1485
|
}
|
|
1141
1486
|
|
|
1142
1487
|
// src/core/validators/delta.ts
|
|
1143
|
-
var
|
|
1144
|
-
var
|
|
1488
|
+
var import_promises9 = require("fs/promises");
|
|
1489
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
1145
1490
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1146
1491
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1147
1492
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
1148
1493
|
var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
|
|
1149
1494
|
var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
|
|
1150
1495
|
async function validateDeltas(root, config) {
|
|
1151
|
-
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1152
|
-
const packs = await collectSpecPackDirs(specsRoot);
|
|
1153
|
-
if (packs.length === 0) {
|
|
1154
|
-
return [];
|
|
1155
|
-
}
|
|
1156
|
-
const issues = [];
|
|
1157
|
-
for (const pack of packs) {
|
|
1158
|
-
const deltaPath =
|
|
1159
|
-
let text;
|
|
1160
|
-
try {
|
|
1161
|
-
text = await (0,
|
|
1162
|
-
} catch (error) {
|
|
1163
|
-
if (isMissingFileError2(error)) {
|
|
1164
|
-
issues.push(
|
|
1165
|
-
issue2(
|
|
1166
|
-
"QFAI-DELTA-001",
|
|
1167
|
-
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1168
|
-
"error",
|
|
1169
|
-
deltaPath,
|
|
1170
|
-
"delta.exists"
|
|
1171
|
-
)
|
|
1172
|
-
);
|
|
1173
|
-
continue;
|
|
1174
|
-
}
|
|
1175
|
-
throw error;
|
|
1176
|
-
}
|
|
1177
|
-
const hasSection = SECTION_RE.test(text);
|
|
1178
|
-
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
1179
|
-
const hasChange = CHANGE_LINE_RE.test(text);
|
|
1180
|
-
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
1181
|
-
issues.push(
|
|
1182
|
-
issue2(
|
|
1183
|
-
"QFAI-DELTA-002",
|
|
1184
|
-
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
1185
|
-
"error",
|
|
1186
|
-
deltaPath,
|
|
1187
|
-
"delta.section"
|
|
1188
|
-
)
|
|
1189
|
-
);
|
|
1190
|
-
continue;
|
|
1191
|
-
}
|
|
1192
|
-
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
1193
|
-
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
1194
|
-
if (compatibilityChecked === changeChecked) {
|
|
1195
|
-
issues.push(
|
|
1196
|
-
issue2(
|
|
1197
|
-
"QFAI-DELTA-003",
|
|
1198
|
-
"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",
|
|
1199
|
-
"error",
|
|
1200
|
-
deltaPath,
|
|
1201
|
-
"delta.classification"
|
|
1202
|
-
)
|
|
1203
|
-
);
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
return issues;
|
|
1207
|
-
}
|
|
1208
|
-
function isMissingFileError2(error) {
|
|
1209
|
-
if (!error || typeof error !== "object") {
|
|
1210
|
-
return false;
|
|
1211
|
-
}
|
|
1212
|
-
return error.code === "ENOENT";
|
|
1213
|
-
}
|
|
1214
|
-
function issue2(code, message, severity, file, rule, refs) {
|
|
1215
|
-
const issue7 = {
|
|
1216
|
-
code,
|
|
1217
|
-
severity,
|
|
1218
|
-
message
|
|
1219
|
-
};
|
|
1220
|
-
if (file) {
|
|
1221
|
-
issue7.file = file;
|
|
1222
|
-
}
|
|
1223
|
-
if (rule) {
|
|
1224
|
-
issue7.rule = rule;
|
|
1225
|
-
}
|
|
1226
|
-
if (refs && refs.length > 0) {
|
|
1227
|
-
issue7.refs = refs;
|
|
1228
|
-
}
|
|
1229
|
-
return issue7;
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
// src/core/validators/ids.ts
|
|
1233
|
-
var import_promises10 = require("fs/promises");
|
|
1234
|
-
var import_node_path9 = __toESM(require("path"), 1);
|
|
1235
|
-
|
|
1236
|
-
// src/core/contractIndex.ts
|
|
1237
|
-
var import_promises9 = require("fs/promises");
|
|
1238
|
-
var import_node_path8 = __toESM(require("path"), 1);
|
|
1239
|
-
async function buildContractIndex(root, config) {
|
|
1240
|
-
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1241
|
-
const uiRoot = import_node_path8.default.join(contractsRoot, "ui");
|
|
1242
|
-
const apiRoot = import_node_path8.default.join(contractsRoot, "api");
|
|
1243
|
-
const dataRoot = import_node_path8.default.join(contractsRoot, "db");
|
|
1244
|
-
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
1245
|
-
collectUiContractFiles(uiRoot),
|
|
1246
|
-
collectApiContractFiles(apiRoot),
|
|
1247
|
-
collectDataContractFiles(dataRoot)
|
|
1248
|
-
]);
|
|
1249
|
-
const index = {
|
|
1250
|
-
ids: /* @__PURE__ */ new Set(),
|
|
1251
|
-
idToFiles: /* @__PURE__ */ new Map(),
|
|
1252
|
-
files: { ui: uiFiles, api: apiFiles, data: dataFiles },
|
|
1253
|
-
structuredParseFailedFiles: /* @__PURE__ */ new Set()
|
|
1254
|
-
};
|
|
1255
|
-
await indexUiContracts(uiFiles, index);
|
|
1256
|
-
await indexApiContracts(apiFiles, index);
|
|
1257
|
-
await indexDataContracts(dataFiles, index);
|
|
1258
|
-
return index;
|
|
1259
|
-
}
|
|
1260
|
-
async function indexUiContracts(files, index) {
|
|
1261
|
-
for (const file of files) {
|
|
1262
|
-
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1263
|
-
try {
|
|
1264
|
-
const doc = parseStructuredContract(file, text);
|
|
1265
|
-
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
1266
|
-
} catch {
|
|
1267
|
-
index.structuredParseFailedFiles.add(file);
|
|
1268
|
-
extractIds(text, "UI").forEach((id) => record(index, id, file));
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
async function indexApiContracts(files, index) {
|
|
1273
|
-
for (const file of files) {
|
|
1274
|
-
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1275
|
-
try {
|
|
1276
|
-
const doc = parseStructuredContract(file, text);
|
|
1277
|
-
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
1278
|
-
} catch {
|
|
1279
|
-
index.structuredParseFailedFiles.add(file);
|
|
1280
|
-
extractIds(text, "API").forEach((id) => record(index, id, file));
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
async function indexDataContracts(files, index) {
|
|
1285
|
-
for (const file of files) {
|
|
1286
|
-
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1287
|
-
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
function record(index, id, file) {
|
|
1291
|
-
index.ids.add(id);
|
|
1292
|
-
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
1293
|
-
current.add(file);
|
|
1294
|
-
index.idToFiles.set(id, current);
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
// src/core/parse/markdown.ts
|
|
1298
|
-
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1299
|
-
function parseHeadings(md) {
|
|
1300
|
-
const lines = md.split(/\r?\n/);
|
|
1301
|
-
const headings = [];
|
|
1302
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1303
|
-
const line = lines[i] ?? "";
|
|
1304
|
-
const match = line.match(HEADING_RE);
|
|
1305
|
-
if (!match) continue;
|
|
1306
|
-
const levelToken = match[1];
|
|
1307
|
-
const title = match[2];
|
|
1308
|
-
if (!levelToken || !title) continue;
|
|
1309
|
-
headings.push({
|
|
1310
|
-
level: levelToken.length,
|
|
1311
|
-
title: title.trim(),
|
|
1312
|
-
line: i + 1
|
|
1313
|
-
});
|
|
1314
|
-
}
|
|
1315
|
-
return headings;
|
|
1316
|
-
}
|
|
1317
|
-
function extractH2Sections(md) {
|
|
1318
|
-
const lines = md.split(/\r?\n/);
|
|
1319
|
-
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1320
|
-
const sections = /* @__PURE__ */ new Map();
|
|
1321
|
-
for (let i = 0; i < headings.length; i++) {
|
|
1322
|
-
const current = headings[i];
|
|
1323
|
-
if (!current) continue;
|
|
1324
|
-
const next = headings[i + 1];
|
|
1325
|
-
const startLine = current.line + 1;
|
|
1326
|
-
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1327
|
-
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1328
|
-
sections.set(current.title.trim(), {
|
|
1329
|
-
title: current.title.trim(),
|
|
1330
|
-
startLine,
|
|
1331
|
-
endLine,
|
|
1332
|
-
body
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
return sections;
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
// src/core/parse/spec.ts
|
|
1339
|
-
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1340
|
-
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1341
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1342
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1343
|
-
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1344
|
-
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1345
|
-
function parseSpec(md, file) {
|
|
1346
|
-
const headings = parseHeadings(md);
|
|
1347
|
-
const h1 = headings.find((heading) => heading.level === 1);
|
|
1348
|
-
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1349
|
-
const sections = extractH2Sections(md);
|
|
1350
|
-
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1351
|
-
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1352
|
-
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1353
|
-
const startLine = brSection?.startLine ?? 1;
|
|
1354
|
-
const brs = [];
|
|
1355
|
-
const brsWithoutPriority = [];
|
|
1356
|
-
const brsWithInvalidPriority = [];
|
|
1357
|
-
for (let i = 0; i < brLines.length; i++) {
|
|
1358
|
-
const lineText = brLines[i] ?? "";
|
|
1359
|
-
const lineNumber = startLine + i;
|
|
1360
|
-
const validMatch = lineText.match(BR_LINE_RE);
|
|
1361
|
-
if (validMatch) {
|
|
1362
|
-
const id = validMatch[1];
|
|
1363
|
-
const priority = validMatch[2];
|
|
1364
|
-
const text = validMatch[3];
|
|
1365
|
-
if (!id || !priority || !text) continue;
|
|
1366
|
-
brs.push({
|
|
1367
|
-
id,
|
|
1368
|
-
priority,
|
|
1369
|
-
text: text.trim(),
|
|
1370
|
-
line: lineNumber
|
|
1371
|
-
});
|
|
1372
|
-
continue;
|
|
1373
|
-
}
|
|
1374
|
-
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1375
|
-
if (anyPriorityMatch) {
|
|
1376
|
-
const id = anyPriorityMatch[1];
|
|
1377
|
-
const priority = anyPriorityMatch[2];
|
|
1378
|
-
const text = anyPriorityMatch[3];
|
|
1379
|
-
if (!id || !priority || !text) continue;
|
|
1380
|
-
if (!VALID_PRIORITIES.has(priority)) {
|
|
1381
|
-
brsWithInvalidPriority.push({
|
|
1382
|
-
id,
|
|
1383
|
-
priority,
|
|
1384
|
-
text: text.trim(),
|
|
1385
|
-
line: lineNumber
|
|
1386
|
-
});
|
|
1496
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1497
|
+
const packs = await collectSpecPackDirs(specsRoot);
|
|
1498
|
+
if (packs.length === 0) {
|
|
1499
|
+
return [];
|
|
1500
|
+
}
|
|
1501
|
+
const issues = [];
|
|
1502
|
+
for (const pack of packs) {
|
|
1503
|
+
const deltaPath = import_node_path9.default.join(pack, "delta.md");
|
|
1504
|
+
let text;
|
|
1505
|
+
try {
|
|
1506
|
+
text = await (0, import_promises9.readFile)(deltaPath, "utf-8");
|
|
1507
|
+
} catch (error) {
|
|
1508
|
+
if (isMissingFileError2(error)) {
|
|
1509
|
+
issues.push(
|
|
1510
|
+
issue2(
|
|
1511
|
+
"QFAI-DELTA-001",
|
|
1512
|
+
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1513
|
+
"error",
|
|
1514
|
+
deltaPath,
|
|
1515
|
+
"delta.exists"
|
|
1516
|
+
)
|
|
1517
|
+
);
|
|
1518
|
+
continue;
|
|
1387
1519
|
}
|
|
1520
|
+
throw error;
|
|
1521
|
+
}
|
|
1522
|
+
const hasSection = SECTION_RE.test(text);
|
|
1523
|
+
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
1524
|
+
const hasChange = CHANGE_LINE_RE.test(text);
|
|
1525
|
+
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
1526
|
+
issues.push(
|
|
1527
|
+
issue2(
|
|
1528
|
+
"QFAI-DELTA-002",
|
|
1529
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
1530
|
+
"error",
|
|
1531
|
+
deltaPath,
|
|
1532
|
+
"delta.section"
|
|
1533
|
+
)
|
|
1534
|
+
);
|
|
1388
1535
|
continue;
|
|
1389
1536
|
}
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1537
|
+
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
1538
|
+
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
1539
|
+
if (compatibilityChecked === changeChecked) {
|
|
1540
|
+
issues.push(
|
|
1541
|
+
issue2(
|
|
1542
|
+
"QFAI-DELTA-003",
|
|
1543
|
+
"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",
|
|
1544
|
+
"error",
|
|
1545
|
+
deltaPath,
|
|
1546
|
+
"delta.classification"
|
|
1547
|
+
)
|
|
1548
|
+
);
|
|
1400
1549
|
}
|
|
1401
1550
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1551
|
+
return issues;
|
|
1552
|
+
}
|
|
1553
|
+
function isMissingFileError2(error) {
|
|
1554
|
+
if (!error || typeof error !== "object") {
|
|
1555
|
+
return false;
|
|
1556
|
+
}
|
|
1557
|
+
return error.code === "ENOENT";
|
|
1558
|
+
}
|
|
1559
|
+
function issue2(code, message, severity, file, rule, refs) {
|
|
1560
|
+
const issue7 = {
|
|
1561
|
+
code,
|
|
1562
|
+
severity,
|
|
1563
|
+
message
|
|
1408
1564
|
};
|
|
1409
|
-
if (
|
|
1410
|
-
|
|
1565
|
+
if (file) {
|
|
1566
|
+
issue7.file = file;
|
|
1411
1567
|
}
|
|
1412
|
-
|
|
1568
|
+
if (rule) {
|
|
1569
|
+
issue7.rule = rule;
|
|
1570
|
+
}
|
|
1571
|
+
if (refs && refs.length > 0) {
|
|
1572
|
+
issue7.refs = refs;
|
|
1573
|
+
}
|
|
1574
|
+
return issue7;
|
|
1413
1575
|
}
|
|
1414
1576
|
|
|
1415
1577
|
// src/core/validators/ids.ts
|
|
1578
|
+
var import_promises10 = require("fs/promises");
|
|
1579
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
1416
1580
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1417
1581
|
async function validateDefinedIds(root, config) {
|
|
1418
1582
|
const issues = [];
|
|
@@ -1478,7 +1642,7 @@ function recordId(out, id, file) {
|
|
|
1478
1642
|
}
|
|
1479
1643
|
function formatFileList(files, root) {
|
|
1480
1644
|
return files.map((file) => {
|
|
1481
|
-
const relative =
|
|
1645
|
+
const relative = import_node_path10.default.relative(root, file);
|
|
1482
1646
|
return relative.length > 0 ? relative : file;
|
|
1483
1647
|
}).join(", ");
|
|
1484
1648
|
}
|
|
@@ -1507,7 +1671,6 @@ var WHEN_PATTERN = /\bWhen\b/;
|
|
|
1507
1671
|
var THEN_PATTERN = /\bThen\b/;
|
|
1508
1672
|
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1509
1673
|
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1510
|
-
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1511
1674
|
async function validateScenarios(root, config) {
|
|
1512
1675
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1513
1676
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -1556,7 +1719,7 @@ function validateScenarioContent(text, file) {
|
|
|
1556
1719
|
"SC",
|
|
1557
1720
|
"UI",
|
|
1558
1721
|
"API",
|
|
1559
|
-
"
|
|
1722
|
+
"DB",
|
|
1560
1723
|
"ADR"
|
|
1561
1724
|
]);
|
|
1562
1725
|
if (invalidIds.length > 0) {
|
|
@@ -1587,17 +1750,7 @@ function validateScenarioContent(text, file) {
|
|
|
1587
1750
|
const featureSpecTags = document.featureTags.filter(
|
|
1588
1751
|
(tag) => SPEC_TAG_RE2.test(tag)
|
|
1589
1752
|
);
|
|
1590
|
-
if (featureSpecTags.length
|
|
1591
|
-
issues.push(
|
|
1592
|
-
issue4(
|
|
1593
|
-
"QFAI-SC-009",
|
|
1594
|
-
"Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1595
|
-
"error",
|
|
1596
|
-
file,
|
|
1597
|
-
"scenario.featureSpec"
|
|
1598
|
-
)
|
|
1599
|
-
);
|
|
1600
|
-
} else if (featureSpecTags.length > 1) {
|
|
1753
|
+
if (featureSpecTags.length > 1) {
|
|
1601
1754
|
issues.push(
|
|
1602
1755
|
issue4(
|
|
1603
1756
|
"QFAI-SC-009",
|
|
@@ -1625,17 +1778,6 @@ function validateScenarioContent(text, file) {
|
|
|
1625
1778
|
)
|
|
1626
1779
|
);
|
|
1627
1780
|
}
|
|
1628
|
-
if (document.scenarios.length > 1) {
|
|
1629
|
-
issues.push(
|
|
1630
|
-
issue4(
|
|
1631
|
-
"QFAI-SC-011",
|
|
1632
|
-
`Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
|
|
1633
|
-
"error",
|
|
1634
|
-
file,
|
|
1635
|
-
"scenario.single"
|
|
1636
|
-
)
|
|
1637
|
-
);
|
|
1638
|
-
}
|
|
1639
1781
|
for (const scenario of document.scenarios) {
|
|
1640
1782
|
if (scenario.tags.length === 0) {
|
|
1641
1783
|
issues.push(
|
|
@@ -1656,12 +1798,6 @@ function validateScenarioContent(text, file) {
|
|
|
1656
1798
|
} else if (scTags.length > 1) {
|
|
1657
1799
|
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1658
1800
|
}
|
|
1659
|
-
if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
|
|
1660
|
-
missingTags.push("SPEC");
|
|
1661
|
-
}
|
|
1662
|
-
if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
|
|
1663
|
-
missingTags.push("BR");
|
|
1664
|
-
}
|
|
1665
1801
|
if (missingTags.length > 0) {
|
|
1666
1802
|
issues.push(
|
|
1667
1803
|
issue4(
|
|
@@ -1781,7 +1917,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1781
1917
|
"SC",
|
|
1782
1918
|
"UI",
|
|
1783
1919
|
"API",
|
|
1784
|
-
"
|
|
1920
|
+
"DB",
|
|
1785
1921
|
"ADR"
|
|
1786
1922
|
]);
|
|
1787
1923
|
if (invalidIds.length > 0) {
|
|
@@ -1896,9 +2032,8 @@ function isMissingFileError4(error) {
|
|
|
1896
2032
|
|
|
1897
2033
|
// src/core/validators/traceability.ts
|
|
1898
2034
|
var import_promises13 = require("fs/promises");
|
|
1899
|
-
var SC_TAG_RE5 = /^SC-\d{4}$/;
|
|
1900
2035
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1901
|
-
var
|
|
2036
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1902
2037
|
async function validateTraceability(root, config) {
|
|
1903
2038
|
const issues = [];
|
|
1904
2039
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1911,8 +2046,7 @@ async function validateTraceability(root, config) {
|
|
|
1911
2046
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
1912
2047
|
const brIdsInScenarios = /* @__PURE__ */ new Set();
|
|
1913
2048
|
const scIdsInScenarios = /* @__PURE__ */ new Set();
|
|
1914
|
-
const
|
|
1915
|
-
const scWithContracts = /* @__PURE__ */ new Set();
|
|
2049
|
+
const specContractIds = /* @__PURE__ */ new Set();
|
|
1916
2050
|
const specToBrIds = /* @__PURE__ */ new Map();
|
|
1917
2051
|
const contractIndex = await buildContractIndex(root, config);
|
|
1918
2052
|
const contractIds = contractIndex.ids;
|
|
@@ -1925,19 +2059,60 @@ async function validateTraceability(root, config) {
|
|
|
1925
2059
|
}
|
|
1926
2060
|
const brIds = parsed.brs.map((br) => br.id);
|
|
1927
2061
|
brIds.forEach((id) => brIdsInSpecs.add(id));
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
const
|
|
2062
|
+
if (parsed.specId) {
|
|
2063
|
+
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
2064
|
+
brIds.forEach((id) => current.add(id));
|
|
2065
|
+
specToBrIds.set(parsed.specId, current);
|
|
2066
|
+
}
|
|
2067
|
+
const contractRefs = parsed.contractRefs;
|
|
2068
|
+
if (contractRefs.lines.length === 0) {
|
|
2069
|
+
issues.push(
|
|
2070
|
+
issue6(
|
|
2071
|
+
"QFAI-TRACE-020",
|
|
2072
|
+
"Spec \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
2073
|
+
"error",
|
|
2074
|
+
file,
|
|
2075
|
+
"traceability.specContractRefRequired"
|
|
2076
|
+
)
|
|
2077
|
+
);
|
|
2078
|
+
} else {
|
|
2079
|
+
if (contractRefs.hasNone && contractRefs.ids.length > 0) {
|
|
2080
|
+
issues.push(
|
|
2081
|
+
issue6(
|
|
2082
|
+
"QFAI-TRACE-021",
|
|
2083
|
+
"Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2084
|
+
"error",
|
|
2085
|
+
file,
|
|
2086
|
+
"traceability.specContractRefFormat"
|
|
2087
|
+
)
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
if (contractRefs.invalidTokens.length > 0) {
|
|
2091
|
+
issues.push(
|
|
2092
|
+
issue6(
|
|
2093
|
+
"QFAI-TRACE-021",
|
|
2094
|
+
`Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
|
|
2095
|
+
", "
|
|
2096
|
+
)}`,
|
|
2097
|
+
"error",
|
|
2098
|
+
file,
|
|
2099
|
+
"traceability.specContractRefFormat",
|
|
2100
|
+
contractRefs.invalidTokens
|
|
2101
|
+
)
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
contractRefs.ids.forEach((id) => {
|
|
2106
|
+
specContractIds.add(id);
|
|
2107
|
+
});
|
|
2108
|
+
const unknownContractIds = contractRefs.ids.filter(
|
|
1934
2109
|
(id) => !contractIds.has(id)
|
|
1935
2110
|
);
|
|
1936
2111
|
if (unknownContractIds.length > 0) {
|
|
1937
2112
|
issues.push(
|
|
1938
2113
|
issue6(
|
|
1939
|
-
"QFAI-TRACE-
|
|
1940
|
-
`Spec \u304C\
|
|
2114
|
+
"QFAI-TRACE-021",
|
|
2115
|
+
`Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1941
2116
|
", "
|
|
1942
2117
|
)}`,
|
|
1943
2118
|
"error",
|
|
@@ -1947,11 +2122,6 @@ async function validateTraceability(root, config) {
|
|
|
1947
2122
|
)
|
|
1948
2123
|
);
|
|
1949
2124
|
}
|
|
1950
|
-
if (parsed.specId) {
|
|
1951
|
-
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
1952
|
-
brIds.forEach((id) => current.add(id));
|
|
1953
|
-
specToBrIds.set(parsed.specId, current);
|
|
1954
|
-
}
|
|
1955
2125
|
}
|
|
1956
2126
|
for (const file of scenarioFiles) {
|
|
1957
2127
|
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
@@ -1961,20 +2131,42 @@ async function validateTraceability(root, config) {
|
|
|
1961
2131
|
continue;
|
|
1962
2132
|
}
|
|
1963
2133
|
const atoms = buildScenarioAtoms(document);
|
|
2134
|
+
const scIdsInFile = /* @__PURE__ */ new Set();
|
|
1964
2135
|
for (const [index, scenario] of document.scenarios.entries()) {
|
|
1965
2136
|
const atom = atoms[index];
|
|
1966
2137
|
if (!atom) {
|
|
1967
2138
|
continue;
|
|
1968
2139
|
}
|
|
1969
2140
|
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
1970
|
-
const brTags = scenario.tags.filter((tag) =>
|
|
1971
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2141
|
+
const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
|
|
2142
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
|
|
2143
|
+
if (specTags.length === 0) {
|
|
2144
|
+
issues.push(
|
|
2145
|
+
issue6(
|
|
2146
|
+
"QFAI-TRACE-014",
|
|
2147
|
+
`Scenario \u304C SPEC \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
|
|
2148
|
+
"error",
|
|
2149
|
+
file,
|
|
2150
|
+
"traceability.scenarioSpecRequired"
|
|
2151
|
+
)
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
if (brTags.length === 0) {
|
|
2155
|
+
issues.push(
|
|
2156
|
+
issue6(
|
|
2157
|
+
"QFAI-TRACE-015",
|
|
2158
|
+
`Scenario \u304C BR \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
|
|
2159
|
+
"error",
|
|
2160
|
+
file,
|
|
2161
|
+
"traceability.scenarioBrRequired"
|
|
2162
|
+
)
|
|
2163
|
+
);
|
|
1977
2164
|
}
|
|
2165
|
+
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
2166
|
+
scTags.forEach((id) => {
|
|
2167
|
+
scIdsInScenarios.add(id);
|
|
2168
|
+
scIdsInFile.add(id);
|
|
2169
|
+
});
|
|
1978
2170
|
const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
|
|
1979
2171
|
if (unknownSpecIds.length > 0) {
|
|
1980
2172
|
issues.push(
|
|
@@ -2048,6 +2240,22 @@ async function validateTraceability(root, config) {
|
|
|
2048
2240
|
}
|
|
2049
2241
|
}
|
|
2050
2242
|
}
|
|
2243
|
+
if (scIdsInFile.size !== 1) {
|
|
2244
|
+
const invalidScIds = Array.from(scIdsInFile).sort(
|
|
2245
|
+
(a, b) => a.localeCompare(b)
|
|
2246
|
+
);
|
|
2247
|
+
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(", ")}`;
|
|
2248
|
+
issues.push(
|
|
2249
|
+
issue6(
|
|
2250
|
+
"QFAI-TRACE-012",
|
|
2251
|
+
`Spec entry \u304C Spec:SC=1:1 \u3092\u6E80\u305F\u3057\u3066\u3044\u307E\u305B\u3093: ${detail}`,
|
|
2252
|
+
"error",
|
|
2253
|
+
file,
|
|
2254
|
+
"traceability.specScOneToOne",
|
|
2255
|
+
invalidScIds
|
|
2256
|
+
)
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2051
2259
|
}
|
|
2052
2260
|
if (upstreamIds.size === 0) {
|
|
2053
2261
|
return [
|
|
@@ -2077,40 +2285,62 @@ async function validateTraceability(root, config) {
|
|
|
2077
2285
|
);
|
|
2078
2286
|
}
|
|
2079
2287
|
}
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2288
|
+
const scRefsResult = await collectScTestReferences(
|
|
2289
|
+
root,
|
|
2290
|
+
config.validation.traceability.testFileGlobs,
|
|
2291
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2292
|
+
);
|
|
2293
|
+
const scTestRefs = scRefsResult.refs;
|
|
2294
|
+
const testFileScan = scRefsResult.scan;
|
|
2295
|
+
const hasScenarios = scIdsInScenarios.size > 0;
|
|
2296
|
+
const hasGlobConfig = testFileScan.globs.length > 0;
|
|
2297
|
+
const hasMatchedTests = testFileScan.matchedFileCount > 0;
|
|
2298
|
+
if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
|
|
2299
|
+
const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
|
|
2300
|
+
issues.push(
|
|
2301
|
+
issue6(
|
|
2302
|
+
"QFAI-TRACE-013",
|
|
2303
|
+
`\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}`,
|
|
2304
|
+
"error",
|
|
2305
|
+
testsRoot,
|
|
2306
|
+
"traceability.testFileGlobs"
|
|
2307
|
+
)
|
|
2308
|
+
);
|
|
2309
|
+
} else {
|
|
2310
|
+
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
2311
|
+
const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
|
|
2312
|
+
const refs = scTestRefs.get(id);
|
|
2313
|
+
return !refs || refs.size === 0;
|
|
2314
|
+
});
|
|
2315
|
+
if (scWithoutTests.length > 0) {
|
|
2316
|
+
issues.push(
|
|
2317
|
+
issue6(
|
|
2318
|
+
"QFAI-TRACE-010",
|
|
2319
|
+
`SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(
|
|
2320
|
+
", "
|
|
2321
|
+
)}\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`,
|
|
2322
|
+
config.validation.traceability.scNoTestSeverity,
|
|
2323
|
+
testsRoot,
|
|
2324
|
+
"traceability.scMustHaveTest",
|
|
2325
|
+
scWithoutTests
|
|
2326
|
+
)
|
|
2327
|
+
);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
const unknownScIds = Array.from(scTestRefs.keys()).filter(
|
|
2331
|
+
(id) => !scIdsInScenarios.has(id)
|
|
2083
2332
|
);
|
|
2084
|
-
if (
|
|
2333
|
+
if (unknownScIds.length > 0) {
|
|
2085
2334
|
issues.push(
|
|
2086
2335
|
issue6(
|
|
2087
|
-
"
|
|
2088
|
-
|
|
2336
|
+
"QFAI-TRACE-011",
|
|
2337
|
+
`\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(
|
|
2089
2338
|
", "
|
|
2090
2339
|
)}`,
|
|
2091
2340
|
"error",
|
|
2092
|
-
specsRoot,
|
|
2093
|
-
"traceability.scMustTouchContracts",
|
|
2094
|
-
scWithoutContracts
|
|
2095
|
-
)
|
|
2096
|
-
);
|
|
2097
|
-
}
|
|
2098
|
-
}
|
|
2099
|
-
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
2100
|
-
const scTestRefs = await collectScTestReferences(testsRoot);
|
|
2101
|
-
const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
|
|
2102
|
-
const refs = scTestRefs.get(id);
|
|
2103
|
-
return !refs || refs.size === 0;
|
|
2104
|
-
});
|
|
2105
|
-
if (scWithoutTests.length > 0) {
|
|
2106
|
-
issues.push(
|
|
2107
|
-
issue6(
|
|
2108
|
-
"QFAI-TRACE-010",
|
|
2109
|
-
`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`,
|
|
2110
|
-
config.validation.traceability.scNoTestSeverity,
|
|
2111
2341
|
testsRoot,
|
|
2112
|
-
"traceability.
|
|
2113
|
-
|
|
2342
|
+
"traceability.scUnknownInTests",
|
|
2343
|
+
unknownScIds
|
|
2114
2344
|
)
|
|
2115
2345
|
);
|
|
2116
2346
|
}
|
|
@@ -2118,16 +2348,16 @@ async function validateTraceability(root, config) {
|
|
|
2118
2348
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
2119
2349
|
if (contractIds.size > 0) {
|
|
2120
2350
|
const orphanContracts = Array.from(contractIds).filter(
|
|
2121
|
-
(id) => !
|
|
2351
|
+
(id) => !specContractIds.has(id)
|
|
2122
2352
|
);
|
|
2123
2353
|
if (orphanContracts.length > 0) {
|
|
2124
2354
|
issues.push(
|
|
2125
2355
|
issue6(
|
|
2126
|
-
"
|
|
2127
|
-
`\u5951\u7D04\u304C
|
|
2356
|
+
"QFAI-TRACE-022",
|
|
2357
|
+
`\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
2128
2358
|
"error",
|
|
2129
2359
|
specsRoot,
|
|
2130
|
-
"traceability.
|
|
2360
|
+
"traceability.contractCoverage",
|
|
2131
2361
|
orphanContracts
|
|
2132
2362
|
)
|
|
2133
2363
|
);
|
|
@@ -2173,8 +2403,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2173
2403
|
issues.push(
|
|
2174
2404
|
issue6(
|
|
2175
2405
|
"QFAI-TRACE-002",
|
|
2176
|
-
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
2177
|
-
"
|
|
2406
|
+
"\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",
|
|
2407
|
+
"info",
|
|
2178
2408
|
srcRoot,
|
|
2179
2409
|
"traceability.codeReferences"
|
|
2180
2410
|
)
|
|
@@ -2217,11 +2447,24 @@ async function validateProject(root, configResult) {
|
|
|
2217
2447
|
...await validateDefinedIds(root, config),
|
|
2218
2448
|
...await validateTraceability(root, config)
|
|
2219
2449
|
];
|
|
2450
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2451
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
2452
|
+
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2453
|
+
const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
|
|
2454
|
+
root,
|
|
2455
|
+
config.validation.traceability.testFileGlobs,
|
|
2456
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2457
|
+
);
|
|
2458
|
+
const scCoverage = buildScCoverage(scIds, scTestRefs);
|
|
2220
2459
|
const toolVersion = await resolveToolVersion();
|
|
2221
2460
|
return {
|
|
2222
2461
|
toolVersion,
|
|
2223
2462
|
issues,
|
|
2224
|
-
counts: countIssues(issues)
|
|
2463
|
+
counts: countIssues(issues),
|
|
2464
|
+
traceability: {
|
|
2465
|
+
sc: scCoverage,
|
|
2466
|
+
testFiles
|
|
2467
|
+
}
|
|
2225
2468
|
};
|
|
2226
2469
|
}
|
|
2227
2470
|
function countIssues(issues) {
|
|
@@ -2235,16 +2478,16 @@ function countIssues(issues) {
|
|
|
2235
2478
|
}
|
|
2236
2479
|
|
|
2237
2480
|
// src/core/report.ts
|
|
2238
|
-
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "
|
|
2481
|
+
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2239
2482
|
async function createReportData(root, validation, configResult) {
|
|
2240
2483
|
const resolved = configResult ?? await loadConfig(root);
|
|
2241
2484
|
const config = resolved.config;
|
|
2242
2485
|
const configPath = resolved.configPath;
|
|
2243
2486
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2244
2487
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2245
|
-
const apiRoot =
|
|
2246
|
-
const uiRoot =
|
|
2247
|
-
const dbRoot =
|
|
2488
|
+
const apiRoot = import_node_path11.default.join(contractsRoot, "api");
|
|
2489
|
+
const uiRoot = import_node_path11.default.join(contractsRoot, "ui");
|
|
2490
|
+
const dbRoot = import_node_path11.default.join(contractsRoot, "db");
|
|
2248
2491
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
2249
2492
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
2250
2493
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -2254,6 +2497,23 @@ async function createReportData(root, validation, configResult) {
|
|
|
2254
2497
|
ui: uiFiles,
|
|
2255
2498
|
db: dbFiles
|
|
2256
2499
|
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
2500
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
2501
|
+
const specContractRefs = await collectSpecContractRefs(specFiles);
|
|
2502
|
+
const contractIdList = Array.from(contractIndex.ids);
|
|
2503
|
+
const referencedContracts = /* @__PURE__ */ new Set();
|
|
2504
|
+
for (const ids of specContractRefs.specToContractIds.values()) {
|
|
2505
|
+
ids.forEach((id) => referencedContracts.add(id));
|
|
2506
|
+
}
|
|
2507
|
+
const referencedContractCount = contractIdList.filter(
|
|
2508
|
+
(id) => referencedContracts.has(id)
|
|
2509
|
+
).length;
|
|
2510
|
+
const orphanContractCount = contractIdList.filter(
|
|
2511
|
+
(id) => !referencedContracts.has(id)
|
|
2512
|
+
).length;
|
|
2513
|
+
const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
|
|
2514
|
+
const specToContractIdsRecord = mapToSortedRecord(
|
|
2515
|
+
specContractRefs.specToContractIds
|
|
2516
|
+
);
|
|
2257
2517
|
const idsByPrefix = await collectIds([
|
|
2258
2518
|
...specFiles,
|
|
2259
2519
|
...scenarioFiles,
|
|
@@ -2271,8 +2531,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
2271
2531
|
testsRoot
|
|
2272
2532
|
);
|
|
2273
2533
|
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2274
|
-
const
|
|
2275
|
-
|
|
2534
|
+
const scRefsResult = await collectScTestReferences(
|
|
2535
|
+
root,
|
|
2536
|
+
config.validation.traceability.testFileGlobs,
|
|
2537
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2538
|
+
);
|
|
2539
|
+
const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
|
|
2540
|
+
const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
|
|
2541
|
+
const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
|
|
2542
|
+
const scSourceRecord = mapToSortedRecord(scSources);
|
|
2276
2543
|
const resolvedValidation = validation ?? await validateProject(root, resolved);
|
|
2277
2544
|
const version = await resolveToolVersion();
|
|
2278
2545
|
return {
|
|
@@ -2297,12 +2564,24 @@ async function createReportData(root, validation, configResult) {
|
|
|
2297
2564
|
sc: idsByPrefix.SC,
|
|
2298
2565
|
ui: idsByPrefix.UI,
|
|
2299
2566
|
api: idsByPrefix.API,
|
|
2300
|
-
|
|
2567
|
+
db: idsByPrefix.DB
|
|
2301
2568
|
},
|
|
2302
2569
|
traceability: {
|
|
2303
2570
|
upstreamIdsFound: upstreamIds.size,
|
|
2304
2571
|
referencedInCodeOrTests: traceability,
|
|
2305
|
-
sc: scCoverage
|
|
2572
|
+
sc: scCoverage,
|
|
2573
|
+
scSources: scSourceRecord,
|
|
2574
|
+
testFiles,
|
|
2575
|
+
contracts: {
|
|
2576
|
+
total: contractIdList.length,
|
|
2577
|
+
referenced: referencedContractCount,
|
|
2578
|
+
orphan: orphanContractCount,
|
|
2579
|
+
idToSpecs: contractIdToSpecsRecord
|
|
2580
|
+
},
|
|
2581
|
+
specs: {
|
|
2582
|
+
contractRefMissing: specContractRefs.missingRefSpecs.size,
|
|
2583
|
+
specToContractIds: specToContractIdsRecord
|
|
2584
|
+
}
|
|
2306
2585
|
},
|
|
2307
2586
|
issues: resolvedValidation.issues
|
|
2308
2587
|
};
|
|
@@ -2331,7 +2610,7 @@ function formatReportMarkdown(data) {
|
|
|
2331
2610
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
2332
2611
|
lines.push(formatIdLine("UI", data.ids.ui));
|
|
2333
2612
|
lines.push(formatIdLine("API", data.ids.api));
|
|
2334
|
-
lines.push(formatIdLine("
|
|
2613
|
+
lines.push(formatIdLine("DB", data.ids.db));
|
|
2335
2614
|
lines.push("");
|
|
2336
2615
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
2337
2616
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
@@ -2339,14 +2618,77 @@ function formatReportMarkdown(data) {
|
|
|
2339
2618
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2340
2619
|
);
|
|
2341
2620
|
lines.push("");
|
|
2621
|
+
lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2622
|
+
lines.push(`- total: ${data.traceability.contracts.total}`);
|
|
2623
|
+
lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
|
|
2624
|
+
lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
|
|
2625
|
+
lines.push(
|
|
2626
|
+
`- specContractRefMissing: ${data.traceability.specs.contractRefMissing}`
|
|
2627
|
+
);
|
|
2628
|
+
lines.push("");
|
|
2629
|
+
lines.push("## \u5951\u7D04\u2192Spec");
|
|
2630
|
+
const contractToSpecs = data.traceability.contracts.idToSpecs;
|
|
2631
|
+
const contractIds = Object.keys(contractToSpecs).sort(
|
|
2632
|
+
(a, b) => a.localeCompare(b)
|
|
2633
|
+
);
|
|
2634
|
+
if (contractIds.length === 0) {
|
|
2635
|
+
lines.push("- (none)");
|
|
2636
|
+
} else {
|
|
2637
|
+
for (const contractId of contractIds) {
|
|
2638
|
+
const specs = contractToSpecs[contractId] ?? [];
|
|
2639
|
+
if (specs.length === 0) {
|
|
2640
|
+
lines.push(`- ${contractId}: (none)`);
|
|
2641
|
+
} else {
|
|
2642
|
+
lines.push(`- ${contractId}: ${specs.join(", ")}`);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
lines.push("");
|
|
2647
|
+
lines.push("## Spec\u2192\u5951\u7D04");
|
|
2648
|
+
const specToContracts = data.traceability.specs.specToContractIds;
|
|
2649
|
+
const specIds = Object.keys(specToContracts).sort(
|
|
2650
|
+
(a, b) => a.localeCompare(b)
|
|
2651
|
+
);
|
|
2652
|
+
if (specIds.length === 0) {
|
|
2653
|
+
lines.push("- (none)");
|
|
2654
|
+
} else {
|
|
2655
|
+
for (const specId of specIds) {
|
|
2656
|
+
const contractIds2 = specToContracts[specId] ?? [];
|
|
2657
|
+
if (contractIds2.length === 0) {
|
|
2658
|
+
lines.push(`- ${specId}: (none)`);
|
|
2659
|
+
} else {
|
|
2660
|
+
lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
lines.push("");
|
|
2342
2665
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2343
2666
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2344
2667
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2345
2668
|
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
2669
|
+
lines.push(
|
|
2670
|
+
`- testFileGlobs: ${formatList(data.traceability.testFiles.globs)}`
|
|
2671
|
+
);
|
|
2672
|
+
lines.push(
|
|
2673
|
+
`- testFileExcludeGlobs: ${formatList(
|
|
2674
|
+
data.traceability.testFiles.excludeGlobs
|
|
2675
|
+
)}`
|
|
2676
|
+
);
|
|
2677
|
+
lines.push(
|
|
2678
|
+
`- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
|
|
2679
|
+
);
|
|
2346
2680
|
if (data.traceability.sc.missingIds.length === 0) {
|
|
2347
2681
|
lines.push("- missingIds: (none)");
|
|
2348
2682
|
} else {
|
|
2349
|
-
|
|
2683
|
+
const sources = data.traceability.scSources;
|
|
2684
|
+
const missingWithSources = data.traceability.sc.missingIds.map((id) => {
|
|
2685
|
+
const files = sources[id] ?? [];
|
|
2686
|
+
if (files.length === 0) {
|
|
2687
|
+
return id;
|
|
2688
|
+
}
|
|
2689
|
+
return `${id} (${files.join(", ")})`;
|
|
2690
|
+
});
|
|
2691
|
+
lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
|
|
2350
2692
|
}
|
|
2351
2693
|
lines.push("");
|
|
2352
2694
|
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
@@ -2365,6 +2707,20 @@ function formatReportMarkdown(data) {
|
|
|
2365
2707
|
}
|
|
2366
2708
|
}
|
|
2367
2709
|
lines.push("");
|
|
2710
|
+
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
2711
|
+
const specScIssues = data.issues.filter(
|
|
2712
|
+
(item) => item.code === "QFAI-TRACE-012"
|
|
2713
|
+
);
|
|
2714
|
+
if (specScIssues.length === 0) {
|
|
2715
|
+
lines.push("- (none)");
|
|
2716
|
+
} else {
|
|
2717
|
+
for (const item of specScIssues) {
|
|
2718
|
+
const location = item.file ?? "(unknown)";
|
|
2719
|
+
const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
|
|
2720
|
+
lines.push(`- ${location}: ${refs}`);
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
lines.push("");
|
|
2368
2724
|
lines.push("## Hotspots");
|
|
2369
2725
|
const hotspots = buildHotspots(data.issues);
|
|
2370
2726
|
if (hotspots.length === 0) {
|
|
@@ -2379,7 +2735,7 @@ function formatReportMarkdown(data) {
|
|
|
2379
2735
|
lines.push("");
|
|
2380
2736
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
2381
2737
|
const traceIssues = data.issues.filter(
|
|
2382
|
-
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2738
|
+
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2383
2739
|
);
|
|
2384
2740
|
if (traceIssues.length === 0) {
|
|
2385
2741
|
lines.push("- (none)");
|
|
@@ -2409,6 +2765,33 @@ function formatReportMarkdown(data) {
|
|
|
2409
2765
|
function formatReportJson(data) {
|
|
2410
2766
|
return JSON.stringify(data, null, 2);
|
|
2411
2767
|
}
|
|
2768
|
+
async function collectSpecContractRefs(specFiles) {
|
|
2769
|
+
const specToContractIds = /* @__PURE__ */ new Map();
|
|
2770
|
+
const idToSpecs = /* @__PURE__ */ new Map();
|
|
2771
|
+
const missingRefSpecs = /* @__PURE__ */ new Set();
|
|
2772
|
+
for (const file of specFiles) {
|
|
2773
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2774
|
+
const parsed = parseSpec(text, file);
|
|
2775
|
+
const specKey = parsed.specId ?? file;
|
|
2776
|
+
const refs = parsed.contractRefs;
|
|
2777
|
+
if (refs.lines.length === 0) {
|
|
2778
|
+
missingRefSpecs.add(specKey);
|
|
2779
|
+
}
|
|
2780
|
+
const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
|
|
2781
|
+
for (const id of refs.ids) {
|
|
2782
|
+
currentContracts.add(id);
|
|
2783
|
+
const specs = idToSpecs.get(id) ?? /* @__PURE__ */ new Set();
|
|
2784
|
+
specs.add(specKey);
|
|
2785
|
+
idToSpecs.set(id, specs);
|
|
2786
|
+
}
|
|
2787
|
+
specToContractIds.set(specKey, currentContracts);
|
|
2788
|
+
}
|
|
2789
|
+
return {
|
|
2790
|
+
specToContractIds,
|
|
2791
|
+
idToSpecs,
|
|
2792
|
+
missingRefSpecs
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2412
2795
|
async function collectIds(files) {
|
|
2413
2796
|
const result = {
|
|
2414
2797
|
SPEC: /* @__PURE__ */ new Set(),
|
|
@@ -2416,7 +2799,7 @@ async function collectIds(files) {
|
|
|
2416
2799
|
SC: /* @__PURE__ */ new Set(),
|
|
2417
2800
|
UI: /* @__PURE__ */ new Set(),
|
|
2418
2801
|
API: /* @__PURE__ */ new Set(),
|
|
2419
|
-
|
|
2802
|
+
DB: /* @__PURE__ */ new Set()
|
|
2420
2803
|
};
|
|
2421
2804
|
for (const file of files) {
|
|
2422
2805
|
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
@@ -2431,7 +2814,7 @@ async function collectIds(files) {
|
|
|
2431
2814
|
SC: toSortedArray2(result.SC),
|
|
2432
2815
|
UI: toSortedArray2(result.UI),
|
|
2433
2816
|
API: toSortedArray2(result.API),
|
|
2434
|
-
|
|
2817
|
+
DB: toSortedArray2(result.DB)
|
|
2435
2818
|
};
|
|
2436
2819
|
}
|
|
2437
2820
|
async function collectUpstreamIds(files) {
|
|
@@ -2475,9 +2858,22 @@ function formatIdLine(label, values) {
|
|
|
2475
2858
|
}
|
|
2476
2859
|
return `- ${label}: ${values.join(", ")}`;
|
|
2477
2860
|
}
|
|
2861
|
+
function formatList(values) {
|
|
2862
|
+
if (values.length === 0) {
|
|
2863
|
+
return "(none)";
|
|
2864
|
+
}
|
|
2865
|
+
return values.join(", ");
|
|
2866
|
+
}
|
|
2478
2867
|
function toSortedArray2(values) {
|
|
2479
2868
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2480
2869
|
}
|
|
2870
|
+
function mapToSortedRecord(values) {
|
|
2871
|
+
const record2 = {};
|
|
2872
|
+
for (const [key, files] of values.entries()) {
|
|
2873
|
+
record2[key] = Array.from(files).sort((a, b) => a.localeCompare(b));
|
|
2874
|
+
}
|
|
2875
|
+
return record2;
|
|
2876
|
+
}
|
|
2481
2877
|
function buildHotspots(issues) {
|
|
2482
2878
|
const map = /* @__PURE__ */ new Map();
|
|
2483
2879
|
for (const issue7 of issues) {
|