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.cjs CHANGED
@@ -30,7 +30,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
- VALIDATION_SCHEMA_VERSION: () => VALIDATION_SCHEMA_VERSION,
34
33
  createReportData: () => createReportData,
35
34
  defaultConfig: () => defaultConfig,
36
35
  extractAllIds: () => extractAllIds,
@@ -85,6 +84,10 @@ var defaultConfig = {
85
84
  traceability: {
86
85
  brMustHaveSc: true,
87
86
  scMustTouchContracts: true,
87
+ scMustHaveTest: true,
88
+ testFileGlobs: [],
89
+ testFileExcludeGlobs: [],
90
+ scNoTestSeverity: "error",
88
91
  allowOrphanContracts: false,
89
92
  unknownContractIdSeverity: "error"
90
93
  }
@@ -264,6 +267,34 @@ function normalizeValidation(raw, configPath, issues) {
264
267
  configPath,
265
268
  issues
266
269
  ),
270
+ scMustHaveTest: readBoolean(
271
+ traceabilityRaw?.scMustHaveTest,
272
+ base.traceability.scMustHaveTest,
273
+ "validation.traceability.scMustHaveTest",
274
+ configPath,
275
+ issues
276
+ ),
277
+ testFileGlobs: readStringArray(
278
+ traceabilityRaw?.testFileGlobs,
279
+ base.traceability.testFileGlobs,
280
+ "validation.traceability.testFileGlobs",
281
+ configPath,
282
+ issues
283
+ ),
284
+ testFileExcludeGlobs: readStringArray(
285
+ traceabilityRaw?.testFileExcludeGlobs,
286
+ base.traceability.testFileExcludeGlobs,
287
+ "validation.traceability.testFileExcludeGlobs",
288
+ configPath,
289
+ issues
290
+ ),
291
+ scNoTestSeverity: readTraceabilitySeverity(
292
+ traceabilityRaw?.scNoTestSeverity,
293
+ base.traceability.scNoTestSeverity,
294
+ "validation.traceability.scNoTestSeverity",
295
+ configPath,
296
+ issues
297
+ ),
267
298
  allowOrphanContracts: readBoolean(
268
299
  traceabilityRaw?.allowOrphanContracts,
269
300
  base.traceability.allowOrphanContracts,
@@ -442,8 +473,8 @@ function isValidId(value, prefix) {
442
473
  }
443
474
 
444
475
  // src/core/report.ts
445
- var import_promises13 = require("fs/promises");
446
- var import_node_path10 = __toESM(require("path"), 1);
476
+ var import_promises14 = require("fs/promises");
477
+ var import_node_path11 = __toESM(require("path"), 1);
447
478
 
448
479
  // src/core/discovery.ts
449
480
  var import_promises4 = require("fs/promises");
@@ -451,6 +482,7 @@ var import_promises4 = require("fs/promises");
451
482
  // src/core/fs.ts
452
483
  var import_promises2 = require("fs/promises");
453
484
  var import_node_path2 = __toESM(require("path"), 1);
485
+ var import_fast_glob = __toESM(require("fast-glob"), 1);
454
486
  var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
455
487
  "node_modules",
456
488
  ".git",
@@ -472,6 +504,18 @@ async function collectFiles(root, options = {}) {
472
504
  await walk(root, root, ignoreDirs, extensions, entries);
473
505
  return entries;
474
506
  }
507
+ async function collectFilesByGlobs(root, options) {
508
+ if (options.globs.length === 0) {
509
+ return [];
510
+ }
511
+ return (0, import_fast_glob.default)(options.globs, {
512
+ cwd: root,
513
+ ignore: options.ignore ?? [],
514
+ onlyFiles: true,
515
+ absolute: true,
516
+ unique: true
517
+ });
518
+ }
475
519
  async function walk(base, current, ignoreDirs, extensions, out) {
476
520
  const items = await (0, import_promises2.readdir)(current, { withFileTypes: true });
477
521
  for (const item of items) {
@@ -583,20 +627,331 @@ async function exists2(target) {
583
627
  }
584
628
  }
585
629
 
586
- // src/core/types.ts
587
- var VALIDATION_SCHEMA_VERSION = "0.2";
588
-
589
- // src/core/version.ts
630
+ // src/core/traceability.ts
590
631
  var import_promises5 = require("fs/promises");
591
632
  var import_node_path4 = __toESM(require("path"), 1);
633
+
634
+ // src/core/gherkin/parse.ts
635
+ var import_gherkin = require("@cucumber/gherkin");
636
+ var import_node_crypto = require("crypto");
637
+ function parseGherkin(source, uri) {
638
+ const errors = [];
639
+ const uuidFn = () => (0, import_node_crypto.randomUUID)();
640
+ const builder = new import_gherkin.AstBuilder(uuidFn);
641
+ const matcher = new import_gherkin.GherkinClassicTokenMatcher();
642
+ const parser = new import_gherkin.Parser(builder, matcher);
643
+ try {
644
+ const gherkinDocument = parser.parse(source);
645
+ gherkinDocument.uri = uri;
646
+ return { gherkinDocument, errors };
647
+ } catch (error) {
648
+ errors.push(formatError2(error));
649
+ return { gherkinDocument: null, errors };
650
+ }
651
+ }
652
+ function formatError2(error) {
653
+ if (error instanceof Error) {
654
+ return error.message;
655
+ }
656
+ return String(error);
657
+ }
658
+
659
+ // src/core/scenarioModel.ts
660
+ var SPEC_TAG_RE = /^SPEC-\d{4}$/;
661
+ var SC_TAG_RE = /^SC-\d{4}$/;
662
+ var BR_TAG_RE = /^BR-\d{4}$/;
663
+ var UI_TAG_RE = /^UI-\d{4}$/;
664
+ var API_TAG_RE = /^API-\d{4}$/;
665
+ var DATA_TAG_RE = /^DATA-\d{4}$/;
666
+ function parseScenarioDocument(text, uri) {
667
+ const { gherkinDocument, errors } = parseGherkin(text, uri);
668
+ if (!gherkinDocument) {
669
+ return { document: null, errors };
670
+ }
671
+ const feature = gherkinDocument.feature;
672
+ if (!feature) {
673
+ return {
674
+ document: { uri, featureTags: [], scenarios: [] },
675
+ errors
676
+ };
677
+ }
678
+ const featureTags = collectTagNames(feature.tags);
679
+ const scenarios = collectScenarioNodes(feature, featureTags);
680
+ return {
681
+ document: {
682
+ uri,
683
+ featureName: feature.name,
684
+ featureTags,
685
+ scenarios
686
+ },
687
+ errors
688
+ };
689
+ }
690
+ function buildScenarioAtoms(document) {
691
+ return document.scenarios.map((scenario) => {
692
+ const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
693
+ const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
694
+ const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
695
+ const contractIds = /* @__PURE__ */ new Set();
696
+ scenario.tags.forEach((tag) => {
697
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
698
+ contractIds.add(tag);
699
+ }
700
+ });
701
+ for (const step of scenario.steps) {
702
+ for (const text of collectStepTexts(step)) {
703
+ extractIds(text, "UI").forEach((id) => contractIds.add(id));
704
+ extractIds(text, "API").forEach((id) => contractIds.add(id));
705
+ extractIds(text, "DATA").forEach((id) => contractIds.add(id));
706
+ }
707
+ }
708
+ const atom = {
709
+ uri: document.uri,
710
+ featureName: document.featureName ?? "",
711
+ scenarioName: scenario.name,
712
+ kind: scenario.kind,
713
+ brIds,
714
+ contractIds: Array.from(contractIds).sort()
715
+ };
716
+ if (scenario.line !== void 0) {
717
+ atom.line = scenario.line;
718
+ }
719
+ if (specIds.length === 1) {
720
+ const specId = specIds[0];
721
+ if (specId) {
722
+ atom.specId = specId;
723
+ }
724
+ }
725
+ if (scIds.length === 1) {
726
+ const scId = scIds[0];
727
+ if (scId) {
728
+ atom.scId = scId;
729
+ }
730
+ }
731
+ return atom;
732
+ });
733
+ }
734
+ function collectScenarioNodes(feature, featureTags) {
735
+ const scenarios = [];
736
+ for (const child of feature.children) {
737
+ if (child.scenario) {
738
+ scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
739
+ }
740
+ if (child.rule) {
741
+ const ruleTags = collectTagNames(child.rule.tags);
742
+ for (const ruleChild of child.rule.children) {
743
+ if (ruleChild.scenario) {
744
+ scenarios.push(
745
+ buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
746
+ );
747
+ }
748
+ }
749
+ }
750
+ }
751
+ return scenarios;
752
+ }
753
+ function buildScenarioNode(scenario, featureTags, ruleTags) {
754
+ const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
755
+ const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
756
+ return {
757
+ name: scenario.name,
758
+ kind,
759
+ line: scenario.location?.line,
760
+ tags,
761
+ steps: scenario.steps
762
+ };
763
+ }
764
+ function collectTagNames(tags) {
765
+ return tags.map((tag) => tag.name.replace(/^@/, ""));
766
+ }
767
+ function collectStepTexts(step) {
768
+ const texts = [];
769
+ if (step.text) {
770
+ texts.push(step.text);
771
+ }
772
+ if (step.docString?.content) {
773
+ texts.push(step.docString.content);
774
+ }
775
+ if (step.dataTable?.rows) {
776
+ for (const row of step.dataTable.rows) {
777
+ for (const cell of row.cells) {
778
+ texts.push(cell.value);
779
+ }
780
+ }
781
+ }
782
+ return texts;
783
+ }
784
+ function unique2(values) {
785
+ return Array.from(new Set(values));
786
+ }
787
+
788
+ // src/core/traceability.ts
789
+ var SC_TAG_RE2 = /^SC-\d{4}$/;
790
+ var SC_TEST_ANNOTATION_RE = /\bQFAI:SC-(\d{4})\b/g;
791
+ var DEFAULT_TEST_FILE_EXCLUDE_GLOBS = [
792
+ "**/node_modules/**",
793
+ "**/.git/**",
794
+ "**/.qfai/**",
795
+ "**/dist/**",
796
+ "**/build/**",
797
+ "**/coverage/**",
798
+ "**/.next/**",
799
+ "**/out/**"
800
+ ];
801
+ function extractAnnotatedScIds(text) {
802
+ const ids = /* @__PURE__ */ new Set();
803
+ for (const match of text.matchAll(SC_TEST_ANNOTATION_RE)) {
804
+ const suffix = match[1];
805
+ if (suffix) {
806
+ ids.add(`SC-${suffix}`);
807
+ }
808
+ }
809
+ return Array.from(ids);
810
+ }
811
+ async function collectScIdsFromScenarioFiles(scenarioFiles) {
812
+ const scIds = /* @__PURE__ */ new Set();
813
+ for (const file of scenarioFiles) {
814
+ const text = await (0, import_promises5.readFile)(file, "utf-8");
815
+ const { document, errors } = parseScenarioDocument(text, file);
816
+ if (!document || errors.length > 0) {
817
+ continue;
818
+ }
819
+ for (const scenario of document.scenarios) {
820
+ for (const tag of scenario.tags) {
821
+ if (SC_TAG_RE2.test(tag)) {
822
+ scIds.add(tag);
823
+ }
824
+ }
825
+ }
826
+ }
827
+ return scIds;
828
+ }
829
+ async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
830
+ const sources = /* @__PURE__ */ new Map();
831
+ for (const file of scenarioFiles) {
832
+ const text = await (0, import_promises5.readFile)(file, "utf-8");
833
+ const { document, errors } = parseScenarioDocument(text, file);
834
+ if (!document || errors.length > 0) {
835
+ continue;
836
+ }
837
+ for (const scenario of document.scenarios) {
838
+ for (const tag of scenario.tags) {
839
+ if (!SC_TAG_RE2.test(tag)) {
840
+ continue;
841
+ }
842
+ const current = sources.get(tag) ?? /* @__PURE__ */ new Set();
843
+ current.add(file);
844
+ sources.set(tag, current);
845
+ }
846
+ }
847
+ }
848
+ return sources;
849
+ }
850
+ async function collectScTestReferences(root, globs, excludeGlobs) {
851
+ const refs = /* @__PURE__ */ new Map();
852
+ const normalizedGlobs = normalizeGlobs(globs);
853
+ const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
854
+ const mergedExcludeGlobs = Array.from(
855
+ /* @__PURE__ */ new Set([...DEFAULT_TEST_FILE_EXCLUDE_GLOBS, ...normalizedExcludeGlobs])
856
+ );
857
+ if (normalizedGlobs.length === 0) {
858
+ return {
859
+ refs,
860
+ scan: {
861
+ globs: normalizedGlobs,
862
+ excludeGlobs: mergedExcludeGlobs,
863
+ matchedFileCount: 0
864
+ }
865
+ };
866
+ }
867
+ let files = [];
868
+ try {
869
+ files = await collectFilesByGlobs(root, {
870
+ globs: normalizedGlobs,
871
+ ignore: mergedExcludeGlobs
872
+ });
873
+ } catch (error) {
874
+ return {
875
+ refs,
876
+ scan: {
877
+ globs: normalizedGlobs,
878
+ excludeGlobs: mergedExcludeGlobs,
879
+ matchedFileCount: 0
880
+ },
881
+ error: formatError3(error)
882
+ };
883
+ }
884
+ const normalizedFiles = Array.from(
885
+ new Set(files.map((file) => import_node_path4.default.normalize(file)))
886
+ );
887
+ for (const file of normalizedFiles) {
888
+ const text = await (0, import_promises5.readFile)(file, "utf-8");
889
+ const scIds = extractAnnotatedScIds(text);
890
+ if (scIds.length === 0) {
891
+ continue;
892
+ }
893
+ for (const scId of scIds) {
894
+ const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
895
+ current.add(file);
896
+ refs.set(scId, current);
897
+ }
898
+ }
899
+ return {
900
+ refs,
901
+ scan: {
902
+ globs: normalizedGlobs,
903
+ excludeGlobs: mergedExcludeGlobs,
904
+ matchedFileCount: normalizedFiles.length
905
+ }
906
+ };
907
+ }
908
+ function buildScCoverage(scIds, refs) {
909
+ const sortedScIds = toSortedArray(scIds);
910
+ const refsRecord = {};
911
+ const missingIds = [];
912
+ let covered = 0;
913
+ for (const scId of sortedScIds) {
914
+ const files = refs.get(scId);
915
+ const sortedFiles = files ? toSortedArray(files) : [];
916
+ refsRecord[scId] = sortedFiles;
917
+ if (sortedFiles.length === 0) {
918
+ missingIds.push(scId);
919
+ } else {
920
+ covered += 1;
921
+ }
922
+ }
923
+ return {
924
+ total: sortedScIds.length,
925
+ covered,
926
+ missing: missingIds.length,
927
+ missingIds,
928
+ refs: refsRecord
929
+ };
930
+ }
931
+ function toSortedArray(values) {
932
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
933
+ }
934
+ function normalizeGlobs(globs) {
935
+ return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
936
+ }
937
+ function formatError3(error) {
938
+ if (error instanceof Error) {
939
+ return error.message;
940
+ }
941
+ return String(error);
942
+ }
943
+
944
+ // src/core/version.ts
945
+ var import_promises6 = require("fs/promises");
946
+ var import_node_path5 = __toESM(require("path"), 1);
592
947
  var import_node_url = require("url");
593
948
  async function resolveToolVersion() {
594
- if ("0.3.5".length > 0) {
595
- return "0.3.5";
949
+ if ("0.4.2".length > 0) {
950
+ return "0.4.2";
596
951
  }
597
952
  try {
598
953
  const packagePath = resolvePackageJsonPath();
599
- const raw = await (0, import_promises5.readFile)(packagePath, "utf-8");
954
+ const raw = await (0, import_promises6.readFile)(packagePath, "utf-8");
600
955
  const parsed = JSON.parse(raw);
601
956
  const version = typeof parsed.version === "string" ? parsed.version : "";
602
957
  return version.length > 0 ? version : "unknown";
@@ -607,18 +962,18 @@ async function resolveToolVersion() {
607
962
  function resolvePackageJsonPath() {
608
963
  const base = __filename;
609
964
  const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
610
- return import_node_path4.default.resolve(import_node_path4.default.dirname(basePath), "../../package.json");
965
+ return import_node_path5.default.resolve(import_node_path5.default.dirname(basePath), "../../package.json");
611
966
  }
612
967
 
613
968
  // src/core/validators/contracts.ts
614
- var import_promises6 = require("fs/promises");
615
- var import_node_path6 = __toESM(require("path"), 1);
969
+ var import_promises7 = require("fs/promises");
970
+ var import_node_path7 = __toESM(require("path"), 1);
616
971
 
617
972
  // src/core/contracts.ts
618
- var import_node_path5 = __toESM(require("path"), 1);
973
+ var import_node_path6 = __toESM(require("path"), 1);
619
974
  var import_yaml2 = require("yaml");
620
975
  function parseStructuredContract(file, text) {
621
- const ext = import_node_path5.default.extname(file).toLowerCase();
976
+ const ext = import_node_path6.default.extname(file).toLowerCase();
622
977
  if (ext === ".json") {
623
978
  return JSON.parse(text);
624
979
  }
@@ -669,9 +1024,9 @@ var SQL_DANGEROUS_PATTERNS = [
669
1024
  async function validateContracts(root, config) {
670
1025
  const issues = [];
671
1026
  const contractsRoot = resolvePath(root, config, "contractsDir");
672
- issues.push(...await validateUiContracts(import_node_path6.default.join(contractsRoot, "ui")));
673
- issues.push(...await validateApiContracts(import_node_path6.default.join(contractsRoot, "api")));
674
- issues.push(...await validateDataContracts(import_node_path6.default.join(contractsRoot, "db")));
1027
+ issues.push(...await validateUiContracts(import_node_path7.default.join(contractsRoot, "ui")));
1028
+ issues.push(...await validateApiContracts(import_node_path7.default.join(contractsRoot, "api")));
1029
+ issues.push(...await validateDataContracts(import_node_path7.default.join(contractsRoot, "db")));
675
1030
  return issues;
676
1031
  }
677
1032
  async function validateUiContracts(uiRoot) {
@@ -689,7 +1044,7 @@ async function validateUiContracts(uiRoot) {
689
1044
  }
690
1045
  const issues = [];
691
1046
  for (const file of files) {
692
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1047
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
693
1048
  const invalidIds = extractInvalidIds(text, [
694
1049
  "SPEC",
695
1050
  "BR",
@@ -718,7 +1073,7 @@ async function validateUiContracts(uiRoot) {
718
1073
  issues.push(
719
1074
  issue(
720
1075
  "QFAI-CONTRACT-001",
721
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error)})`,
1076
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
722
1077
  "error",
723
1078
  file,
724
1079
  "contracts.ui.parse"
@@ -756,7 +1111,7 @@ async function validateApiContracts(apiRoot) {
756
1111
  }
757
1112
  const issues = [];
758
1113
  for (const file of files) {
759
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1114
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
760
1115
  const invalidIds = extractInvalidIds(text, [
761
1116
  "SPEC",
762
1117
  "BR",
@@ -785,7 +1140,7 @@ async function validateApiContracts(apiRoot) {
785
1140
  issues.push(
786
1141
  issue(
787
1142
  "QFAI-CONTRACT-001",
788
- `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error)})`,
1143
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
789
1144
  "error",
790
1145
  file,
791
1146
  "contracts.api.parse"
@@ -834,7 +1189,7 @@ async function validateDataContracts(dataRoot) {
834
1189
  }
835
1190
  const issues = [];
836
1191
  for (const file of files) {
837
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1192
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
838
1193
  const invalidIds = extractInvalidIds(text, [
839
1194
  "SPEC",
840
1195
  "BR",
@@ -880,7 +1235,7 @@ function lintSql(text, file) {
880
1235
  function hasOpenApi(doc) {
881
1236
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
882
1237
  }
883
- function formatError2(error) {
1238
+ function formatError4(error) {
884
1239
  if (error instanceof Error) {
885
1240
  return error.message;
886
1241
  }
@@ -905,8 +1260,8 @@ function issue(code, message, severity, file, rule, refs) {
905
1260
  }
906
1261
 
907
1262
  // src/core/validators/delta.ts
908
- var import_promises7 = require("fs/promises");
909
- var import_node_path7 = __toESM(require("path"), 1);
1263
+ var import_promises8 = require("fs/promises");
1264
+ var import_node_path8 = __toESM(require("path"), 1);
910
1265
  var SECTION_RE = /^##\s+変更区分/m;
911
1266
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
912
1267
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -920,10 +1275,10 @@ async function validateDeltas(root, config) {
920
1275
  }
921
1276
  const issues = [];
922
1277
  for (const pack of packs) {
923
- const deltaPath = import_node_path7.default.join(pack, "delta.md");
1278
+ const deltaPath = import_node_path8.default.join(pack, "delta.md");
924
1279
  let text;
925
1280
  try {
926
- text = await (0, import_promises7.readFile)(deltaPath, "utf-8");
1281
+ text = await (0, import_promises8.readFile)(deltaPath, "utf-8");
927
1282
  } catch (error) {
928
1283
  if (isMissingFileError2(error)) {
929
1284
  issues.push(
@@ -995,17 +1350,17 @@ function issue2(code, message, severity, file, rule, refs) {
995
1350
  }
996
1351
 
997
1352
  // src/core/validators/ids.ts
998
- var import_promises9 = require("fs/promises");
999
- var import_node_path9 = __toESM(require("path"), 1);
1353
+ var import_promises10 = require("fs/promises");
1354
+ var import_node_path10 = __toESM(require("path"), 1);
1000
1355
 
1001
1356
  // src/core/contractIndex.ts
1002
- var import_promises8 = require("fs/promises");
1003
- var import_node_path8 = __toESM(require("path"), 1);
1357
+ var import_promises9 = require("fs/promises");
1358
+ var import_node_path9 = __toESM(require("path"), 1);
1004
1359
  async function buildContractIndex(root, config) {
1005
1360
  const contractsRoot = resolvePath(root, config, "contractsDir");
1006
- const uiRoot = import_node_path8.default.join(contractsRoot, "ui");
1007
- const apiRoot = import_node_path8.default.join(contractsRoot, "api");
1008
- const dataRoot = import_node_path8.default.join(contractsRoot, "db");
1361
+ const uiRoot = import_node_path9.default.join(contractsRoot, "ui");
1362
+ const apiRoot = import_node_path9.default.join(contractsRoot, "api");
1363
+ const dataRoot = import_node_path9.default.join(contractsRoot, "db");
1009
1364
  const [uiFiles, apiFiles, dataFiles] = await Promise.all([
1010
1365
  collectUiContractFiles(uiRoot),
1011
1366
  collectApiContractFiles(apiRoot),
@@ -1024,7 +1379,7 @@ async function buildContractIndex(root, config) {
1024
1379
  }
1025
1380
  async function indexUiContracts(files, index) {
1026
1381
  for (const file of files) {
1027
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1382
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1028
1383
  try {
1029
1384
  const doc = parseStructuredContract(file, text);
1030
1385
  extractUiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1036,7 +1391,7 @@ async function indexUiContracts(files, index) {
1036
1391
  }
1037
1392
  async function indexApiContracts(files, index) {
1038
1393
  for (const file of files) {
1039
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1394
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1040
1395
  try {
1041
1396
  const doc = parseStructuredContract(file, text);
1042
1397
  extractApiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1048,7 +1403,7 @@ async function indexApiContracts(files, index) {
1048
1403
  }
1049
1404
  async function indexDataContracts(files, index) {
1050
1405
  for (const file of files) {
1051
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1406
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1052
1407
  extractIds(text, "DATA").forEach((id) => record(index, id, file));
1053
1408
  }
1054
1409
  }
@@ -1088,251 +1443,97 @@ function extractH2Sections(md) {
1088
1443
  if (!current) continue;
1089
1444
  const next = headings[i + 1];
1090
1445
  const startLine = current.line + 1;
1091
- const endLine = (next?.line ?? lines.length + 1) - 1;
1092
- const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1093
- sections.set(current.title.trim(), {
1094
- title: current.title.trim(),
1095
- startLine,
1096
- endLine,
1097
- body
1098
- });
1099
- }
1100
- return sections;
1101
- }
1102
-
1103
- // src/core/parse/spec.ts
1104
- var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1105
- var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1106
- var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1107
- var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1108
- var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1109
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1110
- function parseSpec(md, file) {
1111
- const headings = parseHeadings(md);
1112
- const h1 = headings.find((heading) => heading.level === 1);
1113
- const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1114
- const sections = extractH2Sections(md);
1115
- const sectionNames = new Set(Array.from(sections.keys()));
1116
- const brSection = sections.get(BR_SECTION_TITLE);
1117
- const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1118
- const startLine = brSection?.startLine ?? 1;
1119
- const brs = [];
1120
- const brsWithoutPriority = [];
1121
- const brsWithInvalidPriority = [];
1122
- for (let i = 0; i < brLines.length; i++) {
1123
- const lineText = brLines[i] ?? "";
1124
- const lineNumber = startLine + i;
1125
- const validMatch = lineText.match(BR_LINE_RE);
1126
- if (validMatch) {
1127
- const id = validMatch[1];
1128
- const priority = validMatch[2];
1129
- const text = validMatch[3];
1130
- if (!id || !priority || !text) continue;
1131
- brs.push({
1132
- id,
1133
- priority,
1134
- text: text.trim(),
1135
- line: lineNumber
1136
- });
1137
- continue;
1138
- }
1139
- const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1140
- if (anyPriorityMatch) {
1141
- const id = anyPriorityMatch[1];
1142
- const priority = anyPriorityMatch[2];
1143
- const text = anyPriorityMatch[3];
1144
- if (!id || !priority || !text) continue;
1145
- if (!VALID_PRIORITIES.has(priority)) {
1146
- brsWithInvalidPriority.push({
1147
- id,
1148
- priority,
1149
- text: text.trim(),
1150
- line: lineNumber
1151
- });
1152
- }
1153
- continue;
1154
- }
1155
- const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1156
- if (noPriorityMatch) {
1157
- const id = noPriorityMatch[1];
1158
- const text = noPriorityMatch[2];
1159
- if (!id || !text) continue;
1160
- brsWithoutPriority.push({
1161
- id,
1162
- text: text.trim(),
1163
- line: lineNumber
1164
- });
1165
- }
1166
- }
1167
- const parsed = {
1168
- file,
1169
- sections: sectionNames,
1170
- brs,
1171
- brsWithoutPriority,
1172
- brsWithInvalidPriority
1173
- };
1174
- if (specId) {
1175
- parsed.specId = specId;
1176
- }
1177
- return parsed;
1178
- }
1179
-
1180
- // src/core/gherkin/parse.ts
1181
- var import_gherkin = require("@cucumber/gherkin");
1182
- var import_node_crypto = require("crypto");
1183
- function parseGherkin(source, uri) {
1184
- const errors = [];
1185
- const uuidFn = () => (0, import_node_crypto.randomUUID)();
1186
- const builder = new import_gherkin.AstBuilder(uuidFn);
1187
- const matcher = new import_gherkin.GherkinClassicTokenMatcher();
1188
- const parser = new import_gherkin.Parser(builder, matcher);
1189
- try {
1190
- const gherkinDocument = parser.parse(source);
1191
- gherkinDocument.uri = uri;
1192
- return { gherkinDocument, errors };
1193
- } catch (error) {
1194
- errors.push(formatError3(error));
1195
- return { gherkinDocument: null, errors };
1196
- }
1197
- }
1198
- function formatError3(error) {
1199
- if (error instanceof Error) {
1200
- return error.message;
1201
- }
1202
- return String(error);
1203
- }
1204
-
1205
- // src/core/scenarioModel.ts
1206
- var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1207
- var SC_TAG_RE = /^SC-\d{4}$/;
1208
- var BR_TAG_RE = /^BR-\d{4}$/;
1209
- var UI_TAG_RE = /^UI-\d{4}$/;
1210
- var API_TAG_RE = /^API-\d{4}$/;
1211
- var DATA_TAG_RE = /^DATA-\d{4}$/;
1212
- function parseScenarioDocument(text, uri) {
1213
- const { gherkinDocument, errors } = parseGherkin(text, uri);
1214
- if (!gherkinDocument) {
1215
- return { document: null, errors };
1216
- }
1217
- const feature = gherkinDocument.feature;
1218
- if (!feature) {
1219
- return {
1220
- document: { uri, featureTags: [], scenarios: [] },
1221
- errors
1222
- };
1223
- }
1224
- const featureTags = collectTagNames(feature.tags);
1225
- const scenarios = collectScenarioNodes(feature, featureTags);
1226
- return {
1227
- document: {
1228
- uri,
1229
- featureName: feature.name,
1230
- featureTags,
1231
- scenarios
1232
- },
1233
- errors
1234
- };
1235
- }
1236
- function buildScenarioAtoms(document) {
1237
- return document.scenarios.map((scenario) => {
1238
- const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
1239
- const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
1240
- const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1241
- const contractIds = /* @__PURE__ */ new Set();
1242
- scenario.tags.forEach((tag) => {
1243
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1244
- contractIds.add(tag);
1245
- }
1246
- });
1247
- for (const step of scenario.steps) {
1248
- for (const text of collectStepTexts(step)) {
1249
- extractIds(text, "UI").forEach((id) => contractIds.add(id));
1250
- extractIds(text, "API").forEach((id) => contractIds.add(id));
1251
- extractIds(text, "DATA").forEach((id) => contractIds.add(id));
1252
- }
1253
- }
1254
- const atom = {
1255
- uri: document.uri,
1256
- featureName: document.featureName ?? "",
1257
- scenarioName: scenario.name,
1258
- kind: scenario.kind,
1259
- brIds,
1260
- contractIds: Array.from(contractIds).sort()
1261
- };
1262
- if (scenario.line !== void 0) {
1263
- atom.line = scenario.line;
1264
- }
1265
- if (specIds.length === 1) {
1266
- const specId = specIds[0];
1267
- if (specId) {
1268
- atom.specId = specId;
1269
- }
1270
- }
1271
- if (scIds.length === 1) {
1272
- const scId = scIds[0];
1273
- if (scId) {
1274
- atom.scId = scId;
1275
- }
1276
- }
1277
- return atom;
1278
- });
1446
+ const endLine = (next?.line ?? lines.length + 1) - 1;
1447
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1448
+ sections.set(current.title.trim(), {
1449
+ title: current.title.trim(),
1450
+ startLine,
1451
+ endLine,
1452
+ body
1453
+ });
1454
+ }
1455
+ return sections;
1279
1456
  }
1280
- function collectScenarioNodes(feature, featureTags) {
1281
- const scenarios = [];
1282
- for (const child of feature.children) {
1283
- if (child.scenario) {
1284
- scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
1457
+
1458
+ // src/core/parse/spec.ts
1459
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1460
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1461
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1462
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1463
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1464
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1465
+ function parseSpec(md, file) {
1466
+ const headings = parseHeadings(md);
1467
+ const h1 = headings.find((heading) => heading.level === 1);
1468
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1469
+ const sections = extractH2Sections(md);
1470
+ const sectionNames = new Set(Array.from(sections.keys()));
1471
+ const brSection = sections.get(BR_SECTION_TITLE);
1472
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1473
+ const startLine = brSection?.startLine ?? 1;
1474
+ const brs = [];
1475
+ const brsWithoutPriority = [];
1476
+ const brsWithInvalidPriority = [];
1477
+ for (let i = 0; i < brLines.length; i++) {
1478
+ const lineText = brLines[i] ?? "";
1479
+ const lineNumber = startLine + i;
1480
+ const validMatch = lineText.match(BR_LINE_RE);
1481
+ if (validMatch) {
1482
+ const id = validMatch[1];
1483
+ const priority = validMatch[2];
1484
+ const text = validMatch[3];
1485
+ if (!id || !priority || !text) continue;
1486
+ brs.push({
1487
+ id,
1488
+ priority,
1489
+ text: text.trim(),
1490
+ line: lineNumber
1491
+ });
1492
+ continue;
1285
1493
  }
1286
- if (child.rule) {
1287
- const ruleTags = collectTagNames(child.rule.tags);
1288
- for (const ruleChild of child.rule.children) {
1289
- if (ruleChild.scenario) {
1290
- scenarios.push(
1291
- buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
1292
- );
1293
- }
1494
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1495
+ if (anyPriorityMatch) {
1496
+ const id = anyPriorityMatch[1];
1497
+ const priority = anyPriorityMatch[2];
1498
+ const text = anyPriorityMatch[3];
1499
+ if (!id || !priority || !text) continue;
1500
+ if (!VALID_PRIORITIES.has(priority)) {
1501
+ brsWithInvalidPriority.push({
1502
+ id,
1503
+ priority,
1504
+ text: text.trim(),
1505
+ line: lineNumber
1506
+ });
1294
1507
  }
1508
+ continue;
1509
+ }
1510
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1511
+ if (noPriorityMatch) {
1512
+ const id = noPriorityMatch[1];
1513
+ const text = noPriorityMatch[2];
1514
+ if (!id || !text) continue;
1515
+ brsWithoutPriority.push({
1516
+ id,
1517
+ text: text.trim(),
1518
+ line: lineNumber
1519
+ });
1295
1520
  }
1296
1521
  }
1297
- return scenarios;
1298
- }
1299
- function buildScenarioNode(scenario, featureTags, ruleTags) {
1300
- const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
1301
- const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
1302
- return {
1303
- name: scenario.name,
1304
- kind,
1305
- line: scenario.location?.line,
1306
- tags,
1307
- steps: scenario.steps
1522
+ const parsed = {
1523
+ file,
1524
+ sections: sectionNames,
1525
+ brs,
1526
+ brsWithoutPriority,
1527
+ brsWithInvalidPriority
1308
1528
  };
1309
- }
1310
- function collectTagNames(tags) {
1311
- return tags.map((tag) => tag.name.replace(/^@/, ""));
1312
- }
1313
- function collectStepTexts(step) {
1314
- const texts = [];
1315
- if (step.text) {
1316
- texts.push(step.text);
1317
- }
1318
- if (step.docString?.content) {
1319
- texts.push(step.docString.content);
1320
- }
1321
- if (step.dataTable?.rows) {
1322
- for (const row of step.dataTable.rows) {
1323
- for (const cell of row.cells) {
1324
- texts.push(cell.value);
1325
- }
1326
- }
1529
+ if (specId) {
1530
+ parsed.specId = specId;
1327
1531
  }
1328
- return texts;
1329
- }
1330
- function unique2(values) {
1331
- return Array.from(new Set(values));
1532
+ return parsed;
1332
1533
  }
1333
1534
 
1334
1535
  // src/core/validators/ids.ts
1335
- var SC_TAG_RE2 = /^SC-\d{4}$/;
1536
+ var SC_TAG_RE3 = /^SC-\d{4}$/;
1336
1537
  async function validateDefinedIds(root, config) {
1337
1538
  const issues = [];
1338
1539
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1366,7 +1567,7 @@ async function validateDefinedIds(root, config) {
1366
1567
  }
1367
1568
  async function collectSpecDefinitionIds(files, out) {
1368
1569
  for (const file of files) {
1369
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1570
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1370
1571
  const parsed = parseSpec(text, file);
1371
1572
  if (parsed.specId) {
1372
1573
  recordId(out, parsed.specId, file);
@@ -1376,14 +1577,14 @@ async function collectSpecDefinitionIds(files, out) {
1376
1577
  }
1377
1578
  async function collectScenarioDefinitionIds(files, out) {
1378
1579
  for (const file of files) {
1379
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1580
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1380
1581
  const { document, errors } = parseScenarioDocument(text, file);
1381
1582
  if (!document || errors.length > 0) {
1382
1583
  continue;
1383
1584
  }
1384
1585
  for (const scenario of document.scenarios) {
1385
1586
  for (const tag of scenario.tags) {
1386
- if (SC_TAG_RE2.test(tag)) {
1587
+ if (SC_TAG_RE3.test(tag)) {
1387
1588
  recordId(out, tag, file);
1388
1589
  }
1389
1590
  }
@@ -1397,7 +1598,7 @@ function recordId(out, id, file) {
1397
1598
  }
1398
1599
  function formatFileList(files, root) {
1399
1600
  return files.map((file) => {
1400
- const relative = import_node_path9.default.relative(root, file);
1601
+ const relative = import_node_path10.default.relative(root, file);
1401
1602
  return relative.length > 0 ? relative : file;
1402
1603
  }).join(", ");
1403
1604
  }
@@ -1420,13 +1621,12 @@ function issue3(code, message, severity, file, rule, refs) {
1420
1621
  }
1421
1622
 
1422
1623
  // src/core/validators/scenario.ts
1423
- var import_promises10 = require("fs/promises");
1624
+ var import_promises11 = require("fs/promises");
1424
1625
  var GIVEN_PATTERN = /\bGiven\b/;
1425
1626
  var WHEN_PATTERN = /\bWhen\b/;
1426
1627
  var THEN_PATTERN = /\bThen\b/;
1427
- var SC_TAG_RE3 = /^SC-\d{4}$/;
1628
+ var SC_TAG_RE4 = /^SC-\d{4}$/;
1428
1629
  var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1429
- var BR_TAG_RE2 = /^BR-\d{4}$/;
1430
1630
  async function validateScenarios(root, config) {
1431
1631
  const specsRoot = resolvePath(root, config, "specsDir");
1432
1632
  const entries = await collectSpecEntries(specsRoot);
@@ -1447,7 +1647,7 @@ async function validateScenarios(root, config) {
1447
1647
  for (const entry of entries) {
1448
1648
  let text;
1449
1649
  try {
1450
- text = await (0, import_promises10.readFile)(entry.scenarioPath, "utf-8");
1650
+ text = await (0, import_promises11.readFile)(entry.scenarioPath, "utf-8");
1451
1651
  } catch (error) {
1452
1652
  if (isMissingFileError3(error)) {
1453
1653
  issues.push(
@@ -1506,17 +1706,7 @@ function validateScenarioContent(text, file) {
1506
1706
  const featureSpecTags = document.featureTags.filter(
1507
1707
  (tag) => SPEC_TAG_RE2.test(tag)
1508
1708
  );
1509
- if (featureSpecTags.length === 0) {
1510
- issues.push(
1511
- issue4(
1512
- "QFAI-SC-009",
1513
- "Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1514
- "error",
1515
- file,
1516
- "scenario.featureSpec"
1517
- )
1518
- );
1519
- } else if (featureSpecTags.length > 1) {
1709
+ if (featureSpecTags.length > 1) {
1520
1710
  issues.push(
1521
1711
  issue4(
1522
1712
  "QFAI-SC-009",
@@ -1558,18 +1748,12 @@ function validateScenarioContent(text, file) {
1558
1748
  continue;
1559
1749
  }
1560
1750
  const missingTags = [];
1561
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
1751
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
1562
1752
  if (scTags.length === 0) {
1563
1753
  missingTags.push("SC(0\u4EF6)");
1564
1754
  } else if (scTags.length > 1) {
1565
1755
  missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1566
1756
  }
1567
- if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
1568
- missingTags.push("SPEC");
1569
- }
1570
- if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
1571
- missingTags.push("BR");
1572
- }
1573
1757
  if (missingTags.length > 0) {
1574
1758
  issues.push(
1575
1759
  issue4(
@@ -1633,7 +1817,7 @@ function isMissingFileError3(error) {
1633
1817
  }
1634
1818
 
1635
1819
  // src/core/validators/spec.ts
1636
- var import_promises11 = require("fs/promises");
1820
+ var import_promises12 = require("fs/promises");
1637
1821
  async function validateSpecs(root, config) {
1638
1822
  const specsRoot = resolvePath(root, config, "specsDir");
1639
1823
  const entries = await collectSpecEntries(specsRoot);
@@ -1654,7 +1838,7 @@ async function validateSpecs(root, config) {
1654
1838
  for (const entry of entries) {
1655
1839
  let text;
1656
1840
  try {
1657
- text = await (0, import_promises11.readFile)(entry.specPath, "utf-8");
1841
+ text = await (0, import_promises12.readFile)(entry.specPath, "utf-8");
1658
1842
  } catch (error) {
1659
1843
  if (isMissingFileError4(error)) {
1660
1844
  issues.push(
@@ -1803,10 +1987,9 @@ function isMissingFileError4(error) {
1803
1987
  }
1804
1988
 
1805
1989
  // src/core/validators/traceability.ts
1806
- var import_promises12 = require("fs/promises");
1807
- var SC_TAG_RE4 = /^SC-\d{4}$/;
1990
+ var import_promises13 = require("fs/promises");
1808
1991
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
1809
- var BR_TAG_RE3 = /^BR-\d{4}$/;
1992
+ var BR_TAG_RE2 = /^BR-\d{4}$/;
1810
1993
  async function validateTraceability(root, config) {
1811
1994
  const issues = [];
1812
1995
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1825,7 +2008,7 @@ async function validateTraceability(root, config) {
1825
2008
  const contractIndex = await buildContractIndex(root, config);
1826
2009
  const contractIds = contractIndex.ids;
1827
2010
  for (const file of specFiles) {
1828
- const text = await (0, import_promises12.readFile)(file, "utf-8");
2011
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
1829
2012
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1830
2013
  const parsed = parseSpec(text, file);
1831
2014
  if (parsed.specId) {
@@ -1833,28 +2016,6 @@ async function validateTraceability(root, config) {
1833
2016
  }
1834
2017
  const brIds = parsed.brs.map((br) => br.id);
1835
2018
  brIds.forEach((id) => brIdsInSpecs.add(id));
1836
- const referencedContractIds = /* @__PURE__ */ new Set([
1837
- ...extractIds(text, "UI"),
1838
- ...extractIds(text, "API"),
1839
- ...extractIds(text, "DATA")
1840
- ]);
1841
- const unknownContractIds = Array.from(referencedContractIds).filter(
1842
- (id) => !contractIds.has(id)
1843
- );
1844
- if (unknownContractIds.length > 0) {
1845
- issues.push(
1846
- issue6(
1847
- "QFAI-TRACE-009",
1848
- `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1849
- ", "
1850
- )}`,
1851
- "error",
1852
- file,
1853
- "traceability.specContractExists",
1854
- unknownContractIds
1855
- )
1856
- );
1857
- }
1858
2019
  if (parsed.specId) {
1859
2020
  const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
1860
2021
  brIds.forEach((id) => current.add(id));
@@ -1862,23 +2023,49 @@ async function validateTraceability(root, config) {
1862
2023
  }
1863
2024
  }
1864
2025
  for (const file of scenarioFiles) {
1865
- const text = await (0, import_promises12.readFile)(file, "utf-8");
2026
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
1866
2027
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1867
2028
  const { document, errors } = parseScenarioDocument(text, file);
1868
2029
  if (!document || errors.length > 0) {
1869
2030
  continue;
1870
2031
  }
1871
2032
  const atoms = buildScenarioAtoms(document);
2033
+ const scIdsInFile = /* @__PURE__ */ new Set();
1872
2034
  for (const [index, scenario] of document.scenarios.entries()) {
1873
2035
  const atom = atoms[index];
1874
2036
  if (!atom) {
1875
2037
  continue;
1876
2038
  }
1877
2039
  const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
1878
- const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
1879
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
2040
+ const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
2041
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
2042
+ if (specTags.length === 0) {
2043
+ issues.push(
2044
+ issue6(
2045
+ "QFAI-TRACE-014",
2046
+ `Scenario \u304C SPEC \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
2047
+ "error",
2048
+ file,
2049
+ "traceability.scenarioSpecRequired"
2050
+ )
2051
+ );
2052
+ }
2053
+ if (brTags.length === 0) {
2054
+ issues.push(
2055
+ issue6(
2056
+ "QFAI-TRACE-015",
2057
+ `Scenario \u304C BR \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
2058
+ "error",
2059
+ file,
2060
+ "traceability.scenarioBrRequired"
2061
+ )
2062
+ );
2063
+ }
1880
2064
  brTags.forEach((id) => brIdsInScenarios.add(id));
1881
- scTags.forEach((id) => scIdsInScenarios.add(id));
2065
+ scTags.forEach((id) => {
2066
+ scIdsInScenarios.add(id);
2067
+ scIdsInFile.add(id);
2068
+ });
1882
2069
  atom.contractIds.forEach((id) => scenarioContractIds.add(id));
1883
2070
  if (atom.contractIds.length > 0) {
1884
2071
  scTags.forEach((id) => scWithContracts.add(id));
@@ -1956,6 +2143,22 @@ async function validateTraceability(root, config) {
1956
2143
  }
1957
2144
  }
1958
2145
  }
2146
+ if (scIdsInFile.size !== 1) {
2147
+ const invalidScIds = Array.from(scIdsInFile).sort(
2148
+ (a, b) => a.localeCompare(b)
2149
+ );
2150
+ 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(", ")}`;
2151
+ issues.push(
2152
+ issue6(
2153
+ "QFAI-TRACE-012",
2154
+ `Spec entry \u304C Spec:SC=1:1 \u3092\u6E80\u305F\u3057\u3066\u3044\u307E\u305B\u3093: ${detail}`,
2155
+ "error",
2156
+ file,
2157
+ "traceability.specScOneToOne",
2158
+ invalidScIds
2159
+ )
2160
+ );
2161
+ }
1959
2162
  }
1960
2163
  if (upstreamIds.size === 0) {
1961
2164
  return [
@@ -2004,6 +2207,66 @@ async function validateTraceability(root, config) {
2004
2207
  );
2005
2208
  }
2006
2209
  }
2210
+ const scRefsResult = await collectScTestReferences(
2211
+ root,
2212
+ config.validation.traceability.testFileGlobs,
2213
+ config.validation.traceability.testFileExcludeGlobs
2214
+ );
2215
+ const scTestRefs = scRefsResult.refs;
2216
+ const testFileScan = scRefsResult.scan;
2217
+ const hasScenarios = scIdsInScenarios.size > 0;
2218
+ const hasGlobConfig = testFileScan.globs.length > 0;
2219
+ const hasMatchedTests = testFileScan.matchedFileCount > 0;
2220
+ if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
2221
+ const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
2222
+ issues.push(
2223
+ issue6(
2224
+ "QFAI-TRACE-013",
2225
+ `\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}`,
2226
+ "error",
2227
+ testsRoot,
2228
+ "traceability.testFileGlobs"
2229
+ )
2230
+ );
2231
+ } else {
2232
+ if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2233
+ const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2234
+ const refs = scTestRefs.get(id);
2235
+ return !refs || refs.size === 0;
2236
+ });
2237
+ if (scWithoutTests.length > 0) {
2238
+ issues.push(
2239
+ issue6(
2240
+ "QFAI-TRACE-010",
2241
+ `SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(
2242
+ ", "
2243
+ )}\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`,
2244
+ config.validation.traceability.scNoTestSeverity,
2245
+ testsRoot,
2246
+ "traceability.scMustHaveTest",
2247
+ scWithoutTests
2248
+ )
2249
+ );
2250
+ }
2251
+ }
2252
+ const unknownScIds = Array.from(scTestRefs.keys()).filter(
2253
+ (id) => !scIdsInScenarios.has(id)
2254
+ );
2255
+ if (unknownScIds.length > 0) {
2256
+ issues.push(
2257
+ issue6(
2258
+ "QFAI-TRACE-011",
2259
+ `\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(
2260
+ ", "
2261
+ )}`,
2262
+ "error",
2263
+ testsRoot,
2264
+ "traceability.scUnknownInTests",
2265
+ unknownScIds
2266
+ )
2267
+ );
2268
+ }
2269
+ }
2007
2270
  if (!config.validation.traceability.allowOrphanContracts) {
2008
2271
  if (contractIds.size > 0) {
2009
2272
  const orphanContracts = Array.from(contractIds).filter(
@@ -2052,7 +2315,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2052
2315
  const pattern = buildIdPattern(Array.from(upstreamIds));
2053
2316
  let found = false;
2054
2317
  for (const file of targetFiles) {
2055
- const text = await (0, import_promises12.readFile)(file, "utf-8");
2318
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
2056
2319
  if (pattern.test(text)) {
2057
2320
  found = true;
2058
2321
  break;
@@ -2062,8 +2325,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2062
2325
  issues.push(
2063
2326
  issue6(
2064
2327
  "QFAI-TRACE-002",
2065
- "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
2066
- "warning",
2328
+ "\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",
2329
+ "info",
2067
2330
  srcRoot,
2068
2331
  "traceability.codeReferences"
2069
2332
  )
@@ -2106,12 +2369,24 @@ async function validateProject(root, configResult) {
2106
2369
  ...await validateDefinedIds(root, config),
2107
2370
  ...await validateTraceability(root, config)
2108
2371
  ];
2372
+ const specsRoot = resolvePath(root, config, "specsDir");
2373
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
2374
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2375
+ const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
2376
+ root,
2377
+ config.validation.traceability.testFileGlobs,
2378
+ config.validation.traceability.testFileExcludeGlobs
2379
+ );
2380
+ const scCoverage = buildScCoverage(scIds, scTestRefs);
2109
2381
  const toolVersion = await resolveToolVersion();
2110
2382
  return {
2111
- schemaVersion: VALIDATION_SCHEMA_VERSION,
2112
2383
  toolVersion,
2113
2384
  issues,
2114
- counts: countIssues(issues)
2385
+ counts: countIssues(issues),
2386
+ traceability: {
2387
+ sc: scCoverage,
2388
+ testFiles
2389
+ }
2115
2390
  };
2116
2391
  }
2117
2392
  function countIssues(issues) {
@@ -2132,9 +2407,9 @@ async function createReportData(root, validation, configResult) {
2132
2407
  const configPath = resolved.configPath;
2133
2408
  const specsRoot = resolvePath(root, config, "specsDir");
2134
2409
  const contractsRoot = resolvePath(root, config, "contractsDir");
2135
- const apiRoot = import_node_path10.default.join(contractsRoot, "api");
2136
- const uiRoot = import_node_path10.default.join(contractsRoot, "ui");
2137
- const dbRoot = import_node_path10.default.join(contractsRoot, "db");
2410
+ const apiRoot = import_node_path11.default.join(contractsRoot, "api");
2411
+ const uiRoot = import_node_path11.default.join(contractsRoot, "ui");
2412
+ const dbRoot = import_node_path11.default.join(contractsRoot, "db");
2138
2413
  const srcRoot = resolvePath(root, config, "srcDir");
2139
2414
  const testsRoot = resolvePath(root, config, "testsDir");
2140
2415
  const specFiles = await collectSpecFiles(specsRoot);
@@ -2160,6 +2435,16 @@ async function createReportData(root, validation, configResult) {
2160
2435
  srcRoot,
2161
2436
  testsRoot
2162
2437
  );
2438
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2439
+ const scRefsResult = await collectScTestReferences(
2440
+ root,
2441
+ config.validation.traceability.testFileGlobs,
2442
+ config.validation.traceability.testFileExcludeGlobs
2443
+ );
2444
+ const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
2445
+ const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
2446
+ const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
2447
+ const scSourceRecord = mapToSortedRecord(scSources);
2163
2448
  const resolvedValidation = validation ?? await validateProject(root, resolved);
2164
2449
  const version = await resolveToolVersion();
2165
2450
  return {
@@ -2188,7 +2473,10 @@ async function createReportData(root, validation, configResult) {
2188
2473
  },
2189
2474
  traceability: {
2190
2475
  upstreamIdsFound: upstreamIds.size,
2191
- referencedInCodeOrTests: traceability
2476
+ referencedInCodeOrTests: traceability,
2477
+ sc: scCoverage,
2478
+ scSources: scSourceRecord,
2479
+ testFiles
2192
2480
  },
2193
2481
  issues: resolvedValidation.issues
2194
2482
  };
@@ -2225,6 +2513,65 @@ function formatReportMarkdown(data) {
2225
2513
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2226
2514
  );
2227
2515
  lines.push("");
2516
+ lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2517
+ lines.push(`- total: ${data.traceability.sc.total}`);
2518
+ lines.push(`- covered: ${data.traceability.sc.covered}`);
2519
+ lines.push(`- missing: ${data.traceability.sc.missing}`);
2520
+ lines.push(
2521
+ `- testFileGlobs: ${formatList(data.traceability.testFiles.globs)}`
2522
+ );
2523
+ lines.push(
2524
+ `- testFileExcludeGlobs: ${formatList(
2525
+ data.traceability.testFiles.excludeGlobs
2526
+ )}`
2527
+ );
2528
+ lines.push(
2529
+ `- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
2530
+ );
2531
+ if (data.traceability.sc.missingIds.length === 0) {
2532
+ lines.push("- missingIds: (none)");
2533
+ } else {
2534
+ const sources = data.traceability.scSources;
2535
+ const missingWithSources = data.traceability.sc.missingIds.map((id) => {
2536
+ const files = sources[id] ?? [];
2537
+ if (files.length === 0) {
2538
+ return id;
2539
+ }
2540
+ return `${id} (${files.join(", ")})`;
2541
+ });
2542
+ lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
2543
+ }
2544
+ lines.push("");
2545
+ lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
2546
+ const scRefs = data.traceability.sc.refs;
2547
+ const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
2548
+ if (scIds.length === 0) {
2549
+ lines.push("- (none)");
2550
+ } else {
2551
+ for (const scId of scIds) {
2552
+ const refs = scRefs[scId] ?? [];
2553
+ if (refs.length === 0) {
2554
+ lines.push(`- ${scId}: (none)`);
2555
+ } else {
2556
+ lines.push(`- ${scId}: ${refs.join(", ")}`);
2557
+ }
2558
+ }
2559
+ }
2560
+ lines.push("");
2561
+ lines.push("## Spec:SC=1:1 \u9055\u53CD");
2562
+ const specScIssues = data.issues.filter(
2563
+ (item) => item.code === "QFAI-TRACE-012"
2564
+ );
2565
+ if (specScIssues.length === 0) {
2566
+ lines.push("- (none)");
2567
+ } else {
2568
+ for (const item of specScIssues) {
2569
+ const location = item.file ?? "(unknown)";
2570
+ const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
2571
+ lines.push(`- ${location}: ${refs}`);
2572
+ }
2573
+ }
2574
+ lines.push("");
2228
2575
  lines.push("## Hotspots");
2229
2576
  const hotspots = buildHotspots(data.issues);
2230
2577
  if (hotspots.length === 0) {
@@ -2279,25 +2626,25 @@ async function collectIds(files) {
2279
2626
  DATA: /* @__PURE__ */ new Set()
2280
2627
  };
2281
2628
  for (const file of files) {
2282
- const text = await (0, import_promises13.readFile)(file, "utf-8");
2629
+ const text = await (0, import_promises14.readFile)(file, "utf-8");
2283
2630
  for (const prefix of ID_PREFIXES2) {
2284
2631
  const ids = extractIds(text, prefix);
2285
2632
  ids.forEach((id) => result[prefix].add(id));
2286
2633
  }
2287
2634
  }
2288
2635
  return {
2289
- SPEC: toSortedArray(result.SPEC),
2290
- BR: toSortedArray(result.BR),
2291
- SC: toSortedArray(result.SC),
2292
- UI: toSortedArray(result.UI),
2293
- API: toSortedArray(result.API),
2294
- DATA: toSortedArray(result.DATA)
2636
+ SPEC: toSortedArray2(result.SPEC),
2637
+ BR: toSortedArray2(result.BR),
2638
+ SC: toSortedArray2(result.SC),
2639
+ UI: toSortedArray2(result.UI),
2640
+ API: toSortedArray2(result.API),
2641
+ DATA: toSortedArray2(result.DATA)
2295
2642
  };
2296
2643
  }
2297
2644
  async function collectUpstreamIds(files) {
2298
2645
  const ids = /* @__PURE__ */ new Set();
2299
2646
  for (const file of files) {
2300
- const text = await (0, import_promises13.readFile)(file, "utf-8");
2647
+ const text = await (0, import_promises14.readFile)(file, "utf-8");
2301
2648
  extractAllIds(text).forEach((id) => ids.add(id));
2302
2649
  }
2303
2650
  return ids;
@@ -2318,7 +2665,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
2318
2665
  }
2319
2666
  const pattern = buildIdPattern2(Array.from(upstreamIds));
2320
2667
  for (const file of targetFiles) {
2321
- const text = await (0, import_promises13.readFile)(file, "utf-8");
2668
+ const text = await (0, import_promises14.readFile)(file, "utf-8");
2322
2669
  if (pattern.test(text)) {
2323
2670
  return true;
2324
2671
  }
@@ -2335,9 +2682,22 @@ function formatIdLine(label, values) {
2335
2682
  }
2336
2683
  return `- ${label}: ${values.join(", ")}`;
2337
2684
  }
2338
- function toSortedArray(values) {
2685
+ function formatList(values) {
2686
+ if (values.length === 0) {
2687
+ return "(none)";
2688
+ }
2689
+ return values.join(", ");
2690
+ }
2691
+ function toSortedArray2(values) {
2339
2692
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2340
2693
  }
2694
+ function mapToSortedRecord(values) {
2695
+ const record2 = {};
2696
+ for (const [key, files] of values.entries()) {
2697
+ record2[key] = Array.from(files).sort((a, b) => a.localeCompare(b));
2698
+ }
2699
+ return record2;
2700
+ }
2341
2701
  function buildHotspots(issues) {
2342
2702
  const map = /* @__PURE__ */ new Map();
2343
2703
  for (const issue7 of issues) {
@@ -2361,7 +2721,6 @@ function buildHotspots(issues) {
2361
2721
  }
2362
2722
  // Annotate the CommonJS export names for ESM import in node:
2363
2723
  0 && (module.exports = {
2364
- VALIDATION_SCHEMA_VERSION,
2365
2724
  createReportData,
2366
2725
  defaultConfig,
2367
2726
  extractAllIds,