qfai 0.4.2 → 0.4.6
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 +14 -0
- package/assets/init/.qfai/README.md +1 -0
- package/assets/init/.qfai/contracts/README.md +21 -11
- 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/promptpack/steering/traceability.md +5 -4
- package/assets/init/.qfai/prompts/makeOverview.md +1 -1
- 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/scenario.md +1 -1
- package/assets/init/.qfai/specs/spec-0001/spec.md +2 -0
- package/assets/init/root/qfai.config.yaml +0 -1
- package/dist/cli/commands/init.d.ts +8 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +30 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/report.d.ts +7 -0
- package/dist/cli/commands/report.d.ts.map +1 -0
- package/dist/cli/commands/report.js +108 -0
- package/dist/cli/commands/report.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +9 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +57 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.cjs +536 -336
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/index.mjs +536 -336
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/lib/args.d.ts +18 -0
- package/dist/cli/lib/args.d.ts.map +1 -0
- package/dist/cli/lib/args.js +98 -0
- package/dist/cli/lib/args.js.map +1 -0
- package/dist/cli/lib/assets.d.ts +2 -0
- package/dist/cli/lib/assets.d.ts.map +1 -0
- package/dist/cli/lib/assets.js +24 -0
- package/dist/cli/lib/assets.js.map +1 -0
- package/dist/cli/lib/failOn.d.ts +5 -0
- package/dist/cli/lib/failOn.d.ts.map +1 -0
- package/dist/cli/lib/failOn.js +10 -0
- package/dist/cli/lib/failOn.js.map +1 -0
- package/dist/cli/lib/fs.d.ts +11 -0
- package/dist/cli/lib/fs.d.ts.map +1 -0
- package/dist/cli/lib/fs.js +91 -0
- package/dist/cli/lib/fs.js.map +1 -0
- package/dist/cli/lib/logger.d.ts +4 -0
- package/dist/cli/lib/logger.d.ts.map +1 -0
- package/dist/cli/lib/logger.js +10 -0
- package/dist/cli/lib/logger.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +66 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/core/config.d.ts +47 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +224 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/contractIndex.d.ts +12 -0
- package/dist/core/contractIndex.d.ts.map +1 -0
- package/dist/core/contractIndex.js +38 -0
- package/dist/core/contractIndex.js.map +1 -0
- package/dist/core/contracts.d.ts +5 -0
- package/dist/core/contracts.d.ts.map +1 -0
- package/dist/core/contracts.js +42 -0
- package/dist/core/contracts.js.map +1 -0
- 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/discovery.d.ts +14 -0
- package/dist/core/discovery.d.ts.map +1 -0
- package/dist/core/discovery.js +55 -0
- package/dist/core/discovery.js.map +1 -0
- package/dist/core/fs.d.ts +11 -0
- package/dist/core/fs.d.ts.map +1 -0
- package/dist/core/fs.js +68 -0
- package/dist/core/fs.js.map +1 -0
- package/dist/core/gherkin/parse.d.ts +7 -0
- package/dist/core/gherkin/parse.d.ts.map +1 -0
- package/dist/core/gherkin/parse.js +25 -0
- package/dist/core/gherkin/parse.js.map +1 -0
- package/dist/core/ids.d.ts +6 -0
- package/dist/core/ids.d.ts.map +1 -0
- package/dist/core/ids.js +52 -0
- package/dist/core/ids.js.map +1 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +13 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/parse/adr.d.ts +13 -0
- package/dist/core/parse/adr.d.ts.map +1 -0
- package/dist/core/parse/adr.js +33 -0
- package/dist/core/parse/adr.js.map +1 -0
- package/dist/core/parse/gherkin.d.ts +12 -0
- package/dist/core/parse/gherkin.d.ts.map +1 -0
- package/dist/core/parse/gherkin.js +22 -0
- package/dist/core/parse/gherkin.js.map +1 -0
- package/dist/core/parse/markdown.d.ts +14 -0
- package/dist/core/parse/markdown.d.ts.map +1 -0
- package/dist/core/parse/markdown.js +45 -0
- package/dist/core/parse/markdown.js.map +1 -0
- package/dist/core/parse/spec.d.ts +36 -0
- package/dist/core/parse/spec.d.ts.map +1 -0
- package/dist/core/parse/spec.js +123 -0
- package/dist/core/parse/spec.js.map +1 -0
- package/dist/core/report.d.ts +55 -0
- package/dist/core/report.d.ts.map +1 -0
- package/dist/core/report.js +393 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/scenarioModel.d.ts +33 -0
- package/dist/core/scenarioModel.d.ts.map +1 -0
- package/dist/core/scenarioModel.js +128 -0
- package/dist/core/scenarioModel.js.map +1 -0
- package/dist/core/specLayout.d.ts +8 -0
- package/dist/core/specLayout.d.ts.map +1 -0
- package/dist/core/specLayout.js +36 -0
- package/dist/core/specLayout.js.map +1 -0
- package/dist/core/traceability.d.ts +26 -0
- package/dist/core/traceability.d.ts.map +1 -0
- package/dist/core/traceability.js +157 -0
- package/dist/core/traceability.js.map +1 -0
- package/dist/core/types.d.ts +31 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/validate.d.ts +4 -0
- package/dist/core/validate.d.ts.map +1 -0
- package/dist/core/validate.js +45 -0
- package/dist/core/validate.js.map +1 -0
- package/dist/core/validators/contracts.d.ts +5 -0
- package/dist/core/validators/contracts.d.ts.map +1 -0
- package/dist/core/validators/contracts.js +189 -0
- package/dist/core/validators/contracts.js.map +1 -0
- package/dist/core/validators/delta.d.ts +4 -0
- package/dist/core/validators/delta.d.ts.map +1 -0
- package/dist/core/validators/delta.js +68 -0
- package/dist/core/validators/delta.js.map +1 -0
- package/dist/core/validators/ids.d.ts +4 -0
- package/dist/core/validators/ids.d.ts.map +1 -0
- package/dist/core/validators/ids.js +88 -0
- package/dist/core/validators/ids.js.map +1 -0
- package/dist/core/validators/scenario.d.ts +5 -0
- package/dist/core/validators/scenario.d.ts.map +1 -0
- package/dist/core/validators/scenario.js +127 -0
- package/dist/core/validators/scenario.js.map +1 -0
- package/dist/core/validators/spec.d.ts +5 -0
- package/dist/core/validators/spec.d.ts.map +1 -0
- package/dist/core/validators/spec.js +94 -0
- package/dist/core/validators/spec.js.map +1 -0
- package/dist/core/validators/traceability.d.ts +4 -0
- package/dist/core/validators/traceability.d.ts.map +1 -0
- package/dist/core/validators/traceability.js +222 -0
- package/dist/core/validators/traceability.js.map +1 -0
- package/dist/core/version.d.ts +2 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/core/version.js +25 -0
- package/dist/core/version.js.map +1 -0
- package/dist/index.cjs +511 -335
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -5
- package/dist/index.d.ts +2 -156
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +511 -335
- package/dist/index.mjs.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -83,7 +83,6 @@ var defaultConfig = {
|
|
|
83
83
|
},
|
|
84
84
|
traceability: {
|
|
85
85
|
brMustHaveSc: true,
|
|
86
|
-
scMustTouchContracts: true,
|
|
87
86
|
scMustHaveTest: true,
|
|
88
87
|
testFileGlobs: [],
|
|
89
88
|
testFileExcludeGlobs: [],
|
|
@@ -260,13 +259,6 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
260
259
|
configPath,
|
|
261
260
|
issues
|
|
262
261
|
),
|
|
263
|
-
scMustTouchContracts: readBoolean(
|
|
264
|
-
traceabilityRaw?.scMustTouchContracts,
|
|
265
|
-
base.traceability.scMustTouchContracts,
|
|
266
|
-
"validation.traceability.scMustTouchContracts",
|
|
267
|
-
configPath,
|
|
268
|
-
issues
|
|
269
|
-
),
|
|
270
262
|
scMustHaveTest: readBoolean(
|
|
271
263
|
traceabilityRaw?.scMustHaveTest,
|
|
272
264
|
base.traceability.scMustHaveTest,
|
|
@@ -420,14 +412,14 @@ function isRecord(value) {
|
|
|
420
412
|
}
|
|
421
413
|
|
|
422
414
|
// src/core/ids.ts
|
|
423
|
-
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "
|
|
415
|
+
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
424
416
|
var STRICT_ID_PATTERNS = {
|
|
425
417
|
SPEC: /\bSPEC-\d{4}\b/g,
|
|
426
418
|
BR: /\bBR-\d{4}\b/g,
|
|
427
419
|
SC: /\bSC-\d{4}\b/g,
|
|
428
420
|
UI: /\bUI-\d{4}\b/g,
|
|
429
421
|
API: /\bAPI-\d{4}\b/g,
|
|
430
|
-
|
|
422
|
+
DB: /\bDB-\d{4}\b/g,
|
|
431
423
|
ADR: /\bADR-\d{4}\b/g
|
|
432
424
|
};
|
|
433
425
|
var LOOSE_ID_PATTERNS = {
|
|
@@ -436,7 +428,7 @@ var LOOSE_ID_PATTERNS = {
|
|
|
436
428
|
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
437
429
|
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
438
430
|
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
439
|
-
|
|
431
|
+
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
440
432
|
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
441
433
|
};
|
|
442
434
|
function extractIds(text, prefix) {
|
|
@@ -476,6 +468,10 @@ function isValidId(value, prefix) {
|
|
|
476
468
|
var import_promises14 = require("fs/promises");
|
|
477
469
|
var import_node_path11 = __toESM(require("path"), 1);
|
|
478
470
|
|
|
471
|
+
// src/core/contractIndex.ts
|
|
472
|
+
var import_promises5 = require("fs/promises");
|
|
473
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
474
|
+
|
|
479
475
|
// src/core/discovery.ts
|
|
480
476
|
var import_promises4 = require("fs/promises");
|
|
481
477
|
|
|
@@ -598,14 +594,14 @@ async function collectUiContractFiles(uiRoot) {
|
|
|
598
594
|
async function collectApiContractFiles(apiRoot) {
|
|
599
595
|
return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
|
|
600
596
|
}
|
|
601
|
-
async function
|
|
602
|
-
return collectFiles(
|
|
597
|
+
async function collectDbContractFiles(dbRoot) {
|
|
598
|
+
return collectFiles(dbRoot, { extensions: [".sql"] });
|
|
603
599
|
}
|
|
604
|
-
async function collectContractFiles(uiRoot, apiRoot,
|
|
600
|
+
async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
|
|
605
601
|
const [ui, api, db] = await Promise.all([
|
|
606
602
|
collectUiContractFiles(uiRoot),
|
|
607
603
|
collectApiContractFiles(apiRoot),
|
|
608
|
-
|
|
604
|
+
collectDbContractFiles(dbRoot)
|
|
609
605
|
]);
|
|
610
606
|
return { ui, api, db };
|
|
611
607
|
}
|
|
@@ -627,9 +623,221 @@ async function exists2(target) {
|
|
|
627
623
|
}
|
|
628
624
|
}
|
|
629
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
|
+
collectDbContractFiles(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
|
+
|
|
630
838
|
// src/core/traceability.ts
|
|
631
|
-
var
|
|
632
|
-
var
|
|
839
|
+
var import_promises6 = require("fs/promises");
|
|
840
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
633
841
|
|
|
634
842
|
// src/core/gherkin/parse.ts
|
|
635
843
|
var import_gherkin = require("@cucumber/gherkin");
|
|
@@ -662,7 +870,7 @@ var SC_TAG_RE = /^SC-\d{4}$/;
|
|
|
662
870
|
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
663
871
|
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
664
872
|
var API_TAG_RE = /^API-\d{4}$/;
|
|
665
|
-
var
|
|
873
|
+
var DB_TAG_RE = /^DB-\d{4}$/;
|
|
666
874
|
function parseScenarioDocument(text, uri) {
|
|
667
875
|
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
668
876
|
if (!gherkinDocument) {
|
|
@@ -691,10 +899,10 @@ function buildScenarioAtoms(document) {
|
|
|
691
899
|
return document.scenarios.map((scenario) => {
|
|
692
900
|
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
693
901
|
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
694
|
-
const brIds =
|
|
902
|
+
const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
695
903
|
const contractIds = /* @__PURE__ */ new Set();
|
|
696
904
|
scenario.tags.forEach((tag) => {
|
|
697
|
-
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)) {
|
|
698
906
|
contractIds.add(tag);
|
|
699
907
|
}
|
|
700
908
|
});
|
|
@@ -702,7 +910,7 @@ function buildScenarioAtoms(document) {
|
|
|
702
910
|
for (const text of collectStepTexts(step)) {
|
|
703
911
|
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
704
912
|
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
705
|
-
extractIds(text, "
|
|
913
|
+
extractIds(text, "DB").forEach((id) => contractIds.add(id));
|
|
706
914
|
}
|
|
707
915
|
}
|
|
708
916
|
const atom = {
|
|
@@ -781,7 +989,7 @@ function collectStepTexts(step) {
|
|
|
781
989
|
}
|
|
782
990
|
return texts;
|
|
783
991
|
}
|
|
784
|
-
function
|
|
992
|
+
function unique3(values) {
|
|
785
993
|
return Array.from(new Set(values));
|
|
786
994
|
}
|
|
787
995
|
|
|
@@ -811,7 +1019,7 @@ function extractAnnotatedScIds(text) {
|
|
|
811
1019
|
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
812
1020
|
const scIds = /* @__PURE__ */ new Set();
|
|
813
1021
|
for (const file of scenarioFiles) {
|
|
814
|
-
const text = await (0,
|
|
1022
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
815
1023
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
816
1024
|
if (!document || errors.length > 0) {
|
|
817
1025
|
continue;
|
|
@@ -829,7 +1037,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
|
829
1037
|
async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
830
1038
|
const sources = /* @__PURE__ */ new Map();
|
|
831
1039
|
for (const file of scenarioFiles) {
|
|
832
|
-
const text = await (0,
|
|
1040
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
833
1041
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
834
1042
|
if (!document || errors.length > 0) {
|
|
835
1043
|
continue;
|
|
@@ -882,10 +1090,10 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
882
1090
|
};
|
|
883
1091
|
}
|
|
884
1092
|
const normalizedFiles = Array.from(
|
|
885
|
-
new Set(files.map((file) =>
|
|
1093
|
+
new Set(files.map((file) => import_node_path5.default.normalize(file)))
|
|
886
1094
|
);
|
|
887
1095
|
for (const file of normalizedFiles) {
|
|
888
|
-
const text = await (0,
|
|
1096
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
889
1097
|
const scIds = extractAnnotatedScIds(text);
|
|
890
1098
|
if (scIds.length === 0) {
|
|
891
1099
|
continue;
|
|
@@ -942,16 +1150,16 @@ function formatError3(error) {
|
|
|
942
1150
|
}
|
|
943
1151
|
|
|
944
1152
|
// src/core/version.ts
|
|
945
|
-
var
|
|
946
|
-
var
|
|
1153
|
+
var import_promises7 = require("fs/promises");
|
|
1154
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
947
1155
|
var import_node_url = require("url");
|
|
948
1156
|
async function resolveToolVersion() {
|
|
949
|
-
if ("0.4.
|
|
950
|
-
return "0.4.
|
|
1157
|
+
if ("0.4.6".length > 0) {
|
|
1158
|
+
return "0.4.6";
|
|
951
1159
|
}
|
|
952
1160
|
try {
|
|
953
1161
|
const packagePath = resolvePackageJsonPath();
|
|
954
|
-
const raw = await (0,
|
|
1162
|
+
const raw = await (0, import_promises7.readFile)(packagePath, "utf-8");
|
|
955
1163
|
const parsed = JSON.parse(raw);
|
|
956
1164
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
957
1165
|
return version.length > 0 ? version : "unknown";
|
|
@@ -962,54 +1170,23 @@ async function resolveToolVersion() {
|
|
|
962
1170
|
function resolvePackageJsonPath() {
|
|
963
1171
|
const base = __filename;
|
|
964
1172
|
const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
|
|
965
|
-
return
|
|
1173
|
+
return import_node_path6.default.resolve(import_node_path6.default.dirname(basePath), "../../package.json");
|
|
966
1174
|
}
|
|
967
1175
|
|
|
968
1176
|
// src/core/validators/contracts.ts
|
|
969
|
-
var
|
|
970
|
-
var
|
|
1177
|
+
var import_promises8 = require("fs/promises");
|
|
1178
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
971
1179
|
|
|
972
1180
|
// src/core/contracts.ts
|
|
973
|
-
var
|
|
1181
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
974
1182
|
var import_yaml2 = require("yaml");
|
|
975
1183
|
function parseStructuredContract(file, text) {
|
|
976
|
-
const ext =
|
|
1184
|
+
const ext = import_node_path7.default.extname(file).toLowerCase();
|
|
977
1185
|
if (ext === ".json") {
|
|
978
1186
|
return JSON.parse(text);
|
|
979
1187
|
}
|
|
980
1188
|
return (0, import_yaml2.parse)(text);
|
|
981
1189
|
}
|
|
982
|
-
function extractUiContractIds(doc) {
|
|
983
|
-
const id = typeof doc.id === "string" ? doc.id : "";
|
|
984
|
-
return extractIds(id, "UI");
|
|
985
|
-
}
|
|
986
|
-
function extractApiContractIds(doc) {
|
|
987
|
-
const operationIds = /* @__PURE__ */ new Set();
|
|
988
|
-
collectOperationIds(doc, operationIds);
|
|
989
|
-
const ids = /* @__PURE__ */ new Set();
|
|
990
|
-
for (const operationId of operationIds) {
|
|
991
|
-
extractIds(operationId, "API").forEach((id) => ids.add(id));
|
|
992
|
-
}
|
|
993
|
-
return Array.from(ids);
|
|
994
|
-
}
|
|
995
|
-
function collectOperationIds(value, out) {
|
|
996
|
-
if (!value || typeof value !== "object") {
|
|
997
|
-
return;
|
|
998
|
-
}
|
|
999
|
-
if (Array.isArray(value)) {
|
|
1000
|
-
for (const item of value) {
|
|
1001
|
-
collectOperationIds(item, out);
|
|
1002
|
-
}
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
1006
|
-
if (key === "operationId" && typeof entry === "string") {
|
|
1007
|
-
out.add(entry);
|
|
1008
|
-
continue;
|
|
1009
|
-
}
|
|
1010
|
-
collectOperationIds(entry, out);
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
1190
|
|
|
1014
1191
|
// src/core/validators/contracts.ts
|
|
1015
1192
|
var SQL_DANGEROUS_PATTERNS = [
|
|
@@ -1024,9 +1201,11 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
1024
1201
|
async function validateContracts(root, config) {
|
|
1025
1202
|
const issues = [];
|
|
1026
1203
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1027
|
-
issues.push(...await validateUiContracts(
|
|
1028
|
-
issues.push(...await validateApiContracts(
|
|
1029
|
-
issues.push(...await
|
|
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 validateDbContracts(import_node_path8.default.join(contractsRoot, "db")));
|
|
1207
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
1208
|
+
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1030
1209
|
return issues;
|
|
1031
1210
|
}
|
|
1032
1211
|
async function validateUiContracts(uiRoot) {
|
|
@@ -1044,14 +1223,14 @@ async function validateUiContracts(uiRoot) {
|
|
|
1044
1223
|
}
|
|
1045
1224
|
const issues = [];
|
|
1046
1225
|
for (const file of files) {
|
|
1047
|
-
const text = await (0,
|
|
1226
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
1048
1227
|
const invalidIds = extractInvalidIds(text, [
|
|
1049
1228
|
"SPEC",
|
|
1050
1229
|
"BR",
|
|
1051
1230
|
"SC",
|
|
1052
1231
|
"UI",
|
|
1053
1232
|
"API",
|
|
1054
|
-
"
|
|
1233
|
+
"DB",
|
|
1055
1234
|
"ADR"
|
|
1056
1235
|
]);
|
|
1057
1236
|
if (invalidIds.length > 0) {
|
|
@@ -1066,9 +1245,10 @@ async function validateUiContracts(uiRoot) {
|
|
|
1066
1245
|
)
|
|
1067
1246
|
);
|
|
1068
1247
|
}
|
|
1069
|
-
|
|
1248
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1249
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
|
|
1070
1250
|
try {
|
|
1071
|
-
|
|
1251
|
+
parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
1072
1252
|
} catch (error) {
|
|
1073
1253
|
issues.push(
|
|
1074
1254
|
issue(
|
|
@@ -1079,19 +1259,6 @@ async function validateUiContracts(uiRoot) {
|
|
|
1079
1259
|
"contracts.ui.parse"
|
|
1080
1260
|
)
|
|
1081
1261
|
);
|
|
1082
|
-
continue;
|
|
1083
|
-
}
|
|
1084
|
-
const uiIds = extractUiContractIds(doc);
|
|
1085
|
-
if (uiIds.length === 0) {
|
|
1086
|
-
issues.push(
|
|
1087
|
-
issue(
|
|
1088
|
-
"QFAI-CONTRACT-002",
|
|
1089
|
-
`UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
1090
|
-
"error",
|
|
1091
|
-
file,
|
|
1092
|
-
"contracts.ui.id"
|
|
1093
|
-
)
|
|
1094
|
-
);
|
|
1095
1262
|
}
|
|
1096
1263
|
}
|
|
1097
1264
|
return issues;
|
|
@@ -1111,14 +1278,14 @@ async function validateApiContracts(apiRoot) {
|
|
|
1111
1278
|
}
|
|
1112
1279
|
const issues = [];
|
|
1113
1280
|
for (const file of files) {
|
|
1114
|
-
const text = await (0,
|
|
1281
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
1115
1282
|
const invalidIds = extractInvalidIds(text, [
|
|
1116
1283
|
"SPEC",
|
|
1117
1284
|
"BR",
|
|
1118
1285
|
"SC",
|
|
1119
1286
|
"UI",
|
|
1120
1287
|
"API",
|
|
1121
|
-
"
|
|
1288
|
+
"DB",
|
|
1122
1289
|
"ADR"
|
|
1123
1290
|
]);
|
|
1124
1291
|
if (invalidIds.length > 0) {
|
|
@@ -1133,9 +1300,11 @@ async function validateApiContracts(apiRoot) {
|
|
|
1133
1300
|
)
|
|
1134
1301
|
);
|
|
1135
1302
|
}
|
|
1303
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1304
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "API"));
|
|
1136
1305
|
let doc;
|
|
1137
1306
|
try {
|
|
1138
|
-
doc = parseStructuredContract(file, text);
|
|
1307
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
1139
1308
|
} catch (error) {
|
|
1140
1309
|
issues.push(
|
|
1141
1310
|
issue(
|
|
@@ -1159,44 +1328,32 @@ async function validateApiContracts(apiRoot) {
|
|
|
1159
1328
|
)
|
|
1160
1329
|
);
|
|
1161
1330
|
}
|
|
1162
|
-
const apiIds = extractApiContractIds(doc);
|
|
1163
|
-
if (apiIds.length === 0) {
|
|
1164
|
-
issues.push(
|
|
1165
|
-
issue(
|
|
1166
|
-
"QFAI-CONTRACT-002",
|
|
1167
|
-
`API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
1168
|
-
"error",
|
|
1169
|
-
file,
|
|
1170
|
-
"contracts.api.id"
|
|
1171
|
-
)
|
|
1172
|
-
);
|
|
1173
|
-
}
|
|
1174
1331
|
}
|
|
1175
1332
|
return issues;
|
|
1176
1333
|
}
|
|
1177
|
-
async function
|
|
1178
|
-
const files = await
|
|
1334
|
+
async function validateDbContracts(dbRoot) {
|
|
1335
|
+
const files = await collectDbContractFiles(dbRoot);
|
|
1179
1336
|
if (files.length === 0) {
|
|
1180
1337
|
return [
|
|
1181
1338
|
issue(
|
|
1182
|
-
"QFAI-
|
|
1183
|
-
"
|
|
1339
|
+
"QFAI-DB-000",
|
|
1340
|
+
"DB \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1184
1341
|
"info",
|
|
1185
|
-
|
|
1186
|
-
"contracts.
|
|
1342
|
+
dbRoot,
|
|
1343
|
+
"contracts.db.files"
|
|
1187
1344
|
)
|
|
1188
1345
|
];
|
|
1189
1346
|
}
|
|
1190
1347
|
const issues = [];
|
|
1191
1348
|
for (const file of files) {
|
|
1192
|
-
const text = await (0,
|
|
1349
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
1193
1350
|
const invalidIds = extractInvalidIds(text, [
|
|
1194
1351
|
"SPEC",
|
|
1195
1352
|
"BR",
|
|
1196
1353
|
"SC",
|
|
1197
1354
|
"UI",
|
|
1198
1355
|
"API",
|
|
1199
|
-
"
|
|
1356
|
+
"DB",
|
|
1200
1357
|
"ADR"
|
|
1201
1358
|
]);
|
|
1202
1359
|
if (invalidIds.length > 0) {
|
|
@@ -1211,6 +1368,8 @@ async function validateDataContracts(dataRoot) {
|
|
|
1211
1368
|
)
|
|
1212
1369
|
);
|
|
1213
1370
|
}
|
|
1371
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1372
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "DB"));
|
|
1214
1373
|
issues.push(...lintSql(text, file));
|
|
1215
1374
|
}
|
|
1216
1375
|
return issues;
|
|
@@ -1221,17 +1380,83 @@ function lintSql(text, file) {
|
|
|
1221
1380
|
if (pattern.test(text)) {
|
|
1222
1381
|
issues.push(
|
|
1223
1382
|
issue(
|
|
1224
|
-
"QFAI-
|
|
1383
|
+
"QFAI-DB-001",
|
|
1225
1384
|
`\u5371\u967A\u306A SQL \u64CD\u4F5C\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059: ${label}`,
|
|
1226
1385
|
"warning",
|
|
1227
1386
|
file,
|
|
1228
|
-
"contracts.
|
|
1387
|
+
"contracts.db.sql"
|
|
1229
1388
|
)
|
|
1230
1389
|
);
|
|
1231
1390
|
}
|
|
1232
1391
|
}
|
|
1233
1392
|
return issues;
|
|
1234
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
|
+
}
|
|
1235
1460
|
function hasOpenApi(doc) {
|
|
1236
1461
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
1237
1462
|
}
|
|
@@ -1260,8 +1485,8 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1260
1485
|
}
|
|
1261
1486
|
|
|
1262
1487
|
// src/core/validators/delta.ts
|
|
1263
|
-
var
|
|
1264
|
-
var
|
|
1488
|
+
var import_promises9 = require("fs/promises");
|
|
1489
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
1265
1490
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1266
1491
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1267
1492
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1275,10 +1500,10 @@ async function validateDeltas(root, config) {
|
|
|
1275
1500
|
}
|
|
1276
1501
|
const issues = [];
|
|
1277
1502
|
for (const pack of packs) {
|
|
1278
|
-
const deltaPath =
|
|
1503
|
+
const deltaPath = import_node_path9.default.join(pack, "delta.md");
|
|
1279
1504
|
let text;
|
|
1280
1505
|
try {
|
|
1281
|
-
text = await (0,
|
|
1506
|
+
text = await (0, import_promises9.readFile)(deltaPath, "utf-8");
|
|
1282
1507
|
} catch (error) {
|
|
1283
1508
|
if (isMissingFileError2(error)) {
|
|
1284
1509
|
issues.push(
|
|
@@ -1352,187 +1577,6 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1352
1577
|
// src/core/validators/ids.ts
|
|
1353
1578
|
var import_promises10 = require("fs/promises");
|
|
1354
1579
|
var import_node_path10 = __toESM(require("path"), 1);
|
|
1355
|
-
|
|
1356
|
-
// src/core/contractIndex.ts
|
|
1357
|
-
var import_promises9 = require("fs/promises");
|
|
1358
|
-
var import_node_path9 = __toESM(require("path"), 1);
|
|
1359
|
-
async function buildContractIndex(root, config) {
|
|
1360
|
-
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1361
|
-
const uiRoot = import_node_path9.default.join(contractsRoot, "ui");
|
|
1362
|
-
const apiRoot = import_node_path9.default.join(contractsRoot, "api");
|
|
1363
|
-
const dataRoot = import_node_path9.default.join(contractsRoot, "db");
|
|
1364
|
-
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
1365
|
-
collectUiContractFiles(uiRoot),
|
|
1366
|
-
collectApiContractFiles(apiRoot),
|
|
1367
|
-
collectDataContractFiles(dataRoot)
|
|
1368
|
-
]);
|
|
1369
|
-
const index = {
|
|
1370
|
-
ids: /* @__PURE__ */ new Set(),
|
|
1371
|
-
idToFiles: /* @__PURE__ */ new Map(),
|
|
1372
|
-
files: { ui: uiFiles, api: apiFiles, data: dataFiles },
|
|
1373
|
-
structuredParseFailedFiles: /* @__PURE__ */ new Set()
|
|
1374
|
-
};
|
|
1375
|
-
await indexUiContracts(uiFiles, index);
|
|
1376
|
-
await indexApiContracts(apiFiles, index);
|
|
1377
|
-
await indexDataContracts(dataFiles, index);
|
|
1378
|
-
return index;
|
|
1379
|
-
}
|
|
1380
|
-
async function indexUiContracts(files, index) {
|
|
1381
|
-
for (const file of files) {
|
|
1382
|
-
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1383
|
-
try {
|
|
1384
|
-
const doc = parseStructuredContract(file, text);
|
|
1385
|
-
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
1386
|
-
} catch {
|
|
1387
|
-
index.structuredParseFailedFiles.add(file);
|
|
1388
|
-
extractIds(text, "UI").forEach((id) => record(index, id, file));
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
async function indexApiContracts(files, index) {
|
|
1393
|
-
for (const file of files) {
|
|
1394
|
-
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1395
|
-
try {
|
|
1396
|
-
const doc = parseStructuredContract(file, text);
|
|
1397
|
-
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
1398
|
-
} catch {
|
|
1399
|
-
index.structuredParseFailedFiles.add(file);
|
|
1400
|
-
extractIds(text, "API").forEach((id) => record(index, id, file));
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
async function indexDataContracts(files, index) {
|
|
1405
|
-
for (const file of files) {
|
|
1406
|
-
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1407
|
-
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
function record(index, id, file) {
|
|
1411
|
-
index.ids.add(id);
|
|
1412
|
-
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
1413
|
-
current.add(file);
|
|
1414
|
-
index.idToFiles.set(id, current);
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
// src/core/parse/markdown.ts
|
|
1418
|
-
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1419
|
-
function parseHeadings(md) {
|
|
1420
|
-
const lines = md.split(/\r?\n/);
|
|
1421
|
-
const headings = [];
|
|
1422
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1423
|
-
const line = lines[i] ?? "";
|
|
1424
|
-
const match = line.match(HEADING_RE);
|
|
1425
|
-
if (!match) continue;
|
|
1426
|
-
const levelToken = match[1];
|
|
1427
|
-
const title = match[2];
|
|
1428
|
-
if (!levelToken || !title) continue;
|
|
1429
|
-
headings.push({
|
|
1430
|
-
level: levelToken.length,
|
|
1431
|
-
title: title.trim(),
|
|
1432
|
-
line: i + 1
|
|
1433
|
-
});
|
|
1434
|
-
}
|
|
1435
|
-
return headings;
|
|
1436
|
-
}
|
|
1437
|
-
function extractH2Sections(md) {
|
|
1438
|
-
const lines = md.split(/\r?\n/);
|
|
1439
|
-
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1440
|
-
const sections = /* @__PURE__ */ new Map();
|
|
1441
|
-
for (let i = 0; i < headings.length; i++) {
|
|
1442
|
-
const current = headings[i];
|
|
1443
|
-
if (!current) continue;
|
|
1444
|
-
const next = headings[i + 1];
|
|
1445
|
-
const startLine = current.line + 1;
|
|
1446
|
-
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1447
|
-
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1448
|
-
sections.set(current.title.trim(), {
|
|
1449
|
-
title: current.title.trim(),
|
|
1450
|
-
startLine,
|
|
1451
|
-
endLine,
|
|
1452
|
-
body
|
|
1453
|
-
});
|
|
1454
|
-
}
|
|
1455
|
-
return sections;
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
// src/core/parse/spec.ts
|
|
1459
|
-
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1460
|
-
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1461
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1462
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1463
|
-
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1464
|
-
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1465
|
-
function parseSpec(md, file) {
|
|
1466
|
-
const headings = parseHeadings(md);
|
|
1467
|
-
const h1 = headings.find((heading) => heading.level === 1);
|
|
1468
|
-
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1469
|
-
const sections = extractH2Sections(md);
|
|
1470
|
-
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1471
|
-
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1472
|
-
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1473
|
-
const startLine = brSection?.startLine ?? 1;
|
|
1474
|
-
const brs = [];
|
|
1475
|
-
const brsWithoutPriority = [];
|
|
1476
|
-
const brsWithInvalidPriority = [];
|
|
1477
|
-
for (let i = 0; i < brLines.length; i++) {
|
|
1478
|
-
const lineText = brLines[i] ?? "";
|
|
1479
|
-
const lineNumber = startLine + i;
|
|
1480
|
-
const validMatch = lineText.match(BR_LINE_RE);
|
|
1481
|
-
if (validMatch) {
|
|
1482
|
-
const id = validMatch[1];
|
|
1483
|
-
const priority = validMatch[2];
|
|
1484
|
-
const text = validMatch[3];
|
|
1485
|
-
if (!id || !priority || !text) continue;
|
|
1486
|
-
brs.push({
|
|
1487
|
-
id,
|
|
1488
|
-
priority,
|
|
1489
|
-
text: text.trim(),
|
|
1490
|
-
line: lineNumber
|
|
1491
|
-
});
|
|
1492
|
-
continue;
|
|
1493
|
-
}
|
|
1494
|
-
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1495
|
-
if (anyPriorityMatch) {
|
|
1496
|
-
const id = anyPriorityMatch[1];
|
|
1497
|
-
const priority = anyPriorityMatch[2];
|
|
1498
|
-
const text = anyPriorityMatch[3];
|
|
1499
|
-
if (!id || !priority || !text) continue;
|
|
1500
|
-
if (!VALID_PRIORITIES.has(priority)) {
|
|
1501
|
-
brsWithInvalidPriority.push({
|
|
1502
|
-
id,
|
|
1503
|
-
priority,
|
|
1504
|
-
text: text.trim(),
|
|
1505
|
-
line: lineNumber
|
|
1506
|
-
});
|
|
1507
|
-
}
|
|
1508
|
-
continue;
|
|
1509
|
-
}
|
|
1510
|
-
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1511
|
-
if (noPriorityMatch) {
|
|
1512
|
-
const id = noPriorityMatch[1];
|
|
1513
|
-
const text = noPriorityMatch[2];
|
|
1514
|
-
if (!id || !text) continue;
|
|
1515
|
-
brsWithoutPriority.push({
|
|
1516
|
-
id,
|
|
1517
|
-
text: text.trim(),
|
|
1518
|
-
line: lineNumber
|
|
1519
|
-
});
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
const parsed = {
|
|
1523
|
-
file,
|
|
1524
|
-
sections: sectionNames,
|
|
1525
|
-
brs,
|
|
1526
|
-
brsWithoutPriority,
|
|
1527
|
-
brsWithInvalidPriority
|
|
1528
|
-
};
|
|
1529
|
-
if (specId) {
|
|
1530
|
-
parsed.specId = specId;
|
|
1531
|
-
}
|
|
1532
|
-
return parsed;
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
// src/core/validators/ids.ts
|
|
1536
1580
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1537
1581
|
async function validateDefinedIds(root, config) {
|
|
1538
1582
|
const issues = [];
|
|
@@ -1675,7 +1719,7 @@ function validateScenarioContent(text, file) {
|
|
|
1675
1719
|
"SC",
|
|
1676
1720
|
"UI",
|
|
1677
1721
|
"API",
|
|
1678
|
-
"
|
|
1722
|
+
"DB",
|
|
1679
1723
|
"ADR"
|
|
1680
1724
|
]);
|
|
1681
1725
|
if (invalidIds.length > 0) {
|
|
@@ -1873,7 +1917,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1873
1917
|
"SC",
|
|
1874
1918
|
"UI",
|
|
1875
1919
|
"API",
|
|
1876
|
-
"
|
|
1920
|
+
"DB",
|
|
1877
1921
|
"ADR"
|
|
1878
1922
|
]);
|
|
1879
1923
|
if (invalidIds.length > 0) {
|
|
@@ -2002,8 +2046,7 @@ async function validateTraceability(root, config) {
|
|
|
2002
2046
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
2003
2047
|
const brIdsInScenarios = /* @__PURE__ */ new Set();
|
|
2004
2048
|
const scIdsInScenarios = /* @__PURE__ */ new Set();
|
|
2005
|
-
const
|
|
2006
|
-
const scWithContracts = /* @__PURE__ */ new Set();
|
|
2049
|
+
const specContractIds = /* @__PURE__ */ new Set();
|
|
2007
2050
|
const specToBrIds = /* @__PURE__ */ new Map();
|
|
2008
2051
|
const contractIndex = await buildContractIndex(root, config);
|
|
2009
2052
|
const contractIds = contractIndex.ids;
|
|
@@ -2021,6 +2064,64 @@ async function validateTraceability(root, config) {
|
|
|
2021
2064
|
brIds.forEach((id) => current.add(id));
|
|
2022
2065
|
specToBrIds.set(parsed.specId, current);
|
|
2023
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(
|
|
2109
|
+
(id) => !contractIds.has(id)
|
|
2110
|
+
);
|
|
2111
|
+
if (unknownContractIds.length > 0) {
|
|
2112
|
+
issues.push(
|
|
2113
|
+
issue6(
|
|
2114
|
+
"QFAI-TRACE-021",
|
|
2115
|
+
`Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
2116
|
+
", "
|
|
2117
|
+
)}`,
|
|
2118
|
+
"error",
|
|
2119
|
+
file,
|
|
2120
|
+
"traceability.specContractExists",
|
|
2121
|
+
unknownContractIds
|
|
2122
|
+
)
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2024
2125
|
}
|
|
2025
2126
|
for (const file of scenarioFiles) {
|
|
2026
2127
|
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
@@ -2066,10 +2167,6 @@ async function validateTraceability(root, config) {
|
|
|
2066
2167
|
scIdsInScenarios.add(id);
|
|
2067
2168
|
scIdsInFile.add(id);
|
|
2068
2169
|
});
|
|
2069
|
-
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
2070
|
-
if (atom.contractIds.length > 0) {
|
|
2071
|
-
scTags.forEach((id) => scWithContracts.add(id));
|
|
2072
|
-
}
|
|
2073
2170
|
const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
|
|
2074
2171
|
if (unknownSpecIds.length > 0) {
|
|
2075
2172
|
issues.push(
|
|
@@ -2188,25 +2285,6 @@ async function validateTraceability(root, config) {
|
|
|
2188
2285
|
);
|
|
2189
2286
|
}
|
|
2190
2287
|
}
|
|
2191
|
-
if (config.validation.traceability.scMustTouchContracts && scIdsInScenarios.size > 0) {
|
|
2192
|
-
const scWithoutContracts = Array.from(scIdsInScenarios).filter(
|
|
2193
|
-
(id) => !scWithContracts.has(id)
|
|
2194
|
-
);
|
|
2195
|
-
if (scWithoutContracts.length > 0) {
|
|
2196
|
-
issues.push(
|
|
2197
|
-
issue6(
|
|
2198
|
-
"QFAI_TRACE_SC_NO_CONTRACT",
|
|
2199
|
-
`SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
|
|
2200
|
-
", "
|
|
2201
|
-
)}`,
|
|
2202
|
-
"error",
|
|
2203
|
-
specsRoot,
|
|
2204
|
-
"traceability.scMustTouchContracts",
|
|
2205
|
-
scWithoutContracts
|
|
2206
|
-
)
|
|
2207
|
-
);
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
2288
|
const scRefsResult = await collectScTestReferences(
|
|
2211
2289
|
root,
|
|
2212
2290
|
config.validation.traceability.testFileGlobs,
|
|
@@ -2270,16 +2348,16 @@ async function validateTraceability(root, config) {
|
|
|
2270
2348
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
2271
2349
|
if (contractIds.size > 0) {
|
|
2272
2350
|
const orphanContracts = Array.from(contractIds).filter(
|
|
2273
|
-
(id) => !
|
|
2351
|
+
(id) => !specContractIds.has(id)
|
|
2274
2352
|
);
|
|
2275
2353
|
if (orphanContracts.length > 0) {
|
|
2276
2354
|
issues.push(
|
|
2277
2355
|
issue6(
|
|
2278
|
-
"
|
|
2279
|
-
`\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(", ")}`,
|
|
2280
2358
|
"error",
|
|
2281
2359
|
specsRoot,
|
|
2282
|
-
"traceability.
|
|
2360
|
+
"traceability.contractCoverage",
|
|
2283
2361
|
orphanContracts
|
|
2284
2362
|
)
|
|
2285
2363
|
);
|
|
@@ -2400,7 +2478,7 @@ function countIssues(issues) {
|
|
|
2400
2478
|
}
|
|
2401
2479
|
|
|
2402
2480
|
// src/core/report.ts
|
|
2403
|
-
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "
|
|
2481
|
+
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2404
2482
|
async function createReportData(root, validation, configResult) {
|
|
2405
2483
|
const resolved = configResult ?? await loadConfig(root);
|
|
2406
2484
|
const config = resolved.config;
|
|
@@ -2419,6 +2497,23 @@ async function createReportData(root, validation, configResult) {
|
|
|
2419
2497
|
ui: uiFiles,
|
|
2420
2498
|
db: dbFiles
|
|
2421
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
|
+
);
|
|
2422
2517
|
const idsByPrefix = await collectIds([
|
|
2423
2518
|
...specFiles,
|
|
2424
2519
|
...scenarioFiles,
|
|
@@ -2469,14 +2564,24 @@ async function createReportData(root, validation, configResult) {
|
|
|
2469
2564
|
sc: idsByPrefix.SC,
|
|
2470
2565
|
ui: idsByPrefix.UI,
|
|
2471
2566
|
api: idsByPrefix.API,
|
|
2472
|
-
|
|
2567
|
+
db: idsByPrefix.DB
|
|
2473
2568
|
},
|
|
2474
2569
|
traceability: {
|
|
2475
2570
|
upstreamIdsFound: upstreamIds.size,
|
|
2476
2571
|
referencedInCodeOrTests: traceability,
|
|
2477
2572
|
sc: scCoverage,
|
|
2478
2573
|
scSources: scSourceRecord,
|
|
2479
|
-
testFiles
|
|
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
|
+
}
|
|
2480
2585
|
},
|
|
2481
2586
|
issues: resolvedValidation.issues
|
|
2482
2587
|
};
|
|
@@ -2505,7 +2610,7 @@ function formatReportMarkdown(data) {
|
|
|
2505
2610
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
2506
2611
|
lines.push(formatIdLine("UI", data.ids.ui));
|
|
2507
2612
|
lines.push(formatIdLine("API", data.ids.api));
|
|
2508
|
-
lines.push(formatIdLine("
|
|
2613
|
+
lines.push(formatIdLine("DB", data.ids.db));
|
|
2509
2614
|
lines.push("");
|
|
2510
2615
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
2511
2616
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
@@ -2513,6 +2618,50 @@ function formatReportMarkdown(data) {
|
|
|
2513
2618
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2514
2619
|
);
|
|
2515
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("");
|
|
2516
2665
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2517
2666
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2518
2667
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
@@ -2586,7 +2735,7 @@ function formatReportMarkdown(data) {
|
|
|
2586
2735
|
lines.push("");
|
|
2587
2736
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
2588
2737
|
const traceIssues = data.issues.filter(
|
|
2589
|
-
(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-")
|
|
2590
2739
|
);
|
|
2591
2740
|
if (traceIssues.length === 0) {
|
|
2592
2741
|
lines.push("- (none)");
|
|
@@ -2616,6 +2765,33 @@ function formatReportMarkdown(data) {
|
|
|
2616
2765
|
function formatReportJson(data) {
|
|
2617
2766
|
return JSON.stringify(data, null, 2);
|
|
2618
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
|
+
}
|
|
2619
2795
|
async function collectIds(files) {
|
|
2620
2796
|
const result = {
|
|
2621
2797
|
SPEC: /* @__PURE__ */ new Set(),
|
|
@@ -2623,7 +2799,7 @@ async function collectIds(files) {
|
|
|
2623
2799
|
SC: /* @__PURE__ */ new Set(),
|
|
2624
2800
|
UI: /* @__PURE__ */ new Set(),
|
|
2625
2801
|
API: /* @__PURE__ */ new Set(),
|
|
2626
|
-
|
|
2802
|
+
DB: /* @__PURE__ */ new Set()
|
|
2627
2803
|
};
|
|
2628
2804
|
for (const file of files) {
|
|
2629
2805
|
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
@@ -2638,7 +2814,7 @@ async function collectIds(files) {
|
|
|
2638
2814
|
SC: toSortedArray2(result.SC),
|
|
2639
2815
|
UI: toSortedArray2(result.UI),
|
|
2640
2816
|
API: toSortedArray2(result.API),
|
|
2641
|
-
|
|
2817
|
+
DB: toSortedArray2(result.DB)
|
|
2642
2818
|
};
|
|
2643
2819
|
}
|
|
2644
2820
|
async function collectUpstreamIds(files) {
|