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
@@ -209,7 +209,6 @@ var defaultConfig = {
209
209
  },
210
210
  traceability: {
211
211
  brMustHaveSc: true,
212
- scMustTouchContracts: true,
213
212
  scMustHaveTest: true,
214
213
  testFileGlobs: [],
215
214
  testFileExcludeGlobs: [],
@@ -386,13 +385,6 @@ function normalizeValidation(raw, configPath, issues) {
386
385
  configPath,
387
386
  issues
388
387
  ),
389
- scMustTouchContracts: readBoolean(
390
- traceabilityRaw?.scMustTouchContracts,
391
- base.traceability.scMustTouchContracts,
392
- "validation.traceability.scMustTouchContracts",
393
- configPath,
394
- issues
395
- ),
396
388
  scMustHaveTest: readBoolean(
397
389
  traceabilityRaw?.scMustHaveTest,
398
390
  base.traceability.scMustHaveTest,
@@ -549,6 +541,10 @@ function isRecord(value) {
549
541
  var import_promises15 = require("fs/promises");
550
542
  var import_node_path14 = __toESM(require("path"), 1);
551
543
 
544
+ // src/core/contractIndex.ts
545
+ var import_promises6 = require("fs/promises");
546
+ var import_node_path7 = __toESM(require("path"), 1);
547
+
552
548
  // src/core/discovery.ts
553
549
  var import_promises5 = require("fs/promises");
554
550
 
@@ -671,14 +667,14 @@ async function collectUiContractFiles(uiRoot) {
671
667
  async function collectApiContractFiles(apiRoot) {
672
668
  return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
673
669
  }
674
- async function collectDataContractFiles(dataRoot) {
675
- return collectFiles(dataRoot, { extensions: [".sql"] });
670
+ async function collectDbContractFiles(dbRoot) {
671
+ return collectFiles(dbRoot, { extensions: [".sql"] });
676
672
  }
677
- async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
673
+ async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
678
674
  const [ui, api, db] = await Promise.all([
679
675
  collectUiContractFiles(uiRoot),
680
676
  collectApiContractFiles(apiRoot),
681
- collectDataContractFiles(dataRoot)
677
+ collectDbContractFiles(dbRoot)
682
678
  ]);
683
679
  return { ui, api, db };
684
680
  }
@@ -700,15 +696,66 @@ async function exists3(target) {
700
696
  }
701
697
  }
702
698
 
699
+ // src/core/contractsDecl.ts
700
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
701
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
702
+ function extractDeclaredContractIds(text) {
703
+ const ids = [];
704
+ for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
705
+ const id = match[1];
706
+ if (id) {
707
+ ids.push(id);
708
+ }
709
+ }
710
+ return ids;
711
+ }
712
+ function stripContractDeclarationLines(text) {
713
+ return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
714
+ }
715
+
716
+ // src/core/contractIndex.ts
717
+ async function buildContractIndex(root, config) {
718
+ const contractsRoot = resolvePath(root, config, "contractsDir");
719
+ const uiRoot = import_node_path7.default.join(contractsRoot, "ui");
720
+ const apiRoot = import_node_path7.default.join(contractsRoot, "api");
721
+ const dbRoot = import_node_path7.default.join(contractsRoot, "db");
722
+ const [uiFiles, apiFiles, dbFiles] = await Promise.all([
723
+ collectUiContractFiles(uiRoot),
724
+ collectApiContractFiles(apiRoot),
725
+ collectDbContractFiles(dbRoot)
726
+ ]);
727
+ const index = {
728
+ ids: /* @__PURE__ */ new Set(),
729
+ idToFiles: /* @__PURE__ */ new Map(),
730
+ files: { ui: uiFiles, api: apiFiles, db: dbFiles }
731
+ };
732
+ await indexContractFiles(uiFiles, index);
733
+ await indexContractFiles(apiFiles, index);
734
+ await indexContractFiles(dbFiles, index);
735
+ return index;
736
+ }
737
+ async function indexContractFiles(files, index) {
738
+ for (const file of files) {
739
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
740
+ extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
741
+ }
742
+ }
743
+ function record(index, id, file) {
744
+ index.ids.add(id);
745
+ const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
746
+ current.add(file);
747
+ index.idToFiles.set(id, current);
748
+ }
749
+
703
750
  // src/core/ids.ts
704
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
751
+ var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
705
752
  var STRICT_ID_PATTERNS = {
706
753
  SPEC: /\bSPEC-\d{4}\b/g,
707
754
  BR: /\bBR-\d{4}\b/g,
708
755
  SC: /\bSC-\d{4}\b/g,
709
756
  UI: /\bUI-\d{4}\b/g,
710
757
  API: /\bAPI-\d{4}\b/g,
711
- DATA: /\bDATA-\d{4}\b/g,
758
+ DB: /\bDB-\d{4}\b/g,
712
759
  ADR: /\bADR-\d{4}\b/g
713
760
  };
714
761
  var LOOSE_ID_PATTERNS = {
@@ -717,7 +764,7 @@ var LOOSE_ID_PATTERNS = {
717
764
  SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
718
765
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
719
766
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
720
- DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi,
767
+ DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
721
768
  ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
722
769
  };
723
770
  function extractIds(text, prefix) {
@@ -753,9 +800,170 @@ function isValidId(value, prefix) {
753
800
  return strict.test(value);
754
801
  }
755
802
 
803
+ // src/core/parse/markdown.ts
804
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
805
+ function parseHeadings(md) {
806
+ const lines = md.split(/\r?\n/);
807
+ const headings = [];
808
+ for (let i = 0; i < lines.length; i++) {
809
+ const line = lines[i] ?? "";
810
+ const match = line.match(HEADING_RE);
811
+ if (!match) continue;
812
+ const levelToken = match[1];
813
+ const title = match[2];
814
+ if (!levelToken || !title) continue;
815
+ headings.push({
816
+ level: levelToken.length,
817
+ title: title.trim(),
818
+ line: i + 1
819
+ });
820
+ }
821
+ return headings;
822
+ }
823
+ function extractH2Sections(md) {
824
+ const lines = md.split(/\r?\n/);
825
+ const headings = parseHeadings(md).filter((heading) => heading.level === 2);
826
+ const sections = /* @__PURE__ */ new Map();
827
+ for (let i = 0; i < headings.length; i++) {
828
+ const current = headings[i];
829
+ if (!current) continue;
830
+ const next = headings[i + 1];
831
+ const startLine = current.line + 1;
832
+ const endLine = (next?.line ?? lines.length + 1) - 1;
833
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
834
+ sections.set(current.title.trim(), {
835
+ title: current.title.trim(),
836
+ startLine,
837
+ endLine,
838
+ body
839
+ });
840
+ }
841
+ return sections;
842
+ }
843
+
844
+ // src/core/parse/spec.ts
845
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
846
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
847
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
848
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
849
+ var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
850
+ var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
851
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
852
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
853
+ function parseSpec(md, file) {
854
+ const headings = parseHeadings(md);
855
+ const h1 = headings.find((heading) => heading.level === 1);
856
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
857
+ const sections = extractH2Sections(md);
858
+ const sectionNames = new Set(Array.from(sections.keys()));
859
+ const brSection = sections.get(BR_SECTION_TITLE);
860
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
861
+ const startLine = brSection?.startLine ?? 1;
862
+ const brs = [];
863
+ const brsWithoutPriority = [];
864
+ const brsWithInvalidPriority = [];
865
+ for (let i = 0; i < brLines.length; i++) {
866
+ const lineText = brLines[i] ?? "";
867
+ const lineNumber = startLine + i;
868
+ const validMatch = lineText.match(BR_LINE_RE);
869
+ if (validMatch) {
870
+ const id = validMatch[1];
871
+ const priority = validMatch[2];
872
+ const text = validMatch[3];
873
+ if (!id || !priority || !text) continue;
874
+ brs.push({
875
+ id,
876
+ priority,
877
+ text: text.trim(),
878
+ line: lineNumber
879
+ });
880
+ continue;
881
+ }
882
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
883
+ if (anyPriorityMatch) {
884
+ const id = anyPriorityMatch[1];
885
+ const priority = anyPriorityMatch[2];
886
+ const text = anyPriorityMatch[3];
887
+ if (!id || !priority || !text) continue;
888
+ if (!VALID_PRIORITIES.has(priority)) {
889
+ brsWithInvalidPriority.push({
890
+ id,
891
+ priority,
892
+ text: text.trim(),
893
+ line: lineNumber
894
+ });
895
+ }
896
+ continue;
897
+ }
898
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
899
+ if (noPriorityMatch) {
900
+ const id = noPriorityMatch[1];
901
+ const text = noPriorityMatch[2];
902
+ if (!id || !text) continue;
903
+ brsWithoutPriority.push({
904
+ id,
905
+ text: text.trim(),
906
+ line: lineNumber
907
+ });
908
+ }
909
+ }
910
+ const parsed = {
911
+ file,
912
+ sections: sectionNames,
913
+ brs,
914
+ brsWithoutPriority,
915
+ brsWithInvalidPriority,
916
+ contractRefs: parseContractRefs(md)
917
+ };
918
+ if (specId) {
919
+ parsed.specId = specId;
920
+ }
921
+ return parsed;
922
+ }
923
+ function parseContractRefs(md) {
924
+ const lines = [];
925
+ for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
926
+ lines.push((match[1] ?? "").trim());
927
+ }
928
+ const ids = [];
929
+ const invalidTokens = [];
930
+ let hasNone = false;
931
+ for (const line of lines) {
932
+ if (line.length === 0) {
933
+ invalidTokens.push("(empty)");
934
+ continue;
935
+ }
936
+ const tokens = line.split(",").map((token) => token.trim());
937
+ for (const token of tokens) {
938
+ if (token.length === 0) {
939
+ invalidTokens.push("(empty)");
940
+ continue;
941
+ }
942
+ if (token === "none") {
943
+ hasNone = true;
944
+ continue;
945
+ }
946
+ if (CONTRACT_REF_ID_RE.test(token)) {
947
+ ids.push(token);
948
+ continue;
949
+ }
950
+ invalidTokens.push(token);
951
+ }
952
+ }
953
+ return {
954
+ lines,
955
+ ids: unique2(ids),
956
+ invalidTokens: unique2(invalidTokens),
957
+ hasNone
958
+ };
959
+ }
960
+ function unique2(values) {
961
+ return Array.from(new Set(values));
962
+ }
963
+
756
964
  // src/core/traceability.ts
757
- var import_promises6 = require("fs/promises");
758
- var import_node_path7 = __toESM(require("path"), 1);
965
+ var import_promises7 = require("fs/promises");
966
+ var import_node_path8 = __toESM(require("path"), 1);
759
967
 
760
968
  // src/core/gherkin/parse.ts
761
969
  var import_gherkin = require("@cucumber/gherkin");
@@ -788,7 +996,7 @@ var SC_TAG_RE = /^SC-\d{4}$/;
788
996
  var BR_TAG_RE = /^BR-\d{4}$/;
789
997
  var UI_TAG_RE = /^UI-\d{4}$/;
790
998
  var API_TAG_RE = /^API-\d{4}$/;
791
- var DATA_TAG_RE = /^DATA-\d{4}$/;
999
+ var DB_TAG_RE = /^DB-\d{4}$/;
792
1000
  function parseScenarioDocument(text, uri) {
793
1001
  const { gherkinDocument, errors } = parseGherkin(text, uri);
794
1002
  if (!gherkinDocument) {
@@ -817,10 +1025,10 @@ function buildScenarioAtoms(document) {
817
1025
  return document.scenarios.map((scenario) => {
818
1026
  const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
819
1027
  const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
820
- const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1028
+ const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
821
1029
  const contractIds = /* @__PURE__ */ new Set();
822
1030
  scenario.tags.forEach((tag) => {
823
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1031
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
824
1032
  contractIds.add(tag);
825
1033
  }
826
1034
  });
@@ -828,7 +1036,7 @@ function buildScenarioAtoms(document) {
828
1036
  for (const text of collectStepTexts(step)) {
829
1037
  extractIds(text, "UI").forEach((id) => contractIds.add(id));
830
1038
  extractIds(text, "API").forEach((id) => contractIds.add(id));
831
- extractIds(text, "DATA").forEach((id) => contractIds.add(id));
1039
+ extractIds(text, "DB").forEach((id) => contractIds.add(id));
832
1040
  }
833
1041
  }
834
1042
  const atom = {
@@ -907,7 +1115,7 @@ function collectStepTexts(step) {
907
1115
  }
908
1116
  return texts;
909
1117
  }
910
- function unique2(values) {
1118
+ function unique3(values) {
911
1119
  return Array.from(new Set(values));
912
1120
  }
913
1121
 
@@ -937,7 +1145,7 @@ function extractAnnotatedScIds(text) {
937
1145
  async function collectScIdsFromScenarioFiles(scenarioFiles) {
938
1146
  const scIds = /* @__PURE__ */ new Set();
939
1147
  for (const file of scenarioFiles) {
940
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1148
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
941
1149
  const { document, errors } = parseScenarioDocument(text, file);
942
1150
  if (!document || errors.length > 0) {
943
1151
  continue;
@@ -955,7 +1163,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
955
1163
  async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
956
1164
  const sources = /* @__PURE__ */ new Map();
957
1165
  for (const file of scenarioFiles) {
958
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1166
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
959
1167
  const { document, errors } = parseScenarioDocument(text, file);
960
1168
  if (!document || errors.length > 0) {
961
1169
  continue;
@@ -1008,10 +1216,10 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
1008
1216
  };
1009
1217
  }
1010
1218
  const normalizedFiles = Array.from(
1011
- new Set(files.map((file) => import_node_path7.default.normalize(file)))
1219
+ new Set(files.map((file) => import_node_path8.default.normalize(file)))
1012
1220
  );
1013
1221
  for (const file of normalizedFiles) {
1014
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1222
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
1015
1223
  const scIds = extractAnnotatedScIds(text);
1016
1224
  if (scIds.length === 0) {
1017
1225
  continue;
@@ -1068,16 +1276,16 @@ function formatError3(error2) {
1068
1276
  }
1069
1277
 
1070
1278
  // src/core/version.ts
1071
- var import_promises7 = require("fs/promises");
1072
- var import_node_path8 = __toESM(require("path"), 1);
1279
+ var import_promises8 = require("fs/promises");
1280
+ var import_node_path9 = __toESM(require("path"), 1);
1073
1281
  var import_node_url2 = require("url");
1074
1282
  async function resolveToolVersion() {
1075
- if ("0.4.2".length > 0) {
1076
- return "0.4.2";
1283
+ if ("0.4.6".length > 0) {
1284
+ return "0.4.6";
1077
1285
  }
1078
1286
  try {
1079
1287
  const packagePath = resolvePackageJsonPath();
1080
- const raw = await (0, import_promises7.readFile)(packagePath, "utf-8");
1288
+ const raw = await (0, import_promises8.readFile)(packagePath, "utf-8");
1081
1289
  const parsed = JSON.parse(raw);
1082
1290
  const version = typeof parsed.version === "string" ? parsed.version : "";
1083
1291
  return version.length > 0 ? version : "unknown";
@@ -1088,54 +1296,23 @@ async function resolveToolVersion() {
1088
1296
  function resolvePackageJsonPath() {
1089
1297
  const base = __filename;
1090
1298
  const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
1091
- return import_node_path8.default.resolve(import_node_path8.default.dirname(basePath), "../../package.json");
1299
+ return import_node_path9.default.resolve(import_node_path9.default.dirname(basePath), "../../package.json");
1092
1300
  }
1093
1301
 
1094
1302
  // src/core/validators/contracts.ts
1095
- var import_promises8 = require("fs/promises");
1096
- var import_node_path10 = __toESM(require("path"), 1);
1303
+ var import_promises9 = require("fs/promises");
1304
+ var import_node_path11 = __toESM(require("path"), 1);
1097
1305
 
1098
1306
  // src/core/contracts.ts
1099
- var import_node_path9 = __toESM(require("path"), 1);
1307
+ var import_node_path10 = __toESM(require("path"), 1);
1100
1308
  var import_yaml2 = require("yaml");
1101
1309
  function parseStructuredContract(file, text) {
1102
- const ext = import_node_path9.default.extname(file).toLowerCase();
1310
+ const ext = import_node_path10.default.extname(file).toLowerCase();
1103
1311
  if (ext === ".json") {
1104
1312
  return JSON.parse(text);
1105
1313
  }
1106
1314
  return (0, import_yaml2.parse)(text);
1107
1315
  }
1108
- function extractUiContractIds(doc) {
1109
- const id = typeof doc.id === "string" ? doc.id : "";
1110
- return extractIds(id, "UI");
1111
- }
1112
- function extractApiContractIds(doc) {
1113
- const operationIds = /* @__PURE__ */ new Set();
1114
- collectOperationIds(doc, operationIds);
1115
- const ids = /* @__PURE__ */ new Set();
1116
- for (const operationId of operationIds) {
1117
- extractIds(operationId, "API").forEach((id) => ids.add(id));
1118
- }
1119
- return Array.from(ids);
1120
- }
1121
- function collectOperationIds(value, out) {
1122
- if (!value || typeof value !== "object") {
1123
- return;
1124
- }
1125
- if (Array.isArray(value)) {
1126
- for (const item of value) {
1127
- collectOperationIds(item, out);
1128
- }
1129
- return;
1130
- }
1131
- for (const [key, entry] of Object.entries(value)) {
1132
- if (key === "operationId" && typeof entry === "string") {
1133
- out.add(entry);
1134
- continue;
1135
- }
1136
- collectOperationIds(entry, out);
1137
- }
1138
- }
1139
1316
 
1140
1317
  // src/core/validators/contracts.ts
1141
1318
  var SQL_DANGEROUS_PATTERNS = [
@@ -1150,9 +1327,11 @@ var SQL_DANGEROUS_PATTERNS = [
1150
1327
  async function validateContracts(root, config) {
1151
1328
  const issues = [];
1152
1329
  const contractsRoot = resolvePath(root, config, "contractsDir");
1153
- issues.push(...await validateUiContracts(import_node_path10.default.join(contractsRoot, "ui")));
1154
- issues.push(...await validateApiContracts(import_node_path10.default.join(contractsRoot, "api")));
1155
- issues.push(...await validateDataContracts(import_node_path10.default.join(contractsRoot, "db")));
1330
+ issues.push(...await validateUiContracts(import_node_path11.default.join(contractsRoot, "ui")));
1331
+ issues.push(...await validateApiContracts(import_node_path11.default.join(contractsRoot, "api")));
1332
+ issues.push(...await validateDbContracts(import_node_path11.default.join(contractsRoot, "db")));
1333
+ const contractIndex = await buildContractIndex(root, config);
1334
+ issues.push(...validateDuplicateContractIds(contractIndex));
1156
1335
  return issues;
1157
1336
  }
1158
1337
  async function validateUiContracts(uiRoot) {
@@ -1170,14 +1349,14 @@ async function validateUiContracts(uiRoot) {
1170
1349
  }
1171
1350
  const issues = [];
1172
1351
  for (const file of files) {
1173
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1352
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1174
1353
  const invalidIds = extractInvalidIds(text, [
1175
1354
  "SPEC",
1176
1355
  "BR",
1177
1356
  "SC",
1178
1357
  "UI",
1179
1358
  "API",
1180
- "DATA",
1359
+ "DB",
1181
1360
  "ADR"
1182
1361
  ]);
1183
1362
  if (invalidIds.length > 0) {
@@ -1192,9 +1371,10 @@ async function validateUiContracts(uiRoot) {
1192
1371
  )
1193
1372
  );
1194
1373
  }
1195
- let doc;
1374
+ const declaredIds = extractDeclaredContractIds(text);
1375
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
1196
1376
  try {
1197
- doc = parseStructuredContract(file, text);
1377
+ parseStructuredContract(file, stripContractDeclarationLines(text));
1198
1378
  } catch (error2) {
1199
1379
  issues.push(
1200
1380
  issue(
@@ -1205,19 +1385,6 @@ async function validateUiContracts(uiRoot) {
1205
1385
  "contracts.ui.parse"
1206
1386
  )
1207
1387
  );
1208
- continue;
1209
- }
1210
- const uiIds = extractUiContractIds(doc);
1211
- if (uiIds.length === 0) {
1212
- issues.push(
1213
- issue(
1214
- "QFAI-CONTRACT-002",
1215
- `UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
1216
- "error",
1217
- file,
1218
- "contracts.ui.id"
1219
- )
1220
- );
1221
1388
  }
1222
1389
  }
1223
1390
  return issues;
@@ -1237,14 +1404,14 @@ async function validateApiContracts(apiRoot) {
1237
1404
  }
1238
1405
  const issues = [];
1239
1406
  for (const file of files) {
1240
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1407
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1241
1408
  const invalidIds = extractInvalidIds(text, [
1242
1409
  "SPEC",
1243
1410
  "BR",
1244
1411
  "SC",
1245
1412
  "UI",
1246
1413
  "API",
1247
- "DATA",
1414
+ "DB",
1248
1415
  "ADR"
1249
1416
  ]);
1250
1417
  if (invalidIds.length > 0) {
@@ -1259,9 +1426,11 @@ async function validateApiContracts(apiRoot) {
1259
1426
  )
1260
1427
  );
1261
1428
  }
1429
+ const declaredIds = extractDeclaredContractIds(text);
1430
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "API"));
1262
1431
  let doc;
1263
1432
  try {
1264
- doc = parseStructuredContract(file, text);
1433
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
1265
1434
  } catch (error2) {
1266
1435
  issues.push(
1267
1436
  issue(
@@ -1285,44 +1454,32 @@ async function validateApiContracts(apiRoot) {
1285
1454
  )
1286
1455
  );
1287
1456
  }
1288
- const apiIds = extractApiContractIds(doc);
1289
- if (apiIds.length === 0) {
1290
- issues.push(
1291
- issue(
1292
- "QFAI-CONTRACT-002",
1293
- `API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
1294
- "error",
1295
- file,
1296
- "contracts.api.id"
1297
- )
1298
- );
1299
- }
1300
1457
  }
1301
1458
  return issues;
1302
1459
  }
1303
- async function validateDataContracts(dataRoot) {
1304
- const files = await collectDataContractFiles(dataRoot);
1460
+ async function validateDbContracts(dbRoot) {
1461
+ const files = await collectDbContractFiles(dbRoot);
1305
1462
  if (files.length === 0) {
1306
1463
  return [
1307
1464
  issue(
1308
- "QFAI-DATA-000",
1309
- "DATA \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1465
+ "QFAI-DB-000",
1466
+ "DB \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1310
1467
  "info",
1311
- dataRoot,
1312
- "contracts.data.files"
1468
+ dbRoot,
1469
+ "contracts.db.files"
1313
1470
  )
1314
1471
  ];
1315
1472
  }
1316
1473
  const issues = [];
1317
1474
  for (const file of files) {
1318
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1475
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1319
1476
  const invalidIds = extractInvalidIds(text, [
1320
1477
  "SPEC",
1321
1478
  "BR",
1322
1479
  "SC",
1323
1480
  "UI",
1324
1481
  "API",
1325
- "DATA",
1482
+ "DB",
1326
1483
  "ADR"
1327
1484
  ]);
1328
1485
  if (invalidIds.length > 0) {
@@ -1337,6 +1494,8 @@ async function validateDataContracts(dataRoot) {
1337
1494
  )
1338
1495
  );
1339
1496
  }
1497
+ const declaredIds = extractDeclaredContractIds(text);
1498
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "DB"));
1340
1499
  issues.push(...lintSql(text, file));
1341
1500
  }
1342
1501
  return issues;
@@ -1347,17 +1506,83 @@ function lintSql(text, file) {
1347
1506
  if (pattern.test(text)) {
1348
1507
  issues.push(
1349
1508
  issue(
1350
- "QFAI-DATA-001",
1509
+ "QFAI-DB-001",
1351
1510
  `\u5371\u967A\u306A SQL \u64CD\u4F5C\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059: ${label}`,
1352
1511
  "warning",
1353
1512
  file,
1354
- "contracts.data.sql"
1513
+ "contracts.db.sql"
1355
1514
  )
1356
1515
  );
1357
1516
  }
1358
1517
  }
1359
1518
  return issues;
1360
1519
  }
1520
+ function validateDeclaredContractIds(ids, file, kind) {
1521
+ const issues = [];
1522
+ if (ids.length === 0) {
1523
+ issues.push(
1524
+ issue(
1525
+ "QFAI-CONTRACT-010",
1526
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
1527
+ "error",
1528
+ file,
1529
+ "contracts.declaration"
1530
+ )
1531
+ );
1532
+ return issues;
1533
+ }
1534
+ if (ids.length > 1) {
1535
+ issues.push(
1536
+ issue(
1537
+ "QFAI-CONTRACT-011",
1538
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B\u8907\u6570\u306E QFAI-CONTRACT-ID \u304C\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${ids.join(
1539
+ ", "
1540
+ )}`,
1541
+ "error",
1542
+ file,
1543
+ "contracts.declaration",
1544
+ ids
1545
+ )
1546
+ );
1547
+ return issues;
1548
+ }
1549
+ const [id] = ids;
1550
+ if (id && !id.startsWith(`${kind}-`)) {
1551
+ issues.push(
1552
+ issue(
1553
+ "QFAI-CONTRACT-013",
1554
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E QFAI-CONTRACT-ID \u304C ${kind}- \u3067\u306F\u3042\u308A\u307E\u305B\u3093: ${id}`,
1555
+ "error",
1556
+ file,
1557
+ "contracts.declarationPrefix",
1558
+ [id]
1559
+ )
1560
+ );
1561
+ }
1562
+ return issues;
1563
+ }
1564
+ function validateDuplicateContractIds(contractIndex) {
1565
+ const issues = [];
1566
+ for (const [id, files] of contractIndex.idToFiles.entries()) {
1567
+ if (files.size <= 1) {
1568
+ continue;
1569
+ }
1570
+ const sortedFiles = Array.from(files).sort((a, b) => a.localeCompare(b));
1571
+ issues.push(
1572
+ issue(
1573
+ "QFAI-CONTRACT-012",
1574
+ `\u5951\u7D04 ID \u304C\u8907\u6570\u30D5\u30A1\u30A4\u30EB\u3067\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${id} (${sortedFiles.join(
1575
+ ", "
1576
+ )})`,
1577
+ "error",
1578
+ sortedFiles[0],
1579
+ "contracts.idDuplicate",
1580
+ [id]
1581
+ )
1582
+ );
1583
+ }
1584
+ return issues;
1585
+ }
1361
1586
  function hasOpenApi(doc) {
1362
1587
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
1363
1588
  }
@@ -1386,8 +1611,8 @@ function issue(code, message, severity, file, rule, refs) {
1386
1611
  }
1387
1612
 
1388
1613
  // src/core/validators/delta.ts
1389
- var import_promises9 = require("fs/promises");
1390
- var import_node_path11 = __toESM(require("path"), 1);
1614
+ var import_promises10 = require("fs/promises");
1615
+ var import_node_path12 = __toESM(require("path"), 1);
1391
1616
  var SECTION_RE = /^##\s+変更区分/m;
1392
1617
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1393
1618
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1401,10 +1626,10 @@ async function validateDeltas(root, config) {
1401
1626
  }
1402
1627
  const issues = [];
1403
1628
  for (const pack of packs) {
1404
- const deltaPath = import_node_path11.default.join(pack, "delta.md");
1629
+ const deltaPath = import_node_path12.default.join(pack, "delta.md");
1405
1630
  let text;
1406
1631
  try {
1407
- text = await (0, import_promises9.readFile)(deltaPath, "utf-8");
1632
+ text = await (0, import_promises10.readFile)(deltaPath, "utf-8");
1408
1633
  } catch (error2) {
1409
1634
  if (isMissingFileError2(error2)) {
1410
1635
  issues.push(
@@ -1478,187 +1703,6 @@ function issue2(code, message, severity, file, rule, refs) {
1478
1703
  // src/core/validators/ids.ts
1479
1704
  var import_promises11 = require("fs/promises");
1480
1705
  var import_node_path13 = __toESM(require("path"), 1);
1481
-
1482
- // src/core/contractIndex.ts
1483
- var import_promises10 = require("fs/promises");
1484
- var import_node_path12 = __toESM(require("path"), 1);
1485
- async function buildContractIndex(root, config) {
1486
- const contractsRoot = resolvePath(root, config, "contractsDir");
1487
- const uiRoot = import_node_path12.default.join(contractsRoot, "ui");
1488
- const apiRoot = import_node_path12.default.join(contractsRoot, "api");
1489
- const dataRoot = import_node_path12.default.join(contractsRoot, "db");
1490
- const [uiFiles, apiFiles, dataFiles] = await Promise.all([
1491
- collectUiContractFiles(uiRoot),
1492
- collectApiContractFiles(apiRoot),
1493
- collectDataContractFiles(dataRoot)
1494
- ]);
1495
- const index = {
1496
- ids: /* @__PURE__ */ new Set(),
1497
- idToFiles: /* @__PURE__ */ new Map(),
1498
- files: { ui: uiFiles, api: apiFiles, data: dataFiles },
1499
- structuredParseFailedFiles: /* @__PURE__ */ new Set()
1500
- };
1501
- await indexUiContracts(uiFiles, index);
1502
- await indexApiContracts(apiFiles, index);
1503
- await indexDataContracts(dataFiles, index);
1504
- return index;
1505
- }
1506
- async function indexUiContracts(files, index) {
1507
- for (const file of files) {
1508
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1509
- try {
1510
- const doc = parseStructuredContract(file, text);
1511
- extractUiContractIds(doc).forEach((id) => record(index, id, file));
1512
- } catch {
1513
- index.structuredParseFailedFiles.add(file);
1514
- extractIds(text, "UI").forEach((id) => record(index, id, file));
1515
- }
1516
- }
1517
- }
1518
- async function indexApiContracts(files, index) {
1519
- for (const file of files) {
1520
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1521
- try {
1522
- const doc = parseStructuredContract(file, text);
1523
- extractApiContractIds(doc).forEach((id) => record(index, id, file));
1524
- } catch {
1525
- index.structuredParseFailedFiles.add(file);
1526
- extractIds(text, "API").forEach((id) => record(index, id, file));
1527
- }
1528
- }
1529
- }
1530
- async function indexDataContracts(files, index) {
1531
- for (const file of files) {
1532
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1533
- extractIds(text, "DATA").forEach((id) => record(index, id, file));
1534
- }
1535
- }
1536
- function record(index, id, file) {
1537
- index.ids.add(id);
1538
- const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
1539
- current.add(file);
1540
- index.idToFiles.set(id, current);
1541
- }
1542
-
1543
- // src/core/parse/markdown.ts
1544
- var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1545
- function parseHeadings(md) {
1546
- const lines = md.split(/\r?\n/);
1547
- const headings = [];
1548
- for (let i = 0; i < lines.length; i++) {
1549
- const line = lines[i] ?? "";
1550
- const match = line.match(HEADING_RE);
1551
- if (!match) continue;
1552
- const levelToken = match[1];
1553
- const title = match[2];
1554
- if (!levelToken || !title) continue;
1555
- headings.push({
1556
- level: levelToken.length,
1557
- title: title.trim(),
1558
- line: i + 1
1559
- });
1560
- }
1561
- return headings;
1562
- }
1563
- function extractH2Sections(md) {
1564
- const lines = md.split(/\r?\n/);
1565
- const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1566
- const sections = /* @__PURE__ */ new Map();
1567
- for (let i = 0; i < headings.length; i++) {
1568
- const current = headings[i];
1569
- if (!current) continue;
1570
- const next = headings[i + 1];
1571
- const startLine = current.line + 1;
1572
- const endLine = (next?.line ?? lines.length + 1) - 1;
1573
- const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1574
- sections.set(current.title.trim(), {
1575
- title: current.title.trim(),
1576
- startLine,
1577
- endLine,
1578
- body
1579
- });
1580
- }
1581
- return sections;
1582
- }
1583
-
1584
- // src/core/parse/spec.ts
1585
- var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1586
- var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1587
- var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1588
- var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1589
- var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1590
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1591
- function parseSpec(md, file) {
1592
- const headings = parseHeadings(md);
1593
- const h1 = headings.find((heading) => heading.level === 1);
1594
- const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1595
- const sections = extractH2Sections(md);
1596
- const sectionNames = new Set(Array.from(sections.keys()));
1597
- const brSection = sections.get(BR_SECTION_TITLE);
1598
- const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1599
- const startLine = brSection?.startLine ?? 1;
1600
- const brs = [];
1601
- const brsWithoutPriority = [];
1602
- const brsWithInvalidPriority = [];
1603
- for (let i = 0; i < brLines.length; i++) {
1604
- const lineText = brLines[i] ?? "";
1605
- const lineNumber = startLine + i;
1606
- const validMatch = lineText.match(BR_LINE_RE);
1607
- if (validMatch) {
1608
- const id = validMatch[1];
1609
- const priority = validMatch[2];
1610
- const text = validMatch[3];
1611
- if (!id || !priority || !text) continue;
1612
- brs.push({
1613
- id,
1614
- priority,
1615
- text: text.trim(),
1616
- line: lineNumber
1617
- });
1618
- continue;
1619
- }
1620
- const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1621
- if (anyPriorityMatch) {
1622
- const id = anyPriorityMatch[1];
1623
- const priority = anyPriorityMatch[2];
1624
- const text = anyPriorityMatch[3];
1625
- if (!id || !priority || !text) continue;
1626
- if (!VALID_PRIORITIES.has(priority)) {
1627
- brsWithInvalidPriority.push({
1628
- id,
1629
- priority,
1630
- text: text.trim(),
1631
- line: lineNumber
1632
- });
1633
- }
1634
- continue;
1635
- }
1636
- const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1637
- if (noPriorityMatch) {
1638
- const id = noPriorityMatch[1];
1639
- const text = noPriorityMatch[2];
1640
- if (!id || !text) continue;
1641
- brsWithoutPriority.push({
1642
- id,
1643
- text: text.trim(),
1644
- line: lineNumber
1645
- });
1646
- }
1647
- }
1648
- const parsed = {
1649
- file,
1650
- sections: sectionNames,
1651
- brs,
1652
- brsWithoutPriority,
1653
- brsWithInvalidPriority
1654
- };
1655
- if (specId) {
1656
- parsed.specId = specId;
1657
- }
1658
- return parsed;
1659
- }
1660
-
1661
- // src/core/validators/ids.ts
1662
1706
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1663
1707
  async function validateDefinedIds(root, config) {
1664
1708
  const issues = [];
@@ -1801,7 +1845,7 @@ function validateScenarioContent(text, file) {
1801
1845
  "SC",
1802
1846
  "UI",
1803
1847
  "API",
1804
- "DATA",
1848
+ "DB",
1805
1849
  "ADR"
1806
1850
  ]);
1807
1851
  if (invalidIds.length > 0) {
@@ -1999,7 +2043,7 @@ function validateSpecContent(text, file, requiredSections) {
1999
2043
  "SC",
2000
2044
  "UI",
2001
2045
  "API",
2002
- "DATA",
2046
+ "DB",
2003
2047
  "ADR"
2004
2048
  ]);
2005
2049
  if (invalidIds.length > 0) {
@@ -2128,8 +2172,7 @@ async function validateTraceability(root, config) {
2128
2172
  const brIdsInSpecs = /* @__PURE__ */ new Set();
2129
2173
  const brIdsInScenarios = /* @__PURE__ */ new Set();
2130
2174
  const scIdsInScenarios = /* @__PURE__ */ new Set();
2131
- const scenarioContractIds = /* @__PURE__ */ new Set();
2132
- const scWithContracts = /* @__PURE__ */ new Set();
2175
+ const specContractIds = /* @__PURE__ */ new Set();
2133
2176
  const specToBrIds = /* @__PURE__ */ new Map();
2134
2177
  const contractIndex = await buildContractIndex(root, config);
2135
2178
  const contractIds = contractIndex.ids;
@@ -2147,6 +2190,64 @@ async function validateTraceability(root, config) {
2147
2190
  brIds.forEach((id) => current.add(id));
2148
2191
  specToBrIds.set(parsed.specId, current);
2149
2192
  }
2193
+ const contractRefs = parsed.contractRefs;
2194
+ if (contractRefs.lines.length === 0) {
2195
+ issues.push(
2196
+ issue6(
2197
+ "QFAI-TRACE-020",
2198
+ "Spec \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
2199
+ "error",
2200
+ file,
2201
+ "traceability.specContractRefRequired"
2202
+ )
2203
+ );
2204
+ } else {
2205
+ if (contractRefs.hasNone && contractRefs.ids.length > 0) {
2206
+ issues.push(
2207
+ issue6(
2208
+ "QFAI-TRACE-021",
2209
+ "Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2210
+ "error",
2211
+ file,
2212
+ "traceability.specContractRefFormat"
2213
+ )
2214
+ );
2215
+ }
2216
+ if (contractRefs.invalidTokens.length > 0) {
2217
+ issues.push(
2218
+ issue6(
2219
+ "QFAI-TRACE-021",
2220
+ `Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
2221
+ ", "
2222
+ )}`,
2223
+ "error",
2224
+ file,
2225
+ "traceability.specContractRefFormat",
2226
+ contractRefs.invalidTokens
2227
+ )
2228
+ );
2229
+ }
2230
+ }
2231
+ contractRefs.ids.forEach((id) => {
2232
+ specContractIds.add(id);
2233
+ });
2234
+ const unknownContractIds = contractRefs.ids.filter(
2235
+ (id) => !contractIds.has(id)
2236
+ );
2237
+ if (unknownContractIds.length > 0) {
2238
+ issues.push(
2239
+ issue6(
2240
+ "QFAI-TRACE-021",
2241
+ `Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
2242
+ ", "
2243
+ )}`,
2244
+ "error",
2245
+ file,
2246
+ "traceability.specContractExists",
2247
+ unknownContractIds
2248
+ )
2249
+ );
2250
+ }
2150
2251
  }
2151
2252
  for (const file of scenarioFiles) {
2152
2253
  const text = await (0, import_promises14.readFile)(file, "utf-8");
@@ -2192,10 +2293,6 @@ async function validateTraceability(root, config) {
2192
2293
  scIdsInScenarios.add(id);
2193
2294
  scIdsInFile.add(id);
2194
2295
  });
2195
- atom.contractIds.forEach((id) => scenarioContractIds.add(id));
2196
- if (atom.contractIds.length > 0) {
2197
- scTags.forEach((id) => scWithContracts.add(id));
2198
- }
2199
2296
  const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
2200
2297
  if (unknownSpecIds.length > 0) {
2201
2298
  issues.push(
@@ -2314,25 +2411,6 @@ async function validateTraceability(root, config) {
2314
2411
  );
2315
2412
  }
2316
2413
  }
2317
- if (config.validation.traceability.scMustTouchContracts && scIdsInScenarios.size > 0) {
2318
- const scWithoutContracts = Array.from(scIdsInScenarios).filter(
2319
- (id) => !scWithContracts.has(id)
2320
- );
2321
- if (scWithoutContracts.length > 0) {
2322
- issues.push(
2323
- issue6(
2324
- "QFAI_TRACE_SC_NO_CONTRACT",
2325
- `SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
2326
- ", "
2327
- )}`,
2328
- "error",
2329
- specsRoot,
2330
- "traceability.scMustTouchContracts",
2331
- scWithoutContracts
2332
- )
2333
- );
2334
- }
2335
- }
2336
2414
  const scRefsResult = await collectScTestReferences(
2337
2415
  root,
2338
2416
  config.validation.traceability.testFileGlobs,
@@ -2396,16 +2474,16 @@ async function validateTraceability(root, config) {
2396
2474
  if (!config.validation.traceability.allowOrphanContracts) {
2397
2475
  if (contractIds.size > 0) {
2398
2476
  const orphanContracts = Array.from(contractIds).filter(
2399
- (id) => !scenarioContractIds.has(id)
2477
+ (id) => !specContractIds.has(id)
2400
2478
  );
2401
2479
  if (orphanContracts.length > 0) {
2402
2480
  issues.push(
2403
2481
  issue6(
2404
- "QFAI_CONTRACT_ORPHAN",
2405
- `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
2482
+ "QFAI-TRACE-022",
2483
+ `\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
2406
2484
  "error",
2407
2485
  specsRoot,
2408
- "traceability.allowOrphanContracts",
2486
+ "traceability.contractCoverage",
2409
2487
  orphanContracts
2410
2488
  )
2411
2489
  );
@@ -2526,7 +2604,7 @@ function countIssues(issues) {
2526
2604
  }
2527
2605
 
2528
2606
  // src/core/report.ts
2529
- var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
2607
+ var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
2530
2608
  async function createReportData(root, validation, configResult) {
2531
2609
  const resolved = configResult ?? await loadConfig(root);
2532
2610
  const config = resolved.config;
@@ -2545,6 +2623,23 @@ async function createReportData(root, validation, configResult) {
2545
2623
  ui: uiFiles,
2546
2624
  db: dbFiles
2547
2625
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
2626
+ const contractIndex = await buildContractIndex(root, config);
2627
+ const specContractRefs = await collectSpecContractRefs(specFiles);
2628
+ const contractIdList = Array.from(contractIndex.ids);
2629
+ const referencedContracts = /* @__PURE__ */ new Set();
2630
+ for (const ids of specContractRefs.specToContractIds.values()) {
2631
+ ids.forEach((id) => referencedContracts.add(id));
2632
+ }
2633
+ const referencedContractCount = contractIdList.filter(
2634
+ (id) => referencedContracts.has(id)
2635
+ ).length;
2636
+ const orphanContractCount = contractIdList.filter(
2637
+ (id) => !referencedContracts.has(id)
2638
+ ).length;
2639
+ const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
2640
+ const specToContractIdsRecord = mapToSortedRecord(
2641
+ specContractRefs.specToContractIds
2642
+ );
2548
2643
  const idsByPrefix = await collectIds([
2549
2644
  ...specFiles,
2550
2645
  ...scenarioFiles,
@@ -2595,14 +2690,24 @@ async function createReportData(root, validation, configResult) {
2595
2690
  sc: idsByPrefix.SC,
2596
2691
  ui: idsByPrefix.UI,
2597
2692
  api: idsByPrefix.API,
2598
- data: idsByPrefix.DATA
2693
+ db: idsByPrefix.DB
2599
2694
  },
2600
2695
  traceability: {
2601
2696
  upstreamIdsFound: upstreamIds.size,
2602
2697
  referencedInCodeOrTests: traceability,
2603
2698
  sc: scCoverage,
2604
2699
  scSources: scSourceRecord,
2605
- testFiles
2700
+ testFiles,
2701
+ contracts: {
2702
+ total: contractIdList.length,
2703
+ referenced: referencedContractCount,
2704
+ orphan: orphanContractCount,
2705
+ idToSpecs: contractIdToSpecsRecord
2706
+ },
2707
+ specs: {
2708
+ contractRefMissing: specContractRefs.missingRefSpecs.size,
2709
+ specToContractIds: specToContractIdsRecord
2710
+ }
2606
2711
  },
2607
2712
  issues: resolvedValidation.issues
2608
2713
  };
@@ -2631,7 +2736,7 @@ function formatReportMarkdown(data) {
2631
2736
  lines.push(formatIdLine("SC", data.ids.sc));
2632
2737
  lines.push(formatIdLine("UI", data.ids.ui));
2633
2738
  lines.push(formatIdLine("API", data.ids.api));
2634
- lines.push(formatIdLine("DATA", data.ids.data));
2739
+ lines.push(formatIdLine("DB", data.ids.db));
2635
2740
  lines.push("");
2636
2741
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
2637
2742
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
@@ -2639,6 +2744,50 @@ function formatReportMarkdown(data) {
2639
2744
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2640
2745
  );
2641
2746
  lines.push("");
2747
+ lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
2748
+ lines.push(`- total: ${data.traceability.contracts.total}`);
2749
+ lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
2750
+ lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
2751
+ lines.push(
2752
+ `- specContractRefMissing: ${data.traceability.specs.contractRefMissing}`
2753
+ );
2754
+ lines.push("");
2755
+ lines.push("## \u5951\u7D04\u2192Spec");
2756
+ const contractToSpecs = data.traceability.contracts.idToSpecs;
2757
+ const contractIds = Object.keys(contractToSpecs).sort(
2758
+ (a, b) => a.localeCompare(b)
2759
+ );
2760
+ if (contractIds.length === 0) {
2761
+ lines.push("- (none)");
2762
+ } else {
2763
+ for (const contractId of contractIds) {
2764
+ const specs = contractToSpecs[contractId] ?? [];
2765
+ if (specs.length === 0) {
2766
+ lines.push(`- ${contractId}: (none)`);
2767
+ } else {
2768
+ lines.push(`- ${contractId}: ${specs.join(", ")}`);
2769
+ }
2770
+ }
2771
+ }
2772
+ lines.push("");
2773
+ lines.push("## Spec\u2192\u5951\u7D04");
2774
+ const specToContracts = data.traceability.specs.specToContractIds;
2775
+ const specIds = Object.keys(specToContracts).sort(
2776
+ (a, b) => a.localeCompare(b)
2777
+ );
2778
+ if (specIds.length === 0) {
2779
+ lines.push("- (none)");
2780
+ } else {
2781
+ for (const specId of specIds) {
2782
+ const contractIds2 = specToContracts[specId] ?? [];
2783
+ if (contractIds2.length === 0) {
2784
+ lines.push(`- ${specId}: (none)`);
2785
+ } else {
2786
+ lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
2787
+ }
2788
+ }
2789
+ }
2790
+ lines.push("");
2642
2791
  lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2643
2792
  lines.push(`- total: ${data.traceability.sc.total}`);
2644
2793
  lines.push(`- covered: ${data.traceability.sc.covered}`);
@@ -2712,7 +2861,7 @@ function formatReportMarkdown(data) {
2712
2861
  lines.push("");
2713
2862
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
2714
2863
  const traceIssues = data.issues.filter(
2715
- (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-") || item.code === "QFAI_CONTRACT_ORPHAN"
2864
+ (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
2716
2865
  );
2717
2866
  if (traceIssues.length === 0) {
2718
2867
  lines.push("- (none)");
@@ -2742,6 +2891,33 @@ function formatReportMarkdown(data) {
2742
2891
  function formatReportJson(data) {
2743
2892
  return JSON.stringify(data, null, 2);
2744
2893
  }
2894
+ async function collectSpecContractRefs(specFiles) {
2895
+ const specToContractIds = /* @__PURE__ */ new Map();
2896
+ const idToSpecs = /* @__PURE__ */ new Map();
2897
+ const missingRefSpecs = /* @__PURE__ */ new Set();
2898
+ for (const file of specFiles) {
2899
+ const text = await (0, import_promises15.readFile)(file, "utf-8");
2900
+ const parsed = parseSpec(text, file);
2901
+ const specKey = parsed.specId ?? file;
2902
+ const refs = parsed.contractRefs;
2903
+ if (refs.lines.length === 0) {
2904
+ missingRefSpecs.add(specKey);
2905
+ }
2906
+ const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
2907
+ for (const id of refs.ids) {
2908
+ currentContracts.add(id);
2909
+ const specs = idToSpecs.get(id) ?? /* @__PURE__ */ new Set();
2910
+ specs.add(specKey);
2911
+ idToSpecs.set(id, specs);
2912
+ }
2913
+ specToContractIds.set(specKey, currentContracts);
2914
+ }
2915
+ return {
2916
+ specToContractIds,
2917
+ idToSpecs,
2918
+ missingRefSpecs
2919
+ };
2920
+ }
2745
2921
  async function collectIds(files) {
2746
2922
  const result = {
2747
2923
  SPEC: /* @__PURE__ */ new Set(),
@@ -2749,7 +2925,7 @@ async function collectIds(files) {
2749
2925
  SC: /* @__PURE__ */ new Set(),
2750
2926
  UI: /* @__PURE__ */ new Set(),
2751
2927
  API: /* @__PURE__ */ new Set(),
2752
- DATA: /* @__PURE__ */ new Set()
2928
+ DB: /* @__PURE__ */ new Set()
2753
2929
  };
2754
2930
  for (const file of files) {
2755
2931
  const text = await (0, import_promises15.readFile)(file, "utf-8");
@@ -2764,7 +2940,7 @@ async function collectIds(files) {
2764
2940
  SC: toSortedArray2(result.SC),
2765
2941
  UI: toSortedArray2(result.UI),
2766
2942
  API: toSortedArray2(result.API),
2767
- DATA: toSortedArray2(result.DATA)
2943
+ DB: toSortedArray2(result.DB)
2768
2944
  };
2769
2945
  }
2770
2946
  async function collectUpstreamIds(files) {
@@ -2910,7 +3086,31 @@ function isValidationResult(value) {
2910
3086
  if (!counts) {
2911
3087
  return false;
2912
3088
  }
2913
- return typeof counts.info === "number" && typeof counts.warning === "number" && typeof counts.error === "number";
3089
+ if (typeof counts.info !== "number" || typeof counts.warning !== "number" || typeof counts.error !== "number") {
3090
+ return false;
3091
+ }
3092
+ const traceability = record2.traceability;
3093
+ if (!traceability || typeof traceability !== "object") {
3094
+ return false;
3095
+ }
3096
+ const sc = traceability.sc;
3097
+ const testFiles = traceability.testFiles;
3098
+ if (!sc || !testFiles) {
3099
+ return false;
3100
+ }
3101
+ if (typeof sc.total !== "number" || typeof sc.covered !== "number" || typeof sc.missing !== "number") {
3102
+ return false;
3103
+ }
3104
+ if (!Array.isArray(sc.missingIds)) {
3105
+ return false;
3106
+ }
3107
+ if (!sc.refs || typeof sc.refs !== "object") {
3108
+ return false;
3109
+ }
3110
+ if (!Array.isArray(testFiles.globs) || !Array.isArray(testFiles.excludeGlobs) || typeof testFiles.matchedFileCount !== "number") {
3111
+ return false;
3112
+ }
3113
+ return true;
2914
3114
  }
2915
3115
  function isMissingFileError5(error2) {
2916
3116
  if (!error2 || typeof error2 !== "object") {