qfai 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) 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 +17 -8
  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/prompts/require-to-spec.md +4 -2
  10. package/assets/init/.qfai/specs/README.md +9 -2
  11. package/assets/init/.qfai/specs/spec-0001/spec.md +2 -0
  12. package/assets/init/root/qfai.config.yaml +0 -1
  13. package/dist/cli/commands/init.d.ts +8 -0
  14. package/dist/cli/commands/init.d.ts.map +1 -0
  15. package/dist/cli/commands/init.js +30 -0
  16. package/dist/cli/commands/init.js.map +1 -0
  17. package/dist/cli/commands/report.d.ts +7 -0
  18. package/dist/cli/commands/report.d.ts.map +1 -0
  19. package/dist/cli/commands/report.js +80 -0
  20. package/dist/cli/commands/report.js.map +1 -0
  21. package/dist/cli/commands/validate.d.ts +9 -0
  22. package/dist/cli/commands/validate.d.ts.map +1 -0
  23. package/dist/cli/commands/validate.js +57 -0
  24. package/dist/cli/commands/validate.js.map +1 -0
  25. package/dist/cli/index.cjs +504 -328
  26. package/dist/cli/index.cjs.map +1 -1
  27. package/dist/cli/index.d.ts +2 -0
  28. package/dist/cli/index.d.ts.map +1 -0
  29. package/dist/cli/index.js +7 -0
  30. package/dist/cli/index.js.map +1 -0
  31. package/dist/cli/index.mjs +504 -328
  32. package/dist/cli/index.mjs.map +1 -1
  33. package/dist/cli/lib/args.d.ts +18 -0
  34. package/dist/cli/lib/args.d.ts.map +1 -0
  35. package/dist/cli/lib/args.js +98 -0
  36. package/dist/cli/lib/args.js.map +1 -0
  37. package/dist/cli/lib/assets.d.ts +2 -0
  38. package/dist/cli/lib/assets.d.ts.map +1 -0
  39. package/dist/cli/lib/assets.js +24 -0
  40. package/dist/cli/lib/assets.js.map +1 -0
  41. package/dist/cli/lib/failOn.d.ts +5 -0
  42. package/dist/cli/lib/failOn.d.ts.map +1 -0
  43. package/dist/cli/lib/failOn.js +10 -0
  44. package/dist/cli/lib/failOn.js.map +1 -0
  45. package/dist/cli/lib/fs.d.ts +11 -0
  46. package/dist/cli/lib/fs.d.ts.map +1 -0
  47. package/dist/cli/lib/fs.js +91 -0
  48. package/dist/cli/lib/fs.js.map +1 -0
  49. package/dist/cli/lib/logger.d.ts +4 -0
  50. package/dist/cli/lib/logger.d.ts.map +1 -0
  51. package/dist/cli/lib/logger.js +10 -0
  52. package/dist/cli/lib/logger.js.map +1 -0
  53. package/dist/cli/main.d.ts +2 -0
  54. package/dist/cli/main.d.ts.map +1 -0
  55. package/dist/cli/main.js +66 -0
  56. package/dist/cli/main.js.map +1 -0
  57. package/dist/core/config.d.ts +47 -0
  58. package/dist/core/config.d.ts.map +1 -0
  59. package/dist/core/config.js +224 -0
  60. package/dist/core/config.js.map +1 -0
  61. package/dist/core/contractIndex.d.ts +12 -0
  62. package/dist/core/contractIndex.d.ts.map +1 -0
  63. package/dist/core/contractIndex.js +38 -0
  64. package/dist/core/contractIndex.js.map +1 -0
  65. package/dist/core/contracts.d.ts +5 -0
  66. package/dist/core/contracts.d.ts.map +1 -0
  67. package/dist/core/contracts.js +42 -0
  68. package/dist/core/contracts.js.map +1 -0
  69. package/dist/core/contractsDecl.d.ts +3 -0
  70. package/dist/core/contractsDecl.d.ts.map +1 -0
  71. package/dist/core/contractsDecl.js +19 -0
  72. package/dist/core/contractsDecl.js.map +1 -0
  73. package/dist/core/discovery.d.ts +14 -0
  74. package/dist/core/discovery.d.ts.map +1 -0
  75. package/dist/core/discovery.js +55 -0
  76. package/dist/core/discovery.js.map +1 -0
  77. package/dist/core/fs.d.ts +11 -0
  78. package/dist/core/fs.d.ts.map +1 -0
  79. package/dist/core/fs.js +68 -0
  80. package/dist/core/fs.js.map +1 -0
  81. package/dist/core/gherkin/parse.d.ts +7 -0
  82. package/dist/core/gherkin/parse.d.ts.map +1 -0
  83. package/dist/core/gherkin/parse.js +25 -0
  84. package/dist/core/gherkin/parse.js.map +1 -0
  85. package/dist/core/ids.d.ts +6 -0
  86. package/dist/core/ids.d.ts.map +1 -0
  87. package/dist/core/ids.js +52 -0
  88. package/dist/core/ids.js.map +1 -0
  89. package/dist/core/index.d.ts +13 -0
  90. package/dist/core/index.d.ts.map +1 -0
  91. package/dist/core/index.js +13 -0
  92. package/dist/core/index.js.map +1 -0
  93. package/dist/core/parse/adr.d.ts +13 -0
  94. package/dist/core/parse/adr.d.ts.map +1 -0
  95. package/dist/core/parse/adr.js +33 -0
  96. package/dist/core/parse/adr.js.map +1 -0
  97. package/dist/core/parse/gherkin.d.ts +12 -0
  98. package/dist/core/parse/gherkin.d.ts.map +1 -0
  99. package/dist/core/parse/gherkin.js +22 -0
  100. package/dist/core/parse/gherkin.js.map +1 -0
  101. package/dist/core/parse/markdown.d.ts +14 -0
  102. package/dist/core/parse/markdown.d.ts.map +1 -0
  103. package/dist/core/parse/markdown.js +45 -0
  104. package/dist/core/parse/markdown.js.map +1 -0
  105. package/dist/core/parse/spec.d.ts +36 -0
  106. package/dist/core/parse/spec.d.ts.map +1 -0
  107. package/dist/core/parse/spec.js +123 -0
  108. package/dist/core/parse/spec.js.map +1 -0
  109. package/dist/core/report.d.ts +55 -0
  110. package/dist/core/report.d.ts.map +1 -0
  111. package/dist/core/report.js +393 -0
  112. package/dist/core/report.js.map +1 -0
  113. package/dist/core/scenarioModel.d.ts +33 -0
  114. package/dist/core/scenarioModel.d.ts.map +1 -0
  115. package/dist/core/scenarioModel.js +128 -0
  116. package/dist/core/scenarioModel.js.map +1 -0
  117. package/dist/core/specLayout.d.ts +8 -0
  118. package/dist/core/specLayout.d.ts.map +1 -0
  119. package/dist/core/specLayout.js +36 -0
  120. package/dist/core/specLayout.js.map +1 -0
  121. package/dist/core/traceability.d.ts +26 -0
  122. package/dist/core/traceability.d.ts.map +1 -0
  123. package/dist/core/traceability.js +157 -0
  124. package/dist/core/traceability.js.map +1 -0
  125. package/dist/core/types.d.ts +31 -0
  126. package/dist/core/types.d.ts.map +1 -0
  127. package/dist/core/types.js +2 -0
  128. package/dist/core/types.js.map +1 -0
  129. package/dist/core/validate.d.ts +4 -0
  130. package/dist/core/validate.d.ts.map +1 -0
  131. package/dist/core/validate.js +45 -0
  132. package/dist/core/validate.js.map +1 -0
  133. package/dist/core/validators/contracts.d.ts +5 -0
  134. package/dist/core/validators/contracts.d.ts.map +1 -0
  135. package/dist/core/validators/contracts.js +189 -0
  136. package/dist/core/validators/contracts.js.map +1 -0
  137. package/dist/core/validators/delta.d.ts +4 -0
  138. package/dist/core/validators/delta.d.ts.map +1 -0
  139. package/dist/core/validators/delta.js +68 -0
  140. package/dist/core/validators/delta.js.map +1 -0
  141. package/dist/core/validators/ids.d.ts +4 -0
  142. package/dist/core/validators/ids.d.ts.map +1 -0
  143. package/dist/core/validators/ids.js +88 -0
  144. package/dist/core/validators/ids.js.map +1 -0
  145. package/dist/core/validators/scenario.d.ts +5 -0
  146. package/dist/core/validators/scenario.d.ts.map +1 -0
  147. package/dist/core/validators/scenario.js +127 -0
  148. package/dist/core/validators/scenario.js.map +1 -0
  149. package/dist/core/validators/spec.d.ts +5 -0
  150. package/dist/core/validators/spec.d.ts.map +1 -0
  151. package/dist/core/validators/spec.js +94 -0
  152. package/dist/core/validators/spec.js.map +1 -0
  153. package/dist/core/validators/traceability.d.ts +4 -0
  154. package/dist/core/validators/traceability.d.ts.map +1 -0
  155. package/dist/core/validators/traceability.js +222 -0
  156. package/dist/core/validators/traceability.js.map +1 -0
  157. package/dist/core/version.d.ts +2 -0
  158. package/dist/core/version.d.ts.map +1 -0
  159. package/dist/core/version.js +25 -0
  160. package/dist/core/version.js.map +1 -0
  161. package/dist/index.cjs +504 -328
  162. package/dist/index.cjs.map +1 -1
  163. package/dist/index.d.cts +15 -4
  164. package/dist/index.d.ts +2 -156
  165. package/dist/index.d.ts.map +1 -0
  166. package/dist/index.js +2 -0
  167. package/dist/index.js.map +1 -0
  168. package/dist/index.mjs +504 -328
  169. package/dist/index.mjs.map +1 -1
  170. package/dist/tsconfig.tsbuildinfo +1 -0
  171. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -83,7 +83,6 @@ var defaultConfig = {
83
83
  },
84
84
  traceability: {
85
85
  brMustHaveSc: true,
86
- scMustTouchContracts: true,
87
86
  scMustHaveTest: true,
88
87
  testFileGlobs: [],
89
88
  testFileExcludeGlobs: [],
@@ -260,13 +259,6 @@ function normalizeValidation(raw, configPath, issues) {
260
259
  configPath,
261
260
  issues
262
261
  ),
263
- scMustTouchContracts: readBoolean(
264
- traceabilityRaw?.scMustTouchContracts,
265
- base.traceability.scMustTouchContracts,
266
- "validation.traceability.scMustTouchContracts",
267
- configPath,
268
- issues
269
- ),
270
262
  scMustHaveTest: readBoolean(
271
263
  traceabilityRaw?.scMustHaveTest,
272
264
  base.traceability.scMustHaveTest,
@@ -420,14 +412,14 @@ function isRecord(value) {
420
412
  }
421
413
 
422
414
  // src/core/ids.ts
423
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
415
+ var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
424
416
  var STRICT_ID_PATTERNS = {
425
417
  SPEC: /\bSPEC-\d{4}\b/g,
426
418
  BR: /\bBR-\d{4}\b/g,
427
419
  SC: /\bSC-\d{4}\b/g,
428
420
  UI: /\bUI-\d{4}\b/g,
429
421
  API: /\bAPI-\d{4}\b/g,
430
- DATA: /\bDATA-\d{4}\b/g,
422
+ DB: /\bDB-\d{4}\b/g,
431
423
  ADR: /\bADR-\d{4}\b/g
432
424
  };
433
425
  var LOOSE_ID_PATTERNS = {
@@ -436,7 +428,7 @@ var LOOSE_ID_PATTERNS = {
436
428
  SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
437
429
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
438
430
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
439
- DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi,
431
+ DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
440
432
  ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
441
433
  };
442
434
  function extractIds(text, prefix) {
@@ -476,6 +468,10 @@ function isValidId(value, prefix) {
476
468
  var import_promises14 = require("fs/promises");
477
469
  var import_node_path11 = __toESM(require("path"), 1);
478
470
 
471
+ // src/core/contractIndex.ts
472
+ var import_promises5 = require("fs/promises");
473
+ var import_node_path4 = __toESM(require("path"), 1);
474
+
479
475
  // src/core/discovery.ts
480
476
  var import_promises4 = require("fs/promises");
481
477
 
@@ -627,9 +623,221 @@ async function exists2(target) {
627
623
  }
628
624
  }
629
625
 
626
+ // src/core/contractsDecl.ts
627
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
628
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
629
+ function extractDeclaredContractIds(text) {
630
+ const ids = [];
631
+ for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
632
+ const id = match[1];
633
+ if (id) {
634
+ ids.push(id);
635
+ }
636
+ }
637
+ return ids;
638
+ }
639
+ function stripContractDeclarationLines(text) {
640
+ return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
641
+ }
642
+
643
+ // src/core/contractIndex.ts
644
+ async function buildContractIndex(root, config) {
645
+ const contractsRoot = resolvePath(root, config, "contractsDir");
646
+ const uiRoot = import_node_path4.default.join(contractsRoot, "ui");
647
+ const apiRoot = import_node_path4.default.join(contractsRoot, "api");
648
+ const dbRoot = import_node_path4.default.join(contractsRoot, "db");
649
+ const [uiFiles, apiFiles, dbFiles] = await Promise.all([
650
+ collectUiContractFiles(uiRoot),
651
+ collectApiContractFiles(apiRoot),
652
+ collectDataContractFiles(dbRoot)
653
+ ]);
654
+ const index = {
655
+ ids: /* @__PURE__ */ new Set(),
656
+ idToFiles: /* @__PURE__ */ new Map(),
657
+ files: { ui: uiFiles, api: apiFiles, db: dbFiles }
658
+ };
659
+ await indexContractFiles(uiFiles, index);
660
+ await indexContractFiles(apiFiles, index);
661
+ await indexContractFiles(dbFiles, index);
662
+ return index;
663
+ }
664
+ async function indexContractFiles(files, index) {
665
+ for (const file of files) {
666
+ const text = await (0, import_promises5.readFile)(file, "utf-8");
667
+ extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
668
+ }
669
+ }
670
+ function record(index, id, file) {
671
+ index.ids.add(id);
672
+ const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
673
+ current.add(file);
674
+ index.idToFiles.set(id, current);
675
+ }
676
+
677
+ // src/core/parse/markdown.ts
678
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
679
+ function parseHeadings(md) {
680
+ const lines = md.split(/\r?\n/);
681
+ const headings = [];
682
+ for (let i = 0; i < lines.length; i++) {
683
+ const line = lines[i] ?? "";
684
+ const match = line.match(HEADING_RE);
685
+ if (!match) continue;
686
+ const levelToken = match[1];
687
+ const title = match[2];
688
+ if (!levelToken || !title) continue;
689
+ headings.push({
690
+ level: levelToken.length,
691
+ title: title.trim(),
692
+ line: i + 1
693
+ });
694
+ }
695
+ return headings;
696
+ }
697
+ function extractH2Sections(md) {
698
+ const lines = md.split(/\r?\n/);
699
+ const headings = parseHeadings(md).filter((heading) => heading.level === 2);
700
+ const sections = /* @__PURE__ */ new Map();
701
+ for (let i = 0; i < headings.length; i++) {
702
+ const current = headings[i];
703
+ if (!current) continue;
704
+ const next = headings[i + 1];
705
+ const startLine = current.line + 1;
706
+ const endLine = (next?.line ?? lines.length + 1) - 1;
707
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
708
+ sections.set(current.title.trim(), {
709
+ title: current.title.trim(),
710
+ startLine,
711
+ endLine,
712
+ body
713
+ });
714
+ }
715
+ return sections;
716
+ }
717
+
718
+ // src/core/parse/spec.ts
719
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
720
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
721
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
722
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
723
+ var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
724
+ var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
725
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
726
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
727
+ function parseSpec(md, file) {
728
+ const headings = parseHeadings(md);
729
+ const h1 = headings.find((heading) => heading.level === 1);
730
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
731
+ const sections = extractH2Sections(md);
732
+ const sectionNames = new Set(Array.from(sections.keys()));
733
+ const brSection = sections.get(BR_SECTION_TITLE);
734
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
735
+ const startLine = brSection?.startLine ?? 1;
736
+ const brs = [];
737
+ const brsWithoutPriority = [];
738
+ const brsWithInvalidPriority = [];
739
+ for (let i = 0; i < brLines.length; i++) {
740
+ const lineText = brLines[i] ?? "";
741
+ const lineNumber = startLine + i;
742
+ const validMatch = lineText.match(BR_LINE_RE);
743
+ if (validMatch) {
744
+ const id = validMatch[1];
745
+ const priority = validMatch[2];
746
+ const text = validMatch[3];
747
+ if (!id || !priority || !text) continue;
748
+ brs.push({
749
+ id,
750
+ priority,
751
+ text: text.trim(),
752
+ line: lineNumber
753
+ });
754
+ continue;
755
+ }
756
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
757
+ if (anyPriorityMatch) {
758
+ const id = anyPriorityMatch[1];
759
+ const priority = anyPriorityMatch[2];
760
+ const text = anyPriorityMatch[3];
761
+ if (!id || !priority || !text) continue;
762
+ if (!VALID_PRIORITIES.has(priority)) {
763
+ brsWithInvalidPriority.push({
764
+ id,
765
+ priority,
766
+ text: text.trim(),
767
+ line: lineNumber
768
+ });
769
+ }
770
+ continue;
771
+ }
772
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
773
+ if (noPriorityMatch) {
774
+ const id = noPriorityMatch[1];
775
+ const text = noPriorityMatch[2];
776
+ if (!id || !text) continue;
777
+ brsWithoutPriority.push({
778
+ id,
779
+ text: text.trim(),
780
+ line: lineNumber
781
+ });
782
+ }
783
+ }
784
+ const parsed = {
785
+ file,
786
+ sections: sectionNames,
787
+ brs,
788
+ brsWithoutPriority,
789
+ brsWithInvalidPriority,
790
+ contractRefs: parseContractRefs(md)
791
+ };
792
+ if (specId) {
793
+ parsed.specId = specId;
794
+ }
795
+ return parsed;
796
+ }
797
+ function parseContractRefs(md) {
798
+ const lines = [];
799
+ for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
800
+ lines.push((match[1] ?? "").trim());
801
+ }
802
+ const ids = [];
803
+ const invalidTokens = [];
804
+ let hasNone = false;
805
+ for (const line of lines) {
806
+ if (line.length === 0) {
807
+ invalidTokens.push("(empty)");
808
+ continue;
809
+ }
810
+ const tokens = line.split(",").map((token) => token.trim());
811
+ for (const token of tokens) {
812
+ if (token.length === 0) {
813
+ invalidTokens.push("(empty)");
814
+ continue;
815
+ }
816
+ if (token === "none") {
817
+ hasNone = true;
818
+ continue;
819
+ }
820
+ if (CONTRACT_REF_ID_RE.test(token)) {
821
+ ids.push(token);
822
+ continue;
823
+ }
824
+ invalidTokens.push(token);
825
+ }
826
+ }
827
+ return {
828
+ lines,
829
+ ids: unique2(ids),
830
+ invalidTokens: unique2(invalidTokens),
831
+ hasNone
832
+ };
833
+ }
834
+ function unique2(values) {
835
+ return Array.from(new Set(values));
836
+ }
837
+
630
838
  // src/core/traceability.ts
631
- var import_promises5 = require("fs/promises");
632
- var import_node_path4 = __toESM(require("path"), 1);
839
+ var import_promises6 = require("fs/promises");
840
+ var import_node_path5 = __toESM(require("path"), 1);
633
841
 
634
842
  // src/core/gherkin/parse.ts
635
843
  var import_gherkin = require("@cucumber/gherkin");
@@ -662,7 +870,7 @@ var SC_TAG_RE = /^SC-\d{4}$/;
662
870
  var BR_TAG_RE = /^BR-\d{4}$/;
663
871
  var UI_TAG_RE = /^UI-\d{4}$/;
664
872
  var API_TAG_RE = /^API-\d{4}$/;
665
- var DATA_TAG_RE = /^DATA-\d{4}$/;
873
+ var DB_TAG_RE = /^DB-\d{4}$/;
666
874
  function parseScenarioDocument(text, uri) {
667
875
  const { gherkinDocument, errors } = parseGherkin(text, uri);
668
876
  if (!gherkinDocument) {
@@ -691,10 +899,10 @@ function buildScenarioAtoms(document) {
691
899
  return document.scenarios.map((scenario) => {
692
900
  const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
693
901
  const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
694
- const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
902
+ const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
695
903
  const contractIds = /* @__PURE__ */ new Set();
696
904
  scenario.tags.forEach((tag) => {
697
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
905
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
698
906
  contractIds.add(tag);
699
907
  }
700
908
  });
@@ -702,7 +910,7 @@ function buildScenarioAtoms(document) {
702
910
  for (const text of collectStepTexts(step)) {
703
911
  extractIds(text, "UI").forEach((id) => contractIds.add(id));
704
912
  extractIds(text, "API").forEach((id) => contractIds.add(id));
705
- extractIds(text, "DATA").forEach((id) => contractIds.add(id));
913
+ extractIds(text, "DB").forEach((id) => contractIds.add(id));
706
914
  }
707
915
  }
708
916
  const atom = {
@@ -781,7 +989,7 @@ function collectStepTexts(step) {
781
989
  }
782
990
  return texts;
783
991
  }
784
- function unique2(values) {
992
+ function unique3(values) {
785
993
  return Array.from(new Set(values));
786
994
  }
787
995
 
@@ -811,7 +1019,7 @@ function extractAnnotatedScIds(text) {
811
1019
  async function collectScIdsFromScenarioFiles(scenarioFiles) {
812
1020
  const scIds = /* @__PURE__ */ new Set();
813
1021
  for (const file of scenarioFiles) {
814
- const text = await (0, import_promises5.readFile)(file, "utf-8");
1022
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
815
1023
  const { document, errors } = parseScenarioDocument(text, file);
816
1024
  if (!document || errors.length > 0) {
817
1025
  continue;
@@ -829,7 +1037,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
829
1037
  async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
830
1038
  const sources = /* @__PURE__ */ new Map();
831
1039
  for (const file of scenarioFiles) {
832
- const text = await (0, import_promises5.readFile)(file, "utf-8");
1040
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
833
1041
  const { document, errors } = parseScenarioDocument(text, file);
834
1042
  if (!document || errors.length > 0) {
835
1043
  continue;
@@ -882,10 +1090,10 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
882
1090
  };
883
1091
  }
884
1092
  const normalizedFiles = Array.from(
885
- new Set(files.map((file) => import_node_path4.default.normalize(file)))
1093
+ new Set(files.map((file) => import_node_path5.default.normalize(file)))
886
1094
  );
887
1095
  for (const file of normalizedFiles) {
888
- const text = await (0, import_promises5.readFile)(file, "utf-8");
1096
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
889
1097
  const scIds = extractAnnotatedScIds(text);
890
1098
  if (scIds.length === 0) {
891
1099
  continue;
@@ -942,16 +1150,16 @@ function formatError3(error) {
942
1150
  }
943
1151
 
944
1152
  // src/core/version.ts
945
- var import_promises6 = require("fs/promises");
946
- var import_node_path5 = __toESM(require("path"), 1);
1153
+ var import_promises7 = require("fs/promises");
1154
+ var import_node_path6 = __toESM(require("path"), 1);
947
1155
  var import_node_url = require("url");
948
1156
  async function resolveToolVersion() {
949
- if ("0.4.2".length > 0) {
950
- return "0.4.2";
1157
+ if ("0.4.4".length > 0) {
1158
+ return "0.4.4";
951
1159
  }
952
1160
  try {
953
1161
  const packagePath = resolvePackageJsonPath();
954
- const raw = await (0, import_promises6.readFile)(packagePath, "utf-8");
1162
+ const raw = await (0, import_promises7.readFile)(packagePath, "utf-8");
955
1163
  const parsed = JSON.parse(raw);
956
1164
  const version = typeof parsed.version === "string" ? parsed.version : "";
957
1165
  return version.length > 0 ? version : "unknown";
@@ -962,54 +1170,23 @@ async function resolveToolVersion() {
962
1170
  function resolvePackageJsonPath() {
963
1171
  const base = __filename;
964
1172
  const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
965
- return import_node_path5.default.resolve(import_node_path5.default.dirname(basePath), "../../package.json");
1173
+ return import_node_path6.default.resolve(import_node_path6.default.dirname(basePath), "../../package.json");
966
1174
  }
967
1175
 
968
1176
  // src/core/validators/contracts.ts
969
- var import_promises7 = require("fs/promises");
970
- var import_node_path7 = __toESM(require("path"), 1);
1177
+ var import_promises8 = require("fs/promises");
1178
+ var import_node_path8 = __toESM(require("path"), 1);
971
1179
 
972
1180
  // src/core/contracts.ts
973
- var import_node_path6 = __toESM(require("path"), 1);
1181
+ var import_node_path7 = __toESM(require("path"), 1);
974
1182
  var import_yaml2 = require("yaml");
975
1183
  function parseStructuredContract(file, text) {
976
- const ext = import_node_path6.default.extname(file).toLowerCase();
1184
+ const ext = import_node_path7.default.extname(file).toLowerCase();
977
1185
  if (ext === ".json") {
978
1186
  return JSON.parse(text);
979
1187
  }
980
1188
  return (0, import_yaml2.parse)(text);
981
1189
  }
982
- function extractUiContractIds(doc) {
983
- const id = typeof doc.id === "string" ? doc.id : "";
984
- return extractIds(id, "UI");
985
- }
986
- function extractApiContractIds(doc) {
987
- const operationIds = /* @__PURE__ */ new Set();
988
- collectOperationIds(doc, operationIds);
989
- const ids = /* @__PURE__ */ new Set();
990
- for (const operationId of operationIds) {
991
- extractIds(operationId, "API").forEach((id) => ids.add(id));
992
- }
993
- return Array.from(ids);
994
- }
995
- function collectOperationIds(value, out) {
996
- if (!value || typeof value !== "object") {
997
- return;
998
- }
999
- if (Array.isArray(value)) {
1000
- for (const item of value) {
1001
- collectOperationIds(item, out);
1002
- }
1003
- return;
1004
- }
1005
- for (const [key, entry] of Object.entries(value)) {
1006
- if (key === "operationId" && typeof entry === "string") {
1007
- out.add(entry);
1008
- continue;
1009
- }
1010
- collectOperationIds(entry, out);
1011
- }
1012
- }
1013
1190
 
1014
1191
  // src/core/validators/contracts.ts
1015
1192
  var SQL_DANGEROUS_PATTERNS = [
@@ -1024,9 +1201,11 @@ var SQL_DANGEROUS_PATTERNS = [
1024
1201
  async function validateContracts(root, config) {
1025
1202
  const issues = [];
1026
1203
  const contractsRoot = resolvePath(root, config, "contractsDir");
1027
- issues.push(...await validateUiContracts(import_node_path7.default.join(contractsRoot, "ui")));
1028
- issues.push(...await validateApiContracts(import_node_path7.default.join(contractsRoot, "api")));
1029
- issues.push(...await validateDataContracts(import_node_path7.default.join(contractsRoot, "db")));
1204
+ issues.push(...await validateUiContracts(import_node_path8.default.join(contractsRoot, "ui")));
1205
+ issues.push(...await validateApiContracts(import_node_path8.default.join(contractsRoot, "api")));
1206
+ issues.push(...await validateDataContracts(import_node_path8.default.join(contractsRoot, "db")));
1207
+ const contractIndex = await buildContractIndex(root, config);
1208
+ issues.push(...validateDuplicateContractIds(contractIndex));
1030
1209
  return issues;
1031
1210
  }
1032
1211
  async function validateUiContracts(uiRoot) {
@@ -1044,14 +1223,14 @@ async function validateUiContracts(uiRoot) {
1044
1223
  }
1045
1224
  const issues = [];
1046
1225
  for (const file of files) {
1047
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1226
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
1048
1227
  const invalidIds = extractInvalidIds(text, [
1049
1228
  "SPEC",
1050
1229
  "BR",
1051
1230
  "SC",
1052
1231
  "UI",
1053
1232
  "API",
1054
- "DATA",
1233
+ "DB",
1055
1234
  "ADR"
1056
1235
  ]);
1057
1236
  if (invalidIds.length > 0) {
@@ -1066,9 +1245,10 @@ async function validateUiContracts(uiRoot) {
1066
1245
  )
1067
1246
  );
1068
1247
  }
1069
- let doc;
1248
+ const declaredIds = extractDeclaredContractIds(text);
1249
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
1070
1250
  try {
1071
- doc = parseStructuredContract(file, text);
1251
+ parseStructuredContract(file, stripContractDeclarationLines(text));
1072
1252
  } catch (error) {
1073
1253
  issues.push(
1074
1254
  issue(
@@ -1079,19 +1259,6 @@ async function validateUiContracts(uiRoot) {
1079
1259
  "contracts.ui.parse"
1080
1260
  )
1081
1261
  );
1082
- continue;
1083
- }
1084
- const uiIds = extractUiContractIds(doc);
1085
- if (uiIds.length === 0) {
1086
- issues.push(
1087
- issue(
1088
- "QFAI-CONTRACT-002",
1089
- `UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
1090
- "error",
1091
- file,
1092
- "contracts.ui.id"
1093
- )
1094
- );
1095
1262
  }
1096
1263
  }
1097
1264
  return issues;
@@ -1111,14 +1278,14 @@ async function validateApiContracts(apiRoot) {
1111
1278
  }
1112
1279
  const issues = [];
1113
1280
  for (const file of files) {
1114
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1281
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
1115
1282
  const invalidIds = extractInvalidIds(text, [
1116
1283
  "SPEC",
1117
1284
  "BR",
1118
1285
  "SC",
1119
1286
  "UI",
1120
1287
  "API",
1121
- "DATA",
1288
+ "DB",
1122
1289
  "ADR"
1123
1290
  ]);
1124
1291
  if (invalidIds.length > 0) {
@@ -1133,9 +1300,11 @@ async function validateApiContracts(apiRoot) {
1133
1300
  )
1134
1301
  );
1135
1302
  }
1303
+ const declaredIds = extractDeclaredContractIds(text);
1304
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "API"));
1136
1305
  let doc;
1137
1306
  try {
1138
- doc = parseStructuredContract(file, text);
1307
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
1139
1308
  } catch (error) {
1140
1309
  issues.push(
1141
1310
  issue(
@@ -1159,18 +1328,6 @@ async function validateApiContracts(apiRoot) {
1159
1328
  )
1160
1329
  );
1161
1330
  }
1162
- const apiIds = extractApiContractIds(doc);
1163
- if (apiIds.length === 0) {
1164
- issues.push(
1165
- issue(
1166
- "QFAI-CONTRACT-002",
1167
- `API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
1168
- "error",
1169
- file,
1170
- "contracts.api.id"
1171
- )
1172
- );
1173
- }
1174
1331
  }
1175
1332
  return issues;
1176
1333
  }
@@ -1179,24 +1336,24 @@ async function validateDataContracts(dataRoot) {
1179
1336
  if (files.length === 0) {
1180
1337
  return [
1181
1338
  issue(
1182
- "QFAI-DATA-000",
1183
- "DATA \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1339
+ "QFAI-DB-000",
1340
+ "DB \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1184
1341
  "info",
1185
1342
  dataRoot,
1186
- "contracts.data.files"
1343
+ "contracts.db.files"
1187
1344
  )
1188
1345
  ];
1189
1346
  }
1190
1347
  const issues = [];
1191
1348
  for (const file of files) {
1192
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1349
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
1193
1350
  const invalidIds = extractInvalidIds(text, [
1194
1351
  "SPEC",
1195
1352
  "BR",
1196
1353
  "SC",
1197
1354
  "UI",
1198
1355
  "API",
1199
- "DATA",
1356
+ "DB",
1200
1357
  "ADR"
1201
1358
  ]);
1202
1359
  if (invalidIds.length > 0) {
@@ -1211,6 +1368,8 @@ async function validateDataContracts(dataRoot) {
1211
1368
  )
1212
1369
  );
1213
1370
  }
1371
+ const declaredIds = extractDeclaredContractIds(text);
1372
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "DB"));
1214
1373
  issues.push(...lintSql(text, file));
1215
1374
  }
1216
1375
  return issues;
@@ -1221,17 +1380,83 @@ function lintSql(text, file) {
1221
1380
  if (pattern.test(text)) {
1222
1381
  issues.push(
1223
1382
  issue(
1224
- "QFAI-DATA-001",
1383
+ "QFAI-DB-001",
1225
1384
  `\u5371\u967A\u306A SQL \u64CD\u4F5C\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059: ${label}`,
1226
1385
  "warning",
1227
1386
  file,
1228
- "contracts.data.sql"
1387
+ "contracts.db.sql"
1229
1388
  )
1230
1389
  );
1231
1390
  }
1232
1391
  }
1233
1392
  return issues;
1234
1393
  }
1394
+ function validateDeclaredContractIds(ids, file, kind) {
1395
+ const issues = [];
1396
+ if (ids.length === 0) {
1397
+ issues.push(
1398
+ issue(
1399
+ "QFAI-CONTRACT-010",
1400
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
1401
+ "error",
1402
+ file,
1403
+ "contracts.declaration"
1404
+ )
1405
+ );
1406
+ return issues;
1407
+ }
1408
+ if (ids.length > 1) {
1409
+ issues.push(
1410
+ issue(
1411
+ "QFAI-CONTRACT-011",
1412
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B\u8907\u6570\u306E QFAI-CONTRACT-ID \u304C\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${ids.join(
1413
+ ", "
1414
+ )}`,
1415
+ "error",
1416
+ file,
1417
+ "contracts.declaration",
1418
+ ids
1419
+ )
1420
+ );
1421
+ return issues;
1422
+ }
1423
+ const [id] = ids;
1424
+ if (id && !id.startsWith(`${kind}-`)) {
1425
+ issues.push(
1426
+ issue(
1427
+ "QFAI-CONTRACT-013",
1428
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E QFAI-CONTRACT-ID \u304C ${kind}- \u3067\u306F\u3042\u308A\u307E\u305B\u3093: ${id}`,
1429
+ "error",
1430
+ file,
1431
+ "contracts.declarationPrefix",
1432
+ [id]
1433
+ )
1434
+ );
1435
+ }
1436
+ return issues;
1437
+ }
1438
+ function validateDuplicateContractIds(contractIndex) {
1439
+ const issues = [];
1440
+ for (const [id, files] of contractIndex.idToFiles.entries()) {
1441
+ if (files.size <= 1) {
1442
+ continue;
1443
+ }
1444
+ const sortedFiles = Array.from(files).sort((a, b) => a.localeCompare(b));
1445
+ issues.push(
1446
+ issue(
1447
+ "QFAI-CONTRACT-012",
1448
+ `\u5951\u7D04 ID \u304C\u8907\u6570\u30D5\u30A1\u30A4\u30EB\u3067\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${id} (${sortedFiles.join(
1449
+ ", "
1450
+ )})`,
1451
+ "error",
1452
+ sortedFiles[0],
1453
+ "contracts.idDuplicate",
1454
+ [id]
1455
+ )
1456
+ );
1457
+ }
1458
+ return issues;
1459
+ }
1235
1460
  function hasOpenApi(doc) {
1236
1461
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
1237
1462
  }
@@ -1260,8 +1485,8 @@ function issue(code, message, severity, file, rule, refs) {
1260
1485
  }
1261
1486
 
1262
1487
  // src/core/validators/delta.ts
1263
- var import_promises8 = require("fs/promises");
1264
- var import_node_path8 = __toESM(require("path"), 1);
1488
+ var import_promises9 = require("fs/promises");
1489
+ var import_node_path9 = __toESM(require("path"), 1);
1265
1490
  var SECTION_RE = /^##\s+変更区分/m;
1266
1491
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1267
1492
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1275,10 +1500,10 @@ async function validateDeltas(root, config) {
1275
1500
  }
1276
1501
  const issues = [];
1277
1502
  for (const pack of packs) {
1278
- const deltaPath = import_node_path8.default.join(pack, "delta.md");
1503
+ const deltaPath = import_node_path9.default.join(pack, "delta.md");
1279
1504
  let text;
1280
1505
  try {
1281
- text = await (0, import_promises8.readFile)(deltaPath, "utf-8");
1506
+ text = await (0, import_promises9.readFile)(deltaPath, "utf-8");
1282
1507
  } catch (error) {
1283
1508
  if (isMissingFileError2(error)) {
1284
1509
  issues.push(
@@ -1352,187 +1577,6 @@ function issue2(code, message, severity, file, rule, refs) {
1352
1577
  // src/core/validators/ids.ts
1353
1578
  var import_promises10 = require("fs/promises");
1354
1579
  var import_node_path10 = __toESM(require("path"), 1);
1355
-
1356
- // src/core/contractIndex.ts
1357
- var import_promises9 = require("fs/promises");
1358
- var import_node_path9 = __toESM(require("path"), 1);
1359
- async function buildContractIndex(root, config) {
1360
- const contractsRoot = resolvePath(root, config, "contractsDir");
1361
- const uiRoot = import_node_path9.default.join(contractsRoot, "ui");
1362
- const apiRoot = import_node_path9.default.join(contractsRoot, "api");
1363
- const dataRoot = import_node_path9.default.join(contractsRoot, "db");
1364
- const [uiFiles, apiFiles, dataFiles] = await Promise.all([
1365
- collectUiContractFiles(uiRoot),
1366
- collectApiContractFiles(apiRoot),
1367
- collectDataContractFiles(dataRoot)
1368
- ]);
1369
- const index = {
1370
- ids: /* @__PURE__ */ new Set(),
1371
- idToFiles: /* @__PURE__ */ new Map(),
1372
- files: { ui: uiFiles, api: apiFiles, data: dataFiles },
1373
- structuredParseFailedFiles: /* @__PURE__ */ new Set()
1374
- };
1375
- await indexUiContracts(uiFiles, index);
1376
- await indexApiContracts(apiFiles, index);
1377
- await indexDataContracts(dataFiles, index);
1378
- return index;
1379
- }
1380
- async function indexUiContracts(files, index) {
1381
- for (const file of files) {
1382
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1383
- try {
1384
- const doc = parseStructuredContract(file, text);
1385
- extractUiContractIds(doc).forEach((id) => record(index, id, file));
1386
- } catch {
1387
- index.structuredParseFailedFiles.add(file);
1388
- extractIds(text, "UI").forEach((id) => record(index, id, file));
1389
- }
1390
- }
1391
- }
1392
- async function indexApiContracts(files, index) {
1393
- for (const file of files) {
1394
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1395
- try {
1396
- const doc = parseStructuredContract(file, text);
1397
- extractApiContractIds(doc).forEach((id) => record(index, id, file));
1398
- } catch {
1399
- index.structuredParseFailedFiles.add(file);
1400
- extractIds(text, "API").forEach((id) => record(index, id, file));
1401
- }
1402
- }
1403
- }
1404
- async function indexDataContracts(files, index) {
1405
- for (const file of files) {
1406
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1407
- extractIds(text, "DATA").forEach((id) => record(index, id, file));
1408
- }
1409
- }
1410
- function record(index, id, file) {
1411
- index.ids.add(id);
1412
- const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
1413
- current.add(file);
1414
- index.idToFiles.set(id, current);
1415
- }
1416
-
1417
- // src/core/parse/markdown.ts
1418
- var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1419
- function parseHeadings(md) {
1420
- const lines = md.split(/\r?\n/);
1421
- const headings = [];
1422
- for (let i = 0; i < lines.length; i++) {
1423
- const line = lines[i] ?? "";
1424
- const match = line.match(HEADING_RE);
1425
- if (!match) continue;
1426
- const levelToken = match[1];
1427
- const title = match[2];
1428
- if (!levelToken || !title) continue;
1429
- headings.push({
1430
- level: levelToken.length,
1431
- title: title.trim(),
1432
- line: i + 1
1433
- });
1434
- }
1435
- return headings;
1436
- }
1437
- function extractH2Sections(md) {
1438
- const lines = md.split(/\r?\n/);
1439
- const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1440
- const sections = /* @__PURE__ */ new Map();
1441
- for (let i = 0; i < headings.length; i++) {
1442
- const current = headings[i];
1443
- if (!current) continue;
1444
- const next = headings[i + 1];
1445
- const startLine = current.line + 1;
1446
- const endLine = (next?.line ?? lines.length + 1) - 1;
1447
- const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1448
- sections.set(current.title.trim(), {
1449
- title: current.title.trim(),
1450
- startLine,
1451
- endLine,
1452
- body
1453
- });
1454
- }
1455
- return sections;
1456
- }
1457
-
1458
- // src/core/parse/spec.ts
1459
- var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1460
- var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1461
- var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1462
- var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1463
- var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1464
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1465
- function parseSpec(md, file) {
1466
- const headings = parseHeadings(md);
1467
- const h1 = headings.find((heading) => heading.level === 1);
1468
- const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1469
- const sections = extractH2Sections(md);
1470
- const sectionNames = new Set(Array.from(sections.keys()));
1471
- const brSection = sections.get(BR_SECTION_TITLE);
1472
- const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1473
- const startLine = brSection?.startLine ?? 1;
1474
- const brs = [];
1475
- const brsWithoutPriority = [];
1476
- const brsWithInvalidPriority = [];
1477
- for (let i = 0; i < brLines.length; i++) {
1478
- const lineText = brLines[i] ?? "";
1479
- const lineNumber = startLine + i;
1480
- const validMatch = lineText.match(BR_LINE_RE);
1481
- if (validMatch) {
1482
- const id = validMatch[1];
1483
- const priority = validMatch[2];
1484
- const text = validMatch[3];
1485
- if (!id || !priority || !text) continue;
1486
- brs.push({
1487
- id,
1488
- priority,
1489
- text: text.trim(),
1490
- line: lineNumber
1491
- });
1492
- continue;
1493
- }
1494
- const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1495
- if (anyPriorityMatch) {
1496
- const id = anyPriorityMatch[1];
1497
- const priority = anyPriorityMatch[2];
1498
- const text = anyPriorityMatch[3];
1499
- if (!id || !priority || !text) continue;
1500
- if (!VALID_PRIORITIES.has(priority)) {
1501
- brsWithInvalidPriority.push({
1502
- id,
1503
- priority,
1504
- text: text.trim(),
1505
- line: lineNumber
1506
- });
1507
- }
1508
- continue;
1509
- }
1510
- const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1511
- if (noPriorityMatch) {
1512
- const id = noPriorityMatch[1];
1513
- const text = noPriorityMatch[2];
1514
- if (!id || !text) continue;
1515
- brsWithoutPriority.push({
1516
- id,
1517
- text: text.trim(),
1518
- line: lineNumber
1519
- });
1520
- }
1521
- }
1522
- const parsed = {
1523
- file,
1524
- sections: sectionNames,
1525
- brs,
1526
- brsWithoutPriority,
1527
- brsWithInvalidPriority
1528
- };
1529
- if (specId) {
1530
- parsed.specId = specId;
1531
- }
1532
- return parsed;
1533
- }
1534
-
1535
- // src/core/validators/ids.ts
1536
1580
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1537
1581
  async function validateDefinedIds(root, config) {
1538
1582
  const issues = [];
@@ -1675,7 +1719,7 @@ function validateScenarioContent(text, file) {
1675
1719
  "SC",
1676
1720
  "UI",
1677
1721
  "API",
1678
- "DATA",
1722
+ "DB",
1679
1723
  "ADR"
1680
1724
  ]);
1681
1725
  if (invalidIds.length > 0) {
@@ -1873,7 +1917,7 @@ function validateSpecContent(text, file, requiredSections) {
1873
1917
  "SC",
1874
1918
  "UI",
1875
1919
  "API",
1876
- "DATA",
1920
+ "DB",
1877
1921
  "ADR"
1878
1922
  ]);
1879
1923
  if (invalidIds.length > 0) {
@@ -2002,8 +2046,7 @@ async function validateTraceability(root, config) {
2002
2046
  const brIdsInSpecs = /* @__PURE__ */ new Set();
2003
2047
  const brIdsInScenarios = /* @__PURE__ */ new Set();
2004
2048
  const scIdsInScenarios = /* @__PURE__ */ new Set();
2005
- const scenarioContractIds = /* @__PURE__ */ new Set();
2006
- const scWithContracts = /* @__PURE__ */ new Set();
2049
+ const specContractIds = /* @__PURE__ */ new Set();
2007
2050
  const specToBrIds = /* @__PURE__ */ new Map();
2008
2051
  const contractIndex = await buildContractIndex(root, config);
2009
2052
  const contractIds = contractIndex.ids;
@@ -2021,6 +2064,64 @@ async function validateTraceability(root, config) {
2021
2064
  brIds.forEach((id) => current.add(id));
2022
2065
  specToBrIds.set(parsed.specId, current);
2023
2066
  }
2067
+ const contractRefs = parsed.contractRefs;
2068
+ if (contractRefs.lines.length === 0) {
2069
+ issues.push(
2070
+ issue6(
2071
+ "QFAI-TRACE-020",
2072
+ "Spec \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
2073
+ "error",
2074
+ file,
2075
+ "traceability.specContractRefRequired"
2076
+ )
2077
+ );
2078
+ } else {
2079
+ if (contractRefs.hasNone && contractRefs.ids.length > 0) {
2080
+ issues.push(
2081
+ issue6(
2082
+ "QFAI-TRACE-021",
2083
+ "Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2084
+ "error",
2085
+ file,
2086
+ "traceability.specContractRefFormat"
2087
+ )
2088
+ );
2089
+ }
2090
+ if (contractRefs.invalidTokens.length > 0) {
2091
+ issues.push(
2092
+ issue6(
2093
+ "QFAI-TRACE-021",
2094
+ `Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
2095
+ ", "
2096
+ )}`,
2097
+ "error",
2098
+ file,
2099
+ "traceability.specContractRefFormat",
2100
+ contractRefs.invalidTokens
2101
+ )
2102
+ );
2103
+ }
2104
+ }
2105
+ contractRefs.ids.forEach((id) => {
2106
+ specContractIds.add(id);
2107
+ });
2108
+ const unknownContractIds = contractRefs.ids.filter(
2109
+ (id) => !contractIds.has(id)
2110
+ );
2111
+ if (unknownContractIds.length > 0) {
2112
+ issues.push(
2113
+ issue6(
2114
+ "QFAI-TRACE-021",
2115
+ `Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
2116
+ ", "
2117
+ )}`,
2118
+ "error",
2119
+ file,
2120
+ "traceability.specContractExists",
2121
+ unknownContractIds
2122
+ )
2123
+ );
2124
+ }
2024
2125
  }
2025
2126
  for (const file of scenarioFiles) {
2026
2127
  const text = await (0, import_promises13.readFile)(file, "utf-8");
@@ -2066,10 +2167,6 @@ async function validateTraceability(root, config) {
2066
2167
  scIdsInScenarios.add(id);
2067
2168
  scIdsInFile.add(id);
2068
2169
  });
2069
- atom.contractIds.forEach((id) => scenarioContractIds.add(id));
2070
- if (atom.contractIds.length > 0) {
2071
- scTags.forEach((id) => scWithContracts.add(id));
2072
- }
2073
2170
  const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
2074
2171
  if (unknownSpecIds.length > 0) {
2075
2172
  issues.push(
@@ -2188,25 +2285,6 @@ async function validateTraceability(root, config) {
2188
2285
  );
2189
2286
  }
2190
2287
  }
2191
- if (config.validation.traceability.scMustTouchContracts && scIdsInScenarios.size > 0) {
2192
- const scWithoutContracts = Array.from(scIdsInScenarios).filter(
2193
- (id) => !scWithContracts.has(id)
2194
- );
2195
- if (scWithoutContracts.length > 0) {
2196
- issues.push(
2197
- issue6(
2198
- "QFAI_TRACE_SC_NO_CONTRACT",
2199
- `SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
2200
- ", "
2201
- )}`,
2202
- "error",
2203
- specsRoot,
2204
- "traceability.scMustTouchContracts",
2205
- scWithoutContracts
2206
- )
2207
- );
2208
- }
2209
- }
2210
2288
  const scRefsResult = await collectScTestReferences(
2211
2289
  root,
2212
2290
  config.validation.traceability.testFileGlobs,
@@ -2270,16 +2348,16 @@ async function validateTraceability(root, config) {
2270
2348
  if (!config.validation.traceability.allowOrphanContracts) {
2271
2349
  if (contractIds.size > 0) {
2272
2350
  const orphanContracts = Array.from(contractIds).filter(
2273
- (id) => !scenarioContractIds.has(id)
2351
+ (id) => !specContractIds.has(id)
2274
2352
  );
2275
2353
  if (orphanContracts.length > 0) {
2276
2354
  issues.push(
2277
2355
  issue6(
2278
- "QFAI_CONTRACT_ORPHAN",
2279
- `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
2356
+ "QFAI-TRACE-022",
2357
+ `\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
2280
2358
  "error",
2281
2359
  specsRoot,
2282
- "traceability.allowOrphanContracts",
2360
+ "traceability.contractCoverage",
2283
2361
  orphanContracts
2284
2362
  )
2285
2363
  );
@@ -2400,7 +2478,7 @@ function countIssues(issues) {
2400
2478
  }
2401
2479
 
2402
2480
  // src/core/report.ts
2403
- var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
2481
+ var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
2404
2482
  async function createReportData(root, validation, configResult) {
2405
2483
  const resolved = configResult ?? await loadConfig(root);
2406
2484
  const config = resolved.config;
@@ -2419,6 +2497,23 @@ async function createReportData(root, validation, configResult) {
2419
2497
  ui: uiFiles,
2420
2498
  db: dbFiles
2421
2499
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
2500
+ const contractIndex = await buildContractIndex(root, config);
2501
+ const specContractRefs = await collectSpecContractRefs(specFiles);
2502
+ const contractIdList = Array.from(contractIndex.ids);
2503
+ const referencedContracts = /* @__PURE__ */ new Set();
2504
+ for (const ids of specContractRefs.specToContractIds.values()) {
2505
+ ids.forEach((id) => referencedContracts.add(id));
2506
+ }
2507
+ const referencedContractCount = contractIdList.filter(
2508
+ (id) => referencedContracts.has(id)
2509
+ ).length;
2510
+ const orphanContractCount = contractIdList.filter(
2511
+ (id) => !referencedContracts.has(id)
2512
+ ).length;
2513
+ const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
2514
+ const specToContractIdsRecord = mapToSortedRecord(
2515
+ specContractRefs.specToContractIds
2516
+ );
2422
2517
  const idsByPrefix = await collectIds([
2423
2518
  ...specFiles,
2424
2519
  ...scenarioFiles,
@@ -2469,14 +2564,24 @@ async function createReportData(root, validation, configResult) {
2469
2564
  sc: idsByPrefix.SC,
2470
2565
  ui: idsByPrefix.UI,
2471
2566
  api: idsByPrefix.API,
2472
- data: idsByPrefix.DATA
2567
+ db: idsByPrefix.DB
2473
2568
  },
2474
2569
  traceability: {
2475
2570
  upstreamIdsFound: upstreamIds.size,
2476
2571
  referencedInCodeOrTests: traceability,
2477
2572
  sc: scCoverage,
2478
2573
  scSources: scSourceRecord,
2479
- testFiles
2574
+ testFiles,
2575
+ contracts: {
2576
+ total: contractIdList.length,
2577
+ referenced: referencedContractCount,
2578
+ orphan: orphanContractCount,
2579
+ idToSpecs: contractIdToSpecsRecord
2580
+ },
2581
+ specs: {
2582
+ contractRefMissing: specContractRefs.missingRefSpecs.size,
2583
+ specToContractIds: specToContractIdsRecord
2584
+ }
2480
2585
  },
2481
2586
  issues: resolvedValidation.issues
2482
2587
  };
@@ -2505,7 +2610,7 @@ function formatReportMarkdown(data) {
2505
2610
  lines.push(formatIdLine("SC", data.ids.sc));
2506
2611
  lines.push(formatIdLine("UI", data.ids.ui));
2507
2612
  lines.push(formatIdLine("API", data.ids.api));
2508
- lines.push(formatIdLine("DATA", data.ids.data));
2613
+ lines.push(formatIdLine("DB", data.ids.db));
2509
2614
  lines.push("");
2510
2615
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
2511
2616
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
@@ -2513,6 +2618,50 @@ function formatReportMarkdown(data) {
2513
2618
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2514
2619
  );
2515
2620
  lines.push("");
2621
+ lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
2622
+ lines.push(`- total: ${data.traceability.contracts.total}`);
2623
+ lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
2624
+ lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
2625
+ lines.push(
2626
+ `- specContractRefMissing: ${data.traceability.specs.contractRefMissing}`
2627
+ );
2628
+ lines.push("");
2629
+ lines.push("## \u5951\u7D04\u2192Spec");
2630
+ const contractToSpecs = data.traceability.contracts.idToSpecs;
2631
+ const contractIds = Object.keys(contractToSpecs).sort(
2632
+ (a, b) => a.localeCompare(b)
2633
+ );
2634
+ if (contractIds.length === 0) {
2635
+ lines.push("- (none)");
2636
+ } else {
2637
+ for (const contractId of contractIds) {
2638
+ const specs = contractToSpecs[contractId] ?? [];
2639
+ if (specs.length === 0) {
2640
+ lines.push(`- ${contractId}: (none)`);
2641
+ } else {
2642
+ lines.push(`- ${contractId}: ${specs.join(", ")}`);
2643
+ }
2644
+ }
2645
+ }
2646
+ lines.push("");
2647
+ lines.push("## Spec\u2192\u5951\u7D04");
2648
+ const specToContracts = data.traceability.specs.specToContractIds;
2649
+ const specIds = Object.keys(specToContracts).sort(
2650
+ (a, b) => a.localeCompare(b)
2651
+ );
2652
+ if (specIds.length === 0) {
2653
+ lines.push("- (none)");
2654
+ } else {
2655
+ for (const specId of specIds) {
2656
+ const contractIds2 = specToContracts[specId] ?? [];
2657
+ if (contractIds2.length === 0) {
2658
+ lines.push(`- ${specId}: (none)`);
2659
+ } else {
2660
+ lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
2661
+ }
2662
+ }
2663
+ }
2664
+ lines.push("");
2516
2665
  lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2517
2666
  lines.push(`- total: ${data.traceability.sc.total}`);
2518
2667
  lines.push(`- covered: ${data.traceability.sc.covered}`);
@@ -2586,7 +2735,7 @@ function formatReportMarkdown(data) {
2586
2735
  lines.push("");
2587
2736
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
2588
2737
  const traceIssues = data.issues.filter(
2589
- (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-") || item.code === "QFAI_CONTRACT_ORPHAN"
2738
+ (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
2590
2739
  );
2591
2740
  if (traceIssues.length === 0) {
2592
2741
  lines.push("- (none)");
@@ -2616,6 +2765,33 @@ function formatReportMarkdown(data) {
2616
2765
  function formatReportJson(data) {
2617
2766
  return JSON.stringify(data, null, 2);
2618
2767
  }
2768
+ async function collectSpecContractRefs(specFiles) {
2769
+ const specToContractIds = /* @__PURE__ */ new Map();
2770
+ const idToSpecs = /* @__PURE__ */ new Map();
2771
+ const missingRefSpecs = /* @__PURE__ */ new Set();
2772
+ for (const file of specFiles) {
2773
+ const text = await (0, import_promises14.readFile)(file, "utf-8");
2774
+ const parsed = parseSpec(text, file);
2775
+ const specKey = parsed.specId ?? file;
2776
+ const refs = parsed.contractRefs;
2777
+ if (refs.lines.length === 0) {
2778
+ missingRefSpecs.add(specKey);
2779
+ }
2780
+ const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
2781
+ for (const id of refs.ids) {
2782
+ currentContracts.add(id);
2783
+ const specs = idToSpecs.get(id) ?? /* @__PURE__ */ new Set();
2784
+ specs.add(specKey);
2785
+ idToSpecs.set(id, specs);
2786
+ }
2787
+ specToContractIds.set(specKey, currentContracts);
2788
+ }
2789
+ return {
2790
+ specToContractIds,
2791
+ idToSpecs,
2792
+ missingRefSpecs
2793
+ };
2794
+ }
2619
2795
  async function collectIds(files) {
2620
2796
  const result = {
2621
2797
  SPEC: /* @__PURE__ */ new Set(),
@@ -2623,7 +2799,7 @@ async function collectIds(files) {
2623
2799
  SC: /* @__PURE__ */ new Set(),
2624
2800
  UI: /* @__PURE__ */ new Set(),
2625
2801
  API: /* @__PURE__ */ new Set(),
2626
- DATA: /* @__PURE__ */ new Set()
2802
+ DB: /* @__PURE__ */ new Set()
2627
2803
  };
2628
2804
  for (const file of files) {
2629
2805
  const text = await (0, import_promises14.readFile)(file, "utf-8");
@@ -2638,7 +2814,7 @@ async function collectIds(files) {
2638
2814
  SC: toSortedArray2(result.SC),
2639
2815
  UI: toSortedArray2(result.UI),
2640
2816
  API: toSortedArray2(result.API),
2641
- DATA: toSortedArray2(result.DATA)
2817
+ DB: toSortedArray2(result.DB)
2642
2818
  };
2643
2819
  }
2644
2820
  async function collectUpstreamIds(files) {