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/cli/index.cjs
CHANGED
|
@@ -209,7 +209,6 @@ var defaultConfig = {
|
|
|
209
209
|
},
|
|
210
210
|
traceability: {
|
|
211
211
|
brMustHaveSc: true,
|
|
212
|
-
scMustTouchContracts: true,
|
|
213
212
|
scMustHaveTest: true,
|
|
214
213
|
testFileGlobs: [],
|
|
215
214
|
testFileExcludeGlobs: [],
|
|
@@ -386,13 +385,6 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
386
385
|
configPath,
|
|
387
386
|
issues
|
|
388
387
|
),
|
|
389
|
-
scMustTouchContracts: readBoolean(
|
|
390
|
-
traceabilityRaw?.scMustTouchContracts,
|
|
391
|
-
base.traceability.scMustTouchContracts,
|
|
392
|
-
"validation.traceability.scMustTouchContracts",
|
|
393
|
-
configPath,
|
|
394
|
-
issues
|
|
395
|
-
),
|
|
396
388
|
scMustHaveTest: readBoolean(
|
|
397
389
|
traceabilityRaw?.scMustHaveTest,
|
|
398
390
|
base.traceability.scMustHaveTest,
|
|
@@ -549,6 +541,10 @@ function isRecord(value) {
|
|
|
549
541
|
var import_promises15 = require("fs/promises");
|
|
550
542
|
var import_node_path14 = __toESM(require("path"), 1);
|
|
551
543
|
|
|
544
|
+
// src/core/contractIndex.ts
|
|
545
|
+
var import_promises6 = require("fs/promises");
|
|
546
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
547
|
+
|
|
552
548
|
// src/core/discovery.ts
|
|
553
549
|
var import_promises5 = require("fs/promises");
|
|
554
550
|
|
|
@@ -671,14 +667,14 @@ async function collectUiContractFiles(uiRoot) {
|
|
|
671
667
|
async function collectApiContractFiles(apiRoot) {
|
|
672
668
|
return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
|
|
673
669
|
}
|
|
674
|
-
async function
|
|
675
|
-
return collectFiles(
|
|
670
|
+
async function collectDbContractFiles(dbRoot) {
|
|
671
|
+
return collectFiles(dbRoot, { extensions: [".sql"] });
|
|
676
672
|
}
|
|
677
|
-
async function collectContractFiles(uiRoot, apiRoot,
|
|
673
|
+
async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
|
|
678
674
|
const [ui, api, db] = await Promise.all([
|
|
679
675
|
collectUiContractFiles(uiRoot),
|
|
680
676
|
collectApiContractFiles(apiRoot),
|
|
681
|
-
|
|
677
|
+
collectDbContractFiles(dbRoot)
|
|
682
678
|
]);
|
|
683
679
|
return { ui, api, db };
|
|
684
680
|
}
|
|
@@ -700,15 +696,66 @@ async function exists3(target) {
|
|
|
700
696
|
}
|
|
701
697
|
}
|
|
702
698
|
|
|
699
|
+
// src/core/contractsDecl.ts
|
|
700
|
+
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
|
|
701
|
+
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
|
|
702
|
+
function extractDeclaredContractIds(text) {
|
|
703
|
+
const ids = [];
|
|
704
|
+
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
705
|
+
const id = match[1];
|
|
706
|
+
if (id) {
|
|
707
|
+
ids.push(id);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return ids;
|
|
711
|
+
}
|
|
712
|
+
function stripContractDeclarationLines(text) {
|
|
713
|
+
return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/core/contractIndex.ts
|
|
717
|
+
async function buildContractIndex(root, config) {
|
|
718
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
719
|
+
const uiRoot = import_node_path7.default.join(contractsRoot, "ui");
|
|
720
|
+
const apiRoot = import_node_path7.default.join(contractsRoot, "api");
|
|
721
|
+
const dbRoot = import_node_path7.default.join(contractsRoot, "db");
|
|
722
|
+
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
723
|
+
collectUiContractFiles(uiRoot),
|
|
724
|
+
collectApiContractFiles(apiRoot),
|
|
725
|
+
collectDbContractFiles(dbRoot)
|
|
726
|
+
]);
|
|
727
|
+
const index = {
|
|
728
|
+
ids: /* @__PURE__ */ new Set(),
|
|
729
|
+
idToFiles: /* @__PURE__ */ new Map(),
|
|
730
|
+
files: { ui: uiFiles, api: apiFiles, db: dbFiles }
|
|
731
|
+
};
|
|
732
|
+
await indexContractFiles(uiFiles, index);
|
|
733
|
+
await indexContractFiles(apiFiles, index);
|
|
734
|
+
await indexContractFiles(dbFiles, index);
|
|
735
|
+
return index;
|
|
736
|
+
}
|
|
737
|
+
async function indexContractFiles(files, index) {
|
|
738
|
+
for (const file of files) {
|
|
739
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
740
|
+
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function record(index, id, file) {
|
|
744
|
+
index.ids.add(id);
|
|
745
|
+
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
746
|
+
current.add(file);
|
|
747
|
+
index.idToFiles.set(id, current);
|
|
748
|
+
}
|
|
749
|
+
|
|
703
750
|
// src/core/ids.ts
|
|
704
|
-
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "
|
|
751
|
+
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
705
752
|
var STRICT_ID_PATTERNS = {
|
|
706
753
|
SPEC: /\bSPEC-\d{4}\b/g,
|
|
707
754
|
BR: /\bBR-\d{4}\b/g,
|
|
708
755
|
SC: /\bSC-\d{4}\b/g,
|
|
709
756
|
UI: /\bUI-\d{4}\b/g,
|
|
710
757
|
API: /\bAPI-\d{4}\b/g,
|
|
711
|
-
|
|
758
|
+
DB: /\bDB-\d{4}\b/g,
|
|
712
759
|
ADR: /\bADR-\d{4}\b/g
|
|
713
760
|
};
|
|
714
761
|
var LOOSE_ID_PATTERNS = {
|
|
@@ -717,7 +764,7 @@ var LOOSE_ID_PATTERNS = {
|
|
|
717
764
|
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
718
765
|
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
719
766
|
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
720
|
-
|
|
767
|
+
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
721
768
|
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
722
769
|
};
|
|
723
770
|
function extractIds(text, prefix) {
|
|
@@ -753,9 +800,170 @@ function isValidId(value, prefix) {
|
|
|
753
800
|
return strict.test(value);
|
|
754
801
|
}
|
|
755
802
|
|
|
803
|
+
// src/core/parse/markdown.ts
|
|
804
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
805
|
+
function parseHeadings(md) {
|
|
806
|
+
const lines = md.split(/\r?\n/);
|
|
807
|
+
const headings = [];
|
|
808
|
+
for (let i = 0; i < lines.length; i++) {
|
|
809
|
+
const line = lines[i] ?? "";
|
|
810
|
+
const match = line.match(HEADING_RE);
|
|
811
|
+
if (!match) continue;
|
|
812
|
+
const levelToken = match[1];
|
|
813
|
+
const title = match[2];
|
|
814
|
+
if (!levelToken || !title) continue;
|
|
815
|
+
headings.push({
|
|
816
|
+
level: levelToken.length,
|
|
817
|
+
title: title.trim(),
|
|
818
|
+
line: i + 1
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
return headings;
|
|
822
|
+
}
|
|
823
|
+
function extractH2Sections(md) {
|
|
824
|
+
const lines = md.split(/\r?\n/);
|
|
825
|
+
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
826
|
+
const sections = /* @__PURE__ */ new Map();
|
|
827
|
+
for (let i = 0; i < headings.length; i++) {
|
|
828
|
+
const current = headings[i];
|
|
829
|
+
if (!current) continue;
|
|
830
|
+
const next = headings[i + 1];
|
|
831
|
+
const startLine = current.line + 1;
|
|
832
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
833
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
834
|
+
sections.set(current.title.trim(), {
|
|
835
|
+
title: current.title.trim(),
|
|
836
|
+
startLine,
|
|
837
|
+
endLine,
|
|
838
|
+
body
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
return sections;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// src/core/parse/spec.ts
|
|
845
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
846
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
847
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
848
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
849
|
+
var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
|
|
850
|
+
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
851
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
852
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
853
|
+
function parseSpec(md, file) {
|
|
854
|
+
const headings = parseHeadings(md);
|
|
855
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
856
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
857
|
+
const sections = extractH2Sections(md);
|
|
858
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
859
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
860
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
861
|
+
const startLine = brSection?.startLine ?? 1;
|
|
862
|
+
const brs = [];
|
|
863
|
+
const brsWithoutPriority = [];
|
|
864
|
+
const brsWithInvalidPriority = [];
|
|
865
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
866
|
+
const lineText = brLines[i] ?? "";
|
|
867
|
+
const lineNumber = startLine + i;
|
|
868
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
869
|
+
if (validMatch) {
|
|
870
|
+
const id = validMatch[1];
|
|
871
|
+
const priority = validMatch[2];
|
|
872
|
+
const text = validMatch[3];
|
|
873
|
+
if (!id || !priority || !text) continue;
|
|
874
|
+
brs.push({
|
|
875
|
+
id,
|
|
876
|
+
priority,
|
|
877
|
+
text: text.trim(),
|
|
878
|
+
line: lineNumber
|
|
879
|
+
});
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
883
|
+
if (anyPriorityMatch) {
|
|
884
|
+
const id = anyPriorityMatch[1];
|
|
885
|
+
const priority = anyPriorityMatch[2];
|
|
886
|
+
const text = anyPriorityMatch[3];
|
|
887
|
+
if (!id || !priority || !text) continue;
|
|
888
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
889
|
+
brsWithInvalidPriority.push({
|
|
890
|
+
id,
|
|
891
|
+
priority,
|
|
892
|
+
text: text.trim(),
|
|
893
|
+
line: lineNumber
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
899
|
+
if (noPriorityMatch) {
|
|
900
|
+
const id = noPriorityMatch[1];
|
|
901
|
+
const text = noPriorityMatch[2];
|
|
902
|
+
if (!id || !text) continue;
|
|
903
|
+
brsWithoutPriority.push({
|
|
904
|
+
id,
|
|
905
|
+
text: text.trim(),
|
|
906
|
+
line: lineNumber
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
const parsed = {
|
|
911
|
+
file,
|
|
912
|
+
sections: sectionNames,
|
|
913
|
+
brs,
|
|
914
|
+
brsWithoutPriority,
|
|
915
|
+
brsWithInvalidPriority,
|
|
916
|
+
contractRefs: parseContractRefs(md)
|
|
917
|
+
};
|
|
918
|
+
if (specId) {
|
|
919
|
+
parsed.specId = specId;
|
|
920
|
+
}
|
|
921
|
+
return parsed;
|
|
922
|
+
}
|
|
923
|
+
function parseContractRefs(md) {
|
|
924
|
+
const lines = [];
|
|
925
|
+
for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
|
|
926
|
+
lines.push((match[1] ?? "").trim());
|
|
927
|
+
}
|
|
928
|
+
const ids = [];
|
|
929
|
+
const invalidTokens = [];
|
|
930
|
+
let hasNone = false;
|
|
931
|
+
for (const line of lines) {
|
|
932
|
+
if (line.length === 0) {
|
|
933
|
+
invalidTokens.push("(empty)");
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
const tokens = line.split(",").map((token) => token.trim());
|
|
937
|
+
for (const token of tokens) {
|
|
938
|
+
if (token.length === 0) {
|
|
939
|
+
invalidTokens.push("(empty)");
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
if (token === "none") {
|
|
943
|
+
hasNone = true;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
947
|
+
ids.push(token);
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
invalidTokens.push(token);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return {
|
|
954
|
+
lines,
|
|
955
|
+
ids: unique2(ids),
|
|
956
|
+
invalidTokens: unique2(invalidTokens),
|
|
957
|
+
hasNone
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function unique2(values) {
|
|
961
|
+
return Array.from(new Set(values));
|
|
962
|
+
}
|
|
963
|
+
|
|
756
964
|
// src/core/traceability.ts
|
|
757
|
-
var
|
|
758
|
-
var
|
|
965
|
+
var import_promises7 = require("fs/promises");
|
|
966
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
759
967
|
|
|
760
968
|
// src/core/gherkin/parse.ts
|
|
761
969
|
var import_gherkin = require("@cucumber/gherkin");
|
|
@@ -788,7 +996,7 @@ var SC_TAG_RE = /^SC-\d{4}$/;
|
|
|
788
996
|
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
789
997
|
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
790
998
|
var API_TAG_RE = /^API-\d{4}$/;
|
|
791
|
-
var
|
|
999
|
+
var DB_TAG_RE = /^DB-\d{4}$/;
|
|
792
1000
|
function parseScenarioDocument(text, uri) {
|
|
793
1001
|
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
794
1002
|
if (!gherkinDocument) {
|
|
@@ -817,10 +1025,10 @@ function buildScenarioAtoms(document) {
|
|
|
817
1025
|
return document.scenarios.map((scenario) => {
|
|
818
1026
|
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
819
1027
|
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
820
|
-
const brIds =
|
|
1028
|
+
const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
821
1029
|
const contractIds = /* @__PURE__ */ new Set();
|
|
822
1030
|
scenario.tags.forEach((tag) => {
|
|
823
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) ||
|
|
1031
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
|
|
824
1032
|
contractIds.add(tag);
|
|
825
1033
|
}
|
|
826
1034
|
});
|
|
@@ -828,7 +1036,7 @@ function buildScenarioAtoms(document) {
|
|
|
828
1036
|
for (const text of collectStepTexts(step)) {
|
|
829
1037
|
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
830
1038
|
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
831
|
-
extractIds(text, "
|
|
1039
|
+
extractIds(text, "DB").forEach((id) => contractIds.add(id));
|
|
832
1040
|
}
|
|
833
1041
|
}
|
|
834
1042
|
const atom = {
|
|
@@ -907,7 +1115,7 @@ function collectStepTexts(step) {
|
|
|
907
1115
|
}
|
|
908
1116
|
return texts;
|
|
909
1117
|
}
|
|
910
|
-
function
|
|
1118
|
+
function unique3(values) {
|
|
911
1119
|
return Array.from(new Set(values));
|
|
912
1120
|
}
|
|
913
1121
|
|
|
@@ -937,7 +1145,7 @@ function extractAnnotatedScIds(text) {
|
|
|
937
1145
|
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
938
1146
|
const scIds = /* @__PURE__ */ new Set();
|
|
939
1147
|
for (const file of scenarioFiles) {
|
|
940
|
-
const text = await (0,
|
|
1148
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
941
1149
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
942
1150
|
if (!document || errors.length > 0) {
|
|
943
1151
|
continue;
|
|
@@ -955,7 +1163,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
|
955
1163
|
async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
956
1164
|
const sources = /* @__PURE__ */ new Map();
|
|
957
1165
|
for (const file of scenarioFiles) {
|
|
958
|
-
const text = await (0,
|
|
1166
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
959
1167
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
960
1168
|
if (!document || errors.length > 0) {
|
|
961
1169
|
continue;
|
|
@@ -1008,10 +1216,10 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
1008
1216
|
};
|
|
1009
1217
|
}
|
|
1010
1218
|
const normalizedFiles = Array.from(
|
|
1011
|
-
new Set(files.map((file) =>
|
|
1219
|
+
new Set(files.map((file) => import_node_path8.default.normalize(file)))
|
|
1012
1220
|
);
|
|
1013
1221
|
for (const file of normalizedFiles) {
|
|
1014
|
-
const text = await (0,
|
|
1222
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
1015
1223
|
const scIds = extractAnnotatedScIds(text);
|
|
1016
1224
|
if (scIds.length === 0) {
|
|
1017
1225
|
continue;
|
|
@@ -1068,16 +1276,16 @@ function formatError3(error2) {
|
|
|
1068
1276
|
}
|
|
1069
1277
|
|
|
1070
1278
|
// src/core/version.ts
|
|
1071
|
-
var
|
|
1072
|
-
var
|
|
1279
|
+
var import_promises8 = require("fs/promises");
|
|
1280
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
1073
1281
|
var import_node_url2 = require("url");
|
|
1074
1282
|
async function resolveToolVersion() {
|
|
1075
|
-
if ("0.4.
|
|
1076
|
-
return "0.4.
|
|
1283
|
+
if ("0.4.6".length > 0) {
|
|
1284
|
+
return "0.4.6";
|
|
1077
1285
|
}
|
|
1078
1286
|
try {
|
|
1079
1287
|
const packagePath = resolvePackageJsonPath();
|
|
1080
|
-
const raw = await (0,
|
|
1288
|
+
const raw = await (0, import_promises8.readFile)(packagePath, "utf-8");
|
|
1081
1289
|
const parsed = JSON.parse(raw);
|
|
1082
1290
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
1083
1291
|
return version.length > 0 ? version : "unknown";
|
|
@@ -1088,54 +1296,23 @@ async function resolveToolVersion() {
|
|
|
1088
1296
|
function resolvePackageJsonPath() {
|
|
1089
1297
|
const base = __filename;
|
|
1090
1298
|
const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
|
|
1091
|
-
return
|
|
1299
|
+
return import_node_path9.default.resolve(import_node_path9.default.dirname(basePath), "../../package.json");
|
|
1092
1300
|
}
|
|
1093
1301
|
|
|
1094
1302
|
// src/core/validators/contracts.ts
|
|
1095
|
-
var
|
|
1096
|
-
var
|
|
1303
|
+
var import_promises9 = require("fs/promises");
|
|
1304
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
1097
1305
|
|
|
1098
1306
|
// src/core/contracts.ts
|
|
1099
|
-
var
|
|
1307
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
1100
1308
|
var import_yaml2 = require("yaml");
|
|
1101
1309
|
function parseStructuredContract(file, text) {
|
|
1102
|
-
const ext =
|
|
1310
|
+
const ext = import_node_path10.default.extname(file).toLowerCase();
|
|
1103
1311
|
if (ext === ".json") {
|
|
1104
1312
|
return JSON.parse(text);
|
|
1105
1313
|
}
|
|
1106
1314
|
return (0, import_yaml2.parse)(text);
|
|
1107
1315
|
}
|
|
1108
|
-
function extractUiContractIds(doc) {
|
|
1109
|
-
const id = typeof doc.id === "string" ? doc.id : "";
|
|
1110
|
-
return extractIds(id, "UI");
|
|
1111
|
-
}
|
|
1112
|
-
function extractApiContractIds(doc) {
|
|
1113
|
-
const operationIds = /* @__PURE__ */ new Set();
|
|
1114
|
-
collectOperationIds(doc, operationIds);
|
|
1115
|
-
const ids = /* @__PURE__ */ new Set();
|
|
1116
|
-
for (const operationId of operationIds) {
|
|
1117
|
-
extractIds(operationId, "API").forEach((id) => ids.add(id));
|
|
1118
|
-
}
|
|
1119
|
-
return Array.from(ids);
|
|
1120
|
-
}
|
|
1121
|
-
function collectOperationIds(value, out) {
|
|
1122
|
-
if (!value || typeof value !== "object") {
|
|
1123
|
-
return;
|
|
1124
|
-
}
|
|
1125
|
-
if (Array.isArray(value)) {
|
|
1126
|
-
for (const item of value) {
|
|
1127
|
-
collectOperationIds(item, out);
|
|
1128
|
-
}
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
1132
|
-
if (key === "operationId" && typeof entry === "string") {
|
|
1133
|
-
out.add(entry);
|
|
1134
|
-
continue;
|
|
1135
|
-
}
|
|
1136
|
-
collectOperationIds(entry, out);
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
1316
|
|
|
1140
1317
|
// src/core/validators/contracts.ts
|
|
1141
1318
|
var SQL_DANGEROUS_PATTERNS = [
|
|
@@ -1150,9 +1327,11 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
1150
1327
|
async function validateContracts(root, config) {
|
|
1151
1328
|
const issues = [];
|
|
1152
1329
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1153
|
-
issues.push(...await validateUiContracts(
|
|
1154
|
-
issues.push(...await validateApiContracts(
|
|
1155
|
-
issues.push(...await
|
|
1330
|
+
issues.push(...await validateUiContracts(import_node_path11.default.join(contractsRoot, "ui")));
|
|
1331
|
+
issues.push(...await validateApiContracts(import_node_path11.default.join(contractsRoot, "api")));
|
|
1332
|
+
issues.push(...await validateDbContracts(import_node_path11.default.join(contractsRoot, "db")));
|
|
1333
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
1334
|
+
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1156
1335
|
return issues;
|
|
1157
1336
|
}
|
|
1158
1337
|
async function validateUiContracts(uiRoot) {
|
|
@@ -1170,14 +1349,14 @@ async function validateUiContracts(uiRoot) {
|
|
|
1170
1349
|
}
|
|
1171
1350
|
const issues = [];
|
|
1172
1351
|
for (const file of files) {
|
|
1173
|
-
const text = await (0,
|
|
1352
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1174
1353
|
const invalidIds = extractInvalidIds(text, [
|
|
1175
1354
|
"SPEC",
|
|
1176
1355
|
"BR",
|
|
1177
1356
|
"SC",
|
|
1178
1357
|
"UI",
|
|
1179
1358
|
"API",
|
|
1180
|
-
"
|
|
1359
|
+
"DB",
|
|
1181
1360
|
"ADR"
|
|
1182
1361
|
]);
|
|
1183
1362
|
if (invalidIds.length > 0) {
|
|
@@ -1192,9 +1371,10 @@ async function validateUiContracts(uiRoot) {
|
|
|
1192
1371
|
)
|
|
1193
1372
|
);
|
|
1194
1373
|
}
|
|
1195
|
-
|
|
1374
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1375
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
|
|
1196
1376
|
try {
|
|
1197
|
-
|
|
1377
|
+
parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
1198
1378
|
} catch (error2) {
|
|
1199
1379
|
issues.push(
|
|
1200
1380
|
issue(
|
|
@@ -1205,19 +1385,6 @@ async function validateUiContracts(uiRoot) {
|
|
|
1205
1385
|
"contracts.ui.parse"
|
|
1206
1386
|
)
|
|
1207
1387
|
);
|
|
1208
|
-
continue;
|
|
1209
|
-
}
|
|
1210
|
-
const uiIds = extractUiContractIds(doc);
|
|
1211
|
-
if (uiIds.length === 0) {
|
|
1212
|
-
issues.push(
|
|
1213
|
-
issue(
|
|
1214
|
-
"QFAI-CONTRACT-002",
|
|
1215
|
-
`UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
1216
|
-
"error",
|
|
1217
|
-
file,
|
|
1218
|
-
"contracts.ui.id"
|
|
1219
|
-
)
|
|
1220
|
-
);
|
|
1221
1388
|
}
|
|
1222
1389
|
}
|
|
1223
1390
|
return issues;
|
|
@@ -1237,14 +1404,14 @@ async function validateApiContracts(apiRoot) {
|
|
|
1237
1404
|
}
|
|
1238
1405
|
const issues = [];
|
|
1239
1406
|
for (const file of files) {
|
|
1240
|
-
const text = await (0,
|
|
1407
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1241
1408
|
const invalidIds = extractInvalidIds(text, [
|
|
1242
1409
|
"SPEC",
|
|
1243
1410
|
"BR",
|
|
1244
1411
|
"SC",
|
|
1245
1412
|
"UI",
|
|
1246
1413
|
"API",
|
|
1247
|
-
"
|
|
1414
|
+
"DB",
|
|
1248
1415
|
"ADR"
|
|
1249
1416
|
]);
|
|
1250
1417
|
if (invalidIds.length > 0) {
|
|
@@ -1259,9 +1426,11 @@ async function validateApiContracts(apiRoot) {
|
|
|
1259
1426
|
)
|
|
1260
1427
|
);
|
|
1261
1428
|
}
|
|
1429
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1430
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "API"));
|
|
1262
1431
|
let doc;
|
|
1263
1432
|
try {
|
|
1264
|
-
doc = parseStructuredContract(file, text);
|
|
1433
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
1265
1434
|
} catch (error2) {
|
|
1266
1435
|
issues.push(
|
|
1267
1436
|
issue(
|
|
@@ -1285,44 +1454,32 @@ async function validateApiContracts(apiRoot) {
|
|
|
1285
1454
|
)
|
|
1286
1455
|
);
|
|
1287
1456
|
}
|
|
1288
|
-
const apiIds = extractApiContractIds(doc);
|
|
1289
|
-
if (apiIds.length === 0) {
|
|
1290
|
-
issues.push(
|
|
1291
|
-
issue(
|
|
1292
|
-
"QFAI-CONTRACT-002",
|
|
1293
|
-
`API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
1294
|
-
"error",
|
|
1295
|
-
file,
|
|
1296
|
-
"contracts.api.id"
|
|
1297
|
-
)
|
|
1298
|
-
);
|
|
1299
|
-
}
|
|
1300
1457
|
}
|
|
1301
1458
|
return issues;
|
|
1302
1459
|
}
|
|
1303
|
-
async function
|
|
1304
|
-
const files = await
|
|
1460
|
+
async function validateDbContracts(dbRoot) {
|
|
1461
|
+
const files = await collectDbContractFiles(dbRoot);
|
|
1305
1462
|
if (files.length === 0) {
|
|
1306
1463
|
return [
|
|
1307
1464
|
issue(
|
|
1308
|
-
"QFAI-
|
|
1309
|
-
"
|
|
1465
|
+
"QFAI-DB-000",
|
|
1466
|
+
"DB \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1310
1467
|
"info",
|
|
1311
|
-
|
|
1312
|
-
"contracts.
|
|
1468
|
+
dbRoot,
|
|
1469
|
+
"contracts.db.files"
|
|
1313
1470
|
)
|
|
1314
1471
|
];
|
|
1315
1472
|
}
|
|
1316
1473
|
const issues = [];
|
|
1317
1474
|
for (const file of files) {
|
|
1318
|
-
const text = await (0,
|
|
1475
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1319
1476
|
const invalidIds = extractInvalidIds(text, [
|
|
1320
1477
|
"SPEC",
|
|
1321
1478
|
"BR",
|
|
1322
1479
|
"SC",
|
|
1323
1480
|
"UI",
|
|
1324
1481
|
"API",
|
|
1325
|
-
"
|
|
1482
|
+
"DB",
|
|
1326
1483
|
"ADR"
|
|
1327
1484
|
]);
|
|
1328
1485
|
if (invalidIds.length > 0) {
|
|
@@ -1337,6 +1494,8 @@ async function validateDataContracts(dataRoot) {
|
|
|
1337
1494
|
)
|
|
1338
1495
|
);
|
|
1339
1496
|
}
|
|
1497
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1498
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "DB"));
|
|
1340
1499
|
issues.push(...lintSql(text, file));
|
|
1341
1500
|
}
|
|
1342
1501
|
return issues;
|
|
@@ -1347,17 +1506,83 @@ function lintSql(text, file) {
|
|
|
1347
1506
|
if (pattern.test(text)) {
|
|
1348
1507
|
issues.push(
|
|
1349
1508
|
issue(
|
|
1350
|
-
"QFAI-
|
|
1509
|
+
"QFAI-DB-001",
|
|
1351
1510
|
`\u5371\u967A\u306A SQL \u64CD\u4F5C\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059: ${label}`,
|
|
1352
1511
|
"warning",
|
|
1353
1512
|
file,
|
|
1354
|
-
"contracts.
|
|
1513
|
+
"contracts.db.sql"
|
|
1355
1514
|
)
|
|
1356
1515
|
);
|
|
1357
1516
|
}
|
|
1358
1517
|
}
|
|
1359
1518
|
return issues;
|
|
1360
1519
|
}
|
|
1520
|
+
function validateDeclaredContractIds(ids, file, kind) {
|
|
1521
|
+
const issues = [];
|
|
1522
|
+
if (ids.length === 0) {
|
|
1523
|
+
issues.push(
|
|
1524
|
+
issue(
|
|
1525
|
+
"QFAI-CONTRACT-010",
|
|
1526
|
+
`\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
|
|
1527
|
+
"error",
|
|
1528
|
+
file,
|
|
1529
|
+
"contracts.declaration"
|
|
1530
|
+
)
|
|
1531
|
+
);
|
|
1532
|
+
return issues;
|
|
1533
|
+
}
|
|
1534
|
+
if (ids.length > 1) {
|
|
1535
|
+
issues.push(
|
|
1536
|
+
issue(
|
|
1537
|
+
"QFAI-CONTRACT-011",
|
|
1538
|
+
`\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B\u8907\u6570\u306E QFAI-CONTRACT-ID \u304C\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${ids.join(
|
|
1539
|
+
", "
|
|
1540
|
+
)}`,
|
|
1541
|
+
"error",
|
|
1542
|
+
file,
|
|
1543
|
+
"contracts.declaration",
|
|
1544
|
+
ids
|
|
1545
|
+
)
|
|
1546
|
+
);
|
|
1547
|
+
return issues;
|
|
1548
|
+
}
|
|
1549
|
+
const [id] = ids;
|
|
1550
|
+
if (id && !id.startsWith(`${kind}-`)) {
|
|
1551
|
+
issues.push(
|
|
1552
|
+
issue(
|
|
1553
|
+
"QFAI-CONTRACT-013",
|
|
1554
|
+
`\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E QFAI-CONTRACT-ID \u304C ${kind}- \u3067\u306F\u3042\u308A\u307E\u305B\u3093: ${id}`,
|
|
1555
|
+
"error",
|
|
1556
|
+
file,
|
|
1557
|
+
"contracts.declarationPrefix",
|
|
1558
|
+
[id]
|
|
1559
|
+
)
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
return issues;
|
|
1563
|
+
}
|
|
1564
|
+
function validateDuplicateContractIds(contractIndex) {
|
|
1565
|
+
const issues = [];
|
|
1566
|
+
for (const [id, files] of contractIndex.idToFiles.entries()) {
|
|
1567
|
+
if (files.size <= 1) {
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
const sortedFiles = Array.from(files).sort((a, b) => a.localeCompare(b));
|
|
1571
|
+
issues.push(
|
|
1572
|
+
issue(
|
|
1573
|
+
"QFAI-CONTRACT-012",
|
|
1574
|
+
`\u5951\u7D04 ID \u304C\u8907\u6570\u30D5\u30A1\u30A4\u30EB\u3067\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${id} (${sortedFiles.join(
|
|
1575
|
+
", "
|
|
1576
|
+
)})`,
|
|
1577
|
+
"error",
|
|
1578
|
+
sortedFiles[0],
|
|
1579
|
+
"contracts.idDuplicate",
|
|
1580
|
+
[id]
|
|
1581
|
+
)
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
return issues;
|
|
1585
|
+
}
|
|
1361
1586
|
function hasOpenApi(doc) {
|
|
1362
1587
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
1363
1588
|
}
|
|
@@ -1386,8 +1611,8 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1386
1611
|
}
|
|
1387
1612
|
|
|
1388
1613
|
// src/core/validators/delta.ts
|
|
1389
|
-
var
|
|
1390
|
-
var
|
|
1614
|
+
var import_promises10 = require("fs/promises");
|
|
1615
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
1391
1616
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1392
1617
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1393
1618
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1401,10 +1626,10 @@ async function validateDeltas(root, config) {
|
|
|
1401
1626
|
}
|
|
1402
1627
|
const issues = [];
|
|
1403
1628
|
for (const pack of packs) {
|
|
1404
|
-
const deltaPath =
|
|
1629
|
+
const deltaPath = import_node_path12.default.join(pack, "delta.md");
|
|
1405
1630
|
let text;
|
|
1406
1631
|
try {
|
|
1407
|
-
text = await (0,
|
|
1632
|
+
text = await (0, import_promises10.readFile)(deltaPath, "utf-8");
|
|
1408
1633
|
} catch (error2) {
|
|
1409
1634
|
if (isMissingFileError2(error2)) {
|
|
1410
1635
|
issues.push(
|
|
@@ -1478,187 +1703,6 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1478
1703
|
// src/core/validators/ids.ts
|
|
1479
1704
|
var import_promises11 = require("fs/promises");
|
|
1480
1705
|
var import_node_path13 = __toESM(require("path"), 1);
|
|
1481
|
-
|
|
1482
|
-
// src/core/contractIndex.ts
|
|
1483
|
-
var import_promises10 = require("fs/promises");
|
|
1484
|
-
var import_node_path12 = __toESM(require("path"), 1);
|
|
1485
|
-
async function buildContractIndex(root, config) {
|
|
1486
|
-
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1487
|
-
const uiRoot = import_node_path12.default.join(contractsRoot, "ui");
|
|
1488
|
-
const apiRoot = import_node_path12.default.join(contractsRoot, "api");
|
|
1489
|
-
const dataRoot = import_node_path12.default.join(contractsRoot, "db");
|
|
1490
|
-
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
1491
|
-
collectUiContractFiles(uiRoot),
|
|
1492
|
-
collectApiContractFiles(apiRoot),
|
|
1493
|
-
collectDataContractFiles(dataRoot)
|
|
1494
|
-
]);
|
|
1495
|
-
const index = {
|
|
1496
|
-
ids: /* @__PURE__ */ new Set(),
|
|
1497
|
-
idToFiles: /* @__PURE__ */ new Map(),
|
|
1498
|
-
files: { ui: uiFiles, api: apiFiles, data: dataFiles },
|
|
1499
|
-
structuredParseFailedFiles: /* @__PURE__ */ new Set()
|
|
1500
|
-
};
|
|
1501
|
-
await indexUiContracts(uiFiles, index);
|
|
1502
|
-
await indexApiContracts(apiFiles, index);
|
|
1503
|
-
await indexDataContracts(dataFiles, index);
|
|
1504
|
-
return index;
|
|
1505
|
-
}
|
|
1506
|
-
async function indexUiContracts(files, index) {
|
|
1507
|
-
for (const file of files) {
|
|
1508
|
-
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1509
|
-
try {
|
|
1510
|
-
const doc = parseStructuredContract(file, text);
|
|
1511
|
-
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
1512
|
-
} catch {
|
|
1513
|
-
index.structuredParseFailedFiles.add(file);
|
|
1514
|
-
extractIds(text, "UI").forEach((id) => record(index, id, file));
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
async function indexApiContracts(files, index) {
|
|
1519
|
-
for (const file of files) {
|
|
1520
|
-
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1521
|
-
try {
|
|
1522
|
-
const doc = parseStructuredContract(file, text);
|
|
1523
|
-
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
1524
|
-
} catch {
|
|
1525
|
-
index.structuredParseFailedFiles.add(file);
|
|
1526
|
-
extractIds(text, "API").forEach((id) => record(index, id, file));
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
async function indexDataContracts(files, index) {
|
|
1531
|
-
for (const file of files) {
|
|
1532
|
-
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1533
|
-
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
function record(index, id, file) {
|
|
1537
|
-
index.ids.add(id);
|
|
1538
|
-
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
1539
|
-
current.add(file);
|
|
1540
|
-
index.idToFiles.set(id, current);
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
// src/core/parse/markdown.ts
|
|
1544
|
-
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1545
|
-
function parseHeadings(md) {
|
|
1546
|
-
const lines = md.split(/\r?\n/);
|
|
1547
|
-
const headings = [];
|
|
1548
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1549
|
-
const line = lines[i] ?? "";
|
|
1550
|
-
const match = line.match(HEADING_RE);
|
|
1551
|
-
if (!match) continue;
|
|
1552
|
-
const levelToken = match[1];
|
|
1553
|
-
const title = match[2];
|
|
1554
|
-
if (!levelToken || !title) continue;
|
|
1555
|
-
headings.push({
|
|
1556
|
-
level: levelToken.length,
|
|
1557
|
-
title: title.trim(),
|
|
1558
|
-
line: i + 1
|
|
1559
|
-
});
|
|
1560
|
-
}
|
|
1561
|
-
return headings;
|
|
1562
|
-
}
|
|
1563
|
-
function extractH2Sections(md) {
|
|
1564
|
-
const lines = md.split(/\r?\n/);
|
|
1565
|
-
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1566
|
-
const sections = /* @__PURE__ */ new Map();
|
|
1567
|
-
for (let i = 0; i < headings.length; i++) {
|
|
1568
|
-
const current = headings[i];
|
|
1569
|
-
if (!current) continue;
|
|
1570
|
-
const next = headings[i + 1];
|
|
1571
|
-
const startLine = current.line + 1;
|
|
1572
|
-
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1573
|
-
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1574
|
-
sections.set(current.title.trim(), {
|
|
1575
|
-
title: current.title.trim(),
|
|
1576
|
-
startLine,
|
|
1577
|
-
endLine,
|
|
1578
|
-
body
|
|
1579
|
-
});
|
|
1580
|
-
}
|
|
1581
|
-
return sections;
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
// src/core/parse/spec.ts
|
|
1585
|
-
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1586
|
-
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1587
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1588
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1589
|
-
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1590
|
-
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1591
|
-
function parseSpec(md, file) {
|
|
1592
|
-
const headings = parseHeadings(md);
|
|
1593
|
-
const h1 = headings.find((heading) => heading.level === 1);
|
|
1594
|
-
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1595
|
-
const sections = extractH2Sections(md);
|
|
1596
|
-
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1597
|
-
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1598
|
-
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1599
|
-
const startLine = brSection?.startLine ?? 1;
|
|
1600
|
-
const brs = [];
|
|
1601
|
-
const brsWithoutPriority = [];
|
|
1602
|
-
const brsWithInvalidPriority = [];
|
|
1603
|
-
for (let i = 0; i < brLines.length; i++) {
|
|
1604
|
-
const lineText = brLines[i] ?? "";
|
|
1605
|
-
const lineNumber = startLine + i;
|
|
1606
|
-
const validMatch = lineText.match(BR_LINE_RE);
|
|
1607
|
-
if (validMatch) {
|
|
1608
|
-
const id = validMatch[1];
|
|
1609
|
-
const priority = validMatch[2];
|
|
1610
|
-
const text = validMatch[3];
|
|
1611
|
-
if (!id || !priority || !text) continue;
|
|
1612
|
-
brs.push({
|
|
1613
|
-
id,
|
|
1614
|
-
priority,
|
|
1615
|
-
text: text.trim(),
|
|
1616
|
-
line: lineNumber
|
|
1617
|
-
});
|
|
1618
|
-
continue;
|
|
1619
|
-
}
|
|
1620
|
-
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1621
|
-
if (anyPriorityMatch) {
|
|
1622
|
-
const id = anyPriorityMatch[1];
|
|
1623
|
-
const priority = anyPriorityMatch[2];
|
|
1624
|
-
const text = anyPriorityMatch[3];
|
|
1625
|
-
if (!id || !priority || !text) continue;
|
|
1626
|
-
if (!VALID_PRIORITIES.has(priority)) {
|
|
1627
|
-
brsWithInvalidPriority.push({
|
|
1628
|
-
id,
|
|
1629
|
-
priority,
|
|
1630
|
-
text: text.trim(),
|
|
1631
|
-
line: lineNumber
|
|
1632
|
-
});
|
|
1633
|
-
}
|
|
1634
|
-
continue;
|
|
1635
|
-
}
|
|
1636
|
-
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1637
|
-
if (noPriorityMatch) {
|
|
1638
|
-
const id = noPriorityMatch[1];
|
|
1639
|
-
const text = noPriorityMatch[2];
|
|
1640
|
-
if (!id || !text) continue;
|
|
1641
|
-
brsWithoutPriority.push({
|
|
1642
|
-
id,
|
|
1643
|
-
text: text.trim(),
|
|
1644
|
-
line: lineNumber
|
|
1645
|
-
});
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
const parsed = {
|
|
1649
|
-
file,
|
|
1650
|
-
sections: sectionNames,
|
|
1651
|
-
brs,
|
|
1652
|
-
brsWithoutPriority,
|
|
1653
|
-
brsWithInvalidPriority
|
|
1654
|
-
};
|
|
1655
|
-
if (specId) {
|
|
1656
|
-
parsed.specId = specId;
|
|
1657
|
-
}
|
|
1658
|
-
return parsed;
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
// src/core/validators/ids.ts
|
|
1662
1706
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1663
1707
|
async function validateDefinedIds(root, config) {
|
|
1664
1708
|
const issues = [];
|
|
@@ -1801,7 +1845,7 @@ function validateScenarioContent(text, file) {
|
|
|
1801
1845
|
"SC",
|
|
1802
1846
|
"UI",
|
|
1803
1847
|
"API",
|
|
1804
|
-
"
|
|
1848
|
+
"DB",
|
|
1805
1849
|
"ADR"
|
|
1806
1850
|
]);
|
|
1807
1851
|
if (invalidIds.length > 0) {
|
|
@@ -1999,7 +2043,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1999
2043
|
"SC",
|
|
2000
2044
|
"UI",
|
|
2001
2045
|
"API",
|
|
2002
|
-
"
|
|
2046
|
+
"DB",
|
|
2003
2047
|
"ADR"
|
|
2004
2048
|
]);
|
|
2005
2049
|
if (invalidIds.length > 0) {
|
|
@@ -2128,8 +2172,7 @@ async function validateTraceability(root, config) {
|
|
|
2128
2172
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
2129
2173
|
const brIdsInScenarios = /* @__PURE__ */ new Set();
|
|
2130
2174
|
const scIdsInScenarios = /* @__PURE__ */ new Set();
|
|
2131
|
-
const
|
|
2132
|
-
const scWithContracts = /* @__PURE__ */ new Set();
|
|
2175
|
+
const specContractIds = /* @__PURE__ */ new Set();
|
|
2133
2176
|
const specToBrIds = /* @__PURE__ */ new Map();
|
|
2134
2177
|
const contractIndex = await buildContractIndex(root, config);
|
|
2135
2178
|
const contractIds = contractIndex.ids;
|
|
@@ -2147,6 +2190,64 @@ async function validateTraceability(root, config) {
|
|
|
2147
2190
|
brIds.forEach((id) => current.add(id));
|
|
2148
2191
|
specToBrIds.set(parsed.specId, current);
|
|
2149
2192
|
}
|
|
2193
|
+
const contractRefs = parsed.contractRefs;
|
|
2194
|
+
if (contractRefs.lines.length === 0) {
|
|
2195
|
+
issues.push(
|
|
2196
|
+
issue6(
|
|
2197
|
+
"QFAI-TRACE-020",
|
|
2198
|
+
"Spec \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
2199
|
+
"error",
|
|
2200
|
+
file,
|
|
2201
|
+
"traceability.specContractRefRequired"
|
|
2202
|
+
)
|
|
2203
|
+
);
|
|
2204
|
+
} else {
|
|
2205
|
+
if (contractRefs.hasNone && contractRefs.ids.length > 0) {
|
|
2206
|
+
issues.push(
|
|
2207
|
+
issue6(
|
|
2208
|
+
"QFAI-TRACE-021",
|
|
2209
|
+
"Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2210
|
+
"error",
|
|
2211
|
+
file,
|
|
2212
|
+
"traceability.specContractRefFormat"
|
|
2213
|
+
)
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
if (contractRefs.invalidTokens.length > 0) {
|
|
2217
|
+
issues.push(
|
|
2218
|
+
issue6(
|
|
2219
|
+
"QFAI-TRACE-021",
|
|
2220
|
+
`Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
|
|
2221
|
+
", "
|
|
2222
|
+
)}`,
|
|
2223
|
+
"error",
|
|
2224
|
+
file,
|
|
2225
|
+
"traceability.specContractRefFormat",
|
|
2226
|
+
contractRefs.invalidTokens
|
|
2227
|
+
)
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
contractRefs.ids.forEach((id) => {
|
|
2232
|
+
specContractIds.add(id);
|
|
2233
|
+
});
|
|
2234
|
+
const unknownContractIds = contractRefs.ids.filter(
|
|
2235
|
+
(id) => !contractIds.has(id)
|
|
2236
|
+
);
|
|
2237
|
+
if (unknownContractIds.length > 0) {
|
|
2238
|
+
issues.push(
|
|
2239
|
+
issue6(
|
|
2240
|
+
"QFAI-TRACE-021",
|
|
2241
|
+
`Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
2242
|
+
", "
|
|
2243
|
+
)}`,
|
|
2244
|
+
"error",
|
|
2245
|
+
file,
|
|
2246
|
+
"traceability.specContractExists",
|
|
2247
|
+
unknownContractIds
|
|
2248
|
+
)
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2150
2251
|
}
|
|
2151
2252
|
for (const file of scenarioFiles) {
|
|
2152
2253
|
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
@@ -2192,10 +2293,6 @@ async function validateTraceability(root, config) {
|
|
|
2192
2293
|
scIdsInScenarios.add(id);
|
|
2193
2294
|
scIdsInFile.add(id);
|
|
2194
2295
|
});
|
|
2195
|
-
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
2196
|
-
if (atom.contractIds.length > 0) {
|
|
2197
|
-
scTags.forEach((id) => scWithContracts.add(id));
|
|
2198
|
-
}
|
|
2199
2296
|
const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
|
|
2200
2297
|
if (unknownSpecIds.length > 0) {
|
|
2201
2298
|
issues.push(
|
|
@@ -2314,25 +2411,6 @@ async function validateTraceability(root, config) {
|
|
|
2314
2411
|
);
|
|
2315
2412
|
}
|
|
2316
2413
|
}
|
|
2317
|
-
if (config.validation.traceability.scMustTouchContracts && scIdsInScenarios.size > 0) {
|
|
2318
|
-
const scWithoutContracts = Array.from(scIdsInScenarios).filter(
|
|
2319
|
-
(id) => !scWithContracts.has(id)
|
|
2320
|
-
);
|
|
2321
|
-
if (scWithoutContracts.length > 0) {
|
|
2322
|
-
issues.push(
|
|
2323
|
-
issue6(
|
|
2324
|
-
"QFAI_TRACE_SC_NO_CONTRACT",
|
|
2325
|
-
`SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
|
|
2326
|
-
", "
|
|
2327
|
-
)}`,
|
|
2328
|
-
"error",
|
|
2329
|
-
specsRoot,
|
|
2330
|
-
"traceability.scMustTouchContracts",
|
|
2331
|
-
scWithoutContracts
|
|
2332
|
-
)
|
|
2333
|
-
);
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
2414
|
const scRefsResult = await collectScTestReferences(
|
|
2337
2415
|
root,
|
|
2338
2416
|
config.validation.traceability.testFileGlobs,
|
|
@@ -2396,16 +2474,16 @@ async function validateTraceability(root, config) {
|
|
|
2396
2474
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
2397
2475
|
if (contractIds.size > 0) {
|
|
2398
2476
|
const orphanContracts = Array.from(contractIds).filter(
|
|
2399
|
-
(id) => !
|
|
2477
|
+
(id) => !specContractIds.has(id)
|
|
2400
2478
|
);
|
|
2401
2479
|
if (orphanContracts.length > 0) {
|
|
2402
2480
|
issues.push(
|
|
2403
2481
|
issue6(
|
|
2404
|
-
"
|
|
2405
|
-
`\u5951\u7D04\u304C
|
|
2482
|
+
"QFAI-TRACE-022",
|
|
2483
|
+
`\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
2406
2484
|
"error",
|
|
2407
2485
|
specsRoot,
|
|
2408
|
-
"traceability.
|
|
2486
|
+
"traceability.contractCoverage",
|
|
2409
2487
|
orphanContracts
|
|
2410
2488
|
)
|
|
2411
2489
|
);
|
|
@@ -2526,7 +2604,7 @@ function countIssues(issues) {
|
|
|
2526
2604
|
}
|
|
2527
2605
|
|
|
2528
2606
|
// src/core/report.ts
|
|
2529
|
-
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "
|
|
2607
|
+
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2530
2608
|
async function createReportData(root, validation, configResult) {
|
|
2531
2609
|
const resolved = configResult ?? await loadConfig(root);
|
|
2532
2610
|
const config = resolved.config;
|
|
@@ -2545,6 +2623,23 @@ async function createReportData(root, validation, configResult) {
|
|
|
2545
2623
|
ui: uiFiles,
|
|
2546
2624
|
db: dbFiles
|
|
2547
2625
|
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
2626
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
2627
|
+
const specContractRefs = await collectSpecContractRefs(specFiles);
|
|
2628
|
+
const contractIdList = Array.from(contractIndex.ids);
|
|
2629
|
+
const referencedContracts = /* @__PURE__ */ new Set();
|
|
2630
|
+
for (const ids of specContractRefs.specToContractIds.values()) {
|
|
2631
|
+
ids.forEach((id) => referencedContracts.add(id));
|
|
2632
|
+
}
|
|
2633
|
+
const referencedContractCount = contractIdList.filter(
|
|
2634
|
+
(id) => referencedContracts.has(id)
|
|
2635
|
+
).length;
|
|
2636
|
+
const orphanContractCount = contractIdList.filter(
|
|
2637
|
+
(id) => !referencedContracts.has(id)
|
|
2638
|
+
).length;
|
|
2639
|
+
const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
|
|
2640
|
+
const specToContractIdsRecord = mapToSortedRecord(
|
|
2641
|
+
specContractRefs.specToContractIds
|
|
2642
|
+
);
|
|
2548
2643
|
const idsByPrefix = await collectIds([
|
|
2549
2644
|
...specFiles,
|
|
2550
2645
|
...scenarioFiles,
|
|
@@ -2595,14 +2690,24 @@ async function createReportData(root, validation, configResult) {
|
|
|
2595
2690
|
sc: idsByPrefix.SC,
|
|
2596
2691
|
ui: idsByPrefix.UI,
|
|
2597
2692
|
api: idsByPrefix.API,
|
|
2598
|
-
|
|
2693
|
+
db: idsByPrefix.DB
|
|
2599
2694
|
},
|
|
2600
2695
|
traceability: {
|
|
2601
2696
|
upstreamIdsFound: upstreamIds.size,
|
|
2602
2697
|
referencedInCodeOrTests: traceability,
|
|
2603
2698
|
sc: scCoverage,
|
|
2604
2699
|
scSources: scSourceRecord,
|
|
2605
|
-
testFiles
|
|
2700
|
+
testFiles,
|
|
2701
|
+
contracts: {
|
|
2702
|
+
total: contractIdList.length,
|
|
2703
|
+
referenced: referencedContractCount,
|
|
2704
|
+
orphan: orphanContractCount,
|
|
2705
|
+
idToSpecs: contractIdToSpecsRecord
|
|
2706
|
+
},
|
|
2707
|
+
specs: {
|
|
2708
|
+
contractRefMissing: specContractRefs.missingRefSpecs.size,
|
|
2709
|
+
specToContractIds: specToContractIdsRecord
|
|
2710
|
+
}
|
|
2606
2711
|
},
|
|
2607
2712
|
issues: resolvedValidation.issues
|
|
2608
2713
|
};
|
|
@@ -2631,7 +2736,7 @@ function formatReportMarkdown(data) {
|
|
|
2631
2736
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
2632
2737
|
lines.push(formatIdLine("UI", data.ids.ui));
|
|
2633
2738
|
lines.push(formatIdLine("API", data.ids.api));
|
|
2634
|
-
lines.push(formatIdLine("
|
|
2739
|
+
lines.push(formatIdLine("DB", data.ids.db));
|
|
2635
2740
|
lines.push("");
|
|
2636
2741
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
2637
2742
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
@@ -2639,6 +2744,50 @@ function formatReportMarkdown(data) {
|
|
|
2639
2744
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2640
2745
|
);
|
|
2641
2746
|
lines.push("");
|
|
2747
|
+
lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2748
|
+
lines.push(`- total: ${data.traceability.contracts.total}`);
|
|
2749
|
+
lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
|
|
2750
|
+
lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
|
|
2751
|
+
lines.push(
|
|
2752
|
+
`- specContractRefMissing: ${data.traceability.specs.contractRefMissing}`
|
|
2753
|
+
);
|
|
2754
|
+
lines.push("");
|
|
2755
|
+
lines.push("## \u5951\u7D04\u2192Spec");
|
|
2756
|
+
const contractToSpecs = data.traceability.contracts.idToSpecs;
|
|
2757
|
+
const contractIds = Object.keys(contractToSpecs).sort(
|
|
2758
|
+
(a, b) => a.localeCompare(b)
|
|
2759
|
+
);
|
|
2760
|
+
if (contractIds.length === 0) {
|
|
2761
|
+
lines.push("- (none)");
|
|
2762
|
+
} else {
|
|
2763
|
+
for (const contractId of contractIds) {
|
|
2764
|
+
const specs = contractToSpecs[contractId] ?? [];
|
|
2765
|
+
if (specs.length === 0) {
|
|
2766
|
+
lines.push(`- ${contractId}: (none)`);
|
|
2767
|
+
} else {
|
|
2768
|
+
lines.push(`- ${contractId}: ${specs.join(", ")}`);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
lines.push("");
|
|
2773
|
+
lines.push("## Spec\u2192\u5951\u7D04");
|
|
2774
|
+
const specToContracts = data.traceability.specs.specToContractIds;
|
|
2775
|
+
const specIds = Object.keys(specToContracts).sort(
|
|
2776
|
+
(a, b) => a.localeCompare(b)
|
|
2777
|
+
);
|
|
2778
|
+
if (specIds.length === 0) {
|
|
2779
|
+
lines.push("- (none)");
|
|
2780
|
+
} else {
|
|
2781
|
+
for (const specId of specIds) {
|
|
2782
|
+
const contractIds2 = specToContracts[specId] ?? [];
|
|
2783
|
+
if (contractIds2.length === 0) {
|
|
2784
|
+
lines.push(`- ${specId}: (none)`);
|
|
2785
|
+
} else {
|
|
2786
|
+
lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
lines.push("");
|
|
2642
2791
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2643
2792
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2644
2793
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
@@ -2712,7 +2861,7 @@ function formatReportMarkdown(data) {
|
|
|
2712
2861
|
lines.push("");
|
|
2713
2862
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
2714
2863
|
const traceIssues = data.issues.filter(
|
|
2715
|
-
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2864
|
+
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2716
2865
|
);
|
|
2717
2866
|
if (traceIssues.length === 0) {
|
|
2718
2867
|
lines.push("- (none)");
|
|
@@ -2742,6 +2891,33 @@ function formatReportMarkdown(data) {
|
|
|
2742
2891
|
function formatReportJson(data) {
|
|
2743
2892
|
return JSON.stringify(data, null, 2);
|
|
2744
2893
|
}
|
|
2894
|
+
async function collectSpecContractRefs(specFiles) {
|
|
2895
|
+
const specToContractIds = /* @__PURE__ */ new Map();
|
|
2896
|
+
const idToSpecs = /* @__PURE__ */ new Map();
|
|
2897
|
+
const missingRefSpecs = /* @__PURE__ */ new Set();
|
|
2898
|
+
for (const file of specFiles) {
|
|
2899
|
+
const text = await (0, import_promises15.readFile)(file, "utf-8");
|
|
2900
|
+
const parsed = parseSpec(text, file);
|
|
2901
|
+
const specKey = parsed.specId ?? file;
|
|
2902
|
+
const refs = parsed.contractRefs;
|
|
2903
|
+
if (refs.lines.length === 0) {
|
|
2904
|
+
missingRefSpecs.add(specKey);
|
|
2905
|
+
}
|
|
2906
|
+
const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
|
|
2907
|
+
for (const id of refs.ids) {
|
|
2908
|
+
currentContracts.add(id);
|
|
2909
|
+
const specs = idToSpecs.get(id) ?? /* @__PURE__ */ new Set();
|
|
2910
|
+
specs.add(specKey);
|
|
2911
|
+
idToSpecs.set(id, specs);
|
|
2912
|
+
}
|
|
2913
|
+
specToContractIds.set(specKey, currentContracts);
|
|
2914
|
+
}
|
|
2915
|
+
return {
|
|
2916
|
+
specToContractIds,
|
|
2917
|
+
idToSpecs,
|
|
2918
|
+
missingRefSpecs
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2745
2921
|
async function collectIds(files) {
|
|
2746
2922
|
const result = {
|
|
2747
2923
|
SPEC: /* @__PURE__ */ new Set(),
|
|
@@ -2749,7 +2925,7 @@ async function collectIds(files) {
|
|
|
2749
2925
|
SC: /* @__PURE__ */ new Set(),
|
|
2750
2926
|
UI: /* @__PURE__ */ new Set(),
|
|
2751
2927
|
API: /* @__PURE__ */ new Set(),
|
|
2752
|
-
|
|
2928
|
+
DB: /* @__PURE__ */ new Set()
|
|
2753
2929
|
};
|
|
2754
2930
|
for (const file of files) {
|
|
2755
2931
|
const text = await (0, import_promises15.readFile)(file, "utf-8");
|
|
@@ -2764,7 +2940,7 @@ async function collectIds(files) {
|
|
|
2764
2940
|
SC: toSortedArray2(result.SC),
|
|
2765
2941
|
UI: toSortedArray2(result.UI),
|
|
2766
2942
|
API: toSortedArray2(result.API),
|
|
2767
|
-
|
|
2943
|
+
DB: toSortedArray2(result.DB)
|
|
2768
2944
|
};
|
|
2769
2945
|
}
|
|
2770
2946
|
async function collectUpstreamIds(files) {
|
|
@@ -2910,7 +3086,31 @@ function isValidationResult(value) {
|
|
|
2910
3086
|
if (!counts) {
|
|
2911
3087
|
return false;
|
|
2912
3088
|
}
|
|
2913
|
-
|
|
3089
|
+
if (typeof counts.info !== "number" || typeof counts.warning !== "number" || typeof counts.error !== "number") {
|
|
3090
|
+
return false;
|
|
3091
|
+
}
|
|
3092
|
+
const traceability = record2.traceability;
|
|
3093
|
+
if (!traceability || typeof traceability !== "object") {
|
|
3094
|
+
return false;
|
|
3095
|
+
}
|
|
3096
|
+
const sc = traceability.sc;
|
|
3097
|
+
const testFiles = traceability.testFiles;
|
|
3098
|
+
if (!sc || !testFiles) {
|
|
3099
|
+
return false;
|
|
3100
|
+
}
|
|
3101
|
+
if (typeof sc.total !== "number" || typeof sc.covered !== "number" || typeof sc.missing !== "number") {
|
|
3102
|
+
return false;
|
|
3103
|
+
}
|
|
3104
|
+
if (!Array.isArray(sc.missingIds)) {
|
|
3105
|
+
return false;
|
|
3106
|
+
}
|
|
3107
|
+
if (!sc.refs || typeof sc.refs !== "object") {
|
|
3108
|
+
return false;
|
|
3109
|
+
}
|
|
3110
|
+
if (!Array.isArray(testFiles.globs) || !Array.isArray(testFiles.excludeGlobs) || typeof testFiles.matchedFileCount !== "number") {
|
|
3111
|
+
return false;
|
|
3112
|
+
}
|
|
3113
|
+
return true;
|
|
2914
3114
|
}
|
|
2915
3115
|
function isMissingFileError5(error2) {
|
|
2916
3116
|
if (!error2 || typeof error2 !== "object") {
|