@aarwitz/tapp 0.16.4 โ†’ 0.17.0-rc.1

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.
@@ -96,14 +96,41 @@ 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
 
104
- export function verdictBadge(report) {
105
- if (report?.platform === "web" && report?.verdict === "ready") return "๐ŸŸข AUTOMATED CHECKS PASSED";
106
- return { ready: "๐ŸŸข SHIP-READY", caution: "๐ŸŸก CAUTION", blocked: "๐Ÿ”ด BLOCKED" }[report?.verdict] || report?.verdict;
105
+ export function findingEvaluationTier(finding, platform = "ios") {
106
+ return platform === "web" && WEB_SAMPLED_ISSUE_TYPES.has(finding?.type) ? "sampled" : "deterministic";
107
+ }
108
+
109
+ // The deterministic block-by-findings rule, expressed over verdict-tier finding COUNTS โ€” not the
110
+ // score scalar and not the `verdict` label โ€” so the CI gate survives removal of verdict/releaseScore
111
+ // from exploration output. It encodes exactly what `verdict === "blocked"` used to: a critical
112
+ // finding always blocks; otherwise, on a CONCLUSIVE run, a risk threshold blocks. The risk threshold
113
+ // is kept as an explicit, chosen rule (ADR-0005) โ€” `riskFromCounts` is the single source of that
114
+ // formula, shared with buildQaReport's verdict label. Inconclusive runs are a separate gate outcome,
115
+ // never a findings-block, so a thin run reports `inconclusive`, not `fail`.
116
+ export function riskFromCounts({ critical = 0, high = 0, medium = 0 } = {}) {
117
+ return Math.max(0, Math.min(100, 100 - critical * 25 - high * 10 - medium * 3));
118
+ }
119
+ export function findingsBlock(deterministicFindingCounts = {}, { inconclusive = false } = {}) {
120
+ if ((deterministicFindingCounts.critical || 0) > 0) return true;
121
+ if (inconclusive) return false;
122
+ return riskFromCounts(deterministicFindingCounts) < 50;
123
+ }
124
+
125
+ // Exploration OBSERVES; it never renders a ship verdict or score (ADR-0005). These label the
126
+ // observation honestly. The release decision (pass/fail/inconclusive) is the gate's, shown separately.
127
+ export function observationBadge(report) {
128
+ return report?.inconclusive ? "๐ŸŸก INCONCLUSIVE (exploration)" : "๐Ÿ”ญ EXPLORED";
129
+ }
130
+
131
+ export function observationSummary(report) {
132
+ const n = report?.findingCounts?.total || 0;
133
+ return `${report?.screensExplored || 0} screens ยท ${report?.actionsPerformed || 0} actions ยท ${n} finding(s) ยท observation only`;
107
134
  }
108
135
 
109
136
  // Turn a capture's OCQA markers into the same ship/no-ship report Tapp produces:
@@ -186,7 +213,15 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
186
213
  const key = `${i.type}|${i.screen}|${i.target ?? ""}`;
187
214
  if (seen.has(key)) continue;
188
215
  seen.add(key);
189
- findings.push({ ...i, category: ISSUE_CATEGORY[i.type] || i.type });
216
+ findings.push({
217
+ ...i,
218
+ category: ISSUE_CATEGORY[i.type] || i.type,
219
+ // Structural evidence authority (ADR-0005): marker-derived findings are deterministic. The
220
+ // default gate consumes only deterministic-authority evidence; model-observed findings
221
+ // (vision/assert_ai) carry authority:"model-observed" and are advisory, never gate fails.
222
+ authority: "deterministic",
223
+ ...(platform === "web" ? { evaluationTier: findingEvaluationTier(i, platform) } : {}),
224
+ });
190
225
  }
191
226
  findings.sort((a, b) => severityRank(a.severity) - severityRank(b.severity));
192
227
 
@@ -197,28 +232,27 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
197
232
  const high = findings.filter((f) => f.severity === "high").length;
198
233
  const med = findings.filter((f) => f.severity === "medium").length;
199
234
  const low = findings.filter((f) => f.severity === "low").length;
235
+ const verdictFindings = platform === "web"
236
+ ? findings.filter((finding) => finding.evaluationTier !== "sampled")
237
+ : findings;
238
+ const verdictCrit = verdictFindings.filter((f) => f.severity === "critical").length;
239
+ const verdictHigh = verdictFindings.filter((f) => f.severity === "high").length;
240
+ const verdictMed = verdictFindings.filter((f) => f.severity === "medium").length;
241
+ const verdictLow = verdictFindings.filter((f) => f.severity === "low").length;
242
+ const sampledFindings = platform === "web" ? findings.filter((finding) => finding.evaluationTier === "sampled") : [];
200
243
 
201
- // Coverage floor: a verdict is only trustworthy if the app was actually exercised.
244
+ // Coverage floor: exploration is inconclusive if the app wasn't actually exercised. Exploration
245
+ // OBSERVES โ€” it does not render a ship verdict or score (ADR-0005). Judgment (pass/fail/
246
+ // inconclusive) is the gate's job (evaluateGate), computed from these findings + coverage + policy.
202
247
  const inconclusive = screensExplored < 2 || actionsPerformed < 3;
203
- let confidence = Math.max(0, Math.min(100, 100 - crit * 25 - high * 10 - med * 3));
204
- if (inconclusive) confidence = Math.min(confidence, 40);
205
-
206
- let verdict;
207
- if (crit > 0) verdict = "blocked";
208
- else if (inconclusive) verdict = "caution";
209
- else if (confidence < 50) verdict = "blocked";
210
- else if (high > 0 || confidence < 80) verdict = "caution";
211
- else verdict = "ready";
212
248
 
213
249
  const headline = inconclusive
214
250
  ? `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
- : verdict === "ready"
251
+ : findings.length === 0
216
252
  ? platform === "web"
217
- ? "Automated web checks passed โ€” no release-blocking technical issues found in the exercised surfaces. This is not a content, privacy, brand, or business-claim review."
218
- : "Ship-ready โ€” no release-blocking issues found."
219
- : verdict === "caution"
220
- ? `Proceed with caution โ€” ${findings.length} issue(s) to review.`
221
- : `Not ready โ€” ${findings.length} issue(s): ${crit} critical, ${high} high, ${med} medium, ${low} low.`;
253
+ ? "Automated web checks completed โ€” no deterministic findings in the exercised surfaces. Sampled control probes are advisory. An observation, not a release decision, and not a content, privacy, brand, or business-claim review."
254
+ : "No issues surfaced in the exercised surfaces. An observation, not a release decision."
255
+ : `${findings.length} issue(s) surfaced for review (${crit} critical, ${high} high, ${med} medium, ${low} low). An observation, not a release decision.`;
222
256
 
223
257
  // The verdict's own honesty label: exactly which defect classes this run checked, which
224
258
  // it structurally could NOT check, and which conditions never came up โ€” so "checked" is
@@ -230,7 +264,7 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
230
264
  if (platform === "web") {
231
265
  checkedFor = [
232
266
  "page errors (uncaught exceptions)", "failed/5xx requests", "broken links (404)",
233
- "placeholder links with no destination", "dead buttons", "error text on pages", "load timeouts",
267
+ "placeholder links with no destination", "sampled dead-button probes (advisory)", "error text on pages", "load timeouts",
234
268
  ];
235
269
  notChecked = [
236
270
  "app-specific business logic (cover with Flows: record or generate, then assert)",
@@ -271,14 +305,21 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
271
305
  else conditionsNotReached.push("sign-in (no login form encountered this run)");
272
306
 
273
307
  return {
274
- verdict,
275
- // `releaseScore` is the honest name: a heuristic quality score from fixed deductions,
276
- // NOT calibrated statistical confidence. `confidence` is kept as an alias for
277
- // compatibility (baselines, desktop app, existing consumers).
278
- confidence,
279
- releaseScore: confidence,
308
+ // An ExplorationRun observation: findings + coverage + evidence, NO ship verdict or score
309
+ // (ADR-0005). The gate (evaluateGate) turns this into a pass/fail/inconclusive release outcome.
310
+ kind: "tapp-exploration-run",
311
+ schemaVersion: 1,
312
+ // Complete ExplorationRun contract (ADR-0005 ยง4). runStatus/stopReason describe HOW the run
313
+ // ended; coverage/evidence/uiMap/comparison are the structured observation. uiMap and comparison
314
+ // are populated by consumers that build the map / diff a baseline (null in the bare observation).
315
+ runStatus: inconclusive ? "limited" : "completed",
316
+ stopReason: inconclusive ? "coverage-floor-not-met" : "completed",
280
317
  headline,
281
318
  inconclusive,
319
+ coverage: { screensExplored, actionsPerformed, screens: Array.from(screens) },
320
+ evidence: { markers: base.relativeMarkersFilePath },
321
+ uiMap: null,
322
+ comparison: null,
282
323
  checkedFor,
283
324
  notChecked,
284
325
  conditionsNotReached,
@@ -286,6 +327,20 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
286
327
  screensExplored,
287
328
  actionsPerformed,
288
329
  findingCounts: { critical: crit, high, medium: med, low, total: findings.length },
330
+ deterministicFindingCounts: {
331
+ critical: verdictCrit,
332
+ high: verdictHigh,
333
+ medium: verdictMed,
334
+ low: verdictLow,
335
+ total: verdictFindings.length,
336
+ },
337
+ sampledFindingCounts: {
338
+ critical: sampledFindings.filter((f) => f.severity === "critical").length,
339
+ high: sampledFindings.filter((f) => f.severity === "high").length,
340
+ medium: sampledFindings.filter((f) => f.severity === "medium").length,
341
+ low: sampledFindings.filter((f) => f.severity === "low").length,
342
+ total: sampledFindings.length,
343
+ },
289
344
  findings,
290
345
  screens: Array.from(screens),
291
346
  screenElementCounts,
@@ -314,6 +369,7 @@ export function computeContentCollapse(currentCounts, baselineCounts) {
314
369
  type: "content_collapse",
315
370
  severity: "high",
316
371
  category: "content_collapse",
372
+ authority: "deterministic",
317
373
  title: `Screen lost most of its content (${base} โ†’ ${cur} elements)`,
318
374
  screen,
319
375
  step: null,
@@ -342,6 +398,7 @@ export function computeReachabilityLoss(current, baseline) {
342
398
  type: "screen_unreachable",
343
399
  severity: "high",
344
400
  category: "navigation_dead_end",
401
+ authority: "deterministic",
345
402
  title: "Screen explored in the baseline was never reached this run",
346
403
  screen,
347
404
  step: null,
@@ -380,15 +437,84 @@ export function computeRegression(current, baseline) {
380
437
  const newFindings = current.filter((f) => !currentMatches(f));
381
438
  const persisting = current.filter((f) => currentMatches(f));
382
439
  const resolved = baseline.filter((b) => !baselineMatched(b));
383
- const newCritical = newFindings.filter((f) => f.severity === "critical").length;
384
- const newHigh = newFindings.filter((f) => f.severity === "high").length;
385
440
 
441
+ // Comparison ONLY โ€” no gate/pass/fail signal (ADR-0005). Exploration surfaces this diff; the merge
442
+ // decision is the gate's job. evaluateGate derives its regression fail from `newFindings` severities.
386
443
  return {
387
444
  hadBaseline: true,
388
445
  counts: { new: newFindings.length, persisting: persisting.length, resolved: resolved.length },
389
446
  newFindings,
390
447
  resolved,
391
- // CI gate: fail the build when this run introduced new high/critical findings vs. the baseline.
392
- gate: { newCritical, newHigh, failed: newCritical + newHigh > 0 },
448
+ };
449
+ }
450
+
451
+ // The gate's public outcome model (ADR-0005). A merge gate is ultimately block / don't-block, but
452
+ // callers need to distinguish WHY: a deterministic violation is not the same as "we couldn't get
453
+ // the evidence." Exit codes are the CI contract; precedence is fail > inconclusive > pass.
454
+ export const GATE_EXIT = { pass: 0, fail: 1, error: 2, inconclusive: 3 };
455
+ // Bump when the gate's decision semantics change (NOT the npm version). Recorded on every GateRun.
456
+ export const GATE_POLICY_VERSION = "1";
457
+
458
+ // Pure gate evaluator: frozen evidence + policy โ†’ a GateRun decision. Extracted verbatim from the
459
+ // former inline logic in ci-report.js so the `[char]` characterization tests keep passing โ€” the
460
+ // merge decision (block/don't-block) is unchanged; this only classifies each reason as a
461
+ // deterministic `fail` or an evidence-absent `inconclusive` and folds them by precedence. Reason
462
+ // MESSAGES are preserved exactly (several are asserted by tests).
463
+ //
464
+ // DECOUPLED FROM THE SCORE (ADR-0005): the block-by-findings decision reads `deterministicFindingCounts` +
465
+ // `inconclusive` via `findingsBlock`, NOT the score scalar or the `verdict` label. `verdict`/
466
+ // `releaseScore` can therefore be removed from exploration output without changing any merge
467
+ // decision. The `riskScore < 50` threshold is retained deliberately (kept explicit, inside
468
+ // `findingsBlock`) and locked by the `[char]` risk-threshold test.
469
+ export function evaluateGate({ report, regression = null, flows = [], scenarios = [], contracts = [], prPlan = null, baseline = null, failOn = "gate" } = {}) {
470
+ const reasons = []; // { kind: "fail" | "inconclusive", message }
471
+ const fail = (message) => reasons.push({ kind: "fail", message });
472
+ const inconclusive = (message) => reasons.push({ kind: "inconclusive", message });
473
+
474
+ // Classify each replayed suite by evidence authority (ADR-0005): a DETERMINISTIC step failure (or
475
+ // a non-model failure like an aborted/missing run) is a real fail; a suite with no deterministic
476
+ // failure that carries a model-observed (assert_ai) assertion cannot be decided by the default
477
+ // deterministic gate โ†’ inconclusive/needs-review (it must not silently pass, and a model verdict
478
+ // must not masquerade as a deterministic fail). No `--policy probabilistic` opt-in in 0.17.
479
+ const deterministicFail = (s) => s.deterministicFailed === true || (s.passed === false && !s.modelObserved);
480
+ const classify = (s) => (deterministicFail(s) ? "fail" : s.modelObserved ? "needs-review" : "pass");
481
+ for (const [label, suites] of [["flow", flows], ["multi-actor scenario", scenarios], ["release contract", contracts]]) {
482
+ const failed = suites.filter((s) => classify(s) === "fail");
483
+ if (failed.length) fail(`${failed.length} ${label}(s) failed`);
484
+ const needsReview = suites.filter((s) => classify(s) === "needs-review");
485
+ if (needsReview.length) inconclusive(`${needsReview.length} ${label}(s) contain assert_ai (model-observed); the deterministic gate cannot decide them โ€” review, or add an explicit probabilistic policy`);
486
+ }
487
+ // Selected-but-unexecuted work is missing evidence, not an observed violation โ†’ inconclusive.
488
+ if (prPlan?.execution?.notRun) inconclusive(`${prPlan.execution.notRun} selected release contract(s) did not run`);
489
+ if (prPlan?.execution?.explorationFailed) inconclusive(`${prPlan.execution.explorationFailed} planned PR exploration target(s) failed or were not reached`);
490
+
491
+ if (failOn === "any") {
492
+ if (report.findingCounts.total > 0) fail(`${report.findingCounts.total} finding(s) (fail-on: any)`);
493
+ // "any" is the strictest policy โ€” an inconclusive run (evidence not obtained) must never pass it.
494
+ if (report.inconclusive) inconclusive("run was inconclusive (coverage floor not met)");
495
+ } else if (failOn === "absolute" || (failOn === "gate" && !regression)) {
496
+ if (findingsBlock(report.deterministicFindingCounts, { inconclusive: report.inconclusive })) fail("blocking deterministic finding(s)");
497
+ if (report.inconclusive) inconclusive("run was inconclusive (coverage floor not met)");
498
+ } else {
499
+ if (regression?.newFindings?.length) {
500
+ const newCritical = regression.newFindings.filter((f) => f.severity === "critical").length;
501
+ const newHigh = regression.newFindings.filter((f) => f.severity === "high").length;
502
+ if (newCritical + newHigh > 0) fail(`${newCritical} new critical + ${newHigh} new high vs. baseline`);
503
+ }
504
+ if (findingsBlock(report.deterministicFindingCounts, { inconclusive: report.inconclusive })) fail("blocking deterministic finding(s)");
505
+ if (report.inconclusive && !baseline?.inconclusive) inconclusive("run became inconclusive vs. baseline (app may no longer launch/explore)");
506
+ }
507
+
508
+ const outcome = reasons.some((r) => r.kind === "fail") ? "fail"
509
+ : reasons.some((r) => r.kind === "inconclusive") ? "inconclusive"
510
+ : "pass";
511
+ return {
512
+ policy: failOn,
513
+ outcome,
514
+ exitCode: GATE_EXIT[outcome],
515
+ failed: outcome !== "pass", // retained for markdown/JSON consumers during migration
516
+ reasons: reasons.map((r) => r.message),
517
+ reasonDetails: reasons,
518
+ policyVersion: GATE_POLICY_VERSION,
393
519
  };
394
520
  }
@@ -109,7 +109,7 @@ export function loadTaskRegistry({ sourcePath, projectDir = "", taskFiles = [] }
109
109
  // Draft contracts generated under `.tapp/proposals/contracts` may compile
110
110
  // against sibling untrusted Task drafts. Ordinary committed contracts never
111
111
  // see this directory, so a proposal cannot silently enter the release gate.
112
- const proposalSource = [".tapp", ".autotap"].some((directory) => String(path.resolve(sourcePath || "")).includes(`${path.sep}${directory}${path.sep}proposals${path.sep}`));
112
+ const proposalSource = String(path.resolve(sourcePath || "")).includes(`${path.sep}.tapp${path.sep}proposals${path.sep}`);
113
113
  const proposalDir = proposalSource && tappDir ? path.join(tappDir, "proposals", "tasks") : "";
114
114
  const proposed = proposalDir && fs.existsSync(proposalDir)
115
115
  ? fs.readdirSync(proposalDir).filter((name) => /\.ya?ml$|\.json$/i.test(name)).map((name) => path.join(proposalDir, name))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aarwitz/tapp",
3
- "version": "0.16.4",
3
+ "version": "0.17.0-rc.1",
4
4
  "mcpName": "io.github.aarwitz/tapp",
5
5
  "description": "Release contracts, autonomous QA, and evidence-backed CI gates for iOS, Android, and web.",
6
6
  "license": "MIT",
@@ -87,7 +87,7 @@
87
87
  "mobile"
88
88
  ],
89
89
  "scripts": {
90
- "test": "node --test tests/report.test.js tests/regression.test.js tests/engine.test.js tests/project-config.test.js tests/application-model.test.js tests/ui-map.test.js tests/task-runtime.test.js tests/release-contract.test.js tests/pr-selection.test.js tests/flow-runtime.test.js tests/web-explorer.test.js tests/web-session.test.js tests/web-flow.test.js tests/scenario-runtime.test.js tests/android-driver.test.js tests/android-explorer.test.js tests/android-flow.test.js tests/managed-web.test.js tests/product-operations.test.js tests/browser-product.test.js tests/browser-onboarding.test.js tests/managed-operation.test.js tests/ci-setup.test.js tests/ci-install.test.js tests/cli.test.js tests/action.test.js tests/package-surface.test.js tests/ci-gate.test.js tests/ci-report.test.js tests/desktop-protocol.test.js tests/ios-flow-protocol.test.js",
90
+ "test": "node --test tests/report.test.js tests/regression.test.js tests/engine.test.js tests/project-config.test.js tests/application-model.test.js tests/ui-map.test.js tests/task-runtime.test.js tests/release-contract.test.js tests/pr-selection.test.js tests/flow-runtime.test.js tests/web-explorer.test.js tests/web-session.test.js tests/web-flow.test.js tests/scenario-runtime.test.js tests/android-driver.test.js tests/android-explorer.test.js tests/android-flow.test.js tests/managed-web.test.js tests/product-operations.test.js tests/browser-product.test.js tests/browser-onboarding.test.js tests/managed-operation.test.js tests/cloud-runner.test.js tests/ci-setup.test.js tests/ci-install.test.js tests/cli.test.js tests/action.test.js tests/package-surface.test.js tests/ci-gate.test.js tests/ci-report.test.js tests/desktop-protocol.test.js tests/ios-flow-protocol.test.js",
91
91
  "test:browser-journey": "node --test tests/browser-journey.test.js",
92
92
  "test:browser-native": "TAPP_RUN_NATIVE_BROWSER=1 node --test tests/browser-native-journey.test.js"
93
93
  }
@@ -25,7 +25,7 @@
25
25
  # [--pr-plan-out <file.json>] # persist the reviewable selection plan
26
26
  # [--baseline <file.json>] # prior report to diff against (skipped if absent)
27
27
  # [--target-key <stable-id>] # isolates target-specific baselines in monorepos
28
- # [--fail-on gate|blocked|any] # gate policy (default gate; see ci-report.js)
28
+ # [--fail-on gate|absolute|any] # gate policy (default gate; see ci-report.js)
29
29
  # [--json-out <file.json>] # write the full report (use as the next baseline)
30
30
  # [--md-out <file.md>] # write the rendered markdown report (for a PR comment)
31
31
  # [--device <name>] # simulator device to boot if none is (default "iPhone 16 Pro")
@@ -74,7 +74,7 @@ done
74
74
  [[ "$PLATFORM" == "ios" || "$PLATFORM" == "android" || "$PLATFORM" == "web" ]] || { echo "โŒ --platform must be ios|android|web" >&2; exit 2; }
75
75
  [[ "$ACTIONS" =~ ^[1-9][0-9]*$ ]] || { echo "โŒ --actions must be a positive integer" >&2; exit 2; }
76
76
  [[ "$TIMEOUT" =~ ^[1-9][0-9]*$ ]] || { echo "โŒ --timeout must be a positive integer" >&2; exit 2; }
77
- [[ "$FAIL_ON" == "gate" || "$FAIL_ON" == "blocked" || "$FAIL_ON" == "any" ]] || { echo "โŒ --fail-on must be gate|blocked|any" >&2; exit 2; }
77
+ [[ "$FAIL_ON" == "gate" || "$FAIL_ON" == "absolute" || "$FAIL_ON" == "any" ]] || { echo "โŒ --fail-on must be gate|absolute|any" >&2; exit 2; }
78
78
  if [[ -n "$PROJECT_DIR" ]]; then
79
79
  [[ -d "$PROJECT_DIR" ]] || { echo "โŒ Project directory not found: $PROJECT_DIR" >&2; exit 2; }
80
80
  PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
@@ -274,7 +274,7 @@ xcrun simctl install "$UDID" "$APP_PATH" || { echo "โŒ simctl install failed
274
274
  # โ”€โ”€ Autonomous exploration (quick-capture builds the harness itself if needed).
275
275
  step "Explore ($ACTIONS actions, ${TIMEOUT}s watchdog)"
276
276
  set +e
277
- CAPTURE_ROOT="${TAPP_HOME:-${AUTOTAP_HOME:-$ROOT}}/captures"
277
+ CAPTURE_ROOT="${TAPP_HOME:-$ROOT}/captures"
278
278
  mkdir -p "$CAPTURE_ROOT"
279
279
  CAPTURE_DIR="$(mktemp -d "$CAPTURE_ROOT/ci.XXXXXX")"
280
280
  TAPP_CAPTURE_DIR="$CAPTURE_DIR" OCQA_PR_TARGET_JSON="$IOS_PR_TARGET_JSON" "$ROOT/scripts/quick-capture.sh" explore "$BUNDLE_ID" --actions "$ACTIONS" --timeout "$TIMEOUT"
@@ -19,7 +19,7 @@ PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
19
19
  # accidentally reuse stale markers). TAPP_HOME redirects all writable output otherwise โ€”
20
20
  # set by the `tapp` CLI when running as an installed npm package, where the package dir
21
21
  # must stay read-only. Unset (repo dev flow), everything lands in the repo as before.
22
- TAPP_RUNTIME_HOME="${TAPP_HOME:-${AUTOTAP_HOME:-}}"
22
+ TAPP_RUNTIME_HOME="${TAPP_HOME:-}"
23
23
  if [[ -n "${TAPP_CAPTURE_DIR:-}" ]]; then
24
24
  CAPTURE_DIR="$TAPP_CAPTURE_DIR"
25
25
  elif [[ -n "$TAPP_RUNTIME_HOME" ]]; then
@@ -26,7 +26,7 @@ UDID="$(xcrun simctl list devices booted -j 2>/dev/null | python3 -c 'import sys
26
26
  [ -z "$UDID" ] && { echo "โŒ No booted simulator."; exit 2; }
27
27
  # When run through the installed `tapp` CLI, the harness cache lives under TAPP_HOME.
28
28
  XCTR=""
29
- TAPP_RUNTIME_HOME="${TAPP_HOME:-${AUTOTAP_HOME:-}}"
29
+ TAPP_RUNTIME_HOME="${TAPP_HOME:-}"
30
30
  [ -n "$TAPP_RUNTIME_HOME" ] && XCTR="$(find "$TAPP_RUNTIME_HOME/harness-derived/Build/Products" -name '*.xctestrun' 2>/dev/null | head -1)"
31
31
  [ -z "$XCTR" ] && XCTR="$(find "$HOME/Library/Developer/Xcode/DerivedData/OCQAHarness-"*/Build/Products -name '*.xctestrun' 2>/dev/null | head -1)"
32
32
  [ -z "$XCTR" ] && XCTR="$(find /tmp/tapp-harness-derived/Build/Products -name '*.xctestrun' 2>/dev/null | head -1)"