qfai 0.4.0 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/assets/init/.qfai/README.md +3 -1
- package/assets/init/.qfai/contracts/README.md +17 -8
- package/assets/init/.qfai/contracts/api/api-0001-sample.yaml +3 -2
- package/assets/init/.qfai/contracts/db/db-0001-sample.sql +2 -1
- package/assets/init/.qfai/contracts/ui/ui-0001-sample.yaml +3 -1
- package/assets/init/.qfai/promptpack/modes/change.md +3 -2
- package/assets/init/.qfai/promptpack/modes/compatibility.md +2 -0
- package/assets/init/.qfai/prompts/README.md +1 -0
- package/assets/init/.qfai/prompts/qfai-generate-test-globs.md +29 -0
- package/assets/init/.qfai/prompts/require-to-spec.md +4 -2
- package/assets/init/.qfai/specs/README.md +9 -2
- package/assets/init/.qfai/specs/spec-0001/spec.md +2 -0
- package/assets/init/root/qfai.config.yaml +6 -1
- package/assets/init/root/tests/qfai-traceability.sample.test.ts +2 -2
- package/dist/cli/index.cjs +885 -489
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +885 -489
- package/dist/cli/index.mjs.map +1 -1
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +4 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/contractIndex.d.ts +1 -2
- package/dist/core/contractIndex.d.ts.map +1 -1
- package/dist/core/contractIndex.js +10 -38
- package/dist/core/contractIndex.js.map +1 -1
- package/dist/core/contractsDecl.d.ts +3 -0
- package/dist/core/contractsDecl.d.ts.map +1 -0
- package/dist/core/contractsDecl.js +19 -0
- package/dist/core/contractsDecl.js.map +1 -0
- package/dist/core/fs.d.ts +5 -0
- package/dist/core/fs.d.ts.map +1 -1
- package/dist/core/fs.js +13 -0
- package/dist/core/fs.js.map +1 -1
- package/dist/core/ids.d.ts +1 -1
- package/dist/core/ids.d.ts.map +1 -1
- package/dist/core/ids.js +3 -3
- package/dist/core/ids.js.map +1 -1
- package/dist/core/parse/spec.d.ts +8 -0
- package/dist/core/parse/spec.d.ts.map +1 -1
- package/dist/core/parse/spec.js +43 -0
- package/dist/core/parse/spec.js.map +1 -1
- package/dist/core/report.d.ts +16 -2
- package/dist/core/report.d.ts.map +1 -1
- package/dist/core/report.js +144 -11
- package/dist/core/report.js.map +1 -1
- package/dist/core/scenarioModel.d.ts.map +1 -1
- package/dist/core/scenarioModel.js +3 -5
- package/dist/core/scenarioModel.js.map +1 -1
- package/dist/core/traceability.d.ts +15 -1
- package/dist/core/traceability.d.ts.map +1 -1
- package/dist/core/traceability.js +96 -9
- package/dist/core/traceability.js.map +1 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/validate.d.ts.map +1 -1
- package/dist/core/validate.js +12 -1
- package/dist/core/validate.js.map +1 -1
- package/dist/core/validators/contracts.d.ts.map +1 -1
- package/dist/core/validators/contracts.js +45 -18
- package/dist/core/validators/contracts.js.map +1 -1
- package/dist/core/validators/scenario.d.ts.map +1 -1
- package/dist/core/validators/scenario.js +2 -15
- package/dist/core/validators/scenario.js.map +1 -1
- package/dist/core/validators/spec.js +1 -1
- package/dist/core/validators/spec.js.map +1 -1
- package/dist/core/validators/traceability.d.ts.map +1 -1
- package/dist/core/validators/traceability.js +66 -34
- package/dist/core/validators/traceability.js.map +1 -1
- package/dist/index.cjs +869 -473
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -12
- package/dist/index.mjs +869 -473
- package/dist/index.mjs.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -27,8 +27,9 @@ var defaultConfig = {
|
|
|
27
27
|
},
|
|
28
28
|
traceability: {
|
|
29
29
|
brMustHaveSc: true,
|
|
30
|
-
scMustTouchContracts: true,
|
|
31
30
|
scMustHaveTest: true,
|
|
31
|
+
testFileGlobs: [],
|
|
32
|
+
testFileExcludeGlobs: [],
|
|
32
33
|
scNoTestSeverity: "error",
|
|
33
34
|
allowOrphanContracts: false,
|
|
34
35
|
unknownContractIdSeverity: "error"
|
|
@@ -202,13 +203,6 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
202
203
|
configPath,
|
|
203
204
|
issues
|
|
204
205
|
),
|
|
205
|
-
scMustTouchContracts: readBoolean(
|
|
206
|
-
traceabilityRaw?.scMustTouchContracts,
|
|
207
|
-
base.traceability.scMustTouchContracts,
|
|
208
|
-
"validation.traceability.scMustTouchContracts",
|
|
209
|
-
configPath,
|
|
210
|
-
issues
|
|
211
|
-
),
|
|
212
206
|
scMustHaveTest: readBoolean(
|
|
213
207
|
traceabilityRaw?.scMustHaveTest,
|
|
214
208
|
base.traceability.scMustHaveTest,
|
|
@@ -216,6 +210,20 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
216
210
|
configPath,
|
|
217
211
|
issues
|
|
218
212
|
),
|
|
213
|
+
testFileGlobs: readStringArray(
|
|
214
|
+
traceabilityRaw?.testFileGlobs,
|
|
215
|
+
base.traceability.testFileGlobs,
|
|
216
|
+
"validation.traceability.testFileGlobs",
|
|
217
|
+
configPath,
|
|
218
|
+
issues
|
|
219
|
+
),
|
|
220
|
+
testFileExcludeGlobs: readStringArray(
|
|
221
|
+
traceabilityRaw?.testFileExcludeGlobs,
|
|
222
|
+
base.traceability.testFileExcludeGlobs,
|
|
223
|
+
"validation.traceability.testFileExcludeGlobs",
|
|
224
|
+
configPath,
|
|
225
|
+
issues
|
|
226
|
+
),
|
|
219
227
|
scNoTestSeverity: readTraceabilitySeverity(
|
|
220
228
|
traceabilityRaw?.scNoTestSeverity,
|
|
221
229
|
base.traceability.scNoTestSeverity,
|
|
@@ -348,14 +356,14 @@ function isRecord(value) {
|
|
|
348
356
|
}
|
|
349
357
|
|
|
350
358
|
// src/core/ids.ts
|
|
351
|
-
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "
|
|
359
|
+
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
352
360
|
var STRICT_ID_PATTERNS = {
|
|
353
361
|
SPEC: /\bSPEC-\d{4}\b/g,
|
|
354
362
|
BR: /\bBR-\d{4}\b/g,
|
|
355
363
|
SC: /\bSC-\d{4}\b/g,
|
|
356
364
|
UI: /\bUI-\d{4}\b/g,
|
|
357
365
|
API: /\bAPI-\d{4}\b/g,
|
|
358
|
-
|
|
366
|
+
DB: /\bDB-\d{4}\b/g,
|
|
359
367
|
ADR: /\bADR-\d{4}\b/g
|
|
360
368
|
};
|
|
361
369
|
var LOOSE_ID_PATTERNS = {
|
|
@@ -364,7 +372,7 @@ var LOOSE_ID_PATTERNS = {
|
|
|
364
372
|
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
365
373
|
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
366
374
|
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
367
|
-
|
|
375
|
+
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
368
376
|
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
369
377
|
};
|
|
370
378
|
function extractIds(text, prefix) {
|
|
@@ -402,7 +410,11 @@ function isValidId(value, prefix) {
|
|
|
402
410
|
|
|
403
411
|
// src/core/report.ts
|
|
404
412
|
import { readFile as readFile11 } from "fs/promises";
|
|
405
|
-
import
|
|
413
|
+
import path11 from "path";
|
|
414
|
+
|
|
415
|
+
// src/core/contractIndex.ts
|
|
416
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
417
|
+
import path4 from "path";
|
|
406
418
|
|
|
407
419
|
// src/core/discovery.ts
|
|
408
420
|
import { access as access2 } from "fs/promises";
|
|
@@ -410,6 +422,7 @@ import { access as access2 } from "fs/promises";
|
|
|
410
422
|
// src/core/fs.ts
|
|
411
423
|
import { access, readdir } from "fs/promises";
|
|
412
424
|
import path2 from "path";
|
|
425
|
+
import fg from "fast-glob";
|
|
413
426
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
414
427
|
"node_modules",
|
|
415
428
|
".git",
|
|
@@ -431,6 +444,18 @@ async function collectFiles(root, options = {}) {
|
|
|
431
444
|
await walk(root, root, ignoreDirs, extensions, entries);
|
|
432
445
|
return entries;
|
|
433
446
|
}
|
|
447
|
+
async function collectFilesByGlobs(root, options) {
|
|
448
|
+
if (options.globs.length === 0) {
|
|
449
|
+
return [];
|
|
450
|
+
}
|
|
451
|
+
return fg(options.globs, {
|
|
452
|
+
cwd: root,
|
|
453
|
+
ignore: options.ignore ?? [],
|
|
454
|
+
onlyFiles: true,
|
|
455
|
+
absolute: true,
|
|
456
|
+
unique: true
|
|
457
|
+
});
|
|
458
|
+
}
|
|
434
459
|
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
435
460
|
const items = await readdir(current, { withFileTypes: true });
|
|
436
461
|
for (const item of items) {
|
|
@@ -542,8 +567,221 @@ async function exists2(target) {
|
|
|
542
567
|
}
|
|
543
568
|
}
|
|
544
569
|
|
|
570
|
+
// src/core/contractsDecl.ts
|
|
571
|
+
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
|
|
572
|
+
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
|
|
573
|
+
function extractDeclaredContractIds(text) {
|
|
574
|
+
const ids = [];
|
|
575
|
+
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
576
|
+
const id = match[1];
|
|
577
|
+
if (id) {
|
|
578
|
+
ids.push(id);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return ids;
|
|
582
|
+
}
|
|
583
|
+
function stripContractDeclarationLines(text) {
|
|
584
|
+
return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/core/contractIndex.ts
|
|
588
|
+
async function buildContractIndex(root, config) {
|
|
589
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
590
|
+
const uiRoot = path4.join(contractsRoot, "ui");
|
|
591
|
+
const apiRoot = path4.join(contractsRoot, "api");
|
|
592
|
+
const dbRoot = path4.join(contractsRoot, "db");
|
|
593
|
+
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
594
|
+
collectUiContractFiles(uiRoot),
|
|
595
|
+
collectApiContractFiles(apiRoot),
|
|
596
|
+
collectDataContractFiles(dbRoot)
|
|
597
|
+
]);
|
|
598
|
+
const index = {
|
|
599
|
+
ids: /* @__PURE__ */ new Set(),
|
|
600
|
+
idToFiles: /* @__PURE__ */ new Map(),
|
|
601
|
+
files: { ui: uiFiles, api: apiFiles, db: dbFiles }
|
|
602
|
+
};
|
|
603
|
+
await indexContractFiles(uiFiles, index);
|
|
604
|
+
await indexContractFiles(apiFiles, index);
|
|
605
|
+
await indexContractFiles(dbFiles, index);
|
|
606
|
+
return index;
|
|
607
|
+
}
|
|
608
|
+
async function indexContractFiles(files, index) {
|
|
609
|
+
for (const file of files) {
|
|
610
|
+
const text = await readFile2(file, "utf-8");
|
|
611
|
+
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function record(index, id, file) {
|
|
615
|
+
index.ids.add(id);
|
|
616
|
+
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
617
|
+
current.add(file);
|
|
618
|
+
index.idToFiles.set(id, current);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/core/parse/markdown.ts
|
|
622
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
623
|
+
function parseHeadings(md) {
|
|
624
|
+
const lines = md.split(/\r?\n/);
|
|
625
|
+
const headings = [];
|
|
626
|
+
for (let i = 0; i < lines.length; i++) {
|
|
627
|
+
const line = lines[i] ?? "";
|
|
628
|
+
const match = line.match(HEADING_RE);
|
|
629
|
+
if (!match) continue;
|
|
630
|
+
const levelToken = match[1];
|
|
631
|
+
const title = match[2];
|
|
632
|
+
if (!levelToken || !title) continue;
|
|
633
|
+
headings.push({
|
|
634
|
+
level: levelToken.length,
|
|
635
|
+
title: title.trim(),
|
|
636
|
+
line: i + 1
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
return headings;
|
|
640
|
+
}
|
|
641
|
+
function extractH2Sections(md) {
|
|
642
|
+
const lines = md.split(/\r?\n/);
|
|
643
|
+
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
644
|
+
const sections = /* @__PURE__ */ new Map();
|
|
645
|
+
for (let i = 0; i < headings.length; i++) {
|
|
646
|
+
const current = headings[i];
|
|
647
|
+
if (!current) continue;
|
|
648
|
+
const next = headings[i + 1];
|
|
649
|
+
const startLine = current.line + 1;
|
|
650
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
651
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
652
|
+
sections.set(current.title.trim(), {
|
|
653
|
+
title: current.title.trim(),
|
|
654
|
+
startLine,
|
|
655
|
+
endLine,
|
|
656
|
+
body
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
return sections;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/core/parse/spec.ts
|
|
663
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
664
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
665
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
666
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
667
|
+
var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
|
|
668
|
+
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
669
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
670
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
671
|
+
function parseSpec(md, file) {
|
|
672
|
+
const headings = parseHeadings(md);
|
|
673
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
674
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
675
|
+
const sections = extractH2Sections(md);
|
|
676
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
677
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
678
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
679
|
+
const startLine = brSection?.startLine ?? 1;
|
|
680
|
+
const brs = [];
|
|
681
|
+
const brsWithoutPriority = [];
|
|
682
|
+
const brsWithInvalidPriority = [];
|
|
683
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
684
|
+
const lineText = brLines[i] ?? "";
|
|
685
|
+
const lineNumber = startLine + i;
|
|
686
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
687
|
+
if (validMatch) {
|
|
688
|
+
const id = validMatch[1];
|
|
689
|
+
const priority = validMatch[2];
|
|
690
|
+
const text = validMatch[3];
|
|
691
|
+
if (!id || !priority || !text) continue;
|
|
692
|
+
brs.push({
|
|
693
|
+
id,
|
|
694
|
+
priority,
|
|
695
|
+
text: text.trim(),
|
|
696
|
+
line: lineNumber
|
|
697
|
+
});
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
701
|
+
if (anyPriorityMatch) {
|
|
702
|
+
const id = anyPriorityMatch[1];
|
|
703
|
+
const priority = anyPriorityMatch[2];
|
|
704
|
+
const text = anyPriorityMatch[3];
|
|
705
|
+
if (!id || !priority || !text) continue;
|
|
706
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
707
|
+
brsWithInvalidPriority.push({
|
|
708
|
+
id,
|
|
709
|
+
priority,
|
|
710
|
+
text: text.trim(),
|
|
711
|
+
line: lineNumber
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
717
|
+
if (noPriorityMatch) {
|
|
718
|
+
const id = noPriorityMatch[1];
|
|
719
|
+
const text = noPriorityMatch[2];
|
|
720
|
+
if (!id || !text) continue;
|
|
721
|
+
brsWithoutPriority.push({
|
|
722
|
+
id,
|
|
723
|
+
text: text.trim(),
|
|
724
|
+
line: lineNumber
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const parsed = {
|
|
729
|
+
file,
|
|
730
|
+
sections: sectionNames,
|
|
731
|
+
brs,
|
|
732
|
+
brsWithoutPriority,
|
|
733
|
+
brsWithInvalidPriority,
|
|
734
|
+
contractRefs: parseContractRefs(md)
|
|
735
|
+
};
|
|
736
|
+
if (specId) {
|
|
737
|
+
parsed.specId = specId;
|
|
738
|
+
}
|
|
739
|
+
return parsed;
|
|
740
|
+
}
|
|
741
|
+
function parseContractRefs(md) {
|
|
742
|
+
const lines = [];
|
|
743
|
+
for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
|
|
744
|
+
lines.push((match[1] ?? "").trim());
|
|
745
|
+
}
|
|
746
|
+
const ids = [];
|
|
747
|
+
const invalidTokens = [];
|
|
748
|
+
let hasNone = false;
|
|
749
|
+
for (const line of lines) {
|
|
750
|
+
if (line.length === 0) {
|
|
751
|
+
invalidTokens.push("(empty)");
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const tokens = line.split(",").map((token) => token.trim());
|
|
755
|
+
for (const token of tokens) {
|
|
756
|
+
if (token.length === 0) {
|
|
757
|
+
invalidTokens.push("(empty)");
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (token === "none") {
|
|
761
|
+
hasNone = true;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
765
|
+
ids.push(token);
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
invalidTokens.push(token);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
lines,
|
|
773
|
+
ids: unique2(ids),
|
|
774
|
+
invalidTokens: unique2(invalidTokens),
|
|
775
|
+
hasNone
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
function unique2(values) {
|
|
779
|
+
return Array.from(new Set(values));
|
|
780
|
+
}
|
|
781
|
+
|
|
545
782
|
// src/core/traceability.ts
|
|
546
|
-
import { readFile as
|
|
783
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
784
|
+
import path5 from "path";
|
|
547
785
|
|
|
548
786
|
// src/core/gherkin/parse.ts
|
|
549
787
|
import {
|
|
@@ -580,7 +818,7 @@ var SC_TAG_RE = /^SC-\d{4}$/;
|
|
|
580
818
|
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
581
819
|
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
582
820
|
var API_TAG_RE = /^API-\d{4}$/;
|
|
583
|
-
var
|
|
821
|
+
var DB_TAG_RE = /^DB-\d{4}$/;
|
|
584
822
|
function parseScenarioDocument(text, uri) {
|
|
585
823
|
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
586
824
|
if (!gherkinDocument) {
|
|
@@ -609,10 +847,10 @@ function buildScenarioAtoms(document) {
|
|
|
609
847
|
return document.scenarios.map((scenario) => {
|
|
610
848
|
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
611
849
|
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
612
|
-
const brIds =
|
|
850
|
+
const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
613
851
|
const contractIds = /* @__PURE__ */ new Set();
|
|
614
852
|
scenario.tags.forEach((tag) => {
|
|
615
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) ||
|
|
853
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
|
|
616
854
|
contractIds.add(tag);
|
|
617
855
|
}
|
|
618
856
|
});
|
|
@@ -620,7 +858,7 @@ function buildScenarioAtoms(document) {
|
|
|
620
858
|
for (const text of collectStepTexts(step)) {
|
|
621
859
|
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
622
860
|
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
623
|
-
extractIds(text, "
|
|
861
|
+
extractIds(text, "DB").forEach((id) => contractIds.add(id));
|
|
624
862
|
}
|
|
625
863
|
}
|
|
626
864
|
const atom = {
|
|
@@ -699,16 +937,37 @@ function collectStepTexts(step) {
|
|
|
699
937
|
}
|
|
700
938
|
return texts;
|
|
701
939
|
}
|
|
702
|
-
function
|
|
940
|
+
function unique3(values) {
|
|
703
941
|
return Array.from(new Set(values));
|
|
704
942
|
}
|
|
705
943
|
|
|
706
944
|
// src/core/traceability.ts
|
|
707
945
|
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
946
|
+
var SC_TEST_ANNOTATION_RE = /\bQFAI:SC-(\d{4})\b/g;
|
|
947
|
+
var DEFAULT_TEST_FILE_EXCLUDE_GLOBS = [
|
|
948
|
+
"**/node_modules/**",
|
|
949
|
+
"**/.git/**",
|
|
950
|
+
"**/.qfai/**",
|
|
951
|
+
"**/dist/**",
|
|
952
|
+
"**/build/**",
|
|
953
|
+
"**/coverage/**",
|
|
954
|
+
"**/.next/**",
|
|
955
|
+
"**/out/**"
|
|
956
|
+
];
|
|
957
|
+
function extractAnnotatedScIds(text) {
|
|
958
|
+
const ids = /* @__PURE__ */ new Set();
|
|
959
|
+
for (const match of text.matchAll(SC_TEST_ANNOTATION_RE)) {
|
|
960
|
+
const suffix = match[1];
|
|
961
|
+
if (suffix) {
|
|
962
|
+
ids.add(`SC-${suffix}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return Array.from(ids);
|
|
966
|
+
}
|
|
708
967
|
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
709
968
|
const scIds = /* @__PURE__ */ new Set();
|
|
710
969
|
for (const file of scenarioFiles) {
|
|
711
|
-
const text = await
|
|
970
|
+
const text = await readFile3(file, "utf-8");
|
|
712
971
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
713
972
|
if (!document || errors.length > 0) {
|
|
714
973
|
continue;
|
|
@@ -723,14 +982,67 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
|
723
982
|
}
|
|
724
983
|
return scIds;
|
|
725
984
|
}
|
|
726
|
-
async function
|
|
985
|
+
async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
986
|
+
const sources = /* @__PURE__ */ new Map();
|
|
987
|
+
for (const file of scenarioFiles) {
|
|
988
|
+
const text = await readFile3(file, "utf-8");
|
|
989
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
990
|
+
if (!document || errors.length > 0) {
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
for (const scenario of document.scenarios) {
|
|
994
|
+
for (const tag of scenario.tags) {
|
|
995
|
+
if (!SC_TAG_RE2.test(tag)) {
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
const current = sources.get(tag) ?? /* @__PURE__ */ new Set();
|
|
999
|
+
current.add(file);
|
|
1000
|
+
sources.set(tag, current);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return sources;
|
|
1005
|
+
}
|
|
1006
|
+
async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
727
1007
|
const refs = /* @__PURE__ */ new Map();
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1008
|
+
const normalizedGlobs = normalizeGlobs(globs);
|
|
1009
|
+
const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
|
|
1010
|
+
const mergedExcludeGlobs = Array.from(
|
|
1011
|
+
/* @__PURE__ */ new Set([...DEFAULT_TEST_FILE_EXCLUDE_GLOBS, ...normalizedExcludeGlobs])
|
|
1012
|
+
);
|
|
1013
|
+
if (normalizedGlobs.length === 0) {
|
|
1014
|
+
return {
|
|
1015
|
+
refs,
|
|
1016
|
+
scan: {
|
|
1017
|
+
globs: normalizedGlobs,
|
|
1018
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
1019
|
+
matchedFileCount: 0
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
let files = [];
|
|
1024
|
+
try {
|
|
1025
|
+
files = await collectFilesByGlobs(root, {
|
|
1026
|
+
globs: normalizedGlobs,
|
|
1027
|
+
ignore: mergedExcludeGlobs
|
|
1028
|
+
});
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
return {
|
|
1031
|
+
refs,
|
|
1032
|
+
scan: {
|
|
1033
|
+
globs: normalizedGlobs,
|
|
1034
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
1035
|
+
matchedFileCount: 0
|
|
1036
|
+
},
|
|
1037
|
+
error: formatError3(error)
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
const normalizedFiles = Array.from(
|
|
1041
|
+
new Set(files.map((file) => path5.normalize(file)))
|
|
1042
|
+
);
|
|
1043
|
+
for (const file of normalizedFiles) {
|
|
1044
|
+
const text = await readFile3(file, "utf-8");
|
|
1045
|
+
const scIds = extractAnnotatedScIds(text);
|
|
734
1046
|
if (scIds.length === 0) {
|
|
735
1047
|
continue;
|
|
736
1048
|
}
|
|
@@ -740,7 +1052,14 @@ async function collectScTestReferences(testsRoot) {
|
|
|
740
1052
|
refs.set(scId, current);
|
|
741
1053
|
}
|
|
742
1054
|
}
|
|
743
|
-
return
|
|
1055
|
+
return {
|
|
1056
|
+
refs,
|
|
1057
|
+
scan: {
|
|
1058
|
+
globs: normalizedGlobs,
|
|
1059
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
1060
|
+
matchedFileCount: normalizedFiles.length
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
744
1063
|
}
|
|
745
1064
|
function buildScCoverage(scIds, refs) {
|
|
746
1065
|
const sortedScIds = toSortedArray(scIds);
|
|
@@ -768,18 +1087,27 @@ function buildScCoverage(scIds, refs) {
|
|
|
768
1087
|
function toSortedArray(values) {
|
|
769
1088
|
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
770
1089
|
}
|
|
1090
|
+
function normalizeGlobs(globs) {
|
|
1091
|
+
return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
1092
|
+
}
|
|
1093
|
+
function formatError3(error) {
|
|
1094
|
+
if (error instanceof Error) {
|
|
1095
|
+
return error.message;
|
|
1096
|
+
}
|
|
1097
|
+
return String(error);
|
|
1098
|
+
}
|
|
771
1099
|
|
|
772
1100
|
// src/core/version.ts
|
|
773
|
-
import { readFile as
|
|
774
|
-
import
|
|
1101
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1102
|
+
import path6 from "path";
|
|
775
1103
|
import { fileURLToPath } from "url";
|
|
776
1104
|
async function resolveToolVersion() {
|
|
777
|
-
if ("0.4.
|
|
778
|
-
return "0.4.
|
|
1105
|
+
if ("0.4.4".length > 0) {
|
|
1106
|
+
return "0.4.4";
|
|
779
1107
|
}
|
|
780
1108
|
try {
|
|
781
1109
|
const packagePath = resolvePackageJsonPath();
|
|
782
|
-
const raw = await
|
|
1110
|
+
const raw = await readFile4(packagePath, "utf-8");
|
|
783
1111
|
const parsed = JSON.parse(raw);
|
|
784
1112
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
785
1113
|
return version.length > 0 ? version : "unknown";
|
|
@@ -790,54 +1118,23 @@ async function resolveToolVersion() {
|
|
|
790
1118
|
function resolvePackageJsonPath() {
|
|
791
1119
|
const base = import.meta.url;
|
|
792
1120
|
const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
|
|
793
|
-
return
|
|
1121
|
+
return path6.resolve(path6.dirname(basePath), "../../package.json");
|
|
794
1122
|
}
|
|
795
1123
|
|
|
796
1124
|
// src/core/validators/contracts.ts
|
|
797
|
-
import { readFile as
|
|
798
|
-
import
|
|
1125
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1126
|
+
import path8 from "path";
|
|
799
1127
|
|
|
800
1128
|
// src/core/contracts.ts
|
|
801
|
-
import
|
|
1129
|
+
import path7 from "path";
|
|
802
1130
|
import { parse as parseYaml2 } from "yaml";
|
|
803
1131
|
function parseStructuredContract(file, text) {
|
|
804
|
-
const ext =
|
|
1132
|
+
const ext = path7.extname(file).toLowerCase();
|
|
805
1133
|
if (ext === ".json") {
|
|
806
1134
|
return JSON.parse(text);
|
|
807
1135
|
}
|
|
808
1136
|
return parseYaml2(text);
|
|
809
1137
|
}
|
|
810
|
-
function extractUiContractIds(doc) {
|
|
811
|
-
const id = typeof doc.id === "string" ? doc.id : "";
|
|
812
|
-
return extractIds(id, "UI");
|
|
813
|
-
}
|
|
814
|
-
function extractApiContractIds(doc) {
|
|
815
|
-
const operationIds = /* @__PURE__ */ new Set();
|
|
816
|
-
collectOperationIds(doc, operationIds);
|
|
817
|
-
const ids = /* @__PURE__ */ new Set();
|
|
818
|
-
for (const operationId of operationIds) {
|
|
819
|
-
extractIds(operationId, "API").forEach((id) => ids.add(id));
|
|
820
|
-
}
|
|
821
|
-
return Array.from(ids);
|
|
822
|
-
}
|
|
823
|
-
function collectOperationIds(value, out) {
|
|
824
|
-
if (!value || typeof value !== "object") {
|
|
825
|
-
return;
|
|
826
|
-
}
|
|
827
|
-
if (Array.isArray(value)) {
|
|
828
|
-
for (const item of value) {
|
|
829
|
-
collectOperationIds(item, out);
|
|
830
|
-
}
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
834
|
-
if (key === "operationId" && typeof entry === "string") {
|
|
835
|
-
out.add(entry);
|
|
836
|
-
continue;
|
|
837
|
-
}
|
|
838
|
-
collectOperationIds(entry, out);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
1138
|
|
|
842
1139
|
// src/core/validators/contracts.ts
|
|
843
1140
|
var SQL_DANGEROUS_PATTERNS = [
|
|
@@ -852,9 +1149,11 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
852
1149
|
async function validateContracts(root, config) {
|
|
853
1150
|
const issues = [];
|
|
854
1151
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
855
|
-
issues.push(...await validateUiContracts(
|
|
856
|
-
issues.push(...await validateApiContracts(
|
|
857
|
-
issues.push(...await validateDataContracts(
|
|
1152
|
+
issues.push(...await validateUiContracts(path8.join(contractsRoot, "ui")));
|
|
1153
|
+
issues.push(...await validateApiContracts(path8.join(contractsRoot, "api")));
|
|
1154
|
+
issues.push(...await validateDataContracts(path8.join(contractsRoot, "db")));
|
|
1155
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
1156
|
+
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
858
1157
|
return issues;
|
|
859
1158
|
}
|
|
860
1159
|
async function validateUiContracts(uiRoot) {
|
|
@@ -872,14 +1171,14 @@ async function validateUiContracts(uiRoot) {
|
|
|
872
1171
|
}
|
|
873
1172
|
const issues = [];
|
|
874
1173
|
for (const file of files) {
|
|
875
|
-
const text = await
|
|
1174
|
+
const text = await readFile5(file, "utf-8");
|
|
876
1175
|
const invalidIds = extractInvalidIds(text, [
|
|
877
1176
|
"SPEC",
|
|
878
1177
|
"BR",
|
|
879
1178
|
"SC",
|
|
880
1179
|
"UI",
|
|
881
1180
|
"API",
|
|
882
|
-
"
|
|
1181
|
+
"DB",
|
|
883
1182
|
"ADR"
|
|
884
1183
|
]);
|
|
885
1184
|
if (invalidIds.length > 0) {
|
|
@@ -894,32 +1193,20 @@ async function validateUiContracts(uiRoot) {
|
|
|
894
1193
|
)
|
|
895
1194
|
);
|
|
896
1195
|
}
|
|
897
|
-
|
|
1196
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1197
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
|
|
898
1198
|
try {
|
|
899
|
-
|
|
1199
|
+
parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
900
1200
|
} catch (error) {
|
|
901
1201
|
issues.push(
|
|
902
1202
|
issue(
|
|
903
1203
|
"QFAI-CONTRACT-001",
|
|
904
|
-
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1204
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
|
|
905
1205
|
"error",
|
|
906
1206
|
file,
|
|
907
1207
|
"contracts.ui.parse"
|
|
908
1208
|
)
|
|
909
1209
|
);
|
|
910
|
-
continue;
|
|
911
|
-
}
|
|
912
|
-
const uiIds = extractUiContractIds(doc);
|
|
913
|
-
if (uiIds.length === 0) {
|
|
914
|
-
issues.push(
|
|
915
|
-
issue(
|
|
916
|
-
"QFAI-CONTRACT-002",
|
|
917
|
-
`UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
918
|
-
"error",
|
|
919
|
-
file,
|
|
920
|
-
"contracts.ui.id"
|
|
921
|
-
)
|
|
922
|
-
);
|
|
923
1210
|
}
|
|
924
1211
|
}
|
|
925
1212
|
return issues;
|
|
@@ -939,14 +1226,14 @@ async function validateApiContracts(apiRoot) {
|
|
|
939
1226
|
}
|
|
940
1227
|
const issues = [];
|
|
941
1228
|
for (const file of files) {
|
|
942
|
-
const text = await
|
|
1229
|
+
const text = await readFile5(file, "utf-8");
|
|
943
1230
|
const invalidIds = extractInvalidIds(text, [
|
|
944
1231
|
"SPEC",
|
|
945
1232
|
"BR",
|
|
946
1233
|
"SC",
|
|
947
1234
|
"UI",
|
|
948
1235
|
"API",
|
|
949
|
-
"
|
|
1236
|
+
"DB",
|
|
950
1237
|
"ADR"
|
|
951
1238
|
]);
|
|
952
1239
|
if (invalidIds.length > 0) {
|
|
@@ -961,14 +1248,16 @@ async function validateApiContracts(apiRoot) {
|
|
|
961
1248
|
)
|
|
962
1249
|
);
|
|
963
1250
|
}
|
|
1251
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1252
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "API"));
|
|
964
1253
|
let doc;
|
|
965
1254
|
try {
|
|
966
|
-
doc = parseStructuredContract(file, text);
|
|
1255
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
967
1256
|
} catch (error) {
|
|
968
1257
|
issues.push(
|
|
969
1258
|
issue(
|
|
970
1259
|
"QFAI-CONTRACT-001",
|
|
971
|
-
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1260
|
+
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
|
|
972
1261
|
"error",
|
|
973
1262
|
file,
|
|
974
1263
|
"contracts.api.parse"
|
|
@@ -987,18 +1276,6 @@ async function validateApiContracts(apiRoot) {
|
|
|
987
1276
|
)
|
|
988
1277
|
);
|
|
989
1278
|
}
|
|
990
|
-
const apiIds = extractApiContractIds(doc);
|
|
991
|
-
if (apiIds.length === 0) {
|
|
992
|
-
issues.push(
|
|
993
|
-
issue(
|
|
994
|
-
"QFAI-CONTRACT-002",
|
|
995
|
-
`API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
996
|
-
"error",
|
|
997
|
-
file,
|
|
998
|
-
"contracts.api.id"
|
|
999
|
-
)
|
|
1000
|
-
);
|
|
1001
|
-
}
|
|
1002
1279
|
}
|
|
1003
1280
|
return issues;
|
|
1004
1281
|
}
|
|
@@ -1007,24 +1284,24 @@ async function validateDataContracts(dataRoot) {
|
|
|
1007
1284
|
if (files.length === 0) {
|
|
1008
1285
|
return [
|
|
1009
1286
|
issue(
|
|
1010
|
-
"QFAI-
|
|
1011
|
-
"
|
|
1287
|
+
"QFAI-DB-000",
|
|
1288
|
+
"DB \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1012
1289
|
"info",
|
|
1013
1290
|
dataRoot,
|
|
1014
|
-
"contracts.
|
|
1291
|
+
"contracts.db.files"
|
|
1015
1292
|
)
|
|
1016
1293
|
];
|
|
1017
1294
|
}
|
|
1018
1295
|
const issues = [];
|
|
1019
1296
|
for (const file of files) {
|
|
1020
|
-
const text = await
|
|
1297
|
+
const text = await readFile5(file, "utf-8");
|
|
1021
1298
|
const invalidIds = extractInvalidIds(text, [
|
|
1022
1299
|
"SPEC",
|
|
1023
1300
|
"BR",
|
|
1024
1301
|
"SC",
|
|
1025
1302
|
"UI",
|
|
1026
1303
|
"API",
|
|
1027
|
-
"
|
|
1304
|
+
"DB",
|
|
1028
1305
|
"ADR"
|
|
1029
1306
|
]);
|
|
1030
1307
|
if (invalidIds.length > 0) {
|
|
@@ -1039,6 +1316,8 @@ async function validateDataContracts(dataRoot) {
|
|
|
1039
1316
|
)
|
|
1040
1317
|
);
|
|
1041
1318
|
}
|
|
1319
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1320
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "DB"));
|
|
1042
1321
|
issues.push(...lintSql(text, file));
|
|
1043
1322
|
}
|
|
1044
1323
|
return issues;
|
|
@@ -1049,21 +1328,87 @@ function lintSql(text, file) {
|
|
|
1049
1328
|
if (pattern.test(text)) {
|
|
1050
1329
|
issues.push(
|
|
1051
1330
|
issue(
|
|
1052
|
-
"QFAI-
|
|
1331
|
+
"QFAI-DB-001",
|
|
1053
1332
|
`\u5371\u967A\u306A SQL \u64CD\u4F5C\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059: ${label}`,
|
|
1054
1333
|
"warning",
|
|
1055
1334
|
file,
|
|
1056
|
-
"contracts.
|
|
1335
|
+
"contracts.db.sql"
|
|
1057
1336
|
)
|
|
1058
1337
|
);
|
|
1059
1338
|
}
|
|
1060
1339
|
}
|
|
1061
1340
|
return issues;
|
|
1062
1341
|
}
|
|
1342
|
+
function validateDeclaredContractIds(ids, file, kind) {
|
|
1343
|
+
const issues = [];
|
|
1344
|
+
if (ids.length === 0) {
|
|
1345
|
+
issues.push(
|
|
1346
|
+
issue(
|
|
1347
|
+
"QFAI-CONTRACT-010",
|
|
1348
|
+
`\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
|
|
1349
|
+
"error",
|
|
1350
|
+
file,
|
|
1351
|
+
"contracts.declaration"
|
|
1352
|
+
)
|
|
1353
|
+
);
|
|
1354
|
+
return issues;
|
|
1355
|
+
}
|
|
1356
|
+
if (ids.length > 1) {
|
|
1357
|
+
issues.push(
|
|
1358
|
+
issue(
|
|
1359
|
+
"QFAI-CONTRACT-011",
|
|
1360
|
+
`\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B\u8907\u6570\u306E QFAI-CONTRACT-ID \u304C\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${ids.join(
|
|
1361
|
+
", "
|
|
1362
|
+
)}`,
|
|
1363
|
+
"error",
|
|
1364
|
+
file,
|
|
1365
|
+
"contracts.declaration",
|
|
1366
|
+
ids
|
|
1367
|
+
)
|
|
1368
|
+
);
|
|
1369
|
+
return issues;
|
|
1370
|
+
}
|
|
1371
|
+
const [id] = ids;
|
|
1372
|
+
if (id && !id.startsWith(`${kind}-`)) {
|
|
1373
|
+
issues.push(
|
|
1374
|
+
issue(
|
|
1375
|
+
"QFAI-CONTRACT-013",
|
|
1376
|
+
`\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E QFAI-CONTRACT-ID \u304C ${kind}- \u3067\u306F\u3042\u308A\u307E\u305B\u3093: ${id}`,
|
|
1377
|
+
"error",
|
|
1378
|
+
file,
|
|
1379
|
+
"contracts.declarationPrefix",
|
|
1380
|
+
[id]
|
|
1381
|
+
)
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
return issues;
|
|
1385
|
+
}
|
|
1386
|
+
function validateDuplicateContractIds(contractIndex) {
|
|
1387
|
+
const issues = [];
|
|
1388
|
+
for (const [id, files] of contractIndex.idToFiles.entries()) {
|
|
1389
|
+
if (files.size <= 1) {
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
const sortedFiles = Array.from(files).sort((a, b) => a.localeCompare(b));
|
|
1393
|
+
issues.push(
|
|
1394
|
+
issue(
|
|
1395
|
+
"QFAI-CONTRACT-012",
|
|
1396
|
+
`\u5951\u7D04 ID \u304C\u8907\u6570\u30D5\u30A1\u30A4\u30EB\u3067\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${id} (${sortedFiles.join(
|
|
1397
|
+
", "
|
|
1398
|
+
)})`,
|
|
1399
|
+
"error",
|
|
1400
|
+
sortedFiles[0],
|
|
1401
|
+
"contracts.idDuplicate",
|
|
1402
|
+
[id]
|
|
1403
|
+
)
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
return issues;
|
|
1407
|
+
}
|
|
1063
1408
|
function hasOpenApi(doc) {
|
|
1064
1409
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
1065
1410
|
}
|
|
1066
|
-
function
|
|
1411
|
+
function formatError4(error) {
|
|
1067
1412
|
if (error instanceof Error) {
|
|
1068
1413
|
return error.message;
|
|
1069
1414
|
}
|
|
@@ -1088,279 +1433,98 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1088
1433
|
}
|
|
1089
1434
|
|
|
1090
1435
|
// src/core/validators/delta.ts
|
|
1091
|
-
import { readFile as
|
|
1092
|
-
import
|
|
1436
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1437
|
+
import path9 from "path";
|
|
1093
1438
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1094
1439
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1095
1440
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
1096
1441
|
var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
|
|
1097
1442
|
var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
|
|
1098
1443
|
async function validateDeltas(root, config) {
|
|
1099
|
-
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1100
|
-
const packs = await collectSpecPackDirs(specsRoot);
|
|
1101
|
-
if (packs.length === 0) {
|
|
1102
|
-
return [];
|
|
1103
|
-
}
|
|
1104
|
-
const issues = [];
|
|
1105
|
-
for (const pack of packs) {
|
|
1106
|
-
const deltaPath =
|
|
1107
|
-
let text;
|
|
1108
|
-
try {
|
|
1109
|
-
text = await
|
|
1110
|
-
} catch (error) {
|
|
1111
|
-
if (isMissingFileError2(error)) {
|
|
1112
|
-
issues.push(
|
|
1113
|
-
issue2(
|
|
1114
|
-
"QFAI-DELTA-001",
|
|
1115
|
-
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1116
|
-
"error",
|
|
1117
|
-
deltaPath,
|
|
1118
|
-
"delta.exists"
|
|
1119
|
-
)
|
|
1120
|
-
);
|
|
1121
|
-
continue;
|
|
1122
|
-
}
|
|
1123
|
-
throw error;
|
|
1124
|
-
}
|
|
1125
|
-
const hasSection = SECTION_RE.test(text);
|
|
1126
|
-
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
1127
|
-
const hasChange = CHANGE_LINE_RE.test(text);
|
|
1128
|
-
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
1129
|
-
issues.push(
|
|
1130
|
-
issue2(
|
|
1131
|
-
"QFAI-DELTA-002",
|
|
1132
|
-
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
1133
|
-
"error",
|
|
1134
|
-
deltaPath,
|
|
1135
|
-
"delta.section"
|
|
1136
|
-
)
|
|
1137
|
-
);
|
|
1138
|
-
continue;
|
|
1139
|
-
}
|
|
1140
|
-
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
1141
|
-
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
1142
|
-
if (compatibilityChecked === changeChecked) {
|
|
1143
|
-
issues.push(
|
|
1144
|
-
issue2(
|
|
1145
|
-
"QFAI-DELTA-003",
|
|
1146
|
-
"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",
|
|
1147
|
-
"error",
|
|
1148
|
-
deltaPath,
|
|
1149
|
-
"delta.classification"
|
|
1150
|
-
)
|
|
1151
|
-
);
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
return issues;
|
|
1155
|
-
}
|
|
1156
|
-
function isMissingFileError2(error) {
|
|
1157
|
-
if (!error || typeof error !== "object") {
|
|
1158
|
-
return false;
|
|
1159
|
-
}
|
|
1160
|
-
return error.code === "ENOENT";
|
|
1161
|
-
}
|
|
1162
|
-
function issue2(code, message, severity, file, rule, refs) {
|
|
1163
|
-
const issue7 = {
|
|
1164
|
-
code,
|
|
1165
|
-
severity,
|
|
1166
|
-
message
|
|
1167
|
-
};
|
|
1168
|
-
if (file) {
|
|
1169
|
-
issue7.file = file;
|
|
1170
|
-
}
|
|
1171
|
-
if (rule) {
|
|
1172
|
-
issue7.rule = rule;
|
|
1173
|
-
}
|
|
1174
|
-
if (refs && refs.length > 0) {
|
|
1175
|
-
issue7.refs = refs;
|
|
1176
|
-
}
|
|
1177
|
-
return issue7;
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
// src/core/validators/ids.ts
|
|
1181
|
-
import { readFile as readFile7 } from "fs/promises";
|
|
1182
|
-
import path9 from "path";
|
|
1183
|
-
|
|
1184
|
-
// src/core/contractIndex.ts
|
|
1185
|
-
import { readFile as readFile6 } from "fs/promises";
|
|
1186
|
-
import path8 from "path";
|
|
1187
|
-
async function buildContractIndex(root, config) {
|
|
1188
|
-
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1189
|
-
const uiRoot = path8.join(contractsRoot, "ui");
|
|
1190
|
-
const apiRoot = path8.join(contractsRoot, "api");
|
|
1191
|
-
const dataRoot = path8.join(contractsRoot, "db");
|
|
1192
|
-
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
1193
|
-
collectUiContractFiles(uiRoot),
|
|
1194
|
-
collectApiContractFiles(apiRoot),
|
|
1195
|
-
collectDataContractFiles(dataRoot)
|
|
1196
|
-
]);
|
|
1197
|
-
const index = {
|
|
1198
|
-
ids: /* @__PURE__ */ new Set(),
|
|
1199
|
-
idToFiles: /* @__PURE__ */ new Map(),
|
|
1200
|
-
files: { ui: uiFiles, api: apiFiles, data: dataFiles },
|
|
1201
|
-
structuredParseFailedFiles: /* @__PURE__ */ new Set()
|
|
1202
|
-
};
|
|
1203
|
-
await indexUiContracts(uiFiles, index);
|
|
1204
|
-
await indexApiContracts(apiFiles, index);
|
|
1205
|
-
await indexDataContracts(dataFiles, index);
|
|
1206
|
-
return index;
|
|
1207
|
-
}
|
|
1208
|
-
async function indexUiContracts(files, index) {
|
|
1209
|
-
for (const file of files) {
|
|
1210
|
-
const text = await readFile6(file, "utf-8");
|
|
1211
|
-
try {
|
|
1212
|
-
const doc = parseStructuredContract(file, text);
|
|
1213
|
-
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
1214
|
-
} catch {
|
|
1215
|
-
index.structuredParseFailedFiles.add(file);
|
|
1216
|
-
extractIds(text, "UI").forEach((id) => record(index, id, file));
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
async function indexApiContracts(files, index) {
|
|
1221
|
-
for (const file of files) {
|
|
1222
|
-
const text = await readFile6(file, "utf-8");
|
|
1223
|
-
try {
|
|
1224
|
-
const doc = parseStructuredContract(file, text);
|
|
1225
|
-
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
1226
|
-
} catch {
|
|
1227
|
-
index.structuredParseFailedFiles.add(file);
|
|
1228
|
-
extractIds(text, "API").forEach((id) => record(index, id, file));
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
async function indexDataContracts(files, index) {
|
|
1233
|
-
for (const file of files) {
|
|
1234
|
-
const text = await readFile6(file, "utf-8");
|
|
1235
|
-
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
function record(index, id, file) {
|
|
1239
|
-
index.ids.add(id);
|
|
1240
|
-
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
1241
|
-
current.add(file);
|
|
1242
|
-
index.idToFiles.set(id, current);
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
// src/core/parse/markdown.ts
|
|
1246
|
-
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1247
|
-
function parseHeadings(md) {
|
|
1248
|
-
const lines = md.split(/\r?\n/);
|
|
1249
|
-
const headings = [];
|
|
1250
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1251
|
-
const line = lines[i] ?? "";
|
|
1252
|
-
const match = line.match(HEADING_RE);
|
|
1253
|
-
if (!match) continue;
|
|
1254
|
-
const levelToken = match[1];
|
|
1255
|
-
const title = match[2];
|
|
1256
|
-
if (!levelToken || !title) continue;
|
|
1257
|
-
headings.push({
|
|
1258
|
-
level: levelToken.length,
|
|
1259
|
-
title: title.trim(),
|
|
1260
|
-
line: i + 1
|
|
1261
|
-
});
|
|
1262
|
-
}
|
|
1263
|
-
return headings;
|
|
1264
|
-
}
|
|
1265
|
-
function extractH2Sections(md) {
|
|
1266
|
-
const lines = md.split(/\r?\n/);
|
|
1267
|
-
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1268
|
-
const sections = /* @__PURE__ */ new Map();
|
|
1269
|
-
for (let i = 0; i < headings.length; i++) {
|
|
1270
|
-
const current = headings[i];
|
|
1271
|
-
if (!current) continue;
|
|
1272
|
-
const next = headings[i + 1];
|
|
1273
|
-
const startLine = current.line + 1;
|
|
1274
|
-
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1275
|
-
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1276
|
-
sections.set(current.title.trim(), {
|
|
1277
|
-
title: current.title.trim(),
|
|
1278
|
-
startLine,
|
|
1279
|
-
endLine,
|
|
1280
|
-
body
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
return sections;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
// src/core/parse/spec.ts
|
|
1287
|
-
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1288
|
-
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1289
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1290
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1291
|
-
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1292
|
-
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1293
|
-
function parseSpec(md, file) {
|
|
1294
|
-
const headings = parseHeadings(md);
|
|
1295
|
-
const h1 = headings.find((heading) => heading.level === 1);
|
|
1296
|
-
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1297
|
-
const sections = extractH2Sections(md);
|
|
1298
|
-
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1299
|
-
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1300
|
-
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1301
|
-
const startLine = brSection?.startLine ?? 1;
|
|
1302
|
-
const brs = [];
|
|
1303
|
-
const brsWithoutPriority = [];
|
|
1304
|
-
const brsWithInvalidPriority = [];
|
|
1305
|
-
for (let i = 0; i < brLines.length; i++) {
|
|
1306
|
-
const lineText = brLines[i] ?? "";
|
|
1307
|
-
const lineNumber = startLine + i;
|
|
1308
|
-
const validMatch = lineText.match(BR_LINE_RE);
|
|
1309
|
-
if (validMatch) {
|
|
1310
|
-
const id = validMatch[1];
|
|
1311
|
-
const priority = validMatch[2];
|
|
1312
|
-
const text = validMatch[3];
|
|
1313
|
-
if (!id || !priority || !text) continue;
|
|
1314
|
-
brs.push({
|
|
1315
|
-
id,
|
|
1316
|
-
priority,
|
|
1317
|
-
text: text.trim(),
|
|
1318
|
-
line: lineNumber
|
|
1319
|
-
});
|
|
1320
|
-
continue;
|
|
1321
|
-
}
|
|
1322
|
-
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1323
|
-
if (anyPriorityMatch) {
|
|
1324
|
-
const id = anyPriorityMatch[1];
|
|
1325
|
-
const priority = anyPriorityMatch[2];
|
|
1326
|
-
const text = anyPriorityMatch[3];
|
|
1327
|
-
if (!id || !priority || !text) continue;
|
|
1328
|
-
if (!VALID_PRIORITIES.has(priority)) {
|
|
1329
|
-
brsWithInvalidPriority.push({
|
|
1330
|
-
id,
|
|
1331
|
-
priority,
|
|
1332
|
-
text: text.trim(),
|
|
1333
|
-
line: lineNumber
|
|
1334
|
-
});
|
|
1444
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1445
|
+
const packs = await collectSpecPackDirs(specsRoot);
|
|
1446
|
+
if (packs.length === 0) {
|
|
1447
|
+
return [];
|
|
1448
|
+
}
|
|
1449
|
+
const issues = [];
|
|
1450
|
+
for (const pack of packs) {
|
|
1451
|
+
const deltaPath = path9.join(pack, "delta.md");
|
|
1452
|
+
let text;
|
|
1453
|
+
try {
|
|
1454
|
+
text = await readFile6(deltaPath, "utf-8");
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
if (isMissingFileError2(error)) {
|
|
1457
|
+
issues.push(
|
|
1458
|
+
issue2(
|
|
1459
|
+
"QFAI-DELTA-001",
|
|
1460
|
+
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1461
|
+
"error",
|
|
1462
|
+
deltaPath,
|
|
1463
|
+
"delta.exists"
|
|
1464
|
+
)
|
|
1465
|
+
);
|
|
1466
|
+
continue;
|
|
1335
1467
|
}
|
|
1468
|
+
throw error;
|
|
1469
|
+
}
|
|
1470
|
+
const hasSection = SECTION_RE.test(text);
|
|
1471
|
+
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
1472
|
+
const hasChange = CHANGE_LINE_RE.test(text);
|
|
1473
|
+
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
1474
|
+
issues.push(
|
|
1475
|
+
issue2(
|
|
1476
|
+
"QFAI-DELTA-002",
|
|
1477
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
1478
|
+
"error",
|
|
1479
|
+
deltaPath,
|
|
1480
|
+
"delta.section"
|
|
1481
|
+
)
|
|
1482
|
+
);
|
|
1336
1483
|
continue;
|
|
1337
1484
|
}
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1485
|
+
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
1486
|
+
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
1487
|
+
if (compatibilityChecked === changeChecked) {
|
|
1488
|
+
issues.push(
|
|
1489
|
+
issue2(
|
|
1490
|
+
"QFAI-DELTA-003",
|
|
1491
|
+
"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",
|
|
1492
|
+
"error",
|
|
1493
|
+
deltaPath,
|
|
1494
|
+
"delta.classification"
|
|
1495
|
+
)
|
|
1496
|
+
);
|
|
1348
1497
|
}
|
|
1349
1498
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1499
|
+
return issues;
|
|
1500
|
+
}
|
|
1501
|
+
function isMissingFileError2(error) {
|
|
1502
|
+
if (!error || typeof error !== "object") {
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
return error.code === "ENOENT";
|
|
1506
|
+
}
|
|
1507
|
+
function issue2(code, message, severity, file, rule, refs) {
|
|
1508
|
+
const issue7 = {
|
|
1509
|
+
code,
|
|
1510
|
+
severity,
|
|
1511
|
+
message
|
|
1356
1512
|
};
|
|
1357
|
-
if (
|
|
1358
|
-
|
|
1513
|
+
if (file) {
|
|
1514
|
+
issue7.file = file;
|
|
1359
1515
|
}
|
|
1360
|
-
|
|
1516
|
+
if (rule) {
|
|
1517
|
+
issue7.rule = rule;
|
|
1518
|
+
}
|
|
1519
|
+
if (refs && refs.length > 0) {
|
|
1520
|
+
issue7.refs = refs;
|
|
1521
|
+
}
|
|
1522
|
+
return issue7;
|
|
1361
1523
|
}
|
|
1362
1524
|
|
|
1363
1525
|
// src/core/validators/ids.ts
|
|
1526
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1527
|
+
import path10 from "path";
|
|
1364
1528
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1365
1529
|
async function validateDefinedIds(root, config) {
|
|
1366
1530
|
const issues = [];
|
|
@@ -1426,7 +1590,7 @@ function recordId(out, id, file) {
|
|
|
1426
1590
|
}
|
|
1427
1591
|
function formatFileList(files, root) {
|
|
1428
1592
|
return files.map((file) => {
|
|
1429
|
-
const relative =
|
|
1593
|
+
const relative = path10.relative(root, file);
|
|
1430
1594
|
return relative.length > 0 ? relative : file;
|
|
1431
1595
|
}).join(", ");
|
|
1432
1596
|
}
|
|
@@ -1455,7 +1619,6 @@ var WHEN_PATTERN = /\bWhen\b/;
|
|
|
1455
1619
|
var THEN_PATTERN = /\bThen\b/;
|
|
1456
1620
|
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1457
1621
|
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1458
|
-
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1459
1622
|
async function validateScenarios(root, config) {
|
|
1460
1623
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1461
1624
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -1504,7 +1667,7 @@ function validateScenarioContent(text, file) {
|
|
|
1504
1667
|
"SC",
|
|
1505
1668
|
"UI",
|
|
1506
1669
|
"API",
|
|
1507
|
-
"
|
|
1670
|
+
"DB",
|
|
1508
1671
|
"ADR"
|
|
1509
1672
|
]);
|
|
1510
1673
|
if (invalidIds.length > 0) {
|
|
@@ -1535,17 +1698,7 @@ function validateScenarioContent(text, file) {
|
|
|
1535
1698
|
const featureSpecTags = document.featureTags.filter(
|
|
1536
1699
|
(tag) => SPEC_TAG_RE2.test(tag)
|
|
1537
1700
|
);
|
|
1538
|
-
if (featureSpecTags.length
|
|
1539
|
-
issues.push(
|
|
1540
|
-
issue4(
|
|
1541
|
-
"QFAI-SC-009",
|
|
1542
|
-
"Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1543
|
-
"error",
|
|
1544
|
-
file,
|
|
1545
|
-
"scenario.featureSpec"
|
|
1546
|
-
)
|
|
1547
|
-
);
|
|
1548
|
-
} else if (featureSpecTags.length > 1) {
|
|
1701
|
+
if (featureSpecTags.length > 1) {
|
|
1549
1702
|
issues.push(
|
|
1550
1703
|
issue4(
|
|
1551
1704
|
"QFAI-SC-009",
|
|
@@ -1573,17 +1726,6 @@ function validateScenarioContent(text, file) {
|
|
|
1573
1726
|
)
|
|
1574
1727
|
);
|
|
1575
1728
|
}
|
|
1576
|
-
if (document.scenarios.length > 1) {
|
|
1577
|
-
issues.push(
|
|
1578
|
-
issue4(
|
|
1579
|
-
"QFAI-SC-011",
|
|
1580
|
-
`Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
|
|
1581
|
-
"error",
|
|
1582
|
-
file,
|
|
1583
|
-
"scenario.single"
|
|
1584
|
-
)
|
|
1585
|
-
);
|
|
1586
|
-
}
|
|
1587
1729
|
for (const scenario of document.scenarios) {
|
|
1588
1730
|
if (scenario.tags.length === 0) {
|
|
1589
1731
|
issues.push(
|
|
@@ -1604,12 +1746,6 @@ function validateScenarioContent(text, file) {
|
|
|
1604
1746
|
} else if (scTags.length > 1) {
|
|
1605
1747
|
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1606
1748
|
}
|
|
1607
|
-
if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
|
|
1608
|
-
missingTags.push("SPEC");
|
|
1609
|
-
}
|
|
1610
|
-
if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
|
|
1611
|
-
missingTags.push("BR");
|
|
1612
|
-
}
|
|
1613
1749
|
if (missingTags.length > 0) {
|
|
1614
1750
|
issues.push(
|
|
1615
1751
|
issue4(
|
|
@@ -1729,7 +1865,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1729
1865
|
"SC",
|
|
1730
1866
|
"UI",
|
|
1731
1867
|
"API",
|
|
1732
|
-
"
|
|
1868
|
+
"DB",
|
|
1733
1869
|
"ADR"
|
|
1734
1870
|
]);
|
|
1735
1871
|
if (invalidIds.length > 0) {
|
|
@@ -1844,9 +1980,8 @@ function isMissingFileError4(error) {
|
|
|
1844
1980
|
|
|
1845
1981
|
// src/core/validators/traceability.ts
|
|
1846
1982
|
import { readFile as readFile10 } from "fs/promises";
|
|
1847
|
-
var SC_TAG_RE5 = /^SC-\d{4}$/;
|
|
1848
1983
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1849
|
-
var
|
|
1984
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1850
1985
|
async function validateTraceability(root, config) {
|
|
1851
1986
|
const issues = [];
|
|
1852
1987
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1859,8 +1994,7 @@ async function validateTraceability(root, config) {
|
|
|
1859
1994
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
1860
1995
|
const brIdsInScenarios = /* @__PURE__ */ new Set();
|
|
1861
1996
|
const scIdsInScenarios = /* @__PURE__ */ new Set();
|
|
1862
|
-
const
|
|
1863
|
-
const scWithContracts = /* @__PURE__ */ new Set();
|
|
1997
|
+
const specContractIds = /* @__PURE__ */ new Set();
|
|
1864
1998
|
const specToBrIds = /* @__PURE__ */ new Map();
|
|
1865
1999
|
const contractIndex = await buildContractIndex(root, config);
|
|
1866
2000
|
const contractIds = contractIndex.ids;
|
|
@@ -1873,19 +2007,60 @@ async function validateTraceability(root, config) {
|
|
|
1873
2007
|
}
|
|
1874
2008
|
const brIds = parsed.brs.map((br) => br.id);
|
|
1875
2009
|
brIds.forEach((id) => brIdsInSpecs.add(id));
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
const
|
|
2010
|
+
if (parsed.specId) {
|
|
2011
|
+
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
2012
|
+
brIds.forEach((id) => current.add(id));
|
|
2013
|
+
specToBrIds.set(parsed.specId, current);
|
|
2014
|
+
}
|
|
2015
|
+
const contractRefs = parsed.contractRefs;
|
|
2016
|
+
if (contractRefs.lines.length === 0) {
|
|
2017
|
+
issues.push(
|
|
2018
|
+
issue6(
|
|
2019
|
+
"QFAI-TRACE-020",
|
|
2020
|
+
"Spec \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
2021
|
+
"error",
|
|
2022
|
+
file,
|
|
2023
|
+
"traceability.specContractRefRequired"
|
|
2024
|
+
)
|
|
2025
|
+
);
|
|
2026
|
+
} else {
|
|
2027
|
+
if (contractRefs.hasNone && contractRefs.ids.length > 0) {
|
|
2028
|
+
issues.push(
|
|
2029
|
+
issue6(
|
|
2030
|
+
"QFAI-TRACE-021",
|
|
2031
|
+
"Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2032
|
+
"error",
|
|
2033
|
+
file,
|
|
2034
|
+
"traceability.specContractRefFormat"
|
|
2035
|
+
)
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
if (contractRefs.invalidTokens.length > 0) {
|
|
2039
|
+
issues.push(
|
|
2040
|
+
issue6(
|
|
2041
|
+
"QFAI-TRACE-021",
|
|
2042
|
+
`Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
|
|
2043
|
+
", "
|
|
2044
|
+
)}`,
|
|
2045
|
+
"error",
|
|
2046
|
+
file,
|
|
2047
|
+
"traceability.specContractRefFormat",
|
|
2048
|
+
contractRefs.invalidTokens
|
|
2049
|
+
)
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
contractRefs.ids.forEach((id) => {
|
|
2054
|
+
specContractIds.add(id);
|
|
2055
|
+
});
|
|
2056
|
+
const unknownContractIds = contractRefs.ids.filter(
|
|
1882
2057
|
(id) => !contractIds.has(id)
|
|
1883
2058
|
);
|
|
1884
2059
|
if (unknownContractIds.length > 0) {
|
|
1885
2060
|
issues.push(
|
|
1886
2061
|
issue6(
|
|
1887
|
-
"QFAI-TRACE-
|
|
1888
|
-
`Spec \u304C\
|
|
2062
|
+
"QFAI-TRACE-021",
|
|
2063
|
+
`Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1889
2064
|
", "
|
|
1890
2065
|
)}`,
|
|
1891
2066
|
"error",
|
|
@@ -1895,11 +2070,6 @@ async function validateTraceability(root, config) {
|
|
|
1895
2070
|
)
|
|
1896
2071
|
);
|
|
1897
2072
|
}
|
|
1898
|
-
if (parsed.specId) {
|
|
1899
|
-
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
1900
|
-
brIds.forEach((id) => current.add(id));
|
|
1901
|
-
specToBrIds.set(parsed.specId, current);
|
|
1902
|
-
}
|
|
1903
2073
|
}
|
|
1904
2074
|
for (const file of scenarioFiles) {
|
|
1905
2075
|
const text = await readFile10(file, "utf-8");
|
|
@@ -1909,20 +2079,42 @@ async function validateTraceability(root, config) {
|
|
|
1909
2079
|
continue;
|
|
1910
2080
|
}
|
|
1911
2081
|
const atoms = buildScenarioAtoms(document);
|
|
2082
|
+
const scIdsInFile = /* @__PURE__ */ new Set();
|
|
1912
2083
|
for (const [index, scenario] of document.scenarios.entries()) {
|
|
1913
2084
|
const atom = atoms[index];
|
|
1914
2085
|
if (!atom) {
|
|
1915
2086
|
continue;
|
|
1916
2087
|
}
|
|
1917
2088
|
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
1918
|
-
const brTags = scenario.tags.filter((tag) =>
|
|
1919
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2089
|
+
const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
|
|
2090
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
|
|
2091
|
+
if (specTags.length === 0) {
|
|
2092
|
+
issues.push(
|
|
2093
|
+
issue6(
|
|
2094
|
+
"QFAI-TRACE-014",
|
|
2095
|
+
`Scenario \u304C SPEC \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
|
|
2096
|
+
"error",
|
|
2097
|
+
file,
|
|
2098
|
+
"traceability.scenarioSpecRequired"
|
|
2099
|
+
)
|
|
2100
|
+
);
|
|
2101
|
+
}
|
|
2102
|
+
if (brTags.length === 0) {
|
|
2103
|
+
issues.push(
|
|
2104
|
+
issue6(
|
|
2105
|
+
"QFAI-TRACE-015",
|
|
2106
|
+
`Scenario \u304C BR \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
|
|
2107
|
+
"error",
|
|
2108
|
+
file,
|
|
2109
|
+
"traceability.scenarioBrRequired"
|
|
2110
|
+
)
|
|
2111
|
+
);
|
|
1925
2112
|
}
|
|
2113
|
+
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
2114
|
+
scTags.forEach((id) => {
|
|
2115
|
+
scIdsInScenarios.add(id);
|
|
2116
|
+
scIdsInFile.add(id);
|
|
2117
|
+
});
|
|
1926
2118
|
const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
|
|
1927
2119
|
if (unknownSpecIds.length > 0) {
|
|
1928
2120
|
issues.push(
|
|
@@ -1996,6 +2188,22 @@ async function validateTraceability(root, config) {
|
|
|
1996
2188
|
}
|
|
1997
2189
|
}
|
|
1998
2190
|
}
|
|
2191
|
+
if (scIdsInFile.size !== 1) {
|
|
2192
|
+
const invalidScIds = Array.from(scIdsInFile).sort(
|
|
2193
|
+
(a, b) => a.localeCompare(b)
|
|
2194
|
+
);
|
|
2195
|
+
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(", ")}`;
|
|
2196
|
+
issues.push(
|
|
2197
|
+
issue6(
|
|
2198
|
+
"QFAI-TRACE-012",
|
|
2199
|
+
`Spec entry \u304C Spec:SC=1:1 \u3092\u6E80\u305F\u3057\u3066\u3044\u307E\u305B\u3093: ${detail}`,
|
|
2200
|
+
"error",
|
|
2201
|
+
file,
|
|
2202
|
+
"traceability.specScOneToOne",
|
|
2203
|
+
invalidScIds
|
|
2204
|
+
)
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
1999
2207
|
}
|
|
2000
2208
|
if (upstreamIds.size === 0) {
|
|
2001
2209
|
return [
|
|
@@ -2025,40 +2233,62 @@ async function validateTraceability(root, config) {
|
|
|
2025
2233
|
);
|
|
2026
2234
|
}
|
|
2027
2235
|
}
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2236
|
+
const scRefsResult = await collectScTestReferences(
|
|
2237
|
+
root,
|
|
2238
|
+
config.validation.traceability.testFileGlobs,
|
|
2239
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2240
|
+
);
|
|
2241
|
+
const scTestRefs = scRefsResult.refs;
|
|
2242
|
+
const testFileScan = scRefsResult.scan;
|
|
2243
|
+
const hasScenarios = scIdsInScenarios.size > 0;
|
|
2244
|
+
const hasGlobConfig = testFileScan.globs.length > 0;
|
|
2245
|
+
const hasMatchedTests = testFileScan.matchedFileCount > 0;
|
|
2246
|
+
if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
|
|
2247
|
+
const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
|
|
2248
|
+
issues.push(
|
|
2249
|
+
issue6(
|
|
2250
|
+
"QFAI-TRACE-013",
|
|
2251
|
+
`\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}`,
|
|
2252
|
+
"error",
|
|
2253
|
+
testsRoot,
|
|
2254
|
+
"traceability.testFileGlobs"
|
|
2255
|
+
)
|
|
2256
|
+
);
|
|
2257
|
+
} else {
|
|
2258
|
+
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
2259
|
+
const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
|
|
2260
|
+
const refs = scTestRefs.get(id);
|
|
2261
|
+
return !refs || refs.size === 0;
|
|
2262
|
+
});
|
|
2263
|
+
if (scWithoutTests.length > 0) {
|
|
2264
|
+
issues.push(
|
|
2265
|
+
issue6(
|
|
2266
|
+
"QFAI-TRACE-010",
|
|
2267
|
+
`SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(
|
|
2268
|
+
", "
|
|
2269
|
+
)}\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`,
|
|
2270
|
+
config.validation.traceability.scNoTestSeverity,
|
|
2271
|
+
testsRoot,
|
|
2272
|
+
"traceability.scMustHaveTest",
|
|
2273
|
+
scWithoutTests
|
|
2274
|
+
)
|
|
2275
|
+
);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
const unknownScIds = Array.from(scTestRefs.keys()).filter(
|
|
2279
|
+
(id) => !scIdsInScenarios.has(id)
|
|
2031
2280
|
);
|
|
2032
|
-
if (
|
|
2281
|
+
if (unknownScIds.length > 0) {
|
|
2033
2282
|
issues.push(
|
|
2034
2283
|
issue6(
|
|
2035
|
-
"
|
|
2036
|
-
|
|
2284
|
+
"QFAI-TRACE-011",
|
|
2285
|
+
`\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(
|
|
2037
2286
|
", "
|
|
2038
2287
|
)}`,
|
|
2039
2288
|
"error",
|
|
2040
|
-
specsRoot,
|
|
2041
|
-
"traceability.scMustTouchContracts",
|
|
2042
|
-
scWithoutContracts
|
|
2043
|
-
)
|
|
2044
|
-
);
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
2048
|
-
const scTestRefs = await collectScTestReferences(testsRoot);
|
|
2049
|
-
const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
|
|
2050
|
-
const refs = scTestRefs.get(id);
|
|
2051
|
-
return !refs || refs.size === 0;
|
|
2052
|
-
});
|
|
2053
|
-
if (scWithoutTests.length > 0) {
|
|
2054
|
-
issues.push(
|
|
2055
|
-
issue6(
|
|
2056
|
-
"QFAI-TRACE-010",
|
|
2057
|
-
`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`,
|
|
2058
|
-
config.validation.traceability.scNoTestSeverity,
|
|
2059
2289
|
testsRoot,
|
|
2060
|
-
"traceability.
|
|
2061
|
-
|
|
2290
|
+
"traceability.scUnknownInTests",
|
|
2291
|
+
unknownScIds
|
|
2062
2292
|
)
|
|
2063
2293
|
);
|
|
2064
2294
|
}
|
|
@@ -2066,16 +2296,16 @@ async function validateTraceability(root, config) {
|
|
|
2066
2296
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
2067
2297
|
if (contractIds.size > 0) {
|
|
2068
2298
|
const orphanContracts = Array.from(contractIds).filter(
|
|
2069
|
-
(id) => !
|
|
2299
|
+
(id) => !specContractIds.has(id)
|
|
2070
2300
|
);
|
|
2071
2301
|
if (orphanContracts.length > 0) {
|
|
2072
2302
|
issues.push(
|
|
2073
2303
|
issue6(
|
|
2074
|
-
"
|
|
2075
|
-
`\u5951\u7D04\u304C
|
|
2304
|
+
"QFAI-TRACE-022",
|
|
2305
|
+
`\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
2076
2306
|
"error",
|
|
2077
2307
|
specsRoot,
|
|
2078
|
-
"traceability.
|
|
2308
|
+
"traceability.contractCoverage",
|
|
2079
2309
|
orphanContracts
|
|
2080
2310
|
)
|
|
2081
2311
|
);
|
|
@@ -2121,8 +2351,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2121
2351
|
issues.push(
|
|
2122
2352
|
issue6(
|
|
2123
2353
|
"QFAI-TRACE-002",
|
|
2124
|
-
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
2125
|
-
"
|
|
2354
|
+
"\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",
|
|
2355
|
+
"info",
|
|
2126
2356
|
srcRoot,
|
|
2127
2357
|
"traceability.codeReferences"
|
|
2128
2358
|
)
|
|
@@ -2165,11 +2395,24 @@ async function validateProject(root, configResult) {
|
|
|
2165
2395
|
...await validateDefinedIds(root, config),
|
|
2166
2396
|
...await validateTraceability(root, config)
|
|
2167
2397
|
];
|
|
2398
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2399
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
2400
|
+
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2401
|
+
const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
|
|
2402
|
+
root,
|
|
2403
|
+
config.validation.traceability.testFileGlobs,
|
|
2404
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2405
|
+
);
|
|
2406
|
+
const scCoverage = buildScCoverage(scIds, scTestRefs);
|
|
2168
2407
|
const toolVersion = await resolveToolVersion();
|
|
2169
2408
|
return {
|
|
2170
2409
|
toolVersion,
|
|
2171
2410
|
issues,
|
|
2172
|
-
counts: countIssues(issues)
|
|
2411
|
+
counts: countIssues(issues),
|
|
2412
|
+
traceability: {
|
|
2413
|
+
sc: scCoverage,
|
|
2414
|
+
testFiles
|
|
2415
|
+
}
|
|
2173
2416
|
};
|
|
2174
2417
|
}
|
|
2175
2418
|
function countIssues(issues) {
|
|
@@ -2183,16 +2426,16 @@ function countIssues(issues) {
|
|
|
2183
2426
|
}
|
|
2184
2427
|
|
|
2185
2428
|
// src/core/report.ts
|
|
2186
|
-
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "
|
|
2429
|
+
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2187
2430
|
async function createReportData(root, validation, configResult) {
|
|
2188
2431
|
const resolved = configResult ?? await loadConfig(root);
|
|
2189
2432
|
const config = resolved.config;
|
|
2190
2433
|
const configPath = resolved.configPath;
|
|
2191
2434
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2192
2435
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2193
|
-
const apiRoot =
|
|
2194
|
-
const uiRoot =
|
|
2195
|
-
const dbRoot =
|
|
2436
|
+
const apiRoot = path11.join(contractsRoot, "api");
|
|
2437
|
+
const uiRoot = path11.join(contractsRoot, "ui");
|
|
2438
|
+
const dbRoot = path11.join(contractsRoot, "db");
|
|
2196
2439
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
2197
2440
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
2198
2441
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -2202,6 +2445,23 @@ async function createReportData(root, validation, configResult) {
|
|
|
2202
2445
|
ui: uiFiles,
|
|
2203
2446
|
db: dbFiles
|
|
2204
2447
|
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
2448
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
2449
|
+
const specContractRefs = await collectSpecContractRefs(specFiles);
|
|
2450
|
+
const contractIdList = Array.from(contractIndex.ids);
|
|
2451
|
+
const referencedContracts = /* @__PURE__ */ new Set();
|
|
2452
|
+
for (const ids of specContractRefs.specToContractIds.values()) {
|
|
2453
|
+
ids.forEach((id) => referencedContracts.add(id));
|
|
2454
|
+
}
|
|
2455
|
+
const referencedContractCount = contractIdList.filter(
|
|
2456
|
+
(id) => referencedContracts.has(id)
|
|
2457
|
+
).length;
|
|
2458
|
+
const orphanContractCount = contractIdList.filter(
|
|
2459
|
+
(id) => !referencedContracts.has(id)
|
|
2460
|
+
).length;
|
|
2461
|
+
const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
|
|
2462
|
+
const specToContractIdsRecord = mapToSortedRecord(
|
|
2463
|
+
specContractRefs.specToContractIds
|
|
2464
|
+
);
|
|
2205
2465
|
const idsByPrefix = await collectIds([
|
|
2206
2466
|
...specFiles,
|
|
2207
2467
|
...scenarioFiles,
|
|
@@ -2219,8 +2479,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
2219
2479
|
testsRoot
|
|
2220
2480
|
);
|
|
2221
2481
|
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2222
|
-
const
|
|
2223
|
-
|
|
2482
|
+
const scRefsResult = await collectScTestReferences(
|
|
2483
|
+
root,
|
|
2484
|
+
config.validation.traceability.testFileGlobs,
|
|
2485
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2486
|
+
);
|
|
2487
|
+
const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
|
|
2488
|
+
const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
|
|
2489
|
+
const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
|
|
2490
|
+
const scSourceRecord = mapToSortedRecord(scSources);
|
|
2224
2491
|
const resolvedValidation = validation ?? await validateProject(root, resolved);
|
|
2225
2492
|
const version = await resolveToolVersion();
|
|
2226
2493
|
return {
|
|
@@ -2245,12 +2512,24 @@ async function createReportData(root, validation, configResult) {
|
|
|
2245
2512
|
sc: idsByPrefix.SC,
|
|
2246
2513
|
ui: idsByPrefix.UI,
|
|
2247
2514
|
api: idsByPrefix.API,
|
|
2248
|
-
|
|
2515
|
+
db: idsByPrefix.DB
|
|
2249
2516
|
},
|
|
2250
2517
|
traceability: {
|
|
2251
2518
|
upstreamIdsFound: upstreamIds.size,
|
|
2252
2519
|
referencedInCodeOrTests: traceability,
|
|
2253
|
-
sc: scCoverage
|
|
2520
|
+
sc: scCoverage,
|
|
2521
|
+
scSources: scSourceRecord,
|
|
2522
|
+
testFiles,
|
|
2523
|
+
contracts: {
|
|
2524
|
+
total: contractIdList.length,
|
|
2525
|
+
referenced: referencedContractCount,
|
|
2526
|
+
orphan: orphanContractCount,
|
|
2527
|
+
idToSpecs: contractIdToSpecsRecord
|
|
2528
|
+
},
|
|
2529
|
+
specs: {
|
|
2530
|
+
contractRefMissing: specContractRefs.missingRefSpecs.size,
|
|
2531
|
+
specToContractIds: specToContractIdsRecord
|
|
2532
|
+
}
|
|
2254
2533
|
},
|
|
2255
2534
|
issues: resolvedValidation.issues
|
|
2256
2535
|
};
|
|
@@ -2279,7 +2558,7 @@ function formatReportMarkdown(data) {
|
|
|
2279
2558
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
2280
2559
|
lines.push(formatIdLine("UI", data.ids.ui));
|
|
2281
2560
|
lines.push(formatIdLine("API", data.ids.api));
|
|
2282
|
-
lines.push(formatIdLine("
|
|
2561
|
+
lines.push(formatIdLine("DB", data.ids.db));
|
|
2283
2562
|
lines.push("");
|
|
2284
2563
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
2285
2564
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
@@ -2287,14 +2566,77 @@ function formatReportMarkdown(data) {
|
|
|
2287
2566
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2288
2567
|
);
|
|
2289
2568
|
lines.push("");
|
|
2569
|
+
lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2570
|
+
lines.push(`- total: ${data.traceability.contracts.total}`);
|
|
2571
|
+
lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
|
|
2572
|
+
lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
|
|
2573
|
+
lines.push(
|
|
2574
|
+
`- specContractRefMissing: ${data.traceability.specs.contractRefMissing}`
|
|
2575
|
+
);
|
|
2576
|
+
lines.push("");
|
|
2577
|
+
lines.push("## \u5951\u7D04\u2192Spec");
|
|
2578
|
+
const contractToSpecs = data.traceability.contracts.idToSpecs;
|
|
2579
|
+
const contractIds = Object.keys(contractToSpecs).sort(
|
|
2580
|
+
(a, b) => a.localeCompare(b)
|
|
2581
|
+
);
|
|
2582
|
+
if (contractIds.length === 0) {
|
|
2583
|
+
lines.push("- (none)");
|
|
2584
|
+
} else {
|
|
2585
|
+
for (const contractId of contractIds) {
|
|
2586
|
+
const specs = contractToSpecs[contractId] ?? [];
|
|
2587
|
+
if (specs.length === 0) {
|
|
2588
|
+
lines.push(`- ${contractId}: (none)`);
|
|
2589
|
+
} else {
|
|
2590
|
+
lines.push(`- ${contractId}: ${specs.join(", ")}`);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
lines.push("");
|
|
2595
|
+
lines.push("## Spec\u2192\u5951\u7D04");
|
|
2596
|
+
const specToContracts = data.traceability.specs.specToContractIds;
|
|
2597
|
+
const specIds = Object.keys(specToContracts).sort(
|
|
2598
|
+
(a, b) => a.localeCompare(b)
|
|
2599
|
+
);
|
|
2600
|
+
if (specIds.length === 0) {
|
|
2601
|
+
lines.push("- (none)");
|
|
2602
|
+
} else {
|
|
2603
|
+
for (const specId of specIds) {
|
|
2604
|
+
const contractIds2 = specToContracts[specId] ?? [];
|
|
2605
|
+
if (contractIds2.length === 0) {
|
|
2606
|
+
lines.push(`- ${specId}: (none)`);
|
|
2607
|
+
} else {
|
|
2608
|
+
lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
lines.push("");
|
|
2290
2613
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2291
2614
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2292
2615
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2293
2616
|
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
2617
|
+
lines.push(
|
|
2618
|
+
`- testFileGlobs: ${formatList(data.traceability.testFiles.globs)}`
|
|
2619
|
+
);
|
|
2620
|
+
lines.push(
|
|
2621
|
+
`- testFileExcludeGlobs: ${formatList(
|
|
2622
|
+
data.traceability.testFiles.excludeGlobs
|
|
2623
|
+
)}`
|
|
2624
|
+
);
|
|
2625
|
+
lines.push(
|
|
2626
|
+
`- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
|
|
2627
|
+
);
|
|
2294
2628
|
if (data.traceability.sc.missingIds.length === 0) {
|
|
2295
2629
|
lines.push("- missingIds: (none)");
|
|
2296
2630
|
} else {
|
|
2297
|
-
|
|
2631
|
+
const sources = data.traceability.scSources;
|
|
2632
|
+
const missingWithSources = data.traceability.sc.missingIds.map((id) => {
|
|
2633
|
+
const files = sources[id] ?? [];
|
|
2634
|
+
if (files.length === 0) {
|
|
2635
|
+
return id;
|
|
2636
|
+
}
|
|
2637
|
+
return `${id} (${files.join(", ")})`;
|
|
2638
|
+
});
|
|
2639
|
+
lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
|
|
2298
2640
|
}
|
|
2299
2641
|
lines.push("");
|
|
2300
2642
|
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
@@ -2313,6 +2655,20 @@ function formatReportMarkdown(data) {
|
|
|
2313
2655
|
}
|
|
2314
2656
|
}
|
|
2315
2657
|
lines.push("");
|
|
2658
|
+
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
2659
|
+
const specScIssues = data.issues.filter(
|
|
2660
|
+
(item) => item.code === "QFAI-TRACE-012"
|
|
2661
|
+
);
|
|
2662
|
+
if (specScIssues.length === 0) {
|
|
2663
|
+
lines.push("- (none)");
|
|
2664
|
+
} else {
|
|
2665
|
+
for (const item of specScIssues) {
|
|
2666
|
+
const location = item.file ?? "(unknown)";
|
|
2667
|
+
const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
|
|
2668
|
+
lines.push(`- ${location}: ${refs}`);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
lines.push("");
|
|
2316
2672
|
lines.push("## Hotspots");
|
|
2317
2673
|
const hotspots = buildHotspots(data.issues);
|
|
2318
2674
|
if (hotspots.length === 0) {
|
|
@@ -2327,7 +2683,7 @@ function formatReportMarkdown(data) {
|
|
|
2327
2683
|
lines.push("");
|
|
2328
2684
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
2329
2685
|
const traceIssues = data.issues.filter(
|
|
2330
|
-
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2686
|
+
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2331
2687
|
);
|
|
2332
2688
|
if (traceIssues.length === 0) {
|
|
2333
2689
|
lines.push("- (none)");
|
|
@@ -2357,6 +2713,33 @@ function formatReportMarkdown(data) {
|
|
|
2357
2713
|
function formatReportJson(data) {
|
|
2358
2714
|
return JSON.stringify(data, null, 2);
|
|
2359
2715
|
}
|
|
2716
|
+
async function collectSpecContractRefs(specFiles) {
|
|
2717
|
+
const specToContractIds = /* @__PURE__ */ new Map();
|
|
2718
|
+
const idToSpecs = /* @__PURE__ */ new Map();
|
|
2719
|
+
const missingRefSpecs = /* @__PURE__ */ new Set();
|
|
2720
|
+
for (const file of specFiles) {
|
|
2721
|
+
const text = await readFile11(file, "utf-8");
|
|
2722
|
+
const parsed = parseSpec(text, file);
|
|
2723
|
+
const specKey = parsed.specId ?? file;
|
|
2724
|
+
const refs = parsed.contractRefs;
|
|
2725
|
+
if (refs.lines.length === 0) {
|
|
2726
|
+
missingRefSpecs.add(specKey);
|
|
2727
|
+
}
|
|
2728
|
+
const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
|
|
2729
|
+
for (const id of refs.ids) {
|
|
2730
|
+
currentContracts.add(id);
|
|
2731
|
+
const specs = idToSpecs.get(id) ?? /* @__PURE__ */ new Set();
|
|
2732
|
+
specs.add(specKey);
|
|
2733
|
+
idToSpecs.set(id, specs);
|
|
2734
|
+
}
|
|
2735
|
+
specToContractIds.set(specKey, currentContracts);
|
|
2736
|
+
}
|
|
2737
|
+
return {
|
|
2738
|
+
specToContractIds,
|
|
2739
|
+
idToSpecs,
|
|
2740
|
+
missingRefSpecs
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2360
2743
|
async function collectIds(files) {
|
|
2361
2744
|
const result = {
|
|
2362
2745
|
SPEC: /* @__PURE__ */ new Set(),
|
|
@@ -2364,7 +2747,7 @@ async function collectIds(files) {
|
|
|
2364
2747
|
SC: /* @__PURE__ */ new Set(),
|
|
2365
2748
|
UI: /* @__PURE__ */ new Set(),
|
|
2366
2749
|
API: /* @__PURE__ */ new Set(),
|
|
2367
|
-
|
|
2750
|
+
DB: /* @__PURE__ */ new Set()
|
|
2368
2751
|
};
|
|
2369
2752
|
for (const file of files) {
|
|
2370
2753
|
const text = await readFile11(file, "utf-8");
|
|
@@ -2379,7 +2762,7 @@ async function collectIds(files) {
|
|
|
2379
2762
|
SC: toSortedArray2(result.SC),
|
|
2380
2763
|
UI: toSortedArray2(result.UI),
|
|
2381
2764
|
API: toSortedArray2(result.API),
|
|
2382
|
-
|
|
2765
|
+
DB: toSortedArray2(result.DB)
|
|
2383
2766
|
};
|
|
2384
2767
|
}
|
|
2385
2768
|
async function collectUpstreamIds(files) {
|
|
@@ -2423,9 +2806,22 @@ function formatIdLine(label, values) {
|
|
|
2423
2806
|
}
|
|
2424
2807
|
return `- ${label}: ${values.join(", ")}`;
|
|
2425
2808
|
}
|
|
2809
|
+
function formatList(values) {
|
|
2810
|
+
if (values.length === 0) {
|
|
2811
|
+
return "(none)";
|
|
2812
|
+
}
|
|
2813
|
+
return values.join(", ");
|
|
2814
|
+
}
|
|
2426
2815
|
function toSortedArray2(values) {
|
|
2427
2816
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2428
2817
|
}
|
|
2818
|
+
function mapToSortedRecord(values) {
|
|
2819
|
+
const record2 = {};
|
|
2820
|
+
for (const [key, files] of values.entries()) {
|
|
2821
|
+
record2[key] = Array.from(files).sort((a, b) => a.localeCompare(b));
|
|
2822
|
+
}
|
|
2823
|
+
return record2;
|
|
2824
|
+
}
|
|
2429
2825
|
function buildHotspots(issues) {
|
|
2430
2826
|
const map = /* @__PURE__ */ new Map();
|
|
2431
2827
|
for (const issue7 of issues) {
|