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.mjs CHANGED
@@ -29,6 +29,8 @@ var defaultConfig = {
29
29
  brMustHaveSc: true,
30
30
  scMustTouchContracts: true,
31
31
  scMustHaveTest: true,
32
+ testFileGlobs: [],
33
+ testFileExcludeGlobs: [],
32
34
  scNoTestSeverity: "error",
33
35
  allowOrphanContracts: false,
34
36
  unknownContractIdSeverity: "error"
@@ -216,6 +218,20 @@ function normalizeValidation(raw, configPath, issues) {
216
218
  configPath,
217
219
  issues
218
220
  ),
221
+ testFileGlobs: readStringArray(
222
+ traceabilityRaw?.testFileGlobs,
223
+ base.traceability.testFileGlobs,
224
+ "validation.traceability.testFileGlobs",
225
+ configPath,
226
+ issues
227
+ ),
228
+ testFileExcludeGlobs: readStringArray(
229
+ traceabilityRaw?.testFileExcludeGlobs,
230
+ base.traceability.testFileExcludeGlobs,
231
+ "validation.traceability.testFileExcludeGlobs",
232
+ configPath,
233
+ issues
234
+ ),
219
235
  scNoTestSeverity: readTraceabilitySeverity(
220
236
  traceabilityRaw?.scNoTestSeverity,
221
237
  base.traceability.scNoTestSeverity,
@@ -402,7 +418,7 @@ function isValidId(value, prefix) {
402
418
 
403
419
  // src/core/report.ts
404
420
  import { readFile as readFile11 } from "fs/promises";
405
- import path10 from "path";
421
+ import path11 from "path";
406
422
 
407
423
  // src/core/discovery.ts
408
424
  import { access as access2 } from "fs/promises";
@@ -410,6 +426,7 @@ import { access as access2 } from "fs/promises";
410
426
  // src/core/fs.ts
411
427
  import { access, readdir } from "fs/promises";
412
428
  import path2 from "path";
429
+ import fg from "fast-glob";
413
430
  var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
414
431
  "node_modules",
415
432
  ".git",
@@ -431,6 +448,18 @@ async function collectFiles(root, options = {}) {
431
448
  await walk(root, root, ignoreDirs, extensions, entries);
432
449
  return entries;
433
450
  }
451
+ async function collectFilesByGlobs(root, options) {
452
+ if (options.globs.length === 0) {
453
+ return [];
454
+ }
455
+ return fg(options.globs, {
456
+ cwd: root,
457
+ ignore: options.ignore ?? [],
458
+ onlyFiles: true,
459
+ absolute: true,
460
+ unique: true
461
+ });
462
+ }
434
463
  async function walk(base, current, ignoreDirs, extensions, out) {
435
464
  const items = await readdir(current, { withFileTypes: true });
436
465
  for (const item of items) {
@@ -544,6 +573,7 @@ async function exists2(target) {
544
573
 
545
574
  // src/core/traceability.ts
546
575
  import { readFile as readFile2 } from "fs/promises";
576
+ import path4 from "path";
547
577
 
548
578
  // src/core/gherkin/parse.ts
549
579
  import {
@@ -705,6 +735,27 @@ function unique2(values) {
705
735
 
706
736
  // src/core/traceability.ts
707
737
  var SC_TAG_RE2 = /^SC-\d{4}$/;
738
+ var SC_TEST_ANNOTATION_RE = /\bQFAI:SC-(\d{4})\b/g;
739
+ var DEFAULT_TEST_FILE_EXCLUDE_GLOBS = [
740
+ "**/node_modules/**",
741
+ "**/.git/**",
742
+ "**/.qfai/**",
743
+ "**/dist/**",
744
+ "**/build/**",
745
+ "**/coverage/**",
746
+ "**/.next/**",
747
+ "**/out/**"
748
+ ];
749
+ function extractAnnotatedScIds(text) {
750
+ const ids = /* @__PURE__ */ new Set();
751
+ for (const match of text.matchAll(SC_TEST_ANNOTATION_RE)) {
752
+ const suffix = match[1];
753
+ if (suffix) {
754
+ ids.add(`SC-${suffix}`);
755
+ }
756
+ }
757
+ return Array.from(ids);
758
+ }
708
759
  async function collectScIdsFromScenarioFiles(scenarioFiles) {
709
760
  const scIds = /* @__PURE__ */ new Set();
710
761
  for (const file of scenarioFiles) {
@@ -723,14 +774,67 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
723
774
  }
724
775
  return scIds;
725
776
  }
726
- async function collectScTestReferences(testsRoot) {
777
+ async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
778
+ const sources = /* @__PURE__ */ new Map();
779
+ for (const file of scenarioFiles) {
780
+ const text = await readFile2(file, "utf-8");
781
+ const { document, errors } = parseScenarioDocument(text, file);
782
+ if (!document || errors.length > 0) {
783
+ continue;
784
+ }
785
+ for (const scenario of document.scenarios) {
786
+ for (const tag of scenario.tags) {
787
+ if (!SC_TAG_RE2.test(tag)) {
788
+ continue;
789
+ }
790
+ const current = sources.get(tag) ?? /* @__PURE__ */ new Set();
791
+ current.add(file);
792
+ sources.set(tag, current);
793
+ }
794
+ }
795
+ }
796
+ return sources;
797
+ }
798
+ async function collectScTestReferences(root, globs, excludeGlobs) {
727
799
  const refs = /* @__PURE__ */ new Map();
728
- const testFiles = await collectFiles(testsRoot, {
729
- extensions: [".ts", ".tsx", ".js", ".jsx"]
730
- });
731
- for (const file of testFiles) {
800
+ const normalizedGlobs = normalizeGlobs(globs);
801
+ const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
802
+ const mergedExcludeGlobs = Array.from(
803
+ /* @__PURE__ */ new Set([...DEFAULT_TEST_FILE_EXCLUDE_GLOBS, ...normalizedExcludeGlobs])
804
+ );
805
+ if (normalizedGlobs.length === 0) {
806
+ return {
807
+ refs,
808
+ scan: {
809
+ globs: normalizedGlobs,
810
+ excludeGlobs: mergedExcludeGlobs,
811
+ matchedFileCount: 0
812
+ }
813
+ };
814
+ }
815
+ let files = [];
816
+ try {
817
+ files = await collectFilesByGlobs(root, {
818
+ globs: normalizedGlobs,
819
+ ignore: mergedExcludeGlobs
820
+ });
821
+ } catch (error) {
822
+ return {
823
+ refs,
824
+ scan: {
825
+ globs: normalizedGlobs,
826
+ excludeGlobs: mergedExcludeGlobs,
827
+ matchedFileCount: 0
828
+ },
829
+ error: formatError3(error)
830
+ };
831
+ }
832
+ const normalizedFiles = Array.from(
833
+ new Set(files.map((file) => path4.normalize(file)))
834
+ );
835
+ for (const file of normalizedFiles) {
732
836
  const text = await readFile2(file, "utf-8");
733
- const scIds = extractIds(text, "SC");
837
+ const scIds = extractAnnotatedScIds(text);
734
838
  if (scIds.length === 0) {
735
839
  continue;
736
840
  }
@@ -740,7 +844,14 @@ async function collectScTestReferences(testsRoot) {
740
844
  refs.set(scId, current);
741
845
  }
742
846
  }
743
- return refs;
847
+ return {
848
+ refs,
849
+ scan: {
850
+ globs: normalizedGlobs,
851
+ excludeGlobs: mergedExcludeGlobs,
852
+ matchedFileCount: normalizedFiles.length
853
+ }
854
+ };
744
855
  }
745
856
  function buildScCoverage(scIds, refs) {
746
857
  const sortedScIds = toSortedArray(scIds);
@@ -768,14 +879,23 @@ function buildScCoverage(scIds, refs) {
768
879
  function toSortedArray(values) {
769
880
  return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
770
881
  }
882
+ function normalizeGlobs(globs) {
883
+ return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
884
+ }
885
+ function formatError3(error) {
886
+ if (error instanceof Error) {
887
+ return error.message;
888
+ }
889
+ return String(error);
890
+ }
771
891
 
772
892
  // src/core/version.ts
773
893
  import { readFile as readFile3 } from "fs/promises";
774
- import path4 from "path";
894
+ import path5 from "path";
775
895
  import { fileURLToPath } from "url";
776
896
  async function resolveToolVersion() {
777
- if ("0.4.0".length > 0) {
778
- return "0.4.0";
897
+ if ("0.4.2".length > 0) {
898
+ return "0.4.2";
779
899
  }
780
900
  try {
781
901
  const packagePath = resolvePackageJsonPath();
@@ -790,18 +910,18 @@ async function resolveToolVersion() {
790
910
  function resolvePackageJsonPath() {
791
911
  const base = import.meta.url;
792
912
  const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
793
- return path4.resolve(path4.dirname(basePath), "../../package.json");
913
+ return path5.resolve(path5.dirname(basePath), "../../package.json");
794
914
  }
795
915
 
796
916
  // src/core/validators/contracts.ts
797
917
  import { readFile as readFile4 } from "fs/promises";
798
- import path6 from "path";
918
+ import path7 from "path";
799
919
 
800
920
  // src/core/contracts.ts
801
- import path5 from "path";
921
+ import path6 from "path";
802
922
  import { parse as parseYaml2 } from "yaml";
803
923
  function parseStructuredContract(file, text) {
804
- const ext = path5.extname(file).toLowerCase();
924
+ const ext = path6.extname(file).toLowerCase();
805
925
  if (ext === ".json") {
806
926
  return JSON.parse(text);
807
927
  }
@@ -852,9 +972,9 @@ var SQL_DANGEROUS_PATTERNS = [
852
972
  async function validateContracts(root, config) {
853
973
  const issues = [];
854
974
  const contractsRoot = resolvePath(root, config, "contractsDir");
855
- issues.push(...await validateUiContracts(path6.join(contractsRoot, "ui")));
856
- issues.push(...await validateApiContracts(path6.join(contractsRoot, "api")));
857
- issues.push(...await validateDataContracts(path6.join(contractsRoot, "db")));
975
+ issues.push(...await validateUiContracts(path7.join(contractsRoot, "ui")));
976
+ issues.push(...await validateApiContracts(path7.join(contractsRoot, "api")));
977
+ issues.push(...await validateDataContracts(path7.join(contractsRoot, "db")));
858
978
  return issues;
859
979
  }
860
980
  async function validateUiContracts(uiRoot) {
@@ -901,7 +1021,7 @@ async function validateUiContracts(uiRoot) {
901
1021
  issues.push(
902
1022
  issue(
903
1023
  "QFAI-CONTRACT-001",
904
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error)})`,
1024
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
905
1025
  "error",
906
1026
  file,
907
1027
  "contracts.ui.parse"
@@ -968,7 +1088,7 @@ async function validateApiContracts(apiRoot) {
968
1088
  issues.push(
969
1089
  issue(
970
1090
  "QFAI-CONTRACT-001",
971
- `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error)})`,
1091
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
972
1092
  "error",
973
1093
  file,
974
1094
  "contracts.api.parse"
@@ -1063,7 +1183,7 @@ function lintSql(text, file) {
1063
1183
  function hasOpenApi(doc) {
1064
1184
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
1065
1185
  }
1066
- function formatError3(error) {
1186
+ function formatError4(error) {
1067
1187
  if (error instanceof Error) {
1068
1188
  return error.message;
1069
1189
  }
@@ -1089,7 +1209,7 @@ function issue(code, message, severity, file, rule, refs) {
1089
1209
 
1090
1210
  // src/core/validators/delta.ts
1091
1211
  import { readFile as readFile5 } from "fs/promises";
1092
- import path7 from "path";
1212
+ import path8 from "path";
1093
1213
  var SECTION_RE = /^##\s+変更区分/m;
1094
1214
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1095
1215
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1103,7 +1223,7 @@ async function validateDeltas(root, config) {
1103
1223
  }
1104
1224
  const issues = [];
1105
1225
  for (const pack of packs) {
1106
- const deltaPath = path7.join(pack, "delta.md");
1226
+ const deltaPath = path8.join(pack, "delta.md");
1107
1227
  let text;
1108
1228
  try {
1109
1229
  text = await readFile5(deltaPath, "utf-8");
@@ -1179,16 +1299,16 @@ function issue2(code, message, severity, file, rule, refs) {
1179
1299
 
1180
1300
  // src/core/validators/ids.ts
1181
1301
  import { readFile as readFile7 } from "fs/promises";
1182
- import path9 from "path";
1302
+ import path10 from "path";
1183
1303
 
1184
1304
  // src/core/contractIndex.ts
1185
1305
  import { readFile as readFile6 } from "fs/promises";
1186
- import path8 from "path";
1306
+ import path9 from "path";
1187
1307
  async function buildContractIndex(root, config) {
1188
1308
  const contractsRoot = resolvePath(root, config, "contractsDir");
1189
- const uiRoot = path8.join(contractsRoot, "ui");
1190
- const apiRoot = path8.join(contractsRoot, "api");
1191
- const dataRoot = path8.join(contractsRoot, "db");
1309
+ const uiRoot = path9.join(contractsRoot, "ui");
1310
+ const apiRoot = path9.join(contractsRoot, "api");
1311
+ const dataRoot = path9.join(contractsRoot, "db");
1192
1312
  const [uiFiles, apiFiles, dataFiles] = await Promise.all([
1193
1313
  collectUiContractFiles(uiRoot),
1194
1314
  collectApiContractFiles(apiRoot),
@@ -1426,7 +1546,7 @@ function recordId(out, id, file) {
1426
1546
  }
1427
1547
  function formatFileList(files, root) {
1428
1548
  return files.map((file) => {
1429
- const relative = path9.relative(root, file);
1549
+ const relative = path10.relative(root, file);
1430
1550
  return relative.length > 0 ? relative : file;
1431
1551
  }).join(", ");
1432
1552
  }
@@ -1455,7 +1575,6 @@ var WHEN_PATTERN = /\bWhen\b/;
1455
1575
  var THEN_PATTERN = /\bThen\b/;
1456
1576
  var SC_TAG_RE4 = /^SC-\d{4}$/;
1457
1577
  var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1458
- var BR_TAG_RE2 = /^BR-\d{4}$/;
1459
1578
  async function validateScenarios(root, config) {
1460
1579
  const specsRoot = resolvePath(root, config, "specsDir");
1461
1580
  const entries = await collectSpecEntries(specsRoot);
@@ -1535,17 +1654,7 @@ function validateScenarioContent(text, file) {
1535
1654
  const featureSpecTags = document.featureTags.filter(
1536
1655
  (tag) => SPEC_TAG_RE2.test(tag)
1537
1656
  );
1538
- if (featureSpecTags.length === 0) {
1539
- issues.push(
1540
- issue4(
1541
- "QFAI-SC-009",
1542
- "Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1543
- "error",
1544
- file,
1545
- "scenario.featureSpec"
1546
- )
1547
- );
1548
- } else if (featureSpecTags.length > 1) {
1657
+ if (featureSpecTags.length > 1) {
1549
1658
  issues.push(
1550
1659
  issue4(
1551
1660
  "QFAI-SC-009",
@@ -1573,17 +1682,6 @@ function validateScenarioContent(text, file) {
1573
1682
  )
1574
1683
  );
1575
1684
  }
1576
- if (document.scenarios.length > 1) {
1577
- issues.push(
1578
- issue4(
1579
- "QFAI-SC-011",
1580
- `Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
1581
- "error",
1582
- file,
1583
- "scenario.single"
1584
- )
1585
- );
1586
- }
1587
1685
  for (const scenario of document.scenarios) {
1588
1686
  if (scenario.tags.length === 0) {
1589
1687
  issues.push(
@@ -1604,12 +1702,6 @@ function validateScenarioContent(text, file) {
1604
1702
  } else if (scTags.length > 1) {
1605
1703
  missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1606
1704
  }
1607
- if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
1608
- missingTags.push("SPEC");
1609
- }
1610
- if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
1611
- missingTags.push("BR");
1612
- }
1613
1705
  if (missingTags.length > 0) {
1614
1706
  issues.push(
1615
1707
  issue4(
@@ -1844,9 +1936,8 @@ function isMissingFileError4(error) {
1844
1936
 
1845
1937
  // src/core/validators/traceability.ts
1846
1938
  import { readFile as readFile10 } from "fs/promises";
1847
- var SC_TAG_RE5 = /^SC-\d{4}$/;
1848
1939
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
1849
- var BR_TAG_RE3 = /^BR-\d{4}$/;
1940
+ var BR_TAG_RE2 = /^BR-\d{4}$/;
1850
1941
  async function validateTraceability(root, config) {
1851
1942
  const issues = [];
1852
1943
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1873,28 +1964,6 @@ async function validateTraceability(root, config) {
1873
1964
  }
1874
1965
  const brIds = parsed.brs.map((br) => br.id);
1875
1966
  brIds.forEach((id) => brIdsInSpecs.add(id));
1876
- const referencedContractIds = /* @__PURE__ */ new Set([
1877
- ...extractIds(text, "UI"),
1878
- ...extractIds(text, "API"),
1879
- ...extractIds(text, "DATA")
1880
- ]);
1881
- const unknownContractIds = Array.from(referencedContractIds).filter(
1882
- (id) => !contractIds.has(id)
1883
- );
1884
- if (unknownContractIds.length > 0) {
1885
- issues.push(
1886
- issue6(
1887
- "QFAI-TRACE-009",
1888
- `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1889
- ", "
1890
- )}`,
1891
- "error",
1892
- file,
1893
- "traceability.specContractExists",
1894
- unknownContractIds
1895
- )
1896
- );
1897
- }
1898
1967
  if (parsed.specId) {
1899
1968
  const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
1900
1969
  brIds.forEach((id) => current.add(id));
@@ -1909,16 +1978,42 @@ async function validateTraceability(root, config) {
1909
1978
  continue;
1910
1979
  }
1911
1980
  const atoms = buildScenarioAtoms(document);
1981
+ const scIdsInFile = /* @__PURE__ */ new Set();
1912
1982
  for (const [index, scenario] of document.scenarios.entries()) {
1913
1983
  const atom = atoms[index];
1914
1984
  if (!atom) {
1915
1985
  continue;
1916
1986
  }
1917
1987
  const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
1918
- const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
1919
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE5.test(tag));
1988
+ const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
1989
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
1990
+ if (specTags.length === 0) {
1991
+ issues.push(
1992
+ issue6(
1993
+ "QFAI-TRACE-014",
1994
+ `Scenario \u304C SPEC \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
1995
+ "error",
1996
+ file,
1997
+ "traceability.scenarioSpecRequired"
1998
+ )
1999
+ );
2000
+ }
2001
+ if (brTags.length === 0) {
2002
+ issues.push(
2003
+ issue6(
2004
+ "QFAI-TRACE-015",
2005
+ `Scenario \u304C BR \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
2006
+ "error",
2007
+ file,
2008
+ "traceability.scenarioBrRequired"
2009
+ )
2010
+ );
2011
+ }
1920
2012
  brTags.forEach((id) => brIdsInScenarios.add(id));
1921
- scTags.forEach((id) => scIdsInScenarios.add(id));
2013
+ scTags.forEach((id) => {
2014
+ scIdsInScenarios.add(id);
2015
+ scIdsInFile.add(id);
2016
+ });
1922
2017
  atom.contractIds.forEach((id) => scenarioContractIds.add(id));
1923
2018
  if (atom.contractIds.length > 0) {
1924
2019
  scTags.forEach((id) => scWithContracts.add(id));
@@ -1996,6 +2091,22 @@ async function validateTraceability(root, config) {
1996
2091
  }
1997
2092
  }
1998
2093
  }
2094
+ if (scIdsInFile.size !== 1) {
2095
+ const invalidScIds = Array.from(scIdsInFile).sort(
2096
+ (a, b) => a.localeCompare(b)
2097
+ );
2098
+ const detail = invalidScIds.length === 0 ? "SC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" : `\u8907\u6570\u306E SC \u304C\u5B58\u5728\u3057\u307E\u3059: ${invalidScIds.join(", ")}`;
2099
+ issues.push(
2100
+ issue6(
2101
+ "QFAI-TRACE-012",
2102
+ `Spec entry \u304C Spec:SC=1:1 \u3092\u6E80\u305F\u3057\u3066\u3044\u307E\u305B\u3093: ${detail}`,
2103
+ "error",
2104
+ file,
2105
+ "traceability.specScOneToOne",
2106
+ invalidScIds
2107
+ )
2108
+ );
2109
+ }
1999
2110
  }
2000
2111
  if (upstreamIds.size === 0) {
2001
2112
  return [
@@ -2044,21 +2155,62 @@ async function validateTraceability(root, config) {
2044
2155
  );
2045
2156
  }
2046
2157
  }
2047
- if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2048
- const scTestRefs = await collectScTestReferences(testsRoot);
2049
- const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2050
- const refs = scTestRefs.get(id);
2051
- return !refs || refs.size === 0;
2052
- });
2053
- if (scWithoutTests.length > 0) {
2158
+ const scRefsResult = await collectScTestReferences(
2159
+ root,
2160
+ config.validation.traceability.testFileGlobs,
2161
+ config.validation.traceability.testFileExcludeGlobs
2162
+ );
2163
+ const scTestRefs = scRefsResult.refs;
2164
+ const testFileScan = scRefsResult.scan;
2165
+ const hasScenarios = scIdsInScenarios.size > 0;
2166
+ const hasGlobConfig = testFileScan.globs.length > 0;
2167
+ const hasMatchedTests = testFileScan.matchedFileCount > 0;
2168
+ if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
2169
+ const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
2170
+ issues.push(
2171
+ issue6(
2172
+ "QFAI-TRACE-013",
2173
+ `\u30C6\u30B9\u30C8\u63A2\u7D22 glob \u304C\u672A\u8A2D\u5B9A/\u4E0D\u6B63/\u4E00\u81F4\u30D5\u30A1\u30A4\u30EB0\u306E\u305F\u3081 SC\u2192Test \u3092\u5224\u5B9A\u3067\u304D\u307E\u305B\u3093\u3002${detail}`,
2174
+ "error",
2175
+ testsRoot,
2176
+ "traceability.testFileGlobs"
2177
+ )
2178
+ );
2179
+ } else {
2180
+ if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2181
+ const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2182
+ const refs = scTestRefs.get(id);
2183
+ return !refs || refs.size === 0;
2184
+ });
2185
+ if (scWithoutTests.length > 0) {
2186
+ issues.push(
2187
+ issue6(
2188
+ "QFAI-TRACE-010",
2189
+ `SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(
2190
+ ", "
2191
+ )}\u3002testFileGlobs \u306B\u4E00\u81F4\u3059\u308B\u30C6\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\u3078 QFAI:SC-xxxx \u3092\u8A18\u8F09\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
2192
+ config.validation.traceability.scNoTestSeverity,
2193
+ testsRoot,
2194
+ "traceability.scMustHaveTest",
2195
+ scWithoutTests
2196
+ )
2197
+ );
2198
+ }
2199
+ }
2200
+ const unknownScIds = Array.from(scTestRefs.keys()).filter(
2201
+ (id) => !scIdsInScenarios.has(id)
2202
+ );
2203
+ if (unknownScIds.length > 0) {
2054
2204
  issues.push(
2055
2205
  issue6(
2056
- "QFAI-TRACE-010",
2057
- `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`,
2058
- config.validation.traceability.scNoTestSeverity,
2206
+ "QFAI-TRACE-011",
2207
+ `\u30C6\u30B9\u30C8\u304C\u672A\u77E5\u306E SC \u3092\u30A2\u30CE\u30C6\u30FC\u30B7\u30E7\u30F3\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownScIds.join(
2208
+ ", "
2209
+ )}`,
2210
+ "error",
2059
2211
  testsRoot,
2060
- "traceability.scMustHaveTest",
2061
- scWithoutTests
2212
+ "traceability.scUnknownInTests",
2213
+ unknownScIds
2062
2214
  )
2063
2215
  );
2064
2216
  }
@@ -2121,8 +2273,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2121
2273
  issues.push(
2122
2274
  issue6(
2123
2275
  "QFAI-TRACE-002",
2124
- "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
2125
- "warning",
2276
+ "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\uFF08\u53C2\u8003\u60C5\u5831\uFF09\u3002",
2277
+ "info",
2126
2278
  srcRoot,
2127
2279
  "traceability.codeReferences"
2128
2280
  )
@@ -2165,11 +2317,24 @@ async function validateProject(root, configResult) {
2165
2317
  ...await validateDefinedIds(root, config),
2166
2318
  ...await validateTraceability(root, config)
2167
2319
  ];
2320
+ const specsRoot = resolvePath(root, config, "specsDir");
2321
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
2322
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2323
+ const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
2324
+ root,
2325
+ config.validation.traceability.testFileGlobs,
2326
+ config.validation.traceability.testFileExcludeGlobs
2327
+ );
2328
+ const scCoverage = buildScCoverage(scIds, scTestRefs);
2168
2329
  const toolVersion = await resolveToolVersion();
2169
2330
  return {
2170
2331
  toolVersion,
2171
2332
  issues,
2172
- counts: countIssues(issues)
2333
+ counts: countIssues(issues),
2334
+ traceability: {
2335
+ sc: scCoverage,
2336
+ testFiles
2337
+ }
2173
2338
  };
2174
2339
  }
2175
2340
  function countIssues(issues) {
@@ -2190,9 +2355,9 @@ async function createReportData(root, validation, configResult) {
2190
2355
  const configPath = resolved.configPath;
2191
2356
  const specsRoot = resolvePath(root, config, "specsDir");
2192
2357
  const contractsRoot = resolvePath(root, config, "contractsDir");
2193
- const apiRoot = path10.join(contractsRoot, "api");
2194
- const uiRoot = path10.join(contractsRoot, "ui");
2195
- const dbRoot = path10.join(contractsRoot, "db");
2358
+ const apiRoot = path11.join(contractsRoot, "api");
2359
+ const uiRoot = path11.join(contractsRoot, "ui");
2360
+ const dbRoot = path11.join(contractsRoot, "db");
2196
2361
  const srcRoot = resolvePath(root, config, "srcDir");
2197
2362
  const testsRoot = resolvePath(root, config, "testsDir");
2198
2363
  const specFiles = await collectSpecFiles(specsRoot);
@@ -2219,8 +2384,15 @@ async function createReportData(root, validation, configResult) {
2219
2384
  testsRoot
2220
2385
  );
2221
2386
  const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2222
- const scTestRefs = await collectScTestReferences(testsRoot);
2223
- const scCoverage = buildScCoverage(scIds, scTestRefs);
2387
+ const scRefsResult = await collectScTestReferences(
2388
+ root,
2389
+ config.validation.traceability.testFileGlobs,
2390
+ config.validation.traceability.testFileExcludeGlobs
2391
+ );
2392
+ const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
2393
+ const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
2394
+ const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
2395
+ const scSourceRecord = mapToSortedRecord(scSources);
2224
2396
  const resolvedValidation = validation ?? await validateProject(root, resolved);
2225
2397
  const version = await resolveToolVersion();
2226
2398
  return {
@@ -2250,7 +2422,9 @@ async function createReportData(root, validation, configResult) {
2250
2422
  traceability: {
2251
2423
  upstreamIdsFound: upstreamIds.size,
2252
2424
  referencedInCodeOrTests: traceability,
2253
- sc: scCoverage
2425
+ sc: scCoverage,
2426
+ scSources: scSourceRecord,
2427
+ testFiles
2254
2428
  },
2255
2429
  issues: resolvedValidation.issues
2256
2430
  };
@@ -2291,10 +2465,29 @@ function formatReportMarkdown(data) {
2291
2465
  lines.push(`- total: ${data.traceability.sc.total}`);
2292
2466
  lines.push(`- covered: ${data.traceability.sc.covered}`);
2293
2467
  lines.push(`- missing: ${data.traceability.sc.missing}`);
2468
+ lines.push(
2469
+ `- testFileGlobs: ${formatList(data.traceability.testFiles.globs)}`
2470
+ );
2471
+ lines.push(
2472
+ `- testFileExcludeGlobs: ${formatList(
2473
+ data.traceability.testFiles.excludeGlobs
2474
+ )}`
2475
+ );
2476
+ lines.push(
2477
+ `- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
2478
+ );
2294
2479
  if (data.traceability.sc.missingIds.length === 0) {
2295
2480
  lines.push("- missingIds: (none)");
2296
2481
  } else {
2297
- lines.push(`- missingIds: ${data.traceability.sc.missingIds.join(", ")}`);
2482
+ const sources = data.traceability.scSources;
2483
+ const missingWithSources = data.traceability.sc.missingIds.map((id) => {
2484
+ const files = sources[id] ?? [];
2485
+ if (files.length === 0) {
2486
+ return id;
2487
+ }
2488
+ return `${id} (${files.join(", ")})`;
2489
+ });
2490
+ lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
2298
2491
  }
2299
2492
  lines.push("");
2300
2493
  lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
@@ -2313,6 +2506,20 @@ function formatReportMarkdown(data) {
2313
2506
  }
2314
2507
  }
2315
2508
  lines.push("");
2509
+ lines.push("## Spec:SC=1:1 \u9055\u53CD");
2510
+ const specScIssues = data.issues.filter(
2511
+ (item) => item.code === "QFAI-TRACE-012"
2512
+ );
2513
+ if (specScIssues.length === 0) {
2514
+ lines.push("- (none)");
2515
+ } else {
2516
+ for (const item of specScIssues) {
2517
+ const location = item.file ?? "(unknown)";
2518
+ const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
2519
+ lines.push(`- ${location}: ${refs}`);
2520
+ }
2521
+ }
2522
+ lines.push("");
2316
2523
  lines.push("## Hotspots");
2317
2524
  const hotspots = buildHotspots(data.issues);
2318
2525
  if (hotspots.length === 0) {
@@ -2423,9 +2630,22 @@ function formatIdLine(label, values) {
2423
2630
  }
2424
2631
  return `- ${label}: ${values.join(", ")}`;
2425
2632
  }
2633
+ function formatList(values) {
2634
+ if (values.length === 0) {
2635
+ return "(none)";
2636
+ }
2637
+ return values.join(", ");
2638
+ }
2426
2639
  function toSortedArray2(values) {
2427
2640
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2428
2641
  }
2642
+ function mapToSortedRecord(values) {
2643
+ const record2 = {};
2644
+ for (const [key, files] of values.entries()) {
2645
+ record2[key] = Array.from(files).sort((a, b) => a.localeCompare(b));
2646
+ }
2647
+ return record2;
2648
+ }
2429
2649
  function buildHotspots(issues) {
2430
2650
  const map = /* @__PURE__ */ new Map();
2431
2651
  for (const issue7 of issues) {