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.
Files changed (174) hide show
  1. package/README.md +14 -0
  2. package/assets/init/.qfai/README.md +1 -0
  3. package/assets/init/.qfai/contracts/README.md +21 -11
  4. package/assets/init/.qfai/contracts/api/api-0001-sample.yaml +3 -2
  5. package/assets/init/.qfai/contracts/db/db-0001-sample.sql +2 -1
  6. package/assets/init/.qfai/contracts/ui/ui-0001-sample.yaml +3 -1
  7. package/assets/init/.qfai/promptpack/modes/change.md +3 -2
  8. package/assets/init/.qfai/promptpack/modes/compatibility.md +2 -0
  9. package/assets/init/.qfai/promptpack/steering/traceability.md +5 -4
  10. package/assets/init/.qfai/prompts/makeOverview.md +1 -1
  11. package/assets/init/.qfai/prompts/require-to-spec.md +4 -2
  12. package/assets/init/.qfai/specs/README.md +9 -2
  13. package/assets/init/.qfai/specs/spec-0001/scenario.md +1 -1
  14. package/assets/init/.qfai/specs/spec-0001/spec.md +2 -0
  15. package/assets/init/root/qfai.config.yaml +0 -1
  16. package/dist/cli/commands/init.d.ts +8 -0
  17. package/dist/cli/commands/init.d.ts.map +1 -0
  18. package/dist/cli/commands/init.js +30 -0
  19. package/dist/cli/commands/init.js.map +1 -0
  20. package/dist/cli/commands/report.d.ts +7 -0
  21. package/dist/cli/commands/report.d.ts.map +1 -0
  22. package/dist/cli/commands/report.js +108 -0
  23. package/dist/cli/commands/report.js.map +1 -0
  24. package/dist/cli/commands/validate.d.ts +9 -0
  25. package/dist/cli/commands/validate.d.ts.map +1 -0
  26. package/dist/cli/commands/validate.js +57 -0
  27. package/dist/cli/commands/validate.js.map +1 -0
  28. package/dist/cli/index.cjs +536 -336
  29. package/dist/cli/index.cjs.map +1 -1
  30. package/dist/cli/index.d.ts +2 -0
  31. package/dist/cli/index.d.ts.map +1 -0
  32. package/dist/cli/index.js +7 -0
  33. package/dist/cli/index.js.map +1 -0
  34. package/dist/cli/index.mjs +536 -336
  35. package/dist/cli/index.mjs.map +1 -1
  36. package/dist/cli/lib/args.d.ts +18 -0
  37. package/dist/cli/lib/args.d.ts.map +1 -0
  38. package/dist/cli/lib/args.js +98 -0
  39. package/dist/cli/lib/args.js.map +1 -0
  40. package/dist/cli/lib/assets.d.ts +2 -0
  41. package/dist/cli/lib/assets.d.ts.map +1 -0
  42. package/dist/cli/lib/assets.js +24 -0
  43. package/dist/cli/lib/assets.js.map +1 -0
  44. package/dist/cli/lib/failOn.d.ts +5 -0
  45. package/dist/cli/lib/failOn.d.ts.map +1 -0
  46. package/dist/cli/lib/failOn.js +10 -0
  47. package/dist/cli/lib/failOn.js.map +1 -0
  48. package/dist/cli/lib/fs.d.ts +11 -0
  49. package/dist/cli/lib/fs.d.ts.map +1 -0
  50. package/dist/cli/lib/fs.js +91 -0
  51. package/dist/cli/lib/fs.js.map +1 -0
  52. package/dist/cli/lib/logger.d.ts +4 -0
  53. package/dist/cli/lib/logger.d.ts.map +1 -0
  54. package/dist/cli/lib/logger.js +10 -0
  55. package/dist/cli/lib/logger.js.map +1 -0
  56. package/dist/cli/main.d.ts +2 -0
  57. package/dist/cli/main.d.ts.map +1 -0
  58. package/dist/cli/main.js +66 -0
  59. package/dist/cli/main.js.map +1 -0
  60. package/dist/core/config.d.ts +47 -0
  61. package/dist/core/config.d.ts.map +1 -0
  62. package/dist/core/config.js +224 -0
  63. package/dist/core/config.js.map +1 -0
  64. package/dist/core/contractIndex.d.ts +12 -0
  65. package/dist/core/contractIndex.d.ts.map +1 -0
  66. package/dist/core/contractIndex.js +38 -0
  67. package/dist/core/contractIndex.js.map +1 -0
  68. package/dist/core/contracts.d.ts +5 -0
  69. package/dist/core/contracts.d.ts.map +1 -0
  70. package/dist/core/contracts.js +42 -0
  71. package/dist/core/contracts.js.map +1 -0
  72. package/dist/core/contractsDecl.d.ts +3 -0
  73. package/dist/core/contractsDecl.d.ts.map +1 -0
  74. package/dist/core/contractsDecl.js +19 -0
  75. package/dist/core/contractsDecl.js.map +1 -0
  76. package/dist/core/discovery.d.ts +14 -0
  77. package/dist/core/discovery.d.ts.map +1 -0
  78. package/dist/core/discovery.js +55 -0
  79. package/dist/core/discovery.js.map +1 -0
  80. package/dist/core/fs.d.ts +11 -0
  81. package/dist/core/fs.d.ts.map +1 -0
  82. package/dist/core/fs.js +68 -0
  83. package/dist/core/fs.js.map +1 -0
  84. package/dist/core/gherkin/parse.d.ts +7 -0
  85. package/dist/core/gherkin/parse.d.ts.map +1 -0
  86. package/dist/core/gherkin/parse.js +25 -0
  87. package/dist/core/gherkin/parse.js.map +1 -0
  88. package/dist/core/ids.d.ts +6 -0
  89. package/dist/core/ids.d.ts.map +1 -0
  90. package/dist/core/ids.js +52 -0
  91. package/dist/core/ids.js.map +1 -0
  92. package/dist/core/index.d.ts +13 -0
  93. package/dist/core/index.d.ts.map +1 -0
  94. package/dist/core/index.js +13 -0
  95. package/dist/core/index.js.map +1 -0
  96. package/dist/core/parse/adr.d.ts +13 -0
  97. package/dist/core/parse/adr.d.ts.map +1 -0
  98. package/dist/core/parse/adr.js +33 -0
  99. package/dist/core/parse/adr.js.map +1 -0
  100. package/dist/core/parse/gherkin.d.ts +12 -0
  101. package/dist/core/parse/gherkin.d.ts.map +1 -0
  102. package/dist/core/parse/gherkin.js +22 -0
  103. package/dist/core/parse/gherkin.js.map +1 -0
  104. package/dist/core/parse/markdown.d.ts +14 -0
  105. package/dist/core/parse/markdown.d.ts.map +1 -0
  106. package/dist/core/parse/markdown.js +45 -0
  107. package/dist/core/parse/markdown.js.map +1 -0
  108. package/dist/core/parse/spec.d.ts +36 -0
  109. package/dist/core/parse/spec.d.ts.map +1 -0
  110. package/dist/core/parse/spec.js +123 -0
  111. package/dist/core/parse/spec.js.map +1 -0
  112. package/dist/core/report.d.ts +55 -0
  113. package/dist/core/report.d.ts.map +1 -0
  114. package/dist/core/report.js +393 -0
  115. package/dist/core/report.js.map +1 -0
  116. package/dist/core/scenarioModel.d.ts +33 -0
  117. package/dist/core/scenarioModel.d.ts.map +1 -0
  118. package/dist/core/scenarioModel.js +128 -0
  119. package/dist/core/scenarioModel.js.map +1 -0
  120. package/dist/core/specLayout.d.ts +8 -0
  121. package/dist/core/specLayout.d.ts.map +1 -0
  122. package/dist/core/specLayout.js +36 -0
  123. package/dist/core/specLayout.js.map +1 -0
  124. package/dist/core/traceability.d.ts +26 -0
  125. package/dist/core/traceability.d.ts.map +1 -0
  126. package/dist/core/traceability.js +157 -0
  127. package/dist/core/traceability.js.map +1 -0
  128. package/dist/core/types.d.ts +31 -0
  129. package/dist/core/types.d.ts.map +1 -0
  130. package/dist/core/types.js +2 -0
  131. package/dist/core/types.js.map +1 -0
  132. package/dist/core/validate.d.ts +4 -0
  133. package/dist/core/validate.d.ts.map +1 -0
  134. package/dist/core/validate.js +45 -0
  135. package/dist/core/validate.js.map +1 -0
  136. package/dist/core/validators/contracts.d.ts +5 -0
  137. package/dist/core/validators/contracts.d.ts.map +1 -0
  138. package/dist/core/validators/contracts.js +189 -0
  139. package/dist/core/validators/contracts.js.map +1 -0
  140. package/dist/core/validators/delta.d.ts +4 -0
  141. package/dist/core/validators/delta.d.ts.map +1 -0
  142. package/dist/core/validators/delta.js +68 -0
  143. package/dist/core/validators/delta.js.map +1 -0
  144. package/dist/core/validators/ids.d.ts +4 -0
  145. package/dist/core/validators/ids.d.ts.map +1 -0
  146. package/dist/core/validators/ids.js +88 -0
  147. package/dist/core/validators/ids.js.map +1 -0
  148. package/dist/core/validators/scenario.d.ts +5 -0
  149. package/dist/core/validators/scenario.d.ts.map +1 -0
  150. package/dist/core/validators/scenario.js +127 -0
  151. package/dist/core/validators/scenario.js.map +1 -0
  152. package/dist/core/validators/spec.d.ts +5 -0
  153. package/dist/core/validators/spec.d.ts.map +1 -0
  154. package/dist/core/validators/spec.js +94 -0
  155. package/dist/core/validators/spec.js.map +1 -0
  156. package/dist/core/validators/traceability.d.ts +4 -0
  157. package/dist/core/validators/traceability.d.ts.map +1 -0
  158. package/dist/core/validators/traceability.js +222 -0
  159. package/dist/core/validators/traceability.js.map +1 -0
  160. package/dist/core/version.d.ts +2 -0
  161. package/dist/core/version.d.ts.map +1 -0
  162. package/dist/core/version.js +25 -0
  163. package/dist/core/version.js.map +1 -0
  164. package/dist/index.cjs +511 -335
  165. package/dist/index.cjs.map +1 -1
  166. package/dist/index.d.cts +16 -5
  167. package/dist/index.d.ts +2 -156
  168. package/dist/index.d.ts.map +1 -0
  169. package/dist/index.js +2 -0
  170. package/dist/index.js.map +1 -0
  171. package/dist/index.mjs +511 -335
  172. package/dist/index.mjs.map +1 -1
  173. package/dist/tsconfig.tsbuildinfo +1 -0
  174. 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", "DATA"];
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
- DATA: /\bDATA-\d{4}\b/g,
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
- DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi,
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 collectDataContractFiles(dataRoot) {
546
- return collectFiles(dataRoot, { extensions: [".sql"] });
541
+ async function collectDbContractFiles(dbRoot) {
542
+ return collectFiles(dbRoot, { extensions: [".sql"] });
547
543
  }
548
- async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
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
- collectDataContractFiles(dataRoot)
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 readFile2 } from "fs/promises";
576
- import path4 from "path";
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 DATA_TAG_RE = /^DATA-\d{4}$/;
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 = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
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) || DATA_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, "DATA").forEach((id) => contractIds.add(id));
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 unique2(values) {
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 readFile2(file, "utf-8");
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 readFile2(file, "utf-8");
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) => path4.normalize(file)))
1041
+ new Set(files.map((file) => path5.normalize(file)))
834
1042
  );
835
1043
  for (const file of normalizedFiles) {
836
- const text = await readFile2(file, "utf-8");
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 readFile3 } from "fs/promises";
894
- import path5 from "path";
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.2".length > 0) {
898
- return "0.4.2";
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 readFile3(packagePath, "utf-8");
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 path5.resolve(path5.dirname(basePath), "../../package.json");
1121
+ return path6.resolve(path6.dirname(basePath), "../../package.json");
914
1122
  }
915
1123
 
916
1124
  // src/core/validators/contracts.ts
917
- import { readFile as readFile4 } from "fs/promises";
918
- import path7 from "path";
1125
+ import { readFile as readFile5 } from "fs/promises";
1126
+ import path8 from "path";
919
1127
 
920
1128
  // src/core/contracts.ts
921
- import path6 from "path";
1129
+ import path7 from "path";
922
1130
  import { parse as parseYaml2 } from "yaml";
923
1131
  function parseStructuredContract(file, text) {
924
- const ext = path6.extname(file).toLowerCase();
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(path7.join(contractsRoot, "ui")));
976
- issues.push(...await validateApiContracts(path7.join(contractsRoot, "api")));
977
- issues.push(...await validateDataContracts(path7.join(contractsRoot, "db")));
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 readFile4(file, "utf-8");
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
- "DATA",
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
- let doc;
1196
+ const declaredIds = extractDeclaredContractIds(text);
1197
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
1018
1198
  try {
1019
- doc = parseStructuredContract(file, text);
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 readFile4(file, "utf-8");
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
- "DATA",
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 validateDataContracts(dataRoot) {
1126
- const files = await collectDataContractFiles(dataRoot);
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-DATA-000",
1131
- "DATA \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
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
- dataRoot,
1134
- "contracts.data.files"
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 readFile4(file, "utf-8");
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
- "DATA",
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-DATA-001",
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.data.sql"
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 readFile5 } from "fs/promises";
1212
- import path8 from "path";
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 = path8.join(pack, "delta.md");
1451
+ const deltaPath = path9.join(pack, "delta.md");
1227
1452
  let text;
1228
1453
  try {
1229
- text = await readFile5(deltaPath, "utf-8");
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
- "DATA",
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
- "DATA",
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 scenarioContractIds = /* @__PURE__ */ new Set();
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) => !scenarioContractIds.has(id)
2299
+ (id) => !specContractIds.has(id)
2222
2300
  );
2223
2301
  if (orphanContracts.length > 0) {
2224
2302
  issues.push(
2225
2303
  issue6(
2226
- "QFAI_CONTRACT_ORPHAN",
2227
- `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
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.allowOrphanContracts",
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", "DATA"];
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
- data: idsByPrefix.DATA
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("DATA", data.ids.data));
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-") || item.code === "QFAI_CONTRACT_ORPHAN"
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
- DATA: /* @__PURE__ */ new Set()
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
- DATA: toSortedArray2(result.DATA)
2765
+ DB: toSortedArray2(result.DB)
2590
2766
  };
2591
2767
  }
2592
2768
  async function collectUpstreamIds(files) {