qfai 0.3.8 → 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.
package/dist/index.mjs CHANGED
@@ -28,6 +28,8 @@ var defaultConfig = {
28
28
  traceability: {
29
29
  brMustHaveSc: true,
30
30
  scMustTouchContracts: true,
31
+ scMustHaveTest: true,
32
+ scNoTestSeverity: "error",
31
33
  allowOrphanContracts: false,
32
34
  unknownContractIdSeverity: "error"
33
35
  }
@@ -207,6 +209,20 @@ function normalizeValidation(raw, configPath, issues) {
207
209
  configPath,
208
210
  issues
209
211
  ),
212
+ scMustHaveTest: readBoolean(
213
+ traceabilityRaw?.scMustHaveTest,
214
+ base.traceability.scMustHaveTest,
215
+ "validation.traceability.scMustHaveTest",
216
+ configPath,
217
+ issues
218
+ ),
219
+ scNoTestSeverity: readTraceabilitySeverity(
220
+ traceabilityRaw?.scNoTestSeverity,
221
+ base.traceability.scNoTestSeverity,
222
+ "validation.traceability.scNoTestSeverity",
223
+ configPath,
224
+ issues
225
+ ),
210
226
  allowOrphanContracts: readBoolean(
211
227
  traceabilityRaw?.allowOrphanContracts,
212
228
  base.traceability.allowOrphanContracts,
@@ -385,7 +401,7 @@ function isValidId(value, prefix) {
385
401
  }
386
402
 
387
403
  // src/core/report.ts
388
- import { readFile as readFile10 } from "fs/promises";
404
+ import { readFile as readFile11 } from "fs/promises";
389
405
  import path10 from "path";
390
406
 
391
407
  // src/core/discovery.ts
@@ -526,20 +542,244 @@ async function exists2(target) {
526
542
  }
527
543
  }
528
544
 
529
- // src/core/types.ts
530
- var VALIDATION_SCHEMA_VERSION = "0.2";
545
+ // src/core/traceability.ts
546
+ import { readFile as readFile2 } from "fs/promises";
547
+
548
+ // src/core/gherkin/parse.ts
549
+ import {
550
+ AstBuilder,
551
+ GherkinClassicTokenMatcher,
552
+ Parser
553
+ } from "@cucumber/gherkin";
554
+ import { randomUUID } from "crypto";
555
+ function parseGherkin(source, uri) {
556
+ const errors = [];
557
+ const uuidFn = () => randomUUID();
558
+ const builder = new AstBuilder(uuidFn);
559
+ const matcher = new GherkinClassicTokenMatcher();
560
+ const parser = new Parser(builder, matcher);
561
+ try {
562
+ const gherkinDocument = parser.parse(source);
563
+ gherkinDocument.uri = uri;
564
+ return { gherkinDocument, errors };
565
+ } catch (error) {
566
+ errors.push(formatError2(error));
567
+ return { gherkinDocument: null, errors };
568
+ }
569
+ }
570
+ function formatError2(error) {
571
+ if (error instanceof Error) {
572
+ return error.message;
573
+ }
574
+ return String(error);
575
+ }
576
+
577
+ // src/core/scenarioModel.ts
578
+ var SPEC_TAG_RE = /^SPEC-\d{4}$/;
579
+ var SC_TAG_RE = /^SC-\d{4}$/;
580
+ var BR_TAG_RE = /^BR-\d{4}$/;
581
+ var UI_TAG_RE = /^UI-\d{4}$/;
582
+ var API_TAG_RE = /^API-\d{4}$/;
583
+ var DATA_TAG_RE = /^DATA-\d{4}$/;
584
+ function parseScenarioDocument(text, uri) {
585
+ const { gherkinDocument, errors } = parseGherkin(text, uri);
586
+ if (!gherkinDocument) {
587
+ return { document: null, errors };
588
+ }
589
+ const feature = gherkinDocument.feature;
590
+ if (!feature) {
591
+ return {
592
+ document: { uri, featureTags: [], scenarios: [] },
593
+ errors
594
+ };
595
+ }
596
+ const featureTags = collectTagNames(feature.tags);
597
+ const scenarios = collectScenarioNodes(feature, featureTags);
598
+ return {
599
+ document: {
600
+ uri,
601
+ featureName: feature.name,
602
+ featureTags,
603
+ scenarios
604
+ },
605
+ errors
606
+ };
607
+ }
608
+ function buildScenarioAtoms(document) {
609
+ return document.scenarios.map((scenario) => {
610
+ const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
611
+ const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
612
+ const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
613
+ const contractIds = /* @__PURE__ */ new Set();
614
+ scenario.tags.forEach((tag) => {
615
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
616
+ contractIds.add(tag);
617
+ }
618
+ });
619
+ for (const step of scenario.steps) {
620
+ for (const text of collectStepTexts(step)) {
621
+ extractIds(text, "UI").forEach((id) => contractIds.add(id));
622
+ extractIds(text, "API").forEach((id) => contractIds.add(id));
623
+ extractIds(text, "DATA").forEach((id) => contractIds.add(id));
624
+ }
625
+ }
626
+ const atom = {
627
+ uri: document.uri,
628
+ featureName: document.featureName ?? "",
629
+ scenarioName: scenario.name,
630
+ kind: scenario.kind,
631
+ brIds,
632
+ contractIds: Array.from(contractIds).sort()
633
+ };
634
+ if (scenario.line !== void 0) {
635
+ atom.line = scenario.line;
636
+ }
637
+ if (specIds.length === 1) {
638
+ const specId = specIds[0];
639
+ if (specId) {
640
+ atom.specId = specId;
641
+ }
642
+ }
643
+ if (scIds.length === 1) {
644
+ const scId = scIds[0];
645
+ if (scId) {
646
+ atom.scId = scId;
647
+ }
648
+ }
649
+ return atom;
650
+ });
651
+ }
652
+ function collectScenarioNodes(feature, featureTags) {
653
+ const scenarios = [];
654
+ for (const child of feature.children) {
655
+ if (child.scenario) {
656
+ scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
657
+ }
658
+ if (child.rule) {
659
+ const ruleTags = collectTagNames(child.rule.tags);
660
+ for (const ruleChild of child.rule.children) {
661
+ if (ruleChild.scenario) {
662
+ scenarios.push(
663
+ buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
664
+ );
665
+ }
666
+ }
667
+ }
668
+ }
669
+ return scenarios;
670
+ }
671
+ function buildScenarioNode(scenario, featureTags, ruleTags) {
672
+ const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
673
+ const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
674
+ return {
675
+ name: scenario.name,
676
+ kind,
677
+ line: scenario.location?.line,
678
+ tags,
679
+ steps: scenario.steps
680
+ };
681
+ }
682
+ function collectTagNames(tags) {
683
+ return tags.map((tag) => tag.name.replace(/^@/, ""));
684
+ }
685
+ function collectStepTexts(step) {
686
+ const texts = [];
687
+ if (step.text) {
688
+ texts.push(step.text);
689
+ }
690
+ if (step.docString?.content) {
691
+ texts.push(step.docString.content);
692
+ }
693
+ if (step.dataTable?.rows) {
694
+ for (const row of step.dataTable.rows) {
695
+ for (const cell of row.cells) {
696
+ texts.push(cell.value);
697
+ }
698
+ }
699
+ }
700
+ return texts;
701
+ }
702
+ function unique2(values) {
703
+ return Array.from(new Set(values));
704
+ }
705
+
706
+ // src/core/traceability.ts
707
+ var SC_TAG_RE2 = /^SC-\d{4}$/;
708
+ async function collectScIdsFromScenarioFiles(scenarioFiles) {
709
+ const scIds = /* @__PURE__ */ new Set();
710
+ for (const file of scenarioFiles) {
711
+ const text = await readFile2(file, "utf-8");
712
+ const { document, errors } = parseScenarioDocument(text, file);
713
+ if (!document || errors.length > 0) {
714
+ continue;
715
+ }
716
+ for (const scenario of document.scenarios) {
717
+ for (const tag of scenario.tags) {
718
+ if (SC_TAG_RE2.test(tag)) {
719
+ scIds.add(tag);
720
+ }
721
+ }
722
+ }
723
+ }
724
+ return scIds;
725
+ }
726
+ async function collectScTestReferences(testsRoot) {
727
+ const refs = /* @__PURE__ */ new Map();
728
+ const testFiles = await collectFiles(testsRoot, {
729
+ extensions: [".ts", ".tsx", ".js", ".jsx"]
730
+ });
731
+ for (const file of testFiles) {
732
+ const text = await readFile2(file, "utf-8");
733
+ const scIds = extractIds(text, "SC");
734
+ if (scIds.length === 0) {
735
+ continue;
736
+ }
737
+ for (const scId of scIds) {
738
+ const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
739
+ current.add(file);
740
+ refs.set(scId, current);
741
+ }
742
+ }
743
+ return refs;
744
+ }
745
+ function buildScCoverage(scIds, refs) {
746
+ const sortedScIds = toSortedArray(scIds);
747
+ const refsRecord = {};
748
+ const missingIds = [];
749
+ let covered = 0;
750
+ for (const scId of sortedScIds) {
751
+ const files = refs.get(scId);
752
+ const sortedFiles = files ? toSortedArray(files) : [];
753
+ refsRecord[scId] = sortedFiles;
754
+ if (sortedFiles.length === 0) {
755
+ missingIds.push(scId);
756
+ } else {
757
+ covered += 1;
758
+ }
759
+ }
760
+ return {
761
+ total: sortedScIds.length,
762
+ covered,
763
+ missing: missingIds.length,
764
+ missingIds,
765
+ refs: refsRecord
766
+ };
767
+ }
768
+ function toSortedArray(values) {
769
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
770
+ }
531
771
 
532
772
  // src/core/version.ts
533
- import { readFile as readFile2 } from "fs/promises";
773
+ import { readFile as readFile3 } from "fs/promises";
534
774
  import path4 from "path";
535
775
  import { fileURLToPath } from "url";
536
776
  async function resolveToolVersion() {
537
- if ("0.3.5".length > 0) {
538
- return "0.3.5";
777
+ if ("0.4.0".length > 0) {
778
+ return "0.4.0";
539
779
  }
540
780
  try {
541
781
  const packagePath = resolvePackageJsonPath();
542
- const raw = await readFile2(packagePath, "utf-8");
782
+ const raw = await readFile3(packagePath, "utf-8");
543
783
  const parsed = JSON.parse(raw);
544
784
  const version = typeof parsed.version === "string" ? parsed.version : "";
545
785
  return version.length > 0 ? version : "unknown";
@@ -554,7 +794,7 @@ function resolvePackageJsonPath() {
554
794
  }
555
795
 
556
796
  // src/core/validators/contracts.ts
557
- import { readFile as readFile3 } from "fs/promises";
797
+ import { readFile as readFile4 } from "fs/promises";
558
798
  import path6 from "path";
559
799
 
560
800
  // src/core/contracts.ts
@@ -632,7 +872,7 @@ async function validateUiContracts(uiRoot) {
632
872
  }
633
873
  const issues = [];
634
874
  for (const file of files) {
635
- const text = await readFile3(file, "utf-8");
875
+ const text = await readFile4(file, "utf-8");
636
876
  const invalidIds = extractInvalidIds(text, [
637
877
  "SPEC",
638
878
  "BR",
@@ -661,7 +901,7 @@ async function validateUiContracts(uiRoot) {
661
901
  issues.push(
662
902
  issue(
663
903
  "QFAI-CONTRACT-001",
664
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error)})`,
904
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error)})`,
665
905
  "error",
666
906
  file,
667
907
  "contracts.ui.parse"
@@ -699,7 +939,7 @@ async function validateApiContracts(apiRoot) {
699
939
  }
700
940
  const issues = [];
701
941
  for (const file of files) {
702
- const text = await readFile3(file, "utf-8");
942
+ const text = await readFile4(file, "utf-8");
703
943
  const invalidIds = extractInvalidIds(text, [
704
944
  "SPEC",
705
945
  "BR",
@@ -728,7 +968,7 @@ async function validateApiContracts(apiRoot) {
728
968
  issues.push(
729
969
  issue(
730
970
  "QFAI-CONTRACT-001",
731
- `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error)})`,
971
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error)})`,
732
972
  "error",
733
973
  file,
734
974
  "contracts.api.parse"
@@ -777,7 +1017,7 @@ async function validateDataContracts(dataRoot) {
777
1017
  }
778
1018
  const issues = [];
779
1019
  for (const file of files) {
780
- const text = await readFile3(file, "utf-8");
1020
+ const text = await readFile4(file, "utf-8");
781
1021
  const invalidIds = extractInvalidIds(text, [
782
1022
  "SPEC",
783
1023
  "BR",
@@ -823,7 +1063,7 @@ function lintSql(text, file) {
823
1063
  function hasOpenApi(doc) {
824
1064
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
825
1065
  }
826
- function formatError2(error) {
1066
+ function formatError3(error) {
827
1067
  if (error instanceof Error) {
828
1068
  return error.message;
829
1069
  }
@@ -848,7 +1088,7 @@ function issue(code, message, severity, file, rule, refs) {
848
1088
  }
849
1089
 
850
1090
  // src/core/validators/delta.ts
851
- import { readFile as readFile4 } from "fs/promises";
1091
+ import { readFile as readFile5 } from "fs/promises";
852
1092
  import path7 from "path";
853
1093
  var SECTION_RE = /^##\s+変更区分/m;
854
1094
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
@@ -866,7 +1106,7 @@ async function validateDeltas(root, config) {
866
1106
  const deltaPath = path7.join(pack, "delta.md");
867
1107
  let text;
868
1108
  try {
869
- text = await readFile4(deltaPath, "utf-8");
1109
+ text = await readFile5(deltaPath, "utf-8");
870
1110
  } catch (error) {
871
1111
  if (isMissingFileError2(error)) {
872
1112
  issues.push(
@@ -938,11 +1178,11 @@ function issue2(code, message, severity, file, rule, refs) {
938
1178
  }
939
1179
 
940
1180
  // src/core/validators/ids.ts
941
- import { readFile as readFile6 } from "fs/promises";
1181
+ import { readFile as readFile7 } from "fs/promises";
942
1182
  import path9 from "path";
943
1183
 
944
1184
  // src/core/contractIndex.ts
945
- import { readFile as readFile5 } from "fs/promises";
1185
+ import { readFile as readFile6 } from "fs/promises";
946
1186
  import path8 from "path";
947
1187
  async function buildContractIndex(root, config) {
948
1188
  const contractsRoot = resolvePath(root, config, "contractsDir");
@@ -967,7 +1207,7 @@ async function buildContractIndex(root, config) {
967
1207
  }
968
1208
  async function indexUiContracts(files, index) {
969
1209
  for (const file of files) {
970
- const text = await readFile5(file, "utf-8");
1210
+ const text = await readFile6(file, "utf-8");
971
1211
  try {
972
1212
  const doc = parseStructuredContract(file, text);
973
1213
  extractUiContractIds(doc).forEach((id) => record(index, id, file));
@@ -979,7 +1219,7 @@ async function indexUiContracts(files, index) {
979
1219
  }
980
1220
  async function indexApiContracts(files, index) {
981
1221
  for (const file of files) {
982
- const text = await readFile5(file, "utf-8");
1222
+ const text = await readFile6(file, "utf-8");
983
1223
  try {
984
1224
  const doc = parseStructuredContract(file, text);
985
1225
  extractApiContractIds(doc).forEach((id) => record(index, id, file));
@@ -991,7 +1231,7 @@ async function indexApiContracts(files, index) {
991
1231
  }
992
1232
  async function indexDataContracts(files, index) {
993
1233
  for (const file of files) {
994
- const text = await readFile5(file, "utf-8");
1234
+ const text = await readFile6(file, "utf-8");
995
1235
  extractIds(text, "DATA").forEach((id) => record(index, id, file));
996
1236
  }
997
1237
  }
@@ -1120,166 +1360,8 @@ function parseSpec(md, file) {
1120
1360
  return parsed;
1121
1361
  }
1122
1362
 
1123
- // src/core/gherkin/parse.ts
1124
- import {
1125
- AstBuilder,
1126
- GherkinClassicTokenMatcher,
1127
- Parser
1128
- } from "@cucumber/gherkin";
1129
- import { randomUUID } from "crypto";
1130
- function parseGherkin(source, uri) {
1131
- const errors = [];
1132
- const uuidFn = () => randomUUID();
1133
- const builder = new AstBuilder(uuidFn);
1134
- const matcher = new GherkinClassicTokenMatcher();
1135
- const parser = new Parser(builder, matcher);
1136
- try {
1137
- const gherkinDocument = parser.parse(source);
1138
- gherkinDocument.uri = uri;
1139
- return { gherkinDocument, errors };
1140
- } catch (error) {
1141
- errors.push(formatError3(error));
1142
- return { gherkinDocument: null, errors };
1143
- }
1144
- }
1145
- function formatError3(error) {
1146
- if (error instanceof Error) {
1147
- return error.message;
1148
- }
1149
- return String(error);
1150
- }
1151
-
1152
- // src/core/scenarioModel.ts
1153
- var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1154
- var SC_TAG_RE = /^SC-\d{4}$/;
1155
- var BR_TAG_RE = /^BR-\d{4}$/;
1156
- var UI_TAG_RE = /^UI-\d{4}$/;
1157
- var API_TAG_RE = /^API-\d{4}$/;
1158
- var DATA_TAG_RE = /^DATA-\d{4}$/;
1159
- function parseScenarioDocument(text, uri) {
1160
- const { gherkinDocument, errors } = parseGherkin(text, uri);
1161
- if (!gherkinDocument) {
1162
- return { document: null, errors };
1163
- }
1164
- const feature = gherkinDocument.feature;
1165
- if (!feature) {
1166
- return {
1167
- document: { uri, featureTags: [], scenarios: [] },
1168
- errors
1169
- };
1170
- }
1171
- const featureTags = collectTagNames(feature.tags);
1172
- const scenarios = collectScenarioNodes(feature, featureTags);
1173
- return {
1174
- document: {
1175
- uri,
1176
- featureName: feature.name,
1177
- featureTags,
1178
- scenarios
1179
- },
1180
- errors
1181
- };
1182
- }
1183
- function buildScenarioAtoms(document) {
1184
- return document.scenarios.map((scenario) => {
1185
- const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
1186
- const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
1187
- const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1188
- const contractIds = /* @__PURE__ */ new Set();
1189
- scenario.tags.forEach((tag) => {
1190
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1191
- contractIds.add(tag);
1192
- }
1193
- });
1194
- for (const step of scenario.steps) {
1195
- for (const text of collectStepTexts(step)) {
1196
- extractIds(text, "UI").forEach((id) => contractIds.add(id));
1197
- extractIds(text, "API").forEach((id) => contractIds.add(id));
1198
- extractIds(text, "DATA").forEach((id) => contractIds.add(id));
1199
- }
1200
- }
1201
- const atom = {
1202
- uri: document.uri,
1203
- featureName: document.featureName ?? "",
1204
- scenarioName: scenario.name,
1205
- kind: scenario.kind,
1206
- brIds,
1207
- contractIds: Array.from(contractIds).sort()
1208
- };
1209
- if (scenario.line !== void 0) {
1210
- atom.line = scenario.line;
1211
- }
1212
- if (specIds.length === 1) {
1213
- const specId = specIds[0];
1214
- if (specId) {
1215
- atom.specId = specId;
1216
- }
1217
- }
1218
- if (scIds.length === 1) {
1219
- const scId = scIds[0];
1220
- if (scId) {
1221
- atom.scId = scId;
1222
- }
1223
- }
1224
- return atom;
1225
- });
1226
- }
1227
- function collectScenarioNodes(feature, featureTags) {
1228
- const scenarios = [];
1229
- for (const child of feature.children) {
1230
- if (child.scenario) {
1231
- scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
1232
- }
1233
- if (child.rule) {
1234
- const ruleTags = collectTagNames(child.rule.tags);
1235
- for (const ruleChild of child.rule.children) {
1236
- if (ruleChild.scenario) {
1237
- scenarios.push(
1238
- buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
1239
- );
1240
- }
1241
- }
1242
- }
1243
- }
1244
- return scenarios;
1245
- }
1246
- function buildScenarioNode(scenario, featureTags, ruleTags) {
1247
- const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
1248
- const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
1249
- return {
1250
- name: scenario.name,
1251
- kind,
1252
- line: scenario.location?.line,
1253
- tags,
1254
- steps: scenario.steps
1255
- };
1256
- }
1257
- function collectTagNames(tags) {
1258
- return tags.map((tag) => tag.name.replace(/^@/, ""));
1259
- }
1260
- function collectStepTexts(step) {
1261
- const texts = [];
1262
- if (step.text) {
1263
- texts.push(step.text);
1264
- }
1265
- if (step.docString?.content) {
1266
- texts.push(step.docString.content);
1267
- }
1268
- if (step.dataTable?.rows) {
1269
- for (const row of step.dataTable.rows) {
1270
- for (const cell of row.cells) {
1271
- texts.push(cell.value);
1272
- }
1273
- }
1274
- }
1275
- return texts;
1276
- }
1277
- function unique2(values) {
1278
- return Array.from(new Set(values));
1279
- }
1280
-
1281
1363
  // src/core/validators/ids.ts
1282
- var SC_TAG_RE2 = /^SC-\d{4}$/;
1364
+ var SC_TAG_RE3 = /^SC-\d{4}$/;
1283
1365
  async function validateDefinedIds(root, config) {
1284
1366
  const issues = [];
1285
1367
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1313,7 +1395,7 @@ async function validateDefinedIds(root, config) {
1313
1395
  }
1314
1396
  async function collectSpecDefinitionIds(files, out) {
1315
1397
  for (const file of files) {
1316
- const text = await readFile6(file, "utf-8");
1398
+ const text = await readFile7(file, "utf-8");
1317
1399
  const parsed = parseSpec(text, file);
1318
1400
  if (parsed.specId) {
1319
1401
  recordId(out, parsed.specId, file);
@@ -1323,14 +1405,14 @@ async function collectSpecDefinitionIds(files, out) {
1323
1405
  }
1324
1406
  async function collectScenarioDefinitionIds(files, out) {
1325
1407
  for (const file of files) {
1326
- const text = await readFile6(file, "utf-8");
1408
+ const text = await readFile7(file, "utf-8");
1327
1409
  const { document, errors } = parseScenarioDocument(text, file);
1328
1410
  if (!document || errors.length > 0) {
1329
1411
  continue;
1330
1412
  }
1331
1413
  for (const scenario of document.scenarios) {
1332
1414
  for (const tag of scenario.tags) {
1333
- if (SC_TAG_RE2.test(tag)) {
1415
+ if (SC_TAG_RE3.test(tag)) {
1334
1416
  recordId(out, tag, file);
1335
1417
  }
1336
1418
  }
@@ -1367,11 +1449,11 @@ function issue3(code, message, severity, file, rule, refs) {
1367
1449
  }
1368
1450
 
1369
1451
  // src/core/validators/scenario.ts
1370
- import { readFile as readFile7 } from "fs/promises";
1452
+ import { readFile as readFile8 } from "fs/promises";
1371
1453
  var GIVEN_PATTERN = /\bGiven\b/;
1372
1454
  var WHEN_PATTERN = /\bWhen\b/;
1373
1455
  var THEN_PATTERN = /\bThen\b/;
1374
- var SC_TAG_RE3 = /^SC-\d{4}$/;
1456
+ var SC_TAG_RE4 = /^SC-\d{4}$/;
1375
1457
  var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1376
1458
  var BR_TAG_RE2 = /^BR-\d{4}$/;
1377
1459
  async function validateScenarios(root, config) {
@@ -1394,7 +1476,7 @@ async function validateScenarios(root, config) {
1394
1476
  for (const entry of entries) {
1395
1477
  let text;
1396
1478
  try {
1397
- text = await readFile7(entry.scenarioPath, "utf-8");
1479
+ text = await readFile8(entry.scenarioPath, "utf-8");
1398
1480
  } catch (error) {
1399
1481
  if (isMissingFileError3(error)) {
1400
1482
  issues.push(
@@ -1491,6 +1573,17 @@ function validateScenarioContent(text, file) {
1491
1573
  )
1492
1574
  );
1493
1575
  }
1576
+ if (document.scenarios.length > 1) {
1577
+ issues.push(
1578
+ issue4(
1579
+ "QFAI-SC-011",
1580
+ `Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
1581
+ "error",
1582
+ file,
1583
+ "scenario.single"
1584
+ )
1585
+ );
1586
+ }
1494
1587
  for (const scenario of document.scenarios) {
1495
1588
  if (scenario.tags.length === 0) {
1496
1589
  issues.push(
@@ -1505,7 +1598,7 @@ function validateScenarioContent(text, file) {
1505
1598
  continue;
1506
1599
  }
1507
1600
  const missingTags = [];
1508
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
1601
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
1509
1602
  if (scTags.length === 0) {
1510
1603
  missingTags.push("SC(0\u4EF6)");
1511
1604
  } else if (scTags.length > 1) {
@@ -1580,7 +1673,7 @@ function isMissingFileError3(error) {
1580
1673
  }
1581
1674
 
1582
1675
  // src/core/validators/spec.ts
1583
- import { readFile as readFile8 } from "fs/promises";
1676
+ import { readFile as readFile9 } from "fs/promises";
1584
1677
  async function validateSpecs(root, config) {
1585
1678
  const specsRoot = resolvePath(root, config, "specsDir");
1586
1679
  const entries = await collectSpecEntries(specsRoot);
@@ -1601,7 +1694,7 @@ async function validateSpecs(root, config) {
1601
1694
  for (const entry of entries) {
1602
1695
  let text;
1603
1696
  try {
1604
- text = await readFile8(entry.specPath, "utf-8");
1697
+ text = await readFile9(entry.specPath, "utf-8");
1605
1698
  } catch (error) {
1606
1699
  if (isMissingFileError4(error)) {
1607
1700
  issues.push(
@@ -1750,8 +1843,8 @@ function isMissingFileError4(error) {
1750
1843
  }
1751
1844
 
1752
1845
  // src/core/validators/traceability.ts
1753
- import { readFile as readFile9 } from "fs/promises";
1754
- var SC_TAG_RE4 = /^SC-\d{4}$/;
1846
+ import { readFile as readFile10 } from "fs/promises";
1847
+ var SC_TAG_RE5 = /^SC-\d{4}$/;
1755
1848
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
1756
1849
  var BR_TAG_RE3 = /^BR-\d{4}$/;
1757
1850
  async function validateTraceability(root, config) {
@@ -1772,7 +1865,7 @@ async function validateTraceability(root, config) {
1772
1865
  const contractIndex = await buildContractIndex(root, config);
1773
1866
  const contractIds = contractIndex.ids;
1774
1867
  for (const file of specFiles) {
1775
- const text = await readFile9(file, "utf-8");
1868
+ const text = await readFile10(file, "utf-8");
1776
1869
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1777
1870
  const parsed = parseSpec(text, file);
1778
1871
  if (parsed.specId) {
@@ -1809,7 +1902,7 @@ async function validateTraceability(root, config) {
1809
1902
  }
1810
1903
  }
1811
1904
  for (const file of scenarioFiles) {
1812
- const text = await readFile9(file, "utf-8");
1905
+ const text = await readFile10(file, "utf-8");
1813
1906
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1814
1907
  const { document, errors } = parseScenarioDocument(text, file);
1815
1908
  if (!document || errors.length > 0) {
@@ -1823,7 +1916,7 @@ async function validateTraceability(root, config) {
1823
1916
  }
1824
1917
  const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
1825
1918
  const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
1826
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
1919
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE5.test(tag));
1827
1920
  brTags.forEach((id) => brIdsInScenarios.add(id));
1828
1921
  scTags.forEach((id) => scIdsInScenarios.add(id));
1829
1922
  atom.contractIds.forEach((id) => scenarioContractIds.add(id));
@@ -1951,6 +2044,25 @@ async function validateTraceability(root, config) {
1951
2044
  );
1952
2045
  }
1953
2046
  }
2047
+ if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2048
+ const scTestRefs = await collectScTestReferences(testsRoot);
2049
+ const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2050
+ const refs = scTestRefs.get(id);
2051
+ return !refs || refs.size === 0;
2052
+ });
2053
+ if (scWithoutTests.length > 0) {
2054
+ issues.push(
2055
+ issue6(
2056
+ "QFAI-TRACE-010",
2057
+ `SC \u304C tests \u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(", ")}\u3002tests/ \u914D\u4E0B\u306E\u30C6\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\uFF08.ts/.tsx/.js/.jsx\uFF09\u306B SC ID \u3092\u30B3\u30E1\u30F3\u30C8\u307E\u305F\u306F\u30B3\u30FC\u30C9\u3067\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
2058
+ config.validation.traceability.scNoTestSeverity,
2059
+ testsRoot,
2060
+ "traceability.scMustHaveTest",
2061
+ scWithoutTests
2062
+ )
2063
+ );
2064
+ }
2065
+ }
1954
2066
  if (!config.validation.traceability.allowOrphanContracts) {
1955
2067
  if (contractIds.size > 0) {
1956
2068
  const orphanContracts = Array.from(contractIds).filter(
@@ -1999,7 +2111,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1999
2111
  const pattern = buildIdPattern(Array.from(upstreamIds));
2000
2112
  let found = false;
2001
2113
  for (const file of targetFiles) {
2002
- const text = await readFile9(file, "utf-8");
2114
+ const text = await readFile10(file, "utf-8");
2003
2115
  if (pattern.test(text)) {
2004
2116
  found = true;
2005
2117
  break;
@@ -2055,7 +2167,6 @@ async function validateProject(root, configResult) {
2055
2167
  ];
2056
2168
  const toolVersion = await resolveToolVersion();
2057
2169
  return {
2058
- schemaVersion: VALIDATION_SCHEMA_VERSION,
2059
2170
  toolVersion,
2060
2171
  issues,
2061
2172
  counts: countIssues(issues)
@@ -2107,6 +2218,9 @@ async function createReportData(root, validation, configResult) {
2107
2218
  srcRoot,
2108
2219
  testsRoot
2109
2220
  );
2221
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2222
+ const scTestRefs = await collectScTestReferences(testsRoot);
2223
+ const scCoverage = buildScCoverage(scIds, scTestRefs);
2110
2224
  const resolvedValidation = validation ?? await validateProject(root, resolved);
2111
2225
  const version = await resolveToolVersion();
2112
2226
  return {
@@ -2135,7 +2249,8 @@ async function createReportData(root, validation, configResult) {
2135
2249
  },
2136
2250
  traceability: {
2137
2251
  upstreamIdsFound: upstreamIds.size,
2138
- referencedInCodeOrTests: traceability
2252
+ referencedInCodeOrTests: traceability,
2253
+ sc: scCoverage
2139
2254
  },
2140
2255
  issues: resolvedValidation.issues
2141
2256
  };
@@ -2172,6 +2287,32 @@ function formatReportMarkdown(data) {
2172
2287
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2173
2288
  );
2174
2289
  lines.push("");
2290
+ lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2291
+ lines.push(`- total: ${data.traceability.sc.total}`);
2292
+ lines.push(`- covered: ${data.traceability.sc.covered}`);
2293
+ lines.push(`- missing: ${data.traceability.sc.missing}`);
2294
+ if (data.traceability.sc.missingIds.length === 0) {
2295
+ lines.push("- missingIds: (none)");
2296
+ } else {
2297
+ lines.push(`- missingIds: ${data.traceability.sc.missingIds.join(", ")}`);
2298
+ }
2299
+ lines.push("");
2300
+ lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
2301
+ const scRefs = data.traceability.sc.refs;
2302
+ const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
2303
+ if (scIds.length === 0) {
2304
+ lines.push("- (none)");
2305
+ } else {
2306
+ for (const scId of scIds) {
2307
+ const refs = scRefs[scId] ?? [];
2308
+ if (refs.length === 0) {
2309
+ lines.push(`- ${scId}: (none)`);
2310
+ } else {
2311
+ lines.push(`- ${scId}: ${refs.join(", ")}`);
2312
+ }
2313
+ }
2314
+ }
2315
+ lines.push("");
2175
2316
  lines.push("## Hotspots");
2176
2317
  const hotspots = buildHotspots(data.issues);
2177
2318
  if (hotspots.length === 0) {
@@ -2226,25 +2367,25 @@ async function collectIds(files) {
2226
2367
  DATA: /* @__PURE__ */ new Set()
2227
2368
  };
2228
2369
  for (const file of files) {
2229
- const text = await readFile10(file, "utf-8");
2370
+ const text = await readFile11(file, "utf-8");
2230
2371
  for (const prefix of ID_PREFIXES2) {
2231
2372
  const ids = extractIds(text, prefix);
2232
2373
  ids.forEach((id) => result[prefix].add(id));
2233
2374
  }
2234
2375
  }
2235
2376
  return {
2236
- SPEC: toSortedArray(result.SPEC),
2237
- BR: toSortedArray(result.BR),
2238
- SC: toSortedArray(result.SC),
2239
- UI: toSortedArray(result.UI),
2240
- API: toSortedArray(result.API),
2241
- DATA: toSortedArray(result.DATA)
2377
+ SPEC: toSortedArray2(result.SPEC),
2378
+ BR: toSortedArray2(result.BR),
2379
+ SC: toSortedArray2(result.SC),
2380
+ UI: toSortedArray2(result.UI),
2381
+ API: toSortedArray2(result.API),
2382
+ DATA: toSortedArray2(result.DATA)
2242
2383
  };
2243
2384
  }
2244
2385
  async function collectUpstreamIds(files) {
2245
2386
  const ids = /* @__PURE__ */ new Set();
2246
2387
  for (const file of files) {
2247
- const text = await readFile10(file, "utf-8");
2388
+ const text = await readFile11(file, "utf-8");
2248
2389
  extractAllIds(text).forEach((id) => ids.add(id));
2249
2390
  }
2250
2391
  return ids;
@@ -2265,7 +2406,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
2265
2406
  }
2266
2407
  const pattern = buildIdPattern2(Array.from(upstreamIds));
2267
2408
  for (const file of targetFiles) {
2268
- const text = await readFile10(file, "utf-8");
2409
+ const text = await readFile11(file, "utf-8");
2269
2410
  if (pattern.test(text)) {
2270
2411
  return true;
2271
2412
  }
@@ -2282,7 +2423,7 @@ function formatIdLine(label, values) {
2282
2423
  }
2283
2424
  return `- ${label}: ${values.join(", ")}`;
2284
2425
  }
2285
- function toSortedArray(values) {
2426
+ function toSortedArray2(values) {
2286
2427
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2287
2428
  }
2288
2429
  function buildHotspots(issues) {
@@ -2307,7 +2448,6 @@ function buildHotspots(issues) {
2307
2448
  );
2308
2449
  }
2309
2450
  export {
2310
- VALIDATION_SCHEMA_VERSION,
2311
2451
  createReportData,
2312
2452
  defaultConfig,
2313
2453
  extractAllIds,