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
package/dist/index.cjs CHANGED
@@ -30,7 +30,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
- VALIDATION_SCHEMA_VERSION: () => VALIDATION_SCHEMA_VERSION,
34
33
  createReportData: () => createReportData,
35
34
  defaultConfig: () => defaultConfig,
36
35
  extractAllIds: () => extractAllIds,
@@ -85,6 +84,8 @@ var defaultConfig = {
85
84
  traceability: {
86
85
  brMustHaveSc: true,
87
86
  scMustTouchContracts: true,
87
+ scMustHaveTest: true,
88
+ scNoTestSeverity: "error",
88
89
  allowOrphanContracts: false,
89
90
  unknownContractIdSeverity: "error"
90
91
  }
@@ -264,6 +265,20 @@ function normalizeValidation(raw, configPath, issues) {
264
265
  configPath,
265
266
  issues
266
267
  ),
268
+ scMustHaveTest: readBoolean(
269
+ traceabilityRaw?.scMustHaveTest,
270
+ base.traceability.scMustHaveTest,
271
+ "validation.traceability.scMustHaveTest",
272
+ configPath,
273
+ issues
274
+ ),
275
+ scNoTestSeverity: readTraceabilitySeverity(
276
+ traceabilityRaw?.scNoTestSeverity,
277
+ base.traceability.scNoTestSeverity,
278
+ "validation.traceability.scNoTestSeverity",
279
+ configPath,
280
+ issues
281
+ ),
267
282
  allowOrphanContracts: readBoolean(
268
283
  traceabilityRaw?.allowOrphanContracts,
269
284
  base.traceability.allowOrphanContracts,
@@ -442,7 +457,7 @@ function isValidId(value, prefix) {
442
457
  }
443
458
 
444
459
  // src/core/report.ts
445
- var import_promises13 = require("fs/promises");
460
+ var import_promises14 = require("fs/promises");
446
461
  var import_node_path10 = __toESM(require("path"), 1);
447
462
 
448
463
  // src/core/discovery.ts
@@ -583,20 +598,240 @@ async function exists2(target) {
583
598
  }
584
599
  }
585
600
 
586
- // src/core/types.ts
587
- var VALIDATION_SCHEMA_VERSION = "0.2";
601
+ // src/core/traceability.ts
602
+ var import_promises5 = require("fs/promises");
603
+
604
+ // src/core/gherkin/parse.ts
605
+ var import_gherkin = require("@cucumber/gherkin");
606
+ var import_node_crypto = require("crypto");
607
+ function parseGherkin(source, uri) {
608
+ const errors = [];
609
+ const uuidFn = () => (0, import_node_crypto.randomUUID)();
610
+ const builder = new import_gherkin.AstBuilder(uuidFn);
611
+ const matcher = new import_gherkin.GherkinClassicTokenMatcher();
612
+ const parser = new import_gherkin.Parser(builder, matcher);
613
+ try {
614
+ const gherkinDocument = parser.parse(source);
615
+ gherkinDocument.uri = uri;
616
+ return { gherkinDocument, errors };
617
+ } catch (error) {
618
+ errors.push(formatError2(error));
619
+ return { gherkinDocument: null, errors };
620
+ }
621
+ }
622
+ function formatError2(error) {
623
+ if (error instanceof Error) {
624
+ return error.message;
625
+ }
626
+ return String(error);
627
+ }
628
+
629
+ // src/core/scenarioModel.ts
630
+ var SPEC_TAG_RE = /^SPEC-\d{4}$/;
631
+ var SC_TAG_RE = /^SC-\d{4}$/;
632
+ var BR_TAG_RE = /^BR-\d{4}$/;
633
+ var UI_TAG_RE = /^UI-\d{4}$/;
634
+ var API_TAG_RE = /^API-\d{4}$/;
635
+ var DATA_TAG_RE = /^DATA-\d{4}$/;
636
+ function parseScenarioDocument(text, uri) {
637
+ const { gherkinDocument, errors } = parseGherkin(text, uri);
638
+ if (!gherkinDocument) {
639
+ return { document: null, errors };
640
+ }
641
+ const feature = gherkinDocument.feature;
642
+ if (!feature) {
643
+ return {
644
+ document: { uri, featureTags: [], scenarios: [] },
645
+ errors
646
+ };
647
+ }
648
+ const featureTags = collectTagNames(feature.tags);
649
+ const scenarios = collectScenarioNodes(feature, featureTags);
650
+ return {
651
+ document: {
652
+ uri,
653
+ featureName: feature.name,
654
+ featureTags,
655
+ scenarios
656
+ },
657
+ errors
658
+ };
659
+ }
660
+ function buildScenarioAtoms(document) {
661
+ return document.scenarios.map((scenario) => {
662
+ const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
663
+ const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
664
+ const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
665
+ const contractIds = /* @__PURE__ */ new Set();
666
+ scenario.tags.forEach((tag) => {
667
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
668
+ contractIds.add(tag);
669
+ }
670
+ });
671
+ for (const step of scenario.steps) {
672
+ for (const text of collectStepTexts(step)) {
673
+ extractIds(text, "UI").forEach((id) => contractIds.add(id));
674
+ extractIds(text, "API").forEach((id) => contractIds.add(id));
675
+ extractIds(text, "DATA").forEach((id) => contractIds.add(id));
676
+ }
677
+ }
678
+ const atom = {
679
+ uri: document.uri,
680
+ featureName: document.featureName ?? "",
681
+ scenarioName: scenario.name,
682
+ kind: scenario.kind,
683
+ brIds,
684
+ contractIds: Array.from(contractIds).sort()
685
+ };
686
+ if (scenario.line !== void 0) {
687
+ atom.line = scenario.line;
688
+ }
689
+ if (specIds.length === 1) {
690
+ const specId = specIds[0];
691
+ if (specId) {
692
+ atom.specId = specId;
693
+ }
694
+ }
695
+ if (scIds.length === 1) {
696
+ const scId = scIds[0];
697
+ if (scId) {
698
+ atom.scId = scId;
699
+ }
700
+ }
701
+ return atom;
702
+ });
703
+ }
704
+ function collectScenarioNodes(feature, featureTags) {
705
+ const scenarios = [];
706
+ for (const child of feature.children) {
707
+ if (child.scenario) {
708
+ scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
709
+ }
710
+ if (child.rule) {
711
+ const ruleTags = collectTagNames(child.rule.tags);
712
+ for (const ruleChild of child.rule.children) {
713
+ if (ruleChild.scenario) {
714
+ scenarios.push(
715
+ buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
716
+ );
717
+ }
718
+ }
719
+ }
720
+ }
721
+ return scenarios;
722
+ }
723
+ function buildScenarioNode(scenario, featureTags, ruleTags) {
724
+ const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
725
+ const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
726
+ return {
727
+ name: scenario.name,
728
+ kind,
729
+ line: scenario.location?.line,
730
+ tags,
731
+ steps: scenario.steps
732
+ };
733
+ }
734
+ function collectTagNames(tags) {
735
+ return tags.map((tag) => tag.name.replace(/^@/, ""));
736
+ }
737
+ function collectStepTexts(step) {
738
+ const texts = [];
739
+ if (step.text) {
740
+ texts.push(step.text);
741
+ }
742
+ if (step.docString?.content) {
743
+ texts.push(step.docString.content);
744
+ }
745
+ if (step.dataTable?.rows) {
746
+ for (const row of step.dataTable.rows) {
747
+ for (const cell of row.cells) {
748
+ texts.push(cell.value);
749
+ }
750
+ }
751
+ }
752
+ return texts;
753
+ }
754
+ function unique2(values) {
755
+ return Array.from(new Set(values));
756
+ }
757
+
758
+ // src/core/traceability.ts
759
+ var SC_TAG_RE2 = /^SC-\d{4}$/;
760
+ async function collectScIdsFromScenarioFiles(scenarioFiles) {
761
+ const scIds = /* @__PURE__ */ new Set();
762
+ for (const file of scenarioFiles) {
763
+ const text = await (0, import_promises5.readFile)(file, "utf-8");
764
+ const { document, errors } = parseScenarioDocument(text, file);
765
+ if (!document || errors.length > 0) {
766
+ continue;
767
+ }
768
+ for (const scenario of document.scenarios) {
769
+ for (const tag of scenario.tags) {
770
+ if (SC_TAG_RE2.test(tag)) {
771
+ scIds.add(tag);
772
+ }
773
+ }
774
+ }
775
+ }
776
+ return scIds;
777
+ }
778
+ async function collectScTestReferences(testsRoot) {
779
+ const refs = /* @__PURE__ */ new Map();
780
+ const testFiles = await collectFiles(testsRoot, {
781
+ extensions: [".ts", ".tsx", ".js", ".jsx"]
782
+ });
783
+ for (const file of testFiles) {
784
+ const text = await (0, import_promises5.readFile)(file, "utf-8");
785
+ const scIds = extractIds(text, "SC");
786
+ if (scIds.length === 0) {
787
+ continue;
788
+ }
789
+ for (const scId of scIds) {
790
+ const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
791
+ current.add(file);
792
+ refs.set(scId, current);
793
+ }
794
+ }
795
+ return refs;
796
+ }
797
+ function buildScCoverage(scIds, refs) {
798
+ const sortedScIds = toSortedArray(scIds);
799
+ const refsRecord = {};
800
+ const missingIds = [];
801
+ let covered = 0;
802
+ for (const scId of sortedScIds) {
803
+ const files = refs.get(scId);
804
+ const sortedFiles = files ? toSortedArray(files) : [];
805
+ refsRecord[scId] = sortedFiles;
806
+ if (sortedFiles.length === 0) {
807
+ missingIds.push(scId);
808
+ } else {
809
+ covered += 1;
810
+ }
811
+ }
812
+ return {
813
+ total: sortedScIds.length,
814
+ covered,
815
+ missing: missingIds.length,
816
+ missingIds,
817
+ refs: refsRecord
818
+ };
819
+ }
820
+ function toSortedArray(values) {
821
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
822
+ }
588
823
 
589
824
  // src/core/version.ts
590
- var import_promises5 = require("fs/promises");
825
+ var import_promises6 = require("fs/promises");
591
826
  var import_node_path4 = __toESM(require("path"), 1);
592
827
  var import_node_url = require("url");
593
828
  async function resolveToolVersion() {
594
- if ("0.3.5".length > 0) {
595
- return "0.3.5";
829
+ if ("0.4.0".length > 0) {
830
+ return "0.4.0";
596
831
  }
597
832
  try {
598
833
  const packagePath = resolvePackageJsonPath();
599
- const raw = await (0, import_promises5.readFile)(packagePath, "utf-8");
834
+ const raw = await (0, import_promises6.readFile)(packagePath, "utf-8");
600
835
  const parsed = JSON.parse(raw);
601
836
  const version = typeof parsed.version === "string" ? parsed.version : "";
602
837
  return version.length > 0 ? version : "unknown";
@@ -611,7 +846,7 @@ function resolvePackageJsonPath() {
611
846
  }
612
847
 
613
848
  // src/core/validators/contracts.ts
614
- var import_promises6 = require("fs/promises");
849
+ var import_promises7 = require("fs/promises");
615
850
  var import_node_path6 = __toESM(require("path"), 1);
616
851
 
617
852
  // src/core/contracts.ts
@@ -689,7 +924,7 @@ async function validateUiContracts(uiRoot) {
689
924
  }
690
925
  const issues = [];
691
926
  for (const file of files) {
692
- const text = await (0, import_promises6.readFile)(file, "utf-8");
927
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
693
928
  const invalidIds = extractInvalidIds(text, [
694
929
  "SPEC",
695
930
  "BR",
@@ -718,7 +953,7 @@ async function validateUiContracts(uiRoot) {
718
953
  issues.push(
719
954
  issue(
720
955
  "QFAI-CONTRACT-001",
721
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error)})`,
956
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error)})`,
722
957
  "error",
723
958
  file,
724
959
  "contracts.ui.parse"
@@ -756,7 +991,7 @@ async function validateApiContracts(apiRoot) {
756
991
  }
757
992
  const issues = [];
758
993
  for (const file of files) {
759
- const text = await (0, import_promises6.readFile)(file, "utf-8");
994
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
760
995
  const invalidIds = extractInvalidIds(text, [
761
996
  "SPEC",
762
997
  "BR",
@@ -785,7 +1020,7 @@ async function validateApiContracts(apiRoot) {
785
1020
  issues.push(
786
1021
  issue(
787
1022
  "QFAI-CONTRACT-001",
788
- `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error)})`,
1023
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error)})`,
789
1024
  "error",
790
1025
  file,
791
1026
  "contracts.api.parse"
@@ -834,7 +1069,7 @@ async function validateDataContracts(dataRoot) {
834
1069
  }
835
1070
  const issues = [];
836
1071
  for (const file of files) {
837
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1072
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
838
1073
  const invalidIds = extractInvalidIds(text, [
839
1074
  "SPEC",
840
1075
  "BR",
@@ -880,7 +1115,7 @@ function lintSql(text, file) {
880
1115
  function hasOpenApi(doc) {
881
1116
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
882
1117
  }
883
- function formatError2(error) {
1118
+ function formatError3(error) {
884
1119
  if (error instanceof Error) {
885
1120
  return error.message;
886
1121
  }
@@ -905,7 +1140,7 @@ function issue(code, message, severity, file, rule, refs) {
905
1140
  }
906
1141
 
907
1142
  // src/core/validators/delta.ts
908
- var import_promises7 = require("fs/promises");
1143
+ var import_promises8 = require("fs/promises");
909
1144
  var import_node_path7 = __toESM(require("path"), 1);
910
1145
  var SECTION_RE = /^##\s+変更区分/m;
911
1146
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
@@ -923,7 +1158,7 @@ async function validateDeltas(root, config) {
923
1158
  const deltaPath = import_node_path7.default.join(pack, "delta.md");
924
1159
  let text;
925
1160
  try {
926
- text = await (0, import_promises7.readFile)(deltaPath, "utf-8");
1161
+ text = await (0, import_promises8.readFile)(deltaPath, "utf-8");
927
1162
  } catch (error) {
928
1163
  if (isMissingFileError2(error)) {
929
1164
  issues.push(
@@ -995,11 +1230,11 @@ function issue2(code, message, severity, file, rule, refs) {
995
1230
  }
996
1231
 
997
1232
  // src/core/validators/ids.ts
998
- var import_promises9 = require("fs/promises");
1233
+ var import_promises10 = require("fs/promises");
999
1234
  var import_node_path9 = __toESM(require("path"), 1);
1000
1235
 
1001
1236
  // src/core/contractIndex.ts
1002
- var import_promises8 = require("fs/promises");
1237
+ var import_promises9 = require("fs/promises");
1003
1238
  var import_node_path8 = __toESM(require("path"), 1);
1004
1239
  async function buildContractIndex(root, config) {
1005
1240
  const contractsRoot = resolvePath(root, config, "contractsDir");
@@ -1024,7 +1259,7 @@ async function buildContractIndex(root, config) {
1024
1259
  }
1025
1260
  async function indexUiContracts(files, index) {
1026
1261
  for (const file of files) {
1027
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1262
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1028
1263
  try {
1029
1264
  const doc = parseStructuredContract(file, text);
1030
1265
  extractUiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1036,7 +1271,7 @@ async function indexUiContracts(files, index) {
1036
1271
  }
1037
1272
  async function indexApiContracts(files, index) {
1038
1273
  for (const file of files) {
1039
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1274
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1040
1275
  try {
1041
1276
  const doc = parseStructuredContract(file, text);
1042
1277
  extractApiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1048,7 +1283,7 @@ async function indexApiContracts(files, index) {
1048
1283
  }
1049
1284
  async function indexDataContracts(files, index) {
1050
1285
  for (const file of files) {
1051
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1286
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1052
1287
  extractIds(text, "DATA").forEach((id) => record(index, id, file));
1053
1288
  }
1054
1289
  }
@@ -1177,162 +1412,8 @@ function parseSpec(md, file) {
1177
1412
  return parsed;
1178
1413
  }
1179
1414
 
1180
- // src/core/gherkin/parse.ts
1181
- var import_gherkin = require("@cucumber/gherkin");
1182
- var import_node_crypto = require("crypto");
1183
- function parseGherkin(source, uri) {
1184
- const errors = [];
1185
- const uuidFn = () => (0, import_node_crypto.randomUUID)();
1186
- const builder = new import_gherkin.AstBuilder(uuidFn);
1187
- const matcher = new import_gherkin.GherkinClassicTokenMatcher();
1188
- const parser = new import_gherkin.Parser(builder, matcher);
1189
- try {
1190
- const gherkinDocument = parser.parse(source);
1191
- gherkinDocument.uri = uri;
1192
- return { gherkinDocument, errors };
1193
- } catch (error) {
1194
- errors.push(formatError3(error));
1195
- return { gherkinDocument: null, errors };
1196
- }
1197
- }
1198
- function formatError3(error) {
1199
- if (error instanceof Error) {
1200
- return error.message;
1201
- }
1202
- return String(error);
1203
- }
1204
-
1205
- // src/core/scenarioModel.ts
1206
- var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1207
- var SC_TAG_RE = /^SC-\d{4}$/;
1208
- var BR_TAG_RE = /^BR-\d{4}$/;
1209
- var UI_TAG_RE = /^UI-\d{4}$/;
1210
- var API_TAG_RE = /^API-\d{4}$/;
1211
- var DATA_TAG_RE = /^DATA-\d{4}$/;
1212
- function parseScenarioDocument(text, uri) {
1213
- const { gherkinDocument, errors } = parseGherkin(text, uri);
1214
- if (!gherkinDocument) {
1215
- return { document: null, errors };
1216
- }
1217
- const feature = gherkinDocument.feature;
1218
- if (!feature) {
1219
- return {
1220
- document: { uri, featureTags: [], scenarios: [] },
1221
- errors
1222
- };
1223
- }
1224
- const featureTags = collectTagNames(feature.tags);
1225
- const scenarios = collectScenarioNodes(feature, featureTags);
1226
- return {
1227
- document: {
1228
- uri,
1229
- featureName: feature.name,
1230
- featureTags,
1231
- scenarios
1232
- },
1233
- errors
1234
- };
1235
- }
1236
- function buildScenarioAtoms(document) {
1237
- return document.scenarios.map((scenario) => {
1238
- const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
1239
- const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
1240
- const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1241
- const contractIds = /* @__PURE__ */ new Set();
1242
- scenario.tags.forEach((tag) => {
1243
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1244
- contractIds.add(tag);
1245
- }
1246
- });
1247
- for (const step of scenario.steps) {
1248
- for (const text of collectStepTexts(step)) {
1249
- extractIds(text, "UI").forEach((id) => contractIds.add(id));
1250
- extractIds(text, "API").forEach((id) => contractIds.add(id));
1251
- extractIds(text, "DATA").forEach((id) => contractIds.add(id));
1252
- }
1253
- }
1254
- const atom = {
1255
- uri: document.uri,
1256
- featureName: document.featureName ?? "",
1257
- scenarioName: scenario.name,
1258
- kind: scenario.kind,
1259
- brIds,
1260
- contractIds: Array.from(contractIds).sort()
1261
- };
1262
- if (scenario.line !== void 0) {
1263
- atom.line = scenario.line;
1264
- }
1265
- if (specIds.length === 1) {
1266
- const specId = specIds[0];
1267
- if (specId) {
1268
- atom.specId = specId;
1269
- }
1270
- }
1271
- if (scIds.length === 1) {
1272
- const scId = scIds[0];
1273
- if (scId) {
1274
- atom.scId = scId;
1275
- }
1276
- }
1277
- return atom;
1278
- });
1279
- }
1280
- function collectScenarioNodes(feature, featureTags) {
1281
- const scenarios = [];
1282
- for (const child of feature.children) {
1283
- if (child.scenario) {
1284
- scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
1285
- }
1286
- if (child.rule) {
1287
- const ruleTags = collectTagNames(child.rule.tags);
1288
- for (const ruleChild of child.rule.children) {
1289
- if (ruleChild.scenario) {
1290
- scenarios.push(
1291
- buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
1292
- );
1293
- }
1294
- }
1295
- }
1296
- }
1297
- return scenarios;
1298
- }
1299
- function buildScenarioNode(scenario, featureTags, ruleTags) {
1300
- const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
1301
- const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
1302
- return {
1303
- name: scenario.name,
1304
- kind,
1305
- line: scenario.location?.line,
1306
- tags,
1307
- steps: scenario.steps
1308
- };
1309
- }
1310
- function collectTagNames(tags) {
1311
- return tags.map((tag) => tag.name.replace(/^@/, ""));
1312
- }
1313
- function collectStepTexts(step) {
1314
- const texts = [];
1315
- if (step.text) {
1316
- texts.push(step.text);
1317
- }
1318
- if (step.docString?.content) {
1319
- texts.push(step.docString.content);
1320
- }
1321
- if (step.dataTable?.rows) {
1322
- for (const row of step.dataTable.rows) {
1323
- for (const cell of row.cells) {
1324
- texts.push(cell.value);
1325
- }
1326
- }
1327
- }
1328
- return texts;
1329
- }
1330
- function unique2(values) {
1331
- return Array.from(new Set(values));
1332
- }
1333
-
1334
1415
  // src/core/validators/ids.ts
1335
- var SC_TAG_RE2 = /^SC-\d{4}$/;
1416
+ var SC_TAG_RE3 = /^SC-\d{4}$/;
1336
1417
  async function validateDefinedIds(root, config) {
1337
1418
  const issues = [];
1338
1419
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1366,7 +1447,7 @@ async function validateDefinedIds(root, config) {
1366
1447
  }
1367
1448
  async function collectSpecDefinitionIds(files, out) {
1368
1449
  for (const file of files) {
1369
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1450
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1370
1451
  const parsed = parseSpec(text, file);
1371
1452
  if (parsed.specId) {
1372
1453
  recordId(out, parsed.specId, file);
@@ -1376,14 +1457,14 @@ async function collectSpecDefinitionIds(files, out) {
1376
1457
  }
1377
1458
  async function collectScenarioDefinitionIds(files, out) {
1378
1459
  for (const file of files) {
1379
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1460
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1380
1461
  const { document, errors } = parseScenarioDocument(text, file);
1381
1462
  if (!document || errors.length > 0) {
1382
1463
  continue;
1383
1464
  }
1384
1465
  for (const scenario of document.scenarios) {
1385
1466
  for (const tag of scenario.tags) {
1386
- if (SC_TAG_RE2.test(tag)) {
1467
+ if (SC_TAG_RE3.test(tag)) {
1387
1468
  recordId(out, tag, file);
1388
1469
  }
1389
1470
  }
@@ -1420,11 +1501,11 @@ function issue3(code, message, severity, file, rule, refs) {
1420
1501
  }
1421
1502
 
1422
1503
  // src/core/validators/scenario.ts
1423
- var import_promises10 = require("fs/promises");
1504
+ var import_promises11 = require("fs/promises");
1424
1505
  var GIVEN_PATTERN = /\bGiven\b/;
1425
1506
  var WHEN_PATTERN = /\bWhen\b/;
1426
1507
  var THEN_PATTERN = /\bThen\b/;
1427
- var SC_TAG_RE3 = /^SC-\d{4}$/;
1508
+ var SC_TAG_RE4 = /^SC-\d{4}$/;
1428
1509
  var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1429
1510
  var BR_TAG_RE2 = /^BR-\d{4}$/;
1430
1511
  async function validateScenarios(root, config) {
@@ -1447,7 +1528,7 @@ async function validateScenarios(root, config) {
1447
1528
  for (const entry of entries) {
1448
1529
  let text;
1449
1530
  try {
1450
- text = await (0, import_promises10.readFile)(entry.scenarioPath, "utf-8");
1531
+ text = await (0, import_promises11.readFile)(entry.scenarioPath, "utf-8");
1451
1532
  } catch (error) {
1452
1533
  if (isMissingFileError3(error)) {
1453
1534
  issues.push(
@@ -1544,6 +1625,17 @@ function validateScenarioContent(text, file) {
1544
1625
  )
1545
1626
  );
1546
1627
  }
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
+ }
1547
1639
  for (const scenario of document.scenarios) {
1548
1640
  if (scenario.tags.length === 0) {
1549
1641
  issues.push(
@@ -1558,7 +1650,7 @@ function validateScenarioContent(text, file) {
1558
1650
  continue;
1559
1651
  }
1560
1652
  const missingTags = [];
1561
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
1653
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
1562
1654
  if (scTags.length === 0) {
1563
1655
  missingTags.push("SC(0\u4EF6)");
1564
1656
  } else if (scTags.length > 1) {
@@ -1633,7 +1725,7 @@ function isMissingFileError3(error) {
1633
1725
  }
1634
1726
 
1635
1727
  // src/core/validators/spec.ts
1636
- var import_promises11 = require("fs/promises");
1728
+ var import_promises12 = require("fs/promises");
1637
1729
  async function validateSpecs(root, config) {
1638
1730
  const specsRoot = resolvePath(root, config, "specsDir");
1639
1731
  const entries = await collectSpecEntries(specsRoot);
@@ -1654,7 +1746,7 @@ async function validateSpecs(root, config) {
1654
1746
  for (const entry of entries) {
1655
1747
  let text;
1656
1748
  try {
1657
- text = await (0, import_promises11.readFile)(entry.specPath, "utf-8");
1749
+ text = await (0, import_promises12.readFile)(entry.specPath, "utf-8");
1658
1750
  } catch (error) {
1659
1751
  if (isMissingFileError4(error)) {
1660
1752
  issues.push(
@@ -1803,8 +1895,8 @@ function isMissingFileError4(error) {
1803
1895
  }
1804
1896
 
1805
1897
  // src/core/validators/traceability.ts
1806
- var import_promises12 = require("fs/promises");
1807
- var SC_TAG_RE4 = /^SC-\d{4}$/;
1898
+ var import_promises13 = require("fs/promises");
1899
+ var SC_TAG_RE5 = /^SC-\d{4}$/;
1808
1900
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
1809
1901
  var BR_TAG_RE3 = /^BR-\d{4}$/;
1810
1902
  async function validateTraceability(root, config) {
@@ -1825,7 +1917,7 @@ async function validateTraceability(root, config) {
1825
1917
  const contractIndex = await buildContractIndex(root, config);
1826
1918
  const contractIds = contractIndex.ids;
1827
1919
  for (const file of specFiles) {
1828
- const text = await (0, import_promises12.readFile)(file, "utf-8");
1920
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
1829
1921
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1830
1922
  const parsed = parseSpec(text, file);
1831
1923
  if (parsed.specId) {
@@ -1862,7 +1954,7 @@ async function validateTraceability(root, config) {
1862
1954
  }
1863
1955
  }
1864
1956
  for (const file of scenarioFiles) {
1865
- const text = await (0, import_promises12.readFile)(file, "utf-8");
1957
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
1866
1958
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1867
1959
  const { document, errors } = parseScenarioDocument(text, file);
1868
1960
  if (!document || errors.length > 0) {
@@ -1876,7 +1968,7 @@ async function validateTraceability(root, config) {
1876
1968
  }
1877
1969
  const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
1878
1970
  const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
1879
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
1971
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE5.test(tag));
1880
1972
  brTags.forEach((id) => brIdsInScenarios.add(id));
1881
1973
  scTags.forEach((id) => scIdsInScenarios.add(id));
1882
1974
  atom.contractIds.forEach((id) => scenarioContractIds.add(id));
@@ -2004,6 +2096,25 @@ async function validateTraceability(root, config) {
2004
2096
  );
2005
2097
  }
2006
2098
  }
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) {
2106
+ issues.push(
2107
+ 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,
2111
+ testsRoot,
2112
+ "traceability.scMustHaveTest",
2113
+ scWithoutTests
2114
+ )
2115
+ );
2116
+ }
2117
+ }
2007
2118
  if (!config.validation.traceability.allowOrphanContracts) {
2008
2119
  if (contractIds.size > 0) {
2009
2120
  const orphanContracts = Array.from(contractIds).filter(
@@ -2052,7 +2163,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2052
2163
  const pattern = buildIdPattern(Array.from(upstreamIds));
2053
2164
  let found = false;
2054
2165
  for (const file of targetFiles) {
2055
- const text = await (0, import_promises12.readFile)(file, "utf-8");
2166
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
2056
2167
  if (pattern.test(text)) {
2057
2168
  found = true;
2058
2169
  break;
@@ -2108,7 +2219,6 @@ async function validateProject(root, configResult) {
2108
2219
  ];
2109
2220
  const toolVersion = await resolveToolVersion();
2110
2221
  return {
2111
- schemaVersion: VALIDATION_SCHEMA_VERSION,
2112
2222
  toolVersion,
2113
2223
  issues,
2114
2224
  counts: countIssues(issues)
@@ -2160,6 +2270,9 @@ async function createReportData(root, validation, configResult) {
2160
2270
  srcRoot,
2161
2271
  testsRoot
2162
2272
  );
2273
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2274
+ const scTestRefs = await collectScTestReferences(testsRoot);
2275
+ const scCoverage = buildScCoverage(scIds, scTestRefs);
2163
2276
  const resolvedValidation = validation ?? await validateProject(root, resolved);
2164
2277
  const version = await resolveToolVersion();
2165
2278
  return {
@@ -2188,7 +2301,8 @@ async function createReportData(root, validation, configResult) {
2188
2301
  },
2189
2302
  traceability: {
2190
2303
  upstreamIdsFound: upstreamIds.size,
2191
- referencedInCodeOrTests: traceability
2304
+ referencedInCodeOrTests: traceability,
2305
+ sc: scCoverage
2192
2306
  },
2193
2307
  issues: resolvedValidation.issues
2194
2308
  };
@@ -2225,6 +2339,32 @@ function formatReportMarkdown(data) {
2225
2339
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2226
2340
  );
2227
2341
  lines.push("");
2342
+ lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2343
+ lines.push(`- total: ${data.traceability.sc.total}`);
2344
+ lines.push(`- covered: ${data.traceability.sc.covered}`);
2345
+ lines.push(`- missing: ${data.traceability.sc.missing}`);
2346
+ if (data.traceability.sc.missingIds.length === 0) {
2347
+ lines.push("- missingIds: (none)");
2348
+ } else {
2349
+ lines.push(`- missingIds: ${data.traceability.sc.missingIds.join(", ")}`);
2350
+ }
2351
+ lines.push("");
2352
+ lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
2353
+ const scRefs = data.traceability.sc.refs;
2354
+ const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
2355
+ if (scIds.length === 0) {
2356
+ lines.push("- (none)");
2357
+ } else {
2358
+ for (const scId of scIds) {
2359
+ const refs = scRefs[scId] ?? [];
2360
+ if (refs.length === 0) {
2361
+ lines.push(`- ${scId}: (none)`);
2362
+ } else {
2363
+ lines.push(`- ${scId}: ${refs.join(", ")}`);
2364
+ }
2365
+ }
2366
+ }
2367
+ lines.push("");
2228
2368
  lines.push("## Hotspots");
2229
2369
  const hotspots = buildHotspots(data.issues);
2230
2370
  if (hotspots.length === 0) {
@@ -2279,25 +2419,25 @@ async function collectIds(files) {
2279
2419
  DATA: /* @__PURE__ */ new Set()
2280
2420
  };
2281
2421
  for (const file of files) {
2282
- const text = await (0, import_promises13.readFile)(file, "utf-8");
2422
+ const text = await (0, import_promises14.readFile)(file, "utf-8");
2283
2423
  for (const prefix of ID_PREFIXES2) {
2284
2424
  const ids = extractIds(text, prefix);
2285
2425
  ids.forEach((id) => result[prefix].add(id));
2286
2426
  }
2287
2427
  }
2288
2428
  return {
2289
- SPEC: toSortedArray(result.SPEC),
2290
- BR: toSortedArray(result.BR),
2291
- SC: toSortedArray(result.SC),
2292
- UI: toSortedArray(result.UI),
2293
- API: toSortedArray(result.API),
2294
- DATA: toSortedArray(result.DATA)
2429
+ SPEC: toSortedArray2(result.SPEC),
2430
+ BR: toSortedArray2(result.BR),
2431
+ SC: toSortedArray2(result.SC),
2432
+ UI: toSortedArray2(result.UI),
2433
+ API: toSortedArray2(result.API),
2434
+ DATA: toSortedArray2(result.DATA)
2295
2435
  };
2296
2436
  }
2297
2437
  async function collectUpstreamIds(files) {
2298
2438
  const ids = /* @__PURE__ */ new Set();
2299
2439
  for (const file of files) {
2300
- const text = await (0, import_promises13.readFile)(file, "utf-8");
2440
+ const text = await (0, import_promises14.readFile)(file, "utf-8");
2301
2441
  extractAllIds(text).forEach((id) => ids.add(id));
2302
2442
  }
2303
2443
  return ids;
@@ -2318,7 +2458,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
2318
2458
  }
2319
2459
  const pattern = buildIdPattern2(Array.from(upstreamIds));
2320
2460
  for (const file of targetFiles) {
2321
- const text = await (0, import_promises13.readFile)(file, "utf-8");
2461
+ const text = await (0, import_promises14.readFile)(file, "utf-8");
2322
2462
  if (pattern.test(text)) {
2323
2463
  return true;
2324
2464
  }
@@ -2335,7 +2475,7 @@ function formatIdLine(label, values) {
2335
2475
  }
2336
2476
  return `- ${label}: ${values.join(", ")}`;
2337
2477
  }
2338
- function toSortedArray(values) {
2478
+ function toSortedArray2(values) {
2339
2479
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2340
2480
  }
2341
2481
  function buildHotspots(issues) {
@@ -2361,7 +2501,6 @@ function buildHotspots(issues) {
2361
2501
  }
2362
2502
  // Annotate the CommonJS export names for ESM import in node:
2363
2503
  0 && (module.exports = {
2364
- VALIDATION_SCHEMA_VERSION,
2365
2504
  createReportData,
2366
2505
  defaultConfig,
2367
2506
  extractAllIds,