


















@@ -0,0 +1,192 @@
1+import type { QaSeedScenarioWithSource } from "./scenario-catalog.js";
2+3+export type QaCoverageScenarioSummary = {
4+id: string;
5+title: string;
6+sourcePath: string;
7+theme: string;
8+surfaces: string[];
9+risk: string;
10+};
11+12+export type QaCoverageIntent = "primary" | "secondary";
13+14+export type QaCoverageScenarioReference = QaCoverageScenarioSummary & {
15+intent: QaCoverageIntent;
16+};
17+18+export type QaCoverageFeatureSummary = {
19+id: string;
20+scenarios: QaCoverageScenarioReference[];
21+};
22+23+export type QaCoverageInventory = {
24+scenarioCount: number;
25+coverageIdCount: number;
26+primaryCoverageIdCount: number;
27+secondaryCoverageIdCount: number;
28+features: QaCoverageFeatureSummary[];
29+overlappingCoverage: QaCoverageFeatureSummary[];
30+missingCoverage: QaCoverageScenarioSummary[];
31+byTheme: Record<string, QaCoverageFeatureSummary[]>;
32+bySurface: Record<string, QaCoverageFeatureSummary[]>;
33+};
34+35+function scenarioTheme(sourcePath: string) {
36+const parts = sourcePath.split("/");
37+return parts[2] ?? "unknown";
38+}
39+40+function scenarioSurfaces(scenario: QaSeedScenarioWithSource) {
41+return scenario.surfaces && scenario.surfaces.length > 0 ? scenario.surfaces : [scenario.surface];
42+}
43+44+function scenarioRisk(scenario: QaSeedScenarioWithSource) {
45+return scenario.risk ?? scenario.riskLevel ?? "unassigned";
46+}
47+48+function summarizeScenario(scenario: QaSeedScenarioWithSource): QaCoverageScenarioSummary {
49+return {
50+id: scenario.id,
51+title: scenario.title,
52+sourcePath: scenario.sourcePath,
53+theme: scenarioTheme(scenario.sourcePath),
54+surfaces: scenarioSurfaces(scenario),
55+risk: scenarioRisk(scenario),
56+};
57+}
58+59+function sortFeatures(features: readonly QaCoverageFeatureSummary[]) {
60+return features.toSorted((left, right) => left.id.localeCompare(right.id));
61+}
62+63+export function buildQaCoverageInventory(
64+scenarios: readonly QaSeedScenarioWithSource[],
65+): QaCoverageInventory {
66+const byCoverageId = new Map<string, QaCoverageFeatureSummary>();
67+const primaryCoverageIds = new Set<string>();
68+const secondaryCoverageIds = new Set<string>();
69+const missingCoverage: QaCoverageScenarioSummary[] = [];
70+71+const addCoverage = (
72+scenario: QaSeedScenarioWithSource,
73+coverageIds: readonly string[] | undefined,
74+intent: QaCoverageIntent,
75+) => {
76+const summary = summarizeScenario(scenario);
77+for (const coverageId of coverageIds ?? []) {
78+const feature = byCoverageId.get(coverageId) ?? {
79+id: coverageId,
80+scenarios: [],
81+};
82+feature.scenarios.push({ ...summary, intent });
83+byCoverageId.set(coverageId, feature);
84+if (intent === "primary") {
85+primaryCoverageIds.add(coverageId);
86+} else {
87+secondaryCoverageIds.add(coverageId);
88+}
89+}
90+};
91+92+for (const scenario of scenarios) {
93+if (!scenario.coverage) {
94+missingCoverage.push(summarizeScenario(scenario));
95+continue;
96+}
97+addCoverage(scenario, scenario.coverage.primary, "primary");
98+addCoverage(scenario, scenario.coverage.secondary, "secondary");
99+}
100+101+const features = sortFeatures([...byCoverageId.values()]);
102+const overlappingCoverage = features.filter((feature) => feature.scenarios.length > 1);
103+const byTheme: Record<string, QaCoverageFeatureSummary[]> = {};
104+const bySurface: Record<string, QaCoverageFeatureSummary[]> = {};
105+106+for (const feature of features) {
107+const themes = new Set(feature.scenarios.map((scenario) => scenario.theme));
108+for (const theme of themes) {
109+byTheme[theme] ??= [];
110+byTheme[theme].push({
111+ ...feature,
112+scenarios: feature.scenarios.filter((scenario) => scenario.theme === theme),
113+});
114+}
115+const surfaces = new Set(feature.scenarios.flatMap((scenario) => scenario.surfaces));
116+for (const surface of surfaces) {
117+bySurface[surface] ??= [];
118+bySurface[surface].push({
119+ ...feature,
120+scenarios: feature.scenarios.filter((scenario) => scenario.surfaces.includes(surface)),
121+});
122+}
123+}
124+125+return {
126+scenarioCount: scenarios.length,
127+coverageIdCount: features.length,
128+primaryCoverageIdCount: primaryCoverageIds.size,
129+secondaryCoverageIdCount: secondaryCoverageIds.size,
130+ features,
131+ overlappingCoverage,
132+ missingCoverage,
133+ byTheme,
134+ bySurface,
135+};
136+}
137+138+function pushFeatureLines(lines: string[], features: readonly QaCoverageFeatureSummary[]) {
139+for (const feature of sortFeatures(features)) {
140+const scenarios = feature.scenarios
141+.map((scenario) => `${scenario.intent}: ${scenario.id} (${scenario.sourcePath})`)
142+.join(", ");
143+lines.push(`- ${feature.id}: ${scenarios}`);
144+}
145+}
146+147+export function renderQaCoverageMarkdownReport(inventory: QaCoverageInventory): string {
148+const lines: string[] = [
149+"# QA Coverage Inventory",
150+"",
151+`- Scenarios: ${inventory.scenarioCount}`,
152+`- Coverage IDs: ${inventory.coverageIdCount}`,
153+`- Primary coverage IDs: ${inventory.primaryCoverageIdCount}`,
154+`- Secondary coverage IDs: ${inventory.secondaryCoverageIdCount}`,
155+`- Overlapping coverage IDs: ${inventory.overlappingCoverage.length}`,
156+`- Missing coverage metadata: ${inventory.missingCoverage.length}`,
157+"",
158+"## By Theme",
159+"",
160+];
161+162+for (const theme of Object.keys(inventory.byTheme).toSorted()) {
163+lines.push(`### ${theme}`, "");
164+pushFeatureLines(lines, inventory.byTheme[theme] ?? []);
165+lines.push("");
166+}
167+168+lines.push("## By Surface", "");
169+for (const surface of Object.keys(inventory.bySurface).toSorted()) {
170+lines.push(`### ${surface}`, "");
171+pushFeatureLines(lines, inventory.bySurface[surface] ?? []);
172+lines.push("");
173+}
174+175+if (inventory.overlappingCoverage.length > 0) {
176+lines.push("## Overlap", "");
177+pushFeatureLines(lines, inventory.overlappingCoverage);
178+lines.push("");
179+}
180+181+if (inventory.missingCoverage.length > 0) {
182+lines.push("## Missing Metadata", "");
183+for (const scenario of inventory.missingCoverage.toSorted((left, right) =>
184+left.id.localeCompare(right.id),
185+)) {
186+lines.push(`- ${scenario.id}: ${scenario.sourcePath}`);
187+}
188+lines.push("");
189+}
190+191+return `${lines.join("\n").trimEnd()}\n`;
192+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。