qfai 0.4.0 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/assets/init/.qfai/README.md +3 -1
- package/assets/init/.qfai/contracts/README.md +17 -8
- package/assets/init/.qfai/contracts/api/api-0001-sample.yaml +3 -2
- package/assets/init/.qfai/contracts/db/db-0001-sample.sql +2 -1
- package/assets/init/.qfai/contracts/ui/ui-0001-sample.yaml +3 -1
- package/assets/init/.qfai/promptpack/modes/change.md +3 -2
- package/assets/init/.qfai/promptpack/modes/compatibility.md +2 -0
- package/assets/init/.qfai/prompts/README.md +1 -0
- package/assets/init/.qfai/prompts/qfai-generate-test-globs.md +29 -0
- package/assets/init/.qfai/prompts/require-to-spec.md +4 -2
- package/assets/init/.qfai/specs/README.md +9 -2
- package/assets/init/.qfai/specs/spec-0001/spec.md +2 -0
- package/assets/init/root/qfai.config.yaml +6 -1
- package/assets/init/root/tests/qfai-traceability.sample.test.ts +2 -2
- package/dist/cli/index.cjs +885 -489
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +885 -489
- package/dist/cli/index.mjs.map +1 -1
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +4 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/contractIndex.d.ts +1 -2
- package/dist/core/contractIndex.d.ts.map +1 -1
- package/dist/core/contractIndex.js +10 -38
- package/dist/core/contractIndex.js.map +1 -1
- package/dist/core/contractsDecl.d.ts +3 -0
- package/dist/core/contractsDecl.d.ts.map +1 -0
- package/dist/core/contractsDecl.js +19 -0
- package/dist/core/contractsDecl.js.map +1 -0
- package/dist/core/fs.d.ts +5 -0
- package/dist/core/fs.d.ts.map +1 -1
- package/dist/core/fs.js +13 -0
- package/dist/core/fs.js.map +1 -1
- package/dist/core/ids.d.ts +1 -1
- package/dist/core/ids.d.ts.map +1 -1
- package/dist/core/ids.js +3 -3
- package/dist/core/ids.js.map +1 -1
- package/dist/core/parse/spec.d.ts +8 -0
- package/dist/core/parse/spec.d.ts.map +1 -1
- package/dist/core/parse/spec.js +43 -0
- package/dist/core/parse/spec.js.map +1 -1
- package/dist/core/report.d.ts +16 -2
- package/dist/core/report.d.ts.map +1 -1
- package/dist/core/report.js +144 -11
- package/dist/core/report.js.map +1 -1
- package/dist/core/scenarioModel.d.ts.map +1 -1
- package/dist/core/scenarioModel.js +3 -5
- package/dist/core/scenarioModel.js.map +1 -1
- package/dist/core/traceability.d.ts +15 -1
- package/dist/core/traceability.d.ts.map +1 -1
- package/dist/core/traceability.js +96 -9
- package/dist/core/traceability.js.map +1 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/validate.d.ts.map +1 -1
- package/dist/core/validate.js +12 -1
- package/dist/core/validate.js.map +1 -1
- package/dist/core/validators/contracts.d.ts.map +1 -1
- package/dist/core/validators/contracts.js +45 -18
- package/dist/core/validators/contracts.js.map +1 -1
- package/dist/core/validators/scenario.d.ts.map +1 -1
- package/dist/core/validators/scenario.js +2 -15
- package/dist/core/validators/scenario.js.map +1 -1
- package/dist/core/validators/spec.js +1 -1
- package/dist/core/validators/spec.js.map +1 -1
- package/dist/core/validators/traceability.d.ts.map +1 -1
- package/dist/core/validators/traceability.js +66 -34
- package/dist/core/validators/traceability.js.map +1 -1
- package/dist/index.cjs +869 -473
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -12
- package/dist/index.mjs +869 -473
- package/dist/index.mjs.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -155,7 +155,7 @@ function report(copied, skipped, dryRun, label) {
|
|
|
155
155
|
|
|
156
156
|
// src/cli/commands/report.ts
|
|
157
157
|
import { mkdir as mkdir2, readFile as readFile12, writeFile } from "fs/promises";
|
|
158
|
-
import
|
|
158
|
+
import path15 from "path";
|
|
159
159
|
|
|
160
160
|
// src/core/config.ts
|
|
161
161
|
import { readFile } from "fs/promises";
|
|
@@ -186,8 +186,9 @@ var defaultConfig = {
|
|
|
186
186
|
},
|
|
187
187
|
traceability: {
|
|
188
188
|
brMustHaveSc: true,
|
|
189
|
-
scMustTouchContracts: true,
|
|
190
189
|
scMustHaveTest: true,
|
|
190
|
+
testFileGlobs: [],
|
|
191
|
+
testFileExcludeGlobs: [],
|
|
191
192
|
scNoTestSeverity: "error",
|
|
192
193
|
allowOrphanContracts: false,
|
|
193
194
|
unknownContractIdSeverity: "error"
|
|
@@ -361,13 +362,6 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
361
362
|
configPath,
|
|
362
363
|
issues
|
|
363
364
|
),
|
|
364
|
-
scMustTouchContracts: readBoolean(
|
|
365
|
-
traceabilityRaw?.scMustTouchContracts,
|
|
366
|
-
base.traceability.scMustTouchContracts,
|
|
367
|
-
"validation.traceability.scMustTouchContracts",
|
|
368
|
-
configPath,
|
|
369
|
-
issues
|
|
370
|
-
),
|
|
371
365
|
scMustHaveTest: readBoolean(
|
|
372
366
|
traceabilityRaw?.scMustHaveTest,
|
|
373
367
|
base.traceability.scMustHaveTest,
|
|
@@ -375,6 +369,20 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
375
369
|
configPath,
|
|
376
370
|
issues
|
|
377
371
|
),
|
|
372
|
+
testFileGlobs: readStringArray(
|
|
373
|
+
traceabilityRaw?.testFileGlobs,
|
|
374
|
+
base.traceability.testFileGlobs,
|
|
375
|
+
"validation.traceability.testFileGlobs",
|
|
376
|
+
configPath,
|
|
377
|
+
issues
|
|
378
|
+
),
|
|
379
|
+
testFileExcludeGlobs: readStringArray(
|
|
380
|
+
traceabilityRaw?.testFileExcludeGlobs,
|
|
381
|
+
base.traceability.testFileExcludeGlobs,
|
|
382
|
+
"validation.traceability.testFileExcludeGlobs",
|
|
383
|
+
configPath,
|
|
384
|
+
issues
|
|
385
|
+
),
|
|
378
386
|
scNoTestSeverity: readTraceabilitySeverity(
|
|
379
387
|
traceabilityRaw?.scNoTestSeverity,
|
|
380
388
|
base.traceability.scNoTestSeverity,
|
|
@@ -508,7 +516,11 @@ function isRecord(value) {
|
|
|
508
516
|
|
|
509
517
|
// src/core/report.ts
|
|
510
518
|
import { readFile as readFile11 } from "fs/promises";
|
|
511
|
-
import
|
|
519
|
+
import path14 from "path";
|
|
520
|
+
|
|
521
|
+
// src/core/contractIndex.ts
|
|
522
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
523
|
+
import path7 from "path";
|
|
512
524
|
|
|
513
525
|
// src/core/discovery.ts
|
|
514
526
|
import { access as access3 } from "fs/promises";
|
|
@@ -516,6 +528,7 @@ import { access as access3 } from "fs/promises";
|
|
|
516
528
|
// src/core/fs.ts
|
|
517
529
|
import { access as access2, readdir as readdir2 } from "fs/promises";
|
|
518
530
|
import path5 from "path";
|
|
531
|
+
import fg from "fast-glob";
|
|
519
532
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
520
533
|
"node_modules",
|
|
521
534
|
".git",
|
|
@@ -537,6 +550,18 @@ async function collectFiles(root, options = {}) {
|
|
|
537
550
|
await walk(root, root, ignoreDirs, extensions, entries);
|
|
538
551
|
return entries;
|
|
539
552
|
}
|
|
553
|
+
async function collectFilesByGlobs(root, options) {
|
|
554
|
+
if (options.globs.length === 0) {
|
|
555
|
+
return [];
|
|
556
|
+
}
|
|
557
|
+
return fg(options.globs, {
|
|
558
|
+
cwd: root,
|
|
559
|
+
ignore: options.ignore ?? [],
|
|
560
|
+
onlyFiles: true,
|
|
561
|
+
absolute: true,
|
|
562
|
+
unique: true
|
|
563
|
+
});
|
|
564
|
+
}
|
|
540
565
|
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
541
566
|
const items = await readdir2(current, { withFileTypes: true });
|
|
542
567
|
for (const item of items) {
|
|
@@ -648,15 +673,66 @@ async function exists3(target) {
|
|
|
648
673
|
}
|
|
649
674
|
}
|
|
650
675
|
|
|
676
|
+
// src/core/contractsDecl.ts
|
|
677
|
+
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
|
|
678
|
+
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
|
|
679
|
+
function extractDeclaredContractIds(text) {
|
|
680
|
+
const ids = [];
|
|
681
|
+
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
682
|
+
const id = match[1];
|
|
683
|
+
if (id) {
|
|
684
|
+
ids.push(id);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return ids;
|
|
688
|
+
}
|
|
689
|
+
function stripContractDeclarationLines(text) {
|
|
690
|
+
return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/core/contractIndex.ts
|
|
694
|
+
async function buildContractIndex(root, config) {
|
|
695
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
696
|
+
const uiRoot = path7.join(contractsRoot, "ui");
|
|
697
|
+
const apiRoot = path7.join(contractsRoot, "api");
|
|
698
|
+
const dbRoot = path7.join(contractsRoot, "db");
|
|
699
|
+
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
700
|
+
collectUiContractFiles(uiRoot),
|
|
701
|
+
collectApiContractFiles(apiRoot),
|
|
702
|
+
collectDataContractFiles(dbRoot)
|
|
703
|
+
]);
|
|
704
|
+
const index = {
|
|
705
|
+
ids: /* @__PURE__ */ new Set(),
|
|
706
|
+
idToFiles: /* @__PURE__ */ new Map(),
|
|
707
|
+
files: { ui: uiFiles, api: apiFiles, db: dbFiles }
|
|
708
|
+
};
|
|
709
|
+
await indexContractFiles(uiFiles, index);
|
|
710
|
+
await indexContractFiles(apiFiles, index);
|
|
711
|
+
await indexContractFiles(dbFiles, index);
|
|
712
|
+
return index;
|
|
713
|
+
}
|
|
714
|
+
async function indexContractFiles(files, index) {
|
|
715
|
+
for (const file of files) {
|
|
716
|
+
const text = await readFile2(file, "utf-8");
|
|
717
|
+
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function record(index, id, file) {
|
|
721
|
+
index.ids.add(id);
|
|
722
|
+
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
723
|
+
current.add(file);
|
|
724
|
+
index.idToFiles.set(id, current);
|
|
725
|
+
}
|
|
726
|
+
|
|
651
727
|
// src/core/ids.ts
|
|
652
|
-
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "
|
|
728
|
+
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
653
729
|
var STRICT_ID_PATTERNS = {
|
|
654
730
|
SPEC: /\bSPEC-\d{4}\b/g,
|
|
655
731
|
BR: /\bBR-\d{4}\b/g,
|
|
656
732
|
SC: /\bSC-\d{4}\b/g,
|
|
657
733
|
UI: /\bUI-\d{4}\b/g,
|
|
658
734
|
API: /\bAPI-\d{4}\b/g,
|
|
659
|
-
|
|
735
|
+
DB: /\bDB-\d{4}\b/g,
|
|
660
736
|
ADR: /\bADR-\d{4}\b/g
|
|
661
737
|
};
|
|
662
738
|
var LOOSE_ID_PATTERNS = {
|
|
@@ -665,7 +741,7 @@ var LOOSE_ID_PATTERNS = {
|
|
|
665
741
|
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
666
742
|
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
667
743
|
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
668
|
-
|
|
744
|
+
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
669
745
|
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
670
746
|
};
|
|
671
747
|
function extractIds(text, prefix) {
|
|
@@ -701,8 +777,170 @@ function isValidId(value, prefix) {
|
|
|
701
777
|
return strict.test(value);
|
|
702
778
|
}
|
|
703
779
|
|
|
780
|
+
// src/core/parse/markdown.ts
|
|
781
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
782
|
+
function parseHeadings(md) {
|
|
783
|
+
const lines = md.split(/\r?\n/);
|
|
784
|
+
const headings = [];
|
|
785
|
+
for (let i = 0; i < lines.length; i++) {
|
|
786
|
+
const line = lines[i] ?? "";
|
|
787
|
+
const match = line.match(HEADING_RE);
|
|
788
|
+
if (!match) continue;
|
|
789
|
+
const levelToken = match[1];
|
|
790
|
+
const title = match[2];
|
|
791
|
+
if (!levelToken || !title) continue;
|
|
792
|
+
headings.push({
|
|
793
|
+
level: levelToken.length,
|
|
794
|
+
title: title.trim(),
|
|
795
|
+
line: i + 1
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
return headings;
|
|
799
|
+
}
|
|
800
|
+
function extractH2Sections(md) {
|
|
801
|
+
const lines = md.split(/\r?\n/);
|
|
802
|
+
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
803
|
+
const sections = /* @__PURE__ */ new Map();
|
|
804
|
+
for (let i = 0; i < headings.length; i++) {
|
|
805
|
+
const current = headings[i];
|
|
806
|
+
if (!current) continue;
|
|
807
|
+
const next = headings[i + 1];
|
|
808
|
+
const startLine = current.line + 1;
|
|
809
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
810
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
811
|
+
sections.set(current.title.trim(), {
|
|
812
|
+
title: current.title.trim(),
|
|
813
|
+
startLine,
|
|
814
|
+
endLine,
|
|
815
|
+
body
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
return sections;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// src/core/parse/spec.ts
|
|
822
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
823
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
824
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
825
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
826
|
+
var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
|
|
827
|
+
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
828
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
829
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
830
|
+
function parseSpec(md, file) {
|
|
831
|
+
const headings = parseHeadings(md);
|
|
832
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
833
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
834
|
+
const sections = extractH2Sections(md);
|
|
835
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
836
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
837
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
838
|
+
const startLine = brSection?.startLine ?? 1;
|
|
839
|
+
const brs = [];
|
|
840
|
+
const brsWithoutPriority = [];
|
|
841
|
+
const brsWithInvalidPriority = [];
|
|
842
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
843
|
+
const lineText = brLines[i] ?? "";
|
|
844
|
+
const lineNumber = startLine + i;
|
|
845
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
846
|
+
if (validMatch) {
|
|
847
|
+
const id = validMatch[1];
|
|
848
|
+
const priority = validMatch[2];
|
|
849
|
+
const text = validMatch[3];
|
|
850
|
+
if (!id || !priority || !text) continue;
|
|
851
|
+
brs.push({
|
|
852
|
+
id,
|
|
853
|
+
priority,
|
|
854
|
+
text: text.trim(),
|
|
855
|
+
line: lineNumber
|
|
856
|
+
});
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
860
|
+
if (anyPriorityMatch) {
|
|
861
|
+
const id = anyPriorityMatch[1];
|
|
862
|
+
const priority = anyPriorityMatch[2];
|
|
863
|
+
const text = anyPriorityMatch[3];
|
|
864
|
+
if (!id || !priority || !text) continue;
|
|
865
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
866
|
+
brsWithInvalidPriority.push({
|
|
867
|
+
id,
|
|
868
|
+
priority,
|
|
869
|
+
text: text.trim(),
|
|
870
|
+
line: lineNumber
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
876
|
+
if (noPriorityMatch) {
|
|
877
|
+
const id = noPriorityMatch[1];
|
|
878
|
+
const text = noPriorityMatch[2];
|
|
879
|
+
if (!id || !text) continue;
|
|
880
|
+
brsWithoutPriority.push({
|
|
881
|
+
id,
|
|
882
|
+
text: text.trim(),
|
|
883
|
+
line: lineNumber
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
const parsed = {
|
|
888
|
+
file,
|
|
889
|
+
sections: sectionNames,
|
|
890
|
+
brs,
|
|
891
|
+
brsWithoutPriority,
|
|
892
|
+
brsWithInvalidPriority,
|
|
893
|
+
contractRefs: parseContractRefs(md)
|
|
894
|
+
};
|
|
895
|
+
if (specId) {
|
|
896
|
+
parsed.specId = specId;
|
|
897
|
+
}
|
|
898
|
+
return parsed;
|
|
899
|
+
}
|
|
900
|
+
function parseContractRefs(md) {
|
|
901
|
+
const lines = [];
|
|
902
|
+
for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
|
|
903
|
+
lines.push((match[1] ?? "").trim());
|
|
904
|
+
}
|
|
905
|
+
const ids = [];
|
|
906
|
+
const invalidTokens = [];
|
|
907
|
+
let hasNone = false;
|
|
908
|
+
for (const line of lines) {
|
|
909
|
+
if (line.length === 0) {
|
|
910
|
+
invalidTokens.push("(empty)");
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
const tokens = line.split(",").map((token) => token.trim());
|
|
914
|
+
for (const token of tokens) {
|
|
915
|
+
if (token.length === 0) {
|
|
916
|
+
invalidTokens.push("(empty)");
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
if (token === "none") {
|
|
920
|
+
hasNone = true;
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
924
|
+
ids.push(token);
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
invalidTokens.push(token);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return {
|
|
931
|
+
lines,
|
|
932
|
+
ids: unique2(ids),
|
|
933
|
+
invalidTokens: unique2(invalidTokens),
|
|
934
|
+
hasNone
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
function unique2(values) {
|
|
938
|
+
return Array.from(new Set(values));
|
|
939
|
+
}
|
|
940
|
+
|
|
704
941
|
// src/core/traceability.ts
|
|
705
|
-
import { readFile as
|
|
942
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
943
|
+
import path8 from "path";
|
|
706
944
|
|
|
707
945
|
// src/core/gherkin/parse.ts
|
|
708
946
|
import {
|
|
@@ -739,7 +977,7 @@ var SC_TAG_RE = /^SC-\d{4}$/;
|
|
|
739
977
|
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
740
978
|
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
741
979
|
var API_TAG_RE = /^API-\d{4}$/;
|
|
742
|
-
var
|
|
980
|
+
var DB_TAG_RE = /^DB-\d{4}$/;
|
|
743
981
|
function parseScenarioDocument(text, uri) {
|
|
744
982
|
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
745
983
|
if (!gherkinDocument) {
|
|
@@ -768,10 +1006,10 @@ function buildScenarioAtoms(document) {
|
|
|
768
1006
|
return document.scenarios.map((scenario) => {
|
|
769
1007
|
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
770
1008
|
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
771
|
-
const brIds =
|
|
1009
|
+
const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
772
1010
|
const contractIds = /* @__PURE__ */ new Set();
|
|
773
1011
|
scenario.tags.forEach((tag) => {
|
|
774
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) ||
|
|
1012
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
|
|
775
1013
|
contractIds.add(tag);
|
|
776
1014
|
}
|
|
777
1015
|
});
|
|
@@ -779,7 +1017,7 @@ function buildScenarioAtoms(document) {
|
|
|
779
1017
|
for (const text of collectStepTexts(step)) {
|
|
780
1018
|
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
781
1019
|
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
782
|
-
extractIds(text, "
|
|
1020
|
+
extractIds(text, "DB").forEach((id) => contractIds.add(id));
|
|
783
1021
|
}
|
|
784
1022
|
}
|
|
785
1023
|
const atom = {
|
|
@@ -858,16 +1096,37 @@ function collectStepTexts(step) {
|
|
|
858
1096
|
}
|
|
859
1097
|
return texts;
|
|
860
1098
|
}
|
|
861
|
-
function
|
|
1099
|
+
function unique3(values) {
|
|
862
1100
|
return Array.from(new Set(values));
|
|
863
1101
|
}
|
|
864
1102
|
|
|
865
1103
|
// src/core/traceability.ts
|
|
866
1104
|
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1105
|
+
var SC_TEST_ANNOTATION_RE = /\bQFAI:SC-(\d{4})\b/g;
|
|
1106
|
+
var DEFAULT_TEST_FILE_EXCLUDE_GLOBS = [
|
|
1107
|
+
"**/node_modules/**",
|
|
1108
|
+
"**/.git/**",
|
|
1109
|
+
"**/.qfai/**",
|
|
1110
|
+
"**/dist/**",
|
|
1111
|
+
"**/build/**",
|
|
1112
|
+
"**/coverage/**",
|
|
1113
|
+
"**/.next/**",
|
|
1114
|
+
"**/out/**"
|
|
1115
|
+
];
|
|
1116
|
+
function extractAnnotatedScIds(text) {
|
|
1117
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1118
|
+
for (const match of text.matchAll(SC_TEST_ANNOTATION_RE)) {
|
|
1119
|
+
const suffix = match[1];
|
|
1120
|
+
if (suffix) {
|
|
1121
|
+
ids.add(`SC-${suffix}`);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return Array.from(ids);
|
|
1125
|
+
}
|
|
867
1126
|
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
868
1127
|
const scIds = /* @__PURE__ */ new Set();
|
|
869
1128
|
for (const file of scenarioFiles) {
|
|
870
|
-
const text = await
|
|
1129
|
+
const text = await readFile3(file, "utf-8");
|
|
871
1130
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
872
1131
|
if (!document || errors.length > 0) {
|
|
873
1132
|
continue;
|
|
@@ -882,14 +1141,67 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
|
882
1141
|
}
|
|
883
1142
|
return scIds;
|
|
884
1143
|
}
|
|
885
|
-
async function
|
|
1144
|
+
async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
1145
|
+
const sources = /* @__PURE__ */ new Map();
|
|
1146
|
+
for (const file of scenarioFiles) {
|
|
1147
|
+
const text = await readFile3(file, "utf-8");
|
|
1148
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1149
|
+
if (!document || errors.length > 0) {
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
for (const scenario of document.scenarios) {
|
|
1153
|
+
for (const tag of scenario.tags) {
|
|
1154
|
+
if (!SC_TAG_RE2.test(tag)) {
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
const current = sources.get(tag) ?? /* @__PURE__ */ new Set();
|
|
1158
|
+
current.add(file);
|
|
1159
|
+
sources.set(tag, current);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return sources;
|
|
1164
|
+
}
|
|
1165
|
+
async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
886
1166
|
const refs = /* @__PURE__ */ new Map();
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1167
|
+
const normalizedGlobs = normalizeGlobs(globs);
|
|
1168
|
+
const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
|
|
1169
|
+
const mergedExcludeGlobs = Array.from(
|
|
1170
|
+
/* @__PURE__ */ new Set([...DEFAULT_TEST_FILE_EXCLUDE_GLOBS, ...normalizedExcludeGlobs])
|
|
1171
|
+
);
|
|
1172
|
+
if (normalizedGlobs.length === 0) {
|
|
1173
|
+
return {
|
|
1174
|
+
refs,
|
|
1175
|
+
scan: {
|
|
1176
|
+
globs: normalizedGlobs,
|
|
1177
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
1178
|
+
matchedFileCount: 0
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
let files = [];
|
|
1183
|
+
try {
|
|
1184
|
+
files = await collectFilesByGlobs(root, {
|
|
1185
|
+
globs: normalizedGlobs,
|
|
1186
|
+
ignore: mergedExcludeGlobs
|
|
1187
|
+
});
|
|
1188
|
+
} catch (error2) {
|
|
1189
|
+
return {
|
|
1190
|
+
refs,
|
|
1191
|
+
scan: {
|
|
1192
|
+
globs: normalizedGlobs,
|
|
1193
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
1194
|
+
matchedFileCount: 0
|
|
1195
|
+
},
|
|
1196
|
+
error: formatError3(error2)
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
const normalizedFiles = Array.from(
|
|
1200
|
+
new Set(files.map((file) => path8.normalize(file)))
|
|
1201
|
+
);
|
|
1202
|
+
for (const file of normalizedFiles) {
|
|
1203
|
+
const text = await readFile3(file, "utf-8");
|
|
1204
|
+
const scIds = extractAnnotatedScIds(text);
|
|
893
1205
|
if (scIds.length === 0) {
|
|
894
1206
|
continue;
|
|
895
1207
|
}
|
|
@@ -899,7 +1211,14 @@ async function collectScTestReferences(testsRoot) {
|
|
|
899
1211
|
refs.set(scId, current);
|
|
900
1212
|
}
|
|
901
1213
|
}
|
|
902
|
-
return
|
|
1214
|
+
return {
|
|
1215
|
+
refs,
|
|
1216
|
+
scan: {
|
|
1217
|
+
globs: normalizedGlobs,
|
|
1218
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
1219
|
+
matchedFileCount: normalizedFiles.length
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
903
1222
|
}
|
|
904
1223
|
function buildScCoverage(scIds, refs) {
|
|
905
1224
|
const sortedScIds = toSortedArray(scIds);
|
|
@@ -927,18 +1246,27 @@ function buildScCoverage(scIds, refs) {
|
|
|
927
1246
|
function toSortedArray(values) {
|
|
928
1247
|
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
929
1248
|
}
|
|
1249
|
+
function normalizeGlobs(globs) {
|
|
1250
|
+
return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
1251
|
+
}
|
|
1252
|
+
function formatError3(error2) {
|
|
1253
|
+
if (error2 instanceof Error) {
|
|
1254
|
+
return error2.message;
|
|
1255
|
+
}
|
|
1256
|
+
return String(error2);
|
|
1257
|
+
}
|
|
930
1258
|
|
|
931
1259
|
// src/core/version.ts
|
|
932
|
-
import { readFile as
|
|
933
|
-
import
|
|
1260
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1261
|
+
import path9 from "path";
|
|
934
1262
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
935
1263
|
async function resolveToolVersion() {
|
|
936
|
-
if ("0.4.
|
|
937
|
-
return "0.4.
|
|
1264
|
+
if ("0.4.4".length > 0) {
|
|
1265
|
+
return "0.4.4";
|
|
938
1266
|
}
|
|
939
1267
|
try {
|
|
940
1268
|
const packagePath = resolvePackageJsonPath();
|
|
941
|
-
const raw = await
|
|
1269
|
+
const raw = await readFile4(packagePath, "utf-8");
|
|
942
1270
|
const parsed = JSON.parse(raw);
|
|
943
1271
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
944
1272
|
return version.length > 0 ? version : "unknown";
|
|
@@ -949,71 +1277,42 @@ async function resolveToolVersion() {
|
|
|
949
1277
|
function resolvePackageJsonPath() {
|
|
950
1278
|
const base = import.meta.url;
|
|
951
1279
|
const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
|
|
952
|
-
return
|
|
1280
|
+
return path9.resolve(path9.dirname(basePath), "../../package.json");
|
|
953
1281
|
}
|
|
954
1282
|
|
|
955
1283
|
// src/core/validators/contracts.ts
|
|
956
|
-
import { readFile as
|
|
957
|
-
import
|
|
1284
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1285
|
+
import path11 from "path";
|
|
958
1286
|
|
|
959
1287
|
// src/core/contracts.ts
|
|
960
|
-
import
|
|
1288
|
+
import path10 from "path";
|
|
961
1289
|
import { parse as parseYaml2 } from "yaml";
|
|
962
1290
|
function parseStructuredContract(file, text) {
|
|
963
|
-
const ext =
|
|
1291
|
+
const ext = path10.extname(file).toLowerCase();
|
|
964
1292
|
if (ext === ".json") {
|
|
965
1293
|
return JSON.parse(text);
|
|
966
1294
|
}
|
|
967
1295
|
return parseYaml2(text);
|
|
968
1296
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
extractIds(operationId, "API").forEach((id) => ids.add(id));
|
|
979
|
-
}
|
|
980
|
-
return Array.from(ids);
|
|
981
|
-
}
|
|
982
|
-
function collectOperationIds(value, out) {
|
|
983
|
-
if (!value || typeof value !== "object") {
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
if (Array.isArray(value)) {
|
|
987
|
-
for (const item of value) {
|
|
988
|
-
collectOperationIds(item, out);
|
|
989
|
-
}
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
993
|
-
if (key === "operationId" && typeof entry === "string") {
|
|
994
|
-
out.add(entry);
|
|
995
|
-
continue;
|
|
996
|
-
}
|
|
997
|
-
collectOperationIds(entry, out);
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// src/core/validators/contracts.ts
|
|
1002
|
-
var SQL_DANGEROUS_PATTERNS = [
|
|
1003
|
-
{ pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
|
|
1004
|
-
{ pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
|
|
1005
|
-
{ pattern: /\bTRUNCATE\b/i, label: "TRUNCATE" },
|
|
1006
|
-
{
|
|
1007
|
-
pattern: /\bALTER\s+TABLE\b[\s\S]*\bDROP\b/i,
|
|
1008
|
-
label: "ALTER TABLE ... DROP"
|
|
1297
|
+
|
|
1298
|
+
// src/core/validators/contracts.ts
|
|
1299
|
+
var SQL_DANGEROUS_PATTERNS = [
|
|
1300
|
+
{ pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
|
|
1301
|
+
{ pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
|
|
1302
|
+
{ pattern: /\bTRUNCATE\b/i, label: "TRUNCATE" },
|
|
1303
|
+
{
|
|
1304
|
+
pattern: /\bALTER\s+TABLE\b[\s\S]*\bDROP\b/i,
|
|
1305
|
+
label: "ALTER TABLE ... DROP"
|
|
1009
1306
|
}
|
|
1010
1307
|
];
|
|
1011
1308
|
async function validateContracts(root, config) {
|
|
1012
1309
|
const issues = [];
|
|
1013
1310
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1014
|
-
issues.push(...await validateUiContracts(
|
|
1015
|
-
issues.push(...await validateApiContracts(
|
|
1016
|
-
issues.push(...await validateDataContracts(
|
|
1311
|
+
issues.push(...await validateUiContracts(path11.join(contractsRoot, "ui")));
|
|
1312
|
+
issues.push(...await validateApiContracts(path11.join(contractsRoot, "api")));
|
|
1313
|
+
issues.push(...await validateDataContracts(path11.join(contractsRoot, "db")));
|
|
1314
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
1315
|
+
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1017
1316
|
return issues;
|
|
1018
1317
|
}
|
|
1019
1318
|
async function validateUiContracts(uiRoot) {
|
|
@@ -1031,14 +1330,14 @@ async function validateUiContracts(uiRoot) {
|
|
|
1031
1330
|
}
|
|
1032
1331
|
const issues = [];
|
|
1033
1332
|
for (const file of files) {
|
|
1034
|
-
const text = await
|
|
1333
|
+
const text = await readFile5(file, "utf-8");
|
|
1035
1334
|
const invalidIds = extractInvalidIds(text, [
|
|
1036
1335
|
"SPEC",
|
|
1037
1336
|
"BR",
|
|
1038
1337
|
"SC",
|
|
1039
1338
|
"UI",
|
|
1040
1339
|
"API",
|
|
1041
|
-
"
|
|
1340
|
+
"DB",
|
|
1042
1341
|
"ADR"
|
|
1043
1342
|
]);
|
|
1044
1343
|
if (invalidIds.length > 0) {
|
|
@@ -1053,32 +1352,20 @@ async function validateUiContracts(uiRoot) {
|
|
|
1053
1352
|
)
|
|
1054
1353
|
);
|
|
1055
1354
|
}
|
|
1056
|
-
|
|
1355
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1356
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
|
|
1057
1357
|
try {
|
|
1058
|
-
|
|
1358
|
+
parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
1059
1359
|
} catch (error2) {
|
|
1060
1360
|
issues.push(
|
|
1061
1361
|
issue(
|
|
1062
1362
|
"QFAI-CONTRACT-001",
|
|
1063
|
-
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1363
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
|
|
1064
1364
|
"error",
|
|
1065
1365
|
file,
|
|
1066
1366
|
"contracts.ui.parse"
|
|
1067
1367
|
)
|
|
1068
1368
|
);
|
|
1069
|
-
continue;
|
|
1070
|
-
}
|
|
1071
|
-
const uiIds = extractUiContractIds(doc);
|
|
1072
|
-
if (uiIds.length === 0) {
|
|
1073
|
-
issues.push(
|
|
1074
|
-
issue(
|
|
1075
|
-
"QFAI-CONTRACT-002",
|
|
1076
|
-
`UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
1077
|
-
"error",
|
|
1078
|
-
file,
|
|
1079
|
-
"contracts.ui.id"
|
|
1080
|
-
)
|
|
1081
|
-
);
|
|
1082
1369
|
}
|
|
1083
1370
|
}
|
|
1084
1371
|
return issues;
|
|
@@ -1098,14 +1385,14 @@ async function validateApiContracts(apiRoot) {
|
|
|
1098
1385
|
}
|
|
1099
1386
|
const issues = [];
|
|
1100
1387
|
for (const file of files) {
|
|
1101
|
-
const text = await
|
|
1388
|
+
const text = await readFile5(file, "utf-8");
|
|
1102
1389
|
const invalidIds = extractInvalidIds(text, [
|
|
1103
1390
|
"SPEC",
|
|
1104
1391
|
"BR",
|
|
1105
1392
|
"SC",
|
|
1106
1393
|
"UI",
|
|
1107
1394
|
"API",
|
|
1108
|
-
"
|
|
1395
|
+
"DB",
|
|
1109
1396
|
"ADR"
|
|
1110
1397
|
]);
|
|
1111
1398
|
if (invalidIds.length > 0) {
|
|
@@ -1120,14 +1407,16 @@ async function validateApiContracts(apiRoot) {
|
|
|
1120
1407
|
)
|
|
1121
1408
|
);
|
|
1122
1409
|
}
|
|
1410
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1411
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "API"));
|
|
1123
1412
|
let doc;
|
|
1124
1413
|
try {
|
|
1125
|
-
doc = parseStructuredContract(file, text);
|
|
1414
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
1126
1415
|
} catch (error2) {
|
|
1127
1416
|
issues.push(
|
|
1128
1417
|
issue(
|
|
1129
1418
|
"QFAI-CONTRACT-001",
|
|
1130
|
-
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1419
|
+
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
|
|
1131
1420
|
"error",
|
|
1132
1421
|
file,
|
|
1133
1422
|
"contracts.api.parse"
|
|
@@ -1146,18 +1435,6 @@ async function validateApiContracts(apiRoot) {
|
|
|
1146
1435
|
)
|
|
1147
1436
|
);
|
|
1148
1437
|
}
|
|
1149
|
-
const apiIds = extractApiContractIds(doc);
|
|
1150
|
-
if (apiIds.length === 0) {
|
|
1151
|
-
issues.push(
|
|
1152
|
-
issue(
|
|
1153
|
-
"QFAI-CONTRACT-002",
|
|
1154
|
-
`API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
1155
|
-
"error",
|
|
1156
|
-
file,
|
|
1157
|
-
"contracts.api.id"
|
|
1158
|
-
)
|
|
1159
|
-
);
|
|
1160
|
-
}
|
|
1161
1438
|
}
|
|
1162
1439
|
return issues;
|
|
1163
1440
|
}
|
|
@@ -1166,24 +1443,24 @@ async function validateDataContracts(dataRoot) {
|
|
|
1166
1443
|
if (files.length === 0) {
|
|
1167
1444
|
return [
|
|
1168
1445
|
issue(
|
|
1169
|
-
"QFAI-
|
|
1170
|
-
"
|
|
1446
|
+
"QFAI-DB-000",
|
|
1447
|
+
"DB \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1171
1448
|
"info",
|
|
1172
1449
|
dataRoot,
|
|
1173
|
-
"contracts.
|
|
1450
|
+
"contracts.db.files"
|
|
1174
1451
|
)
|
|
1175
1452
|
];
|
|
1176
1453
|
}
|
|
1177
1454
|
const issues = [];
|
|
1178
1455
|
for (const file of files) {
|
|
1179
|
-
const text = await
|
|
1456
|
+
const text = await readFile5(file, "utf-8");
|
|
1180
1457
|
const invalidIds = extractInvalidIds(text, [
|
|
1181
1458
|
"SPEC",
|
|
1182
1459
|
"BR",
|
|
1183
1460
|
"SC",
|
|
1184
1461
|
"UI",
|
|
1185
1462
|
"API",
|
|
1186
|
-
"
|
|
1463
|
+
"DB",
|
|
1187
1464
|
"ADR"
|
|
1188
1465
|
]);
|
|
1189
1466
|
if (invalidIds.length > 0) {
|
|
@@ -1198,6 +1475,8 @@ async function validateDataContracts(dataRoot) {
|
|
|
1198
1475
|
)
|
|
1199
1476
|
);
|
|
1200
1477
|
}
|
|
1478
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1479
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "DB"));
|
|
1201
1480
|
issues.push(...lintSql(text, file));
|
|
1202
1481
|
}
|
|
1203
1482
|
return issues;
|
|
@@ -1208,21 +1487,87 @@ function lintSql(text, file) {
|
|
|
1208
1487
|
if (pattern.test(text)) {
|
|
1209
1488
|
issues.push(
|
|
1210
1489
|
issue(
|
|
1211
|
-
"QFAI-
|
|
1490
|
+
"QFAI-DB-001",
|
|
1212
1491
|
`\u5371\u967A\u306A SQL \u64CD\u4F5C\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059: ${label}`,
|
|
1213
1492
|
"warning",
|
|
1214
1493
|
file,
|
|
1215
|
-
"contracts.
|
|
1494
|
+
"contracts.db.sql"
|
|
1216
1495
|
)
|
|
1217
1496
|
);
|
|
1218
1497
|
}
|
|
1219
1498
|
}
|
|
1220
1499
|
return issues;
|
|
1221
1500
|
}
|
|
1501
|
+
function validateDeclaredContractIds(ids, file, kind) {
|
|
1502
|
+
const issues = [];
|
|
1503
|
+
if (ids.length === 0) {
|
|
1504
|
+
issues.push(
|
|
1505
|
+
issue(
|
|
1506
|
+
"QFAI-CONTRACT-010",
|
|
1507
|
+
`\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
|
|
1508
|
+
"error",
|
|
1509
|
+
file,
|
|
1510
|
+
"contracts.declaration"
|
|
1511
|
+
)
|
|
1512
|
+
);
|
|
1513
|
+
return issues;
|
|
1514
|
+
}
|
|
1515
|
+
if (ids.length > 1) {
|
|
1516
|
+
issues.push(
|
|
1517
|
+
issue(
|
|
1518
|
+
"QFAI-CONTRACT-011",
|
|
1519
|
+
`\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B\u8907\u6570\u306E QFAI-CONTRACT-ID \u304C\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${ids.join(
|
|
1520
|
+
", "
|
|
1521
|
+
)}`,
|
|
1522
|
+
"error",
|
|
1523
|
+
file,
|
|
1524
|
+
"contracts.declaration",
|
|
1525
|
+
ids
|
|
1526
|
+
)
|
|
1527
|
+
);
|
|
1528
|
+
return issues;
|
|
1529
|
+
}
|
|
1530
|
+
const [id] = ids;
|
|
1531
|
+
if (id && !id.startsWith(`${kind}-`)) {
|
|
1532
|
+
issues.push(
|
|
1533
|
+
issue(
|
|
1534
|
+
"QFAI-CONTRACT-013",
|
|
1535
|
+
`\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E QFAI-CONTRACT-ID \u304C ${kind}- \u3067\u306F\u3042\u308A\u307E\u305B\u3093: ${id}`,
|
|
1536
|
+
"error",
|
|
1537
|
+
file,
|
|
1538
|
+
"contracts.declarationPrefix",
|
|
1539
|
+
[id]
|
|
1540
|
+
)
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
return issues;
|
|
1544
|
+
}
|
|
1545
|
+
function validateDuplicateContractIds(contractIndex) {
|
|
1546
|
+
const issues = [];
|
|
1547
|
+
for (const [id, files] of contractIndex.idToFiles.entries()) {
|
|
1548
|
+
if (files.size <= 1) {
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
const sortedFiles = Array.from(files).sort((a, b) => a.localeCompare(b));
|
|
1552
|
+
issues.push(
|
|
1553
|
+
issue(
|
|
1554
|
+
"QFAI-CONTRACT-012",
|
|
1555
|
+
`\u5951\u7D04 ID \u304C\u8907\u6570\u30D5\u30A1\u30A4\u30EB\u3067\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${id} (${sortedFiles.join(
|
|
1556
|
+
", "
|
|
1557
|
+
)})`,
|
|
1558
|
+
"error",
|
|
1559
|
+
sortedFiles[0],
|
|
1560
|
+
"contracts.idDuplicate",
|
|
1561
|
+
[id]
|
|
1562
|
+
)
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
return issues;
|
|
1566
|
+
}
|
|
1222
1567
|
function hasOpenApi(doc) {
|
|
1223
1568
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
1224
1569
|
}
|
|
1225
|
-
function
|
|
1570
|
+
function formatError4(error2) {
|
|
1226
1571
|
if (error2 instanceof Error) {
|
|
1227
1572
|
return error2.message;
|
|
1228
1573
|
}
|
|
@@ -1247,8 +1592,8 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1247
1592
|
}
|
|
1248
1593
|
|
|
1249
1594
|
// src/core/validators/delta.ts
|
|
1250
|
-
import { readFile as
|
|
1251
|
-
import
|
|
1595
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1596
|
+
import path12 from "path";
|
|
1252
1597
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1253
1598
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1254
1599
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1258,268 +1603,87 @@ async function validateDeltas(root, config) {
|
|
|
1258
1603
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1259
1604
|
const packs = await collectSpecPackDirs(specsRoot);
|
|
1260
1605
|
if (packs.length === 0) {
|
|
1261
|
-
return [];
|
|
1262
|
-
}
|
|
1263
|
-
const issues = [];
|
|
1264
|
-
for (const pack of packs) {
|
|
1265
|
-
const deltaPath =
|
|
1266
|
-
let text;
|
|
1267
|
-
try {
|
|
1268
|
-
text = await
|
|
1269
|
-
} catch (error2) {
|
|
1270
|
-
if (isMissingFileError2(error2)) {
|
|
1271
|
-
issues.push(
|
|
1272
|
-
issue2(
|
|
1273
|
-
"QFAI-DELTA-001",
|
|
1274
|
-
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1275
|
-
"error",
|
|
1276
|
-
deltaPath,
|
|
1277
|
-
"delta.exists"
|
|
1278
|
-
)
|
|
1279
|
-
);
|
|
1280
|
-
continue;
|
|
1281
|
-
}
|
|
1282
|
-
throw error2;
|
|
1283
|
-
}
|
|
1284
|
-
const hasSection = SECTION_RE.test(text);
|
|
1285
|
-
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
1286
|
-
const hasChange = CHANGE_LINE_RE.test(text);
|
|
1287
|
-
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
1288
|
-
issues.push(
|
|
1289
|
-
issue2(
|
|
1290
|
-
"QFAI-DELTA-002",
|
|
1291
|
-
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
1292
|
-
"error",
|
|
1293
|
-
deltaPath,
|
|
1294
|
-
"delta.section"
|
|
1295
|
-
)
|
|
1296
|
-
);
|
|
1297
|
-
continue;
|
|
1298
|
-
}
|
|
1299
|
-
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
1300
|
-
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
1301
|
-
if (compatibilityChecked === changeChecked) {
|
|
1302
|
-
issues.push(
|
|
1303
|
-
issue2(
|
|
1304
|
-
"QFAI-DELTA-003",
|
|
1305
|
-
"delta.md \u306E\u5909\u66F4\u533A\u5206\u306F\u3069\u3061\u3089\u304B1\u3064\u3060\u3051\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4E21\u65B9ON/\u4E21\u65B9OFF\u306F\u7121\u52B9\u3067\u3059\uFF09\u3002",
|
|
1306
|
-
"error",
|
|
1307
|
-
deltaPath,
|
|
1308
|
-
"delta.classification"
|
|
1309
|
-
)
|
|
1310
|
-
);
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
return issues;
|
|
1314
|
-
}
|
|
1315
|
-
function isMissingFileError2(error2) {
|
|
1316
|
-
if (!error2 || typeof error2 !== "object") {
|
|
1317
|
-
return false;
|
|
1318
|
-
}
|
|
1319
|
-
return error2.code === "ENOENT";
|
|
1320
|
-
}
|
|
1321
|
-
function issue2(code, message, severity, file, rule, refs) {
|
|
1322
|
-
const issue7 = {
|
|
1323
|
-
code,
|
|
1324
|
-
severity,
|
|
1325
|
-
message
|
|
1326
|
-
};
|
|
1327
|
-
if (file) {
|
|
1328
|
-
issue7.file = file;
|
|
1329
|
-
}
|
|
1330
|
-
if (rule) {
|
|
1331
|
-
issue7.rule = rule;
|
|
1332
|
-
}
|
|
1333
|
-
if (refs && refs.length > 0) {
|
|
1334
|
-
issue7.refs = refs;
|
|
1335
|
-
}
|
|
1336
|
-
return issue7;
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
// src/core/validators/ids.ts
|
|
1340
|
-
import { readFile as readFile7 } from "fs/promises";
|
|
1341
|
-
import path12 from "path";
|
|
1342
|
-
|
|
1343
|
-
// src/core/contractIndex.ts
|
|
1344
|
-
import { readFile as readFile6 } from "fs/promises";
|
|
1345
|
-
import path11 from "path";
|
|
1346
|
-
async function buildContractIndex(root, config) {
|
|
1347
|
-
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1348
|
-
const uiRoot = path11.join(contractsRoot, "ui");
|
|
1349
|
-
const apiRoot = path11.join(contractsRoot, "api");
|
|
1350
|
-
const dataRoot = path11.join(contractsRoot, "db");
|
|
1351
|
-
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
1352
|
-
collectUiContractFiles(uiRoot),
|
|
1353
|
-
collectApiContractFiles(apiRoot),
|
|
1354
|
-
collectDataContractFiles(dataRoot)
|
|
1355
|
-
]);
|
|
1356
|
-
const index = {
|
|
1357
|
-
ids: /* @__PURE__ */ new Set(),
|
|
1358
|
-
idToFiles: /* @__PURE__ */ new Map(),
|
|
1359
|
-
files: { ui: uiFiles, api: apiFiles, data: dataFiles },
|
|
1360
|
-
structuredParseFailedFiles: /* @__PURE__ */ new Set()
|
|
1361
|
-
};
|
|
1362
|
-
await indexUiContracts(uiFiles, index);
|
|
1363
|
-
await indexApiContracts(apiFiles, index);
|
|
1364
|
-
await indexDataContracts(dataFiles, index);
|
|
1365
|
-
return index;
|
|
1366
|
-
}
|
|
1367
|
-
async function indexUiContracts(files, index) {
|
|
1368
|
-
for (const file of files) {
|
|
1369
|
-
const text = await readFile6(file, "utf-8");
|
|
1370
|
-
try {
|
|
1371
|
-
const doc = parseStructuredContract(file, text);
|
|
1372
|
-
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
1373
|
-
} catch {
|
|
1374
|
-
index.structuredParseFailedFiles.add(file);
|
|
1375
|
-
extractIds(text, "UI").forEach((id) => record(index, id, file));
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
async function indexApiContracts(files, index) {
|
|
1380
|
-
for (const file of files) {
|
|
1381
|
-
const text = await readFile6(file, "utf-8");
|
|
1382
|
-
try {
|
|
1383
|
-
const doc = parseStructuredContract(file, text);
|
|
1384
|
-
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
1385
|
-
} catch {
|
|
1386
|
-
index.structuredParseFailedFiles.add(file);
|
|
1387
|
-
extractIds(text, "API").forEach((id) => record(index, id, file));
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
async function indexDataContracts(files, index) {
|
|
1392
|
-
for (const file of files) {
|
|
1393
|
-
const text = await readFile6(file, "utf-8");
|
|
1394
|
-
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
function record(index, id, file) {
|
|
1398
|
-
index.ids.add(id);
|
|
1399
|
-
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
1400
|
-
current.add(file);
|
|
1401
|
-
index.idToFiles.set(id, current);
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
// src/core/parse/markdown.ts
|
|
1405
|
-
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1406
|
-
function parseHeadings(md) {
|
|
1407
|
-
const lines = md.split(/\r?\n/);
|
|
1408
|
-
const headings = [];
|
|
1409
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1410
|
-
const line = lines[i] ?? "";
|
|
1411
|
-
const match = line.match(HEADING_RE);
|
|
1412
|
-
if (!match) continue;
|
|
1413
|
-
const levelToken = match[1];
|
|
1414
|
-
const title = match[2];
|
|
1415
|
-
if (!levelToken || !title) continue;
|
|
1416
|
-
headings.push({
|
|
1417
|
-
level: levelToken.length,
|
|
1418
|
-
title: title.trim(),
|
|
1419
|
-
line: i + 1
|
|
1420
|
-
});
|
|
1421
|
-
}
|
|
1422
|
-
return headings;
|
|
1423
|
-
}
|
|
1424
|
-
function extractH2Sections(md) {
|
|
1425
|
-
const lines = md.split(/\r?\n/);
|
|
1426
|
-
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1427
|
-
const sections = /* @__PURE__ */ new Map();
|
|
1428
|
-
for (let i = 0; i < headings.length; i++) {
|
|
1429
|
-
const current = headings[i];
|
|
1430
|
-
if (!current) continue;
|
|
1431
|
-
const next = headings[i + 1];
|
|
1432
|
-
const startLine = current.line + 1;
|
|
1433
|
-
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1434
|
-
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1435
|
-
sections.set(current.title.trim(), {
|
|
1436
|
-
title: current.title.trim(),
|
|
1437
|
-
startLine,
|
|
1438
|
-
endLine,
|
|
1439
|
-
body
|
|
1440
|
-
});
|
|
1441
|
-
}
|
|
1442
|
-
return sections;
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
// src/core/parse/spec.ts
|
|
1446
|
-
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1447
|
-
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1448
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1449
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1450
|
-
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1451
|
-
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1452
|
-
function parseSpec(md, file) {
|
|
1453
|
-
const headings = parseHeadings(md);
|
|
1454
|
-
const h1 = headings.find((heading) => heading.level === 1);
|
|
1455
|
-
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1456
|
-
const sections = extractH2Sections(md);
|
|
1457
|
-
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1458
|
-
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1459
|
-
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1460
|
-
const startLine = brSection?.startLine ?? 1;
|
|
1461
|
-
const brs = [];
|
|
1462
|
-
const brsWithoutPriority = [];
|
|
1463
|
-
const brsWithInvalidPriority = [];
|
|
1464
|
-
for (let i = 0; i < brLines.length; i++) {
|
|
1465
|
-
const lineText = brLines[i] ?? "";
|
|
1466
|
-
const lineNumber = startLine + i;
|
|
1467
|
-
const validMatch = lineText.match(BR_LINE_RE);
|
|
1468
|
-
if (validMatch) {
|
|
1469
|
-
const id = validMatch[1];
|
|
1470
|
-
const priority = validMatch[2];
|
|
1471
|
-
const text = validMatch[3];
|
|
1472
|
-
if (!id || !priority || !text) continue;
|
|
1473
|
-
brs.push({
|
|
1474
|
-
id,
|
|
1475
|
-
priority,
|
|
1476
|
-
text: text.trim(),
|
|
1477
|
-
line: lineNumber
|
|
1478
|
-
});
|
|
1479
|
-
continue;
|
|
1480
|
-
}
|
|
1481
|
-
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1482
|
-
if (anyPriorityMatch) {
|
|
1483
|
-
const id = anyPriorityMatch[1];
|
|
1484
|
-
const priority = anyPriorityMatch[2];
|
|
1485
|
-
const text = anyPriorityMatch[3];
|
|
1486
|
-
if (!id || !priority || !text) continue;
|
|
1487
|
-
if (!VALID_PRIORITIES.has(priority)) {
|
|
1488
|
-
brsWithInvalidPriority.push({
|
|
1489
|
-
id,
|
|
1490
|
-
priority,
|
|
1491
|
-
text: text.trim(),
|
|
1492
|
-
line: lineNumber
|
|
1493
|
-
});
|
|
1606
|
+
return [];
|
|
1607
|
+
}
|
|
1608
|
+
const issues = [];
|
|
1609
|
+
for (const pack of packs) {
|
|
1610
|
+
const deltaPath = path12.join(pack, "delta.md");
|
|
1611
|
+
let text;
|
|
1612
|
+
try {
|
|
1613
|
+
text = await readFile6(deltaPath, "utf-8");
|
|
1614
|
+
} catch (error2) {
|
|
1615
|
+
if (isMissingFileError2(error2)) {
|
|
1616
|
+
issues.push(
|
|
1617
|
+
issue2(
|
|
1618
|
+
"QFAI-DELTA-001",
|
|
1619
|
+
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1620
|
+
"error",
|
|
1621
|
+
deltaPath,
|
|
1622
|
+
"delta.exists"
|
|
1623
|
+
)
|
|
1624
|
+
);
|
|
1625
|
+
continue;
|
|
1494
1626
|
}
|
|
1627
|
+
throw error2;
|
|
1628
|
+
}
|
|
1629
|
+
const hasSection = SECTION_RE.test(text);
|
|
1630
|
+
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
1631
|
+
const hasChange = CHANGE_LINE_RE.test(text);
|
|
1632
|
+
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
1633
|
+
issues.push(
|
|
1634
|
+
issue2(
|
|
1635
|
+
"QFAI-DELTA-002",
|
|
1636
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
1637
|
+
"error",
|
|
1638
|
+
deltaPath,
|
|
1639
|
+
"delta.section"
|
|
1640
|
+
)
|
|
1641
|
+
);
|
|
1495
1642
|
continue;
|
|
1496
1643
|
}
|
|
1497
|
-
const
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1644
|
+
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
1645
|
+
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
1646
|
+
if (compatibilityChecked === changeChecked) {
|
|
1647
|
+
issues.push(
|
|
1648
|
+
issue2(
|
|
1649
|
+
"QFAI-DELTA-003",
|
|
1650
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u306F\u3069\u3061\u3089\u304B1\u3064\u3060\u3051\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4E21\u65B9ON/\u4E21\u65B9OFF\u306F\u7121\u52B9\u3067\u3059\uFF09\u3002",
|
|
1651
|
+
"error",
|
|
1652
|
+
deltaPath,
|
|
1653
|
+
"delta.classification"
|
|
1654
|
+
)
|
|
1655
|
+
);
|
|
1507
1656
|
}
|
|
1508
1657
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1658
|
+
return issues;
|
|
1659
|
+
}
|
|
1660
|
+
function isMissingFileError2(error2) {
|
|
1661
|
+
if (!error2 || typeof error2 !== "object") {
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
return error2.code === "ENOENT";
|
|
1665
|
+
}
|
|
1666
|
+
function issue2(code, message, severity, file, rule, refs) {
|
|
1667
|
+
const issue7 = {
|
|
1668
|
+
code,
|
|
1669
|
+
severity,
|
|
1670
|
+
message
|
|
1515
1671
|
};
|
|
1516
|
-
if (
|
|
1517
|
-
|
|
1672
|
+
if (file) {
|
|
1673
|
+
issue7.file = file;
|
|
1518
1674
|
}
|
|
1519
|
-
|
|
1675
|
+
if (rule) {
|
|
1676
|
+
issue7.rule = rule;
|
|
1677
|
+
}
|
|
1678
|
+
if (refs && refs.length > 0) {
|
|
1679
|
+
issue7.refs = refs;
|
|
1680
|
+
}
|
|
1681
|
+
return issue7;
|
|
1520
1682
|
}
|
|
1521
1683
|
|
|
1522
1684
|
// src/core/validators/ids.ts
|
|
1685
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1686
|
+
import path13 from "path";
|
|
1523
1687
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1524
1688
|
async function validateDefinedIds(root, config) {
|
|
1525
1689
|
const issues = [];
|
|
@@ -1585,7 +1749,7 @@ function recordId(out, id, file) {
|
|
|
1585
1749
|
}
|
|
1586
1750
|
function formatFileList(files, root) {
|
|
1587
1751
|
return files.map((file) => {
|
|
1588
|
-
const relative =
|
|
1752
|
+
const relative = path13.relative(root, file);
|
|
1589
1753
|
return relative.length > 0 ? relative : file;
|
|
1590
1754
|
}).join(", ");
|
|
1591
1755
|
}
|
|
@@ -1614,7 +1778,6 @@ var WHEN_PATTERN = /\bWhen\b/;
|
|
|
1614
1778
|
var THEN_PATTERN = /\bThen\b/;
|
|
1615
1779
|
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1616
1780
|
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1617
|
-
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1618
1781
|
async function validateScenarios(root, config) {
|
|
1619
1782
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1620
1783
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -1663,7 +1826,7 @@ function validateScenarioContent(text, file) {
|
|
|
1663
1826
|
"SC",
|
|
1664
1827
|
"UI",
|
|
1665
1828
|
"API",
|
|
1666
|
-
"
|
|
1829
|
+
"DB",
|
|
1667
1830
|
"ADR"
|
|
1668
1831
|
]);
|
|
1669
1832
|
if (invalidIds.length > 0) {
|
|
@@ -1694,17 +1857,7 @@ function validateScenarioContent(text, file) {
|
|
|
1694
1857
|
const featureSpecTags = document.featureTags.filter(
|
|
1695
1858
|
(tag) => SPEC_TAG_RE2.test(tag)
|
|
1696
1859
|
);
|
|
1697
|
-
if (featureSpecTags.length
|
|
1698
|
-
issues.push(
|
|
1699
|
-
issue4(
|
|
1700
|
-
"QFAI-SC-009",
|
|
1701
|
-
"Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1702
|
-
"error",
|
|
1703
|
-
file,
|
|
1704
|
-
"scenario.featureSpec"
|
|
1705
|
-
)
|
|
1706
|
-
);
|
|
1707
|
-
} else if (featureSpecTags.length > 1) {
|
|
1860
|
+
if (featureSpecTags.length > 1) {
|
|
1708
1861
|
issues.push(
|
|
1709
1862
|
issue4(
|
|
1710
1863
|
"QFAI-SC-009",
|
|
@@ -1732,17 +1885,6 @@ function validateScenarioContent(text, file) {
|
|
|
1732
1885
|
)
|
|
1733
1886
|
);
|
|
1734
1887
|
}
|
|
1735
|
-
if (document.scenarios.length > 1) {
|
|
1736
|
-
issues.push(
|
|
1737
|
-
issue4(
|
|
1738
|
-
"QFAI-SC-011",
|
|
1739
|
-
`Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
|
|
1740
|
-
"error",
|
|
1741
|
-
file,
|
|
1742
|
-
"scenario.single"
|
|
1743
|
-
)
|
|
1744
|
-
);
|
|
1745
|
-
}
|
|
1746
1888
|
for (const scenario of document.scenarios) {
|
|
1747
1889
|
if (scenario.tags.length === 0) {
|
|
1748
1890
|
issues.push(
|
|
@@ -1763,12 +1905,6 @@ function validateScenarioContent(text, file) {
|
|
|
1763
1905
|
} else if (scTags.length > 1) {
|
|
1764
1906
|
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1765
1907
|
}
|
|
1766
|
-
if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
|
|
1767
|
-
missingTags.push("SPEC");
|
|
1768
|
-
}
|
|
1769
|
-
if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
|
|
1770
|
-
missingTags.push("BR");
|
|
1771
|
-
}
|
|
1772
1908
|
if (missingTags.length > 0) {
|
|
1773
1909
|
issues.push(
|
|
1774
1910
|
issue4(
|
|
@@ -1888,7 +2024,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1888
2024
|
"SC",
|
|
1889
2025
|
"UI",
|
|
1890
2026
|
"API",
|
|
1891
|
-
"
|
|
2027
|
+
"DB",
|
|
1892
2028
|
"ADR"
|
|
1893
2029
|
]);
|
|
1894
2030
|
if (invalidIds.length > 0) {
|
|
@@ -2003,9 +2139,8 @@ function isMissingFileError4(error2) {
|
|
|
2003
2139
|
|
|
2004
2140
|
// src/core/validators/traceability.ts
|
|
2005
2141
|
import { readFile as readFile10 } from "fs/promises";
|
|
2006
|
-
var SC_TAG_RE5 = /^SC-\d{4}$/;
|
|
2007
2142
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
2008
|
-
var
|
|
2143
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
2009
2144
|
async function validateTraceability(root, config) {
|
|
2010
2145
|
const issues = [];
|
|
2011
2146
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -2018,8 +2153,7 @@ async function validateTraceability(root, config) {
|
|
|
2018
2153
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
2019
2154
|
const brIdsInScenarios = /* @__PURE__ */ new Set();
|
|
2020
2155
|
const scIdsInScenarios = /* @__PURE__ */ new Set();
|
|
2021
|
-
const
|
|
2022
|
-
const scWithContracts = /* @__PURE__ */ new Set();
|
|
2156
|
+
const specContractIds = /* @__PURE__ */ new Set();
|
|
2023
2157
|
const specToBrIds = /* @__PURE__ */ new Map();
|
|
2024
2158
|
const contractIndex = await buildContractIndex(root, config);
|
|
2025
2159
|
const contractIds = contractIndex.ids;
|
|
@@ -2032,19 +2166,60 @@ async function validateTraceability(root, config) {
|
|
|
2032
2166
|
}
|
|
2033
2167
|
const brIds = parsed.brs.map((br) => br.id);
|
|
2034
2168
|
brIds.forEach((id) => brIdsInSpecs.add(id));
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
const
|
|
2169
|
+
if (parsed.specId) {
|
|
2170
|
+
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
2171
|
+
brIds.forEach((id) => current.add(id));
|
|
2172
|
+
specToBrIds.set(parsed.specId, current);
|
|
2173
|
+
}
|
|
2174
|
+
const contractRefs = parsed.contractRefs;
|
|
2175
|
+
if (contractRefs.lines.length === 0) {
|
|
2176
|
+
issues.push(
|
|
2177
|
+
issue6(
|
|
2178
|
+
"QFAI-TRACE-020",
|
|
2179
|
+
"Spec \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
2180
|
+
"error",
|
|
2181
|
+
file,
|
|
2182
|
+
"traceability.specContractRefRequired"
|
|
2183
|
+
)
|
|
2184
|
+
);
|
|
2185
|
+
} else {
|
|
2186
|
+
if (contractRefs.hasNone && contractRefs.ids.length > 0) {
|
|
2187
|
+
issues.push(
|
|
2188
|
+
issue6(
|
|
2189
|
+
"QFAI-TRACE-021",
|
|
2190
|
+
"Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2191
|
+
"error",
|
|
2192
|
+
file,
|
|
2193
|
+
"traceability.specContractRefFormat"
|
|
2194
|
+
)
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
if (contractRefs.invalidTokens.length > 0) {
|
|
2198
|
+
issues.push(
|
|
2199
|
+
issue6(
|
|
2200
|
+
"QFAI-TRACE-021",
|
|
2201
|
+
`Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
|
|
2202
|
+
", "
|
|
2203
|
+
)}`,
|
|
2204
|
+
"error",
|
|
2205
|
+
file,
|
|
2206
|
+
"traceability.specContractRefFormat",
|
|
2207
|
+
contractRefs.invalidTokens
|
|
2208
|
+
)
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
contractRefs.ids.forEach((id) => {
|
|
2213
|
+
specContractIds.add(id);
|
|
2214
|
+
});
|
|
2215
|
+
const unknownContractIds = contractRefs.ids.filter(
|
|
2041
2216
|
(id) => !contractIds.has(id)
|
|
2042
2217
|
);
|
|
2043
2218
|
if (unknownContractIds.length > 0) {
|
|
2044
2219
|
issues.push(
|
|
2045
2220
|
issue6(
|
|
2046
|
-
"QFAI-TRACE-
|
|
2047
|
-
`Spec \u304C\
|
|
2221
|
+
"QFAI-TRACE-021",
|
|
2222
|
+
`Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
2048
2223
|
", "
|
|
2049
2224
|
)}`,
|
|
2050
2225
|
"error",
|
|
@@ -2054,11 +2229,6 @@ async function validateTraceability(root, config) {
|
|
|
2054
2229
|
)
|
|
2055
2230
|
);
|
|
2056
2231
|
}
|
|
2057
|
-
if (parsed.specId) {
|
|
2058
|
-
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
2059
|
-
brIds.forEach((id) => current.add(id));
|
|
2060
|
-
specToBrIds.set(parsed.specId, current);
|
|
2061
|
-
}
|
|
2062
2232
|
}
|
|
2063
2233
|
for (const file of scenarioFiles) {
|
|
2064
2234
|
const text = await readFile10(file, "utf-8");
|
|
@@ -2068,20 +2238,42 @@ async function validateTraceability(root, config) {
|
|
|
2068
2238
|
continue;
|
|
2069
2239
|
}
|
|
2070
2240
|
const atoms = buildScenarioAtoms(document);
|
|
2241
|
+
const scIdsInFile = /* @__PURE__ */ new Set();
|
|
2071
2242
|
for (const [index, scenario] of document.scenarios.entries()) {
|
|
2072
2243
|
const atom = atoms[index];
|
|
2073
2244
|
if (!atom) {
|
|
2074
2245
|
continue;
|
|
2075
2246
|
}
|
|
2076
2247
|
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
2077
|
-
const brTags = scenario.tags.filter((tag) =>
|
|
2078
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2248
|
+
const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
|
|
2249
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
|
|
2250
|
+
if (specTags.length === 0) {
|
|
2251
|
+
issues.push(
|
|
2252
|
+
issue6(
|
|
2253
|
+
"QFAI-TRACE-014",
|
|
2254
|
+
`Scenario \u304C SPEC \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
|
|
2255
|
+
"error",
|
|
2256
|
+
file,
|
|
2257
|
+
"traceability.scenarioSpecRequired"
|
|
2258
|
+
)
|
|
2259
|
+
);
|
|
2260
|
+
}
|
|
2261
|
+
if (brTags.length === 0) {
|
|
2262
|
+
issues.push(
|
|
2263
|
+
issue6(
|
|
2264
|
+
"QFAI-TRACE-015",
|
|
2265
|
+
`Scenario \u304C BR \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
|
|
2266
|
+
"error",
|
|
2267
|
+
file,
|
|
2268
|
+
"traceability.scenarioBrRequired"
|
|
2269
|
+
)
|
|
2270
|
+
);
|
|
2084
2271
|
}
|
|
2272
|
+
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
2273
|
+
scTags.forEach((id) => {
|
|
2274
|
+
scIdsInScenarios.add(id);
|
|
2275
|
+
scIdsInFile.add(id);
|
|
2276
|
+
});
|
|
2085
2277
|
const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
|
|
2086
2278
|
if (unknownSpecIds.length > 0) {
|
|
2087
2279
|
issues.push(
|
|
@@ -2155,6 +2347,22 @@ async function validateTraceability(root, config) {
|
|
|
2155
2347
|
}
|
|
2156
2348
|
}
|
|
2157
2349
|
}
|
|
2350
|
+
if (scIdsInFile.size !== 1) {
|
|
2351
|
+
const invalidScIds = Array.from(scIdsInFile).sort(
|
|
2352
|
+
(a, b) => a.localeCompare(b)
|
|
2353
|
+
);
|
|
2354
|
+
const detail = invalidScIds.length === 0 ? "SC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" : `\u8907\u6570\u306E SC \u304C\u5B58\u5728\u3057\u307E\u3059: ${invalidScIds.join(", ")}`;
|
|
2355
|
+
issues.push(
|
|
2356
|
+
issue6(
|
|
2357
|
+
"QFAI-TRACE-012",
|
|
2358
|
+
`Spec entry \u304C Spec:SC=1:1 \u3092\u6E80\u305F\u3057\u3066\u3044\u307E\u305B\u3093: ${detail}`,
|
|
2359
|
+
"error",
|
|
2360
|
+
file,
|
|
2361
|
+
"traceability.specScOneToOne",
|
|
2362
|
+
invalidScIds
|
|
2363
|
+
)
|
|
2364
|
+
);
|
|
2365
|
+
}
|
|
2158
2366
|
}
|
|
2159
2367
|
if (upstreamIds.size === 0) {
|
|
2160
2368
|
return [
|
|
@@ -2184,40 +2392,62 @@ async function validateTraceability(root, config) {
|
|
|
2184
2392
|
);
|
|
2185
2393
|
}
|
|
2186
2394
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2395
|
+
const scRefsResult = await collectScTestReferences(
|
|
2396
|
+
root,
|
|
2397
|
+
config.validation.traceability.testFileGlobs,
|
|
2398
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2399
|
+
);
|
|
2400
|
+
const scTestRefs = scRefsResult.refs;
|
|
2401
|
+
const testFileScan = scRefsResult.scan;
|
|
2402
|
+
const hasScenarios = scIdsInScenarios.size > 0;
|
|
2403
|
+
const hasGlobConfig = testFileScan.globs.length > 0;
|
|
2404
|
+
const hasMatchedTests = testFileScan.matchedFileCount > 0;
|
|
2405
|
+
if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
|
|
2406
|
+
const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
|
|
2407
|
+
issues.push(
|
|
2408
|
+
issue6(
|
|
2409
|
+
"QFAI-TRACE-013",
|
|
2410
|
+
`\u30C6\u30B9\u30C8\u63A2\u7D22 glob \u304C\u672A\u8A2D\u5B9A/\u4E0D\u6B63/\u4E00\u81F4\u30D5\u30A1\u30A4\u30EB0\u306E\u305F\u3081 SC\u2192Test \u3092\u5224\u5B9A\u3067\u304D\u307E\u305B\u3093\u3002${detail}`,
|
|
2411
|
+
"error",
|
|
2412
|
+
testsRoot,
|
|
2413
|
+
"traceability.testFileGlobs"
|
|
2414
|
+
)
|
|
2415
|
+
);
|
|
2416
|
+
} else {
|
|
2417
|
+
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
2418
|
+
const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
|
|
2419
|
+
const refs = scTestRefs.get(id);
|
|
2420
|
+
return !refs || refs.size === 0;
|
|
2421
|
+
});
|
|
2422
|
+
if (scWithoutTests.length > 0) {
|
|
2423
|
+
issues.push(
|
|
2424
|
+
issue6(
|
|
2425
|
+
"QFAI-TRACE-010",
|
|
2426
|
+
`SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(
|
|
2427
|
+
", "
|
|
2428
|
+
)}\u3002testFileGlobs \u306B\u4E00\u81F4\u3059\u308B\u30C6\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\u3078 QFAI:SC-xxxx \u3092\u8A18\u8F09\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
|
|
2429
|
+
config.validation.traceability.scNoTestSeverity,
|
|
2430
|
+
testsRoot,
|
|
2431
|
+
"traceability.scMustHaveTest",
|
|
2432
|
+
scWithoutTests
|
|
2433
|
+
)
|
|
2434
|
+
);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
const unknownScIds = Array.from(scTestRefs.keys()).filter(
|
|
2438
|
+
(id) => !scIdsInScenarios.has(id)
|
|
2190
2439
|
);
|
|
2191
|
-
if (
|
|
2440
|
+
if (unknownScIds.length > 0) {
|
|
2192
2441
|
issues.push(
|
|
2193
2442
|
issue6(
|
|
2194
|
-
"
|
|
2195
|
-
|
|
2443
|
+
"QFAI-TRACE-011",
|
|
2444
|
+
`\u30C6\u30B9\u30C8\u304C\u672A\u77E5\u306E SC \u3092\u30A2\u30CE\u30C6\u30FC\u30B7\u30E7\u30F3\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownScIds.join(
|
|
2196
2445
|
", "
|
|
2197
2446
|
)}`,
|
|
2198
2447
|
"error",
|
|
2199
|
-
specsRoot,
|
|
2200
|
-
"traceability.scMustTouchContracts",
|
|
2201
|
-
scWithoutContracts
|
|
2202
|
-
)
|
|
2203
|
-
);
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
2207
|
-
const scTestRefs = await collectScTestReferences(testsRoot);
|
|
2208
|
-
const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
|
|
2209
|
-
const refs = scTestRefs.get(id);
|
|
2210
|
-
return !refs || refs.size === 0;
|
|
2211
|
-
});
|
|
2212
|
-
if (scWithoutTests.length > 0) {
|
|
2213
|
-
issues.push(
|
|
2214
|
-
issue6(
|
|
2215
|
-
"QFAI-TRACE-010",
|
|
2216
|
-
`SC \u304C tests \u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(", ")}\u3002tests/ \u914D\u4E0B\u306E\u30C6\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\uFF08.ts/.tsx/.js/.jsx\uFF09\u306B SC ID \u3092\u30B3\u30E1\u30F3\u30C8\u307E\u305F\u306F\u30B3\u30FC\u30C9\u3067\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
|
|
2217
|
-
config.validation.traceability.scNoTestSeverity,
|
|
2218
2448
|
testsRoot,
|
|
2219
|
-
"traceability.
|
|
2220
|
-
|
|
2449
|
+
"traceability.scUnknownInTests",
|
|
2450
|
+
unknownScIds
|
|
2221
2451
|
)
|
|
2222
2452
|
);
|
|
2223
2453
|
}
|
|
@@ -2225,16 +2455,16 @@ async function validateTraceability(root, config) {
|
|
|
2225
2455
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
2226
2456
|
if (contractIds.size > 0) {
|
|
2227
2457
|
const orphanContracts = Array.from(contractIds).filter(
|
|
2228
|
-
(id) => !
|
|
2458
|
+
(id) => !specContractIds.has(id)
|
|
2229
2459
|
);
|
|
2230
2460
|
if (orphanContracts.length > 0) {
|
|
2231
2461
|
issues.push(
|
|
2232
2462
|
issue6(
|
|
2233
|
-
"
|
|
2234
|
-
`\u5951\u7D04\u304C
|
|
2463
|
+
"QFAI-TRACE-022",
|
|
2464
|
+
`\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
2235
2465
|
"error",
|
|
2236
2466
|
specsRoot,
|
|
2237
|
-
"traceability.
|
|
2467
|
+
"traceability.contractCoverage",
|
|
2238
2468
|
orphanContracts
|
|
2239
2469
|
)
|
|
2240
2470
|
);
|
|
@@ -2280,8 +2510,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2280
2510
|
issues.push(
|
|
2281
2511
|
issue6(
|
|
2282
2512
|
"QFAI-TRACE-002",
|
|
2283
|
-
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
2284
|
-
"
|
|
2513
|
+
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\uFF08\u53C2\u8003\u60C5\u5831\uFF09\u3002",
|
|
2514
|
+
"info",
|
|
2285
2515
|
srcRoot,
|
|
2286
2516
|
"traceability.codeReferences"
|
|
2287
2517
|
)
|
|
@@ -2324,11 +2554,24 @@ async function validateProject(root, configResult) {
|
|
|
2324
2554
|
...await validateDefinedIds(root, config),
|
|
2325
2555
|
...await validateTraceability(root, config)
|
|
2326
2556
|
];
|
|
2557
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2558
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
2559
|
+
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2560
|
+
const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
|
|
2561
|
+
root,
|
|
2562
|
+
config.validation.traceability.testFileGlobs,
|
|
2563
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2564
|
+
);
|
|
2565
|
+
const scCoverage = buildScCoverage(scIds, scTestRefs);
|
|
2327
2566
|
const toolVersion = await resolveToolVersion();
|
|
2328
2567
|
return {
|
|
2329
2568
|
toolVersion,
|
|
2330
2569
|
issues,
|
|
2331
|
-
counts: countIssues(issues)
|
|
2570
|
+
counts: countIssues(issues),
|
|
2571
|
+
traceability: {
|
|
2572
|
+
sc: scCoverage,
|
|
2573
|
+
testFiles
|
|
2574
|
+
}
|
|
2332
2575
|
};
|
|
2333
2576
|
}
|
|
2334
2577
|
function countIssues(issues) {
|
|
@@ -2342,16 +2585,16 @@ function countIssues(issues) {
|
|
|
2342
2585
|
}
|
|
2343
2586
|
|
|
2344
2587
|
// src/core/report.ts
|
|
2345
|
-
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "
|
|
2588
|
+
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2346
2589
|
async function createReportData(root, validation, configResult) {
|
|
2347
2590
|
const resolved = configResult ?? await loadConfig(root);
|
|
2348
2591
|
const config = resolved.config;
|
|
2349
2592
|
const configPath = resolved.configPath;
|
|
2350
2593
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2351
2594
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2352
|
-
const apiRoot =
|
|
2353
|
-
const uiRoot =
|
|
2354
|
-
const dbRoot =
|
|
2595
|
+
const apiRoot = path14.join(contractsRoot, "api");
|
|
2596
|
+
const uiRoot = path14.join(contractsRoot, "ui");
|
|
2597
|
+
const dbRoot = path14.join(contractsRoot, "db");
|
|
2355
2598
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
2356
2599
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
2357
2600
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -2361,6 +2604,23 @@ async function createReportData(root, validation, configResult) {
|
|
|
2361
2604
|
ui: uiFiles,
|
|
2362
2605
|
db: dbFiles
|
|
2363
2606
|
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
2607
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
2608
|
+
const specContractRefs = await collectSpecContractRefs(specFiles);
|
|
2609
|
+
const contractIdList = Array.from(contractIndex.ids);
|
|
2610
|
+
const referencedContracts = /* @__PURE__ */ new Set();
|
|
2611
|
+
for (const ids of specContractRefs.specToContractIds.values()) {
|
|
2612
|
+
ids.forEach((id) => referencedContracts.add(id));
|
|
2613
|
+
}
|
|
2614
|
+
const referencedContractCount = contractIdList.filter(
|
|
2615
|
+
(id) => referencedContracts.has(id)
|
|
2616
|
+
).length;
|
|
2617
|
+
const orphanContractCount = contractIdList.filter(
|
|
2618
|
+
(id) => !referencedContracts.has(id)
|
|
2619
|
+
).length;
|
|
2620
|
+
const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
|
|
2621
|
+
const specToContractIdsRecord = mapToSortedRecord(
|
|
2622
|
+
specContractRefs.specToContractIds
|
|
2623
|
+
);
|
|
2364
2624
|
const idsByPrefix = await collectIds([
|
|
2365
2625
|
...specFiles,
|
|
2366
2626
|
...scenarioFiles,
|
|
@@ -2378,8 +2638,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
2378
2638
|
testsRoot
|
|
2379
2639
|
);
|
|
2380
2640
|
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2381
|
-
const
|
|
2382
|
-
|
|
2641
|
+
const scRefsResult = await collectScTestReferences(
|
|
2642
|
+
root,
|
|
2643
|
+
config.validation.traceability.testFileGlobs,
|
|
2644
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2645
|
+
);
|
|
2646
|
+
const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
|
|
2647
|
+
const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
|
|
2648
|
+
const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
|
|
2649
|
+
const scSourceRecord = mapToSortedRecord(scSources);
|
|
2383
2650
|
const resolvedValidation = validation ?? await validateProject(root, resolved);
|
|
2384
2651
|
const version = await resolveToolVersion();
|
|
2385
2652
|
return {
|
|
@@ -2404,12 +2671,24 @@ async function createReportData(root, validation, configResult) {
|
|
|
2404
2671
|
sc: idsByPrefix.SC,
|
|
2405
2672
|
ui: idsByPrefix.UI,
|
|
2406
2673
|
api: idsByPrefix.API,
|
|
2407
|
-
|
|
2674
|
+
db: idsByPrefix.DB
|
|
2408
2675
|
},
|
|
2409
2676
|
traceability: {
|
|
2410
2677
|
upstreamIdsFound: upstreamIds.size,
|
|
2411
2678
|
referencedInCodeOrTests: traceability,
|
|
2412
|
-
sc: scCoverage
|
|
2679
|
+
sc: scCoverage,
|
|
2680
|
+
scSources: scSourceRecord,
|
|
2681
|
+
testFiles,
|
|
2682
|
+
contracts: {
|
|
2683
|
+
total: contractIdList.length,
|
|
2684
|
+
referenced: referencedContractCount,
|
|
2685
|
+
orphan: orphanContractCount,
|
|
2686
|
+
idToSpecs: contractIdToSpecsRecord
|
|
2687
|
+
},
|
|
2688
|
+
specs: {
|
|
2689
|
+
contractRefMissing: specContractRefs.missingRefSpecs.size,
|
|
2690
|
+
specToContractIds: specToContractIdsRecord
|
|
2691
|
+
}
|
|
2413
2692
|
},
|
|
2414
2693
|
issues: resolvedValidation.issues
|
|
2415
2694
|
};
|
|
@@ -2438,7 +2717,7 @@ function formatReportMarkdown(data) {
|
|
|
2438
2717
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
2439
2718
|
lines.push(formatIdLine("UI", data.ids.ui));
|
|
2440
2719
|
lines.push(formatIdLine("API", data.ids.api));
|
|
2441
|
-
lines.push(formatIdLine("
|
|
2720
|
+
lines.push(formatIdLine("DB", data.ids.db));
|
|
2442
2721
|
lines.push("");
|
|
2443
2722
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
2444
2723
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
@@ -2446,14 +2725,77 @@ function formatReportMarkdown(data) {
|
|
|
2446
2725
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2447
2726
|
);
|
|
2448
2727
|
lines.push("");
|
|
2728
|
+
lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2729
|
+
lines.push(`- total: ${data.traceability.contracts.total}`);
|
|
2730
|
+
lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
|
|
2731
|
+
lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
|
|
2732
|
+
lines.push(
|
|
2733
|
+
`- specContractRefMissing: ${data.traceability.specs.contractRefMissing}`
|
|
2734
|
+
);
|
|
2735
|
+
lines.push("");
|
|
2736
|
+
lines.push("## \u5951\u7D04\u2192Spec");
|
|
2737
|
+
const contractToSpecs = data.traceability.contracts.idToSpecs;
|
|
2738
|
+
const contractIds = Object.keys(contractToSpecs).sort(
|
|
2739
|
+
(a, b) => a.localeCompare(b)
|
|
2740
|
+
);
|
|
2741
|
+
if (contractIds.length === 0) {
|
|
2742
|
+
lines.push("- (none)");
|
|
2743
|
+
} else {
|
|
2744
|
+
for (const contractId of contractIds) {
|
|
2745
|
+
const specs = contractToSpecs[contractId] ?? [];
|
|
2746
|
+
if (specs.length === 0) {
|
|
2747
|
+
lines.push(`- ${contractId}: (none)`);
|
|
2748
|
+
} else {
|
|
2749
|
+
lines.push(`- ${contractId}: ${specs.join(", ")}`);
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
lines.push("");
|
|
2754
|
+
lines.push("## Spec\u2192\u5951\u7D04");
|
|
2755
|
+
const specToContracts = data.traceability.specs.specToContractIds;
|
|
2756
|
+
const specIds = Object.keys(specToContracts).sort(
|
|
2757
|
+
(a, b) => a.localeCompare(b)
|
|
2758
|
+
);
|
|
2759
|
+
if (specIds.length === 0) {
|
|
2760
|
+
lines.push("- (none)");
|
|
2761
|
+
} else {
|
|
2762
|
+
for (const specId of specIds) {
|
|
2763
|
+
const contractIds2 = specToContracts[specId] ?? [];
|
|
2764
|
+
if (contractIds2.length === 0) {
|
|
2765
|
+
lines.push(`- ${specId}: (none)`);
|
|
2766
|
+
} else {
|
|
2767
|
+
lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
lines.push("");
|
|
2449
2772
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2450
2773
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2451
2774
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2452
2775
|
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
2776
|
+
lines.push(
|
|
2777
|
+
`- testFileGlobs: ${formatList(data.traceability.testFiles.globs)}`
|
|
2778
|
+
);
|
|
2779
|
+
lines.push(
|
|
2780
|
+
`- testFileExcludeGlobs: ${formatList(
|
|
2781
|
+
data.traceability.testFiles.excludeGlobs
|
|
2782
|
+
)}`
|
|
2783
|
+
);
|
|
2784
|
+
lines.push(
|
|
2785
|
+
`- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
|
|
2786
|
+
);
|
|
2453
2787
|
if (data.traceability.sc.missingIds.length === 0) {
|
|
2454
2788
|
lines.push("- missingIds: (none)");
|
|
2455
2789
|
} else {
|
|
2456
|
-
|
|
2790
|
+
const sources = data.traceability.scSources;
|
|
2791
|
+
const missingWithSources = data.traceability.sc.missingIds.map((id) => {
|
|
2792
|
+
const files = sources[id] ?? [];
|
|
2793
|
+
if (files.length === 0) {
|
|
2794
|
+
return id;
|
|
2795
|
+
}
|
|
2796
|
+
return `${id} (${files.join(", ")})`;
|
|
2797
|
+
});
|
|
2798
|
+
lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
|
|
2457
2799
|
}
|
|
2458
2800
|
lines.push("");
|
|
2459
2801
|
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
@@ -2472,6 +2814,20 @@ function formatReportMarkdown(data) {
|
|
|
2472
2814
|
}
|
|
2473
2815
|
}
|
|
2474
2816
|
lines.push("");
|
|
2817
|
+
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
2818
|
+
const specScIssues = data.issues.filter(
|
|
2819
|
+
(item) => item.code === "QFAI-TRACE-012"
|
|
2820
|
+
);
|
|
2821
|
+
if (specScIssues.length === 0) {
|
|
2822
|
+
lines.push("- (none)");
|
|
2823
|
+
} else {
|
|
2824
|
+
for (const item of specScIssues) {
|
|
2825
|
+
const location = item.file ?? "(unknown)";
|
|
2826
|
+
const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
|
|
2827
|
+
lines.push(`- ${location}: ${refs}`);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
lines.push("");
|
|
2475
2831
|
lines.push("## Hotspots");
|
|
2476
2832
|
const hotspots = buildHotspots(data.issues);
|
|
2477
2833
|
if (hotspots.length === 0) {
|
|
@@ -2486,7 +2842,7 @@ function formatReportMarkdown(data) {
|
|
|
2486
2842
|
lines.push("");
|
|
2487
2843
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
2488
2844
|
const traceIssues = data.issues.filter(
|
|
2489
|
-
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2845
|
+
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2490
2846
|
);
|
|
2491
2847
|
if (traceIssues.length === 0) {
|
|
2492
2848
|
lines.push("- (none)");
|
|
@@ -2516,6 +2872,33 @@ function formatReportMarkdown(data) {
|
|
|
2516
2872
|
function formatReportJson(data) {
|
|
2517
2873
|
return JSON.stringify(data, null, 2);
|
|
2518
2874
|
}
|
|
2875
|
+
async function collectSpecContractRefs(specFiles) {
|
|
2876
|
+
const specToContractIds = /* @__PURE__ */ new Map();
|
|
2877
|
+
const idToSpecs = /* @__PURE__ */ new Map();
|
|
2878
|
+
const missingRefSpecs = /* @__PURE__ */ new Set();
|
|
2879
|
+
for (const file of specFiles) {
|
|
2880
|
+
const text = await readFile11(file, "utf-8");
|
|
2881
|
+
const parsed = parseSpec(text, file);
|
|
2882
|
+
const specKey = parsed.specId ?? file;
|
|
2883
|
+
const refs = parsed.contractRefs;
|
|
2884
|
+
if (refs.lines.length === 0) {
|
|
2885
|
+
missingRefSpecs.add(specKey);
|
|
2886
|
+
}
|
|
2887
|
+
const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
|
|
2888
|
+
for (const id of refs.ids) {
|
|
2889
|
+
currentContracts.add(id);
|
|
2890
|
+
const specs = idToSpecs.get(id) ?? /* @__PURE__ */ new Set();
|
|
2891
|
+
specs.add(specKey);
|
|
2892
|
+
idToSpecs.set(id, specs);
|
|
2893
|
+
}
|
|
2894
|
+
specToContractIds.set(specKey, currentContracts);
|
|
2895
|
+
}
|
|
2896
|
+
return {
|
|
2897
|
+
specToContractIds,
|
|
2898
|
+
idToSpecs,
|
|
2899
|
+
missingRefSpecs
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2519
2902
|
async function collectIds(files) {
|
|
2520
2903
|
const result = {
|
|
2521
2904
|
SPEC: /* @__PURE__ */ new Set(),
|
|
@@ -2523,7 +2906,7 @@ async function collectIds(files) {
|
|
|
2523
2906
|
SC: /* @__PURE__ */ new Set(),
|
|
2524
2907
|
UI: /* @__PURE__ */ new Set(),
|
|
2525
2908
|
API: /* @__PURE__ */ new Set(),
|
|
2526
|
-
|
|
2909
|
+
DB: /* @__PURE__ */ new Set()
|
|
2527
2910
|
};
|
|
2528
2911
|
for (const file of files) {
|
|
2529
2912
|
const text = await readFile11(file, "utf-8");
|
|
@@ -2538,7 +2921,7 @@ async function collectIds(files) {
|
|
|
2538
2921
|
SC: toSortedArray2(result.SC),
|
|
2539
2922
|
UI: toSortedArray2(result.UI),
|
|
2540
2923
|
API: toSortedArray2(result.API),
|
|
2541
|
-
|
|
2924
|
+
DB: toSortedArray2(result.DB)
|
|
2542
2925
|
};
|
|
2543
2926
|
}
|
|
2544
2927
|
async function collectUpstreamIds(files) {
|
|
@@ -2582,9 +2965,22 @@ function formatIdLine(label, values) {
|
|
|
2582
2965
|
}
|
|
2583
2966
|
return `- ${label}: ${values.join(", ")}`;
|
|
2584
2967
|
}
|
|
2968
|
+
function formatList(values) {
|
|
2969
|
+
if (values.length === 0) {
|
|
2970
|
+
return "(none)";
|
|
2971
|
+
}
|
|
2972
|
+
return values.join(", ");
|
|
2973
|
+
}
|
|
2585
2974
|
function toSortedArray2(values) {
|
|
2586
2975
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2587
2976
|
}
|
|
2977
|
+
function mapToSortedRecord(values) {
|
|
2978
|
+
const record2 = {};
|
|
2979
|
+
for (const [key, files] of values.entries()) {
|
|
2980
|
+
record2[key] = Array.from(files).sort((a, b) => a.localeCompare(b));
|
|
2981
|
+
}
|
|
2982
|
+
return record2;
|
|
2983
|
+
}
|
|
2588
2984
|
function buildHotspots(issues) {
|
|
2589
2985
|
const map = /* @__PURE__ */ new Map();
|
|
2590
2986
|
for (const issue7 of issues) {
|
|
@@ -2609,10 +3005,10 @@ function buildHotspots(issues) {
|
|
|
2609
3005
|
|
|
2610
3006
|
// src/cli/commands/report.ts
|
|
2611
3007
|
async function runReport(options) {
|
|
2612
|
-
const root =
|
|
3008
|
+
const root = path15.resolve(options.root);
|
|
2613
3009
|
const configResult = await loadConfig(root);
|
|
2614
3010
|
const input = configResult.config.output.validateJsonPath;
|
|
2615
|
-
const inputPath =
|
|
3011
|
+
const inputPath = path15.isAbsolute(input) ? input : path15.resolve(root, input);
|
|
2616
3012
|
let validation;
|
|
2617
3013
|
try {
|
|
2618
3014
|
validation = await readValidationResult(inputPath);
|
|
@@ -2637,10 +3033,10 @@ async function runReport(options) {
|
|
|
2637
3033
|
const data = await createReportData(root, validation, configResult);
|
|
2638
3034
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
2639
3035
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
2640
|
-
const defaultOut = options.format === "json" ?
|
|
3036
|
+
const defaultOut = options.format === "json" ? path15.join(outRoot, "report.json") : path15.join(outRoot, "report.md");
|
|
2641
3037
|
const out = options.outPath ?? defaultOut;
|
|
2642
|
-
const outPath =
|
|
2643
|
-
await mkdir2(
|
|
3038
|
+
const outPath = path15.isAbsolute(out) ? out : path15.resolve(root, out);
|
|
3039
|
+
await mkdir2(path15.dirname(outPath), { recursive: true });
|
|
2644
3040
|
await writeFile(outPath, `${output}
|
|
2645
3041
|
`, "utf-8");
|
|
2646
3042
|
info(
|
|
@@ -2683,7 +3079,7 @@ function isMissingFileError5(error2) {
|
|
|
2683
3079
|
|
|
2684
3080
|
// src/cli/commands/validate.ts
|
|
2685
3081
|
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
2686
|
-
import
|
|
3082
|
+
import path16 from "path";
|
|
2687
3083
|
|
|
2688
3084
|
// src/cli/lib/failOn.ts
|
|
2689
3085
|
function shouldFail(result, failOn) {
|
|
@@ -2698,7 +3094,7 @@ function shouldFail(result, failOn) {
|
|
|
2698
3094
|
|
|
2699
3095
|
// src/cli/commands/validate.ts
|
|
2700
3096
|
async function runValidate(options) {
|
|
2701
|
-
const root =
|
|
3097
|
+
const root = path16.resolve(options.root);
|
|
2702
3098
|
const configResult = await loadConfig(root);
|
|
2703
3099
|
const result = await validateProject(root, configResult);
|
|
2704
3100
|
const format = options.format ?? "text";
|
|
@@ -2747,8 +3143,8 @@ function emitGitHub(issue7) {
|
|
|
2747
3143
|
);
|
|
2748
3144
|
}
|
|
2749
3145
|
async function emitJson(result, root, jsonPath) {
|
|
2750
|
-
const abs =
|
|
2751
|
-
await mkdir3(
|
|
3146
|
+
const abs = path16.isAbsolute(jsonPath) ? jsonPath : path16.resolve(root, jsonPath);
|
|
3147
|
+
await mkdir3(path16.dirname(abs), { recursive: true });
|
|
2752
3148
|
await writeFile2(abs, `${JSON.stringify(result, null, 2)}
|
|
2753
3149
|
`, "utf-8");
|
|
2754
3150
|
}
|