qfai 0.4.0 → 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 (161) hide show
  1. package/README.md +6 -1
  2. package/assets/init/.qfai/README.md +2 -1
  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/root/qfai.config.yaml +6 -0
  6. package/assets/init/root/tests/qfai-traceability.sample.test.ts +2 -2
  7. package/dist/cli/index.cjs +337 -117
  8. package/dist/cli/index.cjs.map +1 -1
  9. package/dist/cli/index.d.ts +0 -2
  10. package/dist/cli/index.mjs +337 -117
  11. package/dist/cli/index.mjs.map +1 -1
  12. package/dist/index.cjs +327 -107
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +23 -9
  15. package/dist/index.d.ts +156 -2
  16. package/dist/index.mjs +327 -107
  17. package/dist/index.mjs.map +1 -1
  18. package/package.json +2 -1
  19. package/dist/cli/commands/init.d.ts +0 -8
  20. package/dist/cli/commands/init.d.ts.map +0 -1
  21. package/dist/cli/commands/init.js +0 -30
  22. package/dist/cli/commands/init.js.map +0 -1
  23. package/dist/cli/commands/report.d.ts +0 -7
  24. package/dist/cli/commands/report.d.ts.map +0 -1
  25. package/dist/cli/commands/report.js +0 -80
  26. package/dist/cli/commands/report.js.map +0 -1
  27. package/dist/cli/commands/validate.d.ts +0 -9
  28. package/dist/cli/commands/validate.d.ts.map +0 -1
  29. package/dist/cli/commands/validate.js +0 -57
  30. package/dist/cli/commands/validate.js.map +0 -1
  31. package/dist/cli/index.d.ts.map +0 -1
  32. package/dist/cli/index.js +0 -7
  33. package/dist/cli/index.js.map +0 -1
  34. package/dist/cli/lib/args.d.ts +0 -18
  35. package/dist/cli/lib/args.d.ts.map +0 -1
  36. package/dist/cli/lib/args.js +0 -98
  37. package/dist/cli/lib/args.js.map +0 -1
  38. package/dist/cli/lib/assets.d.ts +0 -2
  39. package/dist/cli/lib/assets.d.ts.map +0 -1
  40. package/dist/cli/lib/assets.js +0 -24
  41. package/dist/cli/lib/assets.js.map +0 -1
  42. package/dist/cli/lib/failOn.d.ts +0 -5
  43. package/dist/cli/lib/failOn.d.ts.map +0 -1
  44. package/dist/cli/lib/failOn.js +0 -10
  45. package/dist/cli/lib/failOn.js.map +0 -1
  46. package/dist/cli/lib/fs.d.ts +0 -11
  47. package/dist/cli/lib/fs.d.ts.map +0 -1
  48. package/dist/cli/lib/fs.js +0 -91
  49. package/dist/cli/lib/fs.js.map +0 -1
  50. package/dist/cli/lib/logger.d.ts +0 -4
  51. package/dist/cli/lib/logger.d.ts.map +0 -1
  52. package/dist/cli/lib/logger.js +0 -10
  53. package/dist/cli/lib/logger.js.map +0 -1
  54. package/dist/cli/main.d.ts +0 -2
  55. package/dist/cli/main.d.ts.map +0 -1
  56. package/dist/cli/main.js +0 -66
  57. package/dist/cli/main.js.map +0 -1
  58. package/dist/core/config.d.ts +0 -46
  59. package/dist/core/config.d.ts.map +0 -1
  60. package/dist/core/config.js +0 -222
  61. package/dist/core/config.js.map +0 -1
  62. package/dist/core/contractIndex.d.ts +0 -13
  63. package/dist/core/contractIndex.d.ts.map +0 -1
  64. package/dist/core/contractIndex.js +0 -66
  65. package/dist/core/contractIndex.js.map +0 -1
  66. package/dist/core/contracts.d.ts +0 -5
  67. package/dist/core/contracts.d.ts.map +0 -1
  68. package/dist/core/contracts.js +0 -42
  69. package/dist/core/contracts.js.map +0 -1
  70. package/dist/core/discovery.d.ts +0 -14
  71. package/dist/core/discovery.d.ts.map +0 -1
  72. package/dist/core/discovery.js +0 -55
  73. package/dist/core/discovery.js.map +0 -1
  74. package/dist/core/fs.d.ts +0 -6
  75. package/dist/core/fs.d.ts.map +0 -1
  76. package/dist/core/fs.js +0 -55
  77. package/dist/core/fs.js.map +0 -1
  78. package/dist/core/gherkin/parse.d.ts +0 -7
  79. package/dist/core/gherkin/parse.d.ts.map +0 -1
  80. package/dist/core/gherkin/parse.js +0 -25
  81. package/dist/core/gherkin/parse.js.map +0 -1
  82. package/dist/core/ids.d.ts +0 -6
  83. package/dist/core/ids.d.ts.map +0 -1
  84. package/dist/core/ids.js +0 -52
  85. package/dist/core/ids.js.map +0 -1
  86. package/dist/core/index.d.ts +0 -13
  87. package/dist/core/index.d.ts.map +0 -1
  88. package/dist/core/index.js +0 -13
  89. package/dist/core/index.js.map +0 -1
  90. package/dist/core/parse/adr.d.ts +0 -13
  91. package/dist/core/parse/adr.d.ts.map +0 -1
  92. package/dist/core/parse/adr.js +0 -33
  93. package/dist/core/parse/adr.js.map +0 -1
  94. package/dist/core/parse/gherkin.d.ts +0 -12
  95. package/dist/core/parse/gherkin.d.ts.map +0 -1
  96. package/dist/core/parse/gherkin.js +0 -22
  97. package/dist/core/parse/gherkin.js.map +0 -1
  98. package/dist/core/parse/markdown.d.ts +0 -14
  99. package/dist/core/parse/markdown.d.ts.map +0 -1
  100. package/dist/core/parse/markdown.js +0 -45
  101. package/dist/core/parse/markdown.js.map +0 -1
  102. package/dist/core/parse/spec.d.ts +0 -28
  103. package/dist/core/parse/spec.d.ts.map +0 -1
  104. package/dist/core/parse/spec.js +0 -80
  105. package/dist/core/parse/spec.js.map +0 -1
  106. package/dist/core/report.d.ts +0 -41
  107. package/dist/core/report.d.ts.map +0 -1
  108. package/dist/core/report.js +0 -260
  109. package/dist/core/report.js.map +0 -1
  110. package/dist/core/scenarioModel.d.ts +0 -33
  111. package/dist/core/scenarioModel.d.ts.map +0 -1
  112. package/dist/core/scenarioModel.js +0 -130
  113. package/dist/core/scenarioModel.js.map +0 -1
  114. package/dist/core/specLayout.d.ts +0 -8
  115. package/dist/core/specLayout.d.ts.map +0 -1
  116. package/dist/core/specLayout.js +0 -36
  117. package/dist/core/specLayout.js.map +0 -1
  118. package/dist/core/traceability.d.ts +0 -12
  119. package/dist/core/traceability.d.ts.map +0 -1
  120. package/dist/core/traceability.js +0 -70
  121. package/dist/core/traceability.js.map +0 -1
  122. package/dist/core/types.d.ts +0 -25
  123. package/dist/core/types.d.ts.map +0 -1
  124. package/dist/core/types.js +0 -2
  125. package/dist/core/types.js.map +0 -1
  126. package/dist/core/validate.d.ts +0 -4
  127. package/dist/core/validate.d.ts.map +0 -1
  128. package/dist/core/validate.js +0 -34
  129. package/dist/core/validate.js.map +0 -1
  130. package/dist/core/validators/contracts.d.ts +0 -5
  131. package/dist/core/validators/contracts.d.ts.map +0 -1
  132. package/dist/core/validators/contracts.js +0 -162
  133. package/dist/core/validators/contracts.js.map +0 -1
  134. package/dist/core/validators/delta.d.ts +0 -4
  135. package/dist/core/validators/delta.d.ts.map +0 -1
  136. package/dist/core/validators/delta.js +0 -68
  137. package/dist/core/validators/delta.js.map +0 -1
  138. package/dist/core/validators/ids.d.ts +0 -4
  139. package/dist/core/validators/ids.d.ts.map +0 -1
  140. package/dist/core/validators/ids.js +0 -88
  141. package/dist/core/validators/ids.js.map +0 -1
  142. package/dist/core/validators/scenario.d.ts +0 -5
  143. package/dist/core/validators/scenario.d.ts.map +0 -1
  144. package/dist/core/validators/scenario.js +0 -140
  145. package/dist/core/validators/scenario.js.map +0 -1
  146. package/dist/core/validators/spec.d.ts +0 -5
  147. package/dist/core/validators/spec.d.ts.map +0 -1
  148. package/dist/core/validators/spec.js +0 -94
  149. package/dist/core/validators/spec.js.map +0 -1
  150. package/dist/core/validators/traceability.d.ts +0 -4
  151. package/dist/core/validators/traceability.d.ts.map +0 -1
  152. package/dist/core/validators/traceability.js +0 -190
  153. package/dist/core/validators/traceability.js.map +0 -1
  154. package/dist/core/version.d.ts +0 -2
  155. package/dist/core/version.d.ts.map +0 -1
  156. package/dist/core/version.js +0 -25
  157. package/dist/core/version.js.map +0 -1
  158. package/dist/index.d.ts.map +0 -1
  159. package/dist/index.js +0 -2
  160. package/dist/index.js.map +0 -1
  161. package/dist/tsconfig.tsbuildinfo +0 -1
package/dist/index.cjs CHANGED
@@ -85,6 +85,8 @@ var defaultConfig = {
85
85
  brMustHaveSc: true,
86
86
  scMustTouchContracts: true,
87
87
  scMustHaveTest: true,
88
+ testFileGlobs: [],
89
+ testFileExcludeGlobs: [],
88
90
  scNoTestSeverity: "error",
89
91
  allowOrphanContracts: false,
90
92
  unknownContractIdSeverity: "error"
@@ -272,6 +274,20 @@ function normalizeValidation(raw, configPath, issues) {
272
274
  configPath,
273
275
  issues
274
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
+ ),
275
291
  scNoTestSeverity: readTraceabilitySeverity(
276
292
  traceabilityRaw?.scNoTestSeverity,
277
293
  base.traceability.scNoTestSeverity,
@@ -458,7 +474,7 @@ function isValidId(value, prefix) {
458
474
 
459
475
  // src/core/report.ts
460
476
  var import_promises14 = require("fs/promises");
461
- var import_node_path10 = __toESM(require("path"), 1);
477
+ var import_node_path11 = __toESM(require("path"), 1);
462
478
 
463
479
  // src/core/discovery.ts
464
480
  var import_promises4 = require("fs/promises");
@@ -466,6 +482,7 @@ var import_promises4 = require("fs/promises");
466
482
  // src/core/fs.ts
467
483
  var import_promises2 = require("fs/promises");
468
484
  var import_node_path2 = __toESM(require("path"), 1);
485
+ var import_fast_glob = __toESM(require("fast-glob"), 1);
469
486
  var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
470
487
  "node_modules",
471
488
  ".git",
@@ -487,6 +504,18 @@ async function collectFiles(root, options = {}) {
487
504
  await walk(root, root, ignoreDirs, extensions, entries);
488
505
  return entries;
489
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
+ }
490
519
  async function walk(base, current, ignoreDirs, extensions, out) {
491
520
  const items = await (0, import_promises2.readdir)(current, { withFileTypes: true });
492
521
  for (const item of items) {
@@ -600,6 +629,7 @@ async function exists2(target) {
600
629
 
601
630
  // src/core/traceability.ts
602
631
  var import_promises5 = require("fs/promises");
632
+ var import_node_path4 = __toESM(require("path"), 1);
603
633
 
604
634
  // src/core/gherkin/parse.ts
605
635
  var import_gherkin = require("@cucumber/gherkin");
@@ -757,6 +787,27 @@ function unique2(values) {
757
787
 
758
788
  // src/core/traceability.ts
759
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
+ }
760
811
  async function collectScIdsFromScenarioFiles(scenarioFiles) {
761
812
  const scIds = /* @__PURE__ */ new Set();
762
813
  for (const file of scenarioFiles) {
@@ -775,14 +826,67 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
775
826
  }
776
827
  return scIds;
777
828
  }
778
- async function collectScTestReferences(testsRoot) {
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) {
779
851
  const refs = /* @__PURE__ */ new Map();
780
- const testFiles = await collectFiles(testsRoot, {
781
- extensions: [".ts", ".tsx", ".js", ".jsx"]
782
- });
783
- for (const file of testFiles) {
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) {
784
888
  const text = await (0, import_promises5.readFile)(file, "utf-8");
785
- const scIds = extractIds(text, "SC");
889
+ const scIds = extractAnnotatedScIds(text);
786
890
  if (scIds.length === 0) {
787
891
  continue;
788
892
  }
@@ -792,7 +896,14 @@ async function collectScTestReferences(testsRoot) {
792
896
  refs.set(scId, current);
793
897
  }
794
898
  }
795
- return refs;
899
+ return {
900
+ refs,
901
+ scan: {
902
+ globs: normalizedGlobs,
903
+ excludeGlobs: mergedExcludeGlobs,
904
+ matchedFileCount: normalizedFiles.length
905
+ }
906
+ };
796
907
  }
797
908
  function buildScCoverage(scIds, refs) {
798
909
  const sortedScIds = toSortedArray(scIds);
@@ -820,14 +931,23 @@ function buildScCoverage(scIds, refs) {
820
931
  function toSortedArray(values) {
821
932
  return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
822
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
+ }
823
943
 
824
944
  // src/core/version.ts
825
945
  var import_promises6 = require("fs/promises");
826
- var import_node_path4 = __toESM(require("path"), 1);
946
+ var import_node_path5 = __toESM(require("path"), 1);
827
947
  var import_node_url = require("url");
828
948
  async function resolveToolVersion() {
829
- if ("0.4.0".length > 0) {
830
- return "0.4.0";
949
+ if ("0.4.2".length > 0) {
950
+ return "0.4.2";
831
951
  }
832
952
  try {
833
953
  const packagePath = resolvePackageJsonPath();
@@ -842,18 +962,18 @@ async function resolveToolVersion() {
842
962
  function resolvePackageJsonPath() {
843
963
  const base = __filename;
844
964
  const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
845
- 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");
846
966
  }
847
967
 
848
968
  // src/core/validators/contracts.ts
849
969
  var import_promises7 = require("fs/promises");
850
- var import_node_path6 = __toESM(require("path"), 1);
970
+ var import_node_path7 = __toESM(require("path"), 1);
851
971
 
852
972
  // src/core/contracts.ts
853
- var import_node_path5 = __toESM(require("path"), 1);
973
+ var import_node_path6 = __toESM(require("path"), 1);
854
974
  var import_yaml2 = require("yaml");
855
975
  function parseStructuredContract(file, text) {
856
- const ext = import_node_path5.default.extname(file).toLowerCase();
976
+ const ext = import_node_path6.default.extname(file).toLowerCase();
857
977
  if (ext === ".json") {
858
978
  return JSON.parse(text);
859
979
  }
@@ -904,9 +1024,9 @@ var SQL_DANGEROUS_PATTERNS = [
904
1024
  async function validateContracts(root, config) {
905
1025
  const issues = [];
906
1026
  const contractsRoot = resolvePath(root, config, "contractsDir");
907
- issues.push(...await validateUiContracts(import_node_path6.default.join(contractsRoot, "ui")));
908
- issues.push(...await validateApiContracts(import_node_path6.default.join(contractsRoot, "api")));
909
- 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")));
910
1030
  return issues;
911
1031
  }
912
1032
  async function validateUiContracts(uiRoot) {
@@ -953,7 +1073,7 @@ async function validateUiContracts(uiRoot) {
953
1073
  issues.push(
954
1074
  issue(
955
1075
  "QFAI-CONTRACT-001",
956
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error)})`,
1076
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
957
1077
  "error",
958
1078
  file,
959
1079
  "contracts.ui.parse"
@@ -1020,7 +1140,7 @@ async function validateApiContracts(apiRoot) {
1020
1140
  issues.push(
1021
1141
  issue(
1022
1142
  "QFAI-CONTRACT-001",
1023
- `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error)})`,
1143
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
1024
1144
  "error",
1025
1145
  file,
1026
1146
  "contracts.api.parse"
@@ -1115,7 +1235,7 @@ function lintSql(text, file) {
1115
1235
  function hasOpenApi(doc) {
1116
1236
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
1117
1237
  }
1118
- function formatError3(error) {
1238
+ function formatError4(error) {
1119
1239
  if (error instanceof Error) {
1120
1240
  return error.message;
1121
1241
  }
@@ -1141,7 +1261,7 @@ function issue(code, message, severity, file, rule, refs) {
1141
1261
 
1142
1262
  // src/core/validators/delta.ts
1143
1263
  var import_promises8 = require("fs/promises");
1144
- var import_node_path7 = __toESM(require("path"), 1);
1264
+ var import_node_path8 = __toESM(require("path"), 1);
1145
1265
  var SECTION_RE = /^##\s+変更区分/m;
1146
1266
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1147
1267
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1155,7 +1275,7 @@ async function validateDeltas(root, config) {
1155
1275
  }
1156
1276
  const issues = [];
1157
1277
  for (const pack of packs) {
1158
- const deltaPath = import_node_path7.default.join(pack, "delta.md");
1278
+ const deltaPath = import_node_path8.default.join(pack, "delta.md");
1159
1279
  let text;
1160
1280
  try {
1161
1281
  text = await (0, import_promises8.readFile)(deltaPath, "utf-8");
@@ -1231,16 +1351,16 @@ function issue2(code, message, severity, file, rule, refs) {
1231
1351
 
1232
1352
  // src/core/validators/ids.ts
1233
1353
  var import_promises10 = require("fs/promises");
1234
- var import_node_path9 = __toESM(require("path"), 1);
1354
+ var import_node_path10 = __toESM(require("path"), 1);
1235
1355
 
1236
1356
  // src/core/contractIndex.ts
1237
1357
  var import_promises9 = require("fs/promises");
1238
- var import_node_path8 = __toESM(require("path"), 1);
1358
+ var import_node_path9 = __toESM(require("path"), 1);
1239
1359
  async function buildContractIndex(root, config) {
1240
1360
  const contractsRoot = resolvePath(root, config, "contractsDir");
1241
- const uiRoot = import_node_path8.default.join(contractsRoot, "ui");
1242
- const apiRoot = import_node_path8.default.join(contractsRoot, "api");
1243
- 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");
1244
1364
  const [uiFiles, apiFiles, dataFiles] = await Promise.all([
1245
1365
  collectUiContractFiles(uiRoot),
1246
1366
  collectApiContractFiles(apiRoot),
@@ -1478,7 +1598,7 @@ function recordId(out, id, file) {
1478
1598
  }
1479
1599
  function formatFileList(files, root) {
1480
1600
  return files.map((file) => {
1481
- const relative = import_node_path9.default.relative(root, file);
1601
+ const relative = import_node_path10.default.relative(root, file);
1482
1602
  return relative.length > 0 ? relative : file;
1483
1603
  }).join(", ");
1484
1604
  }
@@ -1507,7 +1627,6 @@ var WHEN_PATTERN = /\bWhen\b/;
1507
1627
  var THEN_PATTERN = /\bThen\b/;
1508
1628
  var SC_TAG_RE4 = /^SC-\d{4}$/;
1509
1629
  var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1510
- var BR_TAG_RE2 = /^BR-\d{4}$/;
1511
1630
  async function validateScenarios(root, config) {
1512
1631
  const specsRoot = resolvePath(root, config, "specsDir");
1513
1632
  const entries = await collectSpecEntries(specsRoot);
@@ -1587,17 +1706,7 @@ function validateScenarioContent(text, file) {
1587
1706
  const featureSpecTags = document.featureTags.filter(
1588
1707
  (tag) => SPEC_TAG_RE2.test(tag)
1589
1708
  );
1590
- if (featureSpecTags.length === 0) {
1591
- issues.push(
1592
- issue4(
1593
- "QFAI-SC-009",
1594
- "Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1595
- "error",
1596
- file,
1597
- "scenario.featureSpec"
1598
- )
1599
- );
1600
- } else if (featureSpecTags.length > 1) {
1709
+ if (featureSpecTags.length > 1) {
1601
1710
  issues.push(
1602
1711
  issue4(
1603
1712
  "QFAI-SC-009",
@@ -1625,17 +1734,6 @@ function validateScenarioContent(text, file) {
1625
1734
  )
1626
1735
  );
1627
1736
  }
1628
- if (document.scenarios.length > 1) {
1629
- issues.push(
1630
- issue4(
1631
- "QFAI-SC-011",
1632
- `Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
1633
- "error",
1634
- file,
1635
- "scenario.single"
1636
- )
1637
- );
1638
- }
1639
1737
  for (const scenario of document.scenarios) {
1640
1738
  if (scenario.tags.length === 0) {
1641
1739
  issues.push(
@@ -1656,12 +1754,6 @@ function validateScenarioContent(text, file) {
1656
1754
  } else if (scTags.length > 1) {
1657
1755
  missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1658
1756
  }
1659
- if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
1660
- missingTags.push("SPEC");
1661
- }
1662
- if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
1663
- missingTags.push("BR");
1664
- }
1665
1757
  if (missingTags.length > 0) {
1666
1758
  issues.push(
1667
1759
  issue4(
@@ -1896,9 +1988,8 @@ function isMissingFileError4(error) {
1896
1988
 
1897
1989
  // src/core/validators/traceability.ts
1898
1990
  var import_promises13 = require("fs/promises");
1899
- var SC_TAG_RE5 = /^SC-\d{4}$/;
1900
1991
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
1901
- var BR_TAG_RE3 = /^BR-\d{4}$/;
1992
+ var BR_TAG_RE2 = /^BR-\d{4}$/;
1902
1993
  async function validateTraceability(root, config) {
1903
1994
  const issues = [];
1904
1995
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1925,28 +2016,6 @@ async function validateTraceability(root, config) {
1925
2016
  }
1926
2017
  const brIds = parsed.brs.map((br) => br.id);
1927
2018
  brIds.forEach((id) => brIdsInSpecs.add(id));
1928
- const referencedContractIds = /* @__PURE__ */ new Set([
1929
- ...extractIds(text, "UI"),
1930
- ...extractIds(text, "API"),
1931
- ...extractIds(text, "DATA")
1932
- ]);
1933
- const unknownContractIds = Array.from(referencedContractIds).filter(
1934
- (id) => !contractIds.has(id)
1935
- );
1936
- if (unknownContractIds.length > 0) {
1937
- issues.push(
1938
- issue6(
1939
- "QFAI-TRACE-009",
1940
- `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1941
- ", "
1942
- )}`,
1943
- "error",
1944
- file,
1945
- "traceability.specContractExists",
1946
- unknownContractIds
1947
- )
1948
- );
1949
- }
1950
2019
  if (parsed.specId) {
1951
2020
  const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
1952
2021
  brIds.forEach((id) => current.add(id));
@@ -1961,16 +2030,42 @@ async function validateTraceability(root, config) {
1961
2030
  continue;
1962
2031
  }
1963
2032
  const atoms = buildScenarioAtoms(document);
2033
+ const scIdsInFile = /* @__PURE__ */ new Set();
1964
2034
  for (const [index, scenario] of document.scenarios.entries()) {
1965
2035
  const atom = atoms[index];
1966
2036
  if (!atom) {
1967
2037
  continue;
1968
2038
  }
1969
2039
  const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
1970
- const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
1971
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE5.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
+ }
1972
2064
  brTags.forEach((id) => brIdsInScenarios.add(id));
1973
- scTags.forEach((id) => scIdsInScenarios.add(id));
2065
+ scTags.forEach((id) => {
2066
+ scIdsInScenarios.add(id);
2067
+ scIdsInFile.add(id);
2068
+ });
1974
2069
  atom.contractIds.forEach((id) => scenarioContractIds.add(id));
1975
2070
  if (atom.contractIds.length > 0) {
1976
2071
  scTags.forEach((id) => scWithContracts.add(id));
@@ -2048,6 +2143,22 @@ async function validateTraceability(root, config) {
2048
2143
  }
2049
2144
  }
2050
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
+ }
2051
2162
  }
2052
2163
  if (upstreamIds.size === 0) {
2053
2164
  return [
@@ -2096,21 +2207,62 @@ async function validateTraceability(root, config) {
2096
2207
  );
2097
2208
  }
2098
2209
  }
2099
- if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2100
- const scTestRefs = await collectScTestReferences(testsRoot);
2101
- const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2102
- const refs = scTestRefs.get(id);
2103
- return !refs || refs.size === 0;
2104
- });
2105
- if (scWithoutTests.length > 0) {
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) {
2106
2256
  issues.push(
2107
2257
  issue6(
2108
- "QFAI-TRACE-010",
2109
- `SC \u304C tests \u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(", ")}\u3002tests/ \u914D\u4E0B\u306E\u30C6\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\uFF08.ts/.tsx/.js/.jsx\uFF09\u306B SC ID \u3092\u30B3\u30E1\u30F3\u30C8\u307E\u305F\u306F\u30B3\u30FC\u30C9\u3067\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
2110
- config.validation.traceability.scNoTestSeverity,
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",
2111
2263
  testsRoot,
2112
- "traceability.scMustHaveTest",
2113
- scWithoutTests
2264
+ "traceability.scUnknownInTests",
2265
+ unknownScIds
2114
2266
  )
2115
2267
  );
2116
2268
  }
@@ -2173,8 +2325,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2173
2325
  issues.push(
2174
2326
  issue6(
2175
2327
  "QFAI-TRACE-002",
2176
- "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
2177
- "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",
2178
2330
  srcRoot,
2179
2331
  "traceability.codeReferences"
2180
2332
  )
@@ -2217,11 +2369,24 @@ async function validateProject(root, configResult) {
2217
2369
  ...await validateDefinedIds(root, config),
2218
2370
  ...await validateTraceability(root, config)
2219
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);
2220
2381
  const toolVersion = await resolveToolVersion();
2221
2382
  return {
2222
2383
  toolVersion,
2223
2384
  issues,
2224
- counts: countIssues(issues)
2385
+ counts: countIssues(issues),
2386
+ traceability: {
2387
+ sc: scCoverage,
2388
+ testFiles
2389
+ }
2225
2390
  };
2226
2391
  }
2227
2392
  function countIssues(issues) {
@@ -2242,9 +2407,9 @@ async function createReportData(root, validation, configResult) {
2242
2407
  const configPath = resolved.configPath;
2243
2408
  const specsRoot = resolvePath(root, config, "specsDir");
2244
2409
  const contractsRoot = resolvePath(root, config, "contractsDir");
2245
- const apiRoot = import_node_path10.default.join(contractsRoot, "api");
2246
- const uiRoot = import_node_path10.default.join(contractsRoot, "ui");
2247
- 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");
2248
2413
  const srcRoot = resolvePath(root, config, "srcDir");
2249
2414
  const testsRoot = resolvePath(root, config, "testsDir");
2250
2415
  const specFiles = await collectSpecFiles(specsRoot);
@@ -2271,8 +2436,15 @@ async function createReportData(root, validation, configResult) {
2271
2436
  testsRoot
2272
2437
  );
2273
2438
  const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2274
- const scTestRefs = await collectScTestReferences(testsRoot);
2275
- const scCoverage = buildScCoverage(scIds, scTestRefs);
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);
2276
2448
  const resolvedValidation = validation ?? await validateProject(root, resolved);
2277
2449
  const version = await resolveToolVersion();
2278
2450
  return {
@@ -2302,7 +2474,9 @@ async function createReportData(root, validation, configResult) {
2302
2474
  traceability: {
2303
2475
  upstreamIdsFound: upstreamIds.size,
2304
2476
  referencedInCodeOrTests: traceability,
2305
- sc: scCoverage
2477
+ sc: scCoverage,
2478
+ scSources: scSourceRecord,
2479
+ testFiles
2306
2480
  },
2307
2481
  issues: resolvedValidation.issues
2308
2482
  };
@@ -2343,10 +2517,29 @@ function formatReportMarkdown(data) {
2343
2517
  lines.push(`- total: ${data.traceability.sc.total}`);
2344
2518
  lines.push(`- covered: ${data.traceability.sc.covered}`);
2345
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
+ );
2346
2531
  if (data.traceability.sc.missingIds.length === 0) {
2347
2532
  lines.push("- missingIds: (none)");
2348
2533
  } else {
2349
- lines.push(`- missingIds: ${data.traceability.sc.missingIds.join(", ")}`);
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(", ")}`);
2350
2543
  }
2351
2544
  lines.push("");
2352
2545
  lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
@@ -2365,6 +2558,20 @@ function formatReportMarkdown(data) {
2365
2558
  }
2366
2559
  }
2367
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("");
2368
2575
  lines.push("## Hotspots");
2369
2576
  const hotspots = buildHotspots(data.issues);
2370
2577
  if (hotspots.length === 0) {
@@ -2475,9 +2682,22 @@ function formatIdLine(label, values) {
2475
2682
  }
2476
2683
  return `- ${label}: ${values.join(", ")}`;
2477
2684
  }
2685
+ function formatList(values) {
2686
+ if (values.length === 0) {
2687
+ return "(none)";
2688
+ }
2689
+ return values.join(", ");
2690
+ }
2478
2691
  function toSortedArray2(values) {
2479
2692
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2480
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
+ }
2481
2701
  function buildHotspots(issues) {
2482
2702
  const map = /* @__PURE__ */ new Map();
2483
2703
  for (const issue7 of issues) {