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
package/dist/index.mjs CHANGED
@@ -28,6 +28,10 @@ var defaultConfig = {
28
28
  traceability: {
29
29
  brMustHaveSc: true,
30
30
  scMustTouchContracts: true,
31
+ scMustHaveTest: true,
32
+ testFileGlobs: [],
33
+ testFileExcludeGlobs: [],
34
+ scNoTestSeverity: "error",
31
35
  allowOrphanContracts: false,
32
36
  unknownContractIdSeverity: "error"
33
37
  }
@@ -207,6 +211,34 @@ function normalizeValidation(raw, configPath, issues) {
207
211
  configPath,
208
212
  issues
209
213
  ),
214
+ scMustHaveTest: readBoolean(
215
+ traceabilityRaw?.scMustHaveTest,
216
+ base.traceability.scMustHaveTest,
217
+ "validation.traceability.scMustHaveTest",
218
+ configPath,
219
+ issues
220
+ ),
221
+ testFileGlobs: readStringArray(
222
+ traceabilityRaw?.testFileGlobs,
223
+ base.traceability.testFileGlobs,
224
+ "validation.traceability.testFileGlobs",
225
+ configPath,
226
+ issues
227
+ ),
228
+ testFileExcludeGlobs: readStringArray(
229
+ traceabilityRaw?.testFileExcludeGlobs,
230
+ base.traceability.testFileExcludeGlobs,
231
+ "validation.traceability.testFileExcludeGlobs",
232
+ configPath,
233
+ issues
234
+ ),
235
+ scNoTestSeverity: readTraceabilitySeverity(
236
+ traceabilityRaw?.scNoTestSeverity,
237
+ base.traceability.scNoTestSeverity,
238
+ "validation.traceability.scNoTestSeverity",
239
+ configPath,
240
+ issues
241
+ ),
210
242
  allowOrphanContracts: readBoolean(
211
243
  traceabilityRaw?.allowOrphanContracts,
212
244
  base.traceability.allowOrphanContracts,
@@ -385,8 +417,8 @@ function isValidId(value, prefix) {
385
417
  }
386
418
 
387
419
  // src/core/report.ts
388
- import { readFile as readFile10 } from "fs/promises";
389
- import path10 from "path";
420
+ import { readFile as readFile11 } from "fs/promises";
421
+ import path11 from "path";
390
422
 
391
423
  // src/core/discovery.ts
392
424
  import { access as access2 } from "fs/promises";
@@ -394,6 +426,7 @@ import { access as access2 } from "fs/promises";
394
426
  // src/core/fs.ts
395
427
  import { access, readdir } from "fs/promises";
396
428
  import path2 from "path";
429
+ import fg from "fast-glob";
397
430
  var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
398
431
  "node_modules",
399
432
  ".git",
@@ -415,6 +448,18 @@ async function collectFiles(root, options = {}) {
415
448
  await walk(root, root, ignoreDirs, extensions, entries);
416
449
  return entries;
417
450
  }
451
+ async function collectFilesByGlobs(root, options) {
452
+ if (options.globs.length === 0) {
453
+ return [];
454
+ }
455
+ return fg(options.globs, {
456
+ cwd: root,
457
+ ignore: options.ignore ?? [],
458
+ onlyFiles: true,
459
+ absolute: true,
460
+ unique: true
461
+ });
462
+ }
418
463
  async function walk(base, current, ignoreDirs, extensions, out) {
419
464
  const items = await readdir(current, { withFileTypes: true });
420
465
  for (const item of items) {
@@ -526,20 +571,335 @@ async function exists2(target) {
526
571
  }
527
572
  }
528
573
 
529
- // src/core/types.ts
530
- var VALIDATION_SCHEMA_VERSION = "0.2";
531
-
532
- // src/core/version.ts
574
+ // src/core/traceability.ts
533
575
  import { readFile as readFile2 } from "fs/promises";
534
576
  import path4 from "path";
577
+
578
+ // src/core/gherkin/parse.ts
579
+ import {
580
+ AstBuilder,
581
+ GherkinClassicTokenMatcher,
582
+ Parser
583
+ } from "@cucumber/gherkin";
584
+ import { randomUUID } from "crypto";
585
+ function parseGherkin(source, uri) {
586
+ const errors = [];
587
+ const uuidFn = () => randomUUID();
588
+ const builder = new AstBuilder(uuidFn);
589
+ const matcher = new GherkinClassicTokenMatcher();
590
+ const parser = new Parser(builder, matcher);
591
+ try {
592
+ const gherkinDocument = parser.parse(source);
593
+ gherkinDocument.uri = uri;
594
+ return { gherkinDocument, errors };
595
+ } catch (error) {
596
+ errors.push(formatError2(error));
597
+ return { gherkinDocument: null, errors };
598
+ }
599
+ }
600
+ function formatError2(error) {
601
+ if (error instanceof Error) {
602
+ return error.message;
603
+ }
604
+ return String(error);
605
+ }
606
+
607
+ // src/core/scenarioModel.ts
608
+ var SPEC_TAG_RE = /^SPEC-\d{4}$/;
609
+ var SC_TAG_RE = /^SC-\d{4}$/;
610
+ var BR_TAG_RE = /^BR-\d{4}$/;
611
+ var UI_TAG_RE = /^UI-\d{4}$/;
612
+ var API_TAG_RE = /^API-\d{4}$/;
613
+ var DATA_TAG_RE = /^DATA-\d{4}$/;
614
+ function parseScenarioDocument(text, uri) {
615
+ const { gherkinDocument, errors } = parseGherkin(text, uri);
616
+ if (!gherkinDocument) {
617
+ return { document: null, errors };
618
+ }
619
+ const feature = gherkinDocument.feature;
620
+ if (!feature) {
621
+ return {
622
+ document: { uri, featureTags: [], scenarios: [] },
623
+ errors
624
+ };
625
+ }
626
+ const featureTags = collectTagNames(feature.tags);
627
+ const scenarios = collectScenarioNodes(feature, featureTags);
628
+ return {
629
+ document: {
630
+ uri,
631
+ featureName: feature.name,
632
+ featureTags,
633
+ scenarios
634
+ },
635
+ errors
636
+ };
637
+ }
638
+ function buildScenarioAtoms(document) {
639
+ return document.scenarios.map((scenario) => {
640
+ const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
641
+ const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
642
+ const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
643
+ const contractIds = /* @__PURE__ */ new Set();
644
+ scenario.tags.forEach((tag) => {
645
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
646
+ contractIds.add(tag);
647
+ }
648
+ });
649
+ for (const step of scenario.steps) {
650
+ for (const text of collectStepTexts(step)) {
651
+ extractIds(text, "UI").forEach((id) => contractIds.add(id));
652
+ extractIds(text, "API").forEach((id) => contractIds.add(id));
653
+ extractIds(text, "DATA").forEach((id) => contractIds.add(id));
654
+ }
655
+ }
656
+ const atom = {
657
+ uri: document.uri,
658
+ featureName: document.featureName ?? "",
659
+ scenarioName: scenario.name,
660
+ kind: scenario.kind,
661
+ brIds,
662
+ contractIds: Array.from(contractIds).sort()
663
+ };
664
+ if (scenario.line !== void 0) {
665
+ atom.line = scenario.line;
666
+ }
667
+ if (specIds.length === 1) {
668
+ const specId = specIds[0];
669
+ if (specId) {
670
+ atom.specId = specId;
671
+ }
672
+ }
673
+ if (scIds.length === 1) {
674
+ const scId = scIds[0];
675
+ if (scId) {
676
+ atom.scId = scId;
677
+ }
678
+ }
679
+ return atom;
680
+ });
681
+ }
682
+ function collectScenarioNodes(feature, featureTags) {
683
+ const scenarios = [];
684
+ for (const child of feature.children) {
685
+ if (child.scenario) {
686
+ scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
687
+ }
688
+ if (child.rule) {
689
+ const ruleTags = collectTagNames(child.rule.tags);
690
+ for (const ruleChild of child.rule.children) {
691
+ if (ruleChild.scenario) {
692
+ scenarios.push(
693
+ buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
694
+ );
695
+ }
696
+ }
697
+ }
698
+ }
699
+ return scenarios;
700
+ }
701
+ function buildScenarioNode(scenario, featureTags, ruleTags) {
702
+ const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
703
+ const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
704
+ return {
705
+ name: scenario.name,
706
+ kind,
707
+ line: scenario.location?.line,
708
+ tags,
709
+ steps: scenario.steps
710
+ };
711
+ }
712
+ function collectTagNames(tags) {
713
+ return tags.map((tag) => tag.name.replace(/^@/, ""));
714
+ }
715
+ function collectStepTexts(step) {
716
+ const texts = [];
717
+ if (step.text) {
718
+ texts.push(step.text);
719
+ }
720
+ if (step.docString?.content) {
721
+ texts.push(step.docString.content);
722
+ }
723
+ if (step.dataTable?.rows) {
724
+ for (const row of step.dataTable.rows) {
725
+ for (const cell of row.cells) {
726
+ texts.push(cell.value);
727
+ }
728
+ }
729
+ }
730
+ return texts;
731
+ }
732
+ function unique2(values) {
733
+ return Array.from(new Set(values));
734
+ }
735
+
736
+ // src/core/traceability.ts
737
+ var SC_TAG_RE2 = /^SC-\d{4}$/;
738
+ var SC_TEST_ANNOTATION_RE = /\bQFAI:SC-(\d{4})\b/g;
739
+ var DEFAULT_TEST_FILE_EXCLUDE_GLOBS = [
740
+ "**/node_modules/**",
741
+ "**/.git/**",
742
+ "**/.qfai/**",
743
+ "**/dist/**",
744
+ "**/build/**",
745
+ "**/coverage/**",
746
+ "**/.next/**",
747
+ "**/out/**"
748
+ ];
749
+ function extractAnnotatedScIds(text) {
750
+ const ids = /* @__PURE__ */ new Set();
751
+ for (const match of text.matchAll(SC_TEST_ANNOTATION_RE)) {
752
+ const suffix = match[1];
753
+ if (suffix) {
754
+ ids.add(`SC-${suffix}`);
755
+ }
756
+ }
757
+ return Array.from(ids);
758
+ }
759
+ async function collectScIdsFromScenarioFiles(scenarioFiles) {
760
+ const scIds = /* @__PURE__ */ new Set();
761
+ for (const file of scenarioFiles) {
762
+ const text = await readFile2(file, "utf-8");
763
+ const { document, errors } = parseScenarioDocument(text, file);
764
+ if (!document || errors.length > 0) {
765
+ continue;
766
+ }
767
+ for (const scenario of document.scenarios) {
768
+ for (const tag of scenario.tags) {
769
+ if (SC_TAG_RE2.test(tag)) {
770
+ scIds.add(tag);
771
+ }
772
+ }
773
+ }
774
+ }
775
+ return scIds;
776
+ }
777
+ async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
778
+ const sources = /* @__PURE__ */ new Map();
779
+ for (const file of scenarioFiles) {
780
+ const text = await readFile2(file, "utf-8");
781
+ const { document, errors } = parseScenarioDocument(text, file);
782
+ if (!document || errors.length > 0) {
783
+ continue;
784
+ }
785
+ for (const scenario of document.scenarios) {
786
+ for (const tag of scenario.tags) {
787
+ if (!SC_TAG_RE2.test(tag)) {
788
+ continue;
789
+ }
790
+ const current = sources.get(tag) ?? /* @__PURE__ */ new Set();
791
+ current.add(file);
792
+ sources.set(tag, current);
793
+ }
794
+ }
795
+ }
796
+ return sources;
797
+ }
798
+ async function collectScTestReferences(root, globs, excludeGlobs) {
799
+ const refs = /* @__PURE__ */ new Map();
800
+ const normalizedGlobs = normalizeGlobs(globs);
801
+ const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
802
+ const mergedExcludeGlobs = Array.from(
803
+ /* @__PURE__ */ new Set([...DEFAULT_TEST_FILE_EXCLUDE_GLOBS, ...normalizedExcludeGlobs])
804
+ );
805
+ if (normalizedGlobs.length === 0) {
806
+ return {
807
+ refs,
808
+ scan: {
809
+ globs: normalizedGlobs,
810
+ excludeGlobs: mergedExcludeGlobs,
811
+ matchedFileCount: 0
812
+ }
813
+ };
814
+ }
815
+ let files = [];
816
+ try {
817
+ files = await collectFilesByGlobs(root, {
818
+ globs: normalizedGlobs,
819
+ ignore: mergedExcludeGlobs
820
+ });
821
+ } catch (error) {
822
+ return {
823
+ refs,
824
+ scan: {
825
+ globs: normalizedGlobs,
826
+ excludeGlobs: mergedExcludeGlobs,
827
+ matchedFileCount: 0
828
+ },
829
+ error: formatError3(error)
830
+ };
831
+ }
832
+ const normalizedFiles = Array.from(
833
+ new Set(files.map((file) => path4.normalize(file)))
834
+ );
835
+ for (const file of normalizedFiles) {
836
+ const text = await readFile2(file, "utf-8");
837
+ const scIds = extractAnnotatedScIds(text);
838
+ if (scIds.length === 0) {
839
+ continue;
840
+ }
841
+ for (const scId of scIds) {
842
+ const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
843
+ current.add(file);
844
+ refs.set(scId, current);
845
+ }
846
+ }
847
+ return {
848
+ refs,
849
+ scan: {
850
+ globs: normalizedGlobs,
851
+ excludeGlobs: mergedExcludeGlobs,
852
+ matchedFileCount: normalizedFiles.length
853
+ }
854
+ };
855
+ }
856
+ function buildScCoverage(scIds, refs) {
857
+ const sortedScIds = toSortedArray(scIds);
858
+ const refsRecord = {};
859
+ const missingIds = [];
860
+ let covered = 0;
861
+ for (const scId of sortedScIds) {
862
+ const files = refs.get(scId);
863
+ const sortedFiles = files ? toSortedArray(files) : [];
864
+ refsRecord[scId] = sortedFiles;
865
+ if (sortedFiles.length === 0) {
866
+ missingIds.push(scId);
867
+ } else {
868
+ covered += 1;
869
+ }
870
+ }
871
+ return {
872
+ total: sortedScIds.length,
873
+ covered,
874
+ missing: missingIds.length,
875
+ missingIds,
876
+ refs: refsRecord
877
+ };
878
+ }
879
+ function toSortedArray(values) {
880
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
881
+ }
882
+ function normalizeGlobs(globs) {
883
+ return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
884
+ }
885
+ function formatError3(error) {
886
+ if (error instanceof Error) {
887
+ return error.message;
888
+ }
889
+ return String(error);
890
+ }
891
+
892
+ // src/core/version.ts
893
+ import { readFile as readFile3 } from "fs/promises";
894
+ import path5 from "path";
535
895
  import { fileURLToPath } from "url";
536
896
  async function resolveToolVersion() {
537
- if ("0.3.5".length > 0) {
538
- return "0.3.5";
897
+ if ("0.4.2".length > 0) {
898
+ return "0.4.2";
539
899
  }
540
900
  try {
541
901
  const packagePath = resolvePackageJsonPath();
542
- const raw = await readFile2(packagePath, "utf-8");
902
+ const raw = await readFile3(packagePath, "utf-8");
543
903
  const parsed = JSON.parse(raw);
544
904
  const version = typeof parsed.version === "string" ? parsed.version : "";
545
905
  return version.length > 0 ? version : "unknown";
@@ -550,18 +910,18 @@ async function resolveToolVersion() {
550
910
  function resolvePackageJsonPath() {
551
911
  const base = import.meta.url;
552
912
  const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
553
- return path4.resolve(path4.dirname(basePath), "../../package.json");
913
+ return path5.resolve(path5.dirname(basePath), "../../package.json");
554
914
  }
555
915
 
556
916
  // src/core/validators/contracts.ts
557
- import { readFile as readFile3 } from "fs/promises";
558
- import path6 from "path";
917
+ import { readFile as readFile4 } from "fs/promises";
918
+ import path7 from "path";
559
919
 
560
920
  // src/core/contracts.ts
561
- import path5 from "path";
921
+ import path6 from "path";
562
922
  import { parse as parseYaml2 } from "yaml";
563
923
  function parseStructuredContract(file, text) {
564
- const ext = path5.extname(file).toLowerCase();
924
+ const ext = path6.extname(file).toLowerCase();
565
925
  if (ext === ".json") {
566
926
  return JSON.parse(text);
567
927
  }
@@ -612,9 +972,9 @@ var SQL_DANGEROUS_PATTERNS = [
612
972
  async function validateContracts(root, config) {
613
973
  const issues = [];
614
974
  const contractsRoot = resolvePath(root, config, "contractsDir");
615
- issues.push(...await validateUiContracts(path6.join(contractsRoot, "ui")));
616
- issues.push(...await validateApiContracts(path6.join(contractsRoot, "api")));
617
- issues.push(...await validateDataContracts(path6.join(contractsRoot, "db")));
975
+ issues.push(...await validateUiContracts(path7.join(contractsRoot, "ui")));
976
+ issues.push(...await validateApiContracts(path7.join(contractsRoot, "api")));
977
+ issues.push(...await validateDataContracts(path7.join(contractsRoot, "db")));
618
978
  return issues;
619
979
  }
620
980
  async function validateUiContracts(uiRoot) {
@@ -632,7 +992,7 @@ async function validateUiContracts(uiRoot) {
632
992
  }
633
993
  const issues = [];
634
994
  for (const file of files) {
635
- const text = await readFile3(file, "utf-8");
995
+ const text = await readFile4(file, "utf-8");
636
996
  const invalidIds = extractInvalidIds(text, [
637
997
  "SPEC",
638
998
  "BR",
@@ -661,7 +1021,7 @@ async function validateUiContracts(uiRoot) {
661
1021
  issues.push(
662
1022
  issue(
663
1023
  "QFAI-CONTRACT-001",
664
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error)})`,
1024
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
665
1025
  "error",
666
1026
  file,
667
1027
  "contracts.ui.parse"
@@ -699,7 +1059,7 @@ async function validateApiContracts(apiRoot) {
699
1059
  }
700
1060
  const issues = [];
701
1061
  for (const file of files) {
702
- const text = await readFile3(file, "utf-8");
1062
+ const text = await readFile4(file, "utf-8");
703
1063
  const invalidIds = extractInvalidIds(text, [
704
1064
  "SPEC",
705
1065
  "BR",
@@ -728,7 +1088,7 @@ async function validateApiContracts(apiRoot) {
728
1088
  issues.push(
729
1089
  issue(
730
1090
  "QFAI-CONTRACT-001",
731
- `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error)})`,
1091
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
732
1092
  "error",
733
1093
  file,
734
1094
  "contracts.api.parse"
@@ -777,7 +1137,7 @@ async function validateDataContracts(dataRoot) {
777
1137
  }
778
1138
  const issues = [];
779
1139
  for (const file of files) {
780
- const text = await readFile3(file, "utf-8");
1140
+ const text = await readFile4(file, "utf-8");
781
1141
  const invalidIds = extractInvalidIds(text, [
782
1142
  "SPEC",
783
1143
  "BR",
@@ -823,7 +1183,7 @@ function lintSql(text, file) {
823
1183
  function hasOpenApi(doc) {
824
1184
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
825
1185
  }
826
- function formatError2(error) {
1186
+ function formatError4(error) {
827
1187
  if (error instanceof Error) {
828
1188
  return error.message;
829
1189
  }
@@ -848,8 +1208,8 @@ function issue(code, message, severity, file, rule, refs) {
848
1208
  }
849
1209
 
850
1210
  // src/core/validators/delta.ts
851
- import { readFile as readFile4 } from "fs/promises";
852
- import path7 from "path";
1211
+ import { readFile as readFile5 } from "fs/promises";
1212
+ import path8 from "path";
853
1213
  var SECTION_RE = /^##\s+変更区分/m;
854
1214
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
855
1215
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -863,10 +1223,10 @@ async function validateDeltas(root, config) {
863
1223
  }
864
1224
  const issues = [];
865
1225
  for (const pack of packs) {
866
- const deltaPath = path7.join(pack, "delta.md");
1226
+ const deltaPath = path8.join(pack, "delta.md");
867
1227
  let text;
868
1228
  try {
869
- text = await readFile4(deltaPath, "utf-8");
1229
+ text = await readFile5(deltaPath, "utf-8");
870
1230
  } catch (error) {
871
1231
  if (isMissingFileError2(error)) {
872
1232
  issues.push(
@@ -938,17 +1298,17 @@ function issue2(code, message, severity, file, rule, refs) {
938
1298
  }
939
1299
 
940
1300
  // src/core/validators/ids.ts
941
- import { readFile as readFile6 } from "fs/promises";
942
- import path9 from "path";
1301
+ import { readFile as readFile7 } from "fs/promises";
1302
+ import path10 from "path";
943
1303
 
944
1304
  // src/core/contractIndex.ts
945
- import { readFile as readFile5 } from "fs/promises";
946
- import path8 from "path";
1305
+ import { readFile as readFile6 } from "fs/promises";
1306
+ import path9 from "path";
947
1307
  async function buildContractIndex(root, config) {
948
1308
  const contractsRoot = resolvePath(root, config, "contractsDir");
949
- const uiRoot = path8.join(contractsRoot, "ui");
950
- const apiRoot = path8.join(contractsRoot, "api");
951
- const dataRoot = path8.join(contractsRoot, "db");
1309
+ const uiRoot = path9.join(contractsRoot, "ui");
1310
+ const apiRoot = path9.join(contractsRoot, "api");
1311
+ const dataRoot = path9.join(contractsRoot, "db");
952
1312
  const [uiFiles, apiFiles, dataFiles] = await Promise.all([
953
1313
  collectUiContractFiles(uiRoot),
954
1314
  collectApiContractFiles(apiRoot),
@@ -967,7 +1327,7 @@ async function buildContractIndex(root, config) {
967
1327
  }
968
1328
  async function indexUiContracts(files, index) {
969
1329
  for (const file of files) {
970
- const text = await readFile5(file, "utf-8");
1330
+ const text = await readFile6(file, "utf-8");
971
1331
  try {
972
1332
  const doc = parseStructuredContract(file, text);
973
1333
  extractUiContractIds(doc).forEach((id) => record(index, id, file));
@@ -979,7 +1339,7 @@ async function indexUiContracts(files, index) {
979
1339
  }
980
1340
  async function indexApiContracts(files, index) {
981
1341
  for (const file of files) {
982
- const text = await readFile5(file, "utf-8");
1342
+ const text = await readFile6(file, "utf-8");
983
1343
  try {
984
1344
  const doc = parseStructuredContract(file, text);
985
1345
  extractApiContractIds(doc).forEach((id) => record(index, id, file));
@@ -991,7 +1351,7 @@ async function indexApiContracts(files, index) {
991
1351
  }
992
1352
  async function indexDataContracts(files, index) {
993
1353
  for (const file of files) {
994
- const text = await readFile5(file, "utf-8");
1354
+ const text = await readFile6(file, "utf-8");
995
1355
  extractIds(text, "DATA").forEach((id) => record(index, id, file));
996
1356
  }
997
1357
  }
@@ -1031,255 +1391,97 @@ function extractH2Sections(md) {
1031
1391
  if (!current) continue;
1032
1392
  const next = headings[i + 1];
1033
1393
  const startLine = current.line + 1;
1034
- const endLine = (next?.line ?? lines.length + 1) - 1;
1035
- const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1036
- sections.set(current.title.trim(), {
1037
- title: current.title.trim(),
1038
- startLine,
1039
- endLine,
1040
- body
1041
- });
1042
- }
1043
- return sections;
1044
- }
1045
-
1046
- // src/core/parse/spec.ts
1047
- var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1048
- var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1049
- var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1050
- var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1051
- var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1052
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1053
- function parseSpec(md, file) {
1054
- const headings = parseHeadings(md);
1055
- const h1 = headings.find((heading) => heading.level === 1);
1056
- const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1057
- const sections = extractH2Sections(md);
1058
- const sectionNames = new Set(Array.from(sections.keys()));
1059
- const brSection = sections.get(BR_SECTION_TITLE);
1060
- const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1061
- const startLine = brSection?.startLine ?? 1;
1062
- const brs = [];
1063
- const brsWithoutPriority = [];
1064
- const brsWithInvalidPriority = [];
1065
- for (let i = 0; i < brLines.length; i++) {
1066
- const lineText = brLines[i] ?? "";
1067
- const lineNumber = startLine + i;
1068
- const validMatch = lineText.match(BR_LINE_RE);
1069
- if (validMatch) {
1070
- const id = validMatch[1];
1071
- const priority = validMatch[2];
1072
- const text = validMatch[3];
1073
- if (!id || !priority || !text) continue;
1074
- brs.push({
1075
- id,
1076
- priority,
1077
- text: text.trim(),
1078
- line: lineNumber
1079
- });
1080
- continue;
1081
- }
1082
- const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1083
- if (anyPriorityMatch) {
1084
- const id = anyPriorityMatch[1];
1085
- const priority = anyPriorityMatch[2];
1086
- const text = anyPriorityMatch[3];
1087
- if (!id || !priority || !text) continue;
1088
- if (!VALID_PRIORITIES.has(priority)) {
1089
- brsWithInvalidPriority.push({
1090
- id,
1091
- priority,
1092
- text: text.trim(),
1093
- line: lineNumber
1094
- });
1095
- }
1096
- continue;
1097
- }
1098
- const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1099
- if (noPriorityMatch) {
1100
- const id = noPriorityMatch[1];
1101
- const text = noPriorityMatch[2];
1102
- if (!id || !text) continue;
1103
- brsWithoutPriority.push({
1104
- id,
1105
- text: text.trim(),
1106
- line: lineNumber
1107
- });
1108
- }
1109
- }
1110
- const parsed = {
1111
- file,
1112
- sections: sectionNames,
1113
- brs,
1114
- brsWithoutPriority,
1115
- brsWithInvalidPriority
1116
- };
1117
- if (specId) {
1118
- parsed.specId = specId;
1119
- }
1120
- return parsed;
1121
- }
1122
-
1123
- // src/core/gherkin/parse.ts
1124
- import {
1125
- AstBuilder,
1126
- GherkinClassicTokenMatcher,
1127
- Parser
1128
- } from "@cucumber/gherkin";
1129
- import { randomUUID } from "crypto";
1130
- function parseGherkin(source, uri) {
1131
- const errors = [];
1132
- const uuidFn = () => randomUUID();
1133
- const builder = new AstBuilder(uuidFn);
1134
- const matcher = new GherkinClassicTokenMatcher();
1135
- const parser = new Parser(builder, matcher);
1136
- try {
1137
- const gherkinDocument = parser.parse(source);
1138
- gherkinDocument.uri = uri;
1139
- return { gherkinDocument, errors };
1140
- } catch (error) {
1141
- errors.push(formatError3(error));
1142
- return { gherkinDocument: null, errors };
1143
- }
1144
- }
1145
- function formatError3(error) {
1146
- if (error instanceof Error) {
1147
- return error.message;
1148
- }
1149
- return String(error);
1150
- }
1151
-
1152
- // src/core/scenarioModel.ts
1153
- var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1154
- var SC_TAG_RE = /^SC-\d{4}$/;
1155
- var BR_TAG_RE = /^BR-\d{4}$/;
1156
- var UI_TAG_RE = /^UI-\d{4}$/;
1157
- var API_TAG_RE = /^API-\d{4}$/;
1158
- var DATA_TAG_RE = /^DATA-\d{4}$/;
1159
- function parseScenarioDocument(text, uri) {
1160
- const { gherkinDocument, errors } = parseGherkin(text, uri);
1161
- if (!gherkinDocument) {
1162
- return { document: null, errors };
1163
- }
1164
- const feature = gherkinDocument.feature;
1165
- if (!feature) {
1166
- return {
1167
- document: { uri, featureTags: [], scenarios: [] },
1168
- errors
1169
- };
1170
- }
1171
- const featureTags = collectTagNames(feature.tags);
1172
- const scenarios = collectScenarioNodes(feature, featureTags);
1173
- return {
1174
- document: {
1175
- uri,
1176
- featureName: feature.name,
1177
- featureTags,
1178
- scenarios
1179
- },
1180
- errors
1181
- };
1182
- }
1183
- function buildScenarioAtoms(document) {
1184
- return document.scenarios.map((scenario) => {
1185
- const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
1186
- const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
1187
- const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1188
- const contractIds = /* @__PURE__ */ new Set();
1189
- scenario.tags.forEach((tag) => {
1190
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1191
- contractIds.add(tag);
1192
- }
1193
- });
1194
- for (const step of scenario.steps) {
1195
- for (const text of collectStepTexts(step)) {
1196
- extractIds(text, "UI").forEach((id) => contractIds.add(id));
1197
- extractIds(text, "API").forEach((id) => contractIds.add(id));
1198
- extractIds(text, "DATA").forEach((id) => contractIds.add(id));
1199
- }
1200
- }
1201
- const atom = {
1202
- uri: document.uri,
1203
- featureName: document.featureName ?? "",
1204
- scenarioName: scenario.name,
1205
- kind: scenario.kind,
1206
- brIds,
1207
- contractIds: Array.from(contractIds).sort()
1208
- };
1209
- if (scenario.line !== void 0) {
1210
- atom.line = scenario.line;
1211
- }
1212
- if (specIds.length === 1) {
1213
- const specId = specIds[0];
1214
- if (specId) {
1215
- atom.specId = specId;
1216
- }
1217
- }
1218
- if (scIds.length === 1) {
1219
- const scId = scIds[0];
1220
- if (scId) {
1221
- atom.scId = scId;
1222
- }
1223
- }
1224
- return atom;
1225
- });
1394
+ const endLine = (next?.line ?? lines.length + 1) - 1;
1395
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1396
+ sections.set(current.title.trim(), {
1397
+ title: current.title.trim(),
1398
+ startLine,
1399
+ endLine,
1400
+ body
1401
+ });
1402
+ }
1403
+ return sections;
1226
1404
  }
1227
- function collectScenarioNodes(feature, featureTags) {
1228
- const scenarios = [];
1229
- for (const child of feature.children) {
1230
- if (child.scenario) {
1231
- scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
1405
+
1406
+ // src/core/parse/spec.ts
1407
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1408
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1409
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1410
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1411
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1412
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1413
+ function parseSpec(md, file) {
1414
+ const headings = parseHeadings(md);
1415
+ const h1 = headings.find((heading) => heading.level === 1);
1416
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1417
+ const sections = extractH2Sections(md);
1418
+ const sectionNames = new Set(Array.from(sections.keys()));
1419
+ const brSection = sections.get(BR_SECTION_TITLE);
1420
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1421
+ const startLine = brSection?.startLine ?? 1;
1422
+ const brs = [];
1423
+ const brsWithoutPriority = [];
1424
+ const brsWithInvalidPriority = [];
1425
+ for (let i = 0; i < brLines.length; i++) {
1426
+ const lineText = brLines[i] ?? "";
1427
+ const lineNumber = startLine + i;
1428
+ const validMatch = lineText.match(BR_LINE_RE);
1429
+ if (validMatch) {
1430
+ const id = validMatch[1];
1431
+ const priority = validMatch[2];
1432
+ const text = validMatch[3];
1433
+ if (!id || !priority || !text) continue;
1434
+ brs.push({
1435
+ id,
1436
+ priority,
1437
+ text: text.trim(),
1438
+ line: lineNumber
1439
+ });
1440
+ continue;
1232
1441
  }
1233
- if (child.rule) {
1234
- const ruleTags = collectTagNames(child.rule.tags);
1235
- for (const ruleChild of child.rule.children) {
1236
- if (ruleChild.scenario) {
1237
- scenarios.push(
1238
- buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
1239
- );
1240
- }
1442
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1443
+ if (anyPriorityMatch) {
1444
+ const id = anyPriorityMatch[1];
1445
+ const priority = anyPriorityMatch[2];
1446
+ const text = anyPriorityMatch[3];
1447
+ if (!id || !priority || !text) continue;
1448
+ if (!VALID_PRIORITIES.has(priority)) {
1449
+ brsWithInvalidPriority.push({
1450
+ id,
1451
+ priority,
1452
+ text: text.trim(),
1453
+ line: lineNumber
1454
+ });
1241
1455
  }
1456
+ continue;
1457
+ }
1458
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1459
+ if (noPriorityMatch) {
1460
+ const id = noPriorityMatch[1];
1461
+ const text = noPriorityMatch[2];
1462
+ if (!id || !text) continue;
1463
+ brsWithoutPriority.push({
1464
+ id,
1465
+ text: text.trim(),
1466
+ line: lineNumber
1467
+ });
1242
1468
  }
1243
1469
  }
1244
- return scenarios;
1245
- }
1246
- function buildScenarioNode(scenario, featureTags, ruleTags) {
1247
- const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
1248
- const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
1249
- return {
1250
- name: scenario.name,
1251
- kind,
1252
- line: scenario.location?.line,
1253
- tags,
1254
- steps: scenario.steps
1470
+ const parsed = {
1471
+ file,
1472
+ sections: sectionNames,
1473
+ brs,
1474
+ brsWithoutPriority,
1475
+ brsWithInvalidPriority
1255
1476
  };
1256
- }
1257
- function collectTagNames(tags) {
1258
- return tags.map((tag) => tag.name.replace(/^@/, ""));
1259
- }
1260
- function collectStepTexts(step) {
1261
- const texts = [];
1262
- if (step.text) {
1263
- texts.push(step.text);
1264
- }
1265
- if (step.docString?.content) {
1266
- texts.push(step.docString.content);
1267
- }
1268
- if (step.dataTable?.rows) {
1269
- for (const row of step.dataTable.rows) {
1270
- for (const cell of row.cells) {
1271
- texts.push(cell.value);
1272
- }
1273
- }
1477
+ if (specId) {
1478
+ parsed.specId = specId;
1274
1479
  }
1275
- return texts;
1276
- }
1277
- function unique2(values) {
1278
- return Array.from(new Set(values));
1480
+ return parsed;
1279
1481
  }
1280
1482
 
1281
1483
  // src/core/validators/ids.ts
1282
- var SC_TAG_RE2 = /^SC-\d{4}$/;
1484
+ var SC_TAG_RE3 = /^SC-\d{4}$/;
1283
1485
  async function validateDefinedIds(root, config) {
1284
1486
  const issues = [];
1285
1487
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1313,7 +1515,7 @@ async function validateDefinedIds(root, config) {
1313
1515
  }
1314
1516
  async function collectSpecDefinitionIds(files, out) {
1315
1517
  for (const file of files) {
1316
- const text = await readFile6(file, "utf-8");
1518
+ const text = await readFile7(file, "utf-8");
1317
1519
  const parsed = parseSpec(text, file);
1318
1520
  if (parsed.specId) {
1319
1521
  recordId(out, parsed.specId, file);
@@ -1323,14 +1525,14 @@ async function collectSpecDefinitionIds(files, out) {
1323
1525
  }
1324
1526
  async function collectScenarioDefinitionIds(files, out) {
1325
1527
  for (const file of files) {
1326
- const text = await readFile6(file, "utf-8");
1528
+ const text = await readFile7(file, "utf-8");
1327
1529
  const { document, errors } = parseScenarioDocument(text, file);
1328
1530
  if (!document || errors.length > 0) {
1329
1531
  continue;
1330
1532
  }
1331
1533
  for (const scenario of document.scenarios) {
1332
1534
  for (const tag of scenario.tags) {
1333
- if (SC_TAG_RE2.test(tag)) {
1535
+ if (SC_TAG_RE3.test(tag)) {
1334
1536
  recordId(out, tag, file);
1335
1537
  }
1336
1538
  }
@@ -1344,7 +1546,7 @@ function recordId(out, id, file) {
1344
1546
  }
1345
1547
  function formatFileList(files, root) {
1346
1548
  return files.map((file) => {
1347
- const relative = path9.relative(root, file);
1549
+ const relative = path10.relative(root, file);
1348
1550
  return relative.length > 0 ? relative : file;
1349
1551
  }).join(", ");
1350
1552
  }
@@ -1367,13 +1569,12 @@ function issue3(code, message, severity, file, rule, refs) {
1367
1569
  }
1368
1570
 
1369
1571
  // src/core/validators/scenario.ts
1370
- import { readFile as readFile7 } from "fs/promises";
1572
+ import { readFile as readFile8 } from "fs/promises";
1371
1573
  var GIVEN_PATTERN = /\bGiven\b/;
1372
1574
  var WHEN_PATTERN = /\bWhen\b/;
1373
1575
  var THEN_PATTERN = /\bThen\b/;
1374
- var SC_TAG_RE3 = /^SC-\d{4}$/;
1576
+ var SC_TAG_RE4 = /^SC-\d{4}$/;
1375
1577
  var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1376
- var BR_TAG_RE2 = /^BR-\d{4}$/;
1377
1578
  async function validateScenarios(root, config) {
1378
1579
  const specsRoot = resolvePath(root, config, "specsDir");
1379
1580
  const entries = await collectSpecEntries(specsRoot);
@@ -1394,7 +1595,7 @@ async function validateScenarios(root, config) {
1394
1595
  for (const entry of entries) {
1395
1596
  let text;
1396
1597
  try {
1397
- text = await readFile7(entry.scenarioPath, "utf-8");
1598
+ text = await readFile8(entry.scenarioPath, "utf-8");
1398
1599
  } catch (error) {
1399
1600
  if (isMissingFileError3(error)) {
1400
1601
  issues.push(
@@ -1453,17 +1654,7 @@ function validateScenarioContent(text, file) {
1453
1654
  const featureSpecTags = document.featureTags.filter(
1454
1655
  (tag) => SPEC_TAG_RE2.test(tag)
1455
1656
  );
1456
- if (featureSpecTags.length === 0) {
1457
- issues.push(
1458
- issue4(
1459
- "QFAI-SC-009",
1460
- "Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1461
- "error",
1462
- file,
1463
- "scenario.featureSpec"
1464
- )
1465
- );
1466
- } else if (featureSpecTags.length > 1) {
1657
+ if (featureSpecTags.length > 1) {
1467
1658
  issues.push(
1468
1659
  issue4(
1469
1660
  "QFAI-SC-009",
@@ -1505,18 +1696,12 @@ function validateScenarioContent(text, file) {
1505
1696
  continue;
1506
1697
  }
1507
1698
  const missingTags = [];
1508
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
1699
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
1509
1700
  if (scTags.length === 0) {
1510
1701
  missingTags.push("SC(0\u4EF6)");
1511
1702
  } else if (scTags.length > 1) {
1512
1703
  missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1513
1704
  }
1514
- if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
1515
- missingTags.push("SPEC");
1516
- }
1517
- if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
1518
- missingTags.push("BR");
1519
- }
1520
1705
  if (missingTags.length > 0) {
1521
1706
  issues.push(
1522
1707
  issue4(
@@ -1580,7 +1765,7 @@ function isMissingFileError3(error) {
1580
1765
  }
1581
1766
 
1582
1767
  // src/core/validators/spec.ts
1583
- import { readFile as readFile8 } from "fs/promises";
1768
+ import { readFile as readFile9 } from "fs/promises";
1584
1769
  async function validateSpecs(root, config) {
1585
1770
  const specsRoot = resolvePath(root, config, "specsDir");
1586
1771
  const entries = await collectSpecEntries(specsRoot);
@@ -1601,7 +1786,7 @@ async function validateSpecs(root, config) {
1601
1786
  for (const entry of entries) {
1602
1787
  let text;
1603
1788
  try {
1604
- text = await readFile8(entry.specPath, "utf-8");
1789
+ text = await readFile9(entry.specPath, "utf-8");
1605
1790
  } catch (error) {
1606
1791
  if (isMissingFileError4(error)) {
1607
1792
  issues.push(
@@ -1750,10 +1935,9 @@ function isMissingFileError4(error) {
1750
1935
  }
1751
1936
 
1752
1937
  // src/core/validators/traceability.ts
1753
- import { readFile as readFile9 } from "fs/promises";
1754
- var SC_TAG_RE4 = /^SC-\d{4}$/;
1938
+ import { readFile as readFile10 } from "fs/promises";
1755
1939
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
1756
- var BR_TAG_RE3 = /^BR-\d{4}$/;
1940
+ var BR_TAG_RE2 = /^BR-\d{4}$/;
1757
1941
  async function validateTraceability(root, config) {
1758
1942
  const issues = [];
1759
1943
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1772,7 +1956,7 @@ async function validateTraceability(root, config) {
1772
1956
  const contractIndex = await buildContractIndex(root, config);
1773
1957
  const contractIds = contractIndex.ids;
1774
1958
  for (const file of specFiles) {
1775
- const text = await readFile9(file, "utf-8");
1959
+ const text = await readFile10(file, "utf-8");
1776
1960
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1777
1961
  const parsed = parseSpec(text, file);
1778
1962
  if (parsed.specId) {
@@ -1780,28 +1964,6 @@ async function validateTraceability(root, config) {
1780
1964
  }
1781
1965
  const brIds = parsed.brs.map((br) => br.id);
1782
1966
  brIds.forEach((id) => brIdsInSpecs.add(id));
1783
- const referencedContractIds = /* @__PURE__ */ new Set([
1784
- ...extractIds(text, "UI"),
1785
- ...extractIds(text, "API"),
1786
- ...extractIds(text, "DATA")
1787
- ]);
1788
- const unknownContractIds = Array.from(referencedContractIds).filter(
1789
- (id) => !contractIds.has(id)
1790
- );
1791
- if (unknownContractIds.length > 0) {
1792
- issues.push(
1793
- issue6(
1794
- "QFAI-TRACE-009",
1795
- `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1796
- ", "
1797
- )}`,
1798
- "error",
1799
- file,
1800
- "traceability.specContractExists",
1801
- unknownContractIds
1802
- )
1803
- );
1804
- }
1805
1967
  if (parsed.specId) {
1806
1968
  const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
1807
1969
  brIds.forEach((id) => current.add(id));
@@ -1809,23 +1971,49 @@ async function validateTraceability(root, config) {
1809
1971
  }
1810
1972
  }
1811
1973
  for (const file of scenarioFiles) {
1812
- const text = await readFile9(file, "utf-8");
1974
+ const text = await readFile10(file, "utf-8");
1813
1975
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1814
1976
  const { document, errors } = parseScenarioDocument(text, file);
1815
1977
  if (!document || errors.length > 0) {
1816
1978
  continue;
1817
1979
  }
1818
1980
  const atoms = buildScenarioAtoms(document);
1981
+ const scIdsInFile = /* @__PURE__ */ new Set();
1819
1982
  for (const [index, scenario] of document.scenarios.entries()) {
1820
1983
  const atom = atoms[index];
1821
1984
  if (!atom) {
1822
1985
  continue;
1823
1986
  }
1824
1987
  const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
1825
- const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
1826
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
1988
+ const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
1989
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
1990
+ if (specTags.length === 0) {
1991
+ issues.push(
1992
+ issue6(
1993
+ "QFAI-TRACE-014",
1994
+ `Scenario \u304C SPEC \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
1995
+ "error",
1996
+ file,
1997
+ "traceability.scenarioSpecRequired"
1998
+ )
1999
+ );
2000
+ }
2001
+ if (brTags.length === 0) {
2002
+ issues.push(
2003
+ issue6(
2004
+ "QFAI-TRACE-015",
2005
+ `Scenario \u304C BR \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
2006
+ "error",
2007
+ file,
2008
+ "traceability.scenarioBrRequired"
2009
+ )
2010
+ );
2011
+ }
1827
2012
  brTags.forEach((id) => brIdsInScenarios.add(id));
1828
- scTags.forEach((id) => scIdsInScenarios.add(id));
2013
+ scTags.forEach((id) => {
2014
+ scIdsInScenarios.add(id);
2015
+ scIdsInFile.add(id);
2016
+ });
1829
2017
  atom.contractIds.forEach((id) => scenarioContractIds.add(id));
1830
2018
  if (atom.contractIds.length > 0) {
1831
2019
  scTags.forEach((id) => scWithContracts.add(id));
@@ -1903,6 +2091,22 @@ async function validateTraceability(root, config) {
1903
2091
  }
1904
2092
  }
1905
2093
  }
2094
+ if (scIdsInFile.size !== 1) {
2095
+ const invalidScIds = Array.from(scIdsInFile).sort(
2096
+ (a, b) => a.localeCompare(b)
2097
+ );
2098
+ 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(", ")}`;
2099
+ issues.push(
2100
+ issue6(
2101
+ "QFAI-TRACE-012",
2102
+ `Spec entry \u304C Spec:SC=1:1 \u3092\u6E80\u305F\u3057\u3066\u3044\u307E\u305B\u3093: ${detail}`,
2103
+ "error",
2104
+ file,
2105
+ "traceability.specScOneToOne",
2106
+ invalidScIds
2107
+ )
2108
+ );
2109
+ }
1906
2110
  }
1907
2111
  if (upstreamIds.size === 0) {
1908
2112
  return [
@@ -1951,6 +2155,66 @@ async function validateTraceability(root, config) {
1951
2155
  );
1952
2156
  }
1953
2157
  }
2158
+ const scRefsResult = await collectScTestReferences(
2159
+ root,
2160
+ config.validation.traceability.testFileGlobs,
2161
+ config.validation.traceability.testFileExcludeGlobs
2162
+ );
2163
+ const scTestRefs = scRefsResult.refs;
2164
+ const testFileScan = scRefsResult.scan;
2165
+ const hasScenarios = scIdsInScenarios.size > 0;
2166
+ const hasGlobConfig = testFileScan.globs.length > 0;
2167
+ const hasMatchedTests = testFileScan.matchedFileCount > 0;
2168
+ if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
2169
+ const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
2170
+ issues.push(
2171
+ issue6(
2172
+ "QFAI-TRACE-013",
2173
+ `\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}`,
2174
+ "error",
2175
+ testsRoot,
2176
+ "traceability.testFileGlobs"
2177
+ )
2178
+ );
2179
+ } else {
2180
+ if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2181
+ const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2182
+ const refs = scTestRefs.get(id);
2183
+ return !refs || refs.size === 0;
2184
+ });
2185
+ if (scWithoutTests.length > 0) {
2186
+ issues.push(
2187
+ issue6(
2188
+ "QFAI-TRACE-010",
2189
+ `SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(
2190
+ ", "
2191
+ )}\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`,
2192
+ config.validation.traceability.scNoTestSeverity,
2193
+ testsRoot,
2194
+ "traceability.scMustHaveTest",
2195
+ scWithoutTests
2196
+ )
2197
+ );
2198
+ }
2199
+ }
2200
+ const unknownScIds = Array.from(scTestRefs.keys()).filter(
2201
+ (id) => !scIdsInScenarios.has(id)
2202
+ );
2203
+ if (unknownScIds.length > 0) {
2204
+ issues.push(
2205
+ issue6(
2206
+ "QFAI-TRACE-011",
2207
+ `\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(
2208
+ ", "
2209
+ )}`,
2210
+ "error",
2211
+ testsRoot,
2212
+ "traceability.scUnknownInTests",
2213
+ unknownScIds
2214
+ )
2215
+ );
2216
+ }
2217
+ }
1954
2218
  if (!config.validation.traceability.allowOrphanContracts) {
1955
2219
  if (contractIds.size > 0) {
1956
2220
  const orphanContracts = Array.from(contractIds).filter(
@@ -1999,7 +2263,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1999
2263
  const pattern = buildIdPattern(Array.from(upstreamIds));
2000
2264
  let found = false;
2001
2265
  for (const file of targetFiles) {
2002
- const text = await readFile9(file, "utf-8");
2266
+ const text = await readFile10(file, "utf-8");
2003
2267
  if (pattern.test(text)) {
2004
2268
  found = true;
2005
2269
  break;
@@ -2009,8 +2273,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2009
2273
  issues.push(
2010
2274
  issue6(
2011
2275
  "QFAI-TRACE-002",
2012
- "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
2013
- "warning",
2276
+ "\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",
2277
+ "info",
2014
2278
  srcRoot,
2015
2279
  "traceability.codeReferences"
2016
2280
  )
@@ -2053,12 +2317,24 @@ async function validateProject(root, configResult) {
2053
2317
  ...await validateDefinedIds(root, config),
2054
2318
  ...await validateTraceability(root, config)
2055
2319
  ];
2320
+ const specsRoot = resolvePath(root, config, "specsDir");
2321
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
2322
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2323
+ const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
2324
+ root,
2325
+ config.validation.traceability.testFileGlobs,
2326
+ config.validation.traceability.testFileExcludeGlobs
2327
+ );
2328
+ const scCoverage = buildScCoverage(scIds, scTestRefs);
2056
2329
  const toolVersion = await resolveToolVersion();
2057
2330
  return {
2058
- schemaVersion: VALIDATION_SCHEMA_VERSION,
2059
2331
  toolVersion,
2060
2332
  issues,
2061
- counts: countIssues(issues)
2333
+ counts: countIssues(issues),
2334
+ traceability: {
2335
+ sc: scCoverage,
2336
+ testFiles
2337
+ }
2062
2338
  };
2063
2339
  }
2064
2340
  function countIssues(issues) {
@@ -2079,9 +2355,9 @@ async function createReportData(root, validation, configResult) {
2079
2355
  const configPath = resolved.configPath;
2080
2356
  const specsRoot = resolvePath(root, config, "specsDir");
2081
2357
  const contractsRoot = resolvePath(root, config, "contractsDir");
2082
- const apiRoot = path10.join(contractsRoot, "api");
2083
- const uiRoot = path10.join(contractsRoot, "ui");
2084
- const dbRoot = path10.join(contractsRoot, "db");
2358
+ const apiRoot = path11.join(contractsRoot, "api");
2359
+ const uiRoot = path11.join(contractsRoot, "ui");
2360
+ const dbRoot = path11.join(contractsRoot, "db");
2085
2361
  const srcRoot = resolvePath(root, config, "srcDir");
2086
2362
  const testsRoot = resolvePath(root, config, "testsDir");
2087
2363
  const specFiles = await collectSpecFiles(specsRoot);
@@ -2107,6 +2383,16 @@ async function createReportData(root, validation, configResult) {
2107
2383
  srcRoot,
2108
2384
  testsRoot
2109
2385
  );
2386
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2387
+ const scRefsResult = await collectScTestReferences(
2388
+ root,
2389
+ config.validation.traceability.testFileGlobs,
2390
+ config.validation.traceability.testFileExcludeGlobs
2391
+ );
2392
+ const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
2393
+ const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
2394
+ const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
2395
+ const scSourceRecord = mapToSortedRecord(scSources);
2110
2396
  const resolvedValidation = validation ?? await validateProject(root, resolved);
2111
2397
  const version = await resolveToolVersion();
2112
2398
  return {
@@ -2135,7 +2421,10 @@ async function createReportData(root, validation, configResult) {
2135
2421
  },
2136
2422
  traceability: {
2137
2423
  upstreamIdsFound: upstreamIds.size,
2138
- referencedInCodeOrTests: traceability
2424
+ referencedInCodeOrTests: traceability,
2425
+ sc: scCoverage,
2426
+ scSources: scSourceRecord,
2427
+ testFiles
2139
2428
  },
2140
2429
  issues: resolvedValidation.issues
2141
2430
  };
@@ -2172,6 +2461,65 @@ function formatReportMarkdown(data) {
2172
2461
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2173
2462
  );
2174
2463
  lines.push("");
2464
+ lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2465
+ lines.push(`- total: ${data.traceability.sc.total}`);
2466
+ lines.push(`- covered: ${data.traceability.sc.covered}`);
2467
+ lines.push(`- missing: ${data.traceability.sc.missing}`);
2468
+ lines.push(
2469
+ `- testFileGlobs: ${formatList(data.traceability.testFiles.globs)}`
2470
+ );
2471
+ lines.push(
2472
+ `- testFileExcludeGlobs: ${formatList(
2473
+ data.traceability.testFiles.excludeGlobs
2474
+ )}`
2475
+ );
2476
+ lines.push(
2477
+ `- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
2478
+ );
2479
+ if (data.traceability.sc.missingIds.length === 0) {
2480
+ lines.push("- missingIds: (none)");
2481
+ } else {
2482
+ const sources = data.traceability.scSources;
2483
+ const missingWithSources = data.traceability.sc.missingIds.map((id) => {
2484
+ const files = sources[id] ?? [];
2485
+ if (files.length === 0) {
2486
+ return id;
2487
+ }
2488
+ return `${id} (${files.join(", ")})`;
2489
+ });
2490
+ lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
2491
+ }
2492
+ lines.push("");
2493
+ lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
2494
+ const scRefs = data.traceability.sc.refs;
2495
+ const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
2496
+ if (scIds.length === 0) {
2497
+ lines.push("- (none)");
2498
+ } else {
2499
+ for (const scId of scIds) {
2500
+ const refs = scRefs[scId] ?? [];
2501
+ if (refs.length === 0) {
2502
+ lines.push(`- ${scId}: (none)`);
2503
+ } else {
2504
+ lines.push(`- ${scId}: ${refs.join(", ")}`);
2505
+ }
2506
+ }
2507
+ }
2508
+ lines.push("");
2509
+ lines.push("## Spec:SC=1:1 \u9055\u53CD");
2510
+ const specScIssues = data.issues.filter(
2511
+ (item) => item.code === "QFAI-TRACE-012"
2512
+ );
2513
+ if (specScIssues.length === 0) {
2514
+ lines.push("- (none)");
2515
+ } else {
2516
+ for (const item of specScIssues) {
2517
+ const location = item.file ?? "(unknown)";
2518
+ const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
2519
+ lines.push(`- ${location}: ${refs}`);
2520
+ }
2521
+ }
2522
+ lines.push("");
2175
2523
  lines.push("## Hotspots");
2176
2524
  const hotspots = buildHotspots(data.issues);
2177
2525
  if (hotspots.length === 0) {
@@ -2226,25 +2574,25 @@ async function collectIds(files) {
2226
2574
  DATA: /* @__PURE__ */ new Set()
2227
2575
  };
2228
2576
  for (const file of files) {
2229
- const text = await readFile10(file, "utf-8");
2577
+ const text = await readFile11(file, "utf-8");
2230
2578
  for (const prefix of ID_PREFIXES2) {
2231
2579
  const ids = extractIds(text, prefix);
2232
2580
  ids.forEach((id) => result[prefix].add(id));
2233
2581
  }
2234
2582
  }
2235
2583
  return {
2236
- SPEC: toSortedArray(result.SPEC),
2237
- BR: toSortedArray(result.BR),
2238
- SC: toSortedArray(result.SC),
2239
- UI: toSortedArray(result.UI),
2240
- API: toSortedArray(result.API),
2241
- DATA: toSortedArray(result.DATA)
2584
+ SPEC: toSortedArray2(result.SPEC),
2585
+ BR: toSortedArray2(result.BR),
2586
+ SC: toSortedArray2(result.SC),
2587
+ UI: toSortedArray2(result.UI),
2588
+ API: toSortedArray2(result.API),
2589
+ DATA: toSortedArray2(result.DATA)
2242
2590
  };
2243
2591
  }
2244
2592
  async function collectUpstreamIds(files) {
2245
2593
  const ids = /* @__PURE__ */ new Set();
2246
2594
  for (const file of files) {
2247
- const text = await readFile10(file, "utf-8");
2595
+ const text = await readFile11(file, "utf-8");
2248
2596
  extractAllIds(text).forEach((id) => ids.add(id));
2249
2597
  }
2250
2598
  return ids;
@@ -2265,7 +2613,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
2265
2613
  }
2266
2614
  const pattern = buildIdPattern2(Array.from(upstreamIds));
2267
2615
  for (const file of targetFiles) {
2268
- const text = await readFile10(file, "utf-8");
2616
+ const text = await readFile11(file, "utf-8");
2269
2617
  if (pattern.test(text)) {
2270
2618
  return true;
2271
2619
  }
@@ -2282,9 +2630,22 @@ function formatIdLine(label, values) {
2282
2630
  }
2283
2631
  return `- ${label}: ${values.join(", ")}`;
2284
2632
  }
2285
- function toSortedArray(values) {
2633
+ function formatList(values) {
2634
+ if (values.length === 0) {
2635
+ return "(none)";
2636
+ }
2637
+ return values.join(", ");
2638
+ }
2639
+ function toSortedArray2(values) {
2286
2640
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2287
2641
  }
2642
+ function mapToSortedRecord(values) {
2643
+ const record2 = {};
2644
+ for (const [key, files] of values.entries()) {
2645
+ record2[key] = Array.from(files).sort((a, b) => a.localeCompare(b));
2646
+ }
2647
+ return record2;
2648
+ }
2288
2649
  function buildHotspots(issues) {
2289
2650
  const map = /* @__PURE__ */ new Map();
2290
2651
  for (const issue7 of issues) {
@@ -2307,7 +2668,6 @@ function buildHotspots(issues) {
2307
2668
  );
2308
2669
  }
2309
2670
  export {
2310
- VALIDATION_SCHEMA_VERSION,
2311
2671
  createReportData,
2312
2672
  defaultConfig,
2313
2673
  extractAllIds,