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
@@ -155,7 +155,7 @@ function report(copied, skipped, dryRun, label) {
155
155
 
156
156
  // src/cli/commands/report.ts
157
157
  import { mkdir as mkdir2, readFile as readFile12, writeFile } from "fs/promises";
158
- import path14 from "path";
158
+ import path15 from "path";
159
159
 
160
160
  // src/core/config.ts
161
161
  import { readFile } from "fs/promises";
@@ -186,8 +186,9 @@ var defaultConfig = {
186
186
  },
187
187
  traceability: {
188
188
  brMustHaveSc: true,
189
- scMustTouchContracts: true,
190
189
  scMustHaveTest: true,
190
+ testFileGlobs: [],
191
+ testFileExcludeGlobs: [],
191
192
  scNoTestSeverity: "error",
192
193
  allowOrphanContracts: false,
193
194
  unknownContractIdSeverity: "error"
@@ -361,13 +362,6 @@ function normalizeValidation(raw, configPath, issues) {
361
362
  configPath,
362
363
  issues
363
364
  ),
364
- scMustTouchContracts: readBoolean(
365
- traceabilityRaw?.scMustTouchContracts,
366
- base.traceability.scMustTouchContracts,
367
- "validation.traceability.scMustTouchContracts",
368
- configPath,
369
- issues
370
- ),
371
365
  scMustHaveTest: readBoolean(
372
366
  traceabilityRaw?.scMustHaveTest,
373
367
  base.traceability.scMustHaveTest,
@@ -375,6 +369,20 @@ function normalizeValidation(raw, configPath, issues) {
375
369
  configPath,
376
370
  issues
377
371
  ),
372
+ testFileGlobs: readStringArray(
373
+ traceabilityRaw?.testFileGlobs,
374
+ base.traceability.testFileGlobs,
375
+ "validation.traceability.testFileGlobs",
376
+ configPath,
377
+ issues
378
+ ),
379
+ testFileExcludeGlobs: readStringArray(
380
+ traceabilityRaw?.testFileExcludeGlobs,
381
+ base.traceability.testFileExcludeGlobs,
382
+ "validation.traceability.testFileExcludeGlobs",
383
+ configPath,
384
+ issues
385
+ ),
378
386
  scNoTestSeverity: readTraceabilitySeverity(
379
387
  traceabilityRaw?.scNoTestSeverity,
380
388
  base.traceability.scNoTestSeverity,
@@ -508,7 +516,11 @@ function isRecord(value) {
508
516
 
509
517
  // src/core/report.ts
510
518
  import { readFile as readFile11 } from "fs/promises";
511
- import path13 from "path";
519
+ import path14 from "path";
520
+
521
+ // src/core/contractIndex.ts
522
+ import { readFile as readFile2 } from "fs/promises";
523
+ import path7 from "path";
512
524
 
513
525
  // src/core/discovery.ts
514
526
  import { access as access3 } from "fs/promises";
@@ -516,6 +528,7 @@ import { access as access3 } from "fs/promises";
516
528
  // src/core/fs.ts
517
529
  import { access as access2, readdir as readdir2 } from "fs/promises";
518
530
  import path5 from "path";
531
+ import fg from "fast-glob";
519
532
  var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
520
533
  "node_modules",
521
534
  ".git",
@@ -537,6 +550,18 @@ async function collectFiles(root, options = {}) {
537
550
  await walk(root, root, ignoreDirs, extensions, entries);
538
551
  return entries;
539
552
  }
553
+ async function collectFilesByGlobs(root, options) {
554
+ if (options.globs.length === 0) {
555
+ return [];
556
+ }
557
+ return fg(options.globs, {
558
+ cwd: root,
559
+ ignore: options.ignore ?? [],
560
+ onlyFiles: true,
561
+ absolute: true,
562
+ unique: true
563
+ });
564
+ }
540
565
  async function walk(base, current, ignoreDirs, extensions, out) {
541
566
  const items = await readdir2(current, { withFileTypes: true });
542
567
  for (const item of items) {
@@ -648,15 +673,66 @@ async function exists3(target) {
648
673
  }
649
674
  }
650
675
 
676
+ // src/core/contractsDecl.ts
677
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
678
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
679
+ function extractDeclaredContractIds(text) {
680
+ const ids = [];
681
+ for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
682
+ const id = match[1];
683
+ if (id) {
684
+ ids.push(id);
685
+ }
686
+ }
687
+ return ids;
688
+ }
689
+ function stripContractDeclarationLines(text) {
690
+ return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
691
+ }
692
+
693
+ // src/core/contractIndex.ts
694
+ async function buildContractIndex(root, config) {
695
+ const contractsRoot = resolvePath(root, config, "contractsDir");
696
+ const uiRoot = path7.join(contractsRoot, "ui");
697
+ const apiRoot = path7.join(contractsRoot, "api");
698
+ const dbRoot = path7.join(contractsRoot, "db");
699
+ const [uiFiles, apiFiles, dbFiles] = await Promise.all([
700
+ collectUiContractFiles(uiRoot),
701
+ collectApiContractFiles(apiRoot),
702
+ collectDataContractFiles(dbRoot)
703
+ ]);
704
+ const index = {
705
+ ids: /* @__PURE__ */ new Set(),
706
+ idToFiles: /* @__PURE__ */ new Map(),
707
+ files: { ui: uiFiles, api: apiFiles, db: dbFiles }
708
+ };
709
+ await indexContractFiles(uiFiles, index);
710
+ await indexContractFiles(apiFiles, index);
711
+ await indexContractFiles(dbFiles, index);
712
+ return index;
713
+ }
714
+ async function indexContractFiles(files, index) {
715
+ for (const file of files) {
716
+ const text = await readFile2(file, "utf-8");
717
+ extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
718
+ }
719
+ }
720
+ function record(index, id, file) {
721
+ index.ids.add(id);
722
+ const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
723
+ current.add(file);
724
+ index.idToFiles.set(id, current);
725
+ }
726
+
651
727
  // src/core/ids.ts
652
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
728
+ var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
653
729
  var STRICT_ID_PATTERNS = {
654
730
  SPEC: /\bSPEC-\d{4}\b/g,
655
731
  BR: /\bBR-\d{4}\b/g,
656
732
  SC: /\bSC-\d{4}\b/g,
657
733
  UI: /\bUI-\d{4}\b/g,
658
734
  API: /\bAPI-\d{4}\b/g,
659
- DATA: /\bDATA-\d{4}\b/g,
735
+ DB: /\bDB-\d{4}\b/g,
660
736
  ADR: /\bADR-\d{4}\b/g
661
737
  };
662
738
  var LOOSE_ID_PATTERNS = {
@@ -665,7 +741,7 @@ var LOOSE_ID_PATTERNS = {
665
741
  SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
666
742
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
667
743
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
668
- DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi,
744
+ DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
669
745
  ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
670
746
  };
671
747
  function extractIds(text, prefix) {
@@ -701,8 +777,170 @@ function isValidId(value, prefix) {
701
777
  return strict.test(value);
702
778
  }
703
779
 
780
+ // src/core/parse/markdown.ts
781
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
782
+ function parseHeadings(md) {
783
+ const lines = md.split(/\r?\n/);
784
+ const headings = [];
785
+ for (let i = 0; i < lines.length; i++) {
786
+ const line = lines[i] ?? "";
787
+ const match = line.match(HEADING_RE);
788
+ if (!match) continue;
789
+ const levelToken = match[1];
790
+ const title = match[2];
791
+ if (!levelToken || !title) continue;
792
+ headings.push({
793
+ level: levelToken.length,
794
+ title: title.trim(),
795
+ line: i + 1
796
+ });
797
+ }
798
+ return headings;
799
+ }
800
+ function extractH2Sections(md) {
801
+ const lines = md.split(/\r?\n/);
802
+ const headings = parseHeadings(md).filter((heading) => heading.level === 2);
803
+ const sections = /* @__PURE__ */ new Map();
804
+ for (let i = 0; i < headings.length; i++) {
805
+ const current = headings[i];
806
+ if (!current) continue;
807
+ const next = headings[i + 1];
808
+ const startLine = current.line + 1;
809
+ const endLine = (next?.line ?? lines.length + 1) - 1;
810
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
811
+ sections.set(current.title.trim(), {
812
+ title: current.title.trim(),
813
+ startLine,
814
+ endLine,
815
+ body
816
+ });
817
+ }
818
+ return sections;
819
+ }
820
+
821
+ // src/core/parse/spec.ts
822
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
823
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
824
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
825
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
826
+ var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
827
+ var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
828
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
829
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
830
+ function parseSpec(md, file) {
831
+ const headings = parseHeadings(md);
832
+ const h1 = headings.find((heading) => heading.level === 1);
833
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
834
+ const sections = extractH2Sections(md);
835
+ const sectionNames = new Set(Array.from(sections.keys()));
836
+ const brSection = sections.get(BR_SECTION_TITLE);
837
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
838
+ const startLine = brSection?.startLine ?? 1;
839
+ const brs = [];
840
+ const brsWithoutPriority = [];
841
+ const brsWithInvalidPriority = [];
842
+ for (let i = 0; i < brLines.length; i++) {
843
+ const lineText = brLines[i] ?? "";
844
+ const lineNumber = startLine + i;
845
+ const validMatch = lineText.match(BR_LINE_RE);
846
+ if (validMatch) {
847
+ const id = validMatch[1];
848
+ const priority = validMatch[2];
849
+ const text = validMatch[3];
850
+ if (!id || !priority || !text) continue;
851
+ brs.push({
852
+ id,
853
+ priority,
854
+ text: text.trim(),
855
+ line: lineNumber
856
+ });
857
+ continue;
858
+ }
859
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
860
+ if (anyPriorityMatch) {
861
+ const id = anyPriorityMatch[1];
862
+ const priority = anyPriorityMatch[2];
863
+ const text = anyPriorityMatch[3];
864
+ if (!id || !priority || !text) continue;
865
+ if (!VALID_PRIORITIES.has(priority)) {
866
+ brsWithInvalidPriority.push({
867
+ id,
868
+ priority,
869
+ text: text.trim(),
870
+ line: lineNumber
871
+ });
872
+ }
873
+ continue;
874
+ }
875
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
876
+ if (noPriorityMatch) {
877
+ const id = noPriorityMatch[1];
878
+ const text = noPriorityMatch[2];
879
+ if (!id || !text) continue;
880
+ brsWithoutPriority.push({
881
+ id,
882
+ text: text.trim(),
883
+ line: lineNumber
884
+ });
885
+ }
886
+ }
887
+ const parsed = {
888
+ file,
889
+ sections: sectionNames,
890
+ brs,
891
+ brsWithoutPriority,
892
+ brsWithInvalidPriority,
893
+ contractRefs: parseContractRefs(md)
894
+ };
895
+ if (specId) {
896
+ parsed.specId = specId;
897
+ }
898
+ return parsed;
899
+ }
900
+ function parseContractRefs(md) {
901
+ const lines = [];
902
+ for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
903
+ lines.push((match[1] ?? "").trim());
904
+ }
905
+ const ids = [];
906
+ const invalidTokens = [];
907
+ let hasNone = false;
908
+ for (const line of lines) {
909
+ if (line.length === 0) {
910
+ invalidTokens.push("(empty)");
911
+ continue;
912
+ }
913
+ const tokens = line.split(",").map((token) => token.trim());
914
+ for (const token of tokens) {
915
+ if (token.length === 0) {
916
+ invalidTokens.push("(empty)");
917
+ continue;
918
+ }
919
+ if (token === "none") {
920
+ hasNone = true;
921
+ continue;
922
+ }
923
+ if (CONTRACT_REF_ID_RE.test(token)) {
924
+ ids.push(token);
925
+ continue;
926
+ }
927
+ invalidTokens.push(token);
928
+ }
929
+ }
930
+ return {
931
+ lines,
932
+ ids: unique2(ids),
933
+ invalidTokens: unique2(invalidTokens),
934
+ hasNone
935
+ };
936
+ }
937
+ function unique2(values) {
938
+ return Array.from(new Set(values));
939
+ }
940
+
704
941
  // src/core/traceability.ts
705
- import { readFile as readFile2 } from "fs/promises";
942
+ import { readFile as readFile3 } from "fs/promises";
943
+ import path8 from "path";
706
944
 
707
945
  // src/core/gherkin/parse.ts
708
946
  import {
@@ -739,7 +977,7 @@ var SC_TAG_RE = /^SC-\d{4}$/;
739
977
  var BR_TAG_RE = /^BR-\d{4}$/;
740
978
  var UI_TAG_RE = /^UI-\d{4}$/;
741
979
  var API_TAG_RE = /^API-\d{4}$/;
742
- var DATA_TAG_RE = /^DATA-\d{4}$/;
980
+ var DB_TAG_RE = /^DB-\d{4}$/;
743
981
  function parseScenarioDocument(text, uri) {
744
982
  const { gherkinDocument, errors } = parseGherkin(text, uri);
745
983
  if (!gherkinDocument) {
@@ -768,10 +1006,10 @@ function buildScenarioAtoms(document) {
768
1006
  return document.scenarios.map((scenario) => {
769
1007
  const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
770
1008
  const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
771
- const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1009
+ const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
772
1010
  const contractIds = /* @__PURE__ */ new Set();
773
1011
  scenario.tags.forEach((tag) => {
774
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1012
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
775
1013
  contractIds.add(tag);
776
1014
  }
777
1015
  });
@@ -779,7 +1017,7 @@ function buildScenarioAtoms(document) {
779
1017
  for (const text of collectStepTexts(step)) {
780
1018
  extractIds(text, "UI").forEach((id) => contractIds.add(id));
781
1019
  extractIds(text, "API").forEach((id) => contractIds.add(id));
782
- extractIds(text, "DATA").forEach((id) => contractIds.add(id));
1020
+ extractIds(text, "DB").forEach((id) => contractIds.add(id));
783
1021
  }
784
1022
  }
785
1023
  const atom = {
@@ -858,16 +1096,37 @@ function collectStepTexts(step) {
858
1096
  }
859
1097
  return texts;
860
1098
  }
861
- function unique2(values) {
1099
+ function unique3(values) {
862
1100
  return Array.from(new Set(values));
863
1101
  }
864
1102
 
865
1103
  // src/core/traceability.ts
866
1104
  var SC_TAG_RE2 = /^SC-\d{4}$/;
1105
+ var SC_TEST_ANNOTATION_RE = /\bQFAI:SC-(\d{4})\b/g;
1106
+ var DEFAULT_TEST_FILE_EXCLUDE_GLOBS = [
1107
+ "**/node_modules/**",
1108
+ "**/.git/**",
1109
+ "**/.qfai/**",
1110
+ "**/dist/**",
1111
+ "**/build/**",
1112
+ "**/coverage/**",
1113
+ "**/.next/**",
1114
+ "**/out/**"
1115
+ ];
1116
+ function extractAnnotatedScIds(text) {
1117
+ const ids = /* @__PURE__ */ new Set();
1118
+ for (const match of text.matchAll(SC_TEST_ANNOTATION_RE)) {
1119
+ const suffix = match[1];
1120
+ if (suffix) {
1121
+ ids.add(`SC-${suffix}`);
1122
+ }
1123
+ }
1124
+ return Array.from(ids);
1125
+ }
867
1126
  async function collectScIdsFromScenarioFiles(scenarioFiles) {
868
1127
  const scIds = /* @__PURE__ */ new Set();
869
1128
  for (const file of scenarioFiles) {
870
- const text = await readFile2(file, "utf-8");
1129
+ const text = await readFile3(file, "utf-8");
871
1130
  const { document, errors } = parseScenarioDocument(text, file);
872
1131
  if (!document || errors.length > 0) {
873
1132
  continue;
@@ -882,14 +1141,67 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
882
1141
  }
883
1142
  return scIds;
884
1143
  }
885
- async function collectScTestReferences(testsRoot) {
1144
+ async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
1145
+ const sources = /* @__PURE__ */ new Map();
1146
+ for (const file of scenarioFiles) {
1147
+ const text = await readFile3(file, "utf-8");
1148
+ const { document, errors } = parseScenarioDocument(text, file);
1149
+ if (!document || errors.length > 0) {
1150
+ continue;
1151
+ }
1152
+ for (const scenario of document.scenarios) {
1153
+ for (const tag of scenario.tags) {
1154
+ if (!SC_TAG_RE2.test(tag)) {
1155
+ continue;
1156
+ }
1157
+ const current = sources.get(tag) ?? /* @__PURE__ */ new Set();
1158
+ current.add(file);
1159
+ sources.set(tag, current);
1160
+ }
1161
+ }
1162
+ }
1163
+ return sources;
1164
+ }
1165
+ async function collectScTestReferences(root, globs, excludeGlobs) {
886
1166
  const refs = /* @__PURE__ */ new Map();
887
- const testFiles = await collectFiles(testsRoot, {
888
- extensions: [".ts", ".tsx", ".js", ".jsx"]
889
- });
890
- for (const file of testFiles) {
891
- const text = await readFile2(file, "utf-8");
892
- const scIds = extractIds(text, "SC");
1167
+ const normalizedGlobs = normalizeGlobs(globs);
1168
+ const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
1169
+ const mergedExcludeGlobs = Array.from(
1170
+ /* @__PURE__ */ new Set([...DEFAULT_TEST_FILE_EXCLUDE_GLOBS, ...normalizedExcludeGlobs])
1171
+ );
1172
+ if (normalizedGlobs.length === 0) {
1173
+ return {
1174
+ refs,
1175
+ scan: {
1176
+ globs: normalizedGlobs,
1177
+ excludeGlobs: mergedExcludeGlobs,
1178
+ matchedFileCount: 0
1179
+ }
1180
+ };
1181
+ }
1182
+ let files = [];
1183
+ try {
1184
+ files = await collectFilesByGlobs(root, {
1185
+ globs: normalizedGlobs,
1186
+ ignore: mergedExcludeGlobs
1187
+ });
1188
+ } catch (error2) {
1189
+ return {
1190
+ refs,
1191
+ scan: {
1192
+ globs: normalizedGlobs,
1193
+ excludeGlobs: mergedExcludeGlobs,
1194
+ matchedFileCount: 0
1195
+ },
1196
+ error: formatError3(error2)
1197
+ };
1198
+ }
1199
+ const normalizedFiles = Array.from(
1200
+ new Set(files.map((file) => path8.normalize(file)))
1201
+ );
1202
+ for (const file of normalizedFiles) {
1203
+ const text = await readFile3(file, "utf-8");
1204
+ const scIds = extractAnnotatedScIds(text);
893
1205
  if (scIds.length === 0) {
894
1206
  continue;
895
1207
  }
@@ -899,7 +1211,14 @@ async function collectScTestReferences(testsRoot) {
899
1211
  refs.set(scId, current);
900
1212
  }
901
1213
  }
902
- return refs;
1214
+ return {
1215
+ refs,
1216
+ scan: {
1217
+ globs: normalizedGlobs,
1218
+ excludeGlobs: mergedExcludeGlobs,
1219
+ matchedFileCount: normalizedFiles.length
1220
+ }
1221
+ };
903
1222
  }
904
1223
  function buildScCoverage(scIds, refs) {
905
1224
  const sortedScIds = toSortedArray(scIds);
@@ -927,18 +1246,27 @@ function buildScCoverage(scIds, refs) {
927
1246
  function toSortedArray(values) {
928
1247
  return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
929
1248
  }
1249
+ function normalizeGlobs(globs) {
1250
+ return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1251
+ }
1252
+ function formatError3(error2) {
1253
+ if (error2 instanceof Error) {
1254
+ return error2.message;
1255
+ }
1256
+ return String(error2);
1257
+ }
930
1258
 
931
1259
  // src/core/version.ts
932
- import { readFile as readFile3 } from "fs/promises";
933
- import path7 from "path";
1260
+ import { readFile as readFile4 } from "fs/promises";
1261
+ import path9 from "path";
934
1262
  import { fileURLToPath as fileURLToPath2 } from "url";
935
1263
  async function resolveToolVersion() {
936
- if ("0.4.0".length > 0) {
937
- return "0.4.0";
1264
+ if ("0.4.4".length > 0) {
1265
+ return "0.4.4";
938
1266
  }
939
1267
  try {
940
1268
  const packagePath = resolvePackageJsonPath();
941
- const raw = await readFile3(packagePath, "utf-8");
1269
+ const raw = await readFile4(packagePath, "utf-8");
942
1270
  const parsed = JSON.parse(raw);
943
1271
  const version = typeof parsed.version === "string" ? parsed.version : "";
944
1272
  return version.length > 0 ? version : "unknown";
@@ -949,71 +1277,42 @@ async function resolveToolVersion() {
949
1277
  function resolvePackageJsonPath() {
950
1278
  const base = import.meta.url;
951
1279
  const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
952
- return path7.resolve(path7.dirname(basePath), "../../package.json");
1280
+ return path9.resolve(path9.dirname(basePath), "../../package.json");
953
1281
  }
954
1282
 
955
1283
  // src/core/validators/contracts.ts
956
- import { readFile as readFile4 } from "fs/promises";
957
- import path9 from "path";
1284
+ import { readFile as readFile5 } from "fs/promises";
1285
+ import path11 from "path";
958
1286
 
959
1287
  // src/core/contracts.ts
960
- import path8 from "path";
1288
+ import path10 from "path";
961
1289
  import { parse as parseYaml2 } from "yaml";
962
1290
  function parseStructuredContract(file, text) {
963
- const ext = path8.extname(file).toLowerCase();
1291
+ const ext = path10.extname(file).toLowerCase();
964
1292
  if (ext === ".json") {
965
1293
  return JSON.parse(text);
966
1294
  }
967
1295
  return parseYaml2(text);
968
1296
  }
969
- function extractUiContractIds(doc) {
970
- const id = typeof doc.id === "string" ? doc.id : "";
971
- return extractIds(id, "UI");
972
- }
973
- function extractApiContractIds(doc) {
974
- const operationIds = /* @__PURE__ */ new Set();
975
- collectOperationIds(doc, operationIds);
976
- const ids = /* @__PURE__ */ new Set();
977
- for (const operationId of operationIds) {
978
- extractIds(operationId, "API").forEach((id) => ids.add(id));
979
- }
980
- return Array.from(ids);
981
- }
982
- function collectOperationIds(value, out) {
983
- if (!value || typeof value !== "object") {
984
- return;
985
- }
986
- if (Array.isArray(value)) {
987
- for (const item of value) {
988
- collectOperationIds(item, out);
989
- }
990
- return;
991
- }
992
- for (const [key, entry] of Object.entries(value)) {
993
- if (key === "operationId" && typeof entry === "string") {
994
- out.add(entry);
995
- continue;
996
- }
997
- collectOperationIds(entry, out);
998
- }
999
- }
1000
-
1001
- // src/core/validators/contracts.ts
1002
- var SQL_DANGEROUS_PATTERNS = [
1003
- { pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
1004
- { pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
1005
- { pattern: /\bTRUNCATE\b/i, label: "TRUNCATE" },
1006
- {
1007
- pattern: /\bALTER\s+TABLE\b[\s\S]*\bDROP\b/i,
1008
- label: "ALTER TABLE ... DROP"
1297
+
1298
+ // src/core/validators/contracts.ts
1299
+ var SQL_DANGEROUS_PATTERNS = [
1300
+ { pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
1301
+ { pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
1302
+ { pattern: /\bTRUNCATE\b/i, label: "TRUNCATE" },
1303
+ {
1304
+ pattern: /\bALTER\s+TABLE\b[\s\S]*\bDROP\b/i,
1305
+ label: "ALTER TABLE ... DROP"
1009
1306
  }
1010
1307
  ];
1011
1308
  async function validateContracts(root, config) {
1012
1309
  const issues = [];
1013
1310
  const contractsRoot = resolvePath(root, config, "contractsDir");
1014
- issues.push(...await validateUiContracts(path9.join(contractsRoot, "ui")));
1015
- issues.push(...await validateApiContracts(path9.join(contractsRoot, "api")));
1016
- issues.push(...await validateDataContracts(path9.join(contractsRoot, "db")));
1311
+ issues.push(...await validateUiContracts(path11.join(contractsRoot, "ui")));
1312
+ issues.push(...await validateApiContracts(path11.join(contractsRoot, "api")));
1313
+ issues.push(...await validateDataContracts(path11.join(contractsRoot, "db")));
1314
+ const contractIndex = await buildContractIndex(root, config);
1315
+ issues.push(...validateDuplicateContractIds(contractIndex));
1017
1316
  return issues;
1018
1317
  }
1019
1318
  async function validateUiContracts(uiRoot) {
@@ -1031,14 +1330,14 @@ async function validateUiContracts(uiRoot) {
1031
1330
  }
1032
1331
  const issues = [];
1033
1332
  for (const file of files) {
1034
- const text = await readFile4(file, "utf-8");
1333
+ const text = await readFile5(file, "utf-8");
1035
1334
  const invalidIds = extractInvalidIds(text, [
1036
1335
  "SPEC",
1037
1336
  "BR",
1038
1337
  "SC",
1039
1338
  "UI",
1040
1339
  "API",
1041
- "DATA",
1340
+ "DB",
1042
1341
  "ADR"
1043
1342
  ]);
1044
1343
  if (invalidIds.length > 0) {
@@ -1053,32 +1352,20 @@ async function validateUiContracts(uiRoot) {
1053
1352
  )
1054
1353
  );
1055
1354
  }
1056
- let doc;
1355
+ const declaredIds = extractDeclaredContractIds(text);
1356
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
1057
1357
  try {
1058
- doc = parseStructuredContract(file, text);
1358
+ parseStructuredContract(file, stripContractDeclarationLines(text));
1059
1359
  } catch (error2) {
1060
1360
  issues.push(
1061
1361
  issue(
1062
1362
  "QFAI-CONTRACT-001",
1063
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error2)})`,
1363
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
1064
1364
  "error",
1065
1365
  file,
1066
1366
  "contracts.ui.parse"
1067
1367
  )
1068
1368
  );
1069
- continue;
1070
- }
1071
- const uiIds = extractUiContractIds(doc);
1072
- if (uiIds.length === 0) {
1073
- issues.push(
1074
- issue(
1075
- "QFAI-CONTRACT-002",
1076
- `UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
1077
- "error",
1078
- file,
1079
- "contracts.ui.id"
1080
- )
1081
- );
1082
1369
  }
1083
1370
  }
1084
1371
  return issues;
@@ -1098,14 +1385,14 @@ async function validateApiContracts(apiRoot) {
1098
1385
  }
1099
1386
  const issues = [];
1100
1387
  for (const file of files) {
1101
- const text = await readFile4(file, "utf-8");
1388
+ const text = await readFile5(file, "utf-8");
1102
1389
  const invalidIds = extractInvalidIds(text, [
1103
1390
  "SPEC",
1104
1391
  "BR",
1105
1392
  "SC",
1106
1393
  "UI",
1107
1394
  "API",
1108
- "DATA",
1395
+ "DB",
1109
1396
  "ADR"
1110
1397
  ]);
1111
1398
  if (invalidIds.length > 0) {
@@ -1120,14 +1407,16 @@ async function validateApiContracts(apiRoot) {
1120
1407
  )
1121
1408
  );
1122
1409
  }
1410
+ const declaredIds = extractDeclaredContractIds(text);
1411
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "API"));
1123
1412
  let doc;
1124
1413
  try {
1125
- doc = parseStructuredContract(file, text);
1414
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
1126
1415
  } catch (error2) {
1127
1416
  issues.push(
1128
1417
  issue(
1129
1418
  "QFAI-CONTRACT-001",
1130
- `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error2)})`,
1419
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
1131
1420
  "error",
1132
1421
  file,
1133
1422
  "contracts.api.parse"
@@ -1146,18 +1435,6 @@ async function validateApiContracts(apiRoot) {
1146
1435
  )
1147
1436
  );
1148
1437
  }
1149
- const apiIds = extractApiContractIds(doc);
1150
- if (apiIds.length === 0) {
1151
- issues.push(
1152
- issue(
1153
- "QFAI-CONTRACT-002",
1154
- `API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
1155
- "error",
1156
- file,
1157
- "contracts.api.id"
1158
- )
1159
- );
1160
- }
1161
1438
  }
1162
1439
  return issues;
1163
1440
  }
@@ -1166,24 +1443,24 @@ async function validateDataContracts(dataRoot) {
1166
1443
  if (files.length === 0) {
1167
1444
  return [
1168
1445
  issue(
1169
- "QFAI-DATA-000",
1170
- "DATA \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1446
+ "QFAI-DB-000",
1447
+ "DB \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1171
1448
  "info",
1172
1449
  dataRoot,
1173
- "contracts.data.files"
1450
+ "contracts.db.files"
1174
1451
  )
1175
1452
  ];
1176
1453
  }
1177
1454
  const issues = [];
1178
1455
  for (const file of files) {
1179
- const text = await readFile4(file, "utf-8");
1456
+ const text = await readFile5(file, "utf-8");
1180
1457
  const invalidIds = extractInvalidIds(text, [
1181
1458
  "SPEC",
1182
1459
  "BR",
1183
1460
  "SC",
1184
1461
  "UI",
1185
1462
  "API",
1186
- "DATA",
1463
+ "DB",
1187
1464
  "ADR"
1188
1465
  ]);
1189
1466
  if (invalidIds.length > 0) {
@@ -1198,6 +1475,8 @@ async function validateDataContracts(dataRoot) {
1198
1475
  )
1199
1476
  );
1200
1477
  }
1478
+ const declaredIds = extractDeclaredContractIds(text);
1479
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "DB"));
1201
1480
  issues.push(...lintSql(text, file));
1202
1481
  }
1203
1482
  return issues;
@@ -1208,21 +1487,87 @@ function lintSql(text, file) {
1208
1487
  if (pattern.test(text)) {
1209
1488
  issues.push(
1210
1489
  issue(
1211
- "QFAI-DATA-001",
1490
+ "QFAI-DB-001",
1212
1491
  `\u5371\u967A\u306A SQL \u64CD\u4F5C\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059: ${label}`,
1213
1492
  "warning",
1214
1493
  file,
1215
- "contracts.data.sql"
1494
+ "contracts.db.sql"
1216
1495
  )
1217
1496
  );
1218
1497
  }
1219
1498
  }
1220
1499
  return issues;
1221
1500
  }
1501
+ function validateDeclaredContractIds(ids, file, kind) {
1502
+ const issues = [];
1503
+ if (ids.length === 0) {
1504
+ issues.push(
1505
+ issue(
1506
+ "QFAI-CONTRACT-010",
1507
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
1508
+ "error",
1509
+ file,
1510
+ "contracts.declaration"
1511
+ )
1512
+ );
1513
+ return issues;
1514
+ }
1515
+ if (ids.length > 1) {
1516
+ issues.push(
1517
+ issue(
1518
+ "QFAI-CONTRACT-011",
1519
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B\u8907\u6570\u306E QFAI-CONTRACT-ID \u304C\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${ids.join(
1520
+ ", "
1521
+ )}`,
1522
+ "error",
1523
+ file,
1524
+ "contracts.declaration",
1525
+ ids
1526
+ )
1527
+ );
1528
+ return issues;
1529
+ }
1530
+ const [id] = ids;
1531
+ if (id && !id.startsWith(`${kind}-`)) {
1532
+ issues.push(
1533
+ issue(
1534
+ "QFAI-CONTRACT-013",
1535
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E QFAI-CONTRACT-ID \u304C ${kind}- \u3067\u306F\u3042\u308A\u307E\u305B\u3093: ${id}`,
1536
+ "error",
1537
+ file,
1538
+ "contracts.declarationPrefix",
1539
+ [id]
1540
+ )
1541
+ );
1542
+ }
1543
+ return issues;
1544
+ }
1545
+ function validateDuplicateContractIds(contractIndex) {
1546
+ const issues = [];
1547
+ for (const [id, files] of contractIndex.idToFiles.entries()) {
1548
+ if (files.size <= 1) {
1549
+ continue;
1550
+ }
1551
+ const sortedFiles = Array.from(files).sort((a, b) => a.localeCompare(b));
1552
+ issues.push(
1553
+ issue(
1554
+ "QFAI-CONTRACT-012",
1555
+ `\u5951\u7D04 ID \u304C\u8907\u6570\u30D5\u30A1\u30A4\u30EB\u3067\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${id} (${sortedFiles.join(
1556
+ ", "
1557
+ )})`,
1558
+ "error",
1559
+ sortedFiles[0],
1560
+ "contracts.idDuplicate",
1561
+ [id]
1562
+ )
1563
+ );
1564
+ }
1565
+ return issues;
1566
+ }
1222
1567
  function hasOpenApi(doc) {
1223
1568
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
1224
1569
  }
1225
- function formatError3(error2) {
1570
+ function formatError4(error2) {
1226
1571
  if (error2 instanceof Error) {
1227
1572
  return error2.message;
1228
1573
  }
@@ -1247,8 +1592,8 @@ function issue(code, message, severity, file, rule, refs) {
1247
1592
  }
1248
1593
 
1249
1594
  // src/core/validators/delta.ts
1250
- import { readFile as readFile5 } from "fs/promises";
1251
- import path10 from "path";
1595
+ import { readFile as readFile6 } from "fs/promises";
1596
+ import path12 from "path";
1252
1597
  var SECTION_RE = /^##\s+変更区分/m;
1253
1598
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1254
1599
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1258,268 +1603,87 @@ async function validateDeltas(root, config) {
1258
1603
  const specsRoot = resolvePath(root, config, "specsDir");
1259
1604
  const packs = await collectSpecPackDirs(specsRoot);
1260
1605
  if (packs.length === 0) {
1261
- return [];
1262
- }
1263
- const issues = [];
1264
- for (const pack of packs) {
1265
- const deltaPath = path10.join(pack, "delta.md");
1266
- let text;
1267
- try {
1268
- text = await readFile5(deltaPath, "utf-8");
1269
- } catch (error2) {
1270
- if (isMissingFileError2(error2)) {
1271
- issues.push(
1272
- issue2(
1273
- "QFAI-DELTA-001",
1274
- "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1275
- "error",
1276
- deltaPath,
1277
- "delta.exists"
1278
- )
1279
- );
1280
- continue;
1281
- }
1282
- throw error2;
1283
- }
1284
- const hasSection = SECTION_RE.test(text);
1285
- const hasCompatibility = COMPAT_LINE_RE.test(text);
1286
- const hasChange = CHANGE_LINE_RE.test(text);
1287
- if (!hasSection || !hasCompatibility || !hasChange) {
1288
- issues.push(
1289
- issue2(
1290
- "QFAI-DELTA-002",
1291
- "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
1292
- "error",
1293
- deltaPath,
1294
- "delta.section"
1295
- )
1296
- );
1297
- continue;
1298
- }
1299
- const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
1300
- const changeChecked = CHANGE_CHECKED_RE.test(text);
1301
- if (compatibilityChecked === changeChecked) {
1302
- issues.push(
1303
- issue2(
1304
- "QFAI-DELTA-003",
1305
- "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",
1306
- "error",
1307
- deltaPath,
1308
- "delta.classification"
1309
- )
1310
- );
1311
- }
1312
- }
1313
- return issues;
1314
- }
1315
- function isMissingFileError2(error2) {
1316
- if (!error2 || typeof error2 !== "object") {
1317
- return false;
1318
- }
1319
- return error2.code === "ENOENT";
1320
- }
1321
- function issue2(code, message, severity, file, rule, refs) {
1322
- const issue7 = {
1323
- code,
1324
- severity,
1325
- message
1326
- };
1327
- if (file) {
1328
- issue7.file = file;
1329
- }
1330
- if (rule) {
1331
- issue7.rule = rule;
1332
- }
1333
- if (refs && refs.length > 0) {
1334
- issue7.refs = refs;
1335
- }
1336
- return issue7;
1337
- }
1338
-
1339
- // src/core/validators/ids.ts
1340
- import { readFile as readFile7 } from "fs/promises";
1341
- import path12 from "path";
1342
-
1343
- // src/core/contractIndex.ts
1344
- import { readFile as readFile6 } from "fs/promises";
1345
- import path11 from "path";
1346
- async function buildContractIndex(root, config) {
1347
- const contractsRoot = resolvePath(root, config, "contractsDir");
1348
- const uiRoot = path11.join(contractsRoot, "ui");
1349
- const apiRoot = path11.join(contractsRoot, "api");
1350
- const dataRoot = path11.join(contractsRoot, "db");
1351
- const [uiFiles, apiFiles, dataFiles] = await Promise.all([
1352
- collectUiContractFiles(uiRoot),
1353
- collectApiContractFiles(apiRoot),
1354
- collectDataContractFiles(dataRoot)
1355
- ]);
1356
- const index = {
1357
- ids: /* @__PURE__ */ new Set(),
1358
- idToFiles: /* @__PURE__ */ new Map(),
1359
- files: { ui: uiFiles, api: apiFiles, data: dataFiles },
1360
- structuredParseFailedFiles: /* @__PURE__ */ new Set()
1361
- };
1362
- await indexUiContracts(uiFiles, index);
1363
- await indexApiContracts(apiFiles, index);
1364
- await indexDataContracts(dataFiles, index);
1365
- return index;
1366
- }
1367
- async function indexUiContracts(files, index) {
1368
- for (const file of files) {
1369
- const text = await readFile6(file, "utf-8");
1370
- try {
1371
- const doc = parseStructuredContract(file, text);
1372
- extractUiContractIds(doc).forEach((id) => record(index, id, file));
1373
- } catch {
1374
- index.structuredParseFailedFiles.add(file);
1375
- extractIds(text, "UI").forEach((id) => record(index, id, file));
1376
- }
1377
- }
1378
- }
1379
- async function indexApiContracts(files, index) {
1380
- for (const file of files) {
1381
- const text = await readFile6(file, "utf-8");
1382
- try {
1383
- const doc = parseStructuredContract(file, text);
1384
- extractApiContractIds(doc).forEach((id) => record(index, id, file));
1385
- } catch {
1386
- index.structuredParseFailedFiles.add(file);
1387
- extractIds(text, "API").forEach((id) => record(index, id, file));
1388
- }
1389
- }
1390
- }
1391
- async function indexDataContracts(files, index) {
1392
- for (const file of files) {
1393
- const text = await readFile6(file, "utf-8");
1394
- extractIds(text, "DATA").forEach((id) => record(index, id, file));
1395
- }
1396
- }
1397
- function record(index, id, file) {
1398
- index.ids.add(id);
1399
- const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
1400
- current.add(file);
1401
- index.idToFiles.set(id, current);
1402
- }
1403
-
1404
- // src/core/parse/markdown.ts
1405
- var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1406
- function parseHeadings(md) {
1407
- const lines = md.split(/\r?\n/);
1408
- const headings = [];
1409
- for (let i = 0; i < lines.length; i++) {
1410
- const line = lines[i] ?? "";
1411
- const match = line.match(HEADING_RE);
1412
- if (!match) continue;
1413
- const levelToken = match[1];
1414
- const title = match[2];
1415
- if (!levelToken || !title) continue;
1416
- headings.push({
1417
- level: levelToken.length,
1418
- title: title.trim(),
1419
- line: i + 1
1420
- });
1421
- }
1422
- return headings;
1423
- }
1424
- function extractH2Sections(md) {
1425
- const lines = md.split(/\r?\n/);
1426
- const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1427
- const sections = /* @__PURE__ */ new Map();
1428
- for (let i = 0; i < headings.length; i++) {
1429
- const current = headings[i];
1430
- if (!current) continue;
1431
- const next = headings[i + 1];
1432
- const startLine = current.line + 1;
1433
- const endLine = (next?.line ?? lines.length + 1) - 1;
1434
- const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1435
- sections.set(current.title.trim(), {
1436
- title: current.title.trim(),
1437
- startLine,
1438
- endLine,
1439
- body
1440
- });
1441
- }
1442
- return sections;
1443
- }
1444
-
1445
- // src/core/parse/spec.ts
1446
- var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1447
- var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1448
- var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1449
- var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1450
- var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1451
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1452
- function parseSpec(md, file) {
1453
- const headings = parseHeadings(md);
1454
- const h1 = headings.find((heading) => heading.level === 1);
1455
- const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1456
- const sections = extractH2Sections(md);
1457
- const sectionNames = new Set(Array.from(sections.keys()));
1458
- const brSection = sections.get(BR_SECTION_TITLE);
1459
- const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1460
- const startLine = brSection?.startLine ?? 1;
1461
- const brs = [];
1462
- const brsWithoutPriority = [];
1463
- const brsWithInvalidPriority = [];
1464
- for (let i = 0; i < brLines.length; i++) {
1465
- const lineText = brLines[i] ?? "";
1466
- const lineNumber = startLine + i;
1467
- const validMatch = lineText.match(BR_LINE_RE);
1468
- if (validMatch) {
1469
- const id = validMatch[1];
1470
- const priority = validMatch[2];
1471
- const text = validMatch[3];
1472
- if (!id || !priority || !text) continue;
1473
- brs.push({
1474
- id,
1475
- priority,
1476
- text: text.trim(),
1477
- line: lineNumber
1478
- });
1479
- continue;
1480
- }
1481
- const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1482
- if (anyPriorityMatch) {
1483
- const id = anyPriorityMatch[1];
1484
- const priority = anyPriorityMatch[2];
1485
- const text = anyPriorityMatch[3];
1486
- if (!id || !priority || !text) continue;
1487
- if (!VALID_PRIORITIES.has(priority)) {
1488
- brsWithInvalidPriority.push({
1489
- id,
1490
- priority,
1491
- text: text.trim(),
1492
- line: lineNumber
1493
- });
1606
+ return [];
1607
+ }
1608
+ const issues = [];
1609
+ for (const pack of packs) {
1610
+ const deltaPath = path12.join(pack, "delta.md");
1611
+ let text;
1612
+ try {
1613
+ text = await readFile6(deltaPath, "utf-8");
1614
+ } catch (error2) {
1615
+ if (isMissingFileError2(error2)) {
1616
+ issues.push(
1617
+ issue2(
1618
+ "QFAI-DELTA-001",
1619
+ "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1620
+ "error",
1621
+ deltaPath,
1622
+ "delta.exists"
1623
+ )
1624
+ );
1625
+ continue;
1494
1626
  }
1627
+ throw error2;
1628
+ }
1629
+ const hasSection = SECTION_RE.test(text);
1630
+ const hasCompatibility = COMPAT_LINE_RE.test(text);
1631
+ const hasChange = CHANGE_LINE_RE.test(text);
1632
+ if (!hasSection || !hasCompatibility || !hasChange) {
1633
+ issues.push(
1634
+ issue2(
1635
+ "QFAI-DELTA-002",
1636
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
1637
+ "error",
1638
+ deltaPath,
1639
+ "delta.section"
1640
+ )
1641
+ );
1495
1642
  continue;
1496
1643
  }
1497
- const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1498
- if (noPriorityMatch) {
1499
- const id = noPriorityMatch[1];
1500
- const text = noPriorityMatch[2];
1501
- if (!id || !text) continue;
1502
- brsWithoutPriority.push({
1503
- id,
1504
- text: text.trim(),
1505
- line: lineNumber
1506
- });
1644
+ const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
1645
+ const changeChecked = CHANGE_CHECKED_RE.test(text);
1646
+ if (compatibilityChecked === changeChecked) {
1647
+ issues.push(
1648
+ issue2(
1649
+ "QFAI-DELTA-003",
1650
+ "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",
1651
+ "error",
1652
+ deltaPath,
1653
+ "delta.classification"
1654
+ )
1655
+ );
1507
1656
  }
1508
1657
  }
1509
- const parsed = {
1510
- file,
1511
- sections: sectionNames,
1512
- brs,
1513
- brsWithoutPriority,
1514
- brsWithInvalidPriority
1658
+ return issues;
1659
+ }
1660
+ function isMissingFileError2(error2) {
1661
+ if (!error2 || typeof error2 !== "object") {
1662
+ return false;
1663
+ }
1664
+ return error2.code === "ENOENT";
1665
+ }
1666
+ function issue2(code, message, severity, file, rule, refs) {
1667
+ const issue7 = {
1668
+ code,
1669
+ severity,
1670
+ message
1515
1671
  };
1516
- if (specId) {
1517
- parsed.specId = specId;
1672
+ if (file) {
1673
+ issue7.file = file;
1518
1674
  }
1519
- return parsed;
1675
+ if (rule) {
1676
+ issue7.rule = rule;
1677
+ }
1678
+ if (refs && refs.length > 0) {
1679
+ issue7.refs = refs;
1680
+ }
1681
+ return issue7;
1520
1682
  }
1521
1683
 
1522
1684
  // src/core/validators/ids.ts
1685
+ import { readFile as readFile7 } from "fs/promises";
1686
+ import path13 from "path";
1523
1687
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1524
1688
  async function validateDefinedIds(root, config) {
1525
1689
  const issues = [];
@@ -1585,7 +1749,7 @@ function recordId(out, id, file) {
1585
1749
  }
1586
1750
  function formatFileList(files, root) {
1587
1751
  return files.map((file) => {
1588
- const relative = path12.relative(root, file);
1752
+ const relative = path13.relative(root, file);
1589
1753
  return relative.length > 0 ? relative : file;
1590
1754
  }).join(", ");
1591
1755
  }
@@ -1614,7 +1778,6 @@ var WHEN_PATTERN = /\bWhen\b/;
1614
1778
  var THEN_PATTERN = /\bThen\b/;
1615
1779
  var SC_TAG_RE4 = /^SC-\d{4}$/;
1616
1780
  var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1617
- var BR_TAG_RE2 = /^BR-\d{4}$/;
1618
1781
  async function validateScenarios(root, config) {
1619
1782
  const specsRoot = resolvePath(root, config, "specsDir");
1620
1783
  const entries = await collectSpecEntries(specsRoot);
@@ -1663,7 +1826,7 @@ function validateScenarioContent(text, file) {
1663
1826
  "SC",
1664
1827
  "UI",
1665
1828
  "API",
1666
- "DATA",
1829
+ "DB",
1667
1830
  "ADR"
1668
1831
  ]);
1669
1832
  if (invalidIds.length > 0) {
@@ -1694,17 +1857,7 @@ function validateScenarioContent(text, file) {
1694
1857
  const featureSpecTags = document.featureTags.filter(
1695
1858
  (tag) => SPEC_TAG_RE2.test(tag)
1696
1859
  );
1697
- if (featureSpecTags.length === 0) {
1698
- issues.push(
1699
- issue4(
1700
- "QFAI-SC-009",
1701
- "Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1702
- "error",
1703
- file,
1704
- "scenario.featureSpec"
1705
- )
1706
- );
1707
- } else if (featureSpecTags.length > 1) {
1860
+ if (featureSpecTags.length > 1) {
1708
1861
  issues.push(
1709
1862
  issue4(
1710
1863
  "QFAI-SC-009",
@@ -1732,17 +1885,6 @@ function validateScenarioContent(text, file) {
1732
1885
  )
1733
1886
  );
1734
1887
  }
1735
- if (document.scenarios.length > 1) {
1736
- issues.push(
1737
- issue4(
1738
- "QFAI-SC-011",
1739
- `Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
1740
- "error",
1741
- file,
1742
- "scenario.single"
1743
- )
1744
- );
1745
- }
1746
1888
  for (const scenario of document.scenarios) {
1747
1889
  if (scenario.tags.length === 0) {
1748
1890
  issues.push(
@@ -1763,12 +1905,6 @@ function validateScenarioContent(text, file) {
1763
1905
  } else if (scTags.length > 1) {
1764
1906
  missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1765
1907
  }
1766
- if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
1767
- missingTags.push("SPEC");
1768
- }
1769
- if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
1770
- missingTags.push("BR");
1771
- }
1772
1908
  if (missingTags.length > 0) {
1773
1909
  issues.push(
1774
1910
  issue4(
@@ -1888,7 +2024,7 @@ function validateSpecContent(text, file, requiredSections) {
1888
2024
  "SC",
1889
2025
  "UI",
1890
2026
  "API",
1891
- "DATA",
2027
+ "DB",
1892
2028
  "ADR"
1893
2029
  ]);
1894
2030
  if (invalidIds.length > 0) {
@@ -2003,9 +2139,8 @@ function isMissingFileError4(error2) {
2003
2139
 
2004
2140
  // src/core/validators/traceability.ts
2005
2141
  import { readFile as readFile10 } from "fs/promises";
2006
- var SC_TAG_RE5 = /^SC-\d{4}$/;
2007
2142
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
2008
- var BR_TAG_RE3 = /^BR-\d{4}$/;
2143
+ var BR_TAG_RE2 = /^BR-\d{4}$/;
2009
2144
  async function validateTraceability(root, config) {
2010
2145
  const issues = [];
2011
2146
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -2018,8 +2153,7 @@ async function validateTraceability(root, config) {
2018
2153
  const brIdsInSpecs = /* @__PURE__ */ new Set();
2019
2154
  const brIdsInScenarios = /* @__PURE__ */ new Set();
2020
2155
  const scIdsInScenarios = /* @__PURE__ */ new Set();
2021
- const scenarioContractIds = /* @__PURE__ */ new Set();
2022
- const scWithContracts = /* @__PURE__ */ new Set();
2156
+ const specContractIds = /* @__PURE__ */ new Set();
2023
2157
  const specToBrIds = /* @__PURE__ */ new Map();
2024
2158
  const contractIndex = await buildContractIndex(root, config);
2025
2159
  const contractIds = contractIndex.ids;
@@ -2032,19 +2166,60 @@ async function validateTraceability(root, config) {
2032
2166
  }
2033
2167
  const brIds = parsed.brs.map((br) => br.id);
2034
2168
  brIds.forEach((id) => brIdsInSpecs.add(id));
2035
- const referencedContractIds = /* @__PURE__ */ new Set([
2036
- ...extractIds(text, "UI"),
2037
- ...extractIds(text, "API"),
2038
- ...extractIds(text, "DATA")
2039
- ]);
2040
- const unknownContractIds = Array.from(referencedContractIds).filter(
2169
+ if (parsed.specId) {
2170
+ const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
2171
+ brIds.forEach((id) => current.add(id));
2172
+ specToBrIds.set(parsed.specId, current);
2173
+ }
2174
+ const contractRefs = parsed.contractRefs;
2175
+ if (contractRefs.lines.length === 0) {
2176
+ issues.push(
2177
+ issue6(
2178
+ "QFAI-TRACE-020",
2179
+ "Spec \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
2180
+ "error",
2181
+ file,
2182
+ "traceability.specContractRefRequired"
2183
+ )
2184
+ );
2185
+ } else {
2186
+ if (contractRefs.hasNone && contractRefs.ids.length > 0) {
2187
+ issues.push(
2188
+ issue6(
2189
+ "QFAI-TRACE-021",
2190
+ "Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2191
+ "error",
2192
+ file,
2193
+ "traceability.specContractRefFormat"
2194
+ )
2195
+ );
2196
+ }
2197
+ if (contractRefs.invalidTokens.length > 0) {
2198
+ issues.push(
2199
+ issue6(
2200
+ "QFAI-TRACE-021",
2201
+ `Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
2202
+ ", "
2203
+ )}`,
2204
+ "error",
2205
+ file,
2206
+ "traceability.specContractRefFormat",
2207
+ contractRefs.invalidTokens
2208
+ )
2209
+ );
2210
+ }
2211
+ }
2212
+ contractRefs.ids.forEach((id) => {
2213
+ specContractIds.add(id);
2214
+ });
2215
+ const unknownContractIds = contractRefs.ids.filter(
2041
2216
  (id) => !contractIds.has(id)
2042
2217
  );
2043
2218
  if (unknownContractIds.length > 0) {
2044
2219
  issues.push(
2045
2220
  issue6(
2046
- "QFAI-TRACE-009",
2047
- `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
2221
+ "QFAI-TRACE-021",
2222
+ `Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
2048
2223
  ", "
2049
2224
  )}`,
2050
2225
  "error",
@@ -2054,11 +2229,6 @@ async function validateTraceability(root, config) {
2054
2229
  )
2055
2230
  );
2056
2231
  }
2057
- if (parsed.specId) {
2058
- const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
2059
- brIds.forEach((id) => current.add(id));
2060
- specToBrIds.set(parsed.specId, current);
2061
- }
2062
2232
  }
2063
2233
  for (const file of scenarioFiles) {
2064
2234
  const text = await readFile10(file, "utf-8");
@@ -2068,20 +2238,42 @@ async function validateTraceability(root, config) {
2068
2238
  continue;
2069
2239
  }
2070
2240
  const atoms = buildScenarioAtoms(document);
2241
+ const scIdsInFile = /* @__PURE__ */ new Set();
2071
2242
  for (const [index, scenario] of document.scenarios.entries()) {
2072
2243
  const atom = atoms[index];
2073
2244
  if (!atom) {
2074
2245
  continue;
2075
2246
  }
2076
2247
  const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
2077
- const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
2078
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE5.test(tag));
2079
- brTags.forEach((id) => brIdsInScenarios.add(id));
2080
- scTags.forEach((id) => scIdsInScenarios.add(id));
2081
- atom.contractIds.forEach((id) => scenarioContractIds.add(id));
2082
- if (atom.contractIds.length > 0) {
2083
- scTags.forEach((id) => scWithContracts.add(id));
2248
+ const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
2249
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
2250
+ if (specTags.length === 0) {
2251
+ issues.push(
2252
+ issue6(
2253
+ "QFAI-TRACE-014",
2254
+ `Scenario \u304C SPEC \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
2255
+ "error",
2256
+ file,
2257
+ "traceability.scenarioSpecRequired"
2258
+ )
2259
+ );
2260
+ }
2261
+ if (brTags.length === 0) {
2262
+ issues.push(
2263
+ issue6(
2264
+ "QFAI-TRACE-015",
2265
+ `Scenario \u304C BR \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
2266
+ "error",
2267
+ file,
2268
+ "traceability.scenarioBrRequired"
2269
+ )
2270
+ );
2084
2271
  }
2272
+ brTags.forEach((id) => brIdsInScenarios.add(id));
2273
+ scTags.forEach((id) => {
2274
+ scIdsInScenarios.add(id);
2275
+ scIdsInFile.add(id);
2276
+ });
2085
2277
  const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
2086
2278
  if (unknownSpecIds.length > 0) {
2087
2279
  issues.push(
@@ -2155,6 +2347,22 @@ async function validateTraceability(root, config) {
2155
2347
  }
2156
2348
  }
2157
2349
  }
2350
+ if (scIdsInFile.size !== 1) {
2351
+ const invalidScIds = Array.from(scIdsInFile).sort(
2352
+ (a, b) => a.localeCompare(b)
2353
+ );
2354
+ 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(", ")}`;
2355
+ issues.push(
2356
+ issue6(
2357
+ "QFAI-TRACE-012",
2358
+ `Spec entry \u304C Spec:SC=1:1 \u3092\u6E80\u305F\u3057\u3066\u3044\u307E\u305B\u3093: ${detail}`,
2359
+ "error",
2360
+ file,
2361
+ "traceability.specScOneToOne",
2362
+ invalidScIds
2363
+ )
2364
+ );
2365
+ }
2158
2366
  }
2159
2367
  if (upstreamIds.size === 0) {
2160
2368
  return [
@@ -2184,40 +2392,62 @@ async function validateTraceability(root, config) {
2184
2392
  );
2185
2393
  }
2186
2394
  }
2187
- if (config.validation.traceability.scMustTouchContracts && scIdsInScenarios.size > 0) {
2188
- const scWithoutContracts = Array.from(scIdsInScenarios).filter(
2189
- (id) => !scWithContracts.has(id)
2395
+ const scRefsResult = await collectScTestReferences(
2396
+ root,
2397
+ config.validation.traceability.testFileGlobs,
2398
+ config.validation.traceability.testFileExcludeGlobs
2399
+ );
2400
+ const scTestRefs = scRefsResult.refs;
2401
+ const testFileScan = scRefsResult.scan;
2402
+ const hasScenarios = scIdsInScenarios.size > 0;
2403
+ const hasGlobConfig = testFileScan.globs.length > 0;
2404
+ const hasMatchedTests = testFileScan.matchedFileCount > 0;
2405
+ if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
2406
+ const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
2407
+ issues.push(
2408
+ issue6(
2409
+ "QFAI-TRACE-013",
2410
+ `\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}`,
2411
+ "error",
2412
+ testsRoot,
2413
+ "traceability.testFileGlobs"
2414
+ )
2415
+ );
2416
+ } else {
2417
+ if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2418
+ const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2419
+ const refs = scTestRefs.get(id);
2420
+ return !refs || refs.size === 0;
2421
+ });
2422
+ if (scWithoutTests.length > 0) {
2423
+ issues.push(
2424
+ issue6(
2425
+ "QFAI-TRACE-010",
2426
+ `SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(
2427
+ ", "
2428
+ )}\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`,
2429
+ config.validation.traceability.scNoTestSeverity,
2430
+ testsRoot,
2431
+ "traceability.scMustHaveTest",
2432
+ scWithoutTests
2433
+ )
2434
+ );
2435
+ }
2436
+ }
2437
+ const unknownScIds = Array.from(scTestRefs.keys()).filter(
2438
+ (id) => !scIdsInScenarios.has(id)
2190
2439
  );
2191
- if (scWithoutContracts.length > 0) {
2440
+ if (unknownScIds.length > 0) {
2192
2441
  issues.push(
2193
2442
  issue6(
2194
- "QFAI_TRACE_SC_NO_CONTRACT",
2195
- `SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
2443
+ "QFAI-TRACE-011",
2444
+ `\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(
2196
2445
  ", "
2197
2446
  )}`,
2198
2447
  "error",
2199
- specsRoot,
2200
- "traceability.scMustTouchContracts",
2201
- scWithoutContracts
2202
- )
2203
- );
2204
- }
2205
- }
2206
- if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2207
- const scTestRefs = await collectScTestReferences(testsRoot);
2208
- const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2209
- const refs = scTestRefs.get(id);
2210
- return !refs || refs.size === 0;
2211
- });
2212
- if (scWithoutTests.length > 0) {
2213
- issues.push(
2214
- issue6(
2215
- "QFAI-TRACE-010",
2216
- `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`,
2217
- config.validation.traceability.scNoTestSeverity,
2218
2448
  testsRoot,
2219
- "traceability.scMustHaveTest",
2220
- scWithoutTests
2449
+ "traceability.scUnknownInTests",
2450
+ unknownScIds
2221
2451
  )
2222
2452
  );
2223
2453
  }
@@ -2225,16 +2455,16 @@ async function validateTraceability(root, config) {
2225
2455
  if (!config.validation.traceability.allowOrphanContracts) {
2226
2456
  if (contractIds.size > 0) {
2227
2457
  const orphanContracts = Array.from(contractIds).filter(
2228
- (id) => !scenarioContractIds.has(id)
2458
+ (id) => !specContractIds.has(id)
2229
2459
  );
2230
2460
  if (orphanContracts.length > 0) {
2231
2461
  issues.push(
2232
2462
  issue6(
2233
- "QFAI_CONTRACT_ORPHAN",
2234
- `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
2463
+ "QFAI-TRACE-022",
2464
+ `\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
2235
2465
  "error",
2236
2466
  specsRoot,
2237
- "traceability.allowOrphanContracts",
2467
+ "traceability.contractCoverage",
2238
2468
  orphanContracts
2239
2469
  )
2240
2470
  );
@@ -2280,8 +2510,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2280
2510
  issues.push(
2281
2511
  issue6(
2282
2512
  "QFAI-TRACE-002",
2283
- "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
2284
- "warning",
2513
+ "\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",
2514
+ "info",
2285
2515
  srcRoot,
2286
2516
  "traceability.codeReferences"
2287
2517
  )
@@ -2324,11 +2554,24 @@ async function validateProject(root, configResult) {
2324
2554
  ...await validateDefinedIds(root, config),
2325
2555
  ...await validateTraceability(root, config)
2326
2556
  ];
2557
+ const specsRoot = resolvePath(root, config, "specsDir");
2558
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
2559
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2560
+ const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
2561
+ root,
2562
+ config.validation.traceability.testFileGlobs,
2563
+ config.validation.traceability.testFileExcludeGlobs
2564
+ );
2565
+ const scCoverage = buildScCoverage(scIds, scTestRefs);
2327
2566
  const toolVersion = await resolveToolVersion();
2328
2567
  return {
2329
2568
  toolVersion,
2330
2569
  issues,
2331
- counts: countIssues(issues)
2570
+ counts: countIssues(issues),
2571
+ traceability: {
2572
+ sc: scCoverage,
2573
+ testFiles
2574
+ }
2332
2575
  };
2333
2576
  }
2334
2577
  function countIssues(issues) {
@@ -2342,16 +2585,16 @@ function countIssues(issues) {
2342
2585
  }
2343
2586
 
2344
2587
  // src/core/report.ts
2345
- var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
2588
+ var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
2346
2589
  async function createReportData(root, validation, configResult) {
2347
2590
  const resolved = configResult ?? await loadConfig(root);
2348
2591
  const config = resolved.config;
2349
2592
  const configPath = resolved.configPath;
2350
2593
  const specsRoot = resolvePath(root, config, "specsDir");
2351
2594
  const contractsRoot = resolvePath(root, config, "contractsDir");
2352
- const apiRoot = path13.join(contractsRoot, "api");
2353
- const uiRoot = path13.join(contractsRoot, "ui");
2354
- const dbRoot = path13.join(contractsRoot, "db");
2595
+ const apiRoot = path14.join(contractsRoot, "api");
2596
+ const uiRoot = path14.join(contractsRoot, "ui");
2597
+ const dbRoot = path14.join(contractsRoot, "db");
2355
2598
  const srcRoot = resolvePath(root, config, "srcDir");
2356
2599
  const testsRoot = resolvePath(root, config, "testsDir");
2357
2600
  const specFiles = await collectSpecFiles(specsRoot);
@@ -2361,6 +2604,23 @@ async function createReportData(root, validation, configResult) {
2361
2604
  ui: uiFiles,
2362
2605
  db: dbFiles
2363
2606
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
2607
+ const contractIndex = await buildContractIndex(root, config);
2608
+ const specContractRefs = await collectSpecContractRefs(specFiles);
2609
+ const contractIdList = Array.from(contractIndex.ids);
2610
+ const referencedContracts = /* @__PURE__ */ new Set();
2611
+ for (const ids of specContractRefs.specToContractIds.values()) {
2612
+ ids.forEach((id) => referencedContracts.add(id));
2613
+ }
2614
+ const referencedContractCount = contractIdList.filter(
2615
+ (id) => referencedContracts.has(id)
2616
+ ).length;
2617
+ const orphanContractCount = contractIdList.filter(
2618
+ (id) => !referencedContracts.has(id)
2619
+ ).length;
2620
+ const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
2621
+ const specToContractIdsRecord = mapToSortedRecord(
2622
+ specContractRefs.specToContractIds
2623
+ );
2364
2624
  const idsByPrefix = await collectIds([
2365
2625
  ...specFiles,
2366
2626
  ...scenarioFiles,
@@ -2378,8 +2638,15 @@ async function createReportData(root, validation, configResult) {
2378
2638
  testsRoot
2379
2639
  );
2380
2640
  const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2381
- const scTestRefs = await collectScTestReferences(testsRoot);
2382
- const scCoverage = buildScCoverage(scIds, scTestRefs);
2641
+ const scRefsResult = await collectScTestReferences(
2642
+ root,
2643
+ config.validation.traceability.testFileGlobs,
2644
+ config.validation.traceability.testFileExcludeGlobs
2645
+ );
2646
+ const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
2647
+ const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
2648
+ const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
2649
+ const scSourceRecord = mapToSortedRecord(scSources);
2383
2650
  const resolvedValidation = validation ?? await validateProject(root, resolved);
2384
2651
  const version = await resolveToolVersion();
2385
2652
  return {
@@ -2404,12 +2671,24 @@ async function createReportData(root, validation, configResult) {
2404
2671
  sc: idsByPrefix.SC,
2405
2672
  ui: idsByPrefix.UI,
2406
2673
  api: idsByPrefix.API,
2407
- data: idsByPrefix.DATA
2674
+ db: idsByPrefix.DB
2408
2675
  },
2409
2676
  traceability: {
2410
2677
  upstreamIdsFound: upstreamIds.size,
2411
2678
  referencedInCodeOrTests: traceability,
2412
- sc: scCoverage
2679
+ sc: scCoverage,
2680
+ scSources: scSourceRecord,
2681
+ testFiles,
2682
+ contracts: {
2683
+ total: contractIdList.length,
2684
+ referenced: referencedContractCount,
2685
+ orphan: orphanContractCount,
2686
+ idToSpecs: contractIdToSpecsRecord
2687
+ },
2688
+ specs: {
2689
+ contractRefMissing: specContractRefs.missingRefSpecs.size,
2690
+ specToContractIds: specToContractIdsRecord
2691
+ }
2413
2692
  },
2414
2693
  issues: resolvedValidation.issues
2415
2694
  };
@@ -2438,7 +2717,7 @@ function formatReportMarkdown(data) {
2438
2717
  lines.push(formatIdLine("SC", data.ids.sc));
2439
2718
  lines.push(formatIdLine("UI", data.ids.ui));
2440
2719
  lines.push(formatIdLine("API", data.ids.api));
2441
- lines.push(formatIdLine("DATA", data.ids.data));
2720
+ lines.push(formatIdLine("DB", data.ids.db));
2442
2721
  lines.push("");
2443
2722
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
2444
2723
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
@@ -2446,14 +2725,77 @@ function formatReportMarkdown(data) {
2446
2725
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2447
2726
  );
2448
2727
  lines.push("");
2728
+ lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
2729
+ lines.push(`- total: ${data.traceability.contracts.total}`);
2730
+ lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
2731
+ lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
2732
+ lines.push(
2733
+ `- specContractRefMissing: ${data.traceability.specs.contractRefMissing}`
2734
+ );
2735
+ lines.push("");
2736
+ lines.push("## \u5951\u7D04\u2192Spec");
2737
+ const contractToSpecs = data.traceability.contracts.idToSpecs;
2738
+ const contractIds = Object.keys(contractToSpecs).sort(
2739
+ (a, b) => a.localeCompare(b)
2740
+ );
2741
+ if (contractIds.length === 0) {
2742
+ lines.push("- (none)");
2743
+ } else {
2744
+ for (const contractId of contractIds) {
2745
+ const specs = contractToSpecs[contractId] ?? [];
2746
+ if (specs.length === 0) {
2747
+ lines.push(`- ${contractId}: (none)`);
2748
+ } else {
2749
+ lines.push(`- ${contractId}: ${specs.join(", ")}`);
2750
+ }
2751
+ }
2752
+ }
2753
+ lines.push("");
2754
+ lines.push("## Spec\u2192\u5951\u7D04");
2755
+ const specToContracts = data.traceability.specs.specToContractIds;
2756
+ const specIds = Object.keys(specToContracts).sort(
2757
+ (a, b) => a.localeCompare(b)
2758
+ );
2759
+ if (specIds.length === 0) {
2760
+ lines.push("- (none)");
2761
+ } else {
2762
+ for (const specId of specIds) {
2763
+ const contractIds2 = specToContracts[specId] ?? [];
2764
+ if (contractIds2.length === 0) {
2765
+ lines.push(`- ${specId}: (none)`);
2766
+ } else {
2767
+ lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
2768
+ }
2769
+ }
2770
+ }
2771
+ lines.push("");
2449
2772
  lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2450
2773
  lines.push(`- total: ${data.traceability.sc.total}`);
2451
2774
  lines.push(`- covered: ${data.traceability.sc.covered}`);
2452
2775
  lines.push(`- missing: ${data.traceability.sc.missing}`);
2776
+ lines.push(
2777
+ `- testFileGlobs: ${formatList(data.traceability.testFiles.globs)}`
2778
+ );
2779
+ lines.push(
2780
+ `- testFileExcludeGlobs: ${formatList(
2781
+ data.traceability.testFiles.excludeGlobs
2782
+ )}`
2783
+ );
2784
+ lines.push(
2785
+ `- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
2786
+ );
2453
2787
  if (data.traceability.sc.missingIds.length === 0) {
2454
2788
  lines.push("- missingIds: (none)");
2455
2789
  } else {
2456
- lines.push(`- missingIds: ${data.traceability.sc.missingIds.join(", ")}`);
2790
+ const sources = data.traceability.scSources;
2791
+ const missingWithSources = data.traceability.sc.missingIds.map((id) => {
2792
+ const files = sources[id] ?? [];
2793
+ if (files.length === 0) {
2794
+ return id;
2795
+ }
2796
+ return `${id} (${files.join(", ")})`;
2797
+ });
2798
+ lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
2457
2799
  }
2458
2800
  lines.push("");
2459
2801
  lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
@@ -2472,6 +2814,20 @@ function formatReportMarkdown(data) {
2472
2814
  }
2473
2815
  }
2474
2816
  lines.push("");
2817
+ lines.push("## Spec:SC=1:1 \u9055\u53CD");
2818
+ const specScIssues = data.issues.filter(
2819
+ (item) => item.code === "QFAI-TRACE-012"
2820
+ );
2821
+ if (specScIssues.length === 0) {
2822
+ lines.push("- (none)");
2823
+ } else {
2824
+ for (const item of specScIssues) {
2825
+ const location = item.file ?? "(unknown)";
2826
+ const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
2827
+ lines.push(`- ${location}: ${refs}`);
2828
+ }
2829
+ }
2830
+ lines.push("");
2475
2831
  lines.push("## Hotspots");
2476
2832
  const hotspots = buildHotspots(data.issues);
2477
2833
  if (hotspots.length === 0) {
@@ -2486,7 +2842,7 @@ function formatReportMarkdown(data) {
2486
2842
  lines.push("");
2487
2843
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
2488
2844
  const traceIssues = data.issues.filter(
2489
- (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-") || item.code === "QFAI_CONTRACT_ORPHAN"
2845
+ (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
2490
2846
  );
2491
2847
  if (traceIssues.length === 0) {
2492
2848
  lines.push("- (none)");
@@ -2516,6 +2872,33 @@ function formatReportMarkdown(data) {
2516
2872
  function formatReportJson(data) {
2517
2873
  return JSON.stringify(data, null, 2);
2518
2874
  }
2875
+ async function collectSpecContractRefs(specFiles) {
2876
+ const specToContractIds = /* @__PURE__ */ new Map();
2877
+ const idToSpecs = /* @__PURE__ */ new Map();
2878
+ const missingRefSpecs = /* @__PURE__ */ new Set();
2879
+ for (const file of specFiles) {
2880
+ const text = await readFile11(file, "utf-8");
2881
+ const parsed = parseSpec(text, file);
2882
+ const specKey = parsed.specId ?? file;
2883
+ const refs = parsed.contractRefs;
2884
+ if (refs.lines.length === 0) {
2885
+ missingRefSpecs.add(specKey);
2886
+ }
2887
+ const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
2888
+ for (const id of refs.ids) {
2889
+ currentContracts.add(id);
2890
+ const specs = idToSpecs.get(id) ?? /* @__PURE__ */ new Set();
2891
+ specs.add(specKey);
2892
+ idToSpecs.set(id, specs);
2893
+ }
2894
+ specToContractIds.set(specKey, currentContracts);
2895
+ }
2896
+ return {
2897
+ specToContractIds,
2898
+ idToSpecs,
2899
+ missingRefSpecs
2900
+ };
2901
+ }
2519
2902
  async function collectIds(files) {
2520
2903
  const result = {
2521
2904
  SPEC: /* @__PURE__ */ new Set(),
@@ -2523,7 +2906,7 @@ async function collectIds(files) {
2523
2906
  SC: /* @__PURE__ */ new Set(),
2524
2907
  UI: /* @__PURE__ */ new Set(),
2525
2908
  API: /* @__PURE__ */ new Set(),
2526
- DATA: /* @__PURE__ */ new Set()
2909
+ DB: /* @__PURE__ */ new Set()
2527
2910
  };
2528
2911
  for (const file of files) {
2529
2912
  const text = await readFile11(file, "utf-8");
@@ -2538,7 +2921,7 @@ async function collectIds(files) {
2538
2921
  SC: toSortedArray2(result.SC),
2539
2922
  UI: toSortedArray2(result.UI),
2540
2923
  API: toSortedArray2(result.API),
2541
- DATA: toSortedArray2(result.DATA)
2924
+ DB: toSortedArray2(result.DB)
2542
2925
  };
2543
2926
  }
2544
2927
  async function collectUpstreamIds(files) {
@@ -2582,9 +2965,22 @@ function formatIdLine(label, values) {
2582
2965
  }
2583
2966
  return `- ${label}: ${values.join(", ")}`;
2584
2967
  }
2968
+ function formatList(values) {
2969
+ if (values.length === 0) {
2970
+ return "(none)";
2971
+ }
2972
+ return values.join(", ");
2973
+ }
2585
2974
  function toSortedArray2(values) {
2586
2975
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2587
2976
  }
2977
+ function mapToSortedRecord(values) {
2978
+ const record2 = {};
2979
+ for (const [key, files] of values.entries()) {
2980
+ record2[key] = Array.from(files).sort((a, b) => a.localeCompare(b));
2981
+ }
2982
+ return record2;
2983
+ }
2588
2984
  function buildHotspots(issues) {
2589
2985
  const map = /* @__PURE__ */ new Map();
2590
2986
  for (const issue7 of issues) {
@@ -2609,10 +3005,10 @@ function buildHotspots(issues) {
2609
3005
 
2610
3006
  // src/cli/commands/report.ts
2611
3007
  async function runReport(options) {
2612
- const root = path14.resolve(options.root);
3008
+ const root = path15.resolve(options.root);
2613
3009
  const configResult = await loadConfig(root);
2614
3010
  const input = configResult.config.output.validateJsonPath;
2615
- const inputPath = path14.isAbsolute(input) ? input : path14.resolve(root, input);
3011
+ const inputPath = path15.isAbsolute(input) ? input : path15.resolve(root, input);
2616
3012
  let validation;
2617
3013
  try {
2618
3014
  validation = await readValidationResult(inputPath);
@@ -2637,10 +3033,10 @@ async function runReport(options) {
2637
3033
  const data = await createReportData(root, validation, configResult);
2638
3034
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
2639
3035
  const outRoot = resolvePath(root, configResult.config, "outDir");
2640
- const defaultOut = options.format === "json" ? path14.join(outRoot, "report.json") : path14.join(outRoot, "report.md");
3036
+ const defaultOut = options.format === "json" ? path15.join(outRoot, "report.json") : path15.join(outRoot, "report.md");
2641
3037
  const out = options.outPath ?? defaultOut;
2642
- const outPath = path14.isAbsolute(out) ? out : path14.resolve(root, out);
2643
- await mkdir2(path14.dirname(outPath), { recursive: true });
3038
+ const outPath = path15.isAbsolute(out) ? out : path15.resolve(root, out);
3039
+ await mkdir2(path15.dirname(outPath), { recursive: true });
2644
3040
  await writeFile(outPath, `${output}
2645
3041
  `, "utf-8");
2646
3042
  info(
@@ -2683,7 +3079,7 @@ function isMissingFileError5(error2) {
2683
3079
 
2684
3080
  // src/cli/commands/validate.ts
2685
3081
  import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
2686
- import path15 from "path";
3082
+ import path16 from "path";
2687
3083
 
2688
3084
  // src/cli/lib/failOn.ts
2689
3085
  function shouldFail(result, failOn) {
@@ -2698,7 +3094,7 @@ function shouldFail(result, failOn) {
2698
3094
 
2699
3095
  // src/cli/commands/validate.ts
2700
3096
  async function runValidate(options) {
2701
- const root = path15.resolve(options.root);
3097
+ const root = path16.resolve(options.root);
2702
3098
  const configResult = await loadConfig(root);
2703
3099
  const result = await validateProject(root, configResult);
2704
3100
  const format = options.format ?? "text";
@@ -2747,8 +3143,8 @@ function emitGitHub(issue7) {
2747
3143
  );
2748
3144
  }
2749
3145
  async function emitJson(result, root, jsonPath) {
2750
- const abs = path15.isAbsolute(jsonPath) ? jsonPath : path15.resolve(root, jsonPath);
2751
- await mkdir3(path15.dirname(abs), { recursive: true });
3146
+ const abs = path16.isAbsolute(jsonPath) ? jsonPath : path16.resolve(root, jsonPath);
3147
+ await mkdir3(path16.dirname(abs), { recursive: true });
2752
3148
  await writeFile2(abs, `${JSON.stringify(result, null, 2)}
2753
3149
  `, "utf-8");
2754
3150
  }