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
@@ -154,7 +154,7 @@ function report(copied, skipped, dryRun, label) {
154
154
  }
155
155
 
156
156
  // src/cli/commands/report.ts
157
- import { mkdir as mkdir2, readFile as readFile11, writeFile } from "fs/promises";
157
+ import { mkdir as mkdir2, readFile as readFile12, writeFile } from "fs/promises";
158
158
  import path14 from "path";
159
159
 
160
160
  // src/core/config.ts
@@ -187,6 +187,8 @@ var defaultConfig = {
187
187
  traceability: {
188
188
  brMustHaveSc: true,
189
189
  scMustTouchContracts: true,
190
+ scMustHaveTest: true,
191
+ scNoTestSeverity: "error",
190
192
  allowOrphanContracts: false,
191
193
  unknownContractIdSeverity: "error"
192
194
  }
@@ -366,6 +368,20 @@ function normalizeValidation(raw, configPath, issues) {
366
368
  configPath,
367
369
  issues
368
370
  ),
371
+ scMustHaveTest: readBoolean(
372
+ traceabilityRaw?.scMustHaveTest,
373
+ base.traceability.scMustHaveTest,
374
+ "validation.traceability.scMustHaveTest",
375
+ configPath,
376
+ issues
377
+ ),
378
+ scNoTestSeverity: readTraceabilitySeverity(
379
+ traceabilityRaw?.scNoTestSeverity,
380
+ base.traceability.scNoTestSeverity,
381
+ "validation.traceability.scNoTestSeverity",
382
+ configPath,
383
+ issues
384
+ ),
369
385
  allowOrphanContracts: readBoolean(
370
386
  traceabilityRaw?.allowOrphanContracts,
371
387
  base.traceability.allowOrphanContracts,
@@ -491,7 +507,7 @@ function isRecord(value) {
491
507
  }
492
508
 
493
509
  // src/core/report.ts
494
- import { readFile as readFile10 } from "fs/promises";
510
+ import { readFile as readFile11 } from "fs/promises";
495
511
  import path13 from "path";
496
512
 
497
513
  // src/core/discovery.ts
@@ -685,20 +701,244 @@ function isValidId(value, prefix) {
685
701
  return strict.test(value);
686
702
  }
687
703
 
688
- // src/core/types.ts
689
- var VALIDATION_SCHEMA_VERSION = "0.2";
704
+ // src/core/traceability.ts
705
+ import { readFile as readFile2 } from "fs/promises";
706
+
707
+ // src/core/gherkin/parse.ts
708
+ import {
709
+ AstBuilder,
710
+ GherkinClassicTokenMatcher,
711
+ Parser
712
+ } from "@cucumber/gherkin";
713
+ import { randomUUID } from "crypto";
714
+ function parseGherkin(source, uri) {
715
+ const errors = [];
716
+ const uuidFn = () => randomUUID();
717
+ const builder = new AstBuilder(uuidFn);
718
+ const matcher = new GherkinClassicTokenMatcher();
719
+ const parser = new Parser(builder, matcher);
720
+ try {
721
+ const gherkinDocument = parser.parse(source);
722
+ gherkinDocument.uri = uri;
723
+ return { gherkinDocument, errors };
724
+ } catch (error2) {
725
+ errors.push(formatError2(error2));
726
+ return { gherkinDocument: null, errors };
727
+ }
728
+ }
729
+ function formatError2(error2) {
730
+ if (error2 instanceof Error) {
731
+ return error2.message;
732
+ }
733
+ return String(error2);
734
+ }
735
+
736
+ // src/core/scenarioModel.ts
737
+ var SPEC_TAG_RE = /^SPEC-\d{4}$/;
738
+ var SC_TAG_RE = /^SC-\d{4}$/;
739
+ var BR_TAG_RE = /^BR-\d{4}$/;
740
+ var UI_TAG_RE = /^UI-\d{4}$/;
741
+ var API_TAG_RE = /^API-\d{4}$/;
742
+ var DATA_TAG_RE = /^DATA-\d{4}$/;
743
+ function parseScenarioDocument(text, uri) {
744
+ const { gherkinDocument, errors } = parseGherkin(text, uri);
745
+ if (!gherkinDocument) {
746
+ return { document: null, errors };
747
+ }
748
+ const feature = gherkinDocument.feature;
749
+ if (!feature) {
750
+ return {
751
+ document: { uri, featureTags: [], scenarios: [] },
752
+ errors
753
+ };
754
+ }
755
+ const featureTags = collectTagNames(feature.tags);
756
+ const scenarios = collectScenarioNodes(feature, featureTags);
757
+ return {
758
+ document: {
759
+ uri,
760
+ featureName: feature.name,
761
+ featureTags,
762
+ scenarios
763
+ },
764
+ errors
765
+ };
766
+ }
767
+ function buildScenarioAtoms(document) {
768
+ return document.scenarios.map((scenario) => {
769
+ const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
770
+ const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
771
+ const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
772
+ const contractIds = /* @__PURE__ */ new Set();
773
+ scenario.tags.forEach((tag) => {
774
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
775
+ contractIds.add(tag);
776
+ }
777
+ });
778
+ for (const step of scenario.steps) {
779
+ for (const text of collectStepTexts(step)) {
780
+ extractIds(text, "UI").forEach((id) => contractIds.add(id));
781
+ extractIds(text, "API").forEach((id) => contractIds.add(id));
782
+ extractIds(text, "DATA").forEach((id) => contractIds.add(id));
783
+ }
784
+ }
785
+ const atom = {
786
+ uri: document.uri,
787
+ featureName: document.featureName ?? "",
788
+ scenarioName: scenario.name,
789
+ kind: scenario.kind,
790
+ brIds,
791
+ contractIds: Array.from(contractIds).sort()
792
+ };
793
+ if (scenario.line !== void 0) {
794
+ atom.line = scenario.line;
795
+ }
796
+ if (specIds.length === 1) {
797
+ const specId = specIds[0];
798
+ if (specId) {
799
+ atom.specId = specId;
800
+ }
801
+ }
802
+ if (scIds.length === 1) {
803
+ const scId = scIds[0];
804
+ if (scId) {
805
+ atom.scId = scId;
806
+ }
807
+ }
808
+ return atom;
809
+ });
810
+ }
811
+ function collectScenarioNodes(feature, featureTags) {
812
+ const scenarios = [];
813
+ for (const child of feature.children) {
814
+ if (child.scenario) {
815
+ scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
816
+ }
817
+ if (child.rule) {
818
+ const ruleTags = collectTagNames(child.rule.tags);
819
+ for (const ruleChild of child.rule.children) {
820
+ if (ruleChild.scenario) {
821
+ scenarios.push(
822
+ buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
823
+ );
824
+ }
825
+ }
826
+ }
827
+ }
828
+ return scenarios;
829
+ }
830
+ function buildScenarioNode(scenario, featureTags, ruleTags) {
831
+ const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
832
+ const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
833
+ return {
834
+ name: scenario.name,
835
+ kind,
836
+ line: scenario.location?.line,
837
+ tags,
838
+ steps: scenario.steps
839
+ };
840
+ }
841
+ function collectTagNames(tags) {
842
+ return tags.map((tag) => tag.name.replace(/^@/, ""));
843
+ }
844
+ function collectStepTexts(step) {
845
+ const texts = [];
846
+ if (step.text) {
847
+ texts.push(step.text);
848
+ }
849
+ if (step.docString?.content) {
850
+ texts.push(step.docString.content);
851
+ }
852
+ if (step.dataTable?.rows) {
853
+ for (const row of step.dataTable.rows) {
854
+ for (const cell of row.cells) {
855
+ texts.push(cell.value);
856
+ }
857
+ }
858
+ }
859
+ return texts;
860
+ }
861
+ function unique2(values) {
862
+ return Array.from(new Set(values));
863
+ }
864
+
865
+ // src/core/traceability.ts
866
+ var SC_TAG_RE2 = /^SC-\d{4}$/;
867
+ async function collectScIdsFromScenarioFiles(scenarioFiles) {
868
+ const scIds = /* @__PURE__ */ new Set();
869
+ for (const file of scenarioFiles) {
870
+ const text = await readFile2(file, "utf-8");
871
+ const { document, errors } = parseScenarioDocument(text, file);
872
+ if (!document || errors.length > 0) {
873
+ continue;
874
+ }
875
+ for (const scenario of document.scenarios) {
876
+ for (const tag of scenario.tags) {
877
+ if (SC_TAG_RE2.test(tag)) {
878
+ scIds.add(tag);
879
+ }
880
+ }
881
+ }
882
+ }
883
+ return scIds;
884
+ }
885
+ async function collectScTestReferences(testsRoot) {
886
+ const refs = /* @__PURE__ */ new Map();
887
+ const testFiles = await collectFiles(testsRoot, {
888
+ extensions: [".ts", ".tsx", ".js", ".jsx"]
889
+ });
890
+ for (const file of testFiles) {
891
+ const text = await readFile2(file, "utf-8");
892
+ const scIds = extractIds(text, "SC");
893
+ if (scIds.length === 0) {
894
+ continue;
895
+ }
896
+ for (const scId of scIds) {
897
+ const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
898
+ current.add(file);
899
+ refs.set(scId, current);
900
+ }
901
+ }
902
+ return refs;
903
+ }
904
+ function buildScCoverage(scIds, refs) {
905
+ const sortedScIds = toSortedArray(scIds);
906
+ const refsRecord = {};
907
+ const missingIds = [];
908
+ let covered = 0;
909
+ for (const scId of sortedScIds) {
910
+ const files = refs.get(scId);
911
+ const sortedFiles = files ? toSortedArray(files) : [];
912
+ refsRecord[scId] = sortedFiles;
913
+ if (sortedFiles.length === 0) {
914
+ missingIds.push(scId);
915
+ } else {
916
+ covered += 1;
917
+ }
918
+ }
919
+ return {
920
+ total: sortedScIds.length,
921
+ covered,
922
+ missing: missingIds.length,
923
+ missingIds,
924
+ refs: refsRecord
925
+ };
926
+ }
927
+ function toSortedArray(values) {
928
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
929
+ }
690
930
 
691
931
  // src/core/version.ts
692
- import { readFile as readFile2 } from "fs/promises";
932
+ import { readFile as readFile3 } from "fs/promises";
693
933
  import path7 from "path";
694
934
  import { fileURLToPath as fileURLToPath2 } from "url";
695
935
  async function resolveToolVersion() {
696
- if ("0.3.5".length > 0) {
697
- return "0.3.5";
936
+ if ("0.4.0".length > 0) {
937
+ return "0.4.0";
698
938
  }
699
939
  try {
700
940
  const packagePath = resolvePackageJsonPath();
701
- const raw = await readFile2(packagePath, "utf-8");
941
+ const raw = await readFile3(packagePath, "utf-8");
702
942
  const parsed = JSON.parse(raw);
703
943
  const version = typeof parsed.version === "string" ? parsed.version : "";
704
944
  return version.length > 0 ? version : "unknown";
@@ -713,7 +953,7 @@ function resolvePackageJsonPath() {
713
953
  }
714
954
 
715
955
  // src/core/validators/contracts.ts
716
- import { readFile as readFile3 } from "fs/promises";
956
+ import { readFile as readFile4 } from "fs/promises";
717
957
  import path9 from "path";
718
958
 
719
959
  // src/core/contracts.ts
@@ -791,7 +1031,7 @@ async function validateUiContracts(uiRoot) {
791
1031
  }
792
1032
  const issues = [];
793
1033
  for (const file of files) {
794
- const text = await readFile3(file, "utf-8");
1034
+ const text = await readFile4(file, "utf-8");
795
1035
  const invalidIds = extractInvalidIds(text, [
796
1036
  "SPEC",
797
1037
  "BR",
@@ -820,7 +1060,7 @@ async function validateUiContracts(uiRoot) {
820
1060
  issues.push(
821
1061
  issue(
822
1062
  "QFAI-CONTRACT-001",
823
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error2)})`,
1063
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error2)})`,
824
1064
  "error",
825
1065
  file,
826
1066
  "contracts.ui.parse"
@@ -858,7 +1098,7 @@ async function validateApiContracts(apiRoot) {
858
1098
  }
859
1099
  const issues = [];
860
1100
  for (const file of files) {
861
- const text = await readFile3(file, "utf-8");
1101
+ const text = await readFile4(file, "utf-8");
862
1102
  const invalidIds = extractInvalidIds(text, [
863
1103
  "SPEC",
864
1104
  "BR",
@@ -887,7 +1127,7 @@ async function validateApiContracts(apiRoot) {
887
1127
  issues.push(
888
1128
  issue(
889
1129
  "QFAI-CONTRACT-001",
890
- `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error2)})`,
1130
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error2)})`,
891
1131
  "error",
892
1132
  file,
893
1133
  "contracts.api.parse"
@@ -936,7 +1176,7 @@ async function validateDataContracts(dataRoot) {
936
1176
  }
937
1177
  const issues = [];
938
1178
  for (const file of files) {
939
- const text = await readFile3(file, "utf-8");
1179
+ const text = await readFile4(file, "utf-8");
940
1180
  const invalidIds = extractInvalidIds(text, [
941
1181
  "SPEC",
942
1182
  "BR",
@@ -982,7 +1222,7 @@ function lintSql(text, file) {
982
1222
  function hasOpenApi(doc) {
983
1223
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
984
1224
  }
985
- function formatError2(error2) {
1225
+ function formatError3(error2) {
986
1226
  if (error2 instanceof Error) {
987
1227
  return error2.message;
988
1228
  }
@@ -1007,7 +1247,7 @@ function issue(code, message, severity, file, rule, refs) {
1007
1247
  }
1008
1248
 
1009
1249
  // src/core/validators/delta.ts
1010
- import { readFile as readFile4 } from "fs/promises";
1250
+ import { readFile as readFile5 } from "fs/promises";
1011
1251
  import path10 from "path";
1012
1252
  var SECTION_RE = /^##\s+変更区分/m;
1013
1253
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
@@ -1025,7 +1265,7 @@ async function validateDeltas(root, config) {
1025
1265
  const deltaPath = path10.join(pack, "delta.md");
1026
1266
  let text;
1027
1267
  try {
1028
- text = await readFile4(deltaPath, "utf-8");
1268
+ text = await readFile5(deltaPath, "utf-8");
1029
1269
  } catch (error2) {
1030
1270
  if (isMissingFileError2(error2)) {
1031
1271
  issues.push(
@@ -1097,11 +1337,11 @@ function issue2(code, message, severity, file, rule, refs) {
1097
1337
  }
1098
1338
 
1099
1339
  // src/core/validators/ids.ts
1100
- import { readFile as readFile6 } from "fs/promises";
1340
+ import { readFile as readFile7 } from "fs/promises";
1101
1341
  import path12 from "path";
1102
1342
 
1103
1343
  // src/core/contractIndex.ts
1104
- import { readFile as readFile5 } from "fs/promises";
1344
+ import { readFile as readFile6 } from "fs/promises";
1105
1345
  import path11 from "path";
1106
1346
  async function buildContractIndex(root, config) {
1107
1347
  const contractsRoot = resolvePath(root, config, "contractsDir");
@@ -1126,7 +1366,7 @@ async function buildContractIndex(root, config) {
1126
1366
  }
1127
1367
  async function indexUiContracts(files, index) {
1128
1368
  for (const file of files) {
1129
- const text = await readFile5(file, "utf-8");
1369
+ const text = await readFile6(file, "utf-8");
1130
1370
  try {
1131
1371
  const doc = parseStructuredContract(file, text);
1132
1372
  extractUiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1138,7 +1378,7 @@ async function indexUiContracts(files, index) {
1138
1378
  }
1139
1379
  async function indexApiContracts(files, index) {
1140
1380
  for (const file of files) {
1141
- const text = await readFile5(file, "utf-8");
1381
+ const text = await readFile6(file, "utf-8");
1142
1382
  try {
1143
1383
  const doc = parseStructuredContract(file, text);
1144
1384
  extractApiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1150,7 +1390,7 @@ async function indexApiContracts(files, index) {
1150
1390
  }
1151
1391
  async function indexDataContracts(files, index) {
1152
1392
  for (const file of files) {
1153
- const text = await readFile5(file, "utf-8");
1393
+ const text = await readFile6(file, "utf-8");
1154
1394
  extractIds(text, "DATA").forEach((id) => record(index, id, file));
1155
1395
  }
1156
1396
  }
@@ -1279,166 +1519,8 @@ function parseSpec(md, file) {
1279
1519
  return parsed;
1280
1520
  }
1281
1521
 
1282
- // src/core/gherkin/parse.ts
1283
- import {
1284
- AstBuilder,
1285
- GherkinClassicTokenMatcher,
1286
- Parser
1287
- } from "@cucumber/gherkin";
1288
- import { randomUUID } from "crypto";
1289
- function parseGherkin(source, uri) {
1290
- const errors = [];
1291
- const uuidFn = () => randomUUID();
1292
- const builder = new AstBuilder(uuidFn);
1293
- const matcher = new GherkinClassicTokenMatcher();
1294
- const parser = new Parser(builder, matcher);
1295
- try {
1296
- const gherkinDocument = parser.parse(source);
1297
- gherkinDocument.uri = uri;
1298
- return { gherkinDocument, errors };
1299
- } catch (error2) {
1300
- errors.push(formatError3(error2));
1301
- return { gherkinDocument: null, errors };
1302
- }
1303
- }
1304
- function formatError3(error2) {
1305
- if (error2 instanceof Error) {
1306
- return error2.message;
1307
- }
1308
- return String(error2);
1309
- }
1310
-
1311
- // src/core/scenarioModel.ts
1312
- var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1313
- var SC_TAG_RE = /^SC-\d{4}$/;
1314
- var BR_TAG_RE = /^BR-\d{4}$/;
1315
- var UI_TAG_RE = /^UI-\d{4}$/;
1316
- var API_TAG_RE = /^API-\d{4}$/;
1317
- var DATA_TAG_RE = /^DATA-\d{4}$/;
1318
- function parseScenarioDocument(text, uri) {
1319
- const { gherkinDocument, errors } = parseGherkin(text, uri);
1320
- if (!gherkinDocument) {
1321
- return { document: null, errors };
1322
- }
1323
- const feature = gherkinDocument.feature;
1324
- if (!feature) {
1325
- return {
1326
- document: { uri, featureTags: [], scenarios: [] },
1327
- errors
1328
- };
1329
- }
1330
- const featureTags = collectTagNames(feature.tags);
1331
- const scenarios = collectScenarioNodes(feature, featureTags);
1332
- return {
1333
- document: {
1334
- uri,
1335
- featureName: feature.name,
1336
- featureTags,
1337
- scenarios
1338
- },
1339
- errors
1340
- };
1341
- }
1342
- function buildScenarioAtoms(document) {
1343
- return document.scenarios.map((scenario) => {
1344
- const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
1345
- const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
1346
- const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1347
- const contractIds = /* @__PURE__ */ new Set();
1348
- scenario.tags.forEach((tag) => {
1349
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1350
- contractIds.add(tag);
1351
- }
1352
- });
1353
- for (const step of scenario.steps) {
1354
- for (const text of collectStepTexts(step)) {
1355
- extractIds(text, "UI").forEach((id) => contractIds.add(id));
1356
- extractIds(text, "API").forEach((id) => contractIds.add(id));
1357
- extractIds(text, "DATA").forEach((id) => contractIds.add(id));
1358
- }
1359
- }
1360
- const atom = {
1361
- uri: document.uri,
1362
- featureName: document.featureName ?? "",
1363
- scenarioName: scenario.name,
1364
- kind: scenario.kind,
1365
- brIds,
1366
- contractIds: Array.from(contractIds).sort()
1367
- };
1368
- if (scenario.line !== void 0) {
1369
- atom.line = scenario.line;
1370
- }
1371
- if (specIds.length === 1) {
1372
- const specId = specIds[0];
1373
- if (specId) {
1374
- atom.specId = specId;
1375
- }
1376
- }
1377
- if (scIds.length === 1) {
1378
- const scId = scIds[0];
1379
- if (scId) {
1380
- atom.scId = scId;
1381
- }
1382
- }
1383
- return atom;
1384
- });
1385
- }
1386
- function collectScenarioNodes(feature, featureTags) {
1387
- const scenarios = [];
1388
- for (const child of feature.children) {
1389
- if (child.scenario) {
1390
- scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
1391
- }
1392
- if (child.rule) {
1393
- const ruleTags = collectTagNames(child.rule.tags);
1394
- for (const ruleChild of child.rule.children) {
1395
- if (ruleChild.scenario) {
1396
- scenarios.push(
1397
- buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
1398
- );
1399
- }
1400
- }
1401
- }
1402
- }
1403
- return scenarios;
1404
- }
1405
- function buildScenarioNode(scenario, featureTags, ruleTags) {
1406
- const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
1407
- const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
1408
- return {
1409
- name: scenario.name,
1410
- kind,
1411
- line: scenario.location?.line,
1412
- tags,
1413
- steps: scenario.steps
1414
- };
1415
- }
1416
- function collectTagNames(tags) {
1417
- return tags.map((tag) => tag.name.replace(/^@/, ""));
1418
- }
1419
- function collectStepTexts(step) {
1420
- const texts = [];
1421
- if (step.text) {
1422
- texts.push(step.text);
1423
- }
1424
- if (step.docString?.content) {
1425
- texts.push(step.docString.content);
1426
- }
1427
- if (step.dataTable?.rows) {
1428
- for (const row of step.dataTable.rows) {
1429
- for (const cell of row.cells) {
1430
- texts.push(cell.value);
1431
- }
1432
- }
1433
- }
1434
- return texts;
1435
- }
1436
- function unique2(values) {
1437
- return Array.from(new Set(values));
1438
- }
1439
-
1440
1522
  // src/core/validators/ids.ts
1441
- var SC_TAG_RE2 = /^SC-\d{4}$/;
1523
+ var SC_TAG_RE3 = /^SC-\d{4}$/;
1442
1524
  async function validateDefinedIds(root, config) {
1443
1525
  const issues = [];
1444
1526
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1472,7 +1554,7 @@ async function validateDefinedIds(root, config) {
1472
1554
  }
1473
1555
  async function collectSpecDefinitionIds(files, out) {
1474
1556
  for (const file of files) {
1475
- const text = await readFile6(file, "utf-8");
1557
+ const text = await readFile7(file, "utf-8");
1476
1558
  const parsed = parseSpec(text, file);
1477
1559
  if (parsed.specId) {
1478
1560
  recordId(out, parsed.specId, file);
@@ -1482,14 +1564,14 @@ async function collectSpecDefinitionIds(files, out) {
1482
1564
  }
1483
1565
  async function collectScenarioDefinitionIds(files, out) {
1484
1566
  for (const file of files) {
1485
- const text = await readFile6(file, "utf-8");
1567
+ const text = await readFile7(file, "utf-8");
1486
1568
  const { document, errors } = parseScenarioDocument(text, file);
1487
1569
  if (!document || errors.length > 0) {
1488
1570
  continue;
1489
1571
  }
1490
1572
  for (const scenario of document.scenarios) {
1491
1573
  for (const tag of scenario.tags) {
1492
- if (SC_TAG_RE2.test(tag)) {
1574
+ if (SC_TAG_RE3.test(tag)) {
1493
1575
  recordId(out, tag, file);
1494
1576
  }
1495
1577
  }
@@ -1526,11 +1608,11 @@ function issue3(code, message, severity, file, rule, refs) {
1526
1608
  }
1527
1609
 
1528
1610
  // src/core/validators/scenario.ts
1529
- import { readFile as readFile7 } from "fs/promises";
1611
+ import { readFile as readFile8 } from "fs/promises";
1530
1612
  var GIVEN_PATTERN = /\bGiven\b/;
1531
1613
  var WHEN_PATTERN = /\bWhen\b/;
1532
1614
  var THEN_PATTERN = /\bThen\b/;
1533
- var SC_TAG_RE3 = /^SC-\d{4}$/;
1615
+ var SC_TAG_RE4 = /^SC-\d{4}$/;
1534
1616
  var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1535
1617
  var BR_TAG_RE2 = /^BR-\d{4}$/;
1536
1618
  async function validateScenarios(root, config) {
@@ -1553,7 +1635,7 @@ async function validateScenarios(root, config) {
1553
1635
  for (const entry of entries) {
1554
1636
  let text;
1555
1637
  try {
1556
- text = await readFile7(entry.scenarioPath, "utf-8");
1638
+ text = await readFile8(entry.scenarioPath, "utf-8");
1557
1639
  } catch (error2) {
1558
1640
  if (isMissingFileError3(error2)) {
1559
1641
  issues.push(
@@ -1650,6 +1732,17 @@ function validateScenarioContent(text, file) {
1650
1732
  )
1651
1733
  );
1652
1734
  }
1735
+ if (document.scenarios.length > 1) {
1736
+ issues.push(
1737
+ issue4(
1738
+ "QFAI-SC-011",
1739
+ `Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
1740
+ "error",
1741
+ file,
1742
+ "scenario.single"
1743
+ )
1744
+ );
1745
+ }
1653
1746
  for (const scenario of document.scenarios) {
1654
1747
  if (scenario.tags.length === 0) {
1655
1748
  issues.push(
@@ -1664,7 +1757,7 @@ function validateScenarioContent(text, file) {
1664
1757
  continue;
1665
1758
  }
1666
1759
  const missingTags = [];
1667
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
1760
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
1668
1761
  if (scTags.length === 0) {
1669
1762
  missingTags.push("SC(0\u4EF6)");
1670
1763
  } else if (scTags.length > 1) {
@@ -1739,7 +1832,7 @@ function isMissingFileError3(error2) {
1739
1832
  }
1740
1833
 
1741
1834
  // src/core/validators/spec.ts
1742
- import { readFile as readFile8 } from "fs/promises";
1835
+ import { readFile as readFile9 } from "fs/promises";
1743
1836
  async function validateSpecs(root, config) {
1744
1837
  const specsRoot = resolvePath(root, config, "specsDir");
1745
1838
  const entries = await collectSpecEntries(specsRoot);
@@ -1760,7 +1853,7 @@ async function validateSpecs(root, config) {
1760
1853
  for (const entry of entries) {
1761
1854
  let text;
1762
1855
  try {
1763
- text = await readFile8(entry.specPath, "utf-8");
1856
+ text = await readFile9(entry.specPath, "utf-8");
1764
1857
  } catch (error2) {
1765
1858
  if (isMissingFileError4(error2)) {
1766
1859
  issues.push(
@@ -1909,8 +2002,8 @@ function isMissingFileError4(error2) {
1909
2002
  }
1910
2003
 
1911
2004
  // src/core/validators/traceability.ts
1912
- import { readFile as readFile9 } from "fs/promises";
1913
- var SC_TAG_RE4 = /^SC-\d{4}$/;
2005
+ import { readFile as readFile10 } from "fs/promises";
2006
+ var SC_TAG_RE5 = /^SC-\d{4}$/;
1914
2007
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
1915
2008
  var BR_TAG_RE3 = /^BR-\d{4}$/;
1916
2009
  async function validateTraceability(root, config) {
@@ -1931,7 +2024,7 @@ async function validateTraceability(root, config) {
1931
2024
  const contractIndex = await buildContractIndex(root, config);
1932
2025
  const contractIds = contractIndex.ids;
1933
2026
  for (const file of specFiles) {
1934
- const text = await readFile9(file, "utf-8");
2027
+ const text = await readFile10(file, "utf-8");
1935
2028
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1936
2029
  const parsed = parseSpec(text, file);
1937
2030
  if (parsed.specId) {
@@ -1968,7 +2061,7 @@ async function validateTraceability(root, config) {
1968
2061
  }
1969
2062
  }
1970
2063
  for (const file of scenarioFiles) {
1971
- const text = await readFile9(file, "utf-8");
2064
+ const text = await readFile10(file, "utf-8");
1972
2065
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1973
2066
  const { document, errors } = parseScenarioDocument(text, file);
1974
2067
  if (!document || errors.length > 0) {
@@ -1982,7 +2075,7 @@ async function validateTraceability(root, config) {
1982
2075
  }
1983
2076
  const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
1984
2077
  const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
1985
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
2078
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE5.test(tag));
1986
2079
  brTags.forEach((id) => brIdsInScenarios.add(id));
1987
2080
  scTags.forEach((id) => scIdsInScenarios.add(id));
1988
2081
  atom.contractIds.forEach((id) => scenarioContractIds.add(id));
@@ -2110,6 +2203,25 @@ async function validateTraceability(root, config) {
2110
2203
  );
2111
2204
  }
2112
2205
  }
2206
+ if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2207
+ const scTestRefs = await collectScTestReferences(testsRoot);
2208
+ const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2209
+ const refs = scTestRefs.get(id);
2210
+ return !refs || refs.size === 0;
2211
+ });
2212
+ if (scWithoutTests.length > 0) {
2213
+ issues.push(
2214
+ issue6(
2215
+ "QFAI-TRACE-010",
2216
+ `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`,
2217
+ config.validation.traceability.scNoTestSeverity,
2218
+ testsRoot,
2219
+ "traceability.scMustHaveTest",
2220
+ scWithoutTests
2221
+ )
2222
+ );
2223
+ }
2224
+ }
2113
2225
  if (!config.validation.traceability.allowOrphanContracts) {
2114
2226
  if (contractIds.size > 0) {
2115
2227
  const orphanContracts = Array.from(contractIds).filter(
@@ -2158,7 +2270,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2158
2270
  const pattern = buildIdPattern(Array.from(upstreamIds));
2159
2271
  let found = false;
2160
2272
  for (const file of targetFiles) {
2161
- const text = await readFile9(file, "utf-8");
2273
+ const text = await readFile10(file, "utf-8");
2162
2274
  if (pattern.test(text)) {
2163
2275
  found = true;
2164
2276
  break;
@@ -2214,7 +2326,6 @@ async function validateProject(root, configResult) {
2214
2326
  ];
2215
2327
  const toolVersion = await resolveToolVersion();
2216
2328
  return {
2217
- schemaVersion: VALIDATION_SCHEMA_VERSION,
2218
2329
  toolVersion,
2219
2330
  issues,
2220
2331
  counts: countIssues(issues)
@@ -2266,6 +2377,9 @@ async function createReportData(root, validation, configResult) {
2266
2377
  srcRoot,
2267
2378
  testsRoot
2268
2379
  );
2380
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2381
+ const scTestRefs = await collectScTestReferences(testsRoot);
2382
+ const scCoverage = buildScCoverage(scIds, scTestRefs);
2269
2383
  const resolvedValidation = validation ?? await validateProject(root, resolved);
2270
2384
  const version = await resolveToolVersion();
2271
2385
  return {
@@ -2294,7 +2408,8 @@ async function createReportData(root, validation, configResult) {
2294
2408
  },
2295
2409
  traceability: {
2296
2410
  upstreamIdsFound: upstreamIds.size,
2297
- referencedInCodeOrTests: traceability
2411
+ referencedInCodeOrTests: traceability,
2412
+ sc: scCoverage
2298
2413
  },
2299
2414
  issues: resolvedValidation.issues
2300
2415
  };
@@ -2331,6 +2446,32 @@ function formatReportMarkdown(data) {
2331
2446
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2332
2447
  );
2333
2448
  lines.push("");
2449
+ lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2450
+ lines.push(`- total: ${data.traceability.sc.total}`);
2451
+ lines.push(`- covered: ${data.traceability.sc.covered}`);
2452
+ lines.push(`- missing: ${data.traceability.sc.missing}`);
2453
+ if (data.traceability.sc.missingIds.length === 0) {
2454
+ lines.push("- missingIds: (none)");
2455
+ } else {
2456
+ lines.push(`- missingIds: ${data.traceability.sc.missingIds.join(", ")}`);
2457
+ }
2458
+ lines.push("");
2459
+ lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
2460
+ const scRefs = data.traceability.sc.refs;
2461
+ const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
2462
+ if (scIds.length === 0) {
2463
+ lines.push("- (none)");
2464
+ } else {
2465
+ for (const scId of scIds) {
2466
+ const refs = scRefs[scId] ?? [];
2467
+ if (refs.length === 0) {
2468
+ lines.push(`- ${scId}: (none)`);
2469
+ } else {
2470
+ lines.push(`- ${scId}: ${refs.join(", ")}`);
2471
+ }
2472
+ }
2473
+ }
2474
+ lines.push("");
2334
2475
  lines.push("## Hotspots");
2335
2476
  const hotspots = buildHotspots(data.issues);
2336
2477
  if (hotspots.length === 0) {
@@ -2385,25 +2526,25 @@ async function collectIds(files) {
2385
2526
  DATA: /* @__PURE__ */ new Set()
2386
2527
  };
2387
2528
  for (const file of files) {
2388
- const text = await readFile10(file, "utf-8");
2529
+ const text = await readFile11(file, "utf-8");
2389
2530
  for (const prefix of ID_PREFIXES2) {
2390
2531
  const ids = extractIds(text, prefix);
2391
2532
  ids.forEach((id) => result[prefix].add(id));
2392
2533
  }
2393
2534
  }
2394
2535
  return {
2395
- SPEC: toSortedArray(result.SPEC),
2396
- BR: toSortedArray(result.BR),
2397
- SC: toSortedArray(result.SC),
2398
- UI: toSortedArray(result.UI),
2399
- API: toSortedArray(result.API),
2400
- DATA: toSortedArray(result.DATA)
2536
+ SPEC: toSortedArray2(result.SPEC),
2537
+ BR: toSortedArray2(result.BR),
2538
+ SC: toSortedArray2(result.SC),
2539
+ UI: toSortedArray2(result.UI),
2540
+ API: toSortedArray2(result.API),
2541
+ DATA: toSortedArray2(result.DATA)
2401
2542
  };
2402
2543
  }
2403
2544
  async function collectUpstreamIds(files) {
2404
2545
  const ids = /* @__PURE__ */ new Set();
2405
2546
  for (const file of files) {
2406
- const text = await readFile10(file, "utf-8");
2547
+ const text = await readFile11(file, "utf-8");
2407
2548
  extractAllIds(text).forEach((id) => ids.add(id));
2408
2549
  }
2409
2550
  return ids;
@@ -2424,7 +2565,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
2424
2565
  }
2425
2566
  const pattern = buildIdPattern2(Array.from(upstreamIds));
2426
2567
  for (const file of targetFiles) {
2427
- const text = await readFile10(file, "utf-8");
2568
+ const text = await readFile11(file, "utf-8");
2428
2569
  if (pattern.test(text)) {
2429
2570
  return true;
2430
2571
  }
@@ -2441,7 +2582,7 @@ function formatIdLine(label, values) {
2441
2582
  }
2442
2583
  return `- ${label}: ${values.join(", ")}`;
2443
2584
  }
2444
- function toSortedArray(values) {
2585
+ function toSortedArray2(values) {
2445
2586
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2446
2587
  }
2447
2588
  function buildHotspots(issues) {
@@ -2508,16 +2649,11 @@ async function runReport(options) {
2508
2649
  info(`wrote report: ${outPath}`);
2509
2650
  }
2510
2651
  async function readValidationResult(inputPath) {
2511
- const raw = await readFile11(inputPath, "utf-8");
2652
+ const raw = await readFile12(inputPath, "utf-8");
2512
2653
  const parsed = JSON.parse(raw);
2513
2654
  if (!isValidationResult(parsed)) {
2514
2655
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
2515
2656
  }
2516
- if (parsed.schemaVersion !== VALIDATION_SCHEMA_VERSION) {
2517
- throw new Error(
2518
- `validate.json \u306E schemaVersion \u304C\u4E0D\u4E00\u81F4\u3067\u3059: expected ${VALIDATION_SCHEMA_VERSION}, actual ${parsed.schemaVersion}`
2519
- );
2520
- }
2521
2657
  return parsed;
2522
2658
  }
2523
2659
  function isValidationResult(value) {
@@ -2525,9 +2661,6 @@ function isValidationResult(value) {
2525
2661
  return false;
2526
2662
  }
2527
2663
  const record2 = value;
2528
- if (typeof record2.schemaVersion !== "string") {
2529
- return false;
2530
- }
2531
2664
  if (typeof record2.toolVersion !== "string") {
2532
2665
  return false;
2533
2666
  }