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
@@ -177,8 +177,8 @@ function report(copied, skipped, dryRun, label) {
177
177
  }
178
178
 
179
179
  // src/cli/commands/report.ts
180
- var import_promises15 = require("fs/promises");
181
- var import_node_path14 = __toESM(require("path"), 1);
180
+ var import_promises16 = require("fs/promises");
181
+ var import_node_path15 = __toESM(require("path"), 1);
182
182
 
183
183
  // src/core/config.ts
184
184
  var import_promises2 = require("fs/promises");
@@ -210,6 +210,10 @@ var defaultConfig = {
210
210
  traceability: {
211
211
  brMustHaveSc: true,
212
212
  scMustTouchContracts: true,
213
+ scMustHaveTest: true,
214
+ testFileGlobs: [],
215
+ testFileExcludeGlobs: [],
216
+ scNoTestSeverity: "error",
213
217
  allowOrphanContracts: false,
214
218
  unknownContractIdSeverity: "error"
215
219
  }
@@ -389,6 +393,34 @@ function normalizeValidation(raw, configPath, issues) {
389
393
  configPath,
390
394
  issues
391
395
  ),
396
+ scMustHaveTest: readBoolean(
397
+ traceabilityRaw?.scMustHaveTest,
398
+ base.traceability.scMustHaveTest,
399
+ "validation.traceability.scMustHaveTest",
400
+ configPath,
401
+ issues
402
+ ),
403
+ testFileGlobs: readStringArray(
404
+ traceabilityRaw?.testFileGlobs,
405
+ base.traceability.testFileGlobs,
406
+ "validation.traceability.testFileGlobs",
407
+ configPath,
408
+ issues
409
+ ),
410
+ testFileExcludeGlobs: readStringArray(
411
+ traceabilityRaw?.testFileExcludeGlobs,
412
+ base.traceability.testFileExcludeGlobs,
413
+ "validation.traceability.testFileExcludeGlobs",
414
+ configPath,
415
+ issues
416
+ ),
417
+ scNoTestSeverity: readTraceabilitySeverity(
418
+ traceabilityRaw?.scNoTestSeverity,
419
+ base.traceability.scNoTestSeverity,
420
+ "validation.traceability.scNoTestSeverity",
421
+ configPath,
422
+ issues
423
+ ),
392
424
  allowOrphanContracts: readBoolean(
393
425
  traceabilityRaw?.allowOrphanContracts,
394
426
  base.traceability.allowOrphanContracts,
@@ -514,8 +546,8 @@ function isRecord(value) {
514
546
  }
515
547
 
516
548
  // src/core/report.ts
517
- var import_promises14 = require("fs/promises");
518
- var import_node_path13 = __toESM(require("path"), 1);
549
+ var import_promises15 = require("fs/promises");
550
+ var import_node_path14 = __toESM(require("path"), 1);
519
551
 
520
552
  // src/core/discovery.ts
521
553
  var import_promises5 = require("fs/promises");
@@ -523,6 +555,7 @@ var import_promises5 = require("fs/promises");
523
555
  // src/core/fs.ts
524
556
  var import_promises3 = require("fs/promises");
525
557
  var import_node_path5 = __toESM(require("path"), 1);
558
+ var import_fast_glob = __toESM(require("fast-glob"), 1);
526
559
  var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
527
560
  "node_modules",
528
561
  ".git",
@@ -544,6 +577,18 @@ async function collectFiles(root, options = {}) {
544
577
  await walk(root, root, ignoreDirs, extensions, entries);
545
578
  return entries;
546
579
  }
580
+ async function collectFilesByGlobs(root, options) {
581
+ if (options.globs.length === 0) {
582
+ return [];
583
+ }
584
+ return (0, import_fast_glob.default)(options.globs, {
585
+ cwd: root,
586
+ ignore: options.ignore ?? [],
587
+ onlyFiles: true,
588
+ absolute: true,
589
+ unique: true
590
+ });
591
+ }
547
592
  async function walk(base, current, ignoreDirs, extensions, out) {
548
593
  const items = await (0, import_promises3.readdir)(current, { withFileTypes: true });
549
594
  for (const item of items) {
@@ -708,20 +753,331 @@ function isValidId(value, prefix) {
708
753
  return strict.test(value);
709
754
  }
710
755
 
711
- // src/core/types.ts
712
- var VALIDATION_SCHEMA_VERSION = "0.2";
713
-
714
- // src/core/version.ts
756
+ // src/core/traceability.ts
715
757
  var import_promises6 = require("fs/promises");
716
758
  var import_node_path7 = __toESM(require("path"), 1);
759
+
760
+ // src/core/gherkin/parse.ts
761
+ var import_gherkin = require("@cucumber/gherkin");
762
+ var import_node_crypto = require("crypto");
763
+ function parseGherkin(source, uri) {
764
+ const errors = [];
765
+ const uuidFn = () => (0, import_node_crypto.randomUUID)();
766
+ const builder = new import_gherkin.AstBuilder(uuidFn);
767
+ const matcher = new import_gherkin.GherkinClassicTokenMatcher();
768
+ const parser = new import_gherkin.Parser(builder, matcher);
769
+ try {
770
+ const gherkinDocument = parser.parse(source);
771
+ gherkinDocument.uri = uri;
772
+ return { gherkinDocument, errors };
773
+ } catch (error2) {
774
+ errors.push(formatError2(error2));
775
+ return { gherkinDocument: null, errors };
776
+ }
777
+ }
778
+ function formatError2(error2) {
779
+ if (error2 instanceof Error) {
780
+ return error2.message;
781
+ }
782
+ return String(error2);
783
+ }
784
+
785
+ // src/core/scenarioModel.ts
786
+ var SPEC_TAG_RE = /^SPEC-\d{4}$/;
787
+ var SC_TAG_RE = /^SC-\d{4}$/;
788
+ var BR_TAG_RE = /^BR-\d{4}$/;
789
+ var UI_TAG_RE = /^UI-\d{4}$/;
790
+ var API_TAG_RE = /^API-\d{4}$/;
791
+ var DATA_TAG_RE = /^DATA-\d{4}$/;
792
+ function parseScenarioDocument(text, uri) {
793
+ const { gherkinDocument, errors } = parseGherkin(text, uri);
794
+ if (!gherkinDocument) {
795
+ return { document: null, errors };
796
+ }
797
+ const feature = gherkinDocument.feature;
798
+ if (!feature) {
799
+ return {
800
+ document: { uri, featureTags: [], scenarios: [] },
801
+ errors
802
+ };
803
+ }
804
+ const featureTags = collectTagNames(feature.tags);
805
+ const scenarios = collectScenarioNodes(feature, featureTags);
806
+ return {
807
+ document: {
808
+ uri,
809
+ featureName: feature.name,
810
+ featureTags,
811
+ scenarios
812
+ },
813
+ errors
814
+ };
815
+ }
816
+ function buildScenarioAtoms(document) {
817
+ return document.scenarios.map((scenario) => {
818
+ const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
819
+ const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
820
+ const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
821
+ const contractIds = /* @__PURE__ */ new Set();
822
+ scenario.tags.forEach((tag) => {
823
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
824
+ contractIds.add(tag);
825
+ }
826
+ });
827
+ for (const step of scenario.steps) {
828
+ for (const text of collectStepTexts(step)) {
829
+ extractIds(text, "UI").forEach((id) => contractIds.add(id));
830
+ extractIds(text, "API").forEach((id) => contractIds.add(id));
831
+ extractIds(text, "DATA").forEach((id) => contractIds.add(id));
832
+ }
833
+ }
834
+ const atom = {
835
+ uri: document.uri,
836
+ featureName: document.featureName ?? "",
837
+ scenarioName: scenario.name,
838
+ kind: scenario.kind,
839
+ brIds,
840
+ contractIds: Array.from(contractIds).sort()
841
+ };
842
+ if (scenario.line !== void 0) {
843
+ atom.line = scenario.line;
844
+ }
845
+ if (specIds.length === 1) {
846
+ const specId = specIds[0];
847
+ if (specId) {
848
+ atom.specId = specId;
849
+ }
850
+ }
851
+ if (scIds.length === 1) {
852
+ const scId = scIds[0];
853
+ if (scId) {
854
+ atom.scId = scId;
855
+ }
856
+ }
857
+ return atom;
858
+ });
859
+ }
860
+ function collectScenarioNodes(feature, featureTags) {
861
+ const scenarios = [];
862
+ for (const child of feature.children) {
863
+ if (child.scenario) {
864
+ scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
865
+ }
866
+ if (child.rule) {
867
+ const ruleTags = collectTagNames(child.rule.tags);
868
+ for (const ruleChild of child.rule.children) {
869
+ if (ruleChild.scenario) {
870
+ scenarios.push(
871
+ buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
872
+ );
873
+ }
874
+ }
875
+ }
876
+ }
877
+ return scenarios;
878
+ }
879
+ function buildScenarioNode(scenario, featureTags, ruleTags) {
880
+ const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
881
+ const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
882
+ return {
883
+ name: scenario.name,
884
+ kind,
885
+ line: scenario.location?.line,
886
+ tags,
887
+ steps: scenario.steps
888
+ };
889
+ }
890
+ function collectTagNames(tags) {
891
+ return tags.map((tag) => tag.name.replace(/^@/, ""));
892
+ }
893
+ function collectStepTexts(step) {
894
+ const texts = [];
895
+ if (step.text) {
896
+ texts.push(step.text);
897
+ }
898
+ if (step.docString?.content) {
899
+ texts.push(step.docString.content);
900
+ }
901
+ if (step.dataTable?.rows) {
902
+ for (const row of step.dataTable.rows) {
903
+ for (const cell of row.cells) {
904
+ texts.push(cell.value);
905
+ }
906
+ }
907
+ }
908
+ return texts;
909
+ }
910
+ function unique2(values) {
911
+ return Array.from(new Set(values));
912
+ }
913
+
914
+ // src/core/traceability.ts
915
+ var SC_TAG_RE2 = /^SC-\d{4}$/;
916
+ var SC_TEST_ANNOTATION_RE = /\bQFAI:SC-(\d{4})\b/g;
917
+ var DEFAULT_TEST_FILE_EXCLUDE_GLOBS = [
918
+ "**/node_modules/**",
919
+ "**/.git/**",
920
+ "**/.qfai/**",
921
+ "**/dist/**",
922
+ "**/build/**",
923
+ "**/coverage/**",
924
+ "**/.next/**",
925
+ "**/out/**"
926
+ ];
927
+ function extractAnnotatedScIds(text) {
928
+ const ids = /* @__PURE__ */ new Set();
929
+ for (const match of text.matchAll(SC_TEST_ANNOTATION_RE)) {
930
+ const suffix = match[1];
931
+ if (suffix) {
932
+ ids.add(`SC-${suffix}`);
933
+ }
934
+ }
935
+ return Array.from(ids);
936
+ }
937
+ async function collectScIdsFromScenarioFiles(scenarioFiles) {
938
+ const scIds = /* @__PURE__ */ new Set();
939
+ for (const file of scenarioFiles) {
940
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
941
+ const { document, errors } = parseScenarioDocument(text, file);
942
+ if (!document || errors.length > 0) {
943
+ continue;
944
+ }
945
+ for (const scenario of document.scenarios) {
946
+ for (const tag of scenario.tags) {
947
+ if (SC_TAG_RE2.test(tag)) {
948
+ scIds.add(tag);
949
+ }
950
+ }
951
+ }
952
+ }
953
+ return scIds;
954
+ }
955
+ async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
956
+ const sources = /* @__PURE__ */ new Map();
957
+ for (const file of scenarioFiles) {
958
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
959
+ const { document, errors } = parseScenarioDocument(text, file);
960
+ if (!document || errors.length > 0) {
961
+ continue;
962
+ }
963
+ for (const scenario of document.scenarios) {
964
+ for (const tag of scenario.tags) {
965
+ if (!SC_TAG_RE2.test(tag)) {
966
+ continue;
967
+ }
968
+ const current = sources.get(tag) ?? /* @__PURE__ */ new Set();
969
+ current.add(file);
970
+ sources.set(tag, current);
971
+ }
972
+ }
973
+ }
974
+ return sources;
975
+ }
976
+ async function collectScTestReferences(root, globs, excludeGlobs) {
977
+ const refs = /* @__PURE__ */ new Map();
978
+ const normalizedGlobs = normalizeGlobs(globs);
979
+ const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
980
+ const mergedExcludeGlobs = Array.from(
981
+ /* @__PURE__ */ new Set([...DEFAULT_TEST_FILE_EXCLUDE_GLOBS, ...normalizedExcludeGlobs])
982
+ );
983
+ if (normalizedGlobs.length === 0) {
984
+ return {
985
+ refs,
986
+ scan: {
987
+ globs: normalizedGlobs,
988
+ excludeGlobs: mergedExcludeGlobs,
989
+ matchedFileCount: 0
990
+ }
991
+ };
992
+ }
993
+ let files = [];
994
+ try {
995
+ files = await collectFilesByGlobs(root, {
996
+ globs: normalizedGlobs,
997
+ ignore: mergedExcludeGlobs
998
+ });
999
+ } catch (error2) {
1000
+ return {
1001
+ refs,
1002
+ scan: {
1003
+ globs: normalizedGlobs,
1004
+ excludeGlobs: mergedExcludeGlobs,
1005
+ matchedFileCount: 0
1006
+ },
1007
+ error: formatError3(error2)
1008
+ };
1009
+ }
1010
+ const normalizedFiles = Array.from(
1011
+ new Set(files.map((file) => import_node_path7.default.normalize(file)))
1012
+ );
1013
+ for (const file of normalizedFiles) {
1014
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
1015
+ const scIds = extractAnnotatedScIds(text);
1016
+ if (scIds.length === 0) {
1017
+ continue;
1018
+ }
1019
+ for (const scId of scIds) {
1020
+ const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
1021
+ current.add(file);
1022
+ refs.set(scId, current);
1023
+ }
1024
+ }
1025
+ return {
1026
+ refs,
1027
+ scan: {
1028
+ globs: normalizedGlobs,
1029
+ excludeGlobs: mergedExcludeGlobs,
1030
+ matchedFileCount: normalizedFiles.length
1031
+ }
1032
+ };
1033
+ }
1034
+ function buildScCoverage(scIds, refs) {
1035
+ const sortedScIds = toSortedArray(scIds);
1036
+ const refsRecord = {};
1037
+ const missingIds = [];
1038
+ let covered = 0;
1039
+ for (const scId of sortedScIds) {
1040
+ const files = refs.get(scId);
1041
+ const sortedFiles = files ? toSortedArray(files) : [];
1042
+ refsRecord[scId] = sortedFiles;
1043
+ if (sortedFiles.length === 0) {
1044
+ missingIds.push(scId);
1045
+ } else {
1046
+ covered += 1;
1047
+ }
1048
+ }
1049
+ return {
1050
+ total: sortedScIds.length,
1051
+ covered,
1052
+ missing: missingIds.length,
1053
+ missingIds,
1054
+ refs: refsRecord
1055
+ };
1056
+ }
1057
+ function toSortedArray(values) {
1058
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
1059
+ }
1060
+ function normalizeGlobs(globs) {
1061
+ return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1062
+ }
1063
+ function formatError3(error2) {
1064
+ if (error2 instanceof Error) {
1065
+ return error2.message;
1066
+ }
1067
+ return String(error2);
1068
+ }
1069
+
1070
+ // src/core/version.ts
1071
+ var import_promises7 = require("fs/promises");
1072
+ var import_node_path8 = __toESM(require("path"), 1);
717
1073
  var import_node_url2 = require("url");
718
1074
  async function resolveToolVersion() {
719
- if ("0.3.5".length > 0) {
720
- return "0.3.5";
1075
+ if ("0.4.2".length > 0) {
1076
+ return "0.4.2";
721
1077
  }
722
1078
  try {
723
1079
  const packagePath = resolvePackageJsonPath();
724
- const raw = await (0, import_promises6.readFile)(packagePath, "utf-8");
1080
+ const raw = await (0, import_promises7.readFile)(packagePath, "utf-8");
725
1081
  const parsed = JSON.parse(raw);
726
1082
  const version = typeof parsed.version === "string" ? parsed.version : "";
727
1083
  return version.length > 0 ? version : "unknown";
@@ -732,18 +1088,18 @@ async function resolveToolVersion() {
732
1088
  function resolvePackageJsonPath() {
733
1089
  const base = __filename;
734
1090
  const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
735
- return import_node_path7.default.resolve(import_node_path7.default.dirname(basePath), "../../package.json");
1091
+ return import_node_path8.default.resolve(import_node_path8.default.dirname(basePath), "../../package.json");
736
1092
  }
737
1093
 
738
1094
  // src/core/validators/contracts.ts
739
- var import_promises7 = require("fs/promises");
740
- var import_node_path9 = __toESM(require("path"), 1);
1095
+ var import_promises8 = require("fs/promises");
1096
+ var import_node_path10 = __toESM(require("path"), 1);
741
1097
 
742
1098
  // src/core/contracts.ts
743
- var import_node_path8 = __toESM(require("path"), 1);
1099
+ var import_node_path9 = __toESM(require("path"), 1);
744
1100
  var import_yaml2 = require("yaml");
745
1101
  function parseStructuredContract(file, text) {
746
- const ext = import_node_path8.default.extname(file).toLowerCase();
1102
+ const ext = import_node_path9.default.extname(file).toLowerCase();
747
1103
  if (ext === ".json") {
748
1104
  return JSON.parse(text);
749
1105
  }
@@ -794,9 +1150,9 @@ var SQL_DANGEROUS_PATTERNS = [
794
1150
  async function validateContracts(root, config) {
795
1151
  const issues = [];
796
1152
  const contractsRoot = resolvePath(root, config, "contractsDir");
797
- issues.push(...await validateUiContracts(import_node_path9.default.join(contractsRoot, "ui")));
798
- issues.push(...await validateApiContracts(import_node_path9.default.join(contractsRoot, "api")));
799
- issues.push(...await validateDataContracts(import_node_path9.default.join(contractsRoot, "db")));
1153
+ issues.push(...await validateUiContracts(import_node_path10.default.join(contractsRoot, "ui")));
1154
+ issues.push(...await validateApiContracts(import_node_path10.default.join(contractsRoot, "api")));
1155
+ issues.push(...await validateDataContracts(import_node_path10.default.join(contractsRoot, "db")));
800
1156
  return issues;
801
1157
  }
802
1158
  async function validateUiContracts(uiRoot) {
@@ -814,7 +1170,7 @@ async function validateUiContracts(uiRoot) {
814
1170
  }
815
1171
  const issues = [];
816
1172
  for (const file of files) {
817
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1173
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
818
1174
  const invalidIds = extractInvalidIds(text, [
819
1175
  "SPEC",
820
1176
  "BR",
@@ -843,7 +1199,7 @@ async function validateUiContracts(uiRoot) {
843
1199
  issues.push(
844
1200
  issue(
845
1201
  "QFAI-CONTRACT-001",
846
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error2)})`,
1202
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
847
1203
  "error",
848
1204
  file,
849
1205
  "contracts.ui.parse"
@@ -881,7 +1237,7 @@ async function validateApiContracts(apiRoot) {
881
1237
  }
882
1238
  const issues = [];
883
1239
  for (const file of files) {
884
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1240
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
885
1241
  const invalidIds = extractInvalidIds(text, [
886
1242
  "SPEC",
887
1243
  "BR",
@@ -910,7 +1266,7 @@ async function validateApiContracts(apiRoot) {
910
1266
  issues.push(
911
1267
  issue(
912
1268
  "QFAI-CONTRACT-001",
913
- `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error2)})`,
1269
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
914
1270
  "error",
915
1271
  file,
916
1272
  "contracts.api.parse"
@@ -959,7 +1315,7 @@ async function validateDataContracts(dataRoot) {
959
1315
  }
960
1316
  const issues = [];
961
1317
  for (const file of files) {
962
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1318
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
963
1319
  const invalidIds = extractInvalidIds(text, [
964
1320
  "SPEC",
965
1321
  "BR",
@@ -1005,7 +1361,7 @@ function lintSql(text, file) {
1005
1361
  function hasOpenApi(doc) {
1006
1362
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
1007
1363
  }
1008
- function formatError2(error2) {
1364
+ function formatError4(error2) {
1009
1365
  if (error2 instanceof Error) {
1010
1366
  return error2.message;
1011
1367
  }
@@ -1030,8 +1386,8 @@ function issue(code, message, severity, file, rule, refs) {
1030
1386
  }
1031
1387
 
1032
1388
  // src/core/validators/delta.ts
1033
- var import_promises8 = require("fs/promises");
1034
- var import_node_path10 = __toESM(require("path"), 1);
1389
+ var import_promises9 = require("fs/promises");
1390
+ var import_node_path11 = __toESM(require("path"), 1);
1035
1391
  var SECTION_RE = /^##\s+変更区分/m;
1036
1392
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1037
1393
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1045,10 +1401,10 @@ async function validateDeltas(root, config) {
1045
1401
  }
1046
1402
  const issues = [];
1047
1403
  for (const pack of packs) {
1048
- const deltaPath = import_node_path10.default.join(pack, "delta.md");
1404
+ const deltaPath = import_node_path11.default.join(pack, "delta.md");
1049
1405
  let text;
1050
1406
  try {
1051
- text = await (0, import_promises8.readFile)(deltaPath, "utf-8");
1407
+ text = await (0, import_promises9.readFile)(deltaPath, "utf-8");
1052
1408
  } catch (error2) {
1053
1409
  if (isMissingFileError2(error2)) {
1054
1410
  issues.push(
@@ -1120,17 +1476,17 @@ function issue2(code, message, severity, file, rule, refs) {
1120
1476
  }
1121
1477
 
1122
1478
  // src/core/validators/ids.ts
1123
- var import_promises10 = require("fs/promises");
1124
- var import_node_path12 = __toESM(require("path"), 1);
1479
+ var import_promises11 = require("fs/promises");
1480
+ var import_node_path13 = __toESM(require("path"), 1);
1125
1481
 
1126
1482
  // src/core/contractIndex.ts
1127
- var import_promises9 = require("fs/promises");
1128
- var import_node_path11 = __toESM(require("path"), 1);
1483
+ var import_promises10 = require("fs/promises");
1484
+ var import_node_path12 = __toESM(require("path"), 1);
1129
1485
  async function buildContractIndex(root, config) {
1130
1486
  const contractsRoot = resolvePath(root, config, "contractsDir");
1131
- const uiRoot = import_node_path11.default.join(contractsRoot, "ui");
1132
- const apiRoot = import_node_path11.default.join(contractsRoot, "api");
1133
- const dataRoot = import_node_path11.default.join(contractsRoot, "db");
1487
+ const uiRoot = import_node_path12.default.join(contractsRoot, "ui");
1488
+ const apiRoot = import_node_path12.default.join(contractsRoot, "api");
1489
+ const dataRoot = import_node_path12.default.join(contractsRoot, "db");
1134
1490
  const [uiFiles, apiFiles, dataFiles] = await Promise.all([
1135
1491
  collectUiContractFiles(uiRoot),
1136
1492
  collectApiContractFiles(apiRoot),
@@ -1149,7 +1505,7 @@ async function buildContractIndex(root, config) {
1149
1505
  }
1150
1506
  async function indexUiContracts(files, index) {
1151
1507
  for (const file of files) {
1152
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1508
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1153
1509
  try {
1154
1510
  const doc = parseStructuredContract(file, text);
1155
1511
  extractUiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1161,7 +1517,7 @@ async function indexUiContracts(files, index) {
1161
1517
  }
1162
1518
  async function indexApiContracts(files, index) {
1163
1519
  for (const file of files) {
1164
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1520
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1165
1521
  try {
1166
1522
  const doc = parseStructuredContract(file, text);
1167
1523
  extractApiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1173,7 +1529,7 @@ async function indexApiContracts(files, index) {
1173
1529
  }
1174
1530
  async function indexDataContracts(files, index) {
1175
1531
  for (const file of files) {
1176
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1532
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1177
1533
  extractIds(text, "DATA").forEach((id) => record(index, id, file));
1178
1534
  }
1179
1535
  }
@@ -1221,243 +1577,89 @@ function extractH2Sections(md) {
1221
1577
  endLine,
1222
1578
  body
1223
1579
  });
1224
- }
1225
- return sections;
1226
- }
1227
-
1228
- // src/core/parse/spec.ts
1229
- var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1230
- var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1231
- var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1232
- var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1233
- var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1234
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1235
- function parseSpec(md, file) {
1236
- const headings = parseHeadings(md);
1237
- const h1 = headings.find((heading) => heading.level === 1);
1238
- const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1239
- const sections = extractH2Sections(md);
1240
- const sectionNames = new Set(Array.from(sections.keys()));
1241
- const brSection = sections.get(BR_SECTION_TITLE);
1242
- const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1243
- const startLine = brSection?.startLine ?? 1;
1244
- const brs = [];
1245
- const brsWithoutPriority = [];
1246
- const brsWithInvalidPriority = [];
1247
- for (let i = 0; i < brLines.length; i++) {
1248
- const lineText = brLines[i] ?? "";
1249
- const lineNumber = startLine + i;
1250
- const validMatch = lineText.match(BR_LINE_RE);
1251
- if (validMatch) {
1252
- const id = validMatch[1];
1253
- const priority = validMatch[2];
1254
- const text = validMatch[3];
1255
- if (!id || !priority || !text) continue;
1256
- brs.push({
1257
- id,
1258
- priority,
1259
- text: text.trim(),
1260
- line: lineNumber
1261
- });
1262
- continue;
1263
- }
1264
- const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1265
- if (anyPriorityMatch) {
1266
- const id = anyPriorityMatch[1];
1267
- const priority = anyPriorityMatch[2];
1268
- const text = anyPriorityMatch[3];
1269
- if (!id || !priority || !text) continue;
1270
- if (!VALID_PRIORITIES.has(priority)) {
1271
- brsWithInvalidPriority.push({
1272
- id,
1273
- priority,
1274
- text: text.trim(),
1275
- line: lineNumber
1276
- });
1277
- }
1278
- continue;
1279
- }
1280
- const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1281
- if (noPriorityMatch) {
1282
- const id = noPriorityMatch[1];
1283
- const text = noPriorityMatch[2];
1284
- if (!id || !text) continue;
1285
- brsWithoutPriority.push({
1286
- id,
1287
- text: text.trim(),
1288
- line: lineNumber
1289
- });
1290
- }
1291
- }
1292
- const parsed = {
1293
- file,
1294
- sections: sectionNames,
1295
- brs,
1296
- brsWithoutPriority,
1297
- brsWithInvalidPriority
1298
- };
1299
- if (specId) {
1300
- parsed.specId = specId;
1301
- }
1302
- return parsed;
1303
- }
1304
-
1305
- // src/core/gherkin/parse.ts
1306
- var import_gherkin = require("@cucumber/gherkin");
1307
- var import_node_crypto = require("crypto");
1308
- function parseGherkin(source, uri) {
1309
- const errors = [];
1310
- const uuidFn = () => (0, import_node_crypto.randomUUID)();
1311
- const builder = new import_gherkin.AstBuilder(uuidFn);
1312
- const matcher = new import_gherkin.GherkinClassicTokenMatcher();
1313
- const parser = new import_gherkin.Parser(builder, matcher);
1314
- try {
1315
- const gherkinDocument = parser.parse(source);
1316
- gherkinDocument.uri = uri;
1317
- return { gherkinDocument, errors };
1318
- } catch (error2) {
1319
- errors.push(formatError3(error2));
1320
- return { gherkinDocument: null, errors };
1321
- }
1322
- }
1323
- function formatError3(error2) {
1324
- if (error2 instanceof Error) {
1325
- return error2.message;
1326
- }
1327
- return String(error2);
1328
- }
1329
-
1330
- // src/core/scenarioModel.ts
1331
- var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1332
- var SC_TAG_RE = /^SC-\d{4}$/;
1333
- var BR_TAG_RE = /^BR-\d{4}$/;
1334
- var UI_TAG_RE = /^UI-\d{4}$/;
1335
- var API_TAG_RE = /^API-\d{4}$/;
1336
- var DATA_TAG_RE = /^DATA-\d{4}$/;
1337
- function parseScenarioDocument(text, uri) {
1338
- const { gherkinDocument, errors } = parseGherkin(text, uri);
1339
- if (!gherkinDocument) {
1340
- return { document: null, errors };
1341
- }
1342
- const feature = gherkinDocument.feature;
1343
- if (!feature) {
1344
- return {
1345
- document: { uri, featureTags: [], scenarios: [] },
1346
- errors
1347
- };
1348
- }
1349
- const featureTags = collectTagNames(feature.tags);
1350
- const scenarios = collectScenarioNodes(feature, featureTags);
1351
- return {
1352
- document: {
1353
- uri,
1354
- featureName: feature.name,
1355
- featureTags,
1356
- scenarios
1357
- },
1358
- errors
1359
- };
1360
- }
1361
- function buildScenarioAtoms(document) {
1362
- return document.scenarios.map((scenario) => {
1363
- const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
1364
- const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
1365
- const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1366
- const contractIds = /* @__PURE__ */ new Set();
1367
- scenario.tags.forEach((tag) => {
1368
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1369
- contractIds.add(tag);
1370
- }
1371
- });
1372
- for (const step of scenario.steps) {
1373
- for (const text of collectStepTexts(step)) {
1374
- extractIds(text, "UI").forEach((id) => contractIds.add(id));
1375
- extractIds(text, "API").forEach((id) => contractIds.add(id));
1376
- extractIds(text, "DATA").forEach((id) => contractIds.add(id));
1377
- }
1378
- }
1379
- const atom = {
1380
- uri: document.uri,
1381
- featureName: document.featureName ?? "",
1382
- scenarioName: scenario.name,
1383
- kind: scenario.kind,
1384
- brIds,
1385
- contractIds: Array.from(contractIds).sort()
1386
- };
1387
- if (scenario.line !== void 0) {
1388
- atom.line = scenario.line;
1389
- }
1390
- if (specIds.length === 1) {
1391
- const specId = specIds[0];
1392
- if (specId) {
1393
- atom.specId = specId;
1394
- }
1395
- }
1396
- if (scIds.length === 1) {
1397
- const scId = scIds[0];
1398
- if (scId) {
1399
- atom.scId = scId;
1400
- }
1401
- }
1402
- return atom;
1403
- });
1580
+ }
1581
+ return sections;
1404
1582
  }
1405
- function collectScenarioNodes(feature, featureTags) {
1406
- const scenarios = [];
1407
- for (const child of feature.children) {
1408
- if (child.scenario) {
1409
- scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
1583
+
1584
+ // src/core/parse/spec.ts
1585
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1586
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1587
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1588
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1589
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1590
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1591
+ function parseSpec(md, file) {
1592
+ const headings = parseHeadings(md);
1593
+ const h1 = headings.find((heading) => heading.level === 1);
1594
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1595
+ const sections = extractH2Sections(md);
1596
+ const sectionNames = new Set(Array.from(sections.keys()));
1597
+ const brSection = sections.get(BR_SECTION_TITLE);
1598
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1599
+ const startLine = brSection?.startLine ?? 1;
1600
+ const brs = [];
1601
+ const brsWithoutPriority = [];
1602
+ const brsWithInvalidPriority = [];
1603
+ for (let i = 0; i < brLines.length; i++) {
1604
+ const lineText = brLines[i] ?? "";
1605
+ const lineNumber = startLine + i;
1606
+ const validMatch = lineText.match(BR_LINE_RE);
1607
+ if (validMatch) {
1608
+ const id = validMatch[1];
1609
+ const priority = validMatch[2];
1610
+ const text = validMatch[3];
1611
+ if (!id || !priority || !text) continue;
1612
+ brs.push({
1613
+ id,
1614
+ priority,
1615
+ text: text.trim(),
1616
+ line: lineNumber
1617
+ });
1618
+ continue;
1410
1619
  }
1411
- if (child.rule) {
1412
- const ruleTags = collectTagNames(child.rule.tags);
1413
- for (const ruleChild of child.rule.children) {
1414
- if (ruleChild.scenario) {
1415
- scenarios.push(
1416
- buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
1417
- );
1418
- }
1620
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1621
+ if (anyPriorityMatch) {
1622
+ const id = anyPriorityMatch[1];
1623
+ const priority = anyPriorityMatch[2];
1624
+ const text = anyPriorityMatch[3];
1625
+ if (!id || !priority || !text) continue;
1626
+ if (!VALID_PRIORITIES.has(priority)) {
1627
+ brsWithInvalidPriority.push({
1628
+ id,
1629
+ priority,
1630
+ text: text.trim(),
1631
+ line: lineNumber
1632
+ });
1419
1633
  }
1634
+ continue;
1635
+ }
1636
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1637
+ if (noPriorityMatch) {
1638
+ const id = noPriorityMatch[1];
1639
+ const text = noPriorityMatch[2];
1640
+ if (!id || !text) continue;
1641
+ brsWithoutPriority.push({
1642
+ id,
1643
+ text: text.trim(),
1644
+ line: lineNumber
1645
+ });
1420
1646
  }
1421
1647
  }
1422
- return scenarios;
1423
- }
1424
- function buildScenarioNode(scenario, featureTags, ruleTags) {
1425
- const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
1426
- const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
1427
- return {
1428
- name: scenario.name,
1429
- kind,
1430
- line: scenario.location?.line,
1431
- tags,
1432
- steps: scenario.steps
1648
+ const parsed = {
1649
+ file,
1650
+ sections: sectionNames,
1651
+ brs,
1652
+ brsWithoutPriority,
1653
+ brsWithInvalidPriority
1433
1654
  };
1434
- }
1435
- function collectTagNames(tags) {
1436
- return tags.map((tag) => tag.name.replace(/^@/, ""));
1437
- }
1438
- function collectStepTexts(step) {
1439
- const texts = [];
1440
- if (step.text) {
1441
- texts.push(step.text);
1442
- }
1443
- if (step.docString?.content) {
1444
- texts.push(step.docString.content);
1445
- }
1446
- if (step.dataTable?.rows) {
1447
- for (const row of step.dataTable.rows) {
1448
- for (const cell of row.cells) {
1449
- texts.push(cell.value);
1450
- }
1451
- }
1655
+ if (specId) {
1656
+ parsed.specId = specId;
1452
1657
  }
1453
- return texts;
1454
- }
1455
- function unique2(values) {
1456
- return Array.from(new Set(values));
1658
+ return parsed;
1457
1659
  }
1458
1660
 
1459
1661
  // src/core/validators/ids.ts
1460
- var SC_TAG_RE2 = /^SC-\d{4}$/;
1662
+ var SC_TAG_RE3 = /^SC-\d{4}$/;
1461
1663
  async function validateDefinedIds(root, config) {
1462
1664
  const issues = [];
1463
1665
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1491,7 +1693,7 @@ async function validateDefinedIds(root, config) {
1491
1693
  }
1492
1694
  async function collectSpecDefinitionIds(files, out) {
1493
1695
  for (const file of files) {
1494
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1696
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1495
1697
  const parsed = parseSpec(text, file);
1496
1698
  if (parsed.specId) {
1497
1699
  recordId(out, parsed.specId, file);
@@ -1501,14 +1703,14 @@ async function collectSpecDefinitionIds(files, out) {
1501
1703
  }
1502
1704
  async function collectScenarioDefinitionIds(files, out) {
1503
1705
  for (const file of files) {
1504
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1706
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1505
1707
  const { document, errors } = parseScenarioDocument(text, file);
1506
1708
  if (!document || errors.length > 0) {
1507
1709
  continue;
1508
1710
  }
1509
1711
  for (const scenario of document.scenarios) {
1510
1712
  for (const tag of scenario.tags) {
1511
- if (SC_TAG_RE2.test(tag)) {
1713
+ if (SC_TAG_RE3.test(tag)) {
1512
1714
  recordId(out, tag, file);
1513
1715
  }
1514
1716
  }
@@ -1522,7 +1724,7 @@ function recordId(out, id, file) {
1522
1724
  }
1523
1725
  function formatFileList(files, root) {
1524
1726
  return files.map((file) => {
1525
- const relative = import_node_path12.default.relative(root, file);
1727
+ const relative = import_node_path13.default.relative(root, file);
1526
1728
  return relative.length > 0 ? relative : file;
1527
1729
  }).join(", ");
1528
1730
  }
@@ -1545,13 +1747,12 @@ function issue3(code, message, severity, file, rule, refs) {
1545
1747
  }
1546
1748
 
1547
1749
  // src/core/validators/scenario.ts
1548
- var import_promises11 = require("fs/promises");
1750
+ var import_promises12 = require("fs/promises");
1549
1751
  var GIVEN_PATTERN = /\bGiven\b/;
1550
1752
  var WHEN_PATTERN = /\bWhen\b/;
1551
1753
  var THEN_PATTERN = /\bThen\b/;
1552
- var SC_TAG_RE3 = /^SC-\d{4}$/;
1754
+ var SC_TAG_RE4 = /^SC-\d{4}$/;
1553
1755
  var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1554
- var BR_TAG_RE2 = /^BR-\d{4}$/;
1555
1756
  async function validateScenarios(root, config) {
1556
1757
  const specsRoot = resolvePath(root, config, "specsDir");
1557
1758
  const entries = await collectSpecEntries(specsRoot);
@@ -1572,7 +1773,7 @@ async function validateScenarios(root, config) {
1572
1773
  for (const entry of entries) {
1573
1774
  let text;
1574
1775
  try {
1575
- text = await (0, import_promises11.readFile)(entry.scenarioPath, "utf-8");
1776
+ text = await (0, import_promises12.readFile)(entry.scenarioPath, "utf-8");
1576
1777
  } catch (error2) {
1577
1778
  if (isMissingFileError3(error2)) {
1578
1779
  issues.push(
@@ -1631,17 +1832,7 @@ function validateScenarioContent(text, file) {
1631
1832
  const featureSpecTags = document.featureTags.filter(
1632
1833
  (tag) => SPEC_TAG_RE2.test(tag)
1633
1834
  );
1634
- if (featureSpecTags.length === 0) {
1635
- issues.push(
1636
- issue4(
1637
- "QFAI-SC-009",
1638
- "Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1639
- "error",
1640
- file,
1641
- "scenario.featureSpec"
1642
- )
1643
- );
1644
- } else if (featureSpecTags.length > 1) {
1835
+ if (featureSpecTags.length > 1) {
1645
1836
  issues.push(
1646
1837
  issue4(
1647
1838
  "QFAI-SC-009",
@@ -1683,18 +1874,12 @@ function validateScenarioContent(text, file) {
1683
1874
  continue;
1684
1875
  }
1685
1876
  const missingTags = [];
1686
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
1877
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
1687
1878
  if (scTags.length === 0) {
1688
1879
  missingTags.push("SC(0\u4EF6)");
1689
1880
  } else if (scTags.length > 1) {
1690
1881
  missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1691
1882
  }
1692
- if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
1693
- missingTags.push("SPEC");
1694
- }
1695
- if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
1696
- missingTags.push("BR");
1697
- }
1698
1883
  if (missingTags.length > 0) {
1699
1884
  issues.push(
1700
1885
  issue4(
@@ -1758,7 +1943,7 @@ function isMissingFileError3(error2) {
1758
1943
  }
1759
1944
 
1760
1945
  // src/core/validators/spec.ts
1761
- var import_promises12 = require("fs/promises");
1946
+ var import_promises13 = require("fs/promises");
1762
1947
  async function validateSpecs(root, config) {
1763
1948
  const specsRoot = resolvePath(root, config, "specsDir");
1764
1949
  const entries = await collectSpecEntries(specsRoot);
@@ -1779,7 +1964,7 @@ async function validateSpecs(root, config) {
1779
1964
  for (const entry of entries) {
1780
1965
  let text;
1781
1966
  try {
1782
- text = await (0, import_promises12.readFile)(entry.specPath, "utf-8");
1967
+ text = await (0, import_promises13.readFile)(entry.specPath, "utf-8");
1783
1968
  } catch (error2) {
1784
1969
  if (isMissingFileError4(error2)) {
1785
1970
  issues.push(
@@ -1928,10 +2113,9 @@ function isMissingFileError4(error2) {
1928
2113
  }
1929
2114
 
1930
2115
  // src/core/validators/traceability.ts
1931
- var import_promises13 = require("fs/promises");
1932
- var SC_TAG_RE4 = /^SC-\d{4}$/;
2116
+ var import_promises14 = require("fs/promises");
1933
2117
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
1934
- var BR_TAG_RE3 = /^BR-\d{4}$/;
2118
+ var BR_TAG_RE2 = /^BR-\d{4}$/;
1935
2119
  async function validateTraceability(root, config) {
1936
2120
  const issues = [];
1937
2121
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1950,7 +2134,7 @@ async function validateTraceability(root, config) {
1950
2134
  const contractIndex = await buildContractIndex(root, config);
1951
2135
  const contractIds = contractIndex.ids;
1952
2136
  for (const file of specFiles) {
1953
- const text = await (0, import_promises13.readFile)(file, "utf-8");
2137
+ const text = await (0, import_promises14.readFile)(file, "utf-8");
1954
2138
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1955
2139
  const parsed = parseSpec(text, file);
1956
2140
  if (parsed.specId) {
@@ -1958,28 +2142,6 @@ async function validateTraceability(root, config) {
1958
2142
  }
1959
2143
  const brIds = parsed.brs.map((br) => br.id);
1960
2144
  brIds.forEach((id) => brIdsInSpecs.add(id));
1961
- const referencedContractIds = /* @__PURE__ */ new Set([
1962
- ...extractIds(text, "UI"),
1963
- ...extractIds(text, "API"),
1964
- ...extractIds(text, "DATA")
1965
- ]);
1966
- const unknownContractIds = Array.from(referencedContractIds).filter(
1967
- (id) => !contractIds.has(id)
1968
- );
1969
- if (unknownContractIds.length > 0) {
1970
- issues.push(
1971
- issue6(
1972
- "QFAI-TRACE-009",
1973
- `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1974
- ", "
1975
- )}`,
1976
- "error",
1977
- file,
1978
- "traceability.specContractExists",
1979
- unknownContractIds
1980
- )
1981
- );
1982
- }
1983
2145
  if (parsed.specId) {
1984
2146
  const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
1985
2147
  brIds.forEach((id) => current.add(id));
@@ -1987,23 +2149,49 @@ async function validateTraceability(root, config) {
1987
2149
  }
1988
2150
  }
1989
2151
  for (const file of scenarioFiles) {
1990
- const text = await (0, import_promises13.readFile)(file, "utf-8");
2152
+ const text = await (0, import_promises14.readFile)(file, "utf-8");
1991
2153
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1992
2154
  const { document, errors } = parseScenarioDocument(text, file);
1993
2155
  if (!document || errors.length > 0) {
1994
2156
  continue;
1995
2157
  }
1996
2158
  const atoms = buildScenarioAtoms(document);
2159
+ const scIdsInFile = /* @__PURE__ */ new Set();
1997
2160
  for (const [index, scenario] of document.scenarios.entries()) {
1998
2161
  const atom = atoms[index];
1999
2162
  if (!atom) {
2000
2163
  continue;
2001
2164
  }
2002
2165
  const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
2003
- const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
2004
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
2166
+ const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
2167
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
2168
+ if (specTags.length === 0) {
2169
+ issues.push(
2170
+ issue6(
2171
+ "QFAI-TRACE-014",
2172
+ `Scenario \u304C SPEC \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
2173
+ "error",
2174
+ file,
2175
+ "traceability.scenarioSpecRequired"
2176
+ )
2177
+ );
2178
+ }
2179
+ if (brTags.length === 0) {
2180
+ issues.push(
2181
+ issue6(
2182
+ "QFAI-TRACE-015",
2183
+ `Scenario \u304C BR \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
2184
+ "error",
2185
+ file,
2186
+ "traceability.scenarioBrRequired"
2187
+ )
2188
+ );
2189
+ }
2005
2190
  brTags.forEach((id) => brIdsInScenarios.add(id));
2006
- scTags.forEach((id) => scIdsInScenarios.add(id));
2191
+ scTags.forEach((id) => {
2192
+ scIdsInScenarios.add(id);
2193
+ scIdsInFile.add(id);
2194
+ });
2007
2195
  atom.contractIds.forEach((id) => scenarioContractIds.add(id));
2008
2196
  if (atom.contractIds.length > 0) {
2009
2197
  scTags.forEach((id) => scWithContracts.add(id));
@@ -2081,6 +2269,22 @@ async function validateTraceability(root, config) {
2081
2269
  }
2082
2270
  }
2083
2271
  }
2272
+ if (scIdsInFile.size !== 1) {
2273
+ const invalidScIds = Array.from(scIdsInFile).sort(
2274
+ (a, b) => a.localeCompare(b)
2275
+ );
2276
+ 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(", ")}`;
2277
+ issues.push(
2278
+ issue6(
2279
+ "QFAI-TRACE-012",
2280
+ `Spec entry \u304C Spec:SC=1:1 \u3092\u6E80\u305F\u3057\u3066\u3044\u307E\u305B\u3093: ${detail}`,
2281
+ "error",
2282
+ file,
2283
+ "traceability.specScOneToOne",
2284
+ invalidScIds
2285
+ )
2286
+ );
2287
+ }
2084
2288
  }
2085
2289
  if (upstreamIds.size === 0) {
2086
2290
  return [
@@ -2129,6 +2333,66 @@ async function validateTraceability(root, config) {
2129
2333
  );
2130
2334
  }
2131
2335
  }
2336
+ const scRefsResult = await collectScTestReferences(
2337
+ root,
2338
+ config.validation.traceability.testFileGlobs,
2339
+ config.validation.traceability.testFileExcludeGlobs
2340
+ );
2341
+ const scTestRefs = scRefsResult.refs;
2342
+ const testFileScan = scRefsResult.scan;
2343
+ const hasScenarios = scIdsInScenarios.size > 0;
2344
+ const hasGlobConfig = testFileScan.globs.length > 0;
2345
+ const hasMatchedTests = testFileScan.matchedFileCount > 0;
2346
+ if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
2347
+ const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
2348
+ issues.push(
2349
+ issue6(
2350
+ "QFAI-TRACE-013",
2351
+ `\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}`,
2352
+ "error",
2353
+ testsRoot,
2354
+ "traceability.testFileGlobs"
2355
+ )
2356
+ );
2357
+ } else {
2358
+ if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2359
+ const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2360
+ const refs = scTestRefs.get(id);
2361
+ return !refs || refs.size === 0;
2362
+ });
2363
+ if (scWithoutTests.length > 0) {
2364
+ issues.push(
2365
+ issue6(
2366
+ "QFAI-TRACE-010",
2367
+ `SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(
2368
+ ", "
2369
+ )}\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`,
2370
+ config.validation.traceability.scNoTestSeverity,
2371
+ testsRoot,
2372
+ "traceability.scMustHaveTest",
2373
+ scWithoutTests
2374
+ )
2375
+ );
2376
+ }
2377
+ }
2378
+ const unknownScIds = Array.from(scTestRefs.keys()).filter(
2379
+ (id) => !scIdsInScenarios.has(id)
2380
+ );
2381
+ if (unknownScIds.length > 0) {
2382
+ issues.push(
2383
+ issue6(
2384
+ "QFAI-TRACE-011",
2385
+ `\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(
2386
+ ", "
2387
+ )}`,
2388
+ "error",
2389
+ testsRoot,
2390
+ "traceability.scUnknownInTests",
2391
+ unknownScIds
2392
+ )
2393
+ );
2394
+ }
2395
+ }
2132
2396
  if (!config.validation.traceability.allowOrphanContracts) {
2133
2397
  if (contractIds.size > 0) {
2134
2398
  const orphanContracts = Array.from(contractIds).filter(
@@ -2177,7 +2441,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2177
2441
  const pattern = buildIdPattern(Array.from(upstreamIds));
2178
2442
  let found = false;
2179
2443
  for (const file of targetFiles) {
2180
- const text = await (0, import_promises13.readFile)(file, "utf-8");
2444
+ const text = await (0, import_promises14.readFile)(file, "utf-8");
2181
2445
  if (pattern.test(text)) {
2182
2446
  found = true;
2183
2447
  break;
@@ -2187,8 +2451,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2187
2451
  issues.push(
2188
2452
  issue6(
2189
2453
  "QFAI-TRACE-002",
2190
- "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
2191
- "warning",
2454
+ "\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",
2455
+ "info",
2192
2456
  srcRoot,
2193
2457
  "traceability.codeReferences"
2194
2458
  )
@@ -2231,12 +2495,24 @@ async function validateProject(root, configResult) {
2231
2495
  ...await validateDefinedIds(root, config),
2232
2496
  ...await validateTraceability(root, config)
2233
2497
  ];
2498
+ const specsRoot = resolvePath(root, config, "specsDir");
2499
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
2500
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2501
+ const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
2502
+ root,
2503
+ config.validation.traceability.testFileGlobs,
2504
+ config.validation.traceability.testFileExcludeGlobs
2505
+ );
2506
+ const scCoverage = buildScCoverage(scIds, scTestRefs);
2234
2507
  const toolVersion = await resolveToolVersion();
2235
2508
  return {
2236
- schemaVersion: VALIDATION_SCHEMA_VERSION,
2237
2509
  toolVersion,
2238
2510
  issues,
2239
- counts: countIssues(issues)
2511
+ counts: countIssues(issues),
2512
+ traceability: {
2513
+ sc: scCoverage,
2514
+ testFiles
2515
+ }
2240
2516
  };
2241
2517
  }
2242
2518
  function countIssues(issues) {
@@ -2257,9 +2533,9 @@ async function createReportData(root, validation, configResult) {
2257
2533
  const configPath = resolved.configPath;
2258
2534
  const specsRoot = resolvePath(root, config, "specsDir");
2259
2535
  const contractsRoot = resolvePath(root, config, "contractsDir");
2260
- const apiRoot = import_node_path13.default.join(contractsRoot, "api");
2261
- const uiRoot = import_node_path13.default.join(contractsRoot, "ui");
2262
- const dbRoot = import_node_path13.default.join(contractsRoot, "db");
2536
+ const apiRoot = import_node_path14.default.join(contractsRoot, "api");
2537
+ const uiRoot = import_node_path14.default.join(contractsRoot, "ui");
2538
+ const dbRoot = import_node_path14.default.join(contractsRoot, "db");
2263
2539
  const srcRoot = resolvePath(root, config, "srcDir");
2264
2540
  const testsRoot = resolvePath(root, config, "testsDir");
2265
2541
  const specFiles = await collectSpecFiles(specsRoot);
@@ -2285,6 +2561,16 @@ async function createReportData(root, validation, configResult) {
2285
2561
  srcRoot,
2286
2562
  testsRoot
2287
2563
  );
2564
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2565
+ const scRefsResult = await collectScTestReferences(
2566
+ root,
2567
+ config.validation.traceability.testFileGlobs,
2568
+ config.validation.traceability.testFileExcludeGlobs
2569
+ );
2570
+ const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
2571
+ const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
2572
+ const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
2573
+ const scSourceRecord = mapToSortedRecord(scSources);
2288
2574
  const resolvedValidation = validation ?? await validateProject(root, resolved);
2289
2575
  const version = await resolveToolVersion();
2290
2576
  return {
@@ -2313,7 +2599,10 @@ async function createReportData(root, validation, configResult) {
2313
2599
  },
2314
2600
  traceability: {
2315
2601
  upstreamIdsFound: upstreamIds.size,
2316
- referencedInCodeOrTests: traceability
2602
+ referencedInCodeOrTests: traceability,
2603
+ sc: scCoverage,
2604
+ scSources: scSourceRecord,
2605
+ testFiles
2317
2606
  },
2318
2607
  issues: resolvedValidation.issues
2319
2608
  };
@@ -2350,6 +2639,65 @@ function formatReportMarkdown(data) {
2350
2639
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2351
2640
  );
2352
2641
  lines.push("");
2642
+ lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2643
+ lines.push(`- total: ${data.traceability.sc.total}`);
2644
+ lines.push(`- covered: ${data.traceability.sc.covered}`);
2645
+ lines.push(`- missing: ${data.traceability.sc.missing}`);
2646
+ lines.push(
2647
+ `- testFileGlobs: ${formatList(data.traceability.testFiles.globs)}`
2648
+ );
2649
+ lines.push(
2650
+ `- testFileExcludeGlobs: ${formatList(
2651
+ data.traceability.testFiles.excludeGlobs
2652
+ )}`
2653
+ );
2654
+ lines.push(
2655
+ `- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
2656
+ );
2657
+ if (data.traceability.sc.missingIds.length === 0) {
2658
+ lines.push("- missingIds: (none)");
2659
+ } else {
2660
+ const sources = data.traceability.scSources;
2661
+ const missingWithSources = data.traceability.sc.missingIds.map((id) => {
2662
+ const files = sources[id] ?? [];
2663
+ if (files.length === 0) {
2664
+ return id;
2665
+ }
2666
+ return `${id} (${files.join(", ")})`;
2667
+ });
2668
+ lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
2669
+ }
2670
+ lines.push("");
2671
+ lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
2672
+ const scRefs = data.traceability.sc.refs;
2673
+ const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
2674
+ if (scIds.length === 0) {
2675
+ lines.push("- (none)");
2676
+ } else {
2677
+ for (const scId of scIds) {
2678
+ const refs = scRefs[scId] ?? [];
2679
+ if (refs.length === 0) {
2680
+ lines.push(`- ${scId}: (none)`);
2681
+ } else {
2682
+ lines.push(`- ${scId}: ${refs.join(", ")}`);
2683
+ }
2684
+ }
2685
+ }
2686
+ lines.push("");
2687
+ lines.push("## Spec:SC=1:1 \u9055\u53CD");
2688
+ const specScIssues = data.issues.filter(
2689
+ (item) => item.code === "QFAI-TRACE-012"
2690
+ );
2691
+ if (specScIssues.length === 0) {
2692
+ lines.push("- (none)");
2693
+ } else {
2694
+ for (const item of specScIssues) {
2695
+ const location = item.file ?? "(unknown)";
2696
+ const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
2697
+ lines.push(`- ${location}: ${refs}`);
2698
+ }
2699
+ }
2700
+ lines.push("");
2353
2701
  lines.push("## Hotspots");
2354
2702
  const hotspots = buildHotspots(data.issues);
2355
2703
  if (hotspots.length === 0) {
@@ -2404,25 +2752,25 @@ async function collectIds(files) {
2404
2752
  DATA: /* @__PURE__ */ new Set()
2405
2753
  };
2406
2754
  for (const file of files) {
2407
- const text = await (0, import_promises14.readFile)(file, "utf-8");
2755
+ const text = await (0, import_promises15.readFile)(file, "utf-8");
2408
2756
  for (const prefix of ID_PREFIXES2) {
2409
2757
  const ids = extractIds(text, prefix);
2410
2758
  ids.forEach((id) => result[prefix].add(id));
2411
2759
  }
2412
2760
  }
2413
2761
  return {
2414
- SPEC: toSortedArray(result.SPEC),
2415
- BR: toSortedArray(result.BR),
2416
- SC: toSortedArray(result.SC),
2417
- UI: toSortedArray(result.UI),
2418
- API: toSortedArray(result.API),
2419
- DATA: toSortedArray(result.DATA)
2762
+ SPEC: toSortedArray2(result.SPEC),
2763
+ BR: toSortedArray2(result.BR),
2764
+ SC: toSortedArray2(result.SC),
2765
+ UI: toSortedArray2(result.UI),
2766
+ API: toSortedArray2(result.API),
2767
+ DATA: toSortedArray2(result.DATA)
2420
2768
  };
2421
2769
  }
2422
2770
  async function collectUpstreamIds(files) {
2423
2771
  const ids = /* @__PURE__ */ new Set();
2424
2772
  for (const file of files) {
2425
- const text = await (0, import_promises14.readFile)(file, "utf-8");
2773
+ const text = await (0, import_promises15.readFile)(file, "utf-8");
2426
2774
  extractAllIds(text).forEach((id) => ids.add(id));
2427
2775
  }
2428
2776
  return ids;
@@ -2443,7 +2791,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
2443
2791
  }
2444
2792
  const pattern = buildIdPattern2(Array.from(upstreamIds));
2445
2793
  for (const file of targetFiles) {
2446
- const text = await (0, import_promises14.readFile)(file, "utf-8");
2794
+ const text = await (0, import_promises15.readFile)(file, "utf-8");
2447
2795
  if (pattern.test(text)) {
2448
2796
  return true;
2449
2797
  }
@@ -2460,9 +2808,22 @@ function formatIdLine(label, values) {
2460
2808
  }
2461
2809
  return `- ${label}: ${values.join(", ")}`;
2462
2810
  }
2463
- function toSortedArray(values) {
2811
+ function formatList(values) {
2812
+ if (values.length === 0) {
2813
+ return "(none)";
2814
+ }
2815
+ return values.join(", ");
2816
+ }
2817
+ function toSortedArray2(values) {
2464
2818
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2465
2819
  }
2820
+ function mapToSortedRecord(values) {
2821
+ const record2 = {};
2822
+ for (const [key, files] of values.entries()) {
2823
+ record2[key] = Array.from(files).sort((a, b) => a.localeCompare(b));
2824
+ }
2825
+ return record2;
2826
+ }
2466
2827
  function buildHotspots(issues) {
2467
2828
  const map = /* @__PURE__ */ new Map();
2468
2829
  for (const issue7 of issues) {
@@ -2487,10 +2848,10 @@ function buildHotspots(issues) {
2487
2848
 
2488
2849
  // src/cli/commands/report.ts
2489
2850
  async function runReport(options) {
2490
- const root = import_node_path14.default.resolve(options.root);
2851
+ const root = import_node_path15.default.resolve(options.root);
2491
2852
  const configResult = await loadConfig(root);
2492
2853
  const input = configResult.config.output.validateJsonPath;
2493
- const inputPath = import_node_path14.default.isAbsolute(input) ? input : import_node_path14.default.resolve(root, input);
2854
+ const inputPath = import_node_path15.default.isAbsolute(input) ? input : import_node_path15.default.resolve(root, input);
2494
2855
  let validation;
2495
2856
  try {
2496
2857
  validation = await readValidationResult(inputPath);
@@ -2515,11 +2876,11 @@ async function runReport(options) {
2515
2876
  const data = await createReportData(root, validation, configResult);
2516
2877
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
2517
2878
  const outRoot = resolvePath(root, configResult.config, "outDir");
2518
- const defaultOut = options.format === "json" ? import_node_path14.default.join(outRoot, "report.json") : import_node_path14.default.join(outRoot, "report.md");
2879
+ const defaultOut = options.format === "json" ? import_node_path15.default.join(outRoot, "report.json") : import_node_path15.default.join(outRoot, "report.md");
2519
2880
  const out = options.outPath ?? defaultOut;
2520
- const outPath = import_node_path14.default.isAbsolute(out) ? out : import_node_path14.default.resolve(root, out);
2521
- await (0, import_promises15.mkdir)(import_node_path14.default.dirname(outPath), { recursive: true });
2522
- await (0, import_promises15.writeFile)(outPath, `${output}
2881
+ const outPath = import_node_path15.default.isAbsolute(out) ? out : import_node_path15.default.resolve(root, out);
2882
+ await (0, import_promises16.mkdir)(import_node_path15.default.dirname(outPath), { recursive: true });
2883
+ await (0, import_promises16.writeFile)(outPath, `${output}
2523
2884
  `, "utf-8");
2524
2885
  info(
2525
2886
  `report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
@@ -2527,16 +2888,11 @@ async function runReport(options) {
2527
2888
  info(`wrote report: ${outPath}`);
2528
2889
  }
2529
2890
  async function readValidationResult(inputPath) {
2530
- const raw = await (0, import_promises15.readFile)(inputPath, "utf-8");
2891
+ const raw = await (0, import_promises16.readFile)(inputPath, "utf-8");
2531
2892
  const parsed = JSON.parse(raw);
2532
2893
  if (!isValidationResult(parsed)) {
2533
2894
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
2534
2895
  }
2535
- if (parsed.schemaVersion !== VALIDATION_SCHEMA_VERSION) {
2536
- throw new Error(
2537
- `validate.json \u306E schemaVersion \u304C\u4E0D\u4E00\u81F4\u3067\u3059: expected ${VALIDATION_SCHEMA_VERSION}, actual ${parsed.schemaVersion}`
2538
- );
2539
- }
2540
2896
  return parsed;
2541
2897
  }
2542
2898
  function isValidationResult(value) {
@@ -2544,9 +2900,6 @@ function isValidationResult(value) {
2544
2900
  return false;
2545
2901
  }
2546
2902
  const record2 = value;
2547
- if (typeof record2.schemaVersion !== "string") {
2548
- return false;
2549
- }
2550
2903
  if (typeof record2.toolVersion !== "string") {
2551
2904
  return false;
2552
2905
  }
@@ -2568,8 +2921,8 @@ function isMissingFileError5(error2) {
2568
2921
  }
2569
2922
 
2570
2923
  // src/cli/commands/validate.ts
2571
- var import_promises16 = require("fs/promises");
2572
- var import_node_path15 = __toESM(require("path"), 1);
2924
+ var import_promises17 = require("fs/promises");
2925
+ var import_node_path16 = __toESM(require("path"), 1);
2573
2926
 
2574
2927
  // src/cli/lib/failOn.ts
2575
2928
  function shouldFail(result, failOn) {
@@ -2584,7 +2937,7 @@ function shouldFail(result, failOn) {
2584
2937
 
2585
2938
  // src/cli/commands/validate.ts
2586
2939
  async function runValidate(options) {
2587
- const root = import_node_path15.default.resolve(options.root);
2940
+ const root = import_node_path16.default.resolve(options.root);
2588
2941
  const configResult = await loadConfig(root);
2589
2942
  const result = await validateProject(root, configResult);
2590
2943
  const format = options.format ?? "text";
@@ -2633,9 +2986,9 @@ function emitGitHub(issue7) {
2633
2986
  );
2634
2987
  }
2635
2988
  async function emitJson(result, root, jsonPath) {
2636
- const abs = import_node_path15.default.isAbsolute(jsonPath) ? jsonPath : import_node_path15.default.resolve(root, jsonPath);
2637
- await (0, import_promises16.mkdir)(import_node_path15.default.dirname(abs), { recursive: true });
2638
- await (0, import_promises16.writeFile)(abs, `${JSON.stringify(result, null, 2)}
2989
+ const abs = import_node_path16.default.isAbsolute(jsonPath) ? jsonPath : import_node_path16.default.resolve(root, jsonPath);
2990
+ await (0, import_promises17.mkdir)(import_node_path16.default.dirname(abs), { recursive: true });
2991
+ await (0, import_promises17.writeFile)(abs, `${JSON.stringify(result, null, 2)}
2639
2992
  `, "utf-8");
2640
2993
  }
2641
2994