qfai 0.4.2 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/assets/init/.qfai/README.md +1 -0
- package/assets/init/.qfai/contracts/README.md +21 -11
- package/assets/init/.qfai/contracts/api/api-0001-sample.yaml +3 -2
- package/assets/init/.qfai/contracts/db/db-0001-sample.sql +2 -1
- package/assets/init/.qfai/contracts/ui/ui-0001-sample.yaml +3 -1
- package/assets/init/.qfai/promptpack/modes/change.md +3 -2
- package/assets/init/.qfai/promptpack/modes/compatibility.md +2 -0
- package/assets/init/.qfai/promptpack/steering/traceability.md +5 -4
- package/assets/init/.qfai/prompts/makeOverview.md +1 -1
- package/assets/init/.qfai/prompts/require-to-spec.md +4 -2
- package/assets/init/.qfai/specs/README.md +9 -2
- package/assets/init/.qfai/specs/spec-0001/scenario.md +1 -1
- package/assets/init/.qfai/specs/spec-0001/spec.md +2 -0
- package/assets/init/root/qfai.config.yaml +0 -1
- package/dist/cli/commands/init.d.ts +8 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +30 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/report.d.ts +7 -0
- package/dist/cli/commands/report.d.ts.map +1 -0
- package/dist/cli/commands/report.js +108 -0
- package/dist/cli/commands/report.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +9 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +57 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.cjs +536 -336
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/index.mjs +536 -336
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/lib/args.d.ts +18 -0
- package/dist/cli/lib/args.d.ts.map +1 -0
- package/dist/cli/lib/args.js +98 -0
- package/dist/cli/lib/args.js.map +1 -0
- package/dist/cli/lib/assets.d.ts +2 -0
- package/dist/cli/lib/assets.d.ts.map +1 -0
- package/dist/cli/lib/assets.js +24 -0
- package/dist/cli/lib/assets.js.map +1 -0
- package/dist/cli/lib/failOn.d.ts +5 -0
- package/dist/cli/lib/failOn.d.ts.map +1 -0
- package/dist/cli/lib/failOn.js +10 -0
- package/dist/cli/lib/failOn.js.map +1 -0
- package/dist/cli/lib/fs.d.ts +11 -0
- package/dist/cli/lib/fs.d.ts.map +1 -0
- package/dist/cli/lib/fs.js +91 -0
- package/dist/cli/lib/fs.js.map +1 -0
- package/dist/cli/lib/logger.d.ts +4 -0
- package/dist/cli/lib/logger.d.ts.map +1 -0
- package/dist/cli/lib/logger.js +10 -0
- package/dist/cli/lib/logger.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +66 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/core/config.d.ts +47 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +224 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/contractIndex.d.ts +12 -0
- package/dist/core/contractIndex.d.ts.map +1 -0
- package/dist/core/contractIndex.js +38 -0
- package/dist/core/contractIndex.js.map +1 -0
- package/dist/core/contracts.d.ts +5 -0
- package/dist/core/contracts.d.ts.map +1 -0
- package/dist/core/contracts.js +42 -0
- package/dist/core/contracts.js.map +1 -0
- package/dist/core/contractsDecl.d.ts +3 -0
- package/dist/core/contractsDecl.d.ts.map +1 -0
- package/dist/core/contractsDecl.js +19 -0
- package/dist/core/contractsDecl.js.map +1 -0
- package/dist/core/discovery.d.ts +14 -0
- package/dist/core/discovery.d.ts.map +1 -0
- package/dist/core/discovery.js +55 -0
- package/dist/core/discovery.js.map +1 -0
- package/dist/core/fs.d.ts +11 -0
- package/dist/core/fs.d.ts.map +1 -0
- package/dist/core/fs.js +68 -0
- package/dist/core/fs.js.map +1 -0
- package/dist/core/gherkin/parse.d.ts +7 -0
- package/dist/core/gherkin/parse.d.ts.map +1 -0
- package/dist/core/gherkin/parse.js +25 -0
- package/dist/core/gherkin/parse.js.map +1 -0
- package/dist/core/ids.d.ts +6 -0
- package/dist/core/ids.d.ts.map +1 -0
- package/dist/core/ids.js +52 -0
- package/dist/core/ids.js.map +1 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +13 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/parse/adr.d.ts +13 -0
- package/dist/core/parse/adr.d.ts.map +1 -0
- package/dist/core/parse/adr.js +33 -0
- package/dist/core/parse/adr.js.map +1 -0
- package/dist/core/parse/gherkin.d.ts +12 -0
- package/dist/core/parse/gherkin.d.ts.map +1 -0
- package/dist/core/parse/gherkin.js +22 -0
- package/dist/core/parse/gherkin.js.map +1 -0
- package/dist/core/parse/markdown.d.ts +14 -0
- package/dist/core/parse/markdown.d.ts.map +1 -0
- package/dist/core/parse/markdown.js +45 -0
- package/dist/core/parse/markdown.js.map +1 -0
- package/dist/core/parse/spec.d.ts +36 -0
- package/dist/core/parse/spec.d.ts.map +1 -0
- package/dist/core/parse/spec.js +123 -0
- package/dist/core/parse/spec.js.map +1 -0
- package/dist/core/report.d.ts +55 -0
- package/dist/core/report.d.ts.map +1 -0
- package/dist/core/report.js +393 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/scenarioModel.d.ts +33 -0
- package/dist/core/scenarioModel.d.ts.map +1 -0
- package/dist/core/scenarioModel.js +128 -0
- package/dist/core/scenarioModel.js.map +1 -0
- package/dist/core/specLayout.d.ts +8 -0
- package/dist/core/specLayout.d.ts.map +1 -0
- package/dist/core/specLayout.js +36 -0
- package/dist/core/specLayout.js.map +1 -0
- package/dist/core/traceability.d.ts +26 -0
- package/dist/core/traceability.d.ts.map +1 -0
- package/dist/core/traceability.js +157 -0
- package/dist/core/traceability.js.map +1 -0
- package/dist/core/types.d.ts +31 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/validate.d.ts +4 -0
- package/dist/core/validate.d.ts.map +1 -0
- package/dist/core/validate.js +45 -0
- package/dist/core/validate.js.map +1 -0
- package/dist/core/validators/contracts.d.ts +5 -0
- package/dist/core/validators/contracts.d.ts.map +1 -0
- package/dist/core/validators/contracts.js +189 -0
- package/dist/core/validators/contracts.js.map +1 -0
- package/dist/core/validators/delta.d.ts +4 -0
- package/dist/core/validators/delta.d.ts.map +1 -0
- package/dist/core/validators/delta.js +68 -0
- package/dist/core/validators/delta.js.map +1 -0
- package/dist/core/validators/ids.d.ts +4 -0
- package/dist/core/validators/ids.d.ts.map +1 -0
- package/dist/core/validators/ids.js +88 -0
- package/dist/core/validators/ids.js.map +1 -0
- package/dist/core/validators/scenario.d.ts +5 -0
- package/dist/core/validators/scenario.d.ts.map +1 -0
- package/dist/core/validators/scenario.js +127 -0
- package/dist/core/validators/scenario.js.map +1 -0
- package/dist/core/validators/spec.d.ts +5 -0
- package/dist/core/validators/spec.d.ts.map +1 -0
- package/dist/core/validators/spec.js +94 -0
- package/dist/core/validators/spec.js.map +1 -0
- package/dist/core/validators/traceability.d.ts +4 -0
- package/dist/core/validators/traceability.d.ts.map +1 -0
- package/dist/core/validators/traceability.js +222 -0
- package/dist/core/validators/traceability.js.map +1 -0
- package/dist/core/version.d.ts +2 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/core/version.js +25 -0
- package/dist/core/version.js.map +1 -0
- package/dist/index.cjs +511 -335
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -5
- package/dist/index.d.ts +2 -156
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +511 -335
- package/dist/index.mjs.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -27,7 +27,6 @@ var defaultConfig = {
|
|
|
27
27
|
},
|
|
28
28
|
traceability: {
|
|
29
29
|
brMustHaveSc: true,
|
|
30
|
-
scMustTouchContracts: true,
|
|
31
30
|
scMustHaveTest: true,
|
|
32
31
|
testFileGlobs: [],
|
|
33
32
|
testFileExcludeGlobs: [],
|
|
@@ -204,13 +203,6 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
204
203
|
configPath,
|
|
205
204
|
issues
|
|
206
205
|
),
|
|
207
|
-
scMustTouchContracts: readBoolean(
|
|
208
|
-
traceabilityRaw?.scMustTouchContracts,
|
|
209
|
-
base.traceability.scMustTouchContracts,
|
|
210
|
-
"validation.traceability.scMustTouchContracts",
|
|
211
|
-
configPath,
|
|
212
|
-
issues
|
|
213
|
-
),
|
|
214
206
|
scMustHaveTest: readBoolean(
|
|
215
207
|
traceabilityRaw?.scMustHaveTest,
|
|
216
208
|
base.traceability.scMustHaveTest,
|
|
@@ -364,14 +356,14 @@ function isRecord(value) {
|
|
|
364
356
|
}
|
|
365
357
|
|
|
366
358
|
// src/core/ids.ts
|
|
367
|
-
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "
|
|
359
|
+
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
368
360
|
var STRICT_ID_PATTERNS = {
|
|
369
361
|
SPEC: /\bSPEC-\d{4}\b/g,
|
|
370
362
|
BR: /\bBR-\d{4}\b/g,
|
|
371
363
|
SC: /\bSC-\d{4}\b/g,
|
|
372
364
|
UI: /\bUI-\d{4}\b/g,
|
|
373
365
|
API: /\bAPI-\d{4}\b/g,
|
|
374
|
-
|
|
366
|
+
DB: /\bDB-\d{4}\b/g,
|
|
375
367
|
ADR: /\bADR-\d{4}\b/g
|
|
376
368
|
};
|
|
377
369
|
var LOOSE_ID_PATTERNS = {
|
|
@@ -380,7 +372,7 @@ var LOOSE_ID_PATTERNS = {
|
|
|
380
372
|
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
381
373
|
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
382
374
|
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
383
|
-
|
|
375
|
+
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
384
376
|
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
385
377
|
};
|
|
386
378
|
function extractIds(text, prefix) {
|
|
@@ -420,6 +412,10 @@ function isValidId(value, prefix) {
|
|
|
420
412
|
import { readFile as readFile11 } from "fs/promises";
|
|
421
413
|
import path11 from "path";
|
|
422
414
|
|
|
415
|
+
// src/core/contractIndex.ts
|
|
416
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
417
|
+
import path4 from "path";
|
|
418
|
+
|
|
423
419
|
// src/core/discovery.ts
|
|
424
420
|
import { access as access2 } from "fs/promises";
|
|
425
421
|
|
|
@@ -542,14 +538,14 @@ async function collectUiContractFiles(uiRoot) {
|
|
|
542
538
|
async function collectApiContractFiles(apiRoot) {
|
|
543
539
|
return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
|
|
544
540
|
}
|
|
545
|
-
async function
|
|
546
|
-
return collectFiles(
|
|
541
|
+
async function collectDbContractFiles(dbRoot) {
|
|
542
|
+
return collectFiles(dbRoot, { extensions: [".sql"] });
|
|
547
543
|
}
|
|
548
|
-
async function collectContractFiles(uiRoot, apiRoot,
|
|
544
|
+
async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
|
|
549
545
|
const [ui, api, db] = await Promise.all([
|
|
550
546
|
collectUiContractFiles(uiRoot),
|
|
551
547
|
collectApiContractFiles(apiRoot),
|
|
552
|
-
|
|
548
|
+
collectDbContractFiles(dbRoot)
|
|
553
549
|
]);
|
|
554
550
|
return { ui, api, db };
|
|
555
551
|
}
|
|
@@ -571,9 +567,221 @@ async function exists2(target) {
|
|
|
571
567
|
}
|
|
572
568
|
}
|
|
573
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
|
+
collectDbContractFiles(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
|
+
|
|
574
782
|
// src/core/traceability.ts
|
|
575
|
-
import { readFile as
|
|
576
|
-
import
|
|
783
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
784
|
+
import path5 from "path";
|
|
577
785
|
|
|
578
786
|
// src/core/gherkin/parse.ts
|
|
579
787
|
import {
|
|
@@ -610,7 +818,7 @@ var SC_TAG_RE = /^SC-\d{4}$/;
|
|
|
610
818
|
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
611
819
|
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
612
820
|
var API_TAG_RE = /^API-\d{4}$/;
|
|
613
|
-
var
|
|
821
|
+
var DB_TAG_RE = /^DB-\d{4}$/;
|
|
614
822
|
function parseScenarioDocument(text, uri) {
|
|
615
823
|
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
616
824
|
if (!gherkinDocument) {
|
|
@@ -639,10 +847,10 @@ function buildScenarioAtoms(document) {
|
|
|
639
847
|
return document.scenarios.map((scenario) => {
|
|
640
848
|
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
641
849
|
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
642
|
-
const brIds =
|
|
850
|
+
const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
643
851
|
const contractIds = /* @__PURE__ */ new Set();
|
|
644
852
|
scenario.tags.forEach((tag) => {
|
|
645
|
-
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)) {
|
|
646
854
|
contractIds.add(tag);
|
|
647
855
|
}
|
|
648
856
|
});
|
|
@@ -650,7 +858,7 @@ function buildScenarioAtoms(document) {
|
|
|
650
858
|
for (const text of collectStepTexts(step)) {
|
|
651
859
|
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
652
860
|
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
653
|
-
extractIds(text, "
|
|
861
|
+
extractIds(text, "DB").forEach((id) => contractIds.add(id));
|
|
654
862
|
}
|
|
655
863
|
}
|
|
656
864
|
const atom = {
|
|
@@ -729,7 +937,7 @@ function collectStepTexts(step) {
|
|
|
729
937
|
}
|
|
730
938
|
return texts;
|
|
731
939
|
}
|
|
732
|
-
function
|
|
940
|
+
function unique3(values) {
|
|
733
941
|
return Array.from(new Set(values));
|
|
734
942
|
}
|
|
735
943
|
|
|
@@ -759,7 +967,7 @@ function extractAnnotatedScIds(text) {
|
|
|
759
967
|
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
760
968
|
const scIds = /* @__PURE__ */ new Set();
|
|
761
969
|
for (const file of scenarioFiles) {
|
|
762
|
-
const text = await
|
|
970
|
+
const text = await readFile3(file, "utf-8");
|
|
763
971
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
764
972
|
if (!document || errors.length > 0) {
|
|
765
973
|
continue;
|
|
@@ -777,7 +985,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
|
777
985
|
async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
778
986
|
const sources = /* @__PURE__ */ new Map();
|
|
779
987
|
for (const file of scenarioFiles) {
|
|
780
|
-
const text = await
|
|
988
|
+
const text = await readFile3(file, "utf-8");
|
|
781
989
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
782
990
|
if (!document || errors.length > 0) {
|
|
783
991
|
continue;
|
|
@@ -830,10 +1038,10 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
830
1038
|
};
|
|
831
1039
|
}
|
|
832
1040
|
const normalizedFiles = Array.from(
|
|
833
|
-
new Set(files.map((file) =>
|
|
1041
|
+
new Set(files.map((file) => path5.normalize(file)))
|
|
834
1042
|
);
|
|
835
1043
|
for (const file of normalizedFiles) {
|
|
836
|
-
const text = await
|
|
1044
|
+
const text = await readFile3(file, "utf-8");
|
|
837
1045
|
const scIds = extractAnnotatedScIds(text);
|
|
838
1046
|
if (scIds.length === 0) {
|
|
839
1047
|
continue;
|
|
@@ -890,16 +1098,16 @@ function formatError3(error) {
|
|
|
890
1098
|
}
|
|
891
1099
|
|
|
892
1100
|
// src/core/version.ts
|
|
893
|
-
import { readFile as
|
|
894
|
-
import
|
|
1101
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1102
|
+
import path6 from "path";
|
|
895
1103
|
import { fileURLToPath } from "url";
|
|
896
1104
|
async function resolveToolVersion() {
|
|
897
|
-
if ("0.4.
|
|
898
|
-
return "0.4.
|
|
1105
|
+
if ("0.4.6".length > 0) {
|
|
1106
|
+
return "0.4.6";
|
|
899
1107
|
}
|
|
900
1108
|
try {
|
|
901
1109
|
const packagePath = resolvePackageJsonPath();
|
|
902
|
-
const raw = await
|
|
1110
|
+
const raw = await readFile4(packagePath, "utf-8");
|
|
903
1111
|
const parsed = JSON.parse(raw);
|
|
904
1112
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
905
1113
|
return version.length > 0 ? version : "unknown";
|
|
@@ -910,54 +1118,23 @@ async function resolveToolVersion() {
|
|
|
910
1118
|
function resolvePackageJsonPath() {
|
|
911
1119
|
const base = import.meta.url;
|
|
912
1120
|
const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
|
|
913
|
-
return
|
|
1121
|
+
return path6.resolve(path6.dirname(basePath), "../../package.json");
|
|
914
1122
|
}
|
|
915
1123
|
|
|
916
1124
|
// src/core/validators/contracts.ts
|
|
917
|
-
import { readFile as
|
|
918
|
-
import
|
|
1125
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1126
|
+
import path8 from "path";
|
|
919
1127
|
|
|
920
1128
|
// src/core/contracts.ts
|
|
921
|
-
import
|
|
1129
|
+
import path7 from "path";
|
|
922
1130
|
import { parse as parseYaml2 } from "yaml";
|
|
923
1131
|
function parseStructuredContract(file, text) {
|
|
924
|
-
const ext =
|
|
1132
|
+
const ext = path7.extname(file).toLowerCase();
|
|
925
1133
|
if (ext === ".json") {
|
|
926
1134
|
return JSON.parse(text);
|
|
927
1135
|
}
|
|
928
1136
|
return parseYaml2(text);
|
|
929
1137
|
}
|
|
930
|
-
function extractUiContractIds(doc) {
|
|
931
|
-
const id = typeof doc.id === "string" ? doc.id : "";
|
|
932
|
-
return extractIds(id, "UI");
|
|
933
|
-
}
|
|
934
|
-
function extractApiContractIds(doc) {
|
|
935
|
-
const operationIds = /* @__PURE__ */ new Set();
|
|
936
|
-
collectOperationIds(doc, operationIds);
|
|
937
|
-
const ids = /* @__PURE__ */ new Set();
|
|
938
|
-
for (const operationId of operationIds) {
|
|
939
|
-
extractIds(operationId, "API").forEach((id) => ids.add(id));
|
|
940
|
-
}
|
|
941
|
-
return Array.from(ids);
|
|
942
|
-
}
|
|
943
|
-
function collectOperationIds(value, out) {
|
|
944
|
-
if (!value || typeof value !== "object") {
|
|
945
|
-
return;
|
|
946
|
-
}
|
|
947
|
-
if (Array.isArray(value)) {
|
|
948
|
-
for (const item of value) {
|
|
949
|
-
collectOperationIds(item, out);
|
|
950
|
-
}
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
954
|
-
if (key === "operationId" && typeof entry === "string") {
|
|
955
|
-
out.add(entry);
|
|
956
|
-
continue;
|
|
957
|
-
}
|
|
958
|
-
collectOperationIds(entry, out);
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
1138
|
|
|
962
1139
|
// src/core/validators/contracts.ts
|
|
963
1140
|
var SQL_DANGEROUS_PATTERNS = [
|
|
@@ -972,9 +1149,11 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
972
1149
|
async function validateContracts(root, config) {
|
|
973
1150
|
const issues = [];
|
|
974
1151
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
975
|
-
issues.push(...await validateUiContracts(
|
|
976
|
-
issues.push(...await validateApiContracts(
|
|
977
|
-
issues.push(...await
|
|
1152
|
+
issues.push(...await validateUiContracts(path8.join(contractsRoot, "ui")));
|
|
1153
|
+
issues.push(...await validateApiContracts(path8.join(contractsRoot, "api")));
|
|
1154
|
+
issues.push(...await validateDbContracts(path8.join(contractsRoot, "db")));
|
|
1155
|
+
const contractIndex = await buildContractIndex(root, config);
|
|
1156
|
+
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
978
1157
|
return issues;
|
|
979
1158
|
}
|
|
980
1159
|
async function validateUiContracts(uiRoot) {
|
|
@@ -992,14 +1171,14 @@ async function validateUiContracts(uiRoot) {
|
|
|
992
1171
|
}
|
|
993
1172
|
const issues = [];
|
|
994
1173
|
for (const file of files) {
|
|
995
|
-
const text = await
|
|
1174
|
+
const text = await readFile5(file, "utf-8");
|
|
996
1175
|
const invalidIds = extractInvalidIds(text, [
|
|
997
1176
|
"SPEC",
|
|
998
1177
|
"BR",
|
|
999
1178
|
"SC",
|
|
1000
1179
|
"UI",
|
|
1001
1180
|
"API",
|
|
1002
|
-
"
|
|
1181
|
+
"DB",
|
|
1003
1182
|
"ADR"
|
|
1004
1183
|
]);
|
|
1005
1184
|
if (invalidIds.length > 0) {
|
|
@@ -1014,9 +1193,10 @@ async function validateUiContracts(uiRoot) {
|
|
|
1014
1193
|
)
|
|
1015
1194
|
);
|
|
1016
1195
|
}
|
|
1017
|
-
|
|
1196
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1197
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
|
|
1018
1198
|
try {
|
|
1019
|
-
|
|
1199
|
+
parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
1020
1200
|
} catch (error) {
|
|
1021
1201
|
issues.push(
|
|
1022
1202
|
issue(
|
|
@@ -1027,19 +1207,6 @@ async function validateUiContracts(uiRoot) {
|
|
|
1027
1207
|
"contracts.ui.parse"
|
|
1028
1208
|
)
|
|
1029
1209
|
);
|
|
1030
|
-
continue;
|
|
1031
|
-
}
|
|
1032
|
-
const uiIds = extractUiContractIds(doc);
|
|
1033
|
-
if (uiIds.length === 0) {
|
|
1034
|
-
issues.push(
|
|
1035
|
-
issue(
|
|
1036
|
-
"QFAI-CONTRACT-002",
|
|
1037
|
-
`UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
1038
|
-
"error",
|
|
1039
|
-
file,
|
|
1040
|
-
"contracts.ui.id"
|
|
1041
|
-
)
|
|
1042
|
-
);
|
|
1043
1210
|
}
|
|
1044
1211
|
}
|
|
1045
1212
|
return issues;
|
|
@@ -1059,14 +1226,14 @@ async function validateApiContracts(apiRoot) {
|
|
|
1059
1226
|
}
|
|
1060
1227
|
const issues = [];
|
|
1061
1228
|
for (const file of files) {
|
|
1062
|
-
const text = await
|
|
1229
|
+
const text = await readFile5(file, "utf-8");
|
|
1063
1230
|
const invalidIds = extractInvalidIds(text, [
|
|
1064
1231
|
"SPEC",
|
|
1065
1232
|
"BR",
|
|
1066
1233
|
"SC",
|
|
1067
1234
|
"UI",
|
|
1068
1235
|
"API",
|
|
1069
|
-
"
|
|
1236
|
+
"DB",
|
|
1070
1237
|
"ADR"
|
|
1071
1238
|
]);
|
|
1072
1239
|
if (invalidIds.length > 0) {
|
|
@@ -1081,9 +1248,11 @@ async function validateApiContracts(apiRoot) {
|
|
|
1081
1248
|
)
|
|
1082
1249
|
);
|
|
1083
1250
|
}
|
|
1251
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1252
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "API"));
|
|
1084
1253
|
let doc;
|
|
1085
1254
|
try {
|
|
1086
|
-
doc = parseStructuredContract(file, text);
|
|
1255
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
1087
1256
|
} catch (error) {
|
|
1088
1257
|
issues.push(
|
|
1089
1258
|
issue(
|
|
@@ -1107,44 +1276,32 @@ async function validateApiContracts(apiRoot) {
|
|
|
1107
1276
|
)
|
|
1108
1277
|
);
|
|
1109
1278
|
}
|
|
1110
|
-
const apiIds = extractApiContractIds(doc);
|
|
1111
|
-
if (apiIds.length === 0) {
|
|
1112
|
-
issues.push(
|
|
1113
|
-
issue(
|
|
1114
|
-
"QFAI-CONTRACT-002",
|
|
1115
|
-
`API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
|
|
1116
|
-
"error",
|
|
1117
|
-
file,
|
|
1118
|
-
"contracts.api.id"
|
|
1119
|
-
)
|
|
1120
|
-
);
|
|
1121
|
-
}
|
|
1122
1279
|
}
|
|
1123
1280
|
return issues;
|
|
1124
1281
|
}
|
|
1125
|
-
async function
|
|
1126
|
-
const files = await
|
|
1282
|
+
async function validateDbContracts(dbRoot) {
|
|
1283
|
+
const files = await collectDbContractFiles(dbRoot);
|
|
1127
1284
|
if (files.length === 0) {
|
|
1128
1285
|
return [
|
|
1129
1286
|
issue(
|
|
1130
|
-
"QFAI-
|
|
1131
|
-
"
|
|
1287
|
+
"QFAI-DB-000",
|
|
1288
|
+
"DB \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1132
1289
|
"info",
|
|
1133
|
-
|
|
1134
|
-
"contracts.
|
|
1290
|
+
dbRoot,
|
|
1291
|
+
"contracts.db.files"
|
|
1135
1292
|
)
|
|
1136
1293
|
];
|
|
1137
1294
|
}
|
|
1138
1295
|
const issues = [];
|
|
1139
1296
|
for (const file of files) {
|
|
1140
|
-
const text = await
|
|
1297
|
+
const text = await readFile5(file, "utf-8");
|
|
1141
1298
|
const invalidIds = extractInvalidIds(text, [
|
|
1142
1299
|
"SPEC",
|
|
1143
1300
|
"BR",
|
|
1144
1301
|
"SC",
|
|
1145
1302
|
"UI",
|
|
1146
1303
|
"API",
|
|
1147
|
-
"
|
|
1304
|
+
"DB",
|
|
1148
1305
|
"ADR"
|
|
1149
1306
|
]);
|
|
1150
1307
|
if (invalidIds.length > 0) {
|
|
@@ -1159,6 +1316,8 @@ async function validateDataContracts(dataRoot) {
|
|
|
1159
1316
|
)
|
|
1160
1317
|
);
|
|
1161
1318
|
}
|
|
1319
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1320
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "DB"));
|
|
1162
1321
|
issues.push(...lintSql(text, file));
|
|
1163
1322
|
}
|
|
1164
1323
|
return issues;
|
|
@@ -1169,17 +1328,83 @@ function lintSql(text, file) {
|
|
|
1169
1328
|
if (pattern.test(text)) {
|
|
1170
1329
|
issues.push(
|
|
1171
1330
|
issue(
|
|
1172
|
-
"QFAI-
|
|
1331
|
+
"QFAI-DB-001",
|
|
1173
1332
|
`\u5371\u967A\u306A SQL \u64CD\u4F5C\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059: ${label}`,
|
|
1174
1333
|
"warning",
|
|
1175
1334
|
file,
|
|
1176
|
-
"contracts.
|
|
1335
|
+
"contracts.db.sql"
|
|
1177
1336
|
)
|
|
1178
1337
|
);
|
|
1179
1338
|
}
|
|
1180
1339
|
}
|
|
1181
1340
|
return issues;
|
|
1182
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
|
+
}
|
|
1183
1408
|
function hasOpenApi(doc) {
|
|
1184
1409
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
1185
1410
|
}
|
|
@@ -1208,8 +1433,8 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1208
1433
|
}
|
|
1209
1434
|
|
|
1210
1435
|
// src/core/validators/delta.ts
|
|
1211
|
-
import { readFile as
|
|
1212
|
-
import
|
|
1436
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1437
|
+
import path9 from "path";
|
|
1213
1438
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1214
1439
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1215
1440
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1223,10 +1448,10 @@ async function validateDeltas(root, config) {
|
|
|
1223
1448
|
}
|
|
1224
1449
|
const issues = [];
|
|
1225
1450
|
for (const pack of packs) {
|
|
1226
|
-
const deltaPath =
|
|
1451
|
+
const deltaPath = path9.join(pack, "delta.md");
|
|
1227
1452
|
let text;
|
|
1228
1453
|
try {
|
|
1229
|
-
text = await
|
|
1454
|
+
text = await readFile6(deltaPath, "utf-8");
|
|
1230
1455
|
} catch (error) {
|
|
1231
1456
|
if (isMissingFileError2(error)) {
|
|
1232
1457
|
issues.push(
|
|
@@ -1300,187 +1525,6 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1300
1525
|
// src/core/validators/ids.ts
|
|
1301
1526
|
import { readFile as readFile7 } from "fs/promises";
|
|
1302
1527
|
import path10 from "path";
|
|
1303
|
-
|
|
1304
|
-
// src/core/contractIndex.ts
|
|
1305
|
-
import { readFile as readFile6 } from "fs/promises";
|
|
1306
|
-
import path9 from "path";
|
|
1307
|
-
async function buildContractIndex(root, config) {
|
|
1308
|
-
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1309
|
-
const uiRoot = path9.join(contractsRoot, "ui");
|
|
1310
|
-
const apiRoot = path9.join(contractsRoot, "api");
|
|
1311
|
-
const dataRoot = path9.join(contractsRoot, "db");
|
|
1312
|
-
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
1313
|
-
collectUiContractFiles(uiRoot),
|
|
1314
|
-
collectApiContractFiles(apiRoot),
|
|
1315
|
-
collectDataContractFiles(dataRoot)
|
|
1316
|
-
]);
|
|
1317
|
-
const index = {
|
|
1318
|
-
ids: /* @__PURE__ */ new Set(),
|
|
1319
|
-
idToFiles: /* @__PURE__ */ new Map(),
|
|
1320
|
-
files: { ui: uiFiles, api: apiFiles, data: dataFiles },
|
|
1321
|
-
structuredParseFailedFiles: /* @__PURE__ */ new Set()
|
|
1322
|
-
};
|
|
1323
|
-
await indexUiContracts(uiFiles, index);
|
|
1324
|
-
await indexApiContracts(apiFiles, index);
|
|
1325
|
-
await indexDataContracts(dataFiles, index);
|
|
1326
|
-
return index;
|
|
1327
|
-
}
|
|
1328
|
-
async function indexUiContracts(files, index) {
|
|
1329
|
-
for (const file of files) {
|
|
1330
|
-
const text = await readFile6(file, "utf-8");
|
|
1331
|
-
try {
|
|
1332
|
-
const doc = parseStructuredContract(file, text);
|
|
1333
|
-
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
1334
|
-
} catch {
|
|
1335
|
-
index.structuredParseFailedFiles.add(file);
|
|
1336
|
-
extractIds(text, "UI").forEach((id) => record(index, id, file));
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
async function indexApiContracts(files, index) {
|
|
1341
|
-
for (const file of files) {
|
|
1342
|
-
const text = await readFile6(file, "utf-8");
|
|
1343
|
-
try {
|
|
1344
|
-
const doc = parseStructuredContract(file, text);
|
|
1345
|
-
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
1346
|
-
} catch {
|
|
1347
|
-
index.structuredParseFailedFiles.add(file);
|
|
1348
|
-
extractIds(text, "API").forEach((id) => record(index, id, file));
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
async function indexDataContracts(files, index) {
|
|
1353
|
-
for (const file of files) {
|
|
1354
|
-
const text = await readFile6(file, "utf-8");
|
|
1355
|
-
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
function record(index, id, file) {
|
|
1359
|
-
index.ids.add(id);
|
|
1360
|
-
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
1361
|
-
current.add(file);
|
|
1362
|
-
index.idToFiles.set(id, current);
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
// src/core/parse/markdown.ts
|
|
1366
|
-
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1367
|
-
function parseHeadings(md) {
|
|
1368
|
-
const lines = md.split(/\r?\n/);
|
|
1369
|
-
const headings = [];
|
|
1370
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1371
|
-
const line = lines[i] ?? "";
|
|
1372
|
-
const match = line.match(HEADING_RE);
|
|
1373
|
-
if (!match) continue;
|
|
1374
|
-
const levelToken = match[1];
|
|
1375
|
-
const title = match[2];
|
|
1376
|
-
if (!levelToken || !title) continue;
|
|
1377
|
-
headings.push({
|
|
1378
|
-
level: levelToken.length,
|
|
1379
|
-
title: title.trim(),
|
|
1380
|
-
line: i + 1
|
|
1381
|
-
});
|
|
1382
|
-
}
|
|
1383
|
-
return headings;
|
|
1384
|
-
}
|
|
1385
|
-
function extractH2Sections(md) {
|
|
1386
|
-
const lines = md.split(/\r?\n/);
|
|
1387
|
-
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1388
|
-
const sections = /* @__PURE__ */ new Map();
|
|
1389
|
-
for (let i = 0; i < headings.length; i++) {
|
|
1390
|
-
const current = headings[i];
|
|
1391
|
-
if (!current) continue;
|
|
1392
|
-
const next = headings[i + 1];
|
|
1393
|
-
const startLine = current.line + 1;
|
|
1394
|
-
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1395
|
-
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1396
|
-
sections.set(current.title.trim(), {
|
|
1397
|
-
title: current.title.trim(),
|
|
1398
|
-
startLine,
|
|
1399
|
-
endLine,
|
|
1400
|
-
body
|
|
1401
|
-
});
|
|
1402
|
-
}
|
|
1403
|
-
return sections;
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
// src/core/parse/spec.ts
|
|
1407
|
-
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1408
|
-
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1409
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1410
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1411
|
-
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1412
|
-
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1413
|
-
function parseSpec(md, file) {
|
|
1414
|
-
const headings = parseHeadings(md);
|
|
1415
|
-
const h1 = headings.find((heading) => heading.level === 1);
|
|
1416
|
-
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1417
|
-
const sections = extractH2Sections(md);
|
|
1418
|
-
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1419
|
-
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1420
|
-
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1421
|
-
const startLine = brSection?.startLine ?? 1;
|
|
1422
|
-
const brs = [];
|
|
1423
|
-
const brsWithoutPriority = [];
|
|
1424
|
-
const brsWithInvalidPriority = [];
|
|
1425
|
-
for (let i = 0; i < brLines.length; i++) {
|
|
1426
|
-
const lineText = brLines[i] ?? "";
|
|
1427
|
-
const lineNumber = startLine + i;
|
|
1428
|
-
const validMatch = lineText.match(BR_LINE_RE);
|
|
1429
|
-
if (validMatch) {
|
|
1430
|
-
const id = validMatch[1];
|
|
1431
|
-
const priority = validMatch[2];
|
|
1432
|
-
const text = validMatch[3];
|
|
1433
|
-
if (!id || !priority || !text) continue;
|
|
1434
|
-
brs.push({
|
|
1435
|
-
id,
|
|
1436
|
-
priority,
|
|
1437
|
-
text: text.trim(),
|
|
1438
|
-
line: lineNumber
|
|
1439
|
-
});
|
|
1440
|
-
continue;
|
|
1441
|
-
}
|
|
1442
|
-
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1443
|
-
if (anyPriorityMatch) {
|
|
1444
|
-
const id = anyPriorityMatch[1];
|
|
1445
|
-
const priority = anyPriorityMatch[2];
|
|
1446
|
-
const text = anyPriorityMatch[3];
|
|
1447
|
-
if (!id || !priority || !text) continue;
|
|
1448
|
-
if (!VALID_PRIORITIES.has(priority)) {
|
|
1449
|
-
brsWithInvalidPriority.push({
|
|
1450
|
-
id,
|
|
1451
|
-
priority,
|
|
1452
|
-
text: text.trim(),
|
|
1453
|
-
line: lineNumber
|
|
1454
|
-
});
|
|
1455
|
-
}
|
|
1456
|
-
continue;
|
|
1457
|
-
}
|
|
1458
|
-
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1459
|
-
if (noPriorityMatch) {
|
|
1460
|
-
const id = noPriorityMatch[1];
|
|
1461
|
-
const text = noPriorityMatch[2];
|
|
1462
|
-
if (!id || !text) continue;
|
|
1463
|
-
brsWithoutPriority.push({
|
|
1464
|
-
id,
|
|
1465
|
-
text: text.trim(),
|
|
1466
|
-
line: lineNumber
|
|
1467
|
-
});
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
const parsed = {
|
|
1471
|
-
file,
|
|
1472
|
-
sections: sectionNames,
|
|
1473
|
-
brs,
|
|
1474
|
-
brsWithoutPriority,
|
|
1475
|
-
brsWithInvalidPriority
|
|
1476
|
-
};
|
|
1477
|
-
if (specId) {
|
|
1478
|
-
parsed.specId = specId;
|
|
1479
|
-
}
|
|
1480
|
-
return parsed;
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
// src/core/validators/ids.ts
|
|
1484
1528
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1485
1529
|
async function validateDefinedIds(root, config) {
|
|
1486
1530
|
const issues = [];
|
|
@@ -1623,7 +1667,7 @@ function validateScenarioContent(text, file) {
|
|
|
1623
1667
|
"SC",
|
|
1624
1668
|
"UI",
|
|
1625
1669
|
"API",
|
|
1626
|
-
"
|
|
1670
|
+
"DB",
|
|
1627
1671
|
"ADR"
|
|
1628
1672
|
]);
|
|
1629
1673
|
if (invalidIds.length > 0) {
|
|
@@ -1821,7 +1865,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
1821
1865
|
"SC",
|
|
1822
1866
|
"UI",
|
|
1823
1867
|
"API",
|
|
1824
|
-
"
|
|
1868
|
+
"DB",
|
|
1825
1869
|
"ADR"
|
|
1826
1870
|
]);
|
|
1827
1871
|
if (invalidIds.length > 0) {
|
|
@@ -1950,8 +1994,7 @@ async function validateTraceability(root, config) {
|
|
|
1950
1994
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
1951
1995
|
const brIdsInScenarios = /* @__PURE__ */ new Set();
|
|
1952
1996
|
const scIdsInScenarios = /* @__PURE__ */ new Set();
|
|
1953
|
-
const
|
|
1954
|
-
const scWithContracts = /* @__PURE__ */ new Set();
|
|
1997
|
+
const specContractIds = /* @__PURE__ */ new Set();
|
|
1955
1998
|
const specToBrIds = /* @__PURE__ */ new Map();
|
|
1956
1999
|
const contractIndex = await buildContractIndex(root, config);
|
|
1957
2000
|
const contractIds = contractIndex.ids;
|
|
@@ -1969,6 +2012,64 @@ async function validateTraceability(root, config) {
|
|
|
1969
2012
|
brIds.forEach((id) => current.add(id));
|
|
1970
2013
|
specToBrIds.set(parsed.specId, current);
|
|
1971
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(
|
|
2057
|
+
(id) => !contractIds.has(id)
|
|
2058
|
+
);
|
|
2059
|
+
if (unknownContractIds.length > 0) {
|
|
2060
|
+
issues.push(
|
|
2061
|
+
issue6(
|
|
2062
|
+
"QFAI-TRACE-021",
|
|
2063
|
+
`Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
2064
|
+
", "
|
|
2065
|
+
)}`,
|
|
2066
|
+
"error",
|
|
2067
|
+
file,
|
|
2068
|
+
"traceability.specContractExists",
|
|
2069
|
+
unknownContractIds
|
|
2070
|
+
)
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
1972
2073
|
}
|
|
1973
2074
|
for (const file of scenarioFiles) {
|
|
1974
2075
|
const text = await readFile10(file, "utf-8");
|
|
@@ -2014,10 +2115,6 @@ async function validateTraceability(root, config) {
|
|
|
2014
2115
|
scIdsInScenarios.add(id);
|
|
2015
2116
|
scIdsInFile.add(id);
|
|
2016
2117
|
});
|
|
2017
|
-
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
2018
|
-
if (atom.contractIds.length > 0) {
|
|
2019
|
-
scTags.forEach((id) => scWithContracts.add(id));
|
|
2020
|
-
}
|
|
2021
2118
|
const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
|
|
2022
2119
|
if (unknownSpecIds.length > 0) {
|
|
2023
2120
|
issues.push(
|
|
@@ -2136,25 +2233,6 @@ async function validateTraceability(root, config) {
|
|
|
2136
2233
|
);
|
|
2137
2234
|
}
|
|
2138
2235
|
}
|
|
2139
|
-
if (config.validation.traceability.scMustTouchContracts && scIdsInScenarios.size > 0) {
|
|
2140
|
-
const scWithoutContracts = Array.from(scIdsInScenarios).filter(
|
|
2141
|
-
(id) => !scWithContracts.has(id)
|
|
2142
|
-
);
|
|
2143
|
-
if (scWithoutContracts.length > 0) {
|
|
2144
|
-
issues.push(
|
|
2145
|
-
issue6(
|
|
2146
|
-
"QFAI_TRACE_SC_NO_CONTRACT",
|
|
2147
|
-
`SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
|
|
2148
|
-
", "
|
|
2149
|
-
)}`,
|
|
2150
|
-
"error",
|
|
2151
|
-
specsRoot,
|
|
2152
|
-
"traceability.scMustTouchContracts",
|
|
2153
|
-
scWithoutContracts
|
|
2154
|
-
)
|
|
2155
|
-
);
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
2236
|
const scRefsResult = await collectScTestReferences(
|
|
2159
2237
|
root,
|
|
2160
2238
|
config.validation.traceability.testFileGlobs,
|
|
@@ -2218,16 +2296,16 @@ async function validateTraceability(root, config) {
|
|
|
2218
2296
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
2219
2297
|
if (contractIds.size > 0) {
|
|
2220
2298
|
const orphanContracts = Array.from(contractIds).filter(
|
|
2221
|
-
(id) => !
|
|
2299
|
+
(id) => !specContractIds.has(id)
|
|
2222
2300
|
);
|
|
2223
2301
|
if (orphanContracts.length > 0) {
|
|
2224
2302
|
issues.push(
|
|
2225
2303
|
issue6(
|
|
2226
|
-
"
|
|
2227
|
-
`\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(", ")}`,
|
|
2228
2306
|
"error",
|
|
2229
2307
|
specsRoot,
|
|
2230
|
-
"traceability.
|
|
2308
|
+
"traceability.contractCoverage",
|
|
2231
2309
|
orphanContracts
|
|
2232
2310
|
)
|
|
2233
2311
|
);
|
|
@@ -2348,7 +2426,7 @@ function countIssues(issues) {
|
|
|
2348
2426
|
}
|
|
2349
2427
|
|
|
2350
2428
|
// src/core/report.ts
|
|
2351
|
-
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "
|
|
2429
|
+
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2352
2430
|
async function createReportData(root, validation, configResult) {
|
|
2353
2431
|
const resolved = configResult ?? await loadConfig(root);
|
|
2354
2432
|
const config = resolved.config;
|
|
@@ -2367,6 +2445,23 @@ async function createReportData(root, validation, configResult) {
|
|
|
2367
2445
|
ui: uiFiles,
|
|
2368
2446
|
db: dbFiles
|
|
2369
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
|
+
);
|
|
2370
2465
|
const idsByPrefix = await collectIds([
|
|
2371
2466
|
...specFiles,
|
|
2372
2467
|
...scenarioFiles,
|
|
@@ -2417,14 +2512,24 @@ async function createReportData(root, validation, configResult) {
|
|
|
2417
2512
|
sc: idsByPrefix.SC,
|
|
2418
2513
|
ui: idsByPrefix.UI,
|
|
2419
2514
|
api: idsByPrefix.API,
|
|
2420
|
-
|
|
2515
|
+
db: idsByPrefix.DB
|
|
2421
2516
|
},
|
|
2422
2517
|
traceability: {
|
|
2423
2518
|
upstreamIdsFound: upstreamIds.size,
|
|
2424
2519
|
referencedInCodeOrTests: traceability,
|
|
2425
2520
|
sc: scCoverage,
|
|
2426
2521
|
scSources: scSourceRecord,
|
|
2427
|
-
testFiles
|
|
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
|
+
}
|
|
2428
2533
|
},
|
|
2429
2534
|
issues: resolvedValidation.issues
|
|
2430
2535
|
};
|
|
@@ -2453,7 +2558,7 @@ function formatReportMarkdown(data) {
|
|
|
2453
2558
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
2454
2559
|
lines.push(formatIdLine("UI", data.ids.ui));
|
|
2455
2560
|
lines.push(formatIdLine("API", data.ids.api));
|
|
2456
|
-
lines.push(formatIdLine("
|
|
2561
|
+
lines.push(formatIdLine("DB", data.ids.db));
|
|
2457
2562
|
lines.push("");
|
|
2458
2563
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
2459
2564
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
@@ -2461,6 +2566,50 @@ function formatReportMarkdown(data) {
|
|
|
2461
2566
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2462
2567
|
);
|
|
2463
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("");
|
|
2464
2613
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2465
2614
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2466
2615
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
@@ -2534,7 +2683,7 @@ function formatReportMarkdown(data) {
|
|
|
2534
2683
|
lines.push("");
|
|
2535
2684
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
2536
2685
|
const traceIssues = data.issues.filter(
|
|
2537
|
-
(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-")
|
|
2538
2687
|
);
|
|
2539
2688
|
if (traceIssues.length === 0) {
|
|
2540
2689
|
lines.push("- (none)");
|
|
@@ -2564,6 +2713,33 @@ function formatReportMarkdown(data) {
|
|
|
2564
2713
|
function formatReportJson(data) {
|
|
2565
2714
|
return JSON.stringify(data, null, 2);
|
|
2566
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
|
+
}
|
|
2567
2743
|
async function collectIds(files) {
|
|
2568
2744
|
const result = {
|
|
2569
2745
|
SPEC: /* @__PURE__ */ new Set(),
|
|
@@ -2571,7 +2747,7 @@ async function collectIds(files) {
|
|
|
2571
2747
|
SC: /* @__PURE__ */ new Set(),
|
|
2572
2748
|
UI: /* @__PURE__ */ new Set(),
|
|
2573
2749
|
API: /* @__PURE__ */ new Set(),
|
|
2574
|
-
|
|
2750
|
+
DB: /* @__PURE__ */ new Set()
|
|
2575
2751
|
};
|
|
2576
2752
|
for (const file of files) {
|
|
2577
2753
|
const text = await readFile11(file, "utf-8");
|
|
@@ -2586,7 +2762,7 @@ async function collectIds(files) {
|
|
|
2586
2762
|
SC: toSortedArray2(result.SC),
|
|
2587
2763
|
UI: toSortedArray2(result.UI),
|
|
2588
2764
|
API: toSortedArray2(result.API),
|
|
2589
|
-
|
|
2765
|
+
DB: toSortedArray2(result.DB)
|
|
2590
2766
|
};
|
|
2591
2767
|
}
|
|
2592
2768
|
async function collectUpstreamIds(files) {
|