qfai 0.3.6 → 0.4.0

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 (47) hide show
  1. package/README.md +7 -0
  2. package/assets/init/.qfai/README.md +4 -0
  3. package/assets/init/.qfai/prompts/makeBusinessFlow.md +1 -1
  4. package/assets/init/.qfai/prompts/makeOverview.md +1 -1
  5. package/assets/init/.qfai/rules/conventions.md +1 -1
  6. package/assets/init/.qfai/specs/README.md +1 -1
  7. package/assets/init/root/qfai.config.yaml +2 -0
  8. package/assets/init/root/tests/qfai-traceability.sample.test.ts +2 -0
  9. package/dist/cli/commands/report.d.ts.map +1 -1
  10. package/dist/cli/commands/report.js +0 -7
  11. package/dist/cli/commands/report.js.map +1 -1
  12. package/dist/cli/index.cjs +351 -218
  13. package/dist/cli/index.cjs.map +1 -1
  14. package/dist/cli/index.mjs +350 -217
  15. package/dist/cli/index.mjs.map +1 -1
  16. package/dist/core/config.d.ts +2 -0
  17. package/dist/core/config.d.ts.map +1 -1
  18. package/dist/core/config.js +4 -0
  19. package/dist/core/config.js.map +1 -1
  20. package/dist/core/report.d.ts +2 -0
  21. package/dist/core/report.d.ts.map +1 -1
  22. package/dist/core/report.js +34 -0
  23. package/dist/core/report.js.map +1 -1
  24. package/dist/core/traceability.d.ts +12 -0
  25. package/dist/core/traceability.d.ts.map +1 -0
  26. package/dist/core/traceability.js +70 -0
  27. package/dist/core/traceability.js.map +1 -0
  28. package/dist/core/types.d.ts +0 -2
  29. package/dist/core/types.d.ts.map +1 -1
  30. package/dist/core/types.js +1 -1
  31. package/dist/core/types.js.map +1 -1
  32. package/dist/core/validate.d.ts.map +1 -1
  33. package/dist/core/validate.js +0 -2
  34. package/dist/core/validate.js.map +1 -1
  35. package/dist/core/validators/scenario.d.ts.map +1 -1
  36. package/dist/core/validators/scenario.js +3 -0
  37. package/dist/core/validators/scenario.js.map +1 -1
  38. package/dist/core/validators/traceability.d.ts.map +1 -1
  39. package/dist/core/validators/traceability.js +11 -1
  40. package/dist/core/validators/traceability.js.map +1 -1
  41. package/dist/index.cjs +344 -205
  42. package/dist/index.cjs.map +1 -1
  43. package/dist/index.d.cts +12 -3
  44. package/dist/index.mjs +348 -208
  45. package/dist/index.mjs.map +1 -1
  46. package/dist/tsconfig.tsbuildinfo +1 -1
  47. package/package.json +1 -1
@@ -177,7 +177,7 @@ function report(copied, skipped, dryRun, label) {
177
177
  }
178
178
 
179
179
  // src/cli/commands/report.ts
180
- var import_promises15 = require("fs/promises");
180
+ var import_promises16 = require("fs/promises");
181
181
  var import_node_path14 = __toESM(require("path"), 1);
182
182
 
183
183
  // src/core/config.ts
@@ -210,6 +210,8 @@ var defaultConfig = {
210
210
  traceability: {
211
211
  brMustHaveSc: true,
212
212
  scMustTouchContracts: true,
213
+ scMustHaveTest: true,
214
+ scNoTestSeverity: "error",
213
215
  allowOrphanContracts: false,
214
216
  unknownContractIdSeverity: "error"
215
217
  }
@@ -389,6 +391,20 @@ function normalizeValidation(raw, configPath, issues) {
389
391
  configPath,
390
392
  issues
391
393
  ),
394
+ scMustHaveTest: readBoolean(
395
+ traceabilityRaw?.scMustHaveTest,
396
+ base.traceability.scMustHaveTest,
397
+ "validation.traceability.scMustHaveTest",
398
+ configPath,
399
+ issues
400
+ ),
401
+ scNoTestSeverity: readTraceabilitySeverity(
402
+ traceabilityRaw?.scNoTestSeverity,
403
+ base.traceability.scNoTestSeverity,
404
+ "validation.traceability.scNoTestSeverity",
405
+ configPath,
406
+ issues
407
+ ),
392
408
  allowOrphanContracts: readBoolean(
393
409
  traceabilityRaw?.allowOrphanContracts,
394
410
  base.traceability.allowOrphanContracts,
@@ -514,7 +530,7 @@ function isRecord(value) {
514
530
  }
515
531
 
516
532
  // src/core/report.ts
517
- var import_promises14 = require("fs/promises");
533
+ var import_promises15 = require("fs/promises");
518
534
  var import_node_path13 = __toESM(require("path"), 1);
519
535
 
520
536
  // src/core/discovery.ts
@@ -708,20 +724,240 @@ function isValidId(value, prefix) {
708
724
  return strict.test(value);
709
725
  }
710
726
 
711
- // src/core/types.ts
712
- var VALIDATION_SCHEMA_VERSION = "0.2";
727
+ // src/core/traceability.ts
728
+ var import_promises6 = require("fs/promises");
729
+
730
+ // src/core/gherkin/parse.ts
731
+ var import_gherkin = require("@cucumber/gherkin");
732
+ var import_node_crypto = require("crypto");
733
+ function parseGherkin(source, uri) {
734
+ const errors = [];
735
+ const uuidFn = () => (0, import_node_crypto.randomUUID)();
736
+ const builder = new import_gherkin.AstBuilder(uuidFn);
737
+ const matcher = new import_gherkin.GherkinClassicTokenMatcher();
738
+ const parser = new import_gherkin.Parser(builder, matcher);
739
+ try {
740
+ const gherkinDocument = parser.parse(source);
741
+ gherkinDocument.uri = uri;
742
+ return { gherkinDocument, errors };
743
+ } catch (error2) {
744
+ errors.push(formatError2(error2));
745
+ return { gherkinDocument: null, errors };
746
+ }
747
+ }
748
+ function formatError2(error2) {
749
+ if (error2 instanceof Error) {
750
+ return error2.message;
751
+ }
752
+ return String(error2);
753
+ }
754
+
755
+ // src/core/scenarioModel.ts
756
+ var SPEC_TAG_RE = /^SPEC-\d{4}$/;
757
+ var SC_TAG_RE = /^SC-\d{4}$/;
758
+ var BR_TAG_RE = /^BR-\d{4}$/;
759
+ var UI_TAG_RE = /^UI-\d{4}$/;
760
+ var API_TAG_RE = /^API-\d{4}$/;
761
+ var DATA_TAG_RE = /^DATA-\d{4}$/;
762
+ function parseScenarioDocument(text, uri) {
763
+ const { gherkinDocument, errors } = parseGherkin(text, uri);
764
+ if (!gherkinDocument) {
765
+ return { document: null, errors };
766
+ }
767
+ const feature = gherkinDocument.feature;
768
+ if (!feature) {
769
+ return {
770
+ document: { uri, featureTags: [], scenarios: [] },
771
+ errors
772
+ };
773
+ }
774
+ const featureTags = collectTagNames(feature.tags);
775
+ const scenarios = collectScenarioNodes(feature, featureTags);
776
+ return {
777
+ document: {
778
+ uri,
779
+ featureName: feature.name,
780
+ featureTags,
781
+ scenarios
782
+ },
783
+ errors
784
+ };
785
+ }
786
+ function buildScenarioAtoms(document) {
787
+ return document.scenarios.map((scenario) => {
788
+ const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
789
+ const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
790
+ const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
791
+ const contractIds = /* @__PURE__ */ new Set();
792
+ scenario.tags.forEach((tag) => {
793
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
794
+ contractIds.add(tag);
795
+ }
796
+ });
797
+ for (const step of scenario.steps) {
798
+ for (const text of collectStepTexts(step)) {
799
+ extractIds(text, "UI").forEach((id) => contractIds.add(id));
800
+ extractIds(text, "API").forEach((id) => contractIds.add(id));
801
+ extractIds(text, "DATA").forEach((id) => contractIds.add(id));
802
+ }
803
+ }
804
+ const atom = {
805
+ uri: document.uri,
806
+ featureName: document.featureName ?? "",
807
+ scenarioName: scenario.name,
808
+ kind: scenario.kind,
809
+ brIds,
810
+ contractIds: Array.from(contractIds).sort()
811
+ };
812
+ if (scenario.line !== void 0) {
813
+ atom.line = scenario.line;
814
+ }
815
+ if (specIds.length === 1) {
816
+ const specId = specIds[0];
817
+ if (specId) {
818
+ atom.specId = specId;
819
+ }
820
+ }
821
+ if (scIds.length === 1) {
822
+ const scId = scIds[0];
823
+ if (scId) {
824
+ atom.scId = scId;
825
+ }
826
+ }
827
+ return atom;
828
+ });
829
+ }
830
+ function collectScenarioNodes(feature, featureTags) {
831
+ const scenarios = [];
832
+ for (const child of feature.children) {
833
+ if (child.scenario) {
834
+ scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
835
+ }
836
+ if (child.rule) {
837
+ const ruleTags = collectTagNames(child.rule.tags);
838
+ for (const ruleChild of child.rule.children) {
839
+ if (ruleChild.scenario) {
840
+ scenarios.push(
841
+ buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
842
+ );
843
+ }
844
+ }
845
+ }
846
+ }
847
+ return scenarios;
848
+ }
849
+ function buildScenarioNode(scenario, featureTags, ruleTags) {
850
+ const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
851
+ const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
852
+ return {
853
+ name: scenario.name,
854
+ kind,
855
+ line: scenario.location?.line,
856
+ tags,
857
+ steps: scenario.steps
858
+ };
859
+ }
860
+ function collectTagNames(tags) {
861
+ return tags.map((tag) => tag.name.replace(/^@/, ""));
862
+ }
863
+ function collectStepTexts(step) {
864
+ const texts = [];
865
+ if (step.text) {
866
+ texts.push(step.text);
867
+ }
868
+ if (step.docString?.content) {
869
+ texts.push(step.docString.content);
870
+ }
871
+ if (step.dataTable?.rows) {
872
+ for (const row of step.dataTable.rows) {
873
+ for (const cell of row.cells) {
874
+ texts.push(cell.value);
875
+ }
876
+ }
877
+ }
878
+ return texts;
879
+ }
880
+ function unique2(values) {
881
+ return Array.from(new Set(values));
882
+ }
883
+
884
+ // src/core/traceability.ts
885
+ var SC_TAG_RE2 = /^SC-\d{4}$/;
886
+ async function collectScIdsFromScenarioFiles(scenarioFiles) {
887
+ const scIds = /* @__PURE__ */ new Set();
888
+ for (const file of scenarioFiles) {
889
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
890
+ const { document, errors } = parseScenarioDocument(text, file);
891
+ if (!document || errors.length > 0) {
892
+ continue;
893
+ }
894
+ for (const scenario of document.scenarios) {
895
+ for (const tag of scenario.tags) {
896
+ if (SC_TAG_RE2.test(tag)) {
897
+ scIds.add(tag);
898
+ }
899
+ }
900
+ }
901
+ }
902
+ return scIds;
903
+ }
904
+ async function collectScTestReferences(testsRoot) {
905
+ const refs = /* @__PURE__ */ new Map();
906
+ const testFiles = await collectFiles(testsRoot, {
907
+ extensions: [".ts", ".tsx", ".js", ".jsx"]
908
+ });
909
+ for (const file of testFiles) {
910
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
911
+ const scIds = extractIds(text, "SC");
912
+ if (scIds.length === 0) {
913
+ continue;
914
+ }
915
+ for (const scId of scIds) {
916
+ const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
917
+ current.add(file);
918
+ refs.set(scId, current);
919
+ }
920
+ }
921
+ return refs;
922
+ }
923
+ function buildScCoverage(scIds, refs) {
924
+ const sortedScIds = toSortedArray(scIds);
925
+ const refsRecord = {};
926
+ const missingIds = [];
927
+ let covered = 0;
928
+ for (const scId of sortedScIds) {
929
+ const files = refs.get(scId);
930
+ const sortedFiles = files ? toSortedArray(files) : [];
931
+ refsRecord[scId] = sortedFiles;
932
+ if (sortedFiles.length === 0) {
933
+ missingIds.push(scId);
934
+ } else {
935
+ covered += 1;
936
+ }
937
+ }
938
+ return {
939
+ total: sortedScIds.length,
940
+ covered,
941
+ missing: missingIds.length,
942
+ missingIds,
943
+ refs: refsRecord
944
+ };
945
+ }
946
+ function toSortedArray(values) {
947
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
948
+ }
713
949
 
714
950
  // src/core/version.ts
715
- var import_promises6 = require("fs/promises");
951
+ var import_promises7 = require("fs/promises");
716
952
  var import_node_path7 = __toESM(require("path"), 1);
717
953
  var import_node_url2 = require("url");
718
954
  async function resolveToolVersion() {
719
- if ("0.3.5".length > 0) {
720
- return "0.3.5";
955
+ if ("0.4.0".length > 0) {
956
+ return "0.4.0";
721
957
  }
722
958
  try {
723
959
  const packagePath = resolvePackageJsonPath();
724
- const raw = await (0, import_promises6.readFile)(packagePath, "utf-8");
960
+ const raw = await (0, import_promises7.readFile)(packagePath, "utf-8");
725
961
  const parsed = JSON.parse(raw);
726
962
  const version = typeof parsed.version === "string" ? parsed.version : "";
727
963
  return version.length > 0 ? version : "unknown";
@@ -736,7 +972,7 @@ function resolvePackageJsonPath() {
736
972
  }
737
973
 
738
974
  // src/core/validators/contracts.ts
739
- var import_promises7 = require("fs/promises");
975
+ var import_promises8 = require("fs/promises");
740
976
  var import_node_path9 = __toESM(require("path"), 1);
741
977
 
742
978
  // src/core/contracts.ts
@@ -814,7 +1050,7 @@ async function validateUiContracts(uiRoot) {
814
1050
  }
815
1051
  const issues = [];
816
1052
  for (const file of files) {
817
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1053
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
818
1054
  const invalidIds = extractInvalidIds(text, [
819
1055
  "SPEC",
820
1056
  "BR",
@@ -843,7 +1079,7 @@ async function validateUiContracts(uiRoot) {
843
1079
  issues.push(
844
1080
  issue(
845
1081
  "QFAI-CONTRACT-001",
846
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error2)})`,
1082
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error2)})`,
847
1083
  "error",
848
1084
  file,
849
1085
  "contracts.ui.parse"
@@ -881,7 +1117,7 @@ async function validateApiContracts(apiRoot) {
881
1117
  }
882
1118
  const issues = [];
883
1119
  for (const file of files) {
884
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1120
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
885
1121
  const invalidIds = extractInvalidIds(text, [
886
1122
  "SPEC",
887
1123
  "BR",
@@ -910,7 +1146,7 @@ async function validateApiContracts(apiRoot) {
910
1146
  issues.push(
911
1147
  issue(
912
1148
  "QFAI-CONTRACT-001",
913
- `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error2)})`,
1149
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error2)})`,
914
1150
  "error",
915
1151
  file,
916
1152
  "contracts.api.parse"
@@ -959,7 +1195,7 @@ async function validateDataContracts(dataRoot) {
959
1195
  }
960
1196
  const issues = [];
961
1197
  for (const file of files) {
962
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1198
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
963
1199
  const invalidIds = extractInvalidIds(text, [
964
1200
  "SPEC",
965
1201
  "BR",
@@ -1005,7 +1241,7 @@ function lintSql(text, file) {
1005
1241
  function hasOpenApi(doc) {
1006
1242
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
1007
1243
  }
1008
- function formatError2(error2) {
1244
+ function formatError3(error2) {
1009
1245
  if (error2 instanceof Error) {
1010
1246
  return error2.message;
1011
1247
  }
@@ -1030,7 +1266,7 @@ function issue(code, message, severity, file, rule, refs) {
1030
1266
  }
1031
1267
 
1032
1268
  // src/core/validators/delta.ts
1033
- var import_promises8 = require("fs/promises");
1269
+ var import_promises9 = require("fs/promises");
1034
1270
  var import_node_path10 = __toESM(require("path"), 1);
1035
1271
  var SECTION_RE = /^##\s+変更区分/m;
1036
1272
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
@@ -1048,7 +1284,7 @@ async function validateDeltas(root, config) {
1048
1284
  const deltaPath = import_node_path10.default.join(pack, "delta.md");
1049
1285
  let text;
1050
1286
  try {
1051
- text = await (0, import_promises8.readFile)(deltaPath, "utf-8");
1287
+ text = await (0, import_promises9.readFile)(deltaPath, "utf-8");
1052
1288
  } catch (error2) {
1053
1289
  if (isMissingFileError2(error2)) {
1054
1290
  issues.push(
@@ -1120,11 +1356,11 @@ function issue2(code, message, severity, file, rule, refs) {
1120
1356
  }
1121
1357
 
1122
1358
  // src/core/validators/ids.ts
1123
- var import_promises10 = require("fs/promises");
1359
+ var import_promises11 = require("fs/promises");
1124
1360
  var import_node_path12 = __toESM(require("path"), 1);
1125
1361
 
1126
1362
  // src/core/contractIndex.ts
1127
- var import_promises9 = require("fs/promises");
1363
+ var import_promises10 = require("fs/promises");
1128
1364
  var import_node_path11 = __toESM(require("path"), 1);
1129
1365
  async function buildContractIndex(root, config) {
1130
1366
  const contractsRoot = resolvePath(root, config, "contractsDir");
@@ -1149,7 +1385,7 @@ async function buildContractIndex(root, config) {
1149
1385
  }
1150
1386
  async function indexUiContracts(files, index) {
1151
1387
  for (const file of files) {
1152
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1388
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1153
1389
  try {
1154
1390
  const doc = parseStructuredContract(file, text);
1155
1391
  extractUiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1161,7 +1397,7 @@ async function indexUiContracts(files, index) {
1161
1397
  }
1162
1398
  async function indexApiContracts(files, index) {
1163
1399
  for (const file of files) {
1164
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1400
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1165
1401
  try {
1166
1402
  const doc = parseStructuredContract(file, text);
1167
1403
  extractApiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1173,7 +1409,7 @@ async function indexApiContracts(files, index) {
1173
1409
  }
1174
1410
  async function indexDataContracts(files, index) {
1175
1411
  for (const file of files) {
1176
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1412
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1177
1413
  extractIds(text, "DATA").forEach((id) => record(index, id, file));
1178
1414
  }
1179
1415
  }
@@ -1302,162 +1538,8 @@ function parseSpec(md, file) {
1302
1538
  return parsed;
1303
1539
  }
1304
1540
 
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
- });
1404
- }
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, []));
1410
- }
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
- }
1419
- }
1420
- }
1421
- }
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
1433
- };
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
- }
1452
- }
1453
- return texts;
1454
- }
1455
- function unique2(values) {
1456
- return Array.from(new Set(values));
1457
- }
1458
-
1459
1541
  // src/core/validators/ids.ts
1460
- var SC_TAG_RE2 = /^SC-\d{4}$/;
1542
+ var SC_TAG_RE3 = /^SC-\d{4}$/;
1461
1543
  async function validateDefinedIds(root, config) {
1462
1544
  const issues = [];
1463
1545
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1491,7 +1573,7 @@ async function validateDefinedIds(root, config) {
1491
1573
  }
1492
1574
  async function collectSpecDefinitionIds(files, out) {
1493
1575
  for (const file of files) {
1494
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1576
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1495
1577
  const parsed = parseSpec(text, file);
1496
1578
  if (parsed.specId) {
1497
1579
  recordId(out, parsed.specId, file);
@@ -1501,14 +1583,14 @@ async function collectSpecDefinitionIds(files, out) {
1501
1583
  }
1502
1584
  async function collectScenarioDefinitionIds(files, out) {
1503
1585
  for (const file of files) {
1504
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1586
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1505
1587
  const { document, errors } = parseScenarioDocument(text, file);
1506
1588
  if (!document || errors.length > 0) {
1507
1589
  continue;
1508
1590
  }
1509
1591
  for (const scenario of document.scenarios) {
1510
1592
  for (const tag of scenario.tags) {
1511
- if (SC_TAG_RE2.test(tag)) {
1593
+ if (SC_TAG_RE3.test(tag)) {
1512
1594
  recordId(out, tag, file);
1513
1595
  }
1514
1596
  }
@@ -1545,11 +1627,11 @@ function issue3(code, message, severity, file, rule, refs) {
1545
1627
  }
1546
1628
 
1547
1629
  // src/core/validators/scenario.ts
1548
- var import_promises11 = require("fs/promises");
1630
+ var import_promises12 = require("fs/promises");
1549
1631
  var GIVEN_PATTERN = /\bGiven\b/;
1550
1632
  var WHEN_PATTERN = /\bWhen\b/;
1551
1633
  var THEN_PATTERN = /\bThen\b/;
1552
- var SC_TAG_RE3 = /^SC-\d{4}$/;
1634
+ var SC_TAG_RE4 = /^SC-\d{4}$/;
1553
1635
  var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1554
1636
  var BR_TAG_RE2 = /^BR-\d{4}$/;
1555
1637
  async function validateScenarios(root, config) {
@@ -1572,7 +1654,7 @@ async function validateScenarios(root, config) {
1572
1654
  for (const entry of entries) {
1573
1655
  let text;
1574
1656
  try {
1575
- text = await (0, import_promises11.readFile)(entry.scenarioPath, "utf-8");
1657
+ text = await (0, import_promises12.readFile)(entry.scenarioPath, "utf-8");
1576
1658
  } catch (error2) {
1577
1659
  if (isMissingFileError3(error2)) {
1578
1660
  issues.push(
@@ -1669,6 +1751,17 @@ function validateScenarioContent(text, file) {
1669
1751
  )
1670
1752
  );
1671
1753
  }
1754
+ if (document.scenarios.length > 1) {
1755
+ issues.push(
1756
+ issue4(
1757
+ "QFAI-SC-011",
1758
+ `Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
1759
+ "error",
1760
+ file,
1761
+ "scenario.single"
1762
+ )
1763
+ );
1764
+ }
1672
1765
  for (const scenario of document.scenarios) {
1673
1766
  if (scenario.tags.length === 0) {
1674
1767
  issues.push(
@@ -1683,7 +1776,7 @@ function validateScenarioContent(text, file) {
1683
1776
  continue;
1684
1777
  }
1685
1778
  const missingTags = [];
1686
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
1779
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
1687
1780
  if (scTags.length === 0) {
1688
1781
  missingTags.push("SC(0\u4EF6)");
1689
1782
  } else if (scTags.length > 1) {
@@ -1758,7 +1851,7 @@ function isMissingFileError3(error2) {
1758
1851
  }
1759
1852
 
1760
1853
  // src/core/validators/spec.ts
1761
- var import_promises12 = require("fs/promises");
1854
+ var import_promises13 = require("fs/promises");
1762
1855
  async function validateSpecs(root, config) {
1763
1856
  const specsRoot = resolvePath(root, config, "specsDir");
1764
1857
  const entries = await collectSpecEntries(specsRoot);
@@ -1779,7 +1872,7 @@ async function validateSpecs(root, config) {
1779
1872
  for (const entry of entries) {
1780
1873
  let text;
1781
1874
  try {
1782
- text = await (0, import_promises12.readFile)(entry.specPath, "utf-8");
1875
+ text = await (0, import_promises13.readFile)(entry.specPath, "utf-8");
1783
1876
  } catch (error2) {
1784
1877
  if (isMissingFileError4(error2)) {
1785
1878
  issues.push(
@@ -1928,8 +2021,8 @@ function isMissingFileError4(error2) {
1928
2021
  }
1929
2022
 
1930
2023
  // src/core/validators/traceability.ts
1931
- var import_promises13 = require("fs/promises");
1932
- var SC_TAG_RE4 = /^SC-\d{4}$/;
2024
+ var import_promises14 = require("fs/promises");
2025
+ var SC_TAG_RE5 = /^SC-\d{4}$/;
1933
2026
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
1934
2027
  var BR_TAG_RE3 = /^BR-\d{4}$/;
1935
2028
  async function validateTraceability(root, config) {
@@ -1950,7 +2043,7 @@ async function validateTraceability(root, config) {
1950
2043
  const contractIndex = await buildContractIndex(root, config);
1951
2044
  const contractIds = contractIndex.ids;
1952
2045
  for (const file of specFiles) {
1953
- const text = await (0, import_promises13.readFile)(file, "utf-8");
2046
+ const text = await (0, import_promises14.readFile)(file, "utf-8");
1954
2047
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1955
2048
  const parsed = parseSpec(text, file);
1956
2049
  if (parsed.specId) {
@@ -1987,7 +2080,7 @@ async function validateTraceability(root, config) {
1987
2080
  }
1988
2081
  }
1989
2082
  for (const file of scenarioFiles) {
1990
- const text = await (0, import_promises13.readFile)(file, "utf-8");
2083
+ const text = await (0, import_promises14.readFile)(file, "utf-8");
1991
2084
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1992
2085
  const { document, errors } = parseScenarioDocument(text, file);
1993
2086
  if (!document || errors.length > 0) {
@@ -2001,7 +2094,7 @@ async function validateTraceability(root, config) {
2001
2094
  }
2002
2095
  const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
2003
2096
  const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
2004
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
2097
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE5.test(tag));
2005
2098
  brTags.forEach((id) => brIdsInScenarios.add(id));
2006
2099
  scTags.forEach((id) => scIdsInScenarios.add(id));
2007
2100
  atom.contractIds.forEach((id) => scenarioContractIds.add(id));
@@ -2129,6 +2222,25 @@ async function validateTraceability(root, config) {
2129
2222
  );
2130
2223
  }
2131
2224
  }
2225
+ if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2226
+ const scTestRefs = await collectScTestReferences(testsRoot);
2227
+ const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2228
+ const refs = scTestRefs.get(id);
2229
+ return !refs || refs.size === 0;
2230
+ });
2231
+ if (scWithoutTests.length > 0) {
2232
+ issues.push(
2233
+ issue6(
2234
+ "QFAI-TRACE-010",
2235
+ `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`,
2236
+ config.validation.traceability.scNoTestSeverity,
2237
+ testsRoot,
2238
+ "traceability.scMustHaveTest",
2239
+ scWithoutTests
2240
+ )
2241
+ );
2242
+ }
2243
+ }
2132
2244
  if (!config.validation.traceability.allowOrphanContracts) {
2133
2245
  if (contractIds.size > 0) {
2134
2246
  const orphanContracts = Array.from(contractIds).filter(
@@ -2177,7 +2289,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2177
2289
  const pattern = buildIdPattern(Array.from(upstreamIds));
2178
2290
  let found = false;
2179
2291
  for (const file of targetFiles) {
2180
- const text = await (0, import_promises13.readFile)(file, "utf-8");
2292
+ const text = await (0, import_promises14.readFile)(file, "utf-8");
2181
2293
  if (pattern.test(text)) {
2182
2294
  found = true;
2183
2295
  break;
@@ -2233,7 +2345,6 @@ async function validateProject(root, configResult) {
2233
2345
  ];
2234
2346
  const toolVersion = await resolveToolVersion();
2235
2347
  return {
2236
- schemaVersion: VALIDATION_SCHEMA_VERSION,
2237
2348
  toolVersion,
2238
2349
  issues,
2239
2350
  counts: countIssues(issues)
@@ -2285,6 +2396,9 @@ async function createReportData(root, validation, configResult) {
2285
2396
  srcRoot,
2286
2397
  testsRoot
2287
2398
  );
2399
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2400
+ const scTestRefs = await collectScTestReferences(testsRoot);
2401
+ const scCoverage = buildScCoverage(scIds, scTestRefs);
2288
2402
  const resolvedValidation = validation ?? await validateProject(root, resolved);
2289
2403
  const version = await resolveToolVersion();
2290
2404
  return {
@@ -2313,7 +2427,8 @@ async function createReportData(root, validation, configResult) {
2313
2427
  },
2314
2428
  traceability: {
2315
2429
  upstreamIdsFound: upstreamIds.size,
2316
- referencedInCodeOrTests: traceability
2430
+ referencedInCodeOrTests: traceability,
2431
+ sc: scCoverage
2317
2432
  },
2318
2433
  issues: resolvedValidation.issues
2319
2434
  };
@@ -2350,6 +2465,32 @@ function formatReportMarkdown(data) {
2350
2465
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2351
2466
  );
2352
2467
  lines.push("");
2468
+ lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2469
+ lines.push(`- total: ${data.traceability.sc.total}`);
2470
+ lines.push(`- covered: ${data.traceability.sc.covered}`);
2471
+ lines.push(`- missing: ${data.traceability.sc.missing}`);
2472
+ if (data.traceability.sc.missingIds.length === 0) {
2473
+ lines.push("- missingIds: (none)");
2474
+ } else {
2475
+ lines.push(`- missingIds: ${data.traceability.sc.missingIds.join(", ")}`);
2476
+ }
2477
+ lines.push("");
2478
+ lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
2479
+ const scRefs = data.traceability.sc.refs;
2480
+ const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
2481
+ if (scIds.length === 0) {
2482
+ lines.push("- (none)");
2483
+ } else {
2484
+ for (const scId of scIds) {
2485
+ const refs = scRefs[scId] ?? [];
2486
+ if (refs.length === 0) {
2487
+ lines.push(`- ${scId}: (none)`);
2488
+ } else {
2489
+ lines.push(`- ${scId}: ${refs.join(", ")}`);
2490
+ }
2491
+ }
2492
+ }
2493
+ lines.push("");
2353
2494
  lines.push("## Hotspots");
2354
2495
  const hotspots = buildHotspots(data.issues);
2355
2496
  if (hotspots.length === 0) {
@@ -2404,25 +2545,25 @@ async function collectIds(files) {
2404
2545
  DATA: /* @__PURE__ */ new Set()
2405
2546
  };
2406
2547
  for (const file of files) {
2407
- const text = await (0, import_promises14.readFile)(file, "utf-8");
2548
+ const text = await (0, import_promises15.readFile)(file, "utf-8");
2408
2549
  for (const prefix of ID_PREFIXES2) {
2409
2550
  const ids = extractIds(text, prefix);
2410
2551
  ids.forEach((id) => result[prefix].add(id));
2411
2552
  }
2412
2553
  }
2413
2554
  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)
2555
+ SPEC: toSortedArray2(result.SPEC),
2556
+ BR: toSortedArray2(result.BR),
2557
+ SC: toSortedArray2(result.SC),
2558
+ UI: toSortedArray2(result.UI),
2559
+ API: toSortedArray2(result.API),
2560
+ DATA: toSortedArray2(result.DATA)
2420
2561
  };
2421
2562
  }
2422
2563
  async function collectUpstreamIds(files) {
2423
2564
  const ids = /* @__PURE__ */ new Set();
2424
2565
  for (const file of files) {
2425
- const text = await (0, import_promises14.readFile)(file, "utf-8");
2566
+ const text = await (0, import_promises15.readFile)(file, "utf-8");
2426
2567
  extractAllIds(text).forEach((id) => ids.add(id));
2427
2568
  }
2428
2569
  return ids;
@@ -2443,7 +2584,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
2443
2584
  }
2444
2585
  const pattern = buildIdPattern2(Array.from(upstreamIds));
2445
2586
  for (const file of targetFiles) {
2446
- const text = await (0, import_promises14.readFile)(file, "utf-8");
2587
+ const text = await (0, import_promises15.readFile)(file, "utf-8");
2447
2588
  if (pattern.test(text)) {
2448
2589
  return true;
2449
2590
  }
@@ -2460,7 +2601,7 @@ function formatIdLine(label, values) {
2460
2601
  }
2461
2602
  return `- ${label}: ${values.join(", ")}`;
2462
2603
  }
2463
- function toSortedArray(values) {
2604
+ function toSortedArray2(values) {
2464
2605
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2465
2606
  }
2466
2607
  function buildHotspots(issues) {
@@ -2518,8 +2659,8 @@ async function runReport(options) {
2518
2659
  const defaultOut = options.format === "json" ? import_node_path14.default.join(outRoot, "report.json") : import_node_path14.default.join(outRoot, "report.md");
2519
2660
  const out = options.outPath ?? defaultOut;
2520
2661
  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}
2662
+ await (0, import_promises16.mkdir)(import_node_path14.default.dirname(outPath), { recursive: true });
2663
+ await (0, import_promises16.writeFile)(outPath, `${output}
2523
2664
  `, "utf-8");
2524
2665
  info(
2525
2666
  `report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
@@ -2527,16 +2668,11 @@ async function runReport(options) {
2527
2668
  info(`wrote report: ${outPath}`);
2528
2669
  }
2529
2670
  async function readValidationResult(inputPath) {
2530
- const raw = await (0, import_promises15.readFile)(inputPath, "utf-8");
2671
+ const raw = await (0, import_promises16.readFile)(inputPath, "utf-8");
2531
2672
  const parsed = JSON.parse(raw);
2532
2673
  if (!isValidationResult(parsed)) {
2533
2674
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
2534
2675
  }
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
2676
  return parsed;
2541
2677
  }
2542
2678
  function isValidationResult(value) {
@@ -2544,9 +2680,6 @@ function isValidationResult(value) {
2544
2680
  return false;
2545
2681
  }
2546
2682
  const record2 = value;
2547
- if (typeof record2.schemaVersion !== "string") {
2548
- return false;
2549
- }
2550
2683
  if (typeof record2.toolVersion !== "string") {
2551
2684
  return false;
2552
2685
  }
@@ -2568,7 +2701,7 @@ function isMissingFileError5(error2) {
2568
2701
  }
2569
2702
 
2570
2703
  // src/cli/commands/validate.ts
2571
- var import_promises16 = require("fs/promises");
2704
+ var import_promises17 = require("fs/promises");
2572
2705
  var import_node_path15 = __toESM(require("path"), 1);
2573
2706
 
2574
2707
  // src/cli/lib/failOn.ts
@@ -2634,8 +2767,8 @@ function emitGitHub(issue7) {
2634
2767
  }
2635
2768
  async function emitJson(result, root, jsonPath) {
2636
2769
  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)}
2770
+ await (0, import_promises17.mkdir)(import_node_path15.default.dirname(abs), { recursive: true });
2771
+ await (0, import_promises17.writeFile)(abs, `${JSON.stringify(result, null, 2)}
2639
2772
  `, "utf-8");
2640
2773
  }
2641
2774