qfai 0.3.8 → 0.4.2

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 (158) hide show
  1. package/README.md +12 -0
  2. package/assets/init/.qfai/README.md +5 -0
  3. package/assets/init/.qfai/prompts/README.md +1 -0
  4. package/assets/init/.qfai/prompts/qfai-generate-test-globs.md +29 -0
  5. package/assets/init/.qfai/specs/README.md +1 -1
  6. package/assets/init/root/qfai.config.yaml +8 -0
  7. package/assets/init/root/tests/qfai-traceability.sample.test.ts +2 -0
  8. package/dist/cli/index.cjs +717 -364
  9. package/dist/cli/index.cjs.map +1 -1
  10. package/dist/cli/index.d.ts +0 -2
  11. package/dist/cli/index.mjs +719 -366
  12. package/dist/cli/index.mjs.map +1 -1
  13. package/dist/index.cjs +710 -351
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +26 -3
  16. package/dist/index.d.ts +156 -2
  17. package/dist/index.mjs +714 -354
  18. package/dist/index.mjs.map +1 -1
  19. package/package.json +2 -1
  20. package/dist/cli/commands/init.d.ts +0 -8
  21. package/dist/cli/commands/init.d.ts.map +0 -1
  22. package/dist/cli/commands/init.js +0 -30
  23. package/dist/cli/commands/init.js.map +0 -1
  24. package/dist/cli/commands/report.d.ts +0 -7
  25. package/dist/cli/commands/report.d.ts.map +0 -1
  26. package/dist/cli/commands/report.js +0 -80
  27. package/dist/cli/commands/report.js.map +0 -1
  28. package/dist/cli/commands/validate.d.ts +0 -9
  29. package/dist/cli/commands/validate.d.ts.map +0 -1
  30. package/dist/cli/commands/validate.js +0 -57
  31. package/dist/cli/commands/validate.js.map +0 -1
  32. package/dist/cli/index.d.ts.map +0 -1
  33. package/dist/cli/index.js +0 -7
  34. package/dist/cli/index.js.map +0 -1
  35. package/dist/cli/lib/args.d.ts +0 -18
  36. package/dist/cli/lib/args.d.ts.map +0 -1
  37. package/dist/cli/lib/args.js +0 -98
  38. package/dist/cli/lib/args.js.map +0 -1
  39. package/dist/cli/lib/assets.d.ts +0 -2
  40. package/dist/cli/lib/assets.d.ts.map +0 -1
  41. package/dist/cli/lib/assets.js +0 -24
  42. package/dist/cli/lib/assets.js.map +0 -1
  43. package/dist/cli/lib/failOn.d.ts +0 -5
  44. package/dist/cli/lib/failOn.d.ts.map +0 -1
  45. package/dist/cli/lib/failOn.js +0 -10
  46. package/dist/cli/lib/failOn.js.map +0 -1
  47. package/dist/cli/lib/fs.d.ts +0 -11
  48. package/dist/cli/lib/fs.d.ts.map +0 -1
  49. package/dist/cli/lib/fs.js +0 -91
  50. package/dist/cli/lib/fs.js.map +0 -1
  51. package/dist/cli/lib/logger.d.ts +0 -4
  52. package/dist/cli/lib/logger.d.ts.map +0 -1
  53. package/dist/cli/lib/logger.js +0 -10
  54. package/dist/cli/lib/logger.js.map +0 -1
  55. package/dist/cli/main.d.ts +0 -2
  56. package/dist/cli/main.d.ts.map +0 -1
  57. package/dist/cli/main.js +0 -66
  58. package/dist/cli/main.js.map +0 -1
  59. package/dist/core/config.d.ts +0 -44
  60. package/dist/core/config.d.ts.map +0 -1
  61. package/dist/core/config.js +0 -218
  62. package/dist/core/config.js.map +0 -1
  63. package/dist/core/contractIndex.d.ts +0 -13
  64. package/dist/core/contractIndex.d.ts.map +0 -1
  65. package/dist/core/contractIndex.js +0 -66
  66. package/dist/core/contractIndex.js.map +0 -1
  67. package/dist/core/contracts.d.ts +0 -5
  68. package/dist/core/contracts.d.ts.map +0 -1
  69. package/dist/core/contracts.js +0 -42
  70. package/dist/core/contracts.js.map +0 -1
  71. package/dist/core/discovery.d.ts +0 -14
  72. package/dist/core/discovery.d.ts.map +0 -1
  73. package/dist/core/discovery.js +0 -55
  74. package/dist/core/discovery.js.map +0 -1
  75. package/dist/core/fs.d.ts +0 -6
  76. package/dist/core/fs.d.ts.map +0 -1
  77. package/dist/core/fs.js +0 -55
  78. package/dist/core/fs.js.map +0 -1
  79. package/dist/core/gherkin/parse.d.ts +0 -7
  80. package/dist/core/gherkin/parse.d.ts.map +0 -1
  81. package/dist/core/gherkin/parse.js +0 -25
  82. package/dist/core/gherkin/parse.js.map +0 -1
  83. package/dist/core/ids.d.ts +0 -6
  84. package/dist/core/ids.d.ts.map +0 -1
  85. package/dist/core/ids.js +0 -52
  86. package/dist/core/ids.js.map +0 -1
  87. package/dist/core/index.d.ts +0 -13
  88. package/dist/core/index.d.ts.map +0 -1
  89. package/dist/core/index.js +0 -13
  90. package/dist/core/index.js.map +0 -1
  91. package/dist/core/parse/adr.d.ts +0 -13
  92. package/dist/core/parse/adr.d.ts.map +0 -1
  93. package/dist/core/parse/adr.js +0 -33
  94. package/dist/core/parse/adr.js.map +0 -1
  95. package/dist/core/parse/gherkin.d.ts +0 -12
  96. package/dist/core/parse/gherkin.d.ts.map +0 -1
  97. package/dist/core/parse/gherkin.js +0 -22
  98. package/dist/core/parse/gherkin.js.map +0 -1
  99. package/dist/core/parse/markdown.d.ts +0 -14
  100. package/dist/core/parse/markdown.d.ts.map +0 -1
  101. package/dist/core/parse/markdown.js +0 -45
  102. package/dist/core/parse/markdown.js.map +0 -1
  103. package/dist/core/parse/spec.d.ts +0 -28
  104. package/dist/core/parse/spec.d.ts.map +0 -1
  105. package/dist/core/parse/spec.js +0 -80
  106. package/dist/core/parse/spec.js.map +0 -1
  107. package/dist/core/report.d.ts +0 -39
  108. package/dist/core/report.d.ts.map +0 -1
  109. package/dist/core/report.js +0 -226
  110. package/dist/core/report.js.map +0 -1
  111. package/dist/core/scenarioModel.d.ts +0 -33
  112. package/dist/core/scenarioModel.d.ts.map +0 -1
  113. package/dist/core/scenarioModel.js +0 -130
  114. package/dist/core/scenarioModel.js.map +0 -1
  115. package/dist/core/specLayout.d.ts +0 -8
  116. package/dist/core/specLayout.d.ts.map +0 -1
  117. package/dist/core/specLayout.js +0 -36
  118. package/dist/core/specLayout.js.map +0 -1
  119. package/dist/core/types.d.ts +0 -25
  120. package/dist/core/types.d.ts.map +0 -1
  121. package/dist/core/types.js +0 -2
  122. package/dist/core/types.js.map +0 -1
  123. package/dist/core/validate.d.ts +0 -4
  124. package/dist/core/validate.d.ts.map +0 -1
  125. package/dist/core/validate.js +0 -34
  126. package/dist/core/validate.js.map +0 -1
  127. package/dist/core/validators/contracts.d.ts +0 -5
  128. package/dist/core/validators/contracts.d.ts.map +0 -1
  129. package/dist/core/validators/contracts.js +0 -162
  130. package/dist/core/validators/contracts.js.map +0 -1
  131. package/dist/core/validators/delta.d.ts +0 -4
  132. package/dist/core/validators/delta.d.ts.map +0 -1
  133. package/dist/core/validators/delta.js +0 -68
  134. package/dist/core/validators/delta.js.map +0 -1
  135. package/dist/core/validators/ids.d.ts +0 -4
  136. package/dist/core/validators/ids.d.ts.map +0 -1
  137. package/dist/core/validators/ids.js +0 -88
  138. package/dist/core/validators/ids.js.map +0 -1
  139. package/dist/core/validators/scenario.d.ts +0 -5
  140. package/dist/core/validators/scenario.d.ts.map +0 -1
  141. package/dist/core/validators/scenario.js +0 -140
  142. package/dist/core/validators/scenario.js.map +0 -1
  143. package/dist/core/validators/spec.d.ts +0 -5
  144. package/dist/core/validators/spec.d.ts.map +0 -1
  145. package/dist/core/validators/spec.js +0 -94
  146. package/dist/core/validators/spec.js.map +0 -1
  147. package/dist/core/validators/traceability.d.ts +0 -4
  148. package/dist/core/validators/traceability.d.ts.map +0 -1
  149. package/dist/core/validators/traceability.js +0 -180
  150. package/dist/core/validators/traceability.js.map +0 -1
  151. package/dist/core/version.d.ts +0 -2
  152. package/dist/core/version.d.ts.map +0 -1
  153. package/dist/core/version.js +0 -25
  154. package/dist/core/version.js.map +0 -1
  155. package/dist/index.d.ts.map +0 -1
  156. package/dist/index.js +0 -2
  157. package/dist/index.js.map +0 -1
  158. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -154,8 +154,8 @@ function report(copied, skipped, dryRun, label) {
154
154
  }
155
155
 
156
156
  // src/cli/commands/report.ts
157
- import { mkdir as mkdir2, readFile as readFile11, writeFile } from "fs/promises";
158
- import path14 from "path";
157
+ import { mkdir as mkdir2, readFile as readFile12, writeFile } from "fs/promises";
158
+ import path15 from "path";
159
159
 
160
160
  // src/core/config.ts
161
161
  import { readFile } from "fs/promises";
@@ -187,6 +187,10 @@ var defaultConfig = {
187
187
  traceability: {
188
188
  brMustHaveSc: true,
189
189
  scMustTouchContracts: true,
190
+ scMustHaveTest: true,
191
+ testFileGlobs: [],
192
+ testFileExcludeGlobs: [],
193
+ scNoTestSeverity: "error",
190
194
  allowOrphanContracts: false,
191
195
  unknownContractIdSeverity: "error"
192
196
  }
@@ -366,6 +370,34 @@ function normalizeValidation(raw, configPath, issues) {
366
370
  configPath,
367
371
  issues
368
372
  ),
373
+ scMustHaveTest: readBoolean(
374
+ traceabilityRaw?.scMustHaveTest,
375
+ base.traceability.scMustHaveTest,
376
+ "validation.traceability.scMustHaveTest",
377
+ configPath,
378
+ issues
379
+ ),
380
+ testFileGlobs: readStringArray(
381
+ traceabilityRaw?.testFileGlobs,
382
+ base.traceability.testFileGlobs,
383
+ "validation.traceability.testFileGlobs",
384
+ configPath,
385
+ issues
386
+ ),
387
+ testFileExcludeGlobs: readStringArray(
388
+ traceabilityRaw?.testFileExcludeGlobs,
389
+ base.traceability.testFileExcludeGlobs,
390
+ "validation.traceability.testFileExcludeGlobs",
391
+ configPath,
392
+ issues
393
+ ),
394
+ scNoTestSeverity: readTraceabilitySeverity(
395
+ traceabilityRaw?.scNoTestSeverity,
396
+ base.traceability.scNoTestSeverity,
397
+ "validation.traceability.scNoTestSeverity",
398
+ configPath,
399
+ issues
400
+ ),
369
401
  allowOrphanContracts: readBoolean(
370
402
  traceabilityRaw?.allowOrphanContracts,
371
403
  base.traceability.allowOrphanContracts,
@@ -491,8 +523,8 @@ function isRecord(value) {
491
523
  }
492
524
 
493
525
  // src/core/report.ts
494
- import { readFile as readFile10 } from "fs/promises";
495
- import path13 from "path";
526
+ import { readFile as readFile11 } from "fs/promises";
527
+ import path14 from "path";
496
528
 
497
529
  // src/core/discovery.ts
498
530
  import { access as access3 } from "fs/promises";
@@ -500,6 +532,7 @@ import { access as access3 } from "fs/promises";
500
532
  // src/core/fs.ts
501
533
  import { access as access2, readdir as readdir2 } from "fs/promises";
502
534
  import path5 from "path";
535
+ import fg from "fast-glob";
503
536
  var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
504
537
  "node_modules",
505
538
  ".git",
@@ -521,6 +554,18 @@ async function collectFiles(root, options = {}) {
521
554
  await walk(root, root, ignoreDirs, extensions, entries);
522
555
  return entries;
523
556
  }
557
+ async function collectFilesByGlobs(root, options) {
558
+ if (options.globs.length === 0) {
559
+ return [];
560
+ }
561
+ return fg(options.globs, {
562
+ cwd: root,
563
+ ignore: options.ignore ?? [],
564
+ onlyFiles: true,
565
+ absolute: true,
566
+ unique: true
567
+ });
568
+ }
524
569
  async function walk(base, current, ignoreDirs, extensions, out) {
525
570
  const items = await readdir2(current, { withFileTypes: true });
526
571
  for (const item of items) {
@@ -685,20 +730,335 @@ function isValidId(value, prefix) {
685
730
  return strict.test(value);
686
731
  }
687
732
 
688
- // src/core/types.ts
689
- var VALIDATION_SCHEMA_VERSION = "0.2";
690
-
691
- // src/core/version.ts
733
+ // src/core/traceability.ts
692
734
  import { readFile as readFile2 } from "fs/promises";
693
735
  import path7 from "path";
736
+
737
+ // src/core/gherkin/parse.ts
738
+ import {
739
+ AstBuilder,
740
+ GherkinClassicTokenMatcher,
741
+ Parser
742
+ } from "@cucumber/gherkin";
743
+ import { randomUUID } from "crypto";
744
+ function parseGherkin(source, uri) {
745
+ const errors = [];
746
+ const uuidFn = () => randomUUID();
747
+ const builder = new AstBuilder(uuidFn);
748
+ const matcher = new GherkinClassicTokenMatcher();
749
+ const parser = new Parser(builder, matcher);
750
+ try {
751
+ const gherkinDocument = parser.parse(source);
752
+ gherkinDocument.uri = uri;
753
+ return { gherkinDocument, errors };
754
+ } catch (error2) {
755
+ errors.push(formatError2(error2));
756
+ return { gherkinDocument: null, errors };
757
+ }
758
+ }
759
+ function formatError2(error2) {
760
+ if (error2 instanceof Error) {
761
+ return error2.message;
762
+ }
763
+ return String(error2);
764
+ }
765
+
766
+ // src/core/scenarioModel.ts
767
+ var SPEC_TAG_RE = /^SPEC-\d{4}$/;
768
+ var SC_TAG_RE = /^SC-\d{4}$/;
769
+ var BR_TAG_RE = /^BR-\d{4}$/;
770
+ var UI_TAG_RE = /^UI-\d{4}$/;
771
+ var API_TAG_RE = /^API-\d{4}$/;
772
+ var DATA_TAG_RE = /^DATA-\d{4}$/;
773
+ function parseScenarioDocument(text, uri) {
774
+ const { gherkinDocument, errors } = parseGherkin(text, uri);
775
+ if (!gherkinDocument) {
776
+ return { document: null, errors };
777
+ }
778
+ const feature = gherkinDocument.feature;
779
+ if (!feature) {
780
+ return {
781
+ document: { uri, featureTags: [], scenarios: [] },
782
+ errors
783
+ };
784
+ }
785
+ const featureTags = collectTagNames(feature.tags);
786
+ const scenarios = collectScenarioNodes(feature, featureTags);
787
+ return {
788
+ document: {
789
+ uri,
790
+ featureName: feature.name,
791
+ featureTags,
792
+ scenarios
793
+ },
794
+ errors
795
+ };
796
+ }
797
+ function buildScenarioAtoms(document) {
798
+ return document.scenarios.map((scenario) => {
799
+ const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
800
+ const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
801
+ const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
802
+ const contractIds = /* @__PURE__ */ new Set();
803
+ scenario.tags.forEach((tag) => {
804
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
805
+ contractIds.add(tag);
806
+ }
807
+ });
808
+ for (const step of scenario.steps) {
809
+ for (const text of collectStepTexts(step)) {
810
+ extractIds(text, "UI").forEach((id) => contractIds.add(id));
811
+ extractIds(text, "API").forEach((id) => contractIds.add(id));
812
+ extractIds(text, "DATA").forEach((id) => contractIds.add(id));
813
+ }
814
+ }
815
+ const atom = {
816
+ uri: document.uri,
817
+ featureName: document.featureName ?? "",
818
+ scenarioName: scenario.name,
819
+ kind: scenario.kind,
820
+ brIds,
821
+ contractIds: Array.from(contractIds).sort()
822
+ };
823
+ if (scenario.line !== void 0) {
824
+ atom.line = scenario.line;
825
+ }
826
+ if (specIds.length === 1) {
827
+ const specId = specIds[0];
828
+ if (specId) {
829
+ atom.specId = specId;
830
+ }
831
+ }
832
+ if (scIds.length === 1) {
833
+ const scId = scIds[0];
834
+ if (scId) {
835
+ atom.scId = scId;
836
+ }
837
+ }
838
+ return atom;
839
+ });
840
+ }
841
+ function collectScenarioNodes(feature, featureTags) {
842
+ const scenarios = [];
843
+ for (const child of feature.children) {
844
+ if (child.scenario) {
845
+ scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
846
+ }
847
+ if (child.rule) {
848
+ const ruleTags = collectTagNames(child.rule.tags);
849
+ for (const ruleChild of child.rule.children) {
850
+ if (ruleChild.scenario) {
851
+ scenarios.push(
852
+ buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
853
+ );
854
+ }
855
+ }
856
+ }
857
+ }
858
+ return scenarios;
859
+ }
860
+ function buildScenarioNode(scenario, featureTags, ruleTags) {
861
+ const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
862
+ const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
863
+ return {
864
+ name: scenario.name,
865
+ kind,
866
+ line: scenario.location?.line,
867
+ tags,
868
+ steps: scenario.steps
869
+ };
870
+ }
871
+ function collectTagNames(tags) {
872
+ return tags.map((tag) => tag.name.replace(/^@/, ""));
873
+ }
874
+ function collectStepTexts(step) {
875
+ const texts = [];
876
+ if (step.text) {
877
+ texts.push(step.text);
878
+ }
879
+ if (step.docString?.content) {
880
+ texts.push(step.docString.content);
881
+ }
882
+ if (step.dataTable?.rows) {
883
+ for (const row of step.dataTable.rows) {
884
+ for (const cell of row.cells) {
885
+ texts.push(cell.value);
886
+ }
887
+ }
888
+ }
889
+ return texts;
890
+ }
891
+ function unique2(values) {
892
+ return Array.from(new Set(values));
893
+ }
894
+
895
+ // src/core/traceability.ts
896
+ var SC_TAG_RE2 = /^SC-\d{4}$/;
897
+ var SC_TEST_ANNOTATION_RE = /\bQFAI:SC-(\d{4})\b/g;
898
+ var DEFAULT_TEST_FILE_EXCLUDE_GLOBS = [
899
+ "**/node_modules/**",
900
+ "**/.git/**",
901
+ "**/.qfai/**",
902
+ "**/dist/**",
903
+ "**/build/**",
904
+ "**/coverage/**",
905
+ "**/.next/**",
906
+ "**/out/**"
907
+ ];
908
+ function extractAnnotatedScIds(text) {
909
+ const ids = /* @__PURE__ */ new Set();
910
+ for (const match of text.matchAll(SC_TEST_ANNOTATION_RE)) {
911
+ const suffix = match[1];
912
+ if (suffix) {
913
+ ids.add(`SC-${suffix}`);
914
+ }
915
+ }
916
+ return Array.from(ids);
917
+ }
918
+ async function collectScIdsFromScenarioFiles(scenarioFiles) {
919
+ const scIds = /* @__PURE__ */ new Set();
920
+ for (const file of scenarioFiles) {
921
+ const text = await readFile2(file, "utf-8");
922
+ const { document, errors } = parseScenarioDocument(text, file);
923
+ if (!document || errors.length > 0) {
924
+ continue;
925
+ }
926
+ for (const scenario of document.scenarios) {
927
+ for (const tag of scenario.tags) {
928
+ if (SC_TAG_RE2.test(tag)) {
929
+ scIds.add(tag);
930
+ }
931
+ }
932
+ }
933
+ }
934
+ return scIds;
935
+ }
936
+ async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
937
+ const sources = /* @__PURE__ */ new Map();
938
+ for (const file of scenarioFiles) {
939
+ const text = await readFile2(file, "utf-8");
940
+ const { document, errors } = parseScenarioDocument(text, file);
941
+ if (!document || errors.length > 0) {
942
+ continue;
943
+ }
944
+ for (const scenario of document.scenarios) {
945
+ for (const tag of scenario.tags) {
946
+ if (!SC_TAG_RE2.test(tag)) {
947
+ continue;
948
+ }
949
+ const current = sources.get(tag) ?? /* @__PURE__ */ new Set();
950
+ current.add(file);
951
+ sources.set(tag, current);
952
+ }
953
+ }
954
+ }
955
+ return sources;
956
+ }
957
+ async function collectScTestReferences(root, globs, excludeGlobs) {
958
+ const refs = /* @__PURE__ */ new Map();
959
+ const normalizedGlobs = normalizeGlobs(globs);
960
+ const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
961
+ const mergedExcludeGlobs = Array.from(
962
+ /* @__PURE__ */ new Set([...DEFAULT_TEST_FILE_EXCLUDE_GLOBS, ...normalizedExcludeGlobs])
963
+ );
964
+ if (normalizedGlobs.length === 0) {
965
+ return {
966
+ refs,
967
+ scan: {
968
+ globs: normalizedGlobs,
969
+ excludeGlobs: mergedExcludeGlobs,
970
+ matchedFileCount: 0
971
+ }
972
+ };
973
+ }
974
+ let files = [];
975
+ try {
976
+ files = await collectFilesByGlobs(root, {
977
+ globs: normalizedGlobs,
978
+ ignore: mergedExcludeGlobs
979
+ });
980
+ } catch (error2) {
981
+ return {
982
+ refs,
983
+ scan: {
984
+ globs: normalizedGlobs,
985
+ excludeGlobs: mergedExcludeGlobs,
986
+ matchedFileCount: 0
987
+ },
988
+ error: formatError3(error2)
989
+ };
990
+ }
991
+ const normalizedFiles = Array.from(
992
+ new Set(files.map((file) => path7.normalize(file)))
993
+ );
994
+ for (const file of normalizedFiles) {
995
+ const text = await readFile2(file, "utf-8");
996
+ const scIds = extractAnnotatedScIds(text);
997
+ if (scIds.length === 0) {
998
+ continue;
999
+ }
1000
+ for (const scId of scIds) {
1001
+ const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
1002
+ current.add(file);
1003
+ refs.set(scId, current);
1004
+ }
1005
+ }
1006
+ return {
1007
+ refs,
1008
+ scan: {
1009
+ globs: normalizedGlobs,
1010
+ excludeGlobs: mergedExcludeGlobs,
1011
+ matchedFileCount: normalizedFiles.length
1012
+ }
1013
+ };
1014
+ }
1015
+ function buildScCoverage(scIds, refs) {
1016
+ const sortedScIds = toSortedArray(scIds);
1017
+ const refsRecord = {};
1018
+ const missingIds = [];
1019
+ let covered = 0;
1020
+ for (const scId of sortedScIds) {
1021
+ const files = refs.get(scId);
1022
+ const sortedFiles = files ? toSortedArray(files) : [];
1023
+ refsRecord[scId] = sortedFiles;
1024
+ if (sortedFiles.length === 0) {
1025
+ missingIds.push(scId);
1026
+ } else {
1027
+ covered += 1;
1028
+ }
1029
+ }
1030
+ return {
1031
+ total: sortedScIds.length,
1032
+ covered,
1033
+ missing: missingIds.length,
1034
+ missingIds,
1035
+ refs: refsRecord
1036
+ };
1037
+ }
1038
+ function toSortedArray(values) {
1039
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
1040
+ }
1041
+ function normalizeGlobs(globs) {
1042
+ return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1043
+ }
1044
+ function formatError3(error2) {
1045
+ if (error2 instanceof Error) {
1046
+ return error2.message;
1047
+ }
1048
+ return String(error2);
1049
+ }
1050
+
1051
+ // src/core/version.ts
1052
+ import { readFile as readFile3 } from "fs/promises";
1053
+ import path8 from "path";
694
1054
  import { fileURLToPath as fileURLToPath2 } from "url";
695
1055
  async function resolveToolVersion() {
696
- if ("0.3.5".length > 0) {
697
- return "0.3.5";
1056
+ if ("0.4.2".length > 0) {
1057
+ return "0.4.2";
698
1058
  }
699
1059
  try {
700
1060
  const packagePath = resolvePackageJsonPath();
701
- const raw = await readFile2(packagePath, "utf-8");
1061
+ const raw = await readFile3(packagePath, "utf-8");
702
1062
  const parsed = JSON.parse(raw);
703
1063
  const version = typeof parsed.version === "string" ? parsed.version : "";
704
1064
  return version.length > 0 ? version : "unknown";
@@ -709,18 +1069,18 @@ async function resolveToolVersion() {
709
1069
  function resolvePackageJsonPath() {
710
1070
  const base = import.meta.url;
711
1071
  const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
712
- return path7.resolve(path7.dirname(basePath), "../../package.json");
1072
+ return path8.resolve(path8.dirname(basePath), "../../package.json");
713
1073
  }
714
1074
 
715
1075
  // src/core/validators/contracts.ts
716
- import { readFile as readFile3 } from "fs/promises";
717
- import path9 from "path";
1076
+ import { readFile as readFile4 } from "fs/promises";
1077
+ import path10 from "path";
718
1078
 
719
1079
  // src/core/contracts.ts
720
- import path8 from "path";
1080
+ import path9 from "path";
721
1081
  import { parse as parseYaml2 } from "yaml";
722
1082
  function parseStructuredContract(file, text) {
723
- const ext = path8.extname(file).toLowerCase();
1083
+ const ext = path9.extname(file).toLowerCase();
724
1084
  if (ext === ".json") {
725
1085
  return JSON.parse(text);
726
1086
  }
@@ -771,9 +1131,9 @@ var SQL_DANGEROUS_PATTERNS = [
771
1131
  async function validateContracts(root, config) {
772
1132
  const issues = [];
773
1133
  const contractsRoot = resolvePath(root, config, "contractsDir");
774
- issues.push(...await validateUiContracts(path9.join(contractsRoot, "ui")));
775
- issues.push(...await validateApiContracts(path9.join(contractsRoot, "api")));
776
- issues.push(...await validateDataContracts(path9.join(contractsRoot, "db")));
1134
+ issues.push(...await validateUiContracts(path10.join(contractsRoot, "ui")));
1135
+ issues.push(...await validateApiContracts(path10.join(contractsRoot, "api")));
1136
+ issues.push(...await validateDataContracts(path10.join(contractsRoot, "db")));
777
1137
  return issues;
778
1138
  }
779
1139
  async function validateUiContracts(uiRoot) {
@@ -791,7 +1151,7 @@ async function validateUiContracts(uiRoot) {
791
1151
  }
792
1152
  const issues = [];
793
1153
  for (const file of files) {
794
- const text = await readFile3(file, "utf-8");
1154
+ const text = await readFile4(file, "utf-8");
795
1155
  const invalidIds = extractInvalidIds(text, [
796
1156
  "SPEC",
797
1157
  "BR",
@@ -820,7 +1180,7 @@ async function validateUiContracts(uiRoot) {
820
1180
  issues.push(
821
1181
  issue(
822
1182
  "QFAI-CONTRACT-001",
823
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error2)})`,
1183
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
824
1184
  "error",
825
1185
  file,
826
1186
  "contracts.ui.parse"
@@ -858,7 +1218,7 @@ async function validateApiContracts(apiRoot) {
858
1218
  }
859
1219
  const issues = [];
860
1220
  for (const file of files) {
861
- const text = await readFile3(file, "utf-8");
1221
+ const text = await readFile4(file, "utf-8");
862
1222
  const invalidIds = extractInvalidIds(text, [
863
1223
  "SPEC",
864
1224
  "BR",
@@ -887,7 +1247,7 @@ async function validateApiContracts(apiRoot) {
887
1247
  issues.push(
888
1248
  issue(
889
1249
  "QFAI-CONTRACT-001",
890
- `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error2)})`,
1250
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
891
1251
  "error",
892
1252
  file,
893
1253
  "contracts.api.parse"
@@ -936,7 +1296,7 @@ async function validateDataContracts(dataRoot) {
936
1296
  }
937
1297
  const issues = [];
938
1298
  for (const file of files) {
939
- const text = await readFile3(file, "utf-8");
1299
+ const text = await readFile4(file, "utf-8");
940
1300
  const invalidIds = extractInvalidIds(text, [
941
1301
  "SPEC",
942
1302
  "BR",
@@ -982,7 +1342,7 @@ function lintSql(text, file) {
982
1342
  function hasOpenApi(doc) {
983
1343
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
984
1344
  }
985
- function formatError2(error2) {
1345
+ function formatError4(error2) {
986
1346
  if (error2 instanceof Error) {
987
1347
  return error2.message;
988
1348
  }
@@ -1007,8 +1367,8 @@ function issue(code, message, severity, file, rule, refs) {
1007
1367
  }
1008
1368
 
1009
1369
  // src/core/validators/delta.ts
1010
- import { readFile as readFile4 } from "fs/promises";
1011
- import path10 from "path";
1370
+ import { readFile as readFile5 } from "fs/promises";
1371
+ import path11 from "path";
1012
1372
  var SECTION_RE = /^##\s+変更区分/m;
1013
1373
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1014
1374
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1022,10 +1382,10 @@ async function validateDeltas(root, config) {
1022
1382
  }
1023
1383
  const issues = [];
1024
1384
  for (const pack of packs) {
1025
- const deltaPath = path10.join(pack, "delta.md");
1385
+ const deltaPath = path11.join(pack, "delta.md");
1026
1386
  let text;
1027
1387
  try {
1028
- text = await readFile4(deltaPath, "utf-8");
1388
+ text = await readFile5(deltaPath, "utf-8");
1029
1389
  } catch (error2) {
1030
1390
  if (isMissingFileError2(error2)) {
1031
1391
  issues.push(
@@ -1097,17 +1457,17 @@ function issue2(code, message, severity, file, rule, refs) {
1097
1457
  }
1098
1458
 
1099
1459
  // src/core/validators/ids.ts
1100
- import { readFile as readFile6 } from "fs/promises";
1101
- import path12 from "path";
1460
+ import { readFile as readFile7 } from "fs/promises";
1461
+ import path13 from "path";
1102
1462
 
1103
1463
  // src/core/contractIndex.ts
1104
- import { readFile as readFile5 } from "fs/promises";
1105
- import path11 from "path";
1464
+ import { readFile as readFile6 } from "fs/promises";
1465
+ import path12 from "path";
1106
1466
  async function buildContractIndex(root, config) {
1107
1467
  const contractsRoot = resolvePath(root, config, "contractsDir");
1108
- const uiRoot = path11.join(contractsRoot, "ui");
1109
- const apiRoot = path11.join(contractsRoot, "api");
1110
- const dataRoot = path11.join(contractsRoot, "db");
1468
+ const uiRoot = path12.join(contractsRoot, "ui");
1469
+ const apiRoot = path12.join(contractsRoot, "api");
1470
+ const dataRoot = path12.join(contractsRoot, "db");
1111
1471
  const [uiFiles, apiFiles, dataFiles] = await Promise.all([
1112
1472
  collectUiContractFiles(uiRoot),
1113
1473
  collectApiContractFiles(apiRoot),
@@ -1126,7 +1486,7 @@ async function buildContractIndex(root, config) {
1126
1486
  }
1127
1487
  async function indexUiContracts(files, index) {
1128
1488
  for (const file of files) {
1129
- const text = await readFile5(file, "utf-8");
1489
+ const text = await readFile6(file, "utf-8");
1130
1490
  try {
1131
1491
  const doc = parseStructuredContract(file, text);
1132
1492
  extractUiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1138,7 +1498,7 @@ async function indexUiContracts(files, index) {
1138
1498
  }
1139
1499
  async function indexApiContracts(files, index) {
1140
1500
  for (const file of files) {
1141
- const text = await readFile5(file, "utf-8");
1501
+ const text = await readFile6(file, "utf-8");
1142
1502
  try {
1143
1503
  const doc = parseStructuredContract(file, text);
1144
1504
  extractApiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1150,7 +1510,7 @@ async function indexApiContracts(files, index) {
1150
1510
  }
1151
1511
  async function indexDataContracts(files, index) {
1152
1512
  for (const file of files) {
1153
- const text = await readFile5(file, "utf-8");
1513
+ const text = await readFile6(file, "utf-8");
1154
1514
  extractIds(text, "DATA").forEach((id) => record(index, id, file));
1155
1515
  }
1156
1516
  }
@@ -1197,248 +1557,90 @@ function extractH2Sections(md) {
1197
1557
  startLine,
1198
1558
  endLine,
1199
1559
  body
1200
- });
1201
- }
1202
- return sections;
1203
- }
1204
-
1205
- // src/core/parse/spec.ts
1206
- var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1207
- var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1208
- var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1209
- var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1210
- var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1211
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1212
- function parseSpec(md, file) {
1213
- const headings = parseHeadings(md);
1214
- const h1 = headings.find((heading) => heading.level === 1);
1215
- const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1216
- const sections = extractH2Sections(md);
1217
- const sectionNames = new Set(Array.from(sections.keys()));
1218
- const brSection = sections.get(BR_SECTION_TITLE);
1219
- const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1220
- const startLine = brSection?.startLine ?? 1;
1221
- const brs = [];
1222
- const brsWithoutPriority = [];
1223
- const brsWithInvalidPriority = [];
1224
- for (let i = 0; i < brLines.length; i++) {
1225
- const lineText = brLines[i] ?? "";
1226
- const lineNumber = startLine + i;
1227
- const validMatch = lineText.match(BR_LINE_RE);
1228
- if (validMatch) {
1229
- const id = validMatch[1];
1230
- const priority = validMatch[2];
1231
- const text = validMatch[3];
1232
- if (!id || !priority || !text) continue;
1233
- brs.push({
1234
- id,
1235
- priority,
1236
- text: text.trim(),
1237
- line: lineNumber
1238
- });
1239
- continue;
1240
- }
1241
- const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1242
- if (anyPriorityMatch) {
1243
- const id = anyPriorityMatch[1];
1244
- const priority = anyPriorityMatch[2];
1245
- const text = anyPriorityMatch[3];
1246
- if (!id || !priority || !text) continue;
1247
- if (!VALID_PRIORITIES.has(priority)) {
1248
- brsWithInvalidPriority.push({
1249
- id,
1250
- priority,
1251
- text: text.trim(),
1252
- line: lineNumber
1253
- });
1254
- }
1255
- continue;
1256
- }
1257
- const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1258
- if (noPriorityMatch) {
1259
- const id = noPriorityMatch[1];
1260
- const text = noPriorityMatch[2];
1261
- if (!id || !text) continue;
1262
- brsWithoutPriority.push({
1263
- id,
1264
- text: text.trim(),
1265
- line: lineNumber
1266
- });
1267
- }
1268
- }
1269
- const parsed = {
1270
- file,
1271
- sections: sectionNames,
1272
- brs,
1273
- brsWithoutPriority,
1274
- brsWithInvalidPriority
1275
- };
1276
- if (specId) {
1277
- parsed.specId = specId;
1278
- }
1279
- return parsed;
1280
- }
1281
-
1282
- // src/core/gherkin/parse.ts
1283
- import {
1284
- AstBuilder,
1285
- GherkinClassicTokenMatcher,
1286
- Parser
1287
- } from "@cucumber/gherkin";
1288
- import { randomUUID } from "crypto";
1289
- function parseGherkin(source, uri) {
1290
- const errors = [];
1291
- const uuidFn = () => randomUUID();
1292
- const builder = new AstBuilder(uuidFn);
1293
- const matcher = new GherkinClassicTokenMatcher();
1294
- const parser = new Parser(builder, matcher);
1295
- try {
1296
- const gherkinDocument = parser.parse(source);
1297
- gherkinDocument.uri = uri;
1298
- return { gherkinDocument, errors };
1299
- } catch (error2) {
1300
- errors.push(formatError3(error2));
1301
- return { gherkinDocument: null, errors };
1302
- }
1303
- }
1304
- function formatError3(error2) {
1305
- if (error2 instanceof Error) {
1306
- return error2.message;
1307
- }
1308
- return String(error2);
1309
- }
1310
-
1311
- // src/core/scenarioModel.ts
1312
- var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1313
- var SC_TAG_RE = /^SC-\d{4}$/;
1314
- var BR_TAG_RE = /^BR-\d{4}$/;
1315
- var UI_TAG_RE = /^UI-\d{4}$/;
1316
- var API_TAG_RE = /^API-\d{4}$/;
1317
- var DATA_TAG_RE = /^DATA-\d{4}$/;
1318
- function parseScenarioDocument(text, uri) {
1319
- const { gherkinDocument, errors } = parseGherkin(text, uri);
1320
- if (!gherkinDocument) {
1321
- return { document: null, errors };
1322
- }
1323
- const feature = gherkinDocument.feature;
1324
- if (!feature) {
1325
- return {
1326
- document: { uri, featureTags: [], scenarios: [] },
1327
- errors
1328
- };
1329
- }
1330
- const featureTags = collectTagNames(feature.tags);
1331
- const scenarios = collectScenarioNodes(feature, featureTags);
1332
- return {
1333
- document: {
1334
- uri,
1335
- featureName: feature.name,
1336
- featureTags,
1337
- scenarios
1338
- },
1339
- errors
1340
- };
1341
- }
1342
- function buildScenarioAtoms(document) {
1343
- return document.scenarios.map((scenario) => {
1344
- const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
1345
- const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
1346
- const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1347
- const contractIds = /* @__PURE__ */ new Set();
1348
- scenario.tags.forEach((tag) => {
1349
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1350
- contractIds.add(tag);
1351
- }
1352
- });
1353
- for (const step of scenario.steps) {
1354
- for (const text of collectStepTexts(step)) {
1355
- extractIds(text, "UI").forEach((id) => contractIds.add(id));
1356
- extractIds(text, "API").forEach((id) => contractIds.add(id));
1357
- extractIds(text, "DATA").forEach((id) => contractIds.add(id));
1358
- }
1359
- }
1360
- const atom = {
1361
- uri: document.uri,
1362
- featureName: document.featureName ?? "",
1363
- scenarioName: scenario.name,
1364
- kind: scenario.kind,
1365
- brIds,
1366
- contractIds: Array.from(contractIds).sort()
1367
- };
1368
- if (scenario.line !== void 0) {
1369
- atom.line = scenario.line;
1370
- }
1371
- if (specIds.length === 1) {
1372
- const specId = specIds[0];
1373
- if (specId) {
1374
- atom.specId = specId;
1375
- }
1376
- }
1377
- if (scIds.length === 1) {
1378
- const scId = scIds[0];
1379
- if (scId) {
1380
- atom.scId = scId;
1381
- }
1382
- }
1383
- return atom;
1384
- });
1560
+ });
1561
+ }
1562
+ return sections;
1385
1563
  }
1386
- function collectScenarioNodes(feature, featureTags) {
1387
- const scenarios = [];
1388
- for (const child of feature.children) {
1389
- if (child.scenario) {
1390
- scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
1564
+
1565
+ // src/core/parse/spec.ts
1566
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1567
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1568
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1569
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1570
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1571
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1572
+ function parseSpec(md, file) {
1573
+ const headings = parseHeadings(md);
1574
+ const h1 = headings.find((heading) => heading.level === 1);
1575
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1576
+ const sections = extractH2Sections(md);
1577
+ const sectionNames = new Set(Array.from(sections.keys()));
1578
+ const brSection = sections.get(BR_SECTION_TITLE);
1579
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1580
+ const startLine = brSection?.startLine ?? 1;
1581
+ const brs = [];
1582
+ const brsWithoutPriority = [];
1583
+ const brsWithInvalidPriority = [];
1584
+ for (let i = 0; i < brLines.length; i++) {
1585
+ const lineText = brLines[i] ?? "";
1586
+ const lineNumber = startLine + i;
1587
+ const validMatch = lineText.match(BR_LINE_RE);
1588
+ if (validMatch) {
1589
+ const id = validMatch[1];
1590
+ const priority = validMatch[2];
1591
+ const text = validMatch[3];
1592
+ if (!id || !priority || !text) continue;
1593
+ brs.push({
1594
+ id,
1595
+ priority,
1596
+ text: text.trim(),
1597
+ line: lineNumber
1598
+ });
1599
+ continue;
1391
1600
  }
1392
- if (child.rule) {
1393
- const ruleTags = collectTagNames(child.rule.tags);
1394
- for (const ruleChild of child.rule.children) {
1395
- if (ruleChild.scenario) {
1396
- scenarios.push(
1397
- buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
1398
- );
1399
- }
1601
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1602
+ if (anyPriorityMatch) {
1603
+ const id = anyPriorityMatch[1];
1604
+ const priority = anyPriorityMatch[2];
1605
+ const text = anyPriorityMatch[3];
1606
+ if (!id || !priority || !text) continue;
1607
+ if (!VALID_PRIORITIES.has(priority)) {
1608
+ brsWithInvalidPriority.push({
1609
+ id,
1610
+ priority,
1611
+ text: text.trim(),
1612
+ line: lineNumber
1613
+ });
1400
1614
  }
1615
+ continue;
1616
+ }
1617
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1618
+ if (noPriorityMatch) {
1619
+ const id = noPriorityMatch[1];
1620
+ const text = noPriorityMatch[2];
1621
+ if (!id || !text) continue;
1622
+ brsWithoutPriority.push({
1623
+ id,
1624
+ text: text.trim(),
1625
+ line: lineNumber
1626
+ });
1401
1627
  }
1402
1628
  }
1403
- return scenarios;
1404
- }
1405
- function buildScenarioNode(scenario, featureTags, ruleTags) {
1406
- const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
1407
- const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
1408
- return {
1409
- name: scenario.name,
1410
- kind,
1411
- line: scenario.location?.line,
1412
- tags,
1413
- steps: scenario.steps
1629
+ const parsed = {
1630
+ file,
1631
+ sections: sectionNames,
1632
+ brs,
1633
+ brsWithoutPriority,
1634
+ brsWithInvalidPriority
1414
1635
  };
1415
- }
1416
- function collectTagNames(tags) {
1417
- return tags.map((tag) => tag.name.replace(/^@/, ""));
1418
- }
1419
- function collectStepTexts(step) {
1420
- const texts = [];
1421
- if (step.text) {
1422
- texts.push(step.text);
1423
- }
1424
- if (step.docString?.content) {
1425
- texts.push(step.docString.content);
1426
- }
1427
- if (step.dataTable?.rows) {
1428
- for (const row of step.dataTable.rows) {
1429
- for (const cell of row.cells) {
1430
- texts.push(cell.value);
1431
- }
1432
- }
1636
+ if (specId) {
1637
+ parsed.specId = specId;
1433
1638
  }
1434
- return texts;
1435
- }
1436
- function unique2(values) {
1437
- return Array.from(new Set(values));
1639
+ return parsed;
1438
1640
  }
1439
1641
 
1440
1642
  // src/core/validators/ids.ts
1441
- var SC_TAG_RE2 = /^SC-\d{4}$/;
1643
+ var SC_TAG_RE3 = /^SC-\d{4}$/;
1442
1644
  async function validateDefinedIds(root, config) {
1443
1645
  const issues = [];
1444
1646
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1472,7 +1674,7 @@ async function validateDefinedIds(root, config) {
1472
1674
  }
1473
1675
  async function collectSpecDefinitionIds(files, out) {
1474
1676
  for (const file of files) {
1475
- const text = await readFile6(file, "utf-8");
1677
+ const text = await readFile7(file, "utf-8");
1476
1678
  const parsed = parseSpec(text, file);
1477
1679
  if (parsed.specId) {
1478
1680
  recordId(out, parsed.specId, file);
@@ -1482,14 +1684,14 @@ async function collectSpecDefinitionIds(files, out) {
1482
1684
  }
1483
1685
  async function collectScenarioDefinitionIds(files, out) {
1484
1686
  for (const file of files) {
1485
- const text = await readFile6(file, "utf-8");
1687
+ const text = await readFile7(file, "utf-8");
1486
1688
  const { document, errors } = parseScenarioDocument(text, file);
1487
1689
  if (!document || errors.length > 0) {
1488
1690
  continue;
1489
1691
  }
1490
1692
  for (const scenario of document.scenarios) {
1491
1693
  for (const tag of scenario.tags) {
1492
- if (SC_TAG_RE2.test(tag)) {
1694
+ if (SC_TAG_RE3.test(tag)) {
1493
1695
  recordId(out, tag, file);
1494
1696
  }
1495
1697
  }
@@ -1503,7 +1705,7 @@ function recordId(out, id, file) {
1503
1705
  }
1504
1706
  function formatFileList(files, root) {
1505
1707
  return files.map((file) => {
1506
- const relative = path12.relative(root, file);
1708
+ const relative = path13.relative(root, file);
1507
1709
  return relative.length > 0 ? relative : file;
1508
1710
  }).join(", ");
1509
1711
  }
@@ -1526,13 +1728,12 @@ function issue3(code, message, severity, file, rule, refs) {
1526
1728
  }
1527
1729
 
1528
1730
  // src/core/validators/scenario.ts
1529
- import { readFile as readFile7 } from "fs/promises";
1731
+ import { readFile as readFile8 } from "fs/promises";
1530
1732
  var GIVEN_PATTERN = /\bGiven\b/;
1531
1733
  var WHEN_PATTERN = /\bWhen\b/;
1532
1734
  var THEN_PATTERN = /\bThen\b/;
1533
- var SC_TAG_RE3 = /^SC-\d{4}$/;
1735
+ var SC_TAG_RE4 = /^SC-\d{4}$/;
1534
1736
  var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1535
- var BR_TAG_RE2 = /^BR-\d{4}$/;
1536
1737
  async function validateScenarios(root, config) {
1537
1738
  const specsRoot = resolvePath(root, config, "specsDir");
1538
1739
  const entries = await collectSpecEntries(specsRoot);
@@ -1553,7 +1754,7 @@ async function validateScenarios(root, config) {
1553
1754
  for (const entry of entries) {
1554
1755
  let text;
1555
1756
  try {
1556
- text = await readFile7(entry.scenarioPath, "utf-8");
1757
+ text = await readFile8(entry.scenarioPath, "utf-8");
1557
1758
  } catch (error2) {
1558
1759
  if (isMissingFileError3(error2)) {
1559
1760
  issues.push(
@@ -1612,17 +1813,7 @@ function validateScenarioContent(text, file) {
1612
1813
  const featureSpecTags = document.featureTags.filter(
1613
1814
  (tag) => SPEC_TAG_RE2.test(tag)
1614
1815
  );
1615
- if (featureSpecTags.length === 0) {
1616
- issues.push(
1617
- issue4(
1618
- "QFAI-SC-009",
1619
- "Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1620
- "error",
1621
- file,
1622
- "scenario.featureSpec"
1623
- )
1624
- );
1625
- } else if (featureSpecTags.length > 1) {
1816
+ if (featureSpecTags.length > 1) {
1626
1817
  issues.push(
1627
1818
  issue4(
1628
1819
  "QFAI-SC-009",
@@ -1664,18 +1855,12 @@ function validateScenarioContent(text, file) {
1664
1855
  continue;
1665
1856
  }
1666
1857
  const missingTags = [];
1667
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
1858
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
1668
1859
  if (scTags.length === 0) {
1669
1860
  missingTags.push("SC(0\u4EF6)");
1670
1861
  } else if (scTags.length > 1) {
1671
1862
  missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1672
1863
  }
1673
- if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
1674
- missingTags.push("SPEC");
1675
- }
1676
- if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
1677
- missingTags.push("BR");
1678
- }
1679
1864
  if (missingTags.length > 0) {
1680
1865
  issues.push(
1681
1866
  issue4(
@@ -1739,7 +1924,7 @@ function isMissingFileError3(error2) {
1739
1924
  }
1740
1925
 
1741
1926
  // src/core/validators/spec.ts
1742
- import { readFile as readFile8 } from "fs/promises";
1927
+ import { readFile as readFile9 } from "fs/promises";
1743
1928
  async function validateSpecs(root, config) {
1744
1929
  const specsRoot = resolvePath(root, config, "specsDir");
1745
1930
  const entries = await collectSpecEntries(specsRoot);
@@ -1760,7 +1945,7 @@ async function validateSpecs(root, config) {
1760
1945
  for (const entry of entries) {
1761
1946
  let text;
1762
1947
  try {
1763
- text = await readFile8(entry.specPath, "utf-8");
1948
+ text = await readFile9(entry.specPath, "utf-8");
1764
1949
  } catch (error2) {
1765
1950
  if (isMissingFileError4(error2)) {
1766
1951
  issues.push(
@@ -1909,10 +2094,9 @@ function isMissingFileError4(error2) {
1909
2094
  }
1910
2095
 
1911
2096
  // src/core/validators/traceability.ts
1912
- import { readFile as readFile9 } from "fs/promises";
1913
- var SC_TAG_RE4 = /^SC-\d{4}$/;
2097
+ import { readFile as readFile10 } from "fs/promises";
1914
2098
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
1915
- var BR_TAG_RE3 = /^BR-\d{4}$/;
2099
+ var BR_TAG_RE2 = /^BR-\d{4}$/;
1916
2100
  async function validateTraceability(root, config) {
1917
2101
  const issues = [];
1918
2102
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1931,7 +2115,7 @@ async function validateTraceability(root, config) {
1931
2115
  const contractIndex = await buildContractIndex(root, config);
1932
2116
  const contractIds = contractIndex.ids;
1933
2117
  for (const file of specFiles) {
1934
- const text = await readFile9(file, "utf-8");
2118
+ const text = await readFile10(file, "utf-8");
1935
2119
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1936
2120
  const parsed = parseSpec(text, file);
1937
2121
  if (parsed.specId) {
@@ -1939,28 +2123,6 @@ async function validateTraceability(root, config) {
1939
2123
  }
1940
2124
  const brIds = parsed.brs.map((br) => br.id);
1941
2125
  brIds.forEach((id) => brIdsInSpecs.add(id));
1942
- const referencedContractIds = /* @__PURE__ */ new Set([
1943
- ...extractIds(text, "UI"),
1944
- ...extractIds(text, "API"),
1945
- ...extractIds(text, "DATA")
1946
- ]);
1947
- const unknownContractIds = Array.from(referencedContractIds).filter(
1948
- (id) => !contractIds.has(id)
1949
- );
1950
- if (unknownContractIds.length > 0) {
1951
- issues.push(
1952
- issue6(
1953
- "QFAI-TRACE-009",
1954
- `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1955
- ", "
1956
- )}`,
1957
- "error",
1958
- file,
1959
- "traceability.specContractExists",
1960
- unknownContractIds
1961
- )
1962
- );
1963
- }
1964
2126
  if (parsed.specId) {
1965
2127
  const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
1966
2128
  brIds.forEach((id) => current.add(id));
@@ -1968,23 +2130,49 @@ async function validateTraceability(root, config) {
1968
2130
  }
1969
2131
  }
1970
2132
  for (const file of scenarioFiles) {
1971
- const text = await readFile9(file, "utf-8");
2133
+ const text = await readFile10(file, "utf-8");
1972
2134
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1973
2135
  const { document, errors } = parseScenarioDocument(text, file);
1974
2136
  if (!document || errors.length > 0) {
1975
2137
  continue;
1976
2138
  }
1977
2139
  const atoms = buildScenarioAtoms(document);
2140
+ const scIdsInFile = /* @__PURE__ */ new Set();
1978
2141
  for (const [index, scenario] of document.scenarios.entries()) {
1979
2142
  const atom = atoms[index];
1980
2143
  if (!atom) {
1981
2144
  continue;
1982
2145
  }
1983
2146
  const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
1984
- const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
1985
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
2147
+ const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
2148
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
2149
+ if (specTags.length === 0) {
2150
+ issues.push(
2151
+ issue6(
2152
+ "QFAI-TRACE-014",
2153
+ `Scenario \u304C SPEC \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
2154
+ "error",
2155
+ file,
2156
+ "traceability.scenarioSpecRequired"
2157
+ )
2158
+ );
2159
+ }
2160
+ if (brTags.length === 0) {
2161
+ issues.push(
2162
+ issue6(
2163
+ "QFAI-TRACE-015",
2164
+ `Scenario \u304C BR \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
2165
+ "error",
2166
+ file,
2167
+ "traceability.scenarioBrRequired"
2168
+ )
2169
+ );
2170
+ }
1986
2171
  brTags.forEach((id) => brIdsInScenarios.add(id));
1987
- scTags.forEach((id) => scIdsInScenarios.add(id));
2172
+ scTags.forEach((id) => {
2173
+ scIdsInScenarios.add(id);
2174
+ scIdsInFile.add(id);
2175
+ });
1988
2176
  atom.contractIds.forEach((id) => scenarioContractIds.add(id));
1989
2177
  if (atom.contractIds.length > 0) {
1990
2178
  scTags.forEach((id) => scWithContracts.add(id));
@@ -2062,6 +2250,22 @@ async function validateTraceability(root, config) {
2062
2250
  }
2063
2251
  }
2064
2252
  }
2253
+ if (scIdsInFile.size !== 1) {
2254
+ const invalidScIds = Array.from(scIdsInFile).sort(
2255
+ (a, b) => a.localeCompare(b)
2256
+ );
2257
+ 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(", ")}`;
2258
+ issues.push(
2259
+ issue6(
2260
+ "QFAI-TRACE-012",
2261
+ `Spec entry \u304C Spec:SC=1:1 \u3092\u6E80\u305F\u3057\u3066\u3044\u307E\u305B\u3093: ${detail}`,
2262
+ "error",
2263
+ file,
2264
+ "traceability.specScOneToOne",
2265
+ invalidScIds
2266
+ )
2267
+ );
2268
+ }
2065
2269
  }
2066
2270
  if (upstreamIds.size === 0) {
2067
2271
  return [
@@ -2110,6 +2314,66 @@ async function validateTraceability(root, config) {
2110
2314
  );
2111
2315
  }
2112
2316
  }
2317
+ const scRefsResult = await collectScTestReferences(
2318
+ root,
2319
+ config.validation.traceability.testFileGlobs,
2320
+ config.validation.traceability.testFileExcludeGlobs
2321
+ );
2322
+ const scTestRefs = scRefsResult.refs;
2323
+ const testFileScan = scRefsResult.scan;
2324
+ const hasScenarios = scIdsInScenarios.size > 0;
2325
+ const hasGlobConfig = testFileScan.globs.length > 0;
2326
+ const hasMatchedTests = testFileScan.matchedFileCount > 0;
2327
+ if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
2328
+ const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
2329
+ issues.push(
2330
+ issue6(
2331
+ "QFAI-TRACE-013",
2332
+ `\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}`,
2333
+ "error",
2334
+ testsRoot,
2335
+ "traceability.testFileGlobs"
2336
+ )
2337
+ );
2338
+ } else {
2339
+ if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2340
+ const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2341
+ const refs = scTestRefs.get(id);
2342
+ return !refs || refs.size === 0;
2343
+ });
2344
+ if (scWithoutTests.length > 0) {
2345
+ issues.push(
2346
+ issue6(
2347
+ "QFAI-TRACE-010",
2348
+ `SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(
2349
+ ", "
2350
+ )}\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`,
2351
+ config.validation.traceability.scNoTestSeverity,
2352
+ testsRoot,
2353
+ "traceability.scMustHaveTest",
2354
+ scWithoutTests
2355
+ )
2356
+ );
2357
+ }
2358
+ }
2359
+ const unknownScIds = Array.from(scTestRefs.keys()).filter(
2360
+ (id) => !scIdsInScenarios.has(id)
2361
+ );
2362
+ if (unknownScIds.length > 0) {
2363
+ issues.push(
2364
+ issue6(
2365
+ "QFAI-TRACE-011",
2366
+ `\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(
2367
+ ", "
2368
+ )}`,
2369
+ "error",
2370
+ testsRoot,
2371
+ "traceability.scUnknownInTests",
2372
+ unknownScIds
2373
+ )
2374
+ );
2375
+ }
2376
+ }
2113
2377
  if (!config.validation.traceability.allowOrphanContracts) {
2114
2378
  if (contractIds.size > 0) {
2115
2379
  const orphanContracts = Array.from(contractIds).filter(
@@ -2158,7 +2422,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2158
2422
  const pattern = buildIdPattern(Array.from(upstreamIds));
2159
2423
  let found = false;
2160
2424
  for (const file of targetFiles) {
2161
- const text = await readFile9(file, "utf-8");
2425
+ const text = await readFile10(file, "utf-8");
2162
2426
  if (pattern.test(text)) {
2163
2427
  found = true;
2164
2428
  break;
@@ -2168,8 +2432,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2168
2432
  issues.push(
2169
2433
  issue6(
2170
2434
  "QFAI-TRACE-002",
2171
- "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
2172
- "warning",
2435
+ "\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",
2436
+ "info",
2173
2437
  srcRoot,
2174
2438
  "traceability.codeReferences"
2175
2439
  )
@@ -2212,12 +2476,24 @@ async function validateProject(root, configResult) {
2212
2476
  ...await validateDefinedIds(root, config),
2213
2477
  ...await validateTraceability(root, config)
2214
2478
  ];
2479
+ const specsRoot = resolvePath(root, config, "specsDir");
2480
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
2481
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2482
+ const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
2483
+ root,
2484
+ config.validation.traceability.testFileGlobs,
2485
+ config.validation.traceability.testFileExcludeGlobs
2486
+ );
2487
+ const scCoverage = buildScCoverage(scIds, scTestRefs);
2215
2488
  const toolVersion = await resolveToolVersion();
2216
2489
  return {
2217
- schemaVersion: VALIDATION_SCHEMA_VERSION,
2218
2490
  toolVersion,
2219
2491
  issues,
2220
- counts: countIssues(issues)
2492
+ counts: countIssues(issues),
2493
+ traceability: {
2494
+ sc: scCoverage,
2495
+ testFiles
2496
+ }
2221
2497
  };
2222
2498
  }
2223
2499
  function countIssues(issues) {
@@ -2238,9 +2514,9 @@ async function createReportData(root, validation, configResult) {
2238
2514
  const configPath = resolved.configPath;
2239
2515
  const specsRoot = resolvePath(root, config, "specsDir");
2240
2516
  const contractsRoot = resolvePath(root, config, "contractsDir");
2241
- const apiRoot = path13.join(contractsRoot, "api");
2242
- const uiRoot = path13.join(contractsRoot, "ui");
2243
- const dbRoot = path13.join(contractsRoot, "db");
2517
+ const apiRoot = path14.join(contractsRoot, "api");
2518
+ const uiRoot = path14.join(contractsRoot, "ui");
2519
+ const dbRoot = path14.join(contractsRoot, "db");
2244
2520
  const srcRoot = resolvePath(root, config, "srcDir");
2245
2521
  const testsRoot = resolvePath(root, config, "testsDir");
2246
2522
  const specFiles = await collectSpecFiles(specsRoot);
@@ -2266,6 +2542,16 @@ async function createReportData(root, validation, configResult) {
2266
2542
  srcRoot,
2267
2543
  testsRoot
2268
2544
  );
2545
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2546
+ const scRefsResult = await collectScTestReferences(
2547
+ root,
2548
+ config.validation.traceability.testFileGlobs,
2549
+ config.validation.traceability.testFileExcludeGlobs
2550
+ );
2551
+ const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
2552
+ const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
2553
+ const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
2554
+ const scSourceRecord = mapToSortedRecord(scSources);
2269
2555
  const resolvedValidation = validation ?? await validateProject(root, resolved);
2270
2556
  const version = await resolveToolVersion();
2271
2557
  return {
@@ -2294,7 +2580,10 @@ async function createReportData(root, validation, configResult) {
2294
2580
  },
2295
2581
  traceability: {
2296
2582
  upstreamIdsFound: upstreamIds.size,
2297
- referencedInCodeOrTests: traceability
2583
+ referencedInCodeOrTests: traceability,
2584
+ sc: scCoverage,
2585
+ scSources: scSourceRecord,
2586
+ testFiles
2298
2587
  },
2299
2588
  issues: resolvedValidation.issues
2300
2589
  };
@@ -2331,6 +2620,65 @@ function formatReportMarkdown(data) {
2331
2620
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2332
2621
  );
2333
2622
  lines.push("");
2623
+ lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2624
+ lines.push(`- total: ${data.traceability.sc.total}`);
2625
+ lines.push(`- covered: ${data.traceability.sc.covered}`);
2626
+ lines.push(`- missing: ${data.traceability.sc.missing}`);
2627
+ lines.push(
2628
+ `- testFileGlobs: ${formatList(data.traceability.testFiles.globs)}`
2629
+ );
2630
+ lines.push(
2631
+ `- testFileExcludeGlobs: ${formatList(
2632
+ data.traceability.testFiles.excludeGlobs
2633
+ )}`
2634
+ );
2635
+ lines.push(
2636
+ `- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
2637
+ );
2638
+ if (data.traceability.sc.missingIds.length === 0) {
2639
+ lines.push("- missingIds: (none)");
2640
+ } else {
2641
+ const sources = data.traceability.scSources;
2642
+ const missingWithSources = data.traceability.sc.missingIds.map((id) => {
2643
+ const files = sources[id] ?? [];
2644
+ if (files.length === 0) {
2645
+ return id;
2646
+ }
2647
+ return `${id} (${files.join(", ")})`;
2648
+ });
2649
+ lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
2650
+ }
2651
+ lines.push("");
2652
+ lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
2653
+ const scRefs = data.traceability.sc.refs;
2654
+ const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
2655
+ if (scIds.length === 0) {
2656
+ lines.push("- (none)");
2657
+ } else {
2658
+ for (const scId of scIds) {
2659
+ const refs = scRefs[scId] ?? [];
2660
+ if (refs.length === 0) {
2661
+ lines.push(`- ${scId}: (none)`);
2662
+ } else {
2663
+ lines.push(`- ${scId}: ${refs.join(", ")}`);
2664
+ }
2665
+ }
2666
+ }
2667
+ lines.push("");
2668
+ lines.push("## Spec:SC=1:1 \u9055\u53CD");
2669
+ const specScIssues = data.issues.filter(
2670
+ (item) => item.code === "QFAI-TRACE-012"
2671
+ );
2672
+ if (specScIssues.length === 0) {
2673
+ lines.push("- (none)");
2674
+ } else {
2675
+ for (const item of specScIssues) {
2676
+ const location = item.file ?? "(unknown)";
2677
+ const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
2678
+ lines.push(`- ${location}: ${refs}`);
2679
+ }
2680
+ }
2681
+ lines.push("");
2334
2682
  lines.push("## Hotspots");
2335
2683
  const hotspots = buildHotspots(data.issues);
2336
2684
  if (hotspots.length === 0) {
@@ -2385,25 +2733,25 @@ async function collectIds(files) {
2385
2733
  DATA: /* @__PURE__ */ new Set()
2386
2734
  };
2387
2735
  for (const file of files) {
2388
- const text = await readFile10(file, "utf-8");
2736
+ const text = await readFile11(file, "utf-8");
2389
2737
  for (const prefix of ID_PREFIXES2) {
2390
2738
  const ids = extractIds(text, prefix);
2391
2739
  ids.forEach((id) => result[prefix].add(id));
2392
2740
  }
2393
2741
  }
2394
2742
  return {
2395
- SPEC: toSortedArray(result.SPEC),
2396
- BR: toSortedArray(result.BR),
2397
- SC: toSortedArray(result.SC),
2398
- UI: toSortedArray(result.UI),
2399
- API: toSortedArray(result.API),
2400
- DATA: toSortedArray(result.DATA)
2743
+ SPEC: toSortedArray2(result.SPEC),
2744
+ BR: toSortedArray2(result.BR),
2745
+ SC: toSortedArray2(result.SC),
2746
+ UI: toSortedArray2(result.UI),
2747
+ API: toSortedArray2(result.API),
2748
+ DATA: toSortedArray2(result.DATA)
2401
2749
  };
2402
2750
  }
2403
2751
  async function collectUpstreamIds(files) {
2404
2752
  const ids = /* @__PURE__ */ new Set();
2405
2753
  for (const file of files) {
2406
- const text = await readFile10(file, "utf-8");
2754
+ const text = await readFile11(file, "utf-8");
2407
2755
  extractAllIds(text).forEach((id) => ids.add(id));
2408
2756
  }
2409
2757
  return ids;
@@ -2424,7 +2772,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
2424
2772
  }
2425
2773
  const pattern = buildIdPattern2(Array.from(upstreamIds));
2426
2774
  for (const file of targetFiles) {
2427
- const text = await readFile10(file, "utf-8");
2775
+ const text = await readFile11(file, "utf-8");
2428
2776
  if (pattern.test(text)) {
2429
2777
  return true;
2430
2778
  }
@@ -2441,9 +2789,22 @@ function formatIdLine(label, values) {
2441
2789
  }
2442
2790
  return `- ${label}: ${values.join(", ")}`;
2443
2791
  }
2444
- function toSortedArray(values) {
2792
+ function formatList(values) {
2793
+ if (values.length === 0) {
2794
+ return "(none)";
2795
+ }
2796
+ return values.join(", ");
2797
+ }
2798
+ function toSortedArray2(values) {
2445
2799
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2446
2800
  }
2801
+ function mapToSortedRecord(values) {
2802
+ const record2 = {};
2803
+ for (const [key, files] of values.entries()) {
2804
+ record2[key] = Array.from(files).sort((a, b) => a.localeCompare(b));
2805
+ }
2806
+ return record2;
2807
+ }
2447
2808
  function buildHotspots(issues) {
2448
2809
  const map = /* @__PURE__ */ new Map();
2449
2810
  for (const issue7 of issues) {
@@ -2468,10 +2829,10 @@ function buildHotspots(issues) {
2468
2829
 
2469
2830
  // src/cli/commands/report.ts
2470
2831
  async function runReport(options) {
2471
- const root = path14.resolve(options.root);
2832
+ const root = path15.resolve(options.root);
2472
2833
  const configResult = await loadConfig(root);
2473
2834
  const input = configResult.config.output.validateJsonPath;
2474
- const inputPath = path14.isAbsolute(input) ? input : path14.resolve(root, input);
2835
+ const inputPath = path15.isAbsolute(input) ? input : path15.resolve(root, input);
2475
2836
  let validation;
2476
2837
  try {
2477
2838
  validation = await readValidationResult(inputPath);
@@ -2496,10 +2857,10 @@ async function runReport(options) {
2496
2857
  const data = await createReportData(root, validation, configResult);
2497
2858
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
2498
2859
  const outRoot = resolvePath(root, configResult.config, "outDir");
2499
- const defaultOut = options.format === "json" ? path14.join(outRoot, "report.json") : path14.join(outRoot, "report.md");
2860
+ const defaultOut = options.format === "json" ? path15.join(outRoot, "report.json") : path15.join(outRoot, "report.md");
2500
2861
  const out = options.outPath ?? defaultOut;
2501
- const outPath = path14.isAbsolute(out) ? out : path14.resolve(root, out);
2502
- await mkdir2(path14.dirname(outPath), { recursive: true });
2862
+ const outPath = path15.isAbsolute(out) ? out : path15.resolve(root, out);
2863
+ await mkdir2(path15.dirname(outPath), { recursive: true });
2503
2864
  await writeFile(outPath, `${output}
2504
2865
  `, "utf-8");
2505
2866
  info(
@@ -2508,16 +2869,11 @@ async function runReport(options) {
2508
2869
  info(`wrote report: ${outPath}`);
2509
2870
  }
2510
2871
  async function readValidationResult(inputPath) {
2511
- const raw = await readFile11(inputPath, "utf-8");
2872
+ const raw = await readFile12(inputPath, "utf-8");
2512
2873
  const parsed = JSON.parse(raw);
2513
2874
  if (!isValidationResult(parsed)) {
2514
2875
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
2515
2876
  }
2516
- if (parsed.schemaVersion !== VALIDATION_SCHEMA_VERSION) {
2517
- throw new Error(
2518
- `validate.json \u306E schemaVersion \u304C\u4E0D\u4E00\u81F4\u3067\u3059: expected ${VALIDATION_SCHEMA_VERSION}, actual ${parsed.schemaVersion}`
2519
- );
2520
- }
2521
2877
  return parsed;
2522
2878
  }
2523
2879
  function isValidationResult(value) {
@@ -2525,9 +2881,6 @@ function isValidationResult(value) {
2525
2881
  return false;
2526
2882
  }
2527
2883
  const record2 = value;
2528
- if (typeof record2.schemaVersion !== "string") {
2529
- return false;
2530
- }
2531
2884
  if (typeof record2.toolVersion !== "string") {
2532
2885
  return false;
2533
2886
  }
@@ -2550,7 +2903,7 @@ function isMissingFileError5(error2) {
2550
2903
 
2551
2904
  // src/cli/commands/validate.ts
2552
2905
  import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
2553
- import path15 from "path";
2906
+ import path16 from "path";
2554
2907
 
2555
2908
  // src/cli/lib/failOn.ts
2556
2909
  function shouldFail(result, failOn) {
@@ -2565,7 +2918,7 @@ function shouldFail(result, failOn) {
2565
2918
 
2566
2919
  // src/cli/commands/validate.ts
2567
2920
  async function runValidate(options) {
2568
- const root = path15.resolve(options.root);
2921
+ const root = path16.resolve(options.root);
2569
2922
  const configResult = await loadConfig(root);
2570
2923
  const result = await validateProject(root, configResult);
2571
2924
  const format = options.format ?? "text";
@@ -2614,8 +2967,8 @@ function emitGitHub(issue7) {
2614
2967
  );
2615
2968
  }
2616
2969
  async function emitJson(result, root, jsonPath) {
2617
- const abs = path15.isAbsolute(jsonPath) ? jsonPath : path15.resolve(root, jsonPath);
2618
- await mkdir3(path15.dirname(abs), { recursive: true });
2970
+ const abs = path16.isAbsolute(jsonPath) ? jsonPath : path16.resolve(root, jsonPath);
2971
+ await mkdir3(path16.dirname(abs), { recursive: true });
2619
2972
  await writeFile2(abs, `${JSON.stringify(result, null, 2)}
2620
2973
  `, "utf-8");
2621
2974
  }