qfai 1.2.2 → 1.2.3

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.
@@ -26,6 +26,12 @@ var defaultConfig = {
26
26
  require: {
27
27
  specSections: []
28
28
  },
29
+ testStrategy: {
30
+ requireLayerTags: false,
31
+ requireSizeTags: false,
32
+ maxE2eScenarioRatio: null,
33
+ maxE2eScenarioCount: null
34
+ },
29
35
  traceability: {
30
36
  brMustHaveSc: true,
31
37
  scMustHaveTest: true,
@@ -192,6 +198,20 @@ function normalizeValidation(raw, configPath, issues) {
192
198
  );
193
199
  traceabilityRaw = void 0;
194
200
  }
201
+ let testStrategyRaw;
202
+ if (raw.testStrategy === void 0) {
203
+ testStrategyRaw = void 0;
204
+ } else if (isRecord(raw.testStrategy)) {
205
+ testStrategyRaw = raw.testStrategy;
206
+ } else {
207
+ issues.push(
208
+ configIssue(
209
+ configPath,
210
+ "validation.testStrategy \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
211
+ )
212
+ );
213
+ testStrategyRaw = void 0;
214
+ }
195
215
  return {
196
216
  failOn: readFailOn(
197
217
  raw.failOn,
@@ -209,6 +229,36 @@ function normalizeValidation(raw, configPath, issues) {
209
229
  issues
210
230
  )
211
231
  },
232
+ testStrategy: {
233
+ requireLayerTags: readBoolean(
234
+ testStrategyRaw?.requireLayerTags,
235
+ base.testStrategy.requireLayerTags,
236
+ "validation.testStrategy.requireLayerTags",
237
+ configPath,
238
+ issues
239
+ ),
240
+ requireSizeTags: readBoolean(
241
+ testStrategyRaw?.requireSizeTags,
242
+ base.testStrategy.requireSizeTags,
243
+ "validation.testStrategy.requireSizeTags",
244
+ configPath,
245
+ issues
246
+ ),
247
+ maxE2eScenarioRatio: readOptionalRatio(
248
+ testStrategyRaw?.maxE2eScenarioRatio,
249
+ base.testStrategy.maxE2eScenarioRatio,
250
+ "validation.testStrategy.maxE2eScenarioRatio",
251
+ configPath,
252
+ issues
253
+ ),
254
+ maxE2eScenarioCount: readOptionalNonNegativeInt(
255
+ testStrategyRaw?.maxE2eScenarioCount,
256
+ base.testStrategy.maxE2eScenarioCount,
257
+ "validation.testStrategy.maxE2eScenarioCount",
258
+ configPath,
259
+ issues
260
+ )
261
+ },
212
262
  traceability: {
213
263
  brMustHaveSc: readBoolean(
214
264
  traceabilityRaw?.brMustHaveSc,
@@ -294,6 +344,36 @@ function readString(value, fallback, label, configPath, issues) {
294
344
  }
295
345
  return fallback;
296
346
  }
347
+ function readOptionalRatio(value, fallback, label, configPath, issues) {
348
+ if (value === void 0) {
349
+ return fallback;
350
+ }
351
+ if (value === null) {
352
+ return null;
353
+ }
354
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1) {
355
+ return value;
356
+ }
357
+ issues.push(
358
+ configIssue(configPath, `${label} \u306F 0\u301C1 \u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`)
359
+ );
360
+ return fallback;
361
+ }
362
+ function readOptionalNonNegativeInt(value, fallback, label, configPath, issues) {
363
+ if (value === void 0) {
364
+ return fallback;
365
+ }
366
+ if (value === null) {
367
+ return null;
368
+ }
369
+ if (typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0) {
370
+ return value;
371
+ }
372
+ issues.push(
373
+ configIssue(configPath, `${label} \u306F 0 \u4EE5\u4E0A\u306E\u6574\u6570\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`)
374
+ );
375
+ return fallback;
376
+ }
297
377
  function readStringArray(value, fallback, label, configPath, issues) {
298
378
  if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
299
379
  return value;
@@ -1054,8 +1134,8 @@ import { readFile as readFile4 } from "fs/promises";
1054
1134
  import path9 from "path";
1055
1135
  import { fileURLToPath as fileURLToPath2 } from "url";
1056
1136
  async function resolveToolVersion() {
1057
- if ("1.2.2".length > 0) {
1058
- return "1.2.2";
1137
+ if ("1.2.3".length > 0) {
1138
+ return "1.2.3";
1059
1139
  }
1060
1140
  try {
1061
1141
  const packagePath = resolvePackageJsonPath();
@@ -3658,7 +3738,7 @@ async function validateScenarios(root, config) {
3658
3738
  ...collectStrategyCandidates(text, entry.scenarioPath)
3659
3739
  );
3660
3740
  }
3661
- issues.push(...buildStrategyIssues(strategyCandidates));
3741
+ issues.push(...buildStrategyIssues(strategyCandidates, config));
3662
3742
  return issues;
3663
3743
  }
3664
3744
  function validateScenarioContent(text, file) {
@@ -3861,22 +3941,39 @@ function collectStrategyCandidates(text, file) {
3861
3941
  label: buildScenarioLabel(file, scenario.tags, scenario.name),
3862
3942
  hasAnyTag: strategy.hasAnyTag,
3863
3943
  adopted: strategy.isAdopted,
3864
- missing: !strategy.isAdopted
3944
+ missing: !strategy.isAdopted,
3945
+ layerBucket: classifyLayer(scenario.tags),
3946
+ sizeBucket: classifySize(scenario.tags)
3865
3947
  };
3866
3948
  });
3867
3949
  }
3868
- function buildStrategyIssues(candidates) {
3950
+ function buildStrategyIssues(candidates, config) {
3869
3951
  const totalScenarios = candidates.length;
3870
3952
  if (totalScenarios === 0) {
3871
3953
  return [];
3872
3954
  }
3873
3955
  const adoptedCount = candidates.filter((item) => item.adopted).length;
3874
3956
  const missingItems = candidates.filter((item) => item.missing);
3957
+ const missingLayerItems = candidates.filter(
3958
+ (item) => item.layerBucket === "none"
3959
+ );
3960
+ const missingSizeItems = candidates.filter(
3961
+ (item) => item.sizeBucket === "none"
3962
+ );
3875
3963
  const hasAnyStrategyTag = candidates.some((item) => item.hasAnyTag);
3876
3964
  const missingSamples = missingItems.map((item) => item.label).slice(0, STRATEGY_SAMPLE_LIMIT);
3965
+ const missingLayerSamples = missingLayerItems.map((item) => item.label).slice(0, STRATEGY_SAMPLE_LIMIT);
3966
+ const missingSizeSamples = missingSizeItems.map((item) => item.label).slice(0, STRATEGY_SAMPLE_LIMIT);
3877
3967
  const adoptionRate = totalScenarios === 0 ? "0%" : `${Math.round(adoptedCount / totalScenarios * 100)}%`;
3968
+ const e2eCount = candidates.filter(
3969
+ (item) => item.layerBucket === "e2e"
3970
+ ).length;
3971
+ const e2eRatio = totalScenarios === 0 ? 0 : e2eCount / totalScenarios;
3972
+ const maxE2eRatio = config.validation.testStrategy.maxE2eScenarioRatio;
3973
+ const maxE2eCount = config.validation.testStrategy.maxE2eScenarioCount;
3974
+ const issues = [];
3878
3975
  if (!hasAnyStrategyTag) {
3879
- return [
3976
+ issues.push(
3880
3977
  issue(
3881
3978
  "QFAI-TS-100",
3882
3979
  "\u5168\u3066\u306E Scenario \u306B layer/size \u30BF\u30B0\u304C\u3042\u308A\u307E\u305B\u3093\u3002\u5C0E\u5165\u306F\u4EFB\u610F\u3067\u3059\uFF08opt-in\uFF09\u3002",
@@ -3884,10 +3981,10 @@ function buildStrategyIssues(candidates) {
3884
3981
  void 0,
3885
3982
  "scenario.testStrategyAllMissing"
3886
3983
  )
3887
- ];
3984
+ );
3888
3985
  }
3889
- if (adoptedCount < totalScenarios) {
3890
- return [
3986
+ if (hasAnyStrategyTag && adoptedCount < totalScenarios) {
3987
+ issues.push(
3891
3988
  issue(
3892
3989
  "QFAI-TS-101",
3893
3990
  `layer/size \u30BF\u30B0\u306E\u90E8\u5206\u5C0E\u5165\u3067\u3059: missing=${missingItems.length}, total=${totalScenarios}, adopted=${adoptedCount}, adoptionRate=${adoptionRate}, samples=${missingSamples.join(
@@ -3898,9 +3995,59 @@ function buildStrategyIssues(candidates) {
3898
3995
  "scenario.testStrategyPartialAdoption",
3899
3996
  missingSamples
3900
3997
  )
3901
- ];
3998
+ );
3902
3999
  }
3903
- return [];
4000
+ if (config.validation.testStrategy.requireLayerTags && missingLayerItems.length > 0) {
4001
+ issues.push(
4002
+ issue(
4003
+ "QFAI-TS-102",
4004
+ `layer \u30BF\u30B0\u304C\u672A\u8A2D\u5B9A\u3067\u3059: missing=${missingLayerItems.length}, total=${totalScenarios}, samples=${missingLayerSamples.join(
4005
+ ", "
4006
+ )}`,
4007
+ "warning",
4008
+ void 0,
4009
+ "scenario.testStrategyRequireLayerTags",
4010
+ missingLayerSamples
4011
+ )
4012
+ );
4013
+ }
4014
+ if (config.validation.testStrategy.requireSizeTags && missingSizeItems.length > 0) {
4015
+ issues.push(
4016
+ issue(
4017
+ "QFAI-TS-103",
4018
+ `size \u30BF\u30B0\u304C\u672A\u8A2D\u5B9A\u3067\u3059: missing=${missingSizeItems.length}, total=${totalScenarios}, samples=${missingSizeSamples.join(
4019
+ ", "
4020
+ )}`,
4021
+ "warning",
4022
+ void 0,
4023
+ "scenario.testStrategyRequireSizeTags",
4024
+ missingSizeSamples
4025
+ )
4026
+ );
4027
+ }
4028
+ if (maxE2eRatio !== null && e2eRatio > maxE2eRatio) {
4029
+ issues.push(
4030
+ issue(
4031
+ "QFAI-TS-110",
4032
+ `layer-e2e \u306E\u6BD4\u7387\u304C\u4E0A\u9650\u3092\u8D85\u904E\u3057\u3066\u3044\u307E\u3059: e2e=${e2eCount}, total=${totalScenarios}, ratio=${Math.round(e2eRatio * 1e3) / 10}% (max=${Math.round(maxE2eRatio * 1e3) / 10}%)`,
4033
+ "warning",
4034
+ void 0,
4035
+ "scenario.testStrategyMaxE2eRatio"
4036
+ )
4037
+ );
4038
+ }
4039
+ if (maxE2eCount !== null && e2eCount > maxE2eCount) {
4040
+ issues.push(
4041
+ issue(
4042
+ "QFAI-TS-111",
4043
+ `layer-e2e \u306E\u4EF6\u6570\u304C\u4E0A\u9650\u3092\u8D85\u904E\u3057\u3066\u3044\u307E\u3059: e2e=${e2eCount} (max=${maxE2eCount})`,
4044
+ "warning",
4045
+ void 0,
4046
+ "scenario.testStrategyMaxE2eCount"
4047
+ )
4048
+ );
4049
+ }
4050
+ return issues;
3904
4051
  }
3905
4052
  async function fileExists(target) {
3906
4053
  try {
@@ -4381,6 +4528,17 @@ async function validateTraceability(root, config) {
4381
4528
  }
4382
4529
  const specInfo = scenarioToSpec.get(file);
4383
4530
  if (specInfo && specInfo.contractRefs.lines.length > 0) {
4531
+ if (!specInfo.contractRefs.hasNone && specInfo.contractRefs.ids.length > 0 && scenarioContractRefs.hasNone) {
4532
+ issues.push(
4533
+ issue(
4534
+ "QFAI-TRACE-036",
4535
+ `Spec \u304C\u5951\u7D04 ID \u3092\u5217\u6319\u3057\u3066\u3044\u307E\u3059\u304C Scenario \u304C none \u3092\u6307\u5B9A\u3057\u3066\u3044\u307E\u3059 (SPEC: ${specInfo.specId ?? "unknown"})`,
4536
+ "warning",
4537
+ file,
4538
+ "traceability.scenarioContractRefNone"
4539
+ )
4540
+ );
4541
+ }
4384
4542
  if (specInfo.contractRefs.hasNone && scenarioContractRefs.ids.length > 0) {
4385
4543
  issues.push(
4386
4544
  issue(
@@ -4838,6 +4996,7 @@ async function createReportData(root, validation, configResult) {
4838
4996
  const testStrategy = await collectTestStrategy(
4839
4997
  scenarioFiles,
4840
4998
  resolvedRoot,
4999
+ config,
4841
5000
  REPORT_TEST_STRATEGY_SAMPLE_LIMIT
4842
5001
  );
4843
5002
  const {
@@ -5256,6 +5415,27 @@ function formatReportMarkdown(data, options = {}) {
5256
5415
  `- s: ${data.testStrategy.size.s} / m: ${data.testStrategy.size.m} / l: ${data.testStrategy.size.l} / none: ${data.testStrategy.size.none} / unknown: ${data.testStrategy.size.unknown}`
5257
5416
  );
5258
5417
  lines.push("");
5418
+ const e2eHeading = data.testStrategy.e2e.ratioExceeded || data.testStrategy.e2e.countExceeded ? "### E2E guardrails (warning)" : "### E2E guardrails";
5419
+ lines.push(e2eHeading);
5420
+ lines.push("");
5421
+ lines.push(
5422
+ `- e2e: ${data.testStrategy.e2e.count} / ${data.testStrategy.totalScenarios} (ratio=${formatPercent(data.testStrategy.e2e.ratio)})`
5423
+ );
5424
+ lines.push(
5425
+ `- maxRatio: ${formatOptionalPercent(data.testStrategy.e2e.maxRatio)}`
5426
+ );
5427
+ lines.push(
5428
+ `- maxCount: ${formatOptionalNumber(data.testStrategy.e2e.maxCount)}`
5429
+ );
5430
+ if (data.testStrategy.e2e.ratioExceeded || data.testStrategy.e2e.countExceeded) {
5431
+ if (data.testStrategy.e2e.ratioExceeded) {
5432
+ lines.push("- warning: layer-e2e \u306E\u6BD4\u7387\u304C\u4E0A\u9650\u3092\u8D85\u904E\u3057\u3066\u3044\u307E\u3059\u3002");
5433
+ }
5434
+ if (data.testStrategy.e2e.countExceeded) {
5435
+ lines.push("- warning: layer-e2e \u306E\u4EF6\u6570\u304C\u4E0A\u9650\u3092\u8D85\u904E\u3057\u3066\u3044\u307E\u3059\u3002");
5436
+ }
5437
+ }
5438
+ lines.push("");
5259
5439
  lines.push("### Missing layer tags");
5260
5440
  lines.push("");
5261
5441
  lines.push(
@@ -5570,6 +5750,21 @@ function formatList(values) {
5570
5750
  }
5571
5751
  return values.join(", ");
5572
5752
  }
5753
+ function formatOptionalPercent(value) {
5754
+ if (value === null) {
5755
+ return "(unset)";
5756
+ }
5757
+ return formatPercent(value);
5758
+ }
5759
+ function formatOptionalNumber(value) {
5760
+ if (value === null) {
5761
+ return "(unset)";
5762
+ }
5763
+ return String(value);
5764
+ }
5765
+ function formatPercent(value) {
5766
+ return `${Math.round(value * 1e3) / 10}%`;
5767
+ }
5573
5768
  function formatMarkdownTable(headers, rows) {
5574
5769
  const widths = headers.map((header, index) => {
5575
5770
  const candidates = rows.map((row) => row[index] ?? "");
@@ -5662,7 +5857,7 @@ async function countScenarios(scenarioFiles) {
5662
5857
  }
5663
5858
  return total;
5664
5859
  }
5665
- async function collectTestStrategy(scenarioFiles, root, limit) {
5860
+ async function collectTestStrategy(scenarioFiles, root, config, limit) {
5666
5861
  const layerCounts = {
5667
5862
  unit: 0,
5668
5863
  component: 0,
@@ -5682,6 +5877,7 @@ async function collectTestStrategy(scenarioFiles, root, limit) {
5682
5877
  const missingLayer = [];
5683
5878
  const missingSize = [];
5684
5879
  let totalScenarios = 0;
5880
+ let e2eCount = 0;
5685
5881
  for (const file of scenarioFiles) {
5686
5882
  const text = await readFile15(file, "utf-8");
5687
5883
  const { document, errors } = parseScenarioDocument(text, file);
@@ -5701,6 +5897,9 @@ async function collectTestStrategy(scenarioFiles, root, limit) {
5701
5897
  if (layerBucket === "none") {
5702
5898
  missingLayer.push(label);
5703
5899
  }
5900
+ if (layerBucket === "e2e") {
5901
+ e2eCount += 1;
5902
+ }
5704
5903
  const sizeBucket = classifySize(scenario.tags);
5705
5904
  sizeCounts[sizeBucket] += 1;
5706
5905
  if (sizeBucket === "none") {
@@ -5710,6 +5909,9 @@ async function collectTestStrategy(scenarioFiles, root, limit) {
5710
5909
  }
5711
5910
  const layerSamples = missingLayer.slice(0, limit);
5712
5911
  const sizeSamples = missingSize.slice(0, limit);
5912
+ const ratio = totalScenarios === 0 ? 0 : e2eCount / totalScenarios;
5913
+ const maxRatio = config.validation.testStrategy.maxE2eScenarioRatio;
5914
+ const maxCount = config.validation.testStrategy.maxE2eScenarioCount;
5713
5915
  return {
5714
5916
  totalScenarios,
5715
5917
  limit,
@@ -5726,6 +5928,14 @@ async function collectTestStrategy(scenarioFiles, root, limit) {
5726
5928
  samples: sizeSamples,
5727
5929
  truncated: missingSize.length > sizeSamples.length
5728
5930
  }
5931
+ },
5932
+ e2e: {
5933
+ count: e2eCount,
5934
+ ratio,
5935
+ maxRatio,
5936
+ maxCount,
5937
+ ratioExceeded: maxRatio !== null && ratio > maxRatio,
5938
+ countExceeded: maxCount !== null && e2eCount > maxCount
5729
5939
  }
5730
5940
  };
5731
5941
  }