@aarwitz/tapp 0.16.4 → 0.16.5
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/AGENTS.md +9 -5
- package/README.md +17 -8
- package/mcp-server/src/ci-report.js +16 -7
- package/mcp-server/src/html-report.js +3 -2
- package/mcp-server/src/index.js +10 -5
- package/mcp-server/src/report.js +55 -14
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -82,10 +82,13 @@ Rules that prevent 90% of failures:
|
|
|
82
82
|
|
|
83
83
|
## Autonomous QA (`tapp_run_qa`)
|
|
84
84
|
|
|
85
|
-
Returns `{verdict, confidence, headline, screensExplored, actionsPerformed, findings[]}`.
|
|
85
|
+
Returns `{verdict, confidence, releaseScore, headline, screensExplored, actionsPerformed, findings[]}`.
|
|
86
|
+
Exploratory web runs set `confidence` and `releaseScore` to `null`; report their deterministic
|
|
87
|
+
finding counts, advisory sampled-probe counts, and coverage instead of inventing a scalar.
|
|
86
88
|
|
|
87
|
-
- `verdict`: `ready` | `caution` | `blocked`.
|
|
88
|
-
|
|
89
|
+
- `verdict`: `ready` | `caution` | `blocked`. Report it as-is; never soften a `blocked` or inflate a
|
|
90
|
+
`caution`. Judgment is deterministic for a given evidence trace, while adaptive exploration and
|
|
91
|
+
live target state can still change which evidence a run observes.
|
|
89
92
|
- `inconclusive: true` means the run couldn't see enough (crash on launch, login wall). That is
|
|
90
93
|
**not a pass** — tell the user what blocked exploration and what would unblock it.
|
|
91
94
|
- Login walls: pass `testEmail`/`testPassword` (auto-typed into login forms), `appLaunchArgs`
|
|
@@ -95,9 +98,10 @@ Returns `{verdict, confidence, headline, screensExplored, actionsPerformed, find
|
|
|
95
98
|
- Diff two runs: pass the previous run's `findings` as `baselineFindings` → you get a
|
|
96
99
|
`regression` block (`new` / `persisting` / `resolved`, plus a CI `gate` signal).
|
|
97
100
|
- On web, report the exact verdict but preserve its scope: Tapp deterministically checks technical
|
|
98
|
-
behavior such as failed requests, missing assets, placeholder links
|
|
101
|
+
behavior such as failed requests, missing assets, and placeholder links. Dead-control probes are
|
|
102
|
+
budget-capped advisory findings and do not drive the verdict. Tapp does
|
|
99
103
|
not validate marketing claims against APIs, API field privacy, brand consistency, or subjective
|
|
100
|
-
marketplace credibility unless an explicit reviewed test/contract covers them.
|
|
104
|
+
marketplace credibility unless an explicit reviewed test/contract or verifier covers them.
|
|
101
105
|
|
|
102
106
|
## Flows (deterministic E2E tests)
|
|
103
107
|
|
package/README.md
CHANGED
|
@@ -168,7 +168,7 @@ Then ask your agent:
|
|
|
168
168
|
| 📸 | `tapp_screenshot` | Whatever's on the sim right now, as an inline image. |
|
|
169
169
|
| 🌳 | `tapp_ui_tree` | The accessibility tree of the current screen (ids, labels, hittability). |
|
|
170
170
|
| 🕹 | `tapp_session_start/act/end` | **Interactive driving** — the Playwright loop. App launches once; each act (tap/type/swipe/back/wait) returns the fresh tree. |
|
|
171
|
-
| 🧪 | `tapp_run_qa` | **Autonomous QA** — explores with no authored test, returns `{verdict, releaseScore, findings[]}
|
|
171
|
+
| 🧪 | `tapp_run_qa` | **Autonomous QA** — explores with no authored test, returns `{verdict, releaseScore, findings[]}` (`releaseScore` is `null` for exploratory web). Takes `appBundleId` (iOS), `androidAppId` (Android), or `url` (web). |
|
|
172
172
|
| 🧭 | `tapp_init` | **Repository import** — detect targets; optionally explore a real surface; persist the shared UI Map; construct the evidence-classified model and grounded release plan. |
|
|
173
173
|
| 👤 | `tapp_actor_config` | **Actor/session setup** — store roles, isolation/provisioning, and environment-variable names without accepting or persisting credential values. |
|
|
174
174
|
| ✅ | `tapp_release_plan` | **Release-plan lifecycle** — inspect, approve/reject/defer, generate, real-target validate, and explicitly promote proposed guarantees without silent test edits. |
|
|
@@ -193,7 +193,7 @@ deterministic per-platform navigation root used for bounded changed-surface repl
|
|
|
193
193
|
|
|
194
194
|
**Adaptive exploration, deterministic judgment.** Exploration is adaptive — two runs may
|
|
195
195
|
traverse different paths through your app. Judgment is deterministic: the same evidence
|
|
196
|
-
trace always produces the same findings
|
|
196
|
+
trace always produces the same findings and verdict — no LLM variability
|
|
197
197
|
in the decision loop. PR gating keys on the **regression diff**
|
|
198
198
|
(stable finding signatures vs. a baseline), so it reacts to what *changed*, not to
|
|
199
199
|
run-to-run path variance. For critical user journeys, committed **Tasks and Flows** provide the stable CI
|
|
@@ -201,10 +201,11 @@ suite: reusable semantic actions, exact assertions, condition-based waits, fresh
|
|
|
201
201
|
and evidence on failure. We call this *flake-resistant*, not magically flake-free—backend outages,
|
|
202
202
|
unstable test data, and poorly identified controls can still make any E2E test fail.
|
|
203
203
|
|
|
204
|
-
**
|
|
205
|
-
fixed
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
**Native has a heuristic release score; exploratory web does not.** The native 0–100 number comes
|
|
205
|
+
from fixed deductions and is not calibrated probability. Web reports deterministic findings,
|
|
206
|
+
advisory budget-capped control probes, and concrete coverage instead of compressing those unlike
|
|
207
|
+
signals into a scalar. Committed Flows, Tasks, contracts, and baseline regressions provide the web
|
|
208
|
+
merge decision.
|
|
208
209
|
|
|
209
210
|
`tapp_run_qa` explores like a user — accessibility surfaces on iOS/Android and a real browser on web —
|
|
210
211
|
and detects crashes, failed sign-ins, dead buttons, stuck loading screens, error surfaces,
|
|
@@ -219,10 +220,18 @@ broken links and assets, and visible placeholder links with no destination). The
|
|
|
219
220
|
app crashed on launch or a login wall blocked exploration, you get `inconclusive: true`,
|
|
220
221
|
not a false pass. Absence of findings is not a pass.
|
|
221
222
|
|
|
222
|
-
Web beta presents a `ready` result as **AUTOMATED CHECKS
|
|
223
|
+
Web beta presents a `ready` result as **AUTOMATED CHECKS COMPLETE**, not “ship-ready,” and displays
|
|
224
|
+
no scalar score. Exhaustive checks on each exercised page drive the verdict; sampled control probes
|
|
225
|
+
remain visible findings but are advisory. The report
|
|
223
226
|
explicitly excludes content/claim accuracy, privacy and API data minimization, brand/SEO
|
|
224
227
|
consistency, and subjective visual credibility. Those require reviewed contracts, privacy review,
|
|
225
|
-
or human/vision judgment;
|
|
228
|
+
or human/vision judgment; an exploratory crawl must not imply they were validated.
|
|
229
|
+
|
|
230
|
+
For a business guarantee such as “every coach is insured,” use a deterministic app-owned verifier
|
|
231
|
+
endpoint that returns success only when the invariant holds, then require that status and the
|
|
232
|
+
customer-visible claim in a release contract. The current DSL does not yet read arbitrary JSON
|
|
233
|
+
response bodies or compare a cross-origin API payload directly with page copy; use a verifier or an
|
|
234
|
+
explicit CI preflight rather than assuming autonomous QA inferred the guarantee.
|
|
226
235
|
|
|
227
236
|
Apps behind a login? Pass `testEmail`/`testPassword` (typed into the login form automatically),
|
|
228
237
|
`appLaunchArgs` (e.g. `["--uitesting"]` if your app supports a bypass), or explicit `loginSteps`
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// any fail on any finding at all, or any flow failure. Strictest.
|
|
27
27
|
import fs from "fs";
|
|
28
28
|
import path from "node:path";
|
|
29
|
-
import { buildQaReport, computeRegression, computeContentCollapse, computeReachabilityLoss, verdictBadge } from "./report.js";
|
|
29
|
+
import { buildQaReport, computeRegression, computeContentCollapse, computeReachabilityLoss, qaScoreLabel, verdictBadge } from "./report.js";
|
|
30
30
|
import { writeHtmlReport } from "./html-report.js";
|
|
31
31
|
import { buildUiMapFromMarkers, writeUiMap } from "./ui-map.js";
|
|
32
32
|
import { proposeSelectorMaintenance, validateWebMaintenanceProposal } from "./maintenance-proposal.js";
|
|
@@ -345,7 +345,10 @@ function renderMarkdown(report, regression, flows, scenarios, contracts, prPlan,
|
|
|
345
345
|
lines.push("");
|
|
346
346
|
lines.push(report.headline);
|
|
347
347
|
lines.push("");
|
|
348
|
-
lines.push(
|
|
348
|
+
lines.push(`**${qaScoreLabel(report)}** · ${report.screensExplored} screens · ${report.actionsPerformed} actions · ${report.findingCounts.total} finding(s)`);
|
|
349
|
+
if (report.platform === "web") {
|
|
350
|
+
lines.push(`**Verdict basis:** ${report.verdictFindingCounts?.total || 0} deterministic finding(s); ${report.sampledFindingCounts?.total || 0} sampled probe finding(s) are advisory.`);
|
|
351
|
+
}
|
|
349
352
|
if (report.uiMap) lines.push(`**UI Map:** ${report.uiMap.nodeCount} states · ${report.uiMap.edgeCount} transitions · ${report.uiMap.controlCount} semantic controls`);
|
|
350
353
|
if (report.findings.length) {
|
|
351
354
|
lines.push("");
|
|
@@ -488,11 +491,17 @@ if (collapsed.length) {
|
|
|
488
491
|
report.findings.push(...collapsed);
|
|
489
492
|
report.findingCounts.high += collapsed.length;
|
|
490
493
|
report.findingCounts.total += collapsed.length;
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if (
|
|
494
|
+
report.verdictFindingCounts.high += collapsed.length;
|
|
495
|
+
report.verdictFindingCounts.total += collapsed.length;
|
|
496
|
+
// Keep native scoring compatible. Exploratory web deliberately has no scalar; deterministic
|
|
497
|
+
// baseline regressions still raise its verdict directly.
|
|
498
|
+
if (Number.isFinite(report.confidence)) {
|
|
499
|
+
report.confidence = Math.max(0, report.confidence - collapsed.length * 10);
|
|
500
|
+
report.releaseScore = report.confidence;
|
|
501
|
+
}
|
|
502
|
+
if (report.verdict === "ready") {
|
|
503
|
+
report.verdict = Number.isFinite(report.confidence) && report.confidence < 50 ? "blocked" : "caution";
|
|
504
|
+
}
|
|
496
505
|
report.headline = `Proceed with caution — ${collapsed.length} screen(s) regressed vs. baseline (content collapsed or became unreachable).`;
|
|
497
506
|
}
|
|
498
507
|
const regression = computeRegression(report.findings, baseline?.findings ?? null);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import fs from "fs";
|
|
10
10
|
import path from "path";
|
|
11
|
-
import { buildQaReport, verdictBadge } from "./report.js";
|
|
11
|
+
import { buildQaReport, qaScoreLabel, verdictBadge } from "./report.js";
|
|
12
12
|
|
|
13
13
|
const SEV_COLOR = { critical: "#cf222e", high: "#bc4c00", medium: "#9a6700", low: "#57606a" };
|
|
14
14
|
|
|
@@ -101,8 +101,9 @@ export function writeHtmlReport(captureDir, { report, label = "" } = {}) {
|
|
|
101
101
|
</style>
|
|
102
102
|
</head>
|
|
103
103
|
<body>
|
|
104
|
-
<h1>${esc(verdictBadge(r))} <span class="dim">·
|
|
104
|
+
<h1>${esc(verdictBadge(r))} <span class="dim">· ${esc(qaScoreLabel(r))}</span></h1>
|
|
105
105
|
<div class="meta">${esc(label)} · ${r.screensExplored} screens · ${r.actionsPerformed} actions · ${r.findingCounts.total} finding(s)</div>
|
|
106
|
+
${r.platform === "web" ? `<div class="meta">Verdict basis: ${r.verdictFindingCounts?.total || 0} deterministic finding(s); ${r.sampledFindingCounts?.total || 0} sampled probe finding(s) are advisory.</div>` : ""}
|
|
106
107
|
<div class="headline">${esc(r.headline)}</div>
|
|
107
108
|
<h2>Findings</h2>
|
|
108
109
|
<ul class="findings">
|
package/mcp-server/src/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
ListToolsRequestSchema,
|
|
13
13
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
14
|
|
|
15
|
-
import { parseOcqaMarkers, buildQaReport, computeRegression, verdictBadge } from "./report.js";
|
|
15
|
+
import { parseOcqaMarkers, buildQaReport, computeRegression, qaScoreLabel, verdictBadge } from "./report.js";
|
|
16
16
|
import { existingProjectArtifactPath, projectArtifactDirectory } from "./project-paths.js";
|
|
17
17
|
|
|
18
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -1326,11 +1326,14 @@ function formatQaReport(report, { regression, inputHint, timedOut, bundleId, aiC
|
|
|
1326
1326
|
.filter(Boolean)
|
|
1327
1327
|
.join(", ");
|
|
1328
1328
|
const L = [];
|
|
1329
|
-
L.push(`### 🧪 QA complete — ${badge} ·
|
|
1329
|
+
L.push(`### 🧪 QA complete — ${badge} · ${qaScoreLabel(report)}${bundleId ? `\n\`${bundleId}\`` : ""}`);
|
|
1330
1330
|
L.push("");
|
|
1331
1331
|
L.push(report.headline);
|
|
1332
1332
|
L.push("");
|
|
1333
1333
|
L.push(`**Coverage** — ${report.screensExplored} screens · ${report.actionsPerformed} actions${timedOut ? " · ⏱️ hit time limit" : ""}`);
|
|
1334
|
+
if (report.platform === "web") {
|
|
1335
|
+
L.push(`**Verdict basis** — ${report.verdictFindingCounts?.total || 0} deterministic finding(s); ${report.sampledFindingCounts?.total || 0} sampled probe finding(s) are advisory`);
|
|
1336
|
+
}
|
|
1334
1337
|
if (uiMap) L.push(`**UI Map** — ${uiMap.nodeCount} states · ${uiMap.edgeCount} transitions · ${uiMap.controlCount} semantic controls · ${uiMap.path}`);
|
|
1335
1338
|
if (reportHtml) L.push(`**Evidence** — 📄 ${reportHtml} (screenshots of every screen + findings, shareable)`);
|
|
1336
1339
|
if (recording) L.push(`**Recording** — 🎬 ${recording} (full exploration, embedded in the evidence page)`);
|
|
@@ -2070,13 +2073,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2070
2073
|
description:
|
|
2071
2074
|
"Run autonomous QA against iOS (appBundleId), Android (androidAppId), OR a web app " +
|
|
2072
2075
|
"(url — beta, requires Playwright installed) and return a structured " +
|
|
2073
|
-
"
|
|
2076
|
+
"QA verdict. Use ONLY when the user wants a QA assessment / to find bugs / a verdict — this " +
|
|
2074
2077
|
"runs for MINUTES exploring the whole app. Do NOT use it just to view, screenshot, or reach a specific " +
|
|
2075
2078
|
"screen — use tapp_open_app (launch + screenshot) or a session for that. Tapp explores the app " +
|
|
2076
2079
|
"like a tester (taps, types, navigates, scrolls) and detects real issues — crashes, dead buttons, failed sign-ins, error screens, " +
|
|
2077
2080
|
"stuck/hung screens; on web also uncaught JS exceptions, failed/5xx requests, broken links and assets. " +
|
|
2078
|
-
"Returns {verdict: ready|caution|blocked, confidence, headline, screensExplored, " +
|
|
2079
|
-
"actionsPerformed, findings:[{type,severity,category,title,screen}]}.
|
|
2081
|
+
"Returns {verdict: ready|caution|blocked, confidence, releaseScore, headline, screensExplored, " +
|
|
2082
|
+
"actionsPerformed, findings:[{type,severity,category,title,screen,evaluationTier}]}. Exploratory web " +
|
|
2083
|
+
"sets confidence/releaseScore to null and separates deterministic verdict findings from advisory " +
|
|
2084
|
+
"sampled control probes. The verdict has a coverage floor: " +
|
|
2080
2085
|
"if the app barely explored (crash on launch / sign-in wall) it returns 'caution' + inconclusive, never a " +
|
|
2081
2086
|
"false pass. For iOS the app must already be installed on a booted simulator (use tapp_list_simulators / " +
|
|
2082
2087
|
"tapp_boot_simulator first). For web, only point it at an app/environment you own — it CLICKS things. " +
|
package/mcp-server/src/report.js
CHANGED
|
@@ -96,16 +96,28 @@ export const ISSUE_CATEGORY = {
|
|
|
96
96
|
explore_timeout: "performance_timeout",
|
|
97
97
|
};
|
|
98
98
|
export const CRITICAL_ISSUE_TYPES = new Set(["crash"]);
|
|
99
|
+
export const WEB_SAMPLED_ISSUE_TYPES = new Set(["unresponsive_element"]);
|
|
99
100
|
|
|
100
101
|
export function severityRank(s) {
|
|
101
102
|
return { critical: 0, high: 1, medium: 2, low: 3 }[s] ?? 4;
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
export function findingEvaluationTier(finding, platform = "ios") {
|
|
106
|
+
return platform === "web" && WEB_SAMPLED_ISSUE_TYPES.has(finding?.type) ? "sampled" : "deterministic";
|
|
107
|
+
}
|
|
108
|
+
|
|
104
109
|
export function verdictBadge(report) {
|
|
105
|
-
if (report?.platform === "web" && report?.verdict === "ready") return "
|
|
110
|
+
if (report?.platform === "web" && report?.verdict === "ready") return "🔵 AUTOMATED CHECKS COMPLETE";
|
|
106
111
|
return { ready: "🟢 SHIP-READY", caution: "🟡 CAUTION", blocked: "🔴 BLOCKED" }[report?.verdict] || report?.verdict;
|
|
107
112
|
}
|
|
108
113
|
|
|
114
|
+
export function qaScoreLabel(report) {
|
|
115
|
+
const score = report?.releaseScore ?? report?.confidence;
|
|
116
|
+
if (Number.isFinite(score)) return `release score ${score}/100`;
|
|
117
|
+
if (report?.platform === "web") return "exploratory web · no scalar score";
|
|
118
|
+
return "score unavailable";
|
|
119
|
+
}
|
|
120
|
+
|
|
109
121
|
// Turn a capture's OCQA markers into the same ship/no-ship report Tapp produces:
|
|
110
122
|
// deduped findings + a trustworthy verdict with a coverage floor (mirrors OrchestratorService).
|
|
111
123
|
export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
|
|
@@ -186,7 +198,11 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
|
|
|
186
198
|
const key = `${i.type}|${i.screen}|${i.target ?? ""}`;
|
|
187
199
|
if (seen.has(key)) continue;
|
|
188
200
|
seen.add(key);
|
|
189
|
-
findings.push({
|
|
201
|
+
findings.push({
|
|
202
|
+
...i,
|
|
203
|
+
category: ISSUE_CATEGORY[i.type] || i.type,
|
|
204
|
+
...(platform === "web" ? { evaluationTier: findingEvaluationTier(i, platform) } : {}),
|
|
205
|
+
});
|
|
190
206
|
}
|
|
191
207
|
findings.sort((a, b) => severityRank(a.severity) - severityRank(b.severity));
|
|
192
208
|
|
|
@@ -197,24 +213,32 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
|
|
|
197
213
|
const high = findings.filter((f) => f.severity === "high").length;
|
|
198
214
|
const med = findings.filter((f) => f.severity === "medium").length;
|
|
199
215
|
const low = findings.filter((f) => f.severity === "low").length;
|
|
216
|
+
const verdictFindings = platform === "web"
|
|
217
|
+
? findings.filter((finding) => finding.evaluationTier !== "sampled")
|
|
218
|
+
: findings;
|
|
219
|
+
const verdictCrit = verdictFindings.filter((f) => f.severity === "critical").length;
|
|
220
|
+
const verdictHigh = verdictFindings.filter((f) => f.severity === "high").length;
|
|
221
|
+
const verdictMed = verdictFindings.filter((f) => f.severity === "medium").length;
|
|
222
|
+
const verdictLow = verdictFindings.filter((f) => f.severity === "low").length;
|
|
223
|
+
const sampledFindings = platform === "web" ? findings.filter((finding) => finding.evaluationTier === "sampled") : [];
|
|
200
224
|
|
|
201
225
|
// Coverage floor: a verdict is only trustworthy if the app was actually exercised.
|
|
202
226
|
const inconclusive = screensExplored < 2 || actionsPerformed < 3;
|
|
203
|
-
let
|
|
204
|
-
if (inconclusive)
|
|
227
|
+
let riskScore = Math.max(0, Math.min(100, 100 - verdictCrit * 25 - verdictHigh * 10 - verdictMed * 3));
|
|
228
|
+
if (inconclusive) riskScore = Math.min(riskScore, 40);
|
|
205
229
|
|
|
206
230
|
let verdict;
|
|
207
|
-
if (
|
|
231
|
+
if (verdictCrit > 0) verdict = "blocked";
|
|
208
232
|
else if (inconclusive) verdict = "caution";
|
|
209
|
-
else if (
|
|
210
|
-
else if (
|
|
233
|
+
else if (riskScore < 50) verdict = "blocked";
|
|
234
|
+
else if (verdictHigh > 0 || riskScore < 80) verdict = "caution";
|
|
211
235
|
else verdict = "ready";
|
|
212
236
|
|
|
213
237
|
const headline = inconclusive
|
|
214
238
|
? `Inconclusive — only ${screensExplored} screen(s) / ${actionsPerformed} action(s) explored. The app may have crashed on launch, be stuck behind a sign-in wall, or otherwise prevent exploration. Absence of issues is NOT a pass.`
|
|
215
239
|
: verdict === "ready"
|
|
216
240
|
? platform === "web"
|
|
217
|
-
? "Automated web checks
|
|
241
|
+
? "Automated web checks completed — no release-blocking deterministic findings in the exercised surfaces. Sampled control probes are advisory. This is not a content, privacy, brand, or business-claim review."
|
|
218
242
|
: "Ship-ready — no release-blocking issues found."
|
|
219
243
|
: verdict === "caution"
|
|
220
244
|
? `Proceed with caution — ${findings.length} issue(s) to review.`
|
|
@@ -230,7 +254,7 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
|
|
|
230
254
|
if (platform === "web") {
|
|
231
255
|
checkedFor = [
|
|
232
256
|
"page errors (uncaught exceptions)", "failed/5xx requests", "broken links (404)",
|
|
233
|
-
"placeholder links with no destination", "dead
|
|
257
|
+
"placeholder links with no destination", "sampled dead-button probes (advisory)", "error text on pages", "load timeouts",
|
|
234
258
|
];
|
|
235
259
|
notChecked = [
|
|
236
260
|
"app-specific business logic (cover with Flows: record or generate, then assert)",
|
|
@@ -272,11 +296,14 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
|
|
|
272
296
|
|
|
273
297
|
return {
|
|
274
298
|
verdict,
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
confidence,
|
|
279
|
-
releaseScore:
|
|
299
|
+
// Exploratory web QA deliberately has no scalar. Its verdict derives from deterministic
|
|
300
|
+
// checks on exercised pages; budget-capped control probes remain visible but advisory.
|
|
301
|
+
// Native keeps the legacy heuristic score until it has an equivalent tier split.
|
|
302
|
+
confidence: platform === "web" ? null : riskScore,
|
|
303
|
+
releaseScore: platform === "web" ? null : riskScore,
|
|
304
|
+
scoreUnavailableReason: platform === "web"
|
|
305
|
+
? "Exploratory web runs report deterministic findings, advisory sampled probes, and coverage instead of a scalar release score."
|
|
306
|
+
: null,
|
|
280
307
|
headline,
|
|
281
308
|
inconclusive,
|
|
282
309
|
checkedFor,
|
|
@@ -286,6 +313,20 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
|
|
|
286
313
|
screensExplored,
|
|
287
314
|
actionsPerformed,
|
|
288
315
|
findingCounts: { critical: crit, high, medium: med, low, total: findings.length },
|
|
316
|
+
verdictFindingCounts: {
|
|
317
|
+
critical: verdictCrit,
|
|
318
|
+
high: verdictHigh,
|
|
319
|
+
medium: verdictMed,
|
|
320
|
+
low: verdictLow,
|
|
321
|
+
total: verdictFindings.length,
|
|
322
|
+
},
|
|
323
|
+
sampledFindingCounts: {
|
|
324
|
+
critical: sampledFindings.filter((f) => f.severity === "critical").length,
|
|
325
|
+
high: sampledFindings.filter((f) => f.severity === "high").length,
|
|
326
|
+
medium: sampledFindings.filter((f) => f.severity === "medium").length,
|
|
327
|
+
low: sampledFindings.filter((f) => f.severity === "low").length,
|
|
328
|
+
total: sampledFindings.length,
|
|
329
|
+
},
|
|
289
330
|
findings,
|
|
290
331
|
screens: Array.from(screens),
|
|
291
332
|
screenElementCounts,
|
package/package.json
CHANGED