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.
@@ -9,6 +9,11 @@ validation:
9
9
  failOn: error
10
10
  require:
11
11
  specSections: []
12
+ testStrategy:
13
+ requireLayerTags: false
14
+ requireSizeTags: false
15
+ maxE2eScenarioRatio: null
16
+ maxE2eScenarioCount: null
12
17
  traceability:
13
18
  brMustHaveSc: true
14
19
  scMustHaveTest: true
@@ -49,6 +49,12 @@ var defaultConfig = {
49
49
  require: {
50
50
  specSections: []
51
51
  },
52
+ testStrategy: {
53
+ requireLayerTags: false,
54
+ requireSizeTags: false,
55
+ maxE2eScenarioRatio: null,
56
+ maxE2eScenarioCount: null
57
+ },
52
58
  traceability: {
53
59
  brMustHaveSc: true,
54
60
  scMustHaveTest: true,
@@ -215,6 +221,20 @@ function normalizeValidation(raw, configPath, issues) {
215
221
  );
216
222
  traceabilityRaw = void 0;
217
223
  }
224
+ let testStrategyRaw;
225
+ if (raw.testStrategy === void 0) {
226
+ testStrategyRaw = void 0;
227
+ } else if (isRecord(raw.testStrategy)) {
228
+ testStrategyRaw = raw.testStrategy;
229
+ } else {
230
+ issues.push(
231
+ configIssue(
232
+ configPath,
233
+ "validation.testStrategy \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
234
+ )
235
+ );
236
+ testStrategyRaw = void 0;
237
+ }
218
238
  return {
219
239
  failOn: readFailOn(
220
240
  raw.failOn,
@@ -232,6 +252,36 @@ function normalizeValidation(raw, configPath, issues) {
232
252
  issues
233
253
  )
234
254
  },
255
+ testStrategy: {
256
+ requireLayerTags: readBoolean(
257
+ testStrategyRaw?.requireLayerTags,
258
+ base.testStrategy.requireLayerTags,
259
+ "validation.testStrategy.requireLayerTags",
260
+ configPath,
261
+ issues
262
+ ),
263
+ requireSizeTags: readBoolean(
264
+ testStrategyRaw?.requireSizeTags,
265
+ base.testStrategy.requireSizeTags,
266
+ "validation.testStrategy.requireSizeTags",
267
+ configPath,
268
+ issues
269
+ ),
270
+ maxE2eScenarioRatio: readOptionalRatio(
271
+ testStrategyRaw?.maxE2eScenarioRatio,
272
+ base.testStrategy.maxE2eScenarioRatio,
273
+ "validation.testStrategy.maxE2eScenarioRatio",
274
+ configPath,
275
+ issues
276
+ ),
277
+ maxE2eScenarioCount: readOptionalNonNegativeInt(
278
+ testStrategyRaw?.maxE2eScenarioCount,
279
+ base.testStrategy.maxE2eScenarioCount,
280
+ "validation.testStrategy.maxE2eScenarioCount",
281
+ configPath,
282
+ issues
283
+ )
284
+ },
235
285
  traceability: {
236
286
  brMustHaveSc: readBoolean(
237
287
  traceabilityRaw?.brMustHaveSc,
@@ -317,6 +367,36 @@ function readString(value, fallback, label, configPath, issues) {
317
367
  }
318
368
  return fallback;
319
369
  }
370
+ function readOptionalRatio(value, fallback, label, configPath, issues) {
371
+ if (value === void 0) {
372
+ return fallback;
373
+ }
374
+ if (value === null) {
375
+ return null;
376
+ }
377
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1) {
378
+ return value;
379
+ }
380
+ issues.push(
381
+ configIssue(configPath, `${label} \u306F 0\u301C1 \u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`)
382
+ );
383
+ return fallback;
384
+ }
385
+ function readOptionalNonNegativeInt(value, fallback, label, configPath, issues) {
386
+ if (value === void 0) {
387
+ return fallback;
388
+ }
389
+ if (value === null) {
390
+ return null;
391
+ }
392
+ if (typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0) {
393
+ return value;
394
+ }
395
+ issues.push(
396
+ configIssue(configPath, `${label} \u306F 0 \u4EE5\u4E0A\u306E\u6574\u6570\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`)
397
+ );
398
+ return fallback;
399
+ }
320
400
  function readStringArray(value, fallback, label, configPath, issues) {
321
401
  if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
322
402
  return value;
@@ -1073,8 +1153,8 @@ var import_promises7 = require("fs/promises");
1073
1153
  var import_node_path9 = __toESM(require("path"), 1);
1074
1154
  var import_node_url2 = require("url");
1075
1155
  async function resolveToolVersion() {
1076
- if ("1.2.2".length > 0) {
1077
- return "1.2.2";
1156
+ if ("1.2.3".length > 0) {
1157
+ return "1.2.3";
1078
1158
  }
1079
1159
  try {
1080
1160
  const packagePath = resolvePackageJsonPath();
@@ -3677,7 +3757,7 @@ async function validateScenarios(root, config) {
3677
3757
  ...collectStrategyCandidates(text, entry.scenarioPath)
3678
3758
  );
3679
3759
  }
3680
- issues.push(...buildStrategyIssues(strategyCandidates));
3760
+ issues.push(...buildStrategyIssues(strategyCandidates, config));
3681
3761
  return issues;
3682
3762
  }
3683
3763
  function validateScenarioContent(text, file) {
@@ -3880,22 +3960,39 @@ function collectStrategyCandidates(text, file) {
3880
3960
  label: buildScenarioLabel(file, scenario.tags, scenario.name),
3881
3961
  hasAnyTag: strategy.hasAnyTag,
3882
3962
  adopted: strategy.isAdopted,
3883
- missing: !strategy.isAdopted
3963
+ missing: !strategy.isAdopted,
3964
+ layerBucket: classifyLayer(scenario.tags),
3965
+ sizeBucket: classifySize(scenario.tags)
3884
3966
  };
3885
3967
  });
3886
3968
  }
3887
- function buildStrategyIssues(candidates) {
3969
+ function buildStrategyIssues(candidates, config) {
3888
3970
  const totalScenarios = candidates.length;
3889
3971
  if (totalScenarios === 0) {
3890
3972
  return [];
3891
3973
  }
3892
3974
  const adoptedCount = candidates.filter((item) => item.adopted).length;
3893
3975
  const missingItems = candidates.filter((item) => item.missing);
3976
+ const missingLayerItems = candidates.filter(
3977
+ (item) => item.layerBucket === "none"
3978
+ );
3979
+ const missingSizeItems = candidates.filter(
3980
+ (item) => item.sizeBucket === "none"
3981
+ );
3894
3982
  const hasAnyStrategyTag = candidates.some((item) => item.hasAnyTag);
3895
3983
  const missingSamples = missingItems.map((item) => item.label).slice(0, STRATEGY_SAMPLE_LIMIT);
3984
+ const missingLayerSamples = missingLayerItems.map((item) => item.label).slice(0, STRATEGY_SAMPLE_LIMIT);
3985
+ const missingSizeSamples = missingSizeItems.map((item) => item.label).slice(0, STRATEGY_SAMPLE_LIMIT);
3896
3986
  const adoptionRate = totalScenarios === 0 ? "0%" : `${Math.round(adoptedCount / totalScenarios * 100)}%`;
3987
+ const e2eCount = candidates.filter(
3988
+ (item) => item.layerBucket === "e2e"
3989
+ ).length;
3990
+ const e2eRatio = totalScenarios === 0 ? 0 : e2eCount / totalScenarios;
3991
+ const maxE2eRatio = config.validation.testStrategy.maxE2eScenarioRatio;
3992
+ const maxE2eCount = config.validation.testStrategy.maxE2eScenarioCount;
3993
+ const issues = [];
3897
3994
  if (!hasAnyStrategyTag) {
3898
- return [
3995
+ issues.push(
3899
3996
  issue(
3900
3997
  "QFAI-TS-100",
3901
3998
  "\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",
@@ -3903,10 +4000,10 @@ function buildStrategyIssues(candidates) {
3903
4000
  void 0,
3904
4001
  "scenario.testStrategyAllMissing"
3905
4002
  )
3906
- ];
4003
+ );
3907
4004
  }
3908
- if (adoptedCount < totalScenarios) {
3909
- return [
4005
+ if (hasAnyStrategyTag && adoptedCount < totalScenarios) {
4006
+ issues.push(
3910
4007
  issue(
3911
4008
  "QFAI-TS-101",
3912
4009
  `layer/size \u30BF\u30B0\u306E\u90E8\u5206\u5C0E\u5165\u3067\u3059: missing=${missingItems.length}, total=${totalScenarios}, adopted=${adoptedCount}, adoptionRate=${adoptionRate}, samples=${missingSamples.join(
@@ -3917,9 +4014,59 @@ function buildStrategyIssues(candidates) {
3917
4014
  "scenario.testStrategyPartialAdoption",
3918
4015
  missingSamples
3919
4016
  )
3920
- ];
4017
+ );
3921
4018
  }
3922
- return [];
4019
+ if (config.validation.testStrategy.requireLayerTags && missingLayerItems.length > 0) {
4020
+ issues.push(
4021
+ issue(
4022
+ "QFAI-TS-102",
4023
+ `layer \u30BF\u30B0\u304C\u672A\u8A2D\u5B9A\u3067\u3059: missing=${missingLayerItems.length}, total=${totalScenarios}, samples=${missingLayerSamples.join(
4024
+ ", "
4025
+ )}`,
4026
+ "warning",
4027
+ void 0,
4028
+ "scenario.testStrategyRequireLayerTags",
4029
+ missingLayerSamples
4030
+ )
4031
+ );
4032
+ }
4033
+ if (config.validation.testStrategy.requireSizeTags && missingSizeItems.length > 0) {
4034
+ issues.push(
4035
+ issue(
4036
+ "QFAI-TS-103",
4037
+ `size \u30BF\u30B0\u304C\u672A\u8A2D\u5B9A\u3067\u3059: missing=${missingSizeItems.length}, total=${totalScenarios}, samples=${missingSizeSamples.join(
4038
+ ", "
4039
+ )}`,
4040
+ "warning",
4041
+ void 0,
4042
+ "scenario.testStrategyRequireSizeTags",
4043
+ missingSizeSamples
4044
+ )
4045
+ );
4046
+ }
4047
+ if (maxE2eRatio !== null && e2eRatio > maxE2eRatio) {
4048
+ issues.push(
4049
+ issue(
4050
+ "QFAI-TS-110",
4051
+ `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}%)`,
4052
+ "warning",
4053
+ void 0,
4054
+ "scenario.testStrategyMaxE2eRatio"
4055
+ )
4056
+ );
4057
+ }
4058
+ if (maxE2eCount !== null && e2eCount > maxE2eCount) {
4059
+ issues.push(
4060
+ issue(
4061
+ "QFAI-TS-111",
4062
+ `layer-e2e \u306E\u4EF6\u6570\u304C\u4E0A\u9650\u3092\u8D85\u904E\u3057\u3066\u3044\u307E\u3059: e2e=${e2eCount} (max=${maxE2eCount})`,
4063
+ "warning",
4064
+ void 0,
4065
+ "scenario.testStrategyMaxE2eCount"
4066
+ )
4067
+ );
4068
+ }
4069
+ return issues;
3923
4070
  }
3924
4071
  async function fileExists(target) {
3925
4072
  try {
@@ -4400,6 +4547,17 @@ async function validateTraceability(root, config) {
4400
4547
  }
4401
4548
  const specInfo = scenarioToSpec.get(file);
4402
4549
  if (specInfo && specInfo.contractRefs.lines.length > 0) {
4550
+ if (!specInfo.contractRefs.hasNone && specInfo.contractRefs.ids.length > 0 && scenarioContractRefs.hasNone) {
4551
+ issues.push(
4552
+ issue(
4553
+ "QFAI-TRACE-036",
4554
+ `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"})`,
4555
+ "warning",
4556
+ file,
4557
+ "traceability.scenarioContractRefNone"
4558
+ )
4559
+ );
4560
+ }
4403
4561
  if (specInfo.contractRefs.hasNone && scenarioContractRefs.ids.length > 0) {
4404
4562
  issues.push(
4405
4563
  issue(
@@ -4857,6 +5015,7 @@ async function createReportData(root, validation, configResult) {
4857
5015
  const testStrategy = await collectTestStrategy(
4858
5016
  scenarioFiles,
4859
5017
  resolvedRoot,
5018
+ config,
4860
5019
  REPORT_TEST_STRATEGY_SAMPLE_LIMIT
4861
5020
  );
4862
5021
  const {
@@ -5275,6 +5434,27 @@ function formatReportMarkdown(data, options = {}) {
5275
5434
  `- 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}`
5276
5435
  );
5277
5436
  lines.push("");
5437
+ const e2eHeading = data.testStrategy.e2e.ratioExceeded || data.testStrategy.e2e.countExceeded ? "### E2E guardrails (warning)" : "### E2E guardrails";
5438
+ lines.push(e2eHeading);
5439
+ lines.push("");
5440
+ lines.push(
5441
+ `- e2e: ${data.testStrategy.e2e.count} / ${data.testStrategy.totalScenarios} (ratio=${formatPercent(data.testStrategy.e2e.ratio)})`
5442
+ );
5443
+ lines.push(
5444
+ `- maxRatio: ${formatOptionalPercent(data.testStrategy.e2e.maxRatio)}`
5445
+ );
5446
+ lines.push(
5447
+ `- maxCount: ${formatOptionalNumber(data.testStrategy.e2e.maxCount)}`
5448
+ );
5449
+ if (data.testStrategy.e2e.ratioExceeded || data.testStrategy.e2e.countExceeded) {
5450
+ if (data.testStrategy.e2e.ratioExceeded) {
5451
+ lines.push("- warning: layer-e2e \u306E\u6BD4\u7387\u304C\u4E0A\u9650\u3092\u8D85\u904E\u3057\u3066\u3044\u307E\u3059\u3002");
5452
+ }
5453
+ if (data.testStrategy.e2e.countExceeded) {
5454
+ lines.push("- warning: layer-e2e \u306E\u4EF6\u6570\u304C\u4E0A\u9650\u3092\u8D85\u904E\u3057\u3066\u3044\u307E\u3059\u3002");
5455
+ }
5456
+ }
5457
+ lines.push("");
5278
5458
  lines.push("### Missing layer tags");
5279
5459
  lines.push("");
5280
5460
  lines.push(
@@ -5589,6 +5769,21 @@ function formatList(values) {
5589
5769
  }
5590
5770
  return values.join(", ");
5591
5771
  }
5772
+ function formatOptionalPercent(value) {
5773
+ if (value === null) {
5774
+ return "(unset)";
5775
+ }
5776
+ return formatPercent(value);
5777
+ }
5778
+ function formatOptionalNumber(value) {
5779
+ if (value === null) {
5780
+ return "(unset)";
5781
+ }
5782
+ return String(value);
5783
+ }
5784
+ function formatPercent(value) {
5785
+ return `${Math.round(value * 1e3) / 10}%`;
5786
+ }
5592
5787
  function formatMarkdownTable(headers, rows) {
5593
5788
  const widths = headers.map((header, index) => {
5594
5789
  const candidates = rows.map((row) => row[index] ?? "");
@@ -5681,7 +5876,7 @@ async function countScenarios(scenarioFiles) {
5681
5876
  }
5682
5877
  return total;
5683
5878
  }
5684
- async function collectTestStrategy(scenarioFiles, root, limit) {
5879
+ async function collectTestStrategy(scenarioFiles, root, config, limit) {
5685
5880
  const layerCounts = {
5686
5881
  unit: 0,
5687
5882
  component: 0,
@@ -5701,6 +5896,7 @@ async function collectTestStrategy(scenarioFiles, root, limit) {
5701
5896
  const missingLayer = [];
5702
5897
  const missingSize = [];
5703
5898
  let totalScenarios = 0;
5899
+ let e2eCount = 0;
5704
5900
  for (const file of scenarioFiles) {
5705
5901
  const text = await (0, import_promises21.readFile)(file, "utf-8");
5706
5902
  const { document, errors } = parseScenarioDocument(text, file);
@@ -5720,6 +5916,9 @@ async function collectTestStrategy(scenarioFiles, root, limit) {
5720
5916
  if (layerBucket === "none") {
5721
5917
  missingLayer.push(label);
5722
5918
  }
5919
+ if (layerBucket === "e2e") {
5920
+ e2eCount += 1;
5921
+ }
5723
5922
  const sizeBucket = classifySize(scenario.tags);
5724
5923
  sizeCounts[sizeBucket] += 1;
5725
5924
  if (sizeBucket === "none") {
@@ -5729,6 +5928,9 @@ async function collectTestStrategy(scenarioFiles, root, limit) {
5729
5928
  }
5730
5929
  const layerSamples = missingLayer.slice(0, limit);
5731
5930
  const sizeSamples = missingSize.slice(0, limit);
5931
+ const ratio = totalScenarios === 0 ? 0 : e2eCount / totalScenarios;
5932
+ const maxRatio = config.validation.testStrategy.maxE2eScenarioRatio;
5933
+ const maxCount = config.validation.testStrategy.maxE2eScenarioCount;
5732
5934
  return {
5733
5935
  totalScenarios,
5734
5936
  limit,
@@ -5745,6 +5947,14 @@ async function collectTestStrategy(scenarioFiles, root, limit) {
5745
5947
  samples: sizeSamples,
5746
5948
  truncated: missingSize.length > sizeSamples.length
5747
5949
  }
5950
+ },
5951
+ e2e: {
5952
+ count: e2eCount,
5953
+ ratio,
5954
+ maxRatio,
5955
+ maxCount,
5956
+ ratioExceeded: maxRatio !== null && ratio > maxRatio,
5957
+ countExceeded: maxCount !== null && e2eCount > maxCount
5748
5958
  }
5749
5959
  };
5750
5960
  }