qfai 1.2.2 → 1.2.4
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/README.md +11 -2
- package/assets/init/.qfai/assistant/agents/code-reviewer.md +2 -0
- package/assets/init/.qfai/assistant/agents/coverage-planner.md +3 -0
- package/assets/init/.qfai/assistant/agents/planner.md +2 -1
- package/assets/init/.qfai/assistant/agents/qa-engineer.md +3 -0
- package/assets/init/.qfai/assistant/agents/qa-gatekeeper.md +3 -0
- package/assets/init/.qfai/assistant/agents/qa-lead.md +3 -0
- package/assets/init/.qfai/assistant/agents/qa-reviewer.md +3 -0
- package/assets/init/.qfai/assistant/agents/test-engineer.md +2 -0
- package/assets/init/.qfai/assistant/agents/unit-test-scope-enforcer.md +2 -0
- package/assets/init/.qfai/assistant/prompts/qfai-atdd.md +33 -12
- package/assets/init/.qfai/assistant/prompts/qfai-spec.md +12 -7
- package/assets/init/.qfai/assistant/prompts/qfai-tdd-green.md +12 -4
- package/assets/init/.qfai/assistant/prompts/qfai-tdd-red.md +26 -8
- package/assets/init/.qfai/assistant/prompts/qfai-tdd-refactor.md +12 -4
- package/assets/init/root/qfai.config.yaml +6 -0
- package/dist/cli/index.cjs +325 -29
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +325 -29
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +325 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.mjs +325 -29
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -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;
|
|
@@ -810,6 +890,7 @@ async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
|
810
890
|
}
|
|
811
891
|
async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
812
892
|
const refs = /* @__PURE__ */ new Map();
|
|
893
|
+
const parseErrors = [];
|
|
813
894
|
const normalizedGlobs = normalizeGlobs(globs);
|
|
814
895
|
const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
|
|
815
896
|
const mergedExcludeGlobs = Array.from(
|
|
@@ -824,7 +905,8 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
824
905
|
matchedFileCount: 0,
|
|
825
906
|
truncated: false,
|
|
826
907
|
limit: DEFAULT_GLOB_FILE_LIMIT
|
|
827
|
-
}
|
|
908
|
+
},
|
|
909
|
+
parseErrors
|
|
828
910
|
};
|
|
829
911
|
}
|
|
830
912
|
let scanResult;
|
|
@@ -844,6 +926,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
844
926
|
truncated: false,
|
|
845
927
|
limit: DEFAULT_GLOB_FILE_LIMIT
|
|
846
928
|
},
|
|
929
|
+
parseErrors,
|
|
847
930
|
error: formatError3(error2)
|
|
848
931
|
};
|
|
849
932
|
}
|
|
@@ -852,6 +935,33 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
852
935
|
);
|
|
853
936
|
for (const file of normalizedFiles) {
|
|
854
937
|
const text = await readFile2(file, "utf-8");
|
|
938
|
+
if (file.toLowerCase().endsWith(".feature")) {
|
|
939
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
940
|
+
if (!document || errors.length > 0) {
|
|
941
|
+
parseErrors.push({
|
|
942
|
+
file,
|
|
943
|
+
errors: errors.length > 0 ? errors : ["Invalid Gherkin document."]
|
|
944
|
+
});
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
const scIds2 = /* @__PURE__ */ new Set();
|
|
948
|
+
for (const scenario of document.scenarios) {
|
|
949
|
+
for (const tag of scenario.tags) {
|
|
950
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
951
|
+
scIds2.add(tag);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (scIds2.size === 0) {
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
for (const scId of scIds2) {
|
|
959
|
+
const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
|
|
960
|
+
current.add(file);
|
|
961
|
+
refs.set(scId, current);
|
|
962
|
+
}
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
855
965
|
const scIds = extractAnnotatedScIds(text);
|
|
856
966
|
if (scIds.length === 0) {
|
|
857
967
|
continue;
|
|
@@ -870,7 +980,8 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
870
980
|
matchedFileCount: scanResult.matchedFileCount,
|
|
871
981
|
truncated: scanResult.truncated,
|
|
872
982
|
limit: scanResult.limit
|
|
873
|
-
}
|
|
983
|
+
},
|
|
984
|
+
parseErrors
|
|
874
985
|
};
|
|
875
986
|
}
|
|
876
987
|
function buildScCoverage(scIds, refs) {
|
|
@@ -1054,8 +1165,8 @@ import { readFile as readFile4 } from "fs/promises";
|
|
|
1054
1165
|
import path9 from "path";
|
|
1055
1166
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1056
1167
|
async function resolveToolVersion() {
|
|
1057
|
-
if ("1.2.
|
|
1058
|
-
return "1.2.
|
|
1168
|
+
if ("1.2.4".length > 0) {
|
|
1169
|
+
return "1.2.4";
|
|
1059
1170
|
}
|
|
1060
1171
|
try {
|
|
1061
1172
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -3658,7 +3769,7 @@ async function validateScenarios(root, config) {
|
|
|
3658
3769
|
...collectStrategyCandidates(text, entry.scenarioPath)
|
|
3659
3770
|
);
|
|
3660
3771
|
}
|
|
3661
|
-
issues.push(...buildStrategyIssues(strategyCandidates));
|
|
3772
|
+
issues.push(...buildStrategyIssues(strategyCandidates, config));
|
|
3662
3773
|
return issues;
|
|
3663
3774
|
}
|
|
3664
3775
|
function validateScenarioContent(text, file) {
|
|
@@ -3861,22 +3972,39 @@ function collectStrategyCandidates(text, file) {
|
|
|
3861
3972
|
label: buildScenarioLabel(file, scenario.tags, scenario.name),
|
|
3862
3973
|
hasAnyTag: strategy.hasAnyTag,
|
|
3863
3974
|
adopted: strategy.isAdopted,
|
|
3864
|
-
missing: !strategy.isAdopted
|
|
3975
|
+
missing: !strategy.isAdopted,
|
|
3976
|
+
layerBucket: classifyLayer(scenario.tags),
|
|
3977
|
+
sizeBucket: classifySize(scenario.tags)
|
|
3865
3978
|
};
|
|
3866
3979
|
});
|
|
3867
3980
|
}
|
|
3868
|
-
function buildStrategyIssues(candidates) {
|
|
3981
|
+
function buildStrategyIssues(candidates, config) {
|
|
3869
3982
|
const totalScenarios = candidates.length;
|
|
3870
3983
|
if (totalScenarios === 0) {
|
|
3871
3984
|
return [];
|
|
3872
3985
|
}
|
|
3873
3986
|
const adoptedCount = candidates.filter((item) => item.adopted).length;
|
|
3874
3987
|
const missingItems = candidates.filter((item) => item.missing);
|
|
3988
|
+
const missingLayerItems = candidates.filter(
|
|
3989
|
+
(item) => item.layerBucket === "none"
|
|
3990
|
+
);
|
|
3991
|
+
const missingSizeItems = candidates.filter(
|
|
3992
|
+
(item) => item.sizeBucket === "none"
|
|
3993
|
+
);
|
|
3875
3994
|
const hasAnyStrategyTag = candidates.some((item) => item.hasAnyTag);
|
|
3876
3995
|
const missingSamples = missingItems.map((item) => item.label).slice(0, STRATEGY_SAMPLE_LIMIT);
|
|
3996
|
+
const missingLayerSamples = missingLayerItems.map((item) => item.label).slice(0, STRATEGY_SAMPLE_LIMIT);
|
|
3997
|
+
const missingSizeSamples = missingSizeItems.map((item) => item.label).slice(0, STRATEGY_SAMPLE_LIMIT);
|
|
3877
3998
|
const adoptionRate = totalScenarios === 0 ? "0%" : `${Math.round(adoptedCount / totalScenarios * 100)}%`;
|
|
3999
|
+
const e2eCount = candidates.filter(
|
|
4000
|
+
(item) => item.layerBucket === "e2e"
|
|
4001
|
+
).length;
|
|
4002
|
+
const e2eRatio = totalScenarios === 0 ? 0 : e2eCount / totalScenarios;
|
|
4003
|
+
const maxE2eRatio = config.validation.testStrategy.maxE2eScenarioRatio;
|
|
4004
|
+
const maxE2eCount = config.validation.testStrategy.maxE2eScenarioCount;
|
|
4005
|
+
const issues = [];
|
|
3878
4006
|
if (!hasAnyStrategyTag) {
|
|
3879
|
-
|
|
4007
|
+
issues.push(
|
|
3880
4008
|
issue(
|
|
3881
4009
|
"QFAI-TS-100",
|
|
3882
4010
|
"\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 +4012,10 @@ function buildStrategyIssues(candidates) {
|
|
|
3884
4012
|
void 0,
|
|
3885
4013
|
"scenario.testStrategyAllMissing"
|
|
3886
4014
|
)
|
|
3887
|
-
|
|
4015
|
+
);
|
|
3888
4016
|
}
|
|
3889
|
-
if (adoptedCount < totalScenarios) {
|
|
3890
|
-
|
|
4017
|
+
if (hasAnyStrategyTag && adoptedCount < totalScenarios) {
|
|
4018
|
+
issues.push(
|
|
3891
4019
|
issue(
|
|
3892
4020
|
"QFAI-TS-101",
|
|
3893
4021
|
`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 +4026,59 @@ function buildStrategyIssues(candidates) {
|
|
|
3898
4026
|
"scenario.testStrategyPartialAdoption",
|
|
3899
4027
|
missingSamples
|
|
3900
4028
|
)
|
|
3901
|
-
|
|
4029
|
+
);
|
|
4030
|
+
}
|
|
4031
|
+
if (config.validation.testStrategy.requireLayerTags && missingLayerItems.length > 0) {
|
|
4032
|
+
issues.push(
|
|
4033
|
+
issue(
|
|
4034
|
+
"QFAI-TS-102",
|
|
4035
|
+
`layer \u30BF\u30B0\u304C\u672A\u8A2D\u5B9A\u3067\u3059: missing=${missingLayerItems.length}, total=${totalScenarios}, samples=${missingLayerSamples.join(
|
|
4036
|
+
", "
|
|
4037
|
+
)}`,
|
|
4038
|
+
"warning",
|
|
4039
|
+
void 0,
|
|
4040
|
+
"scenario.testStrategyRequireLayerTags",
|
|
4041
|
+
missingLayerSamples
|
|
4042
|
+
)
|
|
4043
|
+
);
|
|
4044
|
+
}
|
|
4045
|
+
if (config.validation.testStrategy.requireSizeTags && missingSizeItems.length > 0) {
|
|
4046
|
+
issues.push(
|
|
4047
|
+
issue(
|
|
4048
|
+
"QFAI-TS-103",
|
|
4049
|
+
`size \u30BF\u30B0\u304C\u672A\u8A2D\u5B9A\u3067\u3059: missing=${missingSizeItems.length}, total=${totalScenarios}, samples=${missingSizeSamples.join(
|
|
4050
|
+
", "
|
|
4051
|
+
)}`,
|
|
4052
|
+
"warning",
|
|
4053
|
+
void 0,
|
|
4054
|
+
"scenario.testStrategyRequireSizeTags",
|
|
4055
|
+
missingSizeSamples
|
|
4056
|
+
)
|
|
4057
|
+
);
|
|
4058
|
+
}
|
|
4059
|
+
if (maxE2eRatio !== null && e2eRatio > maxE2eRatio) {
|
|
4060
|
+
issues.push(
|
|
4061
|
+
issue(
|
|
4062
|
+
"QFAI-TS-110",
|
|
4063
|
+
`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}%)`,
|
|
4064
|
+
"warning",
|
|
4065
|
+
void 0,
|
|
4066
|
+
"scenario.testStrategyMaxE2eRatio"
|
|
4067
|
+
)
|
|
4068
|
+
);
|
|
3902
4069
|
}
|
|
3903
|
-
|
|
4070
|
+
if (maxE2eCount !== null && e2eCount > maxE2eCount) {
|
|
4071
|
+
issues.push(
|
|
4072
|
+
issue(
|
|
4073
|
+
"QFAI-TS-111",
|
|
4074
|
+
`layer-e2e \u306E\u4EF6\u6570\u304C\u4E0A\u9650\u3092\u8D85\u904E\u3057\u3066\u3044\u307E\u3059: e2e=${e2eCount} (max=${maxE2eCount})`,
|
|
4075
|
+
"warning",
|
|
4076
|
+
void 0,
|
|
4077
|
+
"scenario.testStrategyMaxE2eCount"
|
|
4078
|
+
)
|
|
4079
|
+
);
|
|
4080
|
+
}
|
|
4081
|
+
return issues;
|
|
3904
4082
|
}
|
|
3905
4083
|
async function fileExists(target) {
|
|
3906
4084
|
try {
|
|
@@ -4088,6 +4266,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
4088
4266
|
import { readFile as readFile13 } from "fs/promises";
|
|
4089
4267
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
4090
4268
|
var BR_TAG_RE2 = /^BR-\d{4}-\d{4}$/;
|
|
4269
|
+
var SAMPLE_LIMIT = 20;
|
|
4091
4270
|
async function validateTraceability(root, config) {
|
|
4092
4271
|
const issues = [];
|
|
4093
4272
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -4104,6 +4283,8 @@ async function validateTraceability(root, config) {
|
|
|
4104
4283
|
const scIdsInScenarios = /* @__PURE__ */ new Set();
|
|
4105
4284
|
const specContractIds = /* @__PURE__ */ new Set();
|
|
4106
4285
|
const specToBrIds = /* @__PURE__ */ new Map();
|
|
4286
|
+
const scIdToLayer = /* @__PURE__ */ new Map();
|
|
4287
|
+
const layerToScIds = /* @__PURE__ */ new Map();
|
|
4107
4288
|
const contractIndex = await buildContractIndex(root, config);
|
|
4108
4289
|
const contractIds = contractIndex.ids;
|
|
4109
4290
|
for (const file of specFiles) {
|
|
@@ -4236,6 +4417,7 @@ async function validateTraceability(root, config) {
|
|
|
4236
4417
|
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
4237
4418
|
const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
|
|
4238
4419
|
const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
|
|
4420
|
+
const layerBucket = classifyLayer(scenario.tags);
|
|
4239
4421
|
if (specTags.length > 1) {
|
|
4240
4422
|
issues.push(
|
|
4241
4423
|
issue(
|
|
@@ -4273,6 +4455,10 @@ async function validateTraceability(root, config) {
|
|
|
4273
4455
|
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
4274
4456
|
scTags.forEach((id) => {
|
|
4275
4457
|
scIdsInScenarios.add(id);
|
|
4458
|
+
scIdToLayer.set(id, layerBucket);
|
|
4459
|
+
const layerIds = layerToScIds.get(layerBucket) ?? /* @__PURE__ */ new Set();
|
|
4460
|
+
layerIds.add(id);
|
|
4461
|
+
layerToScIds.set(layerBucket, layerIds);
|
|
4276
4462
|
const current = scIdToScenarioInfo.get(id) ?? {
|
|
4277
4463
|
count: 0,
|
|
4278
4464
|
names: /* @__PURE__ */ new Set()
|
|
@@ -4381,6 +4567,17 @@ async function validateTraceability(root, config) {
|
|
|
4381
4567
|
}
|
|
4382
4568
|
const specInfo = scenarioToSpec.get(file);
|
|
4383
4569
|
if (specInfo && specInfo.contractRefs.lines.length > 0) {
|
|
4570
|
+
if (!specInfo.contractRefs.hasNone && specInfo.contractRefs.ids.length > 0 && scenarioContractRefs.hasNone) {
|
|
4571
|
+
issues.push(
|
|
4572
|
+
issue(
|
|
4573
|
+
"QFAI-TRACE-036",
|
|
4574
|
+
`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"})`,
|
|
4575
|
+
"warning",
|
|
4576
|
+
file,
|
|
4577
|
+
"traceability.scenarioContractRefNone"
|
|
4578
|
+
)
|
|
4579
|
+
);
|
|
4580
|
+
}
|
|
4384
4581
|
if (specInfo.contractRefs.hasNone && scenarioContractRefs.ids.length > 0) {
|
|
4385
4582
|
issues.push(
|
|
4386
4583
|
issue(
|
|
@@ -4469,9 +4666,24 @@ async function validateTraceability(root, config) {
|
|
|
4469
4666
|
);
|
|
4470
4667
|
const scTestRefs = scRefsResult.refs;
|
|
4471
4668
|
const testFileScan = scRefsResult.scan;
|
|
4669
|
+
const parseErrors = scRefsResult.parseErrors;
|
|
4472
4670
|
const hasScenarios = scIdsInScenarios.size > 0;
|
|
4473
4671
|
const hasGlobConfig = testFileScan.globs.length > 0;
|
|
4474
4672
|
const hasMatchedTests = testFileScan.matchedFileCount > 0;
|
|
4673
|
+
if (parseErrors.length > 0) {
|
|
4674
|
+
for (const entry of parseErrors) {
|
|
4675
|
+
const detail = entry.errors.join(" / ");
|
|
4676
|
+
issues.push(
|
|
4677
|
+
issue(
|
|
4678
|
+
"QFAI-TRACE-040",
|
|
4679
|
+
`\u30C6\u30B9\u30C8\u8A3C\u8DE1\u306E .feature \u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${detail}`,
|
|
4680
|
+
"error",
|
|
4681
|
+
entry.file,
|
|
4682
|
+
"traceability.testFileParse"
|
|
4683
|
+
)
|
|
4684
|
+
);
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4475
4687
|
if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
|
|
4476
4688
|
const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
|
|
4477
4689
|
issues.push(
|
|
@@ -4485,21 +4697,55 @@ async function validateTraceability(root, config) {
|
|
|
4485
4697
|
);
|
|
4486
4698
|
} else {
|
|
4487
4699
|
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
4488
|
-
const
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4700
|
+
const enforcedLayers = /* @__PURE__ */ new Set();
|
|
4701
|
+
for (const [scId, refs] of scTestRefs.entries()) {
|
|
4702
|
+
if (!refs || refs.size === 0) {
|
|
4703
|
+
continue;
|
|
4704
|
+
}
|
|
4705
|
+
const layer = scIdToLayer.get(scId);
|
|
4706
|
+
if (layer) {
|
|
4707
|
+
enforcedLayers.add(layer);
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
const deferredLayers = [];
|
|
4711
|
+
for (const [layer, scIds] of layerToScIds.entries()) {
|
|
4712
|
+
const missing = Array.from(scIds).filter((id) => {
|
|
4713
|
+
const refs = scTestRefs.get(id);
|
|
4714
|
+
return !refs || refs.size === 0;
|
|
4715
|
+
});
|
|
4716
|
+
if (missing.length === 0) {
|
|
4717
|
+
continue;
|
|
4718
|
+
}
|
|
4719
|
+
if (enforcedLayers.has(layer)) {
|
|
4720
|
+
const samples = missing.slice(0, SAMPLE_LIMIT);
|
|
4721
|
+
const truncated = samples.length < missing.length;
|
|
4722
|
+
const sampleText = samples.join(", ");
|
|
4723
|
+
const suffix = truncated ? " ..." : "";
|
|
4724
|
+
issues.push(
|
|
4725
|
+
issue(
|
|
4726
|
+
"QFAI-TRACE-010",
|
|
4727
|
+
`SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093 (layer=${layer}, missing=${missing.length}, samples=${sampleText}${suffix})\u3002testFileGlobs \u306B\u4E00\u81F4\u3059\u308B\u30C6\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\u3078 QFAI:SC-XXXX-XXXX \u307E\u305F\u306F .feature \u306E @SC-XXXX-XXXX\uFF08\u5BFE\u8C61\u306E SC ID\uFF09\u3092\u8A18\u8F09\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
|
|
4728
|
+
config.validation.traceability.scNoTestSeverity,
|
|
4729
|
+
testsRoot,
|
|
4730
|
+
"traceability.scMustHaveTest",
|
|
4731
|
+
samples
|
|
4732
|
+
)
|
|
4733
|
+
);
|
|
4734
|
+
} else {
|
|
4735
|
+
deferredLayers.push({ layer, missing });
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
if (deferredLayers.length > 0) {
|
|
4739
|
+
const summary = deferredLayers.map(
|
|
4740
|
+
(entry) => `layer=${entry.layer} missing=${entry.missing.length}`
|
|
4741
|
+
).join(", ");
|
|
4493
4742
|
issues.push(
|
|
4494
4743
|
issue(
|
|
4495
|
-
"QFAI-TRACE-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
)}\u3002testFileGlobs \u306B\u4E00\u81F4\u3059\u308B\u30C6\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\u3078 QFAI:SC-XXXX-XXXX\uFF08\u5BFE\u8C61\u306E SC ID\uFF09\u3092\u8A18\u8F09\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
|
|
4499
|
-
config.validation.traceability.scNoTestSeverity,
|
|
4744
|
+
"QFAI-TRACE-041",
|
|
4745
|
+
`\u30C6\u30B9\u30C8\u8A3C\u8DE1\u304C\u672A\u691C\u51FA\u306E\u30EC\u30A4\u30E4\u30FC\u306F SC\u2192Test \u5224\u5B9A\u3092\u4FDD\u7559\u3057\u307E\u3057\u305F: ${summary}\u3002\u8A72\u5F53\u30EC\u30A4\u30E4\u30FC\u306B\u30C6\u30B9\u30C8\u8A3C\u8DE1\u304C1\u4EF6\u3067\u3082\u8FFD\u52A0\u3055\u308C\u305F\u6642\u70B9\u3067 100% \u3092\u5F37\u5236\u3057\u307E\u3059\u3002`,
|
|
4746
|
+
"info",
|
|
4500
4747
|
testsRoot,
|
|
4501
|
-
"traceability.scMustHaveTest"
|
|
4502
|
-
scWithoutTests
|
|
4748
|
+
"traceability.scMustHaveTest"
|
|
4503
4749
|
)
|
|
4504
4750
|
);
|
|
4505
4751
|
}
|
|
@@ -4511,9 +4757,7 @@ async function validateTraceability(root, config) {
|
|
|
4511
4757
|
issues.push(
|
|
4512
4758
|
issue(
|
|
4513
4759
|
"QFAI-TRACE-011",
|
|
4514
|
-
`\u30C6\u30B9\u30C8\u304C\u672A\u77E5\u306E SC \u3092\
|
|
4515
|
-
", "
|
|
4516
|
-
)}`,
|
|
4760
|
+
`\u30C6\u30B9\u30C8\u304C\u672A\u77E5\u306E SC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownScIds.join(", ")}`,
|
|
4517
4761
|
"error",
|
|
4518
4762
|
testsRoot,
|
|
4519
4763
|
"traceability.scUnknownInTests",
|
|
@@ -4838,6 +5082,7 @@ async function createReportData(root, validation, configResult) {
|
|
|
4838
5082
|
const testStrategy = await collectTestStrategy(
|
|
4839
5083
|
scenarioFiles,
|
|
4840
5084
|
resolvedRoot,
|
|
5085
|
+
config,
|
|
4841
5086
|
REPORT_TEST_STRATEGY_SAMPLE_LIMIT
|
|
4842
5087
|
);
|
|
4843
5088
|
const {
|
|
@@ -5256,6 +5501,27 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
5256
5501
|
`- 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
5502
|
);
|
|
5258
5503
|
lines.push("");
|
|
5504
|
+
const e2eHeading = data.testStrategy.e2e.ratioExceeded || data.testStrategy.e2e.countExceeded ? "### E2E guardrails (warning)" : "### E2E guardrails";
|
|
5505
|
+
lines.push(e2eHeading);
|
|
5506
|
+
lines.push("");
|
|
5507
|
+
lines.push(
|
|
5508
|
+
`- e2e: ${data.testStrategy.e2e.count} / ${data.testStrategy.totalScenarios} (ratio=${formatPercent(data.testStrategy.e2e.ratio)})`
|
|
5509
|
+
);
|
|
5510
|
+
lines.push(
|
|
5511
|
+
`- maxRatio: ${formatOptionalPercent(data.testStrategy.e2e.maxRatio)}`
|
|
5512
|
+
);
|
|
5513
|
+
lines.push(
|
|
5514
|
+
`- maxCount: ${formatOptionalNumber(data.testStrategy.e2e.maxCount)}`
|
|
5515
|
+
);
|
|
5516
|
+
if (data.testStrategy.e2e.ratioExceeded || data.testStrategy.e2e.countExceeded) {
|
|
5517
|
+
if (data.testStrategy.e2e.ratioExceeded) {
|
|
5518
|
+
lines.push("- warning: layer-e2e \u306E\u6BD4\u7387\u304C\u4E0A\u9650\u3092\u8D85\u904E\u3057\u3066\u3044\u307E\u3059\u3002");
|
|
5519
|
+
}
|
|
5520
|
+
if (data.testStrategy.e2e.countExceeded) {
|
|
5521
|
+
lines.push("- warning: layer-e2e \u306E\u4EF6\u6570\u304C\u4E0A\u9650\u3092\u8D85\u904E\u3057\u3066\u3044\u307E\u3059\u3002");
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
lines.push("");
|
|
5259
5525
|
lines.push("### Missing layer tags");
|
|
5260
5526
|
lines.push("");
|
|
5261
5527
|
lines.push(
|
|
@@ -5570,6 +5836,21 @@ function formatList(values) {
|
|
|
5570
5836
|
}
|
|
5571
5837
|
return values.join(", ");
|
|
5572
5838
|
}
|
|
5839
|
+
function formatOptionalPercent(value) {
|
|
5840
|
+
if (value === null) {
|
|
5841
|
+
return "(unset)";
|
|
5842
|
+
}
|
|
5843
|
+
return formatPercent(value);
|
|
5844
|
+
}
|
|
5845
|
+
function formatOptionalNumber(value) {
|
|
5846
|
+
if (value === null) {
|
|
5847
|
+
return "(unset)";
|
|
5848
|
+
}
|
|
5849
|
+
return String(value);
|
|
5850
|
+
}
|
|
5851
|
+
function formatPercent(value) {
|
|
5852
|
+
return `${Math.round(value * 1e3) / 10}%`;
|
|
5853
|
+
}
|
|
5573
5854
|
function formatMarkdownTable(headers, rows) {
|
|
5574
5855
|
const widths = headers.map((header, index) => {
|
|
5575
5856
|
const candidates = rows.map((row) => row[index] ?? "");
|
|
@@ -5662,7 +5943,7 @@ async function countScenarios(scenarioFiles) {
|
|
|
5662
5943
|
}
|
|
5663
5944
|
return total;
|
|
5664
5945
|
}
|
|
5665
|
-
async function collectTestStrategy(scenarioFiles, root, limit) {
|
|
5946
|
+
async function collectTestStrategy(scenarioFiles, root, config, limit) {
|
|
5666
5947
|
const layerCounts = {
|
|
5667
5948
|
unit: 0,
|
|
5668
5949
|
component: 0,
|
|
@@ -5682,6 +5963,7 @@ async function collectTestStrategy(scenarioFiles, root, limit) {
|
|
|
5682
5963
|
const missingLayer = [];
|
|
5683
5964
|
const missingSize = [];
|
|
5684
5965
|
let totalScenarios = 0;
|
|
5966
|
+
let e2eCount = 0;
|
|
5685
5967
|
for (const file of scenarioFiles) {
|
|
5686
5968
|
const text = await readFile15(file, "utf-8");
|
|
5687
5969
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
@@ -5701,6 +5983,9 @@ async function collectTestStrategy(scenarioFiles, root, limit) {
|
|
|
5701
5983
|
if (layerBucket === "none") {
|
|
5702
5984
|
missingLayer.push(label);
|
|
5703
5985
|
}
|
|
5986
|
+
if (layerBucket === "e2e") {
|
|
5987
|
+
e2eCount += 1;
|
|
5988
|
+
}
|
|
5704
5989
|
const sizeBucket = classifySize(scenario.tags);
|
|
5705
5990
|
sizeCounts[sizeBucket] += 1;
|
|
5706
5991
|
if (sizeBucket === "none") {
|
|
@@ -5710,6 +5995,9 @@ async function collectTestStrategy(scenarioFiles, root, limit) {
|
|
|
5710
5995
|
}
|
|
5711
5996
|
const layerSamples = missingLayer.slice(0, limit);
|
|
5712
5997
|
const sizeSamples = missingSize.slice(0, limit);
|
|
5998
|
+
const ratio = totalScenarios === 0 ? 0 : e2eCount / totalScenarios;
|
|
5999
|
+
const maxRatio = config.validation.testStrategy.maxE2eScenarioRatio;
|
|
6000
|
+
const maxCount = config.validation.testStrategy.maxE2eScenarioCount;
|
|
5713
6001
|
return {
|
|
5714
6002
|
totalScenarios,
|
|
5715
6003
|
limit,
|
|
@@ -5726,6 +6014,14 @@ async function collectTestStrategy(scenarioFiles, root, limit) {
|
|
|
5726
6014
|
samples: sizeSamples,
|
|
5727
6015
|
truncated: missingSize.length > sizeSamples.length
|
|
5728
6016
|
}
|
|
6017
|
+
},
|
|
6018
|
+
e2e: {
|
|
6019
|
+
count: e2eCount,
|
|
6020
|
+
ratio,
|
|
6021
|
+
maxRatio,
|
|
6022
|
+
maxCount,
|
|
6023
|
+
ratioExceeded: maxRatio !== null && ratio > maxRatio,
|
|
6024
|
+
countExceeded: maxCount !== null && e2eCount > maxCount
|
|
5729
6025
|
}
|
|
5730
6026
|
};
|
|
5731
6027
|
}
|