qfai 0.4.0 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +20 -1
  2. package/assets/init/.qfai/README.md +3 -1
  3. package/assets/init/.qfai/contracts/README.md +17 -8
  4. package/assets/init/.qfai/contracts/api/api-0001-sample.yaml +3 -2
  5. package/assets/init/.qfai/contracts/db/db-0001-sample.sql +2 -1
  6. package/assets/init/.qfai/contracts/ui/ui-0001-sample.yaml +3 -1
  7. package/assets/init/.qfai/promptpack/modes/change.md +3 -2
  8. package/assets/init/.qfai/promptpack/modes/compatibility.md +2 -0
  9. package/assets/init/.qfai/prompts/README.md +1 -0
  10. package/assets/init/.qfai/prompts/qfai-generate-test-globs.md +29 -0
  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/spec.md +2 -0
  14. package/assets/init/root/qfai.config.yaml +6 -1
  15. package/assets/init/root/tests/qfai-traceability.sample.test.ts +2 -2
  16. package/dist/cli/index.cjs +885 -489
  17. package/dist/cli/index.cjs.map +1 -1
  18. package/dist/cli/index.mjs +885 -489
  19. package/dist/cli/index.mjs.map +1 -1
  20. package/dist/core/config.d.ts +2 -1
  21. package/dist/core/config.d.ts.map +1 -1
  22. package/dist/core/config.js +4 -2
  23. package/dist/core/config.js.map +1 -1
  24. package/dist/core/contractIndex.d.ts +1 -2
  25. package/dist/core/contractIndex.d.ts.map +1 -1
  26. package/dist/core/contractIndex.js +10 -38
  27. package/dist/core/contractIndex.js.map +1 -1
  28. package/dist/core/contractsDecl.d.ts +3 -0
  29. package/dist/core/contractsDecl.d.ts.map +1 -0
  30. package/dist/core/contractsDecl.js +19 -0
  31. package/dist/core/contractsDecl.js.map +1 -0
  32. package/dist/core/fs.d.ts +5 -0
  33. package/dist/core/fs.d.ts.map +1 -1
  34. package/dist/core/fs.js +13 -0
  35. package/dist/core/fs.js.map +1 -1
  36. package/dist/core/ids.d.ts +1 -1
  37. package/dist/core/ids.d.ts.map +1 -1
  38. package/dist/core/ids.js +3 -3
  39. package/dist/core/ids.js.map +1 -1
  40. package/dist/core/parse/spec.d.ts +8 -0
  41. package/dist/core/parse/spec.d.ts.map +1 -1
  42. package/dist/core/parse/spec.js +43 -0
  43. package/dist/core/parse/spec.js.map +1 -1
  44. package/dist/core/report.d.ts +16 -2
  45. package/dist/core/report.d.ts.map +1 -1
  46. package/dist/core/report.js +144 -11
  47. package/dist/core/report.js.map +1 -1
  48. package/dist/core/scenarioModel.d.ts.map +1 -1
  49. package/dist/core/scenarioModel.js +3 -5
  50. package/dist/core/scenarioModel.js.map +1 -1
  51. package/dist/core/traceability.d.ts +15 -1
  52. package/dist/core/traceability.d.ts.map +1 -1
  53. package/dist/core/traceability.js +96 -9
  54. package/dist/core/traceability.js.map +1 -1
  55. package/dist/core/types.d.ts +6 -0
  56. package/dist/core/types.d.ts.map +1 -1
  57. package/dist/core/validate.d.ts.map +1 -1
  58. package/dist/core/validate.js +12 -1
  59. package/dist/core/validate.js.map +1 -1
  60. package/dist/core/validators/contracts.d.ts.map +1 -1
  61. package/dist/core/validators/contracts.js +45 -18
  62. package/dist/core/validators/contracts.js.map +1 -1
  63. package/dist/core/validators/scenario.d.ts.map +1 -1
  64. package/dist/core/validators/scenario.js +2 -15
  65. package/dist/core/validators/scenario.js.map +1 -1
  66. package/dist/core/validators/spec.js +1 -1
  67. package/dist/core/validators/spec.js.map +1 -1
  68. package/dist/core/validators/traceability.d.ts.map +1 -1
  69. package/dist/core/validators/traceability.js +66 -34
  70. package/dist/core/validators/traceability.js.map +1 -1
  71. package/dist/index.cjs +869 -473
  72. package/dist/index.cjs.map +1 -1
  73. package/dist/index.d.cts +37 -12
  74. package/dist/index.mjs +869 -473
  75. package/dist/index.mjs.map +1 -1
  76. package/dist/tsconfig.tsbuildinfo +1 -1
  77. package/package.json +2 -1
@@ -178,7 +178,7 @@ function report(copied, skipped, dryRun, label) {
178
178
 
179
179
  // src/cli/commands/report.ts
180
180
  var import_promises16 = require("fs/promises");
181
- var import_node_path14 = __toESM(require("path"), 1);
181
+ var import_node_path15 = __toESM(require("path"), 1);
182
182
 
183
183
  // src/core/config.ts
184
184
  var import_promises2 = require("fs/promises");
@@ -209,8 +209,9 @@ var defaultConfig = {
209
209
  },
210
210
  traceability: {
211
211
  brMustHaveSc: true,
212
- scMustTouchContracts: true,
213
212
  scMustHaveTest: true,
213
+ testFileGlobs: [],
214
+ testFileExcludeGlobs: [],
214
215
  scNoTestSeverity: "error",
215
216
  allowOrphanContracts: false,
216
217
  unknownContractIdSeverity: "error"
@@ -384,13 +385,6 @@ function normalizeValidation(raw, configPath, issues) {
384
385
  configPath,
385
386
  issues
386
387
  ),
387
- scMustTouchContracts: readBoolean(
388
- traceabilityRaw?.scMustTouchContracts,
389
- base.traceability.scMustTouchContracts,
390
- "validation.traceability.scMustTouchContracts",
391
- configPath,
392
- issues
393
- ),
394
388
  scMustHaveTest: readBoolean(
395
389
  traceabilityRaw?.scMustHaveTest,
396
390
  base.traceability.scMustHaveTest,
@@ -398,6 +392,20 @@ function normalizeValidation(raw, configPath, issues) {
398
392
  configPath,
399
393
  issues
400
394
  ),
395
+ testFileGlobs: readStringArray(
396
+ traceabilityRaw?.testFileGlobs,
397
+ base.traceability.testFileGlobs,
398
+ "validation.traceability.testFileGlobs",
399
+ configPath,
400
+ issues
401
+ ),
402
+ testFileExcludeGlobs: readStringArray(
403
+ traceabilityRaw?.testFileExcludeGlobs,
404
+ base.traceability.testFileExcludeGlobs,
405
+ "validation.traceability.testFileExcludeGlobs",
406
+ configPath,
407
+ issues
408
+ ),
401
409
  scNoTestSeverity: readTraceabilitySeverity(
402
410
  traceabilityRaw?.scNoTestSeverity,
403
411
  base.traceability.scNoTestSeverity,
@@ -531,7 +539,11 @@ function isRecord(value) {
531
539
 
532
540
  // src/core/report.ts
533
541
  var import_promises15 = require("fs/promises");
534
- var import_node_path13 = __toESM(require("path"), 1);
542
+ var import_node_path14 = __toESM(require("path"), 1);
543
+
544
+ // src/core/contractIndex.ts
545
+ var import_promises6 = require("fs/promises");
546
+ var import_node_path7 = __toESM(require("path"), 1);
535
547
 
536
548
  // src/core/discovery.ts
537
549
  var import_promises5 = require("fs/promises");
@@ -539,6 +551,7 @@ var import_promises5 = require("fs/promises");
539
551
  // src/core/fs.ts
540
552
  var import_promises3 = require("fs/promises");
541
553
  var import_node_path5 = __toESM(require("path"), 1);
554
+ var import_fast_glob = __toESM(require("fast-glob"), 1);
542
555
  var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
543
556
  "node_modules",
544
557
  ".git",
@@ -560,6 +573,18 @@ async function collectFiles(root, options = {}) {
560
573
  await walk(root, root, ignoreDirs, extensions, entries);
561
574
  return entries;
562
575
  }
576
+ async function collectFilesByGlobs(root, options) {
577
+ if (options.globs.length === 0) {
578
+ return [];
579
+ }
580
+ return (0, import_fast_glob.default)(options.globs, {
581
+ cwd: root,
582
+ ignore: options.ignore ?? [],
583
+ onlyFiles: true,
584
+ absolute: true,
585
+ unique: true
586
+ });
587
+ }
563
588
  async function walk(base, current, ignoreDirs, extensions, out) {
564
589
  const items = await (0, import_promises3.readdir)(current, { withFileTypes: true });
565
590
  for (const item of items) {
@@ -671,15 +696,66 @@ async function exists3(target) {
671
696
  }
672
697
  }
673
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
+ collectDataContractFiles(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
+
674
750
  // src/core/ids.ts
675
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
751
+ var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
676
752
  var STRICT_ID_PATTERNS = {
677
753
  SPEC: /\bSPEC-\d{4}\b/g,
678
754
  BR: /\bBR-\d{4}\b/g,
679
755
  SC: /\bSC-\d{4}\b/g,
680
756
  UI: /\bUI-\d{4}\b/g,
681
757
  API: /\bAPI-\d{4}\b/g,
682
- DATA: /\bDATA-\d{4}\b/g,
758
+ DB: /\bDB-\d{4}\b/g,
683
759
  ADR: /\bADR-\d{4}\b/g
684
760
  };
685
761
  var LOOSE_ID_PATTERNS = {
@@ -688,7 +764,7 @@ var LOOSE_ID_PATTERNS = {
688
764
  SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
689
765
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
690
766
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
691
- DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi,
767
+ DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
692
768
  ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
693
769
  };
694
770
  function extractIds(text, prefix) {
@@ -724,8 +800,170 @@ function isValidId(value, prefix) {
724
800
  return strict.test(value);
725
801
  }
726
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
+
727
964
  // src/core/traceability.ts
728
- var import_promises6 = require("fs/promises");
965
+ var import_promises7 = require("fs/promises");
966
+ var import_node_path8 = __toESM(require("path"), 1);
729
967
 
730
968
  // src/core/gherkin/parse.ts
731
969
  var import_gherkin = require("@cucumber/gherkin");
@@ -758,7 +996,7 @@ var SC_TAG_RE = /^SC-\d{4}$/;
758
996
  var BR_TAG_RE = /^BR-\d{4}$/;
759
997
  var UI_TAG_RE = /^UI-\d{4}$/;
760
998
  var API_TAG_RE = /^API-\d{4}$/;
761
- var DATA_TAG_RE = /^DATA-\d{4}$/;
999
+ var DB_TAG_RE = /^DB-\d{4}$/;
762
1000
  function parseScenarioDocument(text, uri) {
763
1001
  const { gherkinDocument, errors } = parseGherkin(text, uri);
764
1002
  if (!gherkinDocument) {
@@ -787,10 +1025,10 @@ function buildScenarioAtoms(document) {
787
1025
  return document.scenarios.map((scenario) => {
788
1026
  const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
789
1027
  const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
790
- 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)));
791
1029
  const contractIds = /* @__PURE__ */ new Set();
792
1030
  scenario.tags.forEach((tag) => {
793
- 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)) {
794
1032
  contractIds.add(tag);
795
1033
  }
796
1034
  });
@@ -798,7 +1036,7 @@ function buildScenarioAtoms(document) {
798
1036
  for (const text of collectStepTexts(step)) {
799
1037
  extractIds(text, "UI").forEach((id) => contractIds.add(id));
800
1038
  extractIds(text, "API").forEach((id) => contractIds.add(id));
801
- extractIds(text, "DATA").forEach((id) => contractIds.add(id));
1039
+ extractIds(text, "DB").forEach((id) => contractIds.add(id));
802
1040
  }
803
1041
  }
804
1042
  const atom = {
@@ -877,16 +1115,37 @@ function collectStepTexts(step) {
877
1115
  }
878
1116
  return texts;
879
1117
  }
880
- function unique2(values) {
1118
+ function unique3(values) {
881
1119
  return Array.from(new Set(values));
882
1120
  }
883
1121
 
884
1122
  // src/core/traceability.ts
885
1123
  var SC_TAG_RE2 = /^SC-\d{4}$/;
1124
+ var SC_TEST_ANNOTATION_RE = /\bQFAI:SC-(\d{4})\b/g;
1125
+ var DEFAULT_TEST_FILE_EXCLUDE_GLOBS = [
1126
+ "**/node_modules/**",
1127
+ "**/.git/**",
1128
+ "**/.qfai/**",
1129
+ "**/dist/**",
1130
+ "**/build/**",
1131
+ "**/coverage/**",
1132
+ "**/.next/**",
1133
+ "**/out/**"
1134
+ ];
1135
+ function extractAnnotatedScIds(text) {
1136
+ const ids = /* @__PURE__ */ new Set();
1137
+ for (const match of text.matchAll(SC_TEST_ANNOTATION_RE)) {
1138
+ const suffix = match[1];
1139
+ if (suffix) {
1140
+ ids.add(`SC-${suffix}`);
1141
+ }
1142
+ }
1143
+ return Array.from(ids);
1144
+ }
886
1145
  async function collectScIdsFromScenarioFiles(scenarioFiles) {
887
1146
  const scIds = /* @__PURE__ */ new Set();
888
1147
  for (const file of scenarioFiles) {
889
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1148
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
890
1149
  const { document, errors } = parseScenarioDocument(text, file);
891
1150
  if (!document || errors.length > 0) {
892
1151
  continue;
@@ -901,14 +1160,67 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
901
1160
  }
902
1161
  return scIds;
903
1162
  }
904
- async function collectScTestReferences(testsRoot) {
1163
+ async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
1164
+ const sources = /* @__PURE__ */ new Map();
1165
+ for (const file of scenarioFiles) {
1166
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
1167
+ const { document, errors } = parseScenarioDocument(text, file);
1168
+ if (!document || errors.length > 0) {
1169
+ continue;
1170
+ }
1171
+ for (const scenario of document.scenarios) {
1172
+ for (const tag of scenario.tags) {
1173
+ if (!SC_TAG_RE2.test(tag)) {
1174
+ continue;
1175
+ }
1176
+ const current = sources.get(tag) ?? /* @__PURE__ */ new Set();
1177
+ current.add(file);
1178
+ sources.set(tag, current);
1179
+ }
1180
+ }
1181
+ }
1182
+ return sources;
1183
+ }
1184
+ async function collectScTestReferences(root, globs, excludeGlobs) {
905
1185
  const refs = /* @__PURE__ */ new Map();
906
- const testFiles = await collectFiles(testsRoot, {
907
- extensions: [".ts", ".tsx", ".js", ".jsx"]
908
- });
909
- for (const file of testFiles) {
910
- const text = await (0, import_promises6.readFile)(file, "utf-8");
911
- const scIds = extractIds(text, "SC");
1186
+ const normalizedGlobs = normalizeGlobs(globs);
1187
+ const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
1188
+ const mergedExcludeGlobs = Array.from(
1189
+ /* @__PURE__ */ new Set([...DEFAULT_TEST_FILE_EXCLUDE_GLOBS, ...normalizedExcludeGlobs])
1190
+ );
1191
+ if (normalizedGlobs.length === 0) {
1192
+ return {
1193
+ refs,
1194
+ scan: {
1195
+ globs: normalizedGlobs,
1196
+ excludeGlobs: mergedExcludeGlobs,
1197
+ matchedFileCount: 0
1198
+ }
1199
+ };
1200
+ }
1201
+ let files = [];
1202
+ try {
1203
+ files = await collectFilesByGlobs(root, {
1204
+ globs: normalizedGlobs,
1205
+ ignore: mergedExcludeGlobs
1206
+ });
1207
+ } catch (error2) {
1208
+ return {
1209
+ refs,
1210
+ scan: {
1211
+ globs: normalizedGlobs,
1212
+ excludeGlobs: mergedExcludeGlobs,
1213
+ matchedFileCount: 0
1214
+ },
1215
+ error: formatError3(error2)
1216
+ };
1217
+ }
1218
+ const normalizedFiles = Array.from(
1219
+ new Set(files.map((file) => import_node_path8.default.normalize(file)))
1220
+ );
1221
+ for (const file of normalizedFiles) {
1222
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
1223
+ const scIds = extractAnnotatedScIds(text);
912
1224
  if (scIds.length === 0) {
913
1225
  continue;
914
1226
  }
@@ -918,7 +1230,14 @@ async function collectScTestReferences(testsRoot) {
918
1230
  refs.set(scId, current);
919
1231
  }
920
1232
  }
921
- return refs;
1233
+ return {
1234
+ refs,
1235
+ scan: {
1236
+ globs: normalizedGlobs,
1237
+ excludeGlobs: mergedExcludeGlobs,
1238
+ matchedFileCount: normalizedFiles.length
1239
+ }
1240
+ };
922
1241
  }
923
1242
  function buildScCoverage(scIds, refs) {
924
1243
  const sortedScIds = toSortedArray(scIds);
@@ -946,18 +1265,27 @@ function buildScCoverage(scIds, refs) {
946
1265
  function toSortedArray(values) {
947
1266
  return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
948
1267
  }
1268
+ function normalizeGlobs(globs) {
1269
+ return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1270
+ }
1271
+ function formatError3(error2) {
1272
+ if (error2 instanceof Error) {
1273
+ return error2.message;
1274
+ }
1275
+ return String(error2);
1276
+ }
949
1277
 
950
1278
  // src/core/version.ts
951
- var import_promises7 = require("fs/promises");
952
- var import_node_path7 = __toESM(require("path"), 1);
1279
+ var import_promises8 = require("fs/promises");
1280
+ var import_node_path9 = __toESM(require("path"), 1);
953
1281
  var import_node_url2 = require("url");
954
1282
  async function resolveToolVersion() {
955
- if ("0.4.0".length > 0) {
956
- return "0.4.0";
1283
+ if ("0.4.4".length > 0) {
1284
+ return "0.4.4";
957
1285
  }
958
1286
  try {
959
1287
  const packagePath = resolvePackageJsonPath();
960
- const raw = await (0, import_promises7.readFile)(packagePath, "utf-8");
1288
+ const raw = await (0, import_promises8.readFile)(packagePath, "utf-8");
961
1289
  const parsed = JSON.parse(raw);
962
1290
  const version = typeof parsed.version === "string" ? parsed.version : "";
963
1291
  return version.length > 0 ? version : "unknown";
@@ -968,71 +1296,42 @@ async function resolveToolVersion() {
968
1296
  function resolvePackageJsonPath() {
969
1297
  const base = __filename;
970
1298
  const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
971
- return import_node_path7.default.resolve(import_node_path7.default.dirname(basePath), "../../package.json");
1299
+ return import_node_path9.default.resolve(import_node_path9.default.dirname(basePath), "../../package.json");
972
1300
  }
973
1301
 
974
1302
  // src/core/validators/contracts.ts
975
- var import_promises8 = require("fs/promises");
976
- var import_node_path9 = __toESM(require("path"), 1);
1303
+ var import_promises9 = require("fs/promises");
1304
+ var import_node_path11 = __toESM(require("path"), 1);
977
1305
 
978
1306
  // src/core/contracts.ts
979
- var import_node_path8 = __toESM(require("path"), 1);
1307
+ var import_node_path10 = __toESM(require("path"), 1);
980
1308
  var import_yaml2 = require("yaml");
981
1309
  function parseStructuredContract(file, text) {
982
- const ext = import_node_path8.default.extname(file).toLowerCase();
1310
+ const ext = import_node_path10.default.extname(file).toLowerCase();
983
1311
  if (ext === ".json") {
984
1312
  return JSON.parse(text);
985
1313
  }
986
1314
  return (0, import_yaml2.parse)(text);
987
1315
  }
988
- function extractUiContractIds(doc) {
989
- const id = typeof doc.id === "string" ? doc.id : "";
990
- return extractIds(id, "UI");
991
- }
992
- function extractApiContractIds(doc) {
993
- const operationIds = /* @__PURE__ */ new Set();
994
- collectOperationIds(doc, operationIds);
995
- const ids = /* @__PURE__ */ new Set();
996
- for (const operationId of operationIds) {
997
- extractIds(operationId, "API").forEach((id) => ids.add(id));
998
- }
999
- return Array.from(ids);
1000
- }
1001
- function collectOperationIds(value, out) {
1002
- if (!value || typeof value !== "object") {
1003
- return;
1004
- }
1005
- if (Array.isArray(value)) {
1006
- for (const item of value) {
1007
- collectOperationIds(item, out);
1008
- }
1009
- return;
1010
- }
1011
- for (const [key, entry] of Object.entries(value)) {
1012
- if (key === "operationId" && typeof entry === "string") {
1013
- out.add(entry);
1014
- continue;
1015
- }
1016
- collectOperationIds(entry, out);
1017
- }
1018
- }
1019
-
1020
- // src/core/validators/contracts.ts
1021
- var SQL_DANGEROUS_PATTERNS = [
1022
- { pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
1023
- { pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
1024
- { pattern: /\bTRUNCATE\b/i, label: "TRUNCATE" },
1025
- {
1026
- pattern: /\bALTER\s+TABLE\b[\s\S]*\bDROP\b/i,
1027
- label: "ALTER TABLE ... DROP"
1316
+
1317
+ // src/core/validators/contracts.ts
1318
+ var SQL_DANGEROUS_PATTERNS = [
1319
+ { pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
1320
+ { pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
1321
+ { pattern: /\bTRUNCATE\b/i, label: "TRUNCATE" },
1322
+ {
1323
+ pattern: /\bALTER\s+TABLE\b[\s\S]*\bDROP\b/i,
1324
+ label: "ALTER TABLE ... DROP"
1028
1325
  }
1029
1326
  ];
1030
1327
  async function validateContracts(root, config) {
1031
1328
  const issues = [];
1032
1329
  const contractsRoot = resolvePath(root, config, "contractsDir");
1033
- issues.push(...await validateUiContracts(import_node_path9.default.join(contractsRoot, "ui")));
1034
- issues.push(...await validateApiContracts(import_node_path9.default.join(contractsRoot, "api")));
1035
- issues.push(...await validateDataContracts(import_node_path9.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 validateDataContracts(import_node_path11.default.join(contractsRoot, "db")));
1333
+ const contractIndex = await buildContractIndex(root, config);
1334
+ issues.push(...validateDuplicateContractIds(contractIndex));
1036
1335
  return issues;
1037
1336
  }
1038
1337
  async function validateUiContracts(uiRoot) {
@@ -1050,14 +1349,14 @@ async function validateUiContracts(uiRoot) {
1050
1349
  }
1051
1350
  const issues = [];
1052
1351
  for (const file of files) {
1053
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1352
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1054
1353
  const invalidIds = extractInvalidIds(text, [
1055
1354
  "SPEC",
1056
1355
  "BR",
1057
1356
  "SC",
1058
1357
  "UI",
1059
1358
  "API",
1060
- "DATA",
1359
+ "DB",
1061
1360
  "ADR"
1062
1361
  ]);
1063
1362
  if (invalidIds.length > 0) {
@@ -1072,32 +1371,20 @@ async function validateUiContracts(uiRoot) {
1072
1371
  )
1073
1372
  );
1074
1373
  }
1075
- let doc;
1374
+ const declaredIds = extractDeclaredContractIds(text);
1375
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
1076
1376
  try {
1077
- doc = parseStructuredContract(file, text);
1377
+ parseStructuredContract(file, stripContractDeclarationLines(text));
1078
1378
  } catch (error2) {
1079
1379
  issues.push(
1080
1380
  issue(
1081
1381
  "QFAI-CONTRACT-001",
1082
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error2)})`,
1382
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
1083
1383
  "error",
1084
1384
  file,
1085
1385
  "contracts.ui.parse"
1086
1386
  )
1087
1387
  );
1088
- continue;
1089
- }
1090
- const uiIds = extractUiContractIds(doc);
1091
- if (uiIds.length === 0) {
1092
- issues.push(
1093
- issue(
1094
- "QFAI-CONTRACT-002",
1095
- `UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
1096
- "error",
1097
- file,
1098
- "contracts.ui.id"
1099
- )
1100
- );
1101
1388
  }
1102
1389
  }
1103
1390
  return issues;
@@ -1117,14 +1404,14 @@ async function validateApiContracts(apiRoot) {
1117
1404
  }
1118
1405
  const issues = [];
1119
1406
  for (const file of files) {
1120
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1407
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1121
1408
  const invalidIds = extractInvalidIds(text, [
1122
1409
  "SPEC",
1123
1410
  "BR",
1124
1411
  "SC",
1125
1412
  "UI",
1126
1413
  "API",
1127
- "DATA",
1414
+ "DB",
1128
1415
  "ADR"
1129
1416
  ]);
1130
1417
  if (invalidIds.length > 0) {
@@ -1139,14 +1426,16 @@ async function validateApiContracts(apiRoot) {
1139
1426
  )
1140
1427
  );
1141
1428
  }
1429
+ const declaredIds = extractDeclaredContractIds(text);
1430
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "API"));
1142
1431
  let doc;
1143
1432
  try {
1144
- doc = parseStructuredContract(file, text);
1433
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
1145
1434
  } catch (error2) {
1146
1435
  issues.push(
1147
1436
  issue(
1148
1437
  "QFAI-CONTRACT-001",
1149
- `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error2)})`,
1438
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
1150
1439
  "error",
1151
1440
  file,
1152
1441
  "contracts.api.parse"
@@ -1165,18 +1454,6 @@ async function validateApiContracts(apiRoot) {
1165
1454
  )
1166
1455
  );
1167
1456
  }
1168
- const apiIds = extractApiContractIds(doc);
1169
- if (apiIds.length === 0) {
1170
- issues.push(
1171
- issue(
1172
- "QFAI-CONTRACT-002",
1173
- `API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
1174
- "error",
1175
- file,
1176
- "contracts.api.id"
1177
- )
1178
- );
1179
- }
1180
1457
  }
1181
1458
  return issues;
1182
1459
  }
@@ -1185,24 +1462,24 @@ async function validateDataContracts(dataRoot) {
1185
1462
  if (files.length === 0) {
1186
1463
  return [
1187
1464
  issue(
1188
- "QFAI-DATA-000",
1189
- "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",
1190
1467
  "info",
1191
1468
  dataRoot,
1192
- "contracts.data.files"
1469
+ "contracts.db.files"
1193
1470
  )
1194
1471
  ];
1195
1472
  }
1196
1473
  const issues = [];
1197
1474
  for (const file of files) {
1198
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1475
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1199
1476
  const invalidIds = extractInvalidIds(text, [
1200
1477
  "SPEC",
1201
1478
  "BR",
1202
1479
  "SC",
1203
1480
  "UI",
1204
1481
  "API",
1205
- "DATA",
1482
+ "DB",
1206
1483
  "ADR"
1207
1484
  ]);
1208
1485
  if (invalidIds.length > 0) {
@@ -1217,6 +1494,8 @@ async function validateDataContracts(dataRoot) {
1217
1494
  )
1218
1495
  );
1219
1496
  }
1497
+ const declaredIds = extractDeclaredContractIds(text);
1498
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "DB"));
1220
1499
  issues.push(...lintSql(text, file));
1221
1500
  }
1222
1501
  return issues;
@@ -1227,21 +1506,87 @@ function lintSql(text, file) {
1227
1506
  if (pattern.test(text)) {
1228
1507
  issues.push(
1229
1508
  issue(
1230
- "QFAI-DATA-001",
1509
+ "QFAI-DB-001",
1231
1510
  `\u5371\u967A\u306A SQL \u64CD\u4F5C\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059: ${label}`,
1232
1511
  "warning",
1233
1512
  file,
1234
- "contracts.data.sql"
1513
+ "contracts.db.sql"
1235
1514
  )
1236
1515
  );
1237
1516
  }
1238
1517
  }
1239
1518
  return issues;
1240
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
+ }
1241
1586
  function hasOpenApi(doc) {
1242
1587
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
1243
1588
  }
1244
- function formatError3(error2) {
1589
+ function formatError4(error2) {
1245
1590
  if (error2 instanceof Error) {
1246
1591
  return error2.message;
1247
1592
  }
@@ -1266,8 +1611,8 @@ function issue(code, message, severity, file, rule, refs) {
1266
1611
  }
1267
1612
 
1268
1613
  // src/core/validators/delta.ts
1269
- var import_promises9 = require("fs/promises");
1270
- var import_node_path10 = __toESM(require("path"), 1);
1614
+ var import_promises10 = require("fs/promises");
1615
+ var import_node_path12 = __toESM(require("path"), 1);
1271
1616
  var SECTION_RE = /^##\s+変更区分/m;
1272
1617
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1273
1618
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1277,268 +1622,87 @@ async function validateDeltas(root, config) {
1277
1622
  const specsRoot = resolvePath(root, config, "specsDir");
1278
1623
  const packs = await collectSpecPackDirs(specsRoot);
1279
1624
  if (packs.length === 0) {
1280
- return [];
1281
- }
1282
- const issues = [];
1283
- for (const pack of packs) {
1284
- const deltaPath = import_node_path10.default.join(pack, "delta.md");
1285
- let text;
1286
- try {
1287
- text = await (0, import_promises9.readFile)(deltaPath, "utf-8");
1288
- } catch (error2) {
1289
- if (isMissingFileError2(error2)) {
1290
- issues.push(
1291
- issue2(
1292
- "QFAI-DELTA-001",
1293
- "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1294
- "error",
1295
- deltaPath,
1296
- "delta.exists"
1297
- )
1298
- );
1299
- continue;
1300
- }
1301
- throw error2;
1302
- }
1303
- const hasSection = SECTION_RE.test(text);
1304
- const hasCompatibility = COMPAT_LINE_RE.test(text);
1305
- const hasChange = CHANGE_LINE_RE.test(text);
1306
- if (!hasSection || !hasCompatibility || !hasChange) {
1307
- issues.push(
1308
- issue2(
1309
- "QFAI-DELTA-002",
1310
- "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
1311
- "error",
1312
- deltaPath,
1313
- "delta.section"
1314
- )
1315
- );
1316
- continue;
1317
- }
1318
- const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
1319
- const changeChecked = CHANGE_CHECKED_RE.test(text);
1320
- if (compatibilityChecked === changeChecked) {
1321
- issues.push(
1322
- issue2(
1323
- "QFAI-DELTA-003",
1324
- "delta.md \u306E\u5909\u66F4\u533A\u5206\u306F\u3069\u3061\u3089\u304B1\u3064\u3060\u3051\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4E21\u65B9ON/\u4E21\u65B9OFF\u306F\u7121\u52B9\u3067\u3059\uFF09\u3002",
1325
- "error",
1326
- deltaPath,
1327
- "delta.classification"
1328
- )
1329
- );
1330
- }
1331
- }
1332
- return issues;
1333
- }
1334
- function isMissingFileError2(error2) {
1335
- if (!error2 || typeof error2 !== "object") {
1336
- return false;
1337
- }
1338
- return error2.code === "ENOENT";
1339
- }
1340
- function issue2(code, message, severity, file, rule, refs) {
1341
- const issue7 = {
1342
- code,
1343
- severity,
1344
- message
1345
- };
1346
- if (file) {
1347
- issue7.file = file;
1348
- }
1349
- if (rule) {
1350
- issue7.rule = rule;
1351
- }
1352
- if (refs && refs.length > 0) {
1353
- issue7.refs = refs;
1354
- }
1355
- return issue7;
1356
- }
1357
-
1358
- // src/core/validators/ids.ts
1359
- var import_promises11 = require("fs/promises");
1360
- var import_node_path12 = __toESM(require("path"), 1);
1361
-
1362
- // src/core/contractIndex.ts
1363
- var import_promises10 = require("fs/promises");
1364
- var import_node_path11 = __toESM(require("path"), 1);
1365
- async function buildContractIndex(root, config) {
1366
- const contractsRoot = resolvePath(root, config, "contractsDir");
1367
- const uiRoot = import_node_path11.default.join(contractsRoot, "ui");
1368
- const apiRoot = import_node_path11.default.join(contractsRoot, "api");
1369
- const dataRoot = import_node_path11.default.join(contractsRoot, "db");
1370
- const [uiFiles, apiFiles, dataFiles] = await Promise.all([
1371
- collectUiContractFiles(uiRoot),
1372
- collectApiContractFiles(apiRoot),
1373
- collectDataContractFiles(dataRoot)
1374
- ]);
1375
- const index = {
1376
- ids: /* @__PURE__ */ new Set(),
1377
- idToFiles: /* @__PURE__ */ new Map(),
1378
- files: { ui: uiFiles, api: apiFiles, data: dataFiles },
1379
- structuredParseFailedFiles: /* @__PURE__ */ new Set()
1380
- };
1381
- await indexUiContracts(uiFiles, index);
1382
- await indexApiContracts(apiFiles, index);
1383
- await indexDataContracts(dataFiles, index);
1384
- return index;
1385
- }
1386
- async function indexUiContracts(files, index) {
1387
- for (const file of files) {
1388
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1389
- try {
1390
- const doc = parseStructuredContract(file, text);
1391
- extractUiContractIds(doc).forEach((id) => record(index, id, file));
1392
- } catch {
1393
- index.structuredParseFailedFiles.add(file);
1394
- extractIds(text, "UI").forEach((id) => record(index, id, file));
1395
- }
1396
- }
1397
- }
1398
- async function indexApiContracts(files, index) {
1399
- for (const file of files) {
1400
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1401
- try {
1402
- const doc = parseStructuredContract(file, text);
1403
- extractApiContractIds(doc).forEach((id) => record(index, id, file));
1404
- } catch {
1405
- index.structuredParseFailedFiles.add(file);
1406
- extractIds(text, "API").forEach((id) => record(index, id, file));
1407
- }
1408
- }
1409
- }
1410
- async function indexDataContracts(files, index) {
1411
- for (const file of files) {
1412
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1413
- extractIds(text, "DATA").forEach((id) => record(index, id, file));
1414
- }
1415
- }
1416
- function record(index, id, file) {
1417
- index.ids.add(id);
1418
- const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
1419
- current.add(file);
1420
- index.idToFiles.set(id, current);
1421
- }
1422
-
1423
- // src/core/parse/markdown.ts
1424
- var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1425
- function parseHeadings(md) {
1426
- const lines = md.split(/\r?\n/);
1427
- const headings = [];
1428
- for (let i = 0; i < lines.length; i++) {
1429
- const line = lines[i] ?? "";
1430
- const match = line.match(HEADING_RE);
1431
- if (!match) continue;
1432
- const levelToken = match[1];
1433
- const title = match[2];
1434
- if (!levelToken || !title) continue;
1435
- headings.push({
1436
- level: levelToken.length,
1437
- title: title.trim(),
1438
- line: i + 1
1439
- });
1440
- }
1441
- return headings;
1442
- }
1443
- function extractH2Sections(md) {
1444
- const lines = md.split(/\r?\n/);
1445
- const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1446
- const sections = /* @__PURE__ */ new Map();
1447
- for (let i = 0; i < headings.length; i++) {
1448
- const current = headings[i];
1449
- if (!current) continue;
1450
- const next = headings[i + 1];
1451
- const startLine = current.line + 1;
1452
- const endLine = (next?.line ?? lines.length + 1) - 1;
1453
- const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1454
- sections.set(current.title.trim(), {
1455
- title: current.title.trim(),
1456
- startLine,
1457
- endLine,
1458
- body
1459
- });
1460
- }
1461
- return sections;
1462
- }
1463
-
1464
- // src/core/parse/spec.ts
1465
- var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1466
- var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1467
- var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1468
- var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1469
- var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1470
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1471
- function parseSpec(md, file) {
1472
- const headings = parseHeadings(md);
1473
- const h1 = headings.find((heading) => heading.level === 1);
1474
- const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1475
- const sections = extractH2Sections(md);
1476
- const sectionNames = new Set(Array.from(sections.keys()));
1477
- const brSection = sections.get(BR_SECTION_TITLE);
1478
- const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1479
- const startLine = brSection?.startLine ?? 1;
1480
- const brs = [];
1481
- const brsWithoutPriority = [];
1482
- const brsWithInvalidPriority = [];
1483
- for (let i = 0; i < brLines.length; i++) {
1484
- const lineText = brLines[i] ?? "";
1485
- const lineNumber = startLine + i;
1486
- const validMatch = lineText.match(BR_LINE_RE);
1487
- if (validMatch) {
1488
- const id = validMatch[1];
1489
- const priority = validMatch[2];
1490
- const text = validMatch[3];
1491
- if (!id || !priority || !text) continue;
1492
- brs.push({
1493
- id,
1494
- priority,
1495
- text: text.trim(),
1496
- line: lineNumber
1497
- });
1498
- continue;
1499
- }
1500
- const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1501
- if (anyPriorityMatch) {
1502
- const id = anyPriorityMatch[1];
1503
- const priority = anyPriorityMatch[2];
1504
- const text = anyPriorityMatch[3];
1505
- if (!id || !priority || !text) continue;
1506
- if (!VALID_PRIORITIES.has(priority)) {
1507
- brsWithInvalidPriority.push({
1508
- id,
1509
- priority,
1510
- text: text.trim(),
1511
- line: lineNumber
1512
- });
1625
+ return [];
1626
+ }
1627
+ const issues = [];
1628
+ for (const pack of packs) {
1629
+ const deltaPath = import_node_path12.default.join(pack, "delta.md");
1630
+ let text;
1631
+ try {
1632
+ text = await (0, import_promises10.readFile)(deltaPath, "utf-8");
1633
+ } catch (error2) {
1634
+ if (isMissingFileError2(error2)) {
1635
+ issues.push(
1636
+ issue2(
1637
+ "QFAI-DELTA-001",
1638
+ "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1639
+ "error",
1640
+ deltaPath,
1641
+ "delta.exists"
1642
+ )
1643
+ );
1644
+ continue;
1513
1645
  }
1646
+ throw error2;
1647
+ }
1648
+ const hasSection = SECTION_RE.test(text);
1649
+ const hasCompatibility = COMPAT_LINE_RE.test(text);
1650
+ const hasChange = CHANGE_LINE_RE.test(text);
1651
+ if (!hasSection || !hasCompatibility || !hasChange) {
1652
+ issues.push(
1653
+ issue2(
1654
+ "QFAI-DELTA-002",
1655
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
1656
+ "error",
1657
+ deltaPath,
1658
+ "delta.section"
1659
+ )
1660
+ );
1514
1661
  continue;
1515
1662
  }
1516
- const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1517
- if (noPriorityMatch) {
1518
- const id = noPriorityMatch[1];
1519
- const text = noPriorityMatch[2];
1520
- if (!id || !text) continue;
1521
- brsWithoutPriority.push({
1522
- id,
1523
- text: text.trim(),
1524
- line: lineNumber
1525
- });
1663
+ const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
1664
+ const changeChecked = CHANGE_CHECKED_RE.test(text);
1665
+ if (compatibilityChecked === changeChecked) {
1666
+ issues.push(
1667
+ issue2(
1668
+ "QFAI-DELTA-003",
1669
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u306F\u3069\u3061\u3089\u304B1\u3064\u3060\u3051\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4E21\u65B9ON/\u4E21\u65B9OFF\u306F\u7121\u52B9\u3067\u3059\uFF09\u3002",
1670
+ "error",
1671
+ deltaPath,
1672
+ "delta.classification"
1673
+ )
1674
+ );
1526
1675
  }
1527
1676
  }
1528
- const parsed = {
1529
- file,
1530
- sections: sectionNames,
1531
- brs,
1532
- brsWithoutPriority,
1533
- brsWithInvalidPriority
1677
+ return issues;
1678
+ }
1679
+ function isMissingFileError2(error2) {
1680
+ if (!error2 || typeof error2 !== "object") {
1681
+ return false;
1682
+ }
1683
+ return error2.code === "ENOENT";
1684
+ }
1685
+ function issue2(code, message, severity, file, rule, refs) {
1686
+ const issue7 = {
1687
+ code,
1688
+ severity,
1689
+ message
1534
1690
  };
1535
- if (specId) {
1536
- parsed.specId = specId;
1691
+ if (file) {
1692
+ issue7.file = file;
1537
1693
  }
1538
- return parsed;
1694
+ if (rule) {
1695
+ issue7.rule = rule;
1696
+ }
1697
+ if (refs && refs.length > 0) {
1698
+ issue7.refs = refs;
1699
+ }
1700
+ return issue7;
1539
1701
  }
1540
1702
 
1541
1703
  // src/core/validators/ids.ts
1704
+ var import_promises11 = require("fs/promises");
1705
+ var import_node_path13 = __toESM(require("path"), 1);
1542
1706
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1543
1707
  async function validateDefinedIds(root, config) {
1544
1708
  const issues = [];
@@ -1604,7 +1768,7 @@ function recordId(out, id, file) {
1604
1768
  }
1605
1769
  function formatFileList(files, root) {
1606
1770
  return files.map((file) => {
1607
- const relative = import_node_path12.default.relative(root, file);
1771
+ const relative = import_node_path13.default.relative(root, file);
1608
1772
  return relative.length > 0 ? relative : file;
1609
1773
  }).join(", ");
1610
1774
  }
@@ -1633,7 +1797,6 @@ var WHEN_PATTERN = /\bWhen\b/;
1633
1797
  var THEN_PATTERN = /\bThen\b/;
1634
1798
  var SC_TAG_RE4 = /^SC-\d{4}$/;
1635
1799
  var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1636
- var BR_TAG_RE2 = /^BR-\d{4}$/;
1637
1800
  async function validateScenarios(root, config) {
1638
1801
  const specsRoot = resolvePath(root, config, "specsDir");
1639
1802
  const entries = await collectSpecEntries(specsRoot);
@@ -1682,7 +1845,7 @@ function validateScenarioContent(text, file) {
1682
1845
  "SC",
1683
1846
  "UI",
1684
1847
  "API",
1685
- "DATA",
1848
+ "DB",
1686
1849
  "ADR"
1687
1850
  ]);
1688
1851
  if (invalidIds.length > 0) {
@@ -1713,17 +1876,7 @@ function validateScenarioContent(text, file) {
1713
1876
  const featureSpecTags = document.featureTags.filter(
1714
1877
  (tag) => SPEC_TAG_RE2.test(tag)
1715
1878
  );
1716
- if (featureSpecTags.length === 0) {
1717
- issues.push(
1718
- issue4(
1719
- "QFAI-SC-009",
1720
- "Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1721
- "error",
1722
- file,
1723
- "scenario.featureSpec"
1724
- )
1725
- );
1726
- } else if (featureSpecTags.length > 1) {
1879
+ if (featureSpecTags.length > 1) {
1727
1880
  issues.push(
1728
1881
  issue4(
1729
1882
  "QFAI-SC-009",
@@ -1751,17 +1904,6 @@ function validateScenarioContent(text, file) {
1751
1904
  )
1752
1905
  );
1753
1906
  }
1754
- if (document.scenarios.length > 1) {
1755
- issues.push(
1756
- issue4(
1757
- "QFAI-SC-011",
1758
- `Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
1759
- "error",
1760
- file,
1761
- "scenario.single"
1762
- )
1763
- );
1764
- }
1765
1907
  for (const scenario of document.scenarios) {
1766
1908
  if (scenario.tags.length === 0) {
1767
1909
  issues.push(
@@ -1782,12 +1924,6 @@ function validateScenarioContent(text, file) {
1782
1924
  } else if (scTags.length > 1) {
1783
1925
  missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1784
1926
  }
1785
- if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
1786
- missingTags.push("SPEC");
1787
- }
1788
- if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
1789
- missingTags.push("BR");
1790
- }
1791
1927
  if (missingTags.length > 0) {
1792
1928
  issues.push(
1793
1929
  issue4(
@@ -1907,7 +2043,7 @@ function validateSpecContent(text, file, requiredSections) {
1907
2043
  "SC",
1908
2044
  "UI",
1909
2045
  "API",
1910
- "DATA",
2046
+ "DB",
1911
2047
  "ADR"
1912
2048
  ]);
1913
2049
  if (invalidIds.length > 0) {
@@ -2022,9 +2158,8 @@ function isMissingFileError4(error2) {
2022
2158
 
2023
2159
  // src/core/validators/traceability.ts
2024
2160
  var import_promises14 = require("fs/promises");
2025
- var SC_TAG_RE5 = /^SC-\d{4}$/;
2026
2161
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
2027
- var BR_TAG_RE3 = /^BR-\d{4}$/;
2162
+ var BR_TAG_RE2 = /^BR-\d{4}$/;
2028
2163
  async function validateTraceability(root, config) {
2029
2164
  const issues = [];
2030
2165
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -2037,8 +2172,7 @@ async function validateTraceability(root, config) {
2037
2172
  const brIdsInSpecs = /* @__PURE__ */ new Set();
2038
2173
  const brIdsInScenarios = /* @__PURE__ */ new Set();
2039
2174
  const scIdsInScenarios = /* @__PURE__ */ new Set();
2040
- const scenarioContractIds = /* @__PURE__ */ new Set();
2041
- const scWithContracts = /* @__PURE__ */ new Set();
2175
+ const specContractIds = /* @__PURE__ */ new Set();
2042
2176
  const specToBrIds = /* @__PURE__ */ new Map();
2043
2177
  const contractIndex = await buildContractIndex(root, config);
2044
2178
  const contractIds = contractIndex.ids;
@@ -2051,19 +2185,60 @@ async function validateTraceability(root, config) {
2051
2185
  }
2052
2186
  const brIds = parsed.brs.map((br) => br.id);
2053
2187
  brIds.forEach((id) => brIdsInSpecs.add(id));
2054
- const referencedContractIds = /* @__PURE__ */ new Set([
2055
- ...extractIds(text, "UI"),
2056
- ...extractIds(text, "API"),
2057
- ...extractIds(text, "DATA")
2058
- ]);
2059
- const unknownContractIds = Array.from(referencedContractIds).filter(
2188
+ if (parsed.specId) {
2189
+ const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
2190
+ brIds.forEach((id) => current.add(id));
2191
+ specToBrIds.set(parsed.specId, current);
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(
2060
2235
  (id) => !contractIds.has(id)
2061
2236
  );
2062
2237
  if (unknownContractIds.length > 0) {
2063
2238
  issues.push(
2064
2239
  issue6(
2065
- "QFAI-TRACE-009",
2066
- `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
2240
+ "QFAI-TRACE-021",
2241
+ `Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
2067
2242
  ", "
2068
2243
  )}`,
2069
2244
  "error",
@@ -2073,11 +2248,6 @@ async function validateTraceability(root, config) {
2073
2248
  )
2074
2249
  );
2075
2250
  }
2076
- if (parsed.specId) {
2077
- const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
2078
- brIds.forEach((id) => current.add(id));
2079
- specToBrIds.set(parsed.specId, current);
2080
- }
2081
2251
  }
2082
2252
  for (const file of scenarioFiles) {
2083
2253
  const text = await (0, import_promises14.readFile)(file, "utf-8");
@@ -2087,20 +2257,42 @@ async function validateTraceability(root, config) {
2087
2257
  continue;
2088
2258
  }
2089
2259
  const atoms = buildScenarioAtoms(document);
2260
+ const scIdsInFile = /* @__PURE__ */ new Set();
2090
2261
  for (const [index, scenario] of document.scenarios.entries()) {
2091
2262
  const atom = atoms[index];
2092
2263
  if (!atom) {
2093
2264
  continue;
2094
2265
  }
2095
2266
  const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
2096
- const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
2097
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE5.test(tag));
2098
- brTags.forEach((id) => brIdsInScenarios.add(id));
2099
- scTags.forEach((id) => scIdsInScenarios.add(id));
2100
- atom.contractIds.forEach((id) => scenarioContractIds.add(id));
2101
- if (atom.contractIds.length > 0) {
2102
- scTags.forEach((id) => scWithContracts.add(id));
2267
+ const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
2268
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
2269
+ if (specTags.length === 0) {
2270
+ issues.push(
2271
+ issue6(
2272
+ "QFAI-TRACE-014",
2273
+ `Scenario \u304C SPEC \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
2274
+ "error",
2275
+ file,
2276
+ "traceability.scenarioSpecRequired"
2277
+ )
2278
+ );
2279
+ }
2280
+ if (brTags.length === 0) {
2281
+ issues.push(
2282
+ issue6(
2283
+ "QFAI-TRACE-015",
2284
+ `Scenario \u304C BR \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
2285
+ "error",
2286
+ file,
2287
+ "traceability.scenarioBrRequired"
2288
+ )
2289
+ );
2103
2290
  }
2291
+ brTags.forEach((id) => brIdsInScenarios.add(id));
2292
+ scTags.forEach((id) => {
2293
+ scIdsInScenarios.add(id);
2294
+ scIdsInFile.add(id);
2295
+ });
2104
2296
  const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
2105
2297
  if (unknownSpecIds.length > 0) {
2106
2298
  issues.push(
@@ -2174,6 +2366,22 @@ async function validateTraceability(root, config) {
2174
2366
  }
2175
2367
  }
2176
2368
  }
2369
+ if (scIdsInFile.size !== 1) {
2370
+ const invalidScIds = Array.from(scIdsInFile).sort(
2371
+ (a, b) => a.localeCompare(b)
2372
+ );
2373
+ const detail = invalidScIds.length === 0 ? "SC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" : `\u8907\u6570\u306E SC \u304C\u5B58\u5728\u3057\u307E\u3059: ${invalidScIds.join(", ")}`;
2374
+ issues.push(
2375
+ issue6(
2376
+ "QFAI-TRACE-012",
2377
+ `Spec entry \u304C Spec:SC=1:1 \u3092\u6E80\u305F\u3057\u3066\u3044\u307E\u305B\u3093: ${detail}`,
2378
+ "error",
2379
+ file,
2380
+ "traceability.specScOneToOne",
2381
+ invalidScIds
2382
+ )
2383
+ );
2384
+ }
2177
2385
  }
2178
2386
  if (upstreamIds.size === 0) {
2179
2387
  return [
@@ -2203,40 +2411,62 @@ async function validateTraceability(root, config) {
2203
2411
  );
2204
2412
  }
2205
2413
  }
2206
- if (config.validation.traceability.scMustTouchContracts && scIdsInScenarios.size > 0) {
2207
- const scWithoutContracts = Array.from(scIdsInScenarios).filter(
2208
- (id) => !scWithContracts.has(id)
2414
+ const scRefsResult = await collectScTestReferences(
2415
+ root,
2416
+ config.validation.traceability.testFileGlobs,
2417
+ config.validation.traceability.testFileExcludeGlobs
2418
+ );
2419
+ const scTestRefs = scRefsResult.refs;
2420
+ const testFileScan = scRefsResult.scan;
2421
+ const hasScenarios = scIdsInScenarios.size > 0;
2422
+ const hasGlobConfig = testFileScan.globs.length > 0;
2423
+ const hasMatchedTests = testFileScan.matchedFileCount > 0;
2424
+ if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
2425
+ const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
2426
+ issues.push(
2427
+ issue6(
2428
+ "QFAI-TRACE-013",
2429
+ `\u30C6\u30B9\u30C8\u63A2\u7D22 glob \u304C\u672A\u8A2D\u5B9A/\u4E0D\u6B63/\u4E00\u81F4\u30D5\u30A1\u30A4\u30EB0\u306E\u305F\u3081 SC\u2192Test \u3092\u5224\u5B9A\u3067\u304D\u307E\u305B\u3093\u3002${detail}`,
2430
+ "error",
2431
+ testsRoot,
2432
+ "traceability.testFileGlobs"
2433
+ )
2434
+ );
2435
+ } else {
2436
+ if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2437
+ const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2438
+ const refs = scTestRefs.get(id);
2439
+ return !refs || refs.size === 0;
2440
+ });
2441
+ if (scWithoutTests.length > 0) {
2442
+ issues.push(
2443
+ issue6(
2444
+ "QFAI-TRACE-010",
2445
+ `SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(
2446
+ ", "
2447
+ )}\u3002testFileGlobs \u306B\u4E00\u81F4\u3059\u308B\u30C6\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\u3078 QFAI:SC-xxxx \u3092\u8A18\u8F09\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
2448
+ config.validation.traceability.scNoTestSeverity,
2449
+ testsRoot,
2450
+ "traceability.scMustHaveTest",
2451
+ scWithoutTests
2452
+ )
2453
+ );
2454
+ }
2455
+ }
2456
+ const unknownScIds = Array.from(scTestRefs.keys()).filter(
2457
+ (id) => !scIdsInScenarios.has(id)
2209
2458
  );
2210
- if (scWithoutContracts.length > 0) {
2459
+ if (unknownScIds.length > 0) {
2211
2460
  issues.push(
2212
2461
  issue6(
2213
- "QFAI_TRACE_SC_NO_CONTRACT",
2214
- `SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
2462
+ "QFAI-TRACE-011",
2463
+ `\u30C6\u30B9\u30C8\u304C\u672A\u77E5\u306E SC \u3092\u30A2\u30CE\u30C6\u30FC\u30B7\u30E7\u30F3\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownScIds.join(
2215
2464
  ", "
2216
2465
  )}`,
2217
2466
  "error",
2218
- specsRoot,
2219
- "traceability.scMustTouchContracts",
2220
- scWithoutContracts
2221
- )
2222
- );
2223
- }
2224
- }
2225
- if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2226
- const scTestRefs = await collectScTestReferences(testsRoot);
2227
- const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2228
- const refs = scTestRefs.get(id);
2229
- return !refs || refs.size === 0;
2230
- });
2231
- if (scWithoutTests.length > 0) {
2232
- issues.push(
2233
- issue6(
2234
- "QFAI-TRACE-010",
2235
- `SC \u304C tests \u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(", ")}\u3002tests/ \u914D\u4E0B\u306E\u30C6\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\uFF08.ts/.tsx/.js/.jsx\uFF09\u306B SC ID \u3092\u30B3\u30E1\u30F3\u30C8\u307E\u305F\u306F\u30B3\u30FC\u30C9\u3067\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
2236
- config.validation.traceability.scNoTestSeverity,
2237
2467
  testsRoot,
2238
- "traceability.scMustHaveTest",
2239
- scWithoutTests
2468
+ "traceability.scUnknownInTests",
2469
+ unknownScIds
2240
2470
  )
2241
2471
  );
2242
2472
  }
@@ -2244,16 +2474,16 @@ async function validateTraceability(root, config) {
2244
2474
  if (!config.validation.traceability.allowOrphanContracts) {
2245
2475
  if (contractIds.size > 0) {
2246
2476
  const orphanContracts = Array.from(contractIds).filter(
2247
- (id) => !scenarioContractIds.has(id)
2477
+ (id) => !specContractIds.has(id)
2248
2478
  );
2249
2479
  if (orphanContracts.length > 0) {
2250
2480
  issues.push(
2251
2481
  issue6(
2252
- "QFAI_CONTRACT_ORPHAN",
2253
- `\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(", ")}`,
2254
2484
  "error",
2255
2485
  specsRoot,
2256
- "traceability.allowOrphanContracts",
2486
+ "traceability.contractCoverage",
2257
2487
  orphanContracts
2258
2488
  )
2259
2489
  );
@@ -2299,8 +2529,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2299
2529
  issues.push(
2300
2530
  issue6(
2301
2531
  "QFAI-TRACE-002",
2302
- "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
2303
- "warning",
2532
+ "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\uFF08\u53C2\u8003\u60C5\u5831\uFF09\u3002",
2533
+ "info",
2304
2534
  srcRoot,
2305
2535
  "traceability.codeReferences"
2306
2536
  )
@@ -2343,11 +2573,24 @@ async function validateProject(root, configResult) {
2343
2573
  ...await validateDefinedIds(root, config),
2344
2574
  ...await validateTraceability(root, config)
2345
2575
  ];
2576
+ const specsRoot = resolvePath(root, config, "specsDir");
2577
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
2578
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2579
+ const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
2580
+ root,
2581
+ config.validation.traceability.testFileGlobs,
2582
+ config.validation.traceability.testFileExcludeGlobs
2583
+ );
2584
+ const scCoverage = buildScCoverage(scIds, scTestRefs);
2346
2585
  const toolVersion = await resolveToolVersion();
2347
2586
  return {
2348
2587
  toolVersion,
2349
2588
  issues,
2350
- counts: countIssues(issues)
2589
+ counts: countIssues(issues),
2590
+ traceability: {
2591
+ sc: scCoverage,
2592
+ testFiles
2593
+ }
2351
2594
  };
2352
2595
  }
2353
2596
  function countIssues(issues) {
@@ -2361,16 +2604,16 @@ function countIssues(issues) {
2361
2604
  }
2362
2605
 
2363
2606
  // src/core/report.ts
2364
- var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
2607
+ var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
2365
2608
  async function createReportData(root, validation, configResult) {
2366
2609
  const resolved = configResult ?? await loadConfig(root);
2367
2610
  const config = resolved.config;
2368
2611
  const configPath = resolved.configPath;
2369
2612
  const specsRoot = resolvePath(root, config, "specsDir");
2370
2613
  const contractsRoot = resolvePath(root, config, "contractsDir");
2371
- const apiRoot = import_node_path13.default.join(contractsRoot, "api");
2372
- const uiRoot = import_node_path13.default.join(contractsRoot, "ui");
2373
- const dbRoot = import_node_path13.default.join(contractsRoot, "db");
2614
+ const apiRoot = import_node_path14.default.join(contractsRoot, "api");
2615
+ const uiRoot = import_node_path14.default.join(contractsRoot, "ui");
2616
+ const dbRoot = import_node_path14.default.join(contractsRoot, "db");
2374
2617
  const srcRoot = resolvePath(root, config, "srcDir");
2375
2618
  const testsRoot = resolvePath(root, config, "testsDir");
2376
2619
  const specFiles = await collectSpecFiles(specsRoot);
@@ -2380,6 +2623,23 @@ async function createReportData(root, validation, configResult) {
2380
2623
  ui: uiFiles,
2381
2624
  db: dbFiles
2382
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
+ );
2383
2643
  const idsByPrefix = await collectIds([
2384
2644
  ...specFiles,
2385
2645
  ...scenarioFiles,
@@ -2397,8 +2657,15 @@ async function createReportData(root, validation, configResult) {
2397
2657
  testsRoot
2398
2658
  );
2399
2659
  const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2400
- const scTestRefs = await collectScTestReferences(testsRoot);
2401
- const scCoverage = buildScCoverage(scIds, scTestRefs);
2660
+ const scRefsResult = await collectScTestReferences(
2661
+ root,
2662
+ config.validation.traceability.testFileGlobs,
2663
+ config.validation.traceability.testFileExcludeGlobs
2664
+ );
2665
+ const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
2666
+ const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
2667
+ const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
2668
+ const scSourceRecord = mapToSortedRecord(scSources);
2402
2669
  const resolvedValidation = validation ?? await validateProject(root, resolved);
2403
2670
  const version = await resolveToolVersion();
2404
2671
  return {
@@ -2423,12 +2690,24 @@ async function createReportData(root, validation, configResult) {
2423
2690
  sc: idsByPrefix.SC,
2424
2691
  ui: idsByPrefix.UI,
2425
2692
  api: idsByPrefix.API,
2426
- data: idsByPrefix.DATA
2693
+ db: idsByPrefix.DB
2427
2694
  },
2428
2695
  traceability: {
2429
2696
  upstreamIdsFound: upstreamIds.size,
2430
2697
  referencedInCodeOrTests: traceability,
2431
- sc: scCoverage
2698
+ sc: scCoverage,
2699
+ scSources: scSourceRecord,
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
+ }
2432
2711
  },
2433
2712
  issues: resolvedValidation.issues
2434
2713
  };
@@ -2457,7 +2736,7 @@ function formatReportMarkdown(data) {
2457
2736
  lines.push(formatIdLine("SC", data.ids.sc));
2458
2737
  lines.push(formatIdLine("UI", data.ids.ui));
2459
2738
  lines.push(formatIdLine("API", data.ids.api));
2460
- lines.push(formatIdLine("DATA", data.ids.data));
2739
+ lines.push(formatIdLine("DB", data.ids.db));
2461
2740
  lines.push("");
2462
2741
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
2463
2742
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
@@ -2465,14 +2744,77 @@ function formatReportMarkdown(data) {
2465
2744
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2466
2745
  );
2467
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("");
2468
2791
  lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2469
2792
  lines.push(`- total: ${data.traceability.sc.total}`);
2470
2793
  lines.push(`- covered: ${data.traceability.sc.covered}`);
2471
2794
  lines.push(`- missing: ${data.traceability.sc.missing}`);
2795
+ lines.push(
2796
+ `- testFileGlobs: ${formatList(data.traceability.testFiles.globs)}`
2797
+ );
2798
+ lines.push(
2799
+ `- testFileExcludeGlobs: ${formatList(
2800
+ data.traceability.testFiles.excludeGlobs
2801
+ )}`
2802
+ );
2803
+ lines.push(
2804
+ `- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
2805
+ );
2472
2806
  if (data.traceability.sc.missingIds.length === 0) {
2473
2807
  lines.push("- missingIds: (none)");
2474
2808
  } else {
2475
- lines.push(`- missingIds: ${data.traceability.sc.missingIds.join(", ")}`);
2809
+ const sources = data.traceability.scSources;
2810
+ const missingWithSources = data.traceability.sc.missingIds.map((id) => {
2811
+ const files = sources[id] ?? [];
2812
+ if (files.length === 0) {
2813
+ return id;
2814
+ }
2815
+ return `${id} (${files.join(", ")})`;
2816
+ });
2817
+ lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
2476
2818
  }
2477
2819
  lines.push("");
2478
2820
  lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
@@ -2491,6 +2833,20 @@ function formatReportMarkdown(data) {
2491
2833
  }
2492
2834
  }
2493
2835
  lines.push("");
2836
+ lines.push("## Spec:SC=1:1 \u9055\u53CD");
2837
+ const specScIssues = data.issues.filter(
2838
+ (item) => item.code === "QFAI-TRACE-012"
2839
+ );
2840
+ if (specScIssues.length === 0) {
2841
+ lines.push("- (none)");
2842
+ } else {
2843
+ for (const item of specScIssues) {
2844
+ const location = item.file ?? "(unknown)";
2845
+ const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
2846
+ lines.push(`- ${location}: ${refs}`);
2847
+ }
2848
+ }
2849
+ lines.push("");
2494
2850
  lines.push("## Hotspots");
2495
2851
  const hotspots = buildHotspots(data.issues);
2496
2852
  if (hotspots.length === 0) {
@@ -2505,7 +2861,7 @@ function formatReportMarkdown(data) {
2505
2861
  lines.push("");
2506
2862
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
2507
2863
  const traceIssues = data.issues.filter(
2508
- (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-")
2509
2865
  );
2510
2866
  if (traceIssues.length === 0) {
2511
2867
  lines.push("- (none)");
@@ -2535,6 +2891,33 @@ function formatReportMarkdown(data) {
2535
2891
  function formatReportJson(data) {
2536
2892
  return JSON.stringify(data, null, 2);
2537
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
+ }
2538
2921
  async function collectIds(files) {
2539
2922
  const result = {
2540
2923
  SPEC: /* @__PURE__ */ new Set(),
@@ -2542,7 +2925,7 @@ async function collectIds(files) {
2542
2925
  SC: /* @__PURE__ */ new Set(),
2543
2926
  UI: /* @__PURE__ */ new Set(),
2544
2927
  API: /* @__PURE__ */ new Set(),
2545
- DATA: /* @__PURE__ */ new Set()
2928
+ DB: /* @__PURE__ */ new Set()
2546
2929
  };
2547
2930
  for (const file of files) {
2548
2931
  const text = await (0, import_promises15.readFile)(file, "utf-8");
@@ -2557,7 +2940,7 @@ async function collectIds(files) {
2557
2940
  SC: toSortedArray2(result.SC),
2558
2941
  UI: toSortedArray2(result.UI),
2559
2942
  API: toSortedArray2(result.API),
2560
- DATA: toSortedArray2(result.DATA)
2943
+ DB: toSortedArray2(result.DB)
2561
2944
  };
2562
2945
  }
2563
2946
  async function collectUpstreamIds(files) {
@@ -2601,9 +2984,22 @@ function formatIdLine(label, values) {
2601
2984
  }
2602
2985
  return `- ${label}: ${values.join(", ")}`;
2603
2986
  }
2987
+ function formatList(values) {
2988
+ if (values.length === 0) {
2989
+ return "(none)";
2990
+ }
2991
+ return values.join(", ");
2992
+ }
2604
2993
  function toSortedArray2(values) {
2605
2994
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2606
2995
  }
2996
+ function mapToSortedRecord(values) {
2997
+ const record2 = {};
2998
+ for (const [key, files] of values.entries()) {
2999
+ record2[key] = Array.from(files).sort((a, b) => a.localeCompare(b));
3000
+ }
3001
+ return record2;
3002
+ }
2607
3003
  function buildHotspots(issues) {
2608
3004
  const map = /* @__PURE__ */ new Map();
2609
3005
  for (const issue7 of issues) {
@@ -2628,10 +3024,10 @@ function buildHotspots(issues) {
2628
3024
 
2629
3025
  // src/cli/commands/report.ts
2630
3026
  async function runReport(options) {
2631
- const root = import_node_path14.default.resolve(options.root);
3027
+ const root = import_node_path15.default.resolve(options.root);
2632
3028
  const configResult = await loadConfig(root);
2633
3029
  const input = configResult.config.output.validateJsonPath;
2634
- const inputPath = import_node_path14.default.isAbsolute(input) ? input : import_node_path14.default.resolve(root, input);
3030
+ const inputPath = import_node_path15.default.isAbsolute(input) ? input : import_node_path15.default.resolve(root, input);
2635
3031
  let validation;
2636
3032
  try {
2637
3033
  validation = await readValidationResult(inputPath);
@@ -2656,10 +3052,10 @@ async function runReport(options) {
2656
3052
  const data = await createReportData(root, validation, configResult);
2657
3053
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
2658
3054
  const outRoot = resolvePath(root, configResult.config, "outDir");
2659
- const defaultOut = options.format === "json" ? import_node_path14.default.join(outRoot, "report.json") : import_node_path14.default.join(outRoot, "report.md");
3055
+ const defaultOut = options.format === "json" ? import_node_path15.default.join(outRoot, "report.json") : import_node_path15.default.join(outRoot, "report.md");
2660
3056
  const out = options.outPath ?? defaultOut;
2661
- const outPath = import_node_path14.default.isAbsolute(out) ? out : import_node_path14.default.resolve(root, out);
2662
- await (0, import_promises16.mkdir)(import_node_path14.default.dirname(outPath), { recursive: true });
3057
+ const outPath = import_node_path15.default.isAbsolute(out) ? out : import_node_path15.default.resolve(root, out);
3058
+ await (0, import_promises16.mkdir)(import_node_path15.default.dirname(outPath), { recursive: true });
2663
3059
  await (0, import_promises16.writeFile)(outPath, `${output}
2664
3060
  `, "utf-8");
2665
3061
  info(
@@ -2702,7 +3098,7 @@ function isMissingFileError5(error2) {
2702
3098
 
2703
3099
  // src/cli/commands/validate.ts
2704
3100
  var import_promises17 = require("fs/promises");
2705
- var import_node_path15 = __toESM(require("path"), 1);
3101
+ var import_node_path16 = __toESM(require("path"), 1);
2706
3102
 
2707
3103
  // src/cli/lib/failOn.ts
2708
3104
  function shouldFail(result, failOn) {
@@ -2717,7 +3113,7 @@ function shouldFail(result, failOn) {
2717
3113
 
2718
3114
  // src/cli/commands/validate.ts
2719
3115
  async function runValidate(options) {
2720
- const root = import_node_path15.default.resolve(options.root);
3116
+ const root = import_node_path16.default.resolve(options.root);
2721
3117
  const configResult = await loadConfig(root);
2722
3118
  const result = await validateProject(root, configResult);
2723
3119
  const format = options.format ?? "text";
@@ -2766,8 +3162,8 @@ function emitGitHub(issue7) {
2766
3162
  );
2767
3163
  }
2768
3164
  async function emitJson(result, root, jsonPath) {
2769
- const abs = import_node_path15.default.isAbsolute(jsonPath) ? jsonPath : import_node_path15.default.resolve(root, jsonPath);
2770
- await (0, import_promises17.mkdir)(import_node_path15.default.dirname(abs), { recursive: true });
3165
+ const abs = import_node_path16.default.isAbsolute(jsonPath) ? jsonPath : import_node_path16.default.resolve(root, jsonPath);
3166
+ await (0, import_promises17.mkdir)(import_node_path16.default.dirname(abs), { recursive: true });
2771
3167
  await (0, import_promises17.writeFile)(abs, `${JSON.stringify(result, null, 2)}
2772
3168
  `, "utf-8");
2773
3169
  }