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
@@ -186,7 +186,6 @@ var defaultConfig = {
186
186
  },
187
187
  traceability: {
188
188
  brMustHaveSc: true,
189
- scMustTouchContracts: true,
190
189
  scMustHaveTest: true,
191
190
  testFileGlobs: [],
192
191
  testFileExcludeGlobs: [],
@@ -363,13 +362,6 @@ function normalizeValidation(raw, configPath, issues) {
363
362
  configPath,
364
363
  issues
365
364
  ),
366
- scMustTouchContracts: readBoolean(
367
- traceabilityRaw?.scMustTouchContracts,
368
- base.traceability.scMustTouchContracts,
369
- "validation.traceability.scMustTouchContracts",
370
- configPath,
371
- issues
372
- ),
373
365
  scMustHaveTest: readBoolean(
374
366
  traceabilityRaw?.scMustHaveTest,
375
367
  base.traceability.scMustHaveTest,
@@ -526,6 +518,10 @@ function isRecord(value) {
526
518
  import { readFile as readFile11 } from "fs/promises";
527
519
  import path14 from "path";
528
520
 
521
+ // src/core/contractIndex.ts
522
+ import { readFile as readFile2 } from "fs/promises";
523
+ import path7 from "path";
524
+
529
525
  // src/core/discovery.ts
530
526
  import { access as access3 } from "fs/promises";
531
527
 
@@ -648,14 +644,14 @@ async function collectUiContractFiles(uiRoot) {
648
644
  async function collectApiContractFiles(apiRoot) {
649
645
  return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
650
646
  }
651
- async function collectDataContractFiles(dataRoot) {
652
- return collectFiles(dataRoot, { extensions: [".sql"] });
647
+ async function collectDbContractFiles(dbRoot) {
648
+ return collectFiles(dbRoot, { extensions: [".sql"] });
653
649
  }
654
- async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
650
+ async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
655
651
  const [ui, api, db] = await Promise.all([
656
652
  collectUiContractFiles(uiRoot),
657
653
  collectApiContractFiles(apiRoot),
658
- collectDataContractFiles(dataRoot)
654
+ collectDbContractFiles(dbRoot)
659
655
  ]);
660
656
  return { ui, api, db };
661
657
  }
@@ -677,15 +673,66 @@ async function exists3(target) {
677
673
  }
678
674
  }
679
675
 
676
+ // src/core/contractsDecl.ts
677
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
678
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
679
+ function extractDeclaredContractIds(text) {
680
+ const ids = [];
681
+ for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
682
+ const id = match[1];
683
+ if (id) {
684
+ ids.push(id);
685
+ }
686
+ }
687
+ return ids;
688
+ }
689
+ function stripContractDeclarationLines(text) {
690
+ return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
691
+ }
692
+
693
+ // src/core/contractIndex.ts
694
+ async function buildContractIndex(root, config) {
695
+ const contractsRoot = resolvePath(root, config, "contractsDir");
696
+ const uiRoot = path7.join(contractsRoot, "ui");
697
+ const apiRoot = path7.join(contractsRoot, "api");
698
+ const dbRoot = path7.join(contractsRoot, "db");
699
+ const [uiFiles, apiFiles, dbFiles] = await Promise.all([
700
+ collectUiContractFiles(uiRoot),
701
+ collectApiContractFiles(apiRoot),
702
+ collectDbContractFiles(dbRoot)
703
+ ]);
704
+ const index = {
705
+ ids: /* @__PURE__ */ new Set(),
706
+ idToFiles: /* @__PURE__ */ new Map(),
707
+ files: { ui: uiFiles, api: apiFiles, db: dbFiles }
708
+ };
709
+ await indexContractFiles(uiFiles, index);
710
+ await indexContractFiles(apiFiles, index);
711
+ await indexContractFiles(dbFiles, index);
712
+ return index;
713
+ }
714
+ async function indexContractFiles(files, index) {
715
+ for (const file of files) {
716
+ const text = await readFile2(file, "utf-8");
717
+ extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
718
+ }
719
+ }
720
+ function record(index, id, file) {
721
+ index.ids.add(id);
722
+ const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
723
+ current.add(file);
724
+ index.idToFiles.set(id, current);
725
+ }
726
+
680
727
  // src/core/ids.ts
681
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
728
+ var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
682
729
  var STRICT_ID_PATTERNS = {
683
730
  SPEC: /\bSPEC-\d{4}\b/g,
684
731
  BR: /\bBR-\d{4}\b/g,
685
732
  SC: /\bSC-\d{4}\b/g,
686
733
  UI: /\bUI-\d{4}\b/g,
687
734
  API: /\bAPI-\d{4}\b/g,
688
- DATA: /\bDATA-\d{4}\b/g,
735
+ DB: /\bDB-\d{4}\b/g,
689
736
  ADR: /\bADR-\d{4}\b/g
690
737
  };
691
738
  var LOOSE_ID_PATTERNS = {
@@ -694,7 +741,7 @@ var LOOSE_ID_PATTERNS = {
694
741
  SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
695
742
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
696
743
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
697
- DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi,
744
+ DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
698
745
  ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
699
746
  };
700
747
  function extractIds(text, prefix) {
@@ -730,9 +777,170 @@ function isValidId(value, prefix) {
730
777
  return strict.test(value);
731
778
  }
732
779
 
780
+ // src/core/parse/markdown.ts
781
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
782
+ function parseHeadings(md) {
783
+ const lines = md.split(/\r?\n/);
784
+ const headings = [];
785
+ for (let i = 0; i < lines.length; i++) {
786
+ const line = lines[i] ?? "";
787
+ const match = line.match(HEADING_RE);
788
+ if (!match) continue;
789
+ const levelToken = match[1];
790
+ const title = match[2];
791
+ if (!levelToken || !title) continue;
792
+ headings.push({
793
+ level: levelToken.length,
794
+ title: title.trim(),
795
+ line: i + 1
796
+ });
797
+ }
798
+ return headings;
799
+ }
800
+ function extractH2Sections(md) {
801
+ const lines = md.split(/\r?\n/);
802
+ const headings = parseHeadings(md).filter((heading) => heading.level === 2);
803
+ const sections = /* @__PURE__ */ new Map();
804
+ for (let i = 0; i < headings.length; i++) {
805
+ const current = headings[i];
806
+ if (!current) continue;
807
+ const next = headings[i + 1];
808
+ const startLine = current.line + 1;
809
+ const endLine = (next?.line ?? lines.length + 1) - 1;
810
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
811
+ sections.set(current.title.trim(), {
812
+ title: current.title.trim(),
813
+ startLine,
814
+ endLine,
815
+ body
816
+ });
817
+ }
818
+ return sections;
819
+ }
820
+
821
+ // src/core/parse/spec.ts
822
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
823
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
824
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
825
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
826
+ var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
827
+ var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
828
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
829
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
830
+ function parseSpec(md, file) {
831
+ const headings = parseHeadings(md);
832
+ const h1 = headings.find((heading) => heading.level === 1);
833
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
834
+ const sections = extractH2Sections(md);
835
+ const sectionNames = new Set(Array.from(sections.keys()));
836
+ const brSection = sections.get(BR_SECTION_TITLE);
837
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
838
+ const startLine = brSection?.startLine ?? 1;
839
+ const brs = [];
840
+ const brsWithoutPriority = [];
841
+ const brsWithInvalidPriority = [];
842
+ for (let i = 0; i < brLines.length; i++) {
843
+ const lineText = brLines[i] ?? "";
844
+ const lineNumber = startLine + i;
845
+ const validMatch = lineText.match(BR_LINE_RE);
846
+ if (validMatch) {
847
+ const id = validMatch[1];
848
+ const priority = validMatch[2];
849
+ const text = validMatch[3];
850
+ if (!id || !priority || !text) continue;
851
+ brs.push({
852
+ id,
853
+ priority,
854
+ text: text.trim(),
855
+ line: lineNumber
856
+ });
857
+ continue;
858
+ }
859
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
860
+ if (anyPriorityMatch) {
861
+ const id = anyPriorityMatch[1];
862
+ const priority = anyPriorityMatch[2];
863
+ const text = anyPriorityMatch[3];
864
+ if (!id || !priority || !text) continue;
865
+ if (!VALID_PRIORITIES.has(priority)) {
866
+ brsWithInvalidPriority.push({
867
+ id,
868
+ priority,
869
+ text: text.trim(),
870
+ line: lineNumber
871
+ });
872
+ }
873
+ continue;
874
+ }
875
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
876
+ if (noPriorityMatch) {
877
+ const id = noPriorityMatch[1];
878
+ const text = noPriorityMatch[2];
879
+ if (!id || !text) continue;
880
+ brsWithoutPriority.push({
881
+ id,
882
+ text: text.trim(),
883
+ line: lineNumber
884
+ });
885
+ }
886
+ }
887
+ const parsed = {
888
+ file,
889
+ sections: sectionNames,
890
+ brs,
891
+ brsWithoutPriority,
892
+ brsWithInvalidPriority,
893
+ contractRefs: parseContractRefs(md)
894
+ };
895
+ if (specId) {
896
+ parsed.specId = specId;
897
+ }
898
+ return parsed;
899
+ }
900
+ function parseContractRefs(md) {
901
+ const lines = [];
902
+ for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
903
+ lines.push((match[1] ?? "").trim());
904
+ }
905
+ const ids = [];
906
+ const invalidTokens = [];
907
+ let hasNone = false;
908
+ for (const line of lines) {
909
+ if (line.length === 0) {
910
+ invalidTokens.push("(empty)");
911
+ continue;
912
+ }
913
+ const tokens = line.split(",").map((token) => token.trim());
914
+ for (const token of tokens) {
915
+ if (token.length === 0) {
916
+ invalidTokens.push("(empty)");
917
+ continue;
918
+ }
919
+ if (token === "none") {
920
+ hasNone = true;
921
+ continue;
922
+ }
923
+ if (CONTRACT_REF_ID_RE.test(token)) {
924
+ ids.push(token);
925
+ continue;
926
+ }
927
+ invalidTokens.push(token);
928
+ }
929
+ }
930
+ return {
931
+ lines,
932
+ ids: unique2(ids),
933
+ invalidTokens: unique2(invalidTokens),
934
+ hasNone
935
+ };
936
+ }
937
+ function unique2(values) {
938
+ return Array.from(new Set(values));
939
+ }
940
+
733
941
  // src/core/traceability.ts
734
- import { readFile as readFile2 } from "fs/promises";
735
- import path7 from "path";
942
+ import { readFile as readFile3 } from "fs/promises";
943
+ import path8 from "path";
736
944
 
737
945
  // src/core/gherkin/parse.ts
738
946
  import {
@@ -769,7 +977,7 @@ var SC_TAG_RE = /^SC-\d{4}$/;
769
977
  var BR_TAG_RE = /^BR-\d{4}$/;
770
978
  var UI_TAG_RE = /^UI-\d{4}$/;
771
979
  var API_TAG_RE = /^API-\d{4}$/;
772
- var DATA_TAG_RE = /^DATA-\d{4}$/;
980
+ var DB_TAG_RE = /^DB-\d{4}$/;
773
981
  function parseScenarioDocument(text, uri) {
774
982
  const { gherkinDocument, errors } = parseGherkin(text, uri);
775
983
  if (!gherkinDocument) {
@@ -798,10 +1006,10 @@ function buildScenarioAtoms(document) {
798
1006
  return document.scenarios.map((scenario) => {
799
1007
  const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
800
1008
  const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
801
- const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1009
+ const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
802
1010
  const contractIds = /* @__PURE__ */ new Set();
803
1011
  scenario.tags.forEach((tag) => {
804
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1012
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
805
1013
  contractIds.add(tag);
806
1014
  }
807
1015
  });
@@ -809,7 +1017,7 @@ function buildScenarioAtoms(document) {
809
1017
  for (const text of collectStepTexts(step)) {
810
1018
  extractIds(text, "UI").forEach((id) => contractIds.add(id));
811
1019
  extractIds(text, "API").forEach((id) => contractIds.add(id));
812
- extractIds(text, "DATA").forEach((id) => contractIds.add(id));
1020
+ extractIds(text, "DB").forEach((id) => contractIds.add(id));
813
1021
  }
814
1022
  }
815
1023
  const atom = {
@@ -888,7 +1096,7 @@ function collectStepTexts(step) {
888
1096
  }
889
1097
  return texts;
890
1098
  }
891
- function unique2(values) {
1099
+ function unique3(values) {
892
1100
  return Array.from(new Set(values));
893
1101
  }
894
1102
 
@@ -918,7 +1126,7 @@ function extractAnnotatedScIds(text) {
918
1126
  async function collectScIdsFromScenarioFiles(scenarioFiles) {
919
1127
  const scIds = /* @__PURE__ */ new Set();
920
1128
  for (const file of scenarioFiles) {
921
- const text = await readFile2(file, "utf-8");
1129
+ const text = await readFile3(file, "utf-8");
922
1130
  const { document, errors } = parseScenarioDocument(text, file);
923
1131
  if (!document || errors.length > 0) {
924
1132
  continue;
@@ -936,7 +1144,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
936
1144
  async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
937
1145
  const sources = /* @__PURE__ */ new Map();
938
1146
  for (const file of scenarioFiles) {
939
- const text = await readFile2(file, "utf-8");
1147
+ const text = await readFile3(file, "utf-8");
940
1148
  const { document, errors } = parseScenarioDocument(text, file);
941
1149
  if (!document || errors.length > 0) {
942
1150
  continue;
@@ -989,10 +1197,10 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
989
1197
  };
990
1198
  }
991
1199
  const normalizedFiles = Array.from(
992
- new Set(files.map((file) => path7.normalize(file)))
1200
+ new Set(files.map((file) => path8.normalize(file)))
993
1201
  );
994
1202
  for (const file of normalizedFiles) {
995
- const text = await readFile2(file, "utf-8");
1203
+ const text = await readFile3(file, "utf-8");
996
1204
  const scIds = extractAnnotatedScIds(text);
997
1205
  if (scIds.length === 0) {
998
1206
  continue;
@@ -1049,16 +1257,16 @@ function formatError3(error2) {
1049
1257
  }
1050
1258
 
1051
1259
  // src/core/version.ts
1052
- import { readFile as readFile3 } from "fs/promises";
1053
- import path8 from "path";
1260
+ import { readFile as readFile4 } from "fs/promises";
1261
+ import path9 from "path";
1054
1262
  import { fileURLToPath as fileURLToPath2 } from "url";
1055
1263
  async function resolveToolVersion() {
1056
- if ("0.4.2".length > 0) {
1057
- return "0.4.2";
1264
+ if ("0.4.6".length > 0) {
1265
+ return "0.4.6";
1058
1266
  }
1059
1267
  try {
1060
1268
  const packagePath = resolvePackageJsonPath();
1061
- const raw = await readFile3(packagePath, "utf-8");
1269
+ const raw = await readFile4(packagePath, "utf-8");
1062
1270
  const parsed = JSON.parse(raw);
1063
1271
  const version = typeof parsed.version === "string" ? parsed.version : "";
1064
1272
  return version.length > 0 ? version : "unknown";
@@ -1069,54 +1277,23 @@ async function resolveToolVersion() {
1069
1277
  function resolvePackageJsonPath() {
1070
1278
  const base = import.meta.url;
1071
1279
  const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
1072
- return path8.resolve(path8.dirname(basePath), "../../package.json");
1280
+ return path9.resolve(path9.dirname(basePath), "../../package.json");
1073
1281
  }
1074
1282
 
1075
1283
  // src/core/validators/contracts.ts
1076
- import { readFile as readFile4 } from "fs/promises";
1077
- import path10 from "path";
1284
+ import { readFile as readFile5 } from "fs/promises";
1285
+ import path11 from "path";
1078
1286
 
1079
1287
  // src/core/contracts.ts
1080
- import path9 from "path";
1288
+ import path10 from "path";
1081
1289
  import { parse as parseYaml2 } from "yaml";
1082
1290
  function parseStructuredContract(file, text) {
1083
- const ext = path9.extname(file).toLowerCase();
1291
+ const ext = path10.extname(file).toLowerCase();
1084
1292
  if (ext === ".json") {
1085
1293
  return JSON.parse(text);
1086
1294
  }
1087
1295
  return parseYaml2(text);
1088
1296
  }
1089
- function extractUiContractIds(doc) {
1090
- const id = typeof doc.id === "string" ? doc.id : "";
1091
- return extractIds(id, "UI");
1092
- }
1093
- function extractApiContractIds(doc) {
1094
- const operationIds = /* @__PURE__ */ new Set();
1095
- collectOperationIds(doc, operationIds);
1096
- const ids = /* @__PURE__ */ new Set();
1097
- for (const operationId of operationIds) {
1098
- extractIds(operationId, "API").forEach((id) => ids.add(id));
1099
- }
1100
- return Array.from(ids);
1101
- }
1102
- function collectOperationIds(value, out) {
1103
- if (!value || typeof value !== "object") {
1104
- return;
1105
- }
1106
- if (Array.isArray(value)) {
1107
- for (const item of value) {
1108
- collectOperationIds(item, out);
1109
- }
1110
- return;
1111
- }
1112
- for (const [key, entry] of Object.entries(value)) {
1113
- if (key === "operationId" && typeof entry === "string") {
1114
- out.add(entry);
1115
- continue;
1116
- }
1117
- collectOperationIds(entry, out);
1118
- }
1119
- }
1120
1297
 
1121
1298
  // src/core/validators/contracts.ts
1122
1299
  var SQL_DANGEROUS_PATTERNS = [
@@ -1131,9 +1308,11 @@ var SQL_DANGEROUS_PATTERNS = [
1131
1308
  async function validateContracts(root, config) {
1132
1309
  const issues = [];
1133
1310
  const contractsRoot = resolvePath(root, config, "contractsDir");
1134
- issues.push(...await validateUiContracts(path10.join(contractsRoot, "ui")));
1135
- issues.push(...await validateApiContracts(path10.join(contractsRoot, "api")));
1136
- issues.push(...await validateDataContracts(path10.join(contractsRoot, "db")));
1311
+ issues.push(...await validateUiContracts(path11.join(contractsRoot, "ui")));
1312
+ issues.push(...await validateApiContracts(path11.join(contractsRoot, "api")));
1313
+ issues.push(...await validateDbContracts(path11.join(contractsRoot, "db")));
1314
+ const contractIndex = await buildContractIndex(root, config);
1315
+ issues.push(...validateDuplicateContractIds(contractIndex));
1137
1316
  return issues;
1138
1317
  }
1139
1318
  async function validateUiContracts(uiRoot) {
@@ -1151,14 +1330,14 @@ async function validateUiContracts(uiRoot) {
1151
1330
  }
1152
1331
  const issues = [];
1153
1332
  for (const file of files) {
1154
- const text = await readFile4(file, "utf-8");
1333
+ const text = await readFile5(file, "utf-8");
1155
1334
  const invalidIds = extractInvalidIds(text, [
1156
1335
  "SPEC",
1157
1336
  "BR",
1158
1337
  "SC",
1159
1338
  "UI",
1160
1339
  "API",
1161
- "DATA",
1340
+ "DB",
1162
1341
  "ADR"
1163
1342
  ]);
1164
1343
  if (invalidIds.length > 0) {
@@ -1173,9 +1352,10 @@ async function validateUiContracts(uiRoot) {
1173
1352
  )
1174
1353
  );
1175
1354
  }
1176
- let doc;
1355
+ const declaredIds = extractDeclaredContractIds(text);
1356
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
1177
1357
  try {
1178
- doc = parseStructuredContract(file, text);
1358
+ parseStructuredContract(file, stripContractDeclarationLines(text));
1179
1359
  } catch (error2) {
1180
1360
  issues.push(
1181
1361
  issue(
@@ -1186,19 +1366,6 @@ async function validateUiContracts(uiRoot) {
1186
1366
  "contracts.ui.parse"
1187
1367
  )
1188
1368
  );
1189
- continue;
1190
- }
1191
- const uiIds = extractUiContractIds(doc);
1192
- if (uiIds.length === 0) {
1193
- issues.push(
1194
- issue(
1195
- "QFAI-CONTRACT-002",
1196
- `UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
1197
- "error",
1198
- file,
1199
- "contracts.ui.id"
1200
- )
1201
- );
1202
1369
  }
1203
1370
  }
1204
1371
  return issues;
@@ -1218,14 +1385,14 @@ async function validateApiContracts(apiRoot) {
1218
1385
  }
1219
1386
  const issues = [];
1220
1387
  for (const file of files) {
1221
- const text = await readFile4(file, "utf-8");
1388
+ const text = await readFile5(file, "utf-8");
1222
1389
  const invalidIds = extractInvalidIds(text, [
1223
1390
  "SPEC",
1224
1391
  "BR",
1225
1392
  "SC",
1226
1393
  "UI",
1227
1394
  "API",
1228
- "DATA",
1395
+ "DB",
1229
1396
  "ADR"
1230
1397
  ]);
1231
1398
  if (invalidIds.length > 0) {
@@ -1240,9 +1407,11 @@ async function validateApiContracts(apiRoot) {
1240
1407
  )
1241
1408
  );
1242
1409
  }
1410
+ const declaredIds = extractDeclaredContractIds(text);
1411
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "API"));
1243
1412
  let doc;
1244
1413
  try {
1245
- doc = parseStructuredContract(file, text);
1414
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
1246
1415
  } catch (error2) {
1247
1416
  issues.push(
1248
1417
  issue(
@@ -1266,44 +1435,32 @@ async function validateApiContracts(apiRoot) {
1266
1435
  )
1267
1436
  );
1268
1437
  }
1269
- const apiIds = extractApiContractIds(doc);
1270
- if (apiIds.length === 0) {
1271
- issues.push(
1272
- issue(
1273
- "QFAI-CONTRACT-002",
1274
- `API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
1275
- "error",
1276
- file,
1277
- "contracts.api.id"
1278
- )
1279
- );
1280
- }
1281
1438
  }
1282
1439
  return issues;
1283
1440
  }
1284
- async function validateDataContracts(dataRoot) {
1285
- const files = await collectDataContractFiles(dataRoot);
1441
+ async function validateDbContracts(dbRoot) {
1442
+ const files = await collectDbContractFiles(dbRoot);
1286
1443
  if (files.length === 0) {
1287
1444
  return [
1288
1445
  issue(
1289
- "QFAI-DATA-000",
1290
- "DATA \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1446
+ "QFAI-DB-000",
1447
+ "DB \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1291
1448
  "info",
1292
- dataRoot,
1293
- "contracts.data.files"
1449
+ dbRoot,
1450
+ "contracts.db.files"
1294
1451
  )
1295
1452
  ];
1296
1453
  }
1297
1454
  const issues = [];
1298
1455
  for (const file of files) {
1299
- const text = await readFile4(file, "utf-8");
1456
+ const text = await readFile5(file, "utf-8");
1300
1457
  const invalidIds = extractInvalidIds(text, [
1301
1458
  "SPEC",
1302
1459
  "BR",
1303
1460
  "SC",
1304
1461
  "UI",
1305
1462
  "API",
1306
- "DATA",
1463
+ "DB",
1307
1464
  "ADR"
1308
1465
  ]);
1309
1466
  if (invalidIds.length > 0) {
@@ -1318,6 +1475,8 @@ async function validateDataContracts(dataRoot) {
1318
1475
  )
1319
1476
  );
1320
1477
  }
1478
+ const declaredIds = extractDeclaredContractIds(text);
1479
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "DB"));
1321
1480
  issues.push(...lintSql(text, file));
1322
1481
  }
1323
1482
  return issues;
@@ -1328,17 +1487,83 @@ function lintSql(text, file) {
1328
1487
  if (pattern.test(text)) {
1329
1488
  issues.push(
1330
1489
  issue(
1331
- "QFAI-DATA-001",
1490
+ "QFAI-DB-001",
1332
1491
  `\u5371\u967A\u306A SQL \u64CD\u4F5C\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059: ${label}`,
1333
1492
  "warning",
1334
1493
  file,
1335
- "contracts.data.sql"
1494
+ "contracts.db.sql"
1336
1495
  )
1337
1496
  );
1338
1497
  }
1339
1498
  }
1340
1499
  return issues;
1341
1500
  }
1501
+ function validateDeclaredContractIds(ids, file, kind) {
1502
+ const issues = [];
1503
+ if (ids.length === 0) {
1504
+ issues.push(
1505
+ issue(
1506
+ "QFAI-CONTRACT-010",
1507
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
1508
+ "error",
1509
+ file,
1510
+ "contracts.declaration"
1511
+ )
1512
+ );
1513
+ return issues;
1514
+ }
1515
+ if (ids.length > 1) {
1516
+ issues.push(
1517
+ issue(
1518
+ "QFAI-CONTRACT-011",
1519
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B\u8907\u6570\u306E QFAI-CONTRACT-ID \u304C\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${ids.join(
1520
+ ", "
1521
+ )}`,
1522
+ "error",
1523
+ file,
1524
+ "contracts.declaration",
1525
+ ids
1526
+ )
1527
+ );
1528
+ return issues;
1529
+ }
1530
+ const [id] = ids;
1531
+ if (id && !id.startsWith(`${kind}-`)) {
1532
+ issues.push(
1533
+ issue(
1534
+ "QFAI-CONTRACT-013",
1535
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E QFAI-CONTRACT-ID \u304C ${kind}- \u3067\u306F\u3042\u308A\u307E\u305B\u3093: ${id}`,
1536
+ "error",
1537
+ file,
1538
+ "contracts.declarationPrefix",
1539
+ [id]
1540
+ )
1541
+ );
1542
+ }
1543
+ return issues;
1544
+ }
1545
+ function validateDuplicateContractIds(contractIndex) {
1546
+ const issues = [];
1547
+ for (const [id, files] of contractIndex.idToFiles.entries()) {
1548
+ if (files.size <= 1) {
1549
+ continue;
1550
+ }
1551
+ const sortedFiles = Array.from(files).sort((a, b) => a.localeCompare(b));
1552
+ issues.push(
1553
+ issue(
1554
+ "QFAI-CONTRACT-012",
1555
+ `\u5951\u7D04 ID \u304C\u8907\u6570\u30D5\u30A1\u30A4\u30EB\u3067\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${id} (${sortedFiles.join(
1556
+ ", "
1557
+ )})`,
1558
+ "error",
1559
+ sortedFiles[0],
1560
+ "contracts.idDuplicate",
1561
+ [id]
1562
+ )
1563
+ );
1564
+ }
1565
+ return issues;
1566
+ }
1342
1567
  function hasOpenApi(doc) {
1343
1568
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
1344
1569
  }
@@ -1367,8 +1592,8 @@ function issue(code, message, severity, file, rule, refs) {
1367
1592
  }
1368
1593
 
1369
1594
  // src/core/validators/delta.ts
1370
- import { readFile as readFile5 } from "fs/promises";
1371
- import path11 from "path";
1595
+ import { readFile as readFile6 } from "fs/promises";
1596
+ import path12 from "path";
1372
1597
  var SECTION_RE = /^##\s+変更区分/m;
1373
1598
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1374
1599
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1382,10 +1607,10 @@ async function validateDeltas(root, config) {
1382
1607
  }
1383
1608
  const issues = [];
1384
1609
  for (const pack of packs) {
1385
- const deltaPath = path11.join(pack, "delta.md");
1610
+ const deltaPath = path12.join(pack, "delta.md");
1386
1611
  let text;
1387
1612
  try {
1388
- text = await readFile5(deltaPath, "utf-8");
1613
+ text = await readFile6(deltaPath, "utf-8");
1389
1614
  } catch (error2) {
1390
1615
  if (isMissingFileError2(error2)) {
1391
1616
  issues.push(
@@ -1459,187 +1684,6 @@ function issue2(code, message, severity, file, rule, refs) {
1459
1684
  // src/core/validators/ids.ts
1460
1685
  import { readFile as readFile7 } from "fs/promises";
1461
1686
  import path13 from "path";
1462
-
1463
- // src/core/contractIndex.ts
1464
- import { readFile as readFile6 } from "fs/promises";
1465
- import path12 from "path";
1466
- async function buildContractIndex(root, config) {
1467
- const contractsRoot = resolvePath(root, config, "contractsDir");
1468
- const uiRoot = path12.join(contractsRoot, "ui");
1469
- const apiRoot = path12.join(contractsRoot, "api");
1470
- const dataRoot = path12.join(contractsRoot, "db");
1471
- const [uiFiles, apiFiles, dataFiles] = await Promise.all([
1472
- collectUiContractFiles(uiRoot),
1473
- collectApiContractFiles(apiRoot),
1474
- collectDataContractFiles(dataRoot)
1475
- ]);
1476
- const index = {
1477
- ids: /* @__PURE__ */ new Set(),
1478
- idToFiles: /* @__PURE__ */ new Map(),
1479
- files: { ui: uiFiles, api: apiFiles, data: dataFiles },
1480
- structuredParseFailedFiles: /* @__PURE__ */ new Set()
1481
- };
1482
- await indexUiContracts(uiFiles, index);
1483
- await indexApiContracts(apiFiles, index);
1484
- await indexDataContracts(dataFiles, index);
1485
- return index;
1486
- }
1487
- async function indexUiContracts(files, index) {
1488
- for (const file of files) {
1489
- const text = await readFile6(file, "utf-8");
1490
- try {
1491
- const doc = parseStructuredContract(file, text);
1492
- extractUiContractIds(doc).forEach((id) => record(index, id, file));
1493
- } catch {
1494
- index.structuredParseFailedFiles.add(file);
1495
- extractIds(text, "UI").forEach((id) => record(index, id, file));
1496
- }
1497
- }
1498
- }
1499
- async function indexApiContracts(files, index) {
1500
- for (const file of files) {
1501
- const text = await readFile6(file, "utf-8");
1502
- try {
1503
- const doc = parseStructuredContract(file, text);
1504
- extractApiContractIds(doc).forEach((id) => record(index, id, file));
1505
- } catch {
1506
- index.structuredParseFailedFiles.add(file);
1507
- extractIds(text, "API").forEach((id) => record(index, id, file));
1508
- }
1509
- }
1510
- }
1511
- async function indexDataContracts(files, index) {
1512
- for (const file of files) {
1513
- const text = await readFile6(file, "utf-8");
1514
- extractIds(text, "DATA").forEach((id) => record(index, id, file));
1515
- }
1516
- }
1517
- function record(index, id, file) {
1518
- index.ids.add(id);
1519
- const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
1520
- current.add(file);
1521
- index.idToFiles.set(id, current);
1522
- }
1523
-
1524
- // src/core/parse/markdown.ts
1525
- var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1526
- function parseHeadings(md) {
1527
- const lines = md.split(/\r?\n/);
1528
- const headings = [];
1529
- for (let i = 0; i < lines.length; i++) {
1530
- const line = lines[i] ?? "";
1531
- const match = line.match(HEADING_RE);
1532
- if (!match) continue;
1533
- const levelToken = match[1];
1534
- const title = match[2];
1535
- if (!levelToken || !title) continue;
1536
- headings.push({
1537
- level: levelToken.length,
1538
- title: title.trim(),
1539
- line: i + 1
1540
- });
1541
- }
1542
- return headings;
1543
- }
1544
- function extractH2Sections(md) {
1545
- const lines = md.split(/\r?\n/);
1546
- const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1547
- const sections = /* @__PURE__ */ new Map();
1548
- for (let i = 0; i < headings.length; i++) {
1549
- const current = headings[i];
1550
- if (!current) continue;
1551
- const next = headings[i + 1];
1552
- const startLine = current.line + 1;
1553
- const endLine = (next?.line ?? lines.length + 1) - 1;
1554
- const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1555
- sections.set(current.title.trim(), {
1556
- title: current.title.trim(),
1557
- startLine,
1558
- endLine,
1559
- body
1560
- });
1561
- }
1562
- return sections;
1563
- }
1564
-
1565
- // src/core/parse/spec.ts
1566
- var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1567
- var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1568
- var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1569
- var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1570
- var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1571
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1572
- function parseSpec(md, file) {
1573
- const headings = parseHeadings(md);
1574
- const h1 = headings.find((heading) => heading.level === 1);
1575
- const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1576
- const sections = extractH2Sections(md);
1577
- const sectionNames = new Set(Array.from(sections.keys()));
1578
- const brSection = sections.get(BR_SECTION_TITLE);
1579
- const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1580
- const startLine = brSection?.startLine ?? 1;
1581
- const brs = [];
1582
- const brsWithoutPriority = [];
1583
- const brsWithInvalidPriority = [];
1584
- for (let i = 0; i < brLines.length; i++) {
1585
- const lineText = brLines[i] ?? "";
1586
- const lineNumber = startLine + i;
1587
- const validMatch = lineText.match(BR_LINE_RE);
1588
- if (validMatch) {
1589
- const id = validMatch[1];
1590
- const priority = validMatch[2];
1591
- const text = validMatch[3];
1592
- if (!id || !priority || !text) continue;
1593
- brs.push({
1594
- id,
1595
- priority,
1596
- text: text.trim(),
1597
- line: lineNumber
1598
- });
1599
- continue;
1600
- }
1601
- const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1602
- if (anyPriorityMatch) {
1603
- const id = anyPriorityMatch[1];
1604
- const priority = anyPriorityMatch[2];
1605
- const text = anyPriorityMatch[3];
1606
- if (!id || !priority || !text) continue;
1607
- if (!VALID_PRIORITIES.has(priority)) {
1608
- brsWithInvalidPriority.push({
1609
- id,
1610
- priority,
1611
- text: text.trim(),
1612
- line: lineNumber
1613
- });
1614
- }
1615
- continue;
1616
- }
1617
- const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1618
- if (noPriorityMatch) {
1619
- const id = noPriorityMatch[1];
1620
- const text = noPriorityMatch[2];
1621
- if (!id || !text) continue;
1622
- brsWithoutPriority.push({
1623
- id,
1624
- text: text.trim(),
1625
- line: lineNumber
1626
- });
1627
- }
1628
- }
1629
- const parsed = {
1630
- file,
1631
- sections: sectionNames,
1632
- brs,
1633
- brsWithoutPriority,
1634
- brsWithInvalidPriority
1635
- };
1636
- if (specId) {
1637
- parsed.specId = specId;
1638
- }
1639
- return parsed;
1640
- }
1641
-
1642
- // src/core/validators/ids.ts
1643
1687
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1644
1688
  async function validateDefinedIds(root, config) {
1645
1689
  const issues = [];
@@ -1782,7 +1826,7 @@ function validateScenarioContent(text, file) {
1782
1826
  "SC",
1783
1827
  "UI",
1784
1828
  "API",
1785
- "DATA",
1829
+ "DB",
1786
1830
  "ADR"
1787
1831
  ]);
1788
1832
  if (invalidIds.length > 0) {
@@ -1980,7 +2024,7 @@ function validateSpecContent(text, file, requiredSections) {
1980
2024
  "SC",
1981
2025
  "UI",
1982
2026
  "API",
1983
- "DATA",
2027
+ "DB",
1984
2028
  "ADR"
1985
2029
  ]);
1986
2030
  if (invalidIds.length > 0) {
@@ -2109,8 +2153,7 @@ async function validateTraceability(root, config) {
2109
2153
  const brIdsInSpecs = /* @__PURE__ */ new Set();
2110
2154
  const brIdsInScenarios = /* @__PURE__ */ new Set();
2111
2155
  const scIdsInScenarios = /* @__PURE__ */ new Set();
2112
- const scenarioContractIds = /* @__PURE__ */ new Set();
2113
- const scWithContracts = /* @__PURE__ */ new Set();
2156
+ const specContractIds = /* @__PURE__ */ new Set();
2114
2157
  const specToBrIds = /* @__PURE__ */ new Map();
2115
2158
  const contractIndex = await buildContractIndex(root, config);
2116
2159
  const contractIds = contractIndex.ids;
@@ -2128,6 +2171,64 @@ async function validateTraceability(root, config) {
2128
2171
  brIds.forEach((id) => current.add(id));
2129
2172
  specToBrIds.set(parsed.specId, current);
2130
2173
  }
2174
+ const contractRefs = parsed.contractRefs;
2175
+ if (contractRefs.lines.length === 0) {
2176
+ issues.push(
2177
+ issue6(
2178
+ "QFAI-TRACE-020",
2179
+ "Spec \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
2180
+ "error",
2181
+ file,
2182
+ "traceability.specContractRefRequired"
2183
+ )
2184
+ );
2185
+ } else {
2186
+ if (contractRefs.hasNone && contractRefs.ids.length > 0) {
2187
+ issues.push(
2188
+ issue6(
2189
+ "QFAI-TRACE-021",
2190
+ "Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2191
+ "error",
2192
+ file,
2193
+ "traceability.specContractRefFormat"
2194
+ )
2195
+ );
2196
+ }
2197
+ if (contractRefs.invalidTokens.length > 0) {
2198
+ issues.push(
2199
+ issue6(
2200
+ "QFAI-TRACE-021",
2201
+ `Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
2202
+ ", "
2203
+ )}`,
2204
+ "error",
2205
+ file,
2206
+ "traceability.specContractRefFormat",
2207
+ contractRefs.invalidTokens
2208
+ )
2209
+ );
2210
+ }
2211
+ }
2212
+ contractRefs.ids.forEach((id) => {
2213
+ specContractIds.add(id);
2214
+ });
2215
+ const unknownContractIds = contractRefs.ids.filter(
2216
+ (id) => !contractIds.has(id)
2217
+ );
2218
+ if (unknownContractIds.length > 0) {
2219
+ issues.push(
2220
+ issue6(
2221
+ "QFAI-TRACE-021",
2222
+ `Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
2223
+ ", "
2224
+ )}`,
2225
+ "error",
2226
+ file,
2227
+ "traceability.specContractExists",
2228
+ unknownContractIds
2229
+ )
2230
+ );
2231
+ }
2131
2232
  }
2132
2233
  for (const file of scenarioFiles) {
2133
2234
  const text = await readFile10(file, "utf-8");
@@ -2173,10 +2274,6 @@ async function validateTraceability(root, config) {
2173
2274
  scIdsInScenarios.add(id);
2174
2275
  scIdsInFile.add(id);
2175
2276
  });
2176
- atom.contractIds.forEach((id) => scenarioContractIds.add(id));
2177
- if (atom.contractIds.length > 0) {
2178
- scTags.forEach((id) => scWithContracts.add(id));
2179
- }
2180
2277
  const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
2181
2278
  if (unknownSpecIds.length > 0) {
2182
2279
  issues.push(
@@ -2295,25 +2392,6 @@ async function validateTraceability(root, config) {
2295
2392
  );
2296
2393
  }
2297
2394
  }
2298
- if (config.validation.traceability.scMustTouchContracts && scIdsInScenarios.size > 0) {
2299
- const scWithoutContracts = Array.from(scIdsInScenarios).filter(
2300
- (id) => !scWithContracts.has(id)
2301
- );
2302
- if (scWithoutContracts.length > 0) {
2303
- issues.push(
2304
- issue6(
2305
- "QFAI_TRACE_SC_NO_CONTRACT",
2306
- `SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
2307
- ", "
2308
- )}`,
2309
- "error",
2310
- specsRoot,
2311
- "traceability.scMustTouchContracts",
2312
- scWithoutContracts
2313
- )
2314
- );
2315
- }
2316
- }
2317
2395
  const scRefsResult = await collectScTestReferences(
2318
2396
  root,
2319
2397
  config.validation.traceability.testFileGlobs,
@@ -2377,16 +2455,16 @@ async function validateTraceability(root, config) {
2377
2455
  if (!config.validation.traceability.allowOrphanContracts) {
2378
2456
  if (contractIds.size > 0) {
2379
2457
  const orphanContracts = Array.from(contractIds).filter(
2380
- (id) => !scenarioContractIds.has(id)
2458
+ (id) => !specContractIds.has(id)
2381
2459
  );
2382
2460
  if (orphanContracts.length > 0) {
2383
2461
  issues.push(
2384
2462
  issue6(
2385
- "QFAI_CONTRACT_ORPHAN",
2386
- `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
2463
+ "QFAI-TRACE-022",
2464
+ `\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
2387
2465
  "error",
2388
2466
  specsRoot,
2389
- "traceability.allowOrphanContracts",
2467
+ "traceability.contractCoverage",
2390
2468
  orphanContracts
2391
2469
  )
2392
2470
  );
@@ -2507,7 +2585,7 @@ function countIssues(issues) {
2507
2585
  }
2508
2586
 
2509
2587
  // src/core/report.ts
2510
- var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
2588
+ var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
2511
2589
  async function createReportData(root, validation, configResult) {
2512
2590
  const resolved = configResult ?? await loadConfig(root);
2513
2591
  const config = resolved.config;
@@ -2526,6 +2604,23 @@ async function createReportData(root, validation, configResult) {
2526
2604
  ui: uiFiles,
2527
2605
  db: dbFiles
2528
2606
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
2607
+ const contractIndex = await buildContractIndex(root, config);
2608
+ const specContractRefs = await collectSpecContractRefs(specFiles);
2609
+ const contractIdList = Array.from(contractIndex.ids);
2610
+ const referencedContracts = /* @__PURE__ */ new Set();
2611
+ for (const ids of specContractRefs.specToContractIds.values()) {
2612
+ ids.forEach((id) => referencedContracts.add(id));
2613
+ }
2614
+ const referencedContractCount = contractIdList.filter(
2615
+ (id) => referencedContracts.has(id)
2616
+ ).length;
2617
+ const orphanContractCount = contractIdList.filter(
2618
+ (id) => !referencedContracts.has(id)
2619
+ ).length;
2620
+ const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
2621
+ const specToContractIdsRecord = mapToSortedRecord(
2622
+ specContractRefs.specToContractIds
2623
+ );
2529
2624
  const idsByPrefix = await collectIds([
2530
2625
  ...specFiles,
2531
2626
  ...scenarioFiles,
@@ -2576,14 +2671,24 @@ async function createReportData(root, validation, configResult) {
2576
2671
  sc: idsByPrefix.SC,
2577
2672
  ui: idsByPrefix.UI,
2578
2673
  api: idsByPrefix.API,
2579
- data: idsByPrefix.DATA
2674
+ db: idsByPrefix.DB
2580
2675
  },
2581
2676
  traceability: {
2582
2677
  upstreamIdsFound: upstreamIds.size,
2583
2678
  referencedInCodeOrTests: traceability,
2584
2679
  sc: scCoverage,
2585
2680
  scSources: scSourceRecord,
2586
- testFiles
2681
+ testFiles,
2682
+ contracts: {
2683
+ total: contractIdList.length,
2684
+ referenced: referencedContractCount,
2685
+ orphan: orphanContractCount,
2686
+ idToSpecs: contractIdToSpecsRecord
2687
+ },
2688
+ specs: {
2689
+ contractRefMissing: specContractRefs.missingRefSpecs.size,
2690
+ specToContractIds: specToContractIdsRecord
2691
+ }
2587
2692
  },
2588
2693
  issues: resolvedValidation.issues
2589
2694
  };
@@ -2612,7 +2717,7 @@ function formatReportMarkdown(data) {
2612
2717
  lines.push(formatIdLine("SC", data.ids.sc));
2613
2718
  lines.push(formatIdLine("UI", data.ids.ui));
2614
2719
  lines.push(formatIdLine("API", data.ids.api));
2615
- lines.push(formatIdLine("DATA", data.ids.data));
2720
+ lines.push(formatIdLine("DB", data.ids.db));
2616
2721
  lines.push("");
2617
2722
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
2618
2723
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
@@ -2620,6 +2725,50 @@ function formatReportMarkdown(data) {
2620
2725
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2621
2726
  );
2622
2727
  lines.push("");
2728
+ lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
2729
+ lines.push(`- total: ${data.traceability.contracts.total}`);
2730
+ lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
2731
+ lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
2732
+ lines.push(
2733
+ `- specContractRefMissing: ${data.traceability.specs.contractRefMissing}`
2734
+ );
2735
+ lines.push("");
2736
+ lines.push("## \u5951\u7D04\u2192Spec");
2737
+ const contractToSpecs = data.traceability.contracts.idToSpecs;
2738
+ const contractIds = Object.keys(contractToSpecs).sort(
2739
+ (a, b) => a.localeCompare(b)
2740
+ );
2741
+ if (contractIds.length === 0) {
2742
+ lines.push("- (none)");
2743
+ } else {
2744
+ for (const contractId of contractIds) {
2745
+ const specs = contractToSpecs[contractId] ?? [];
2746
+ if (specs.length === 0) {
2747
+ lines.push(`- ${contractId}: (none)`);
2748
+ } else {
2749
+ lines.push(`- ${contractId}: ${specs.join(", ")}`);
2750
+ }
2751
+ }
2752
+ }
2753
+ lines.push("");
2754
+ lines.push("## Spec\u2192\u5951\u7D04");
2755
+ const specToContracts = data.traceability.specs.specToContractIds;
2756
+ const specIds = Object.keys(specToContracts).sort(
2757
+ (a, b) => a.localeCompare(b)
2758
+ );
2759
+ if (specIds.length === 0) {
2760
+ lines.push("- (none)");
2761
+ } else {
2762
+ for (const specId of specIds) {
2763
+ const contractIds2 = specToContracts[specId] ?? [];
2764
+ if (contractIds2.length === 0) {
2765
+ lines.push(`- ${specId}: (none)`);
2766
+ } else {
2767
+ lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
2768
+ }
2769
+ }
2770
+ }
2771
+ lines.push("");
2623
2772
  lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2624
2773
  lines.push(`- total: ${data.traceability.sc.total}`);
2625
2774
  lines.push(`- covered: ${data.traceability.sc.covered}`);
@@ -2693,7 +2842,7 @@ function formatReportMarkdown(data) {
2693
2842
  lines.push("");
2694
2843
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
2695
2844
  const traceIssues = data.issues.filter(
2696
- (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-") || item.code === "QFAI_CONTRACT_ORPHAN"
2845
+ (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
2697
2846
  );
2698
2847
  if (traceIssues.length === 0) {
2699
2848
  lines.push("- (none)");
@@ -2723,6 +2872,33 @@ function formatReportMarkdown(data) {
2723
2872
  function formatReportJson(data) {
2724
2873
  return JSON.stringify(data, null, 2);
2725
2874
  }
2875
+ async function collectSpecContractRefs(specFiles) {
2876
+ const specToContractIds = /* @__PURE__ */ new Map();
2877
+ const idToSpecs = /* @__PURE__ */ new Map();
2878
+ const missingRefSpecs = /* @__PURE__ */ new Set();
2879
+ for (const file of specFiles) {
2880
+ const text = await readFile11(file, "utf-8");
2881
+ const parsed = parseSpec(text, file);
2882
+ const specKey = parsed.specId ?? file;
2883
+ const refs = parsed.contractRefs;
2884
+ if (refs.lines.length === 0) {
2885
+ missingRefSpecs.add(specKey);
2886
+ }
2887
+ const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
2888
+ for (const id of refs.ids) {
2889
+ currentContracts.add(id);
2890
+ const specs = idToSpecs.get(id) ?? /* @__PURE__ */ new Set();
2891
+ specs.add(specKey);
2892
+ idToSpecs.set(id, specs);
2893
+ }
2894
+ specToContractIds.set(specKey, currentContracts);
2895
+ }
2896
+ return {
2897
+ specToContractIds,
2898
+ idToSpecs,
2899
+ missingRefSpecs
2900
+ };
2901
+ }
2726
2902
  async function collectIds(files) {
2727
2903
  const result = {
2728
2904
  SPEC: /* @__PURE__ */ new Set(),
@@ -2730,7 +2906,7 @@ async function collectIds(files) {
2730
2906
  SC: /* @__PURE__ */ new Set(),
2731
2907
  UI: /* @__PURE__ */ new Set(),
2732
2908
  API: /* @__PURE__ */ new Set(),
2733
- DATA: /* @__PURE__ */ new Set()
2909
+ DB: /* @__PURE__ */ new Set()
2734
2910
  };
2735
2911
  for (const file of files) {
2736
2912
  const text = await readFile11(file, "utf-8");
@@ -2745,7 +2921,7 @@ async function collectIds(files) {
2745
2921
  SC: toSortedArray2(result.SC),
2746
2922
  UI: toSortedArray2(result.UI),
2747
2923
  API: toSortedArray2(result.API),
2748
- DATA: toSortedArray2(result.DATA)
2924
+ DB: toSortedArray2(result.DB)
2749
2925
  };
2750
2926
  }
2751
2927
  async function collectUpstreamIds(files) {
@@ -2891,7 +3067,31 @@ function isValidationResult(value) {
2891
3067
  if (!counts) {
2892
3068
  return false;
2893
3069
  }
2894
- return typeof counts.info === "number" && typeof counts.warning === "number" && typeof counts.error === "number";
3070
+ if (typeof counts.info !== "number" || typeof counts.warning !== "number" || typeof counts.error !== "number") {
3071
+ return false;
3072
+ }
3073
+ const traceability = record2.traceability;
3074
+ if (!traceability || typeof traceability !== "object") {
3075
+ return false;
3076
+ }
3077
+ const sc = traceability.sc;
3078
+ const testFiles = traceability.testFiles;
3079
+ if (!sc || !testFiles) {
3080
+ return false;
3081
+ }
3082
+ if (typeof sc.total !== "number" || typeof sc.covered !== "number" || typeof sc.missing !== "number") {
3083
+ return false;
3084
+ }
3085
+ if (!Array.isArray(sc.missingIds)) {
3086
+ return false;
3087
+ }
3088
+ if (!sc.refs || typeof sc.refs !== "object") {
3089
+ return false;
3090
+ }
3091
+ if (!Array.isArray(testFiles.globs) || !Array.isArray(testFiles.excludeGlobs) || typeof testFiles.matchedFileCount !== "number") {
3092
+ return false;
3093
+ }
3094
+ return true;
2895
3095
  }
2896
3096
  function isMissingFileError5(error2) {
2897
3097
  if (!error2 || typeof error2 !== "object") {