docs(adr): slim ADR 0004 to a decision record

Merged
TH
1d ago
+490-846

Summary

Slims ADR 0004 to a decision record in ADR 0038's shape.
Size: 63,541 B → 9,307 B (about 1.5 pages).
ADR 0038 owns routing (#4205). 0004 keeps the iOS acquisition, presentation, geometry, and system-surface contracts.

Contracts that only 0004 held now live with their owners: apple/snapshot-presentation/README.md (new), apple/snapshot-bridge/README.md, apple/runner/README.md, and docs/evidence/ios-snapshot-adr-0004-measurements-2026-11-06.md (new).
Four code comments cite the new owners. Docs and comments only.

The ADR stays above the 8 KB aim by about 1.2 KB. The remaining text is decisions, their reasons, one-line rejected alternatives, and open gaps. A further cut would drop a decision or a reason.

Validation

Head edbd51de7d:

  • pnpm check:doc-prose: pass. pnpm check:doc-links: pass.
  • pnpm check:affected --run --head edbd51d…: all runnable checks pass (layering, lint, typecheck, unit, and 40 more, because the layering test comment changed). Swift and replay lanes are GitHub-authoritative or parked.
  • Claims checked against snapshot-route.ts, snapshot-route-failure-policy.ts, system-surface-foreground.ts, and contracts/fixtures/ios-snapshot-bridge-refusals.json.
  • Citations checked by hand with git grep "ADR 0004\|0004-ios-snapshot". The cited section names ("Geometry") are unchanged.

Docs only, so runtime and device tests do not apply.

Meaning-review fixes (round 2)
  • Fallback: "If bridge acquisition fails without cancellation, or presentation fails, use one typed XCTest fallback." snapshot-route.ts rethrows a cancelled acquisition (rethrowCancelledFailure).
  • Circuit: "After a generation-scoped fallback, disable the bridge for that app generation." fallbackAfterFailure adds the generation only for scope === 'generation'.
  • Surface proof: "If the proof fails or a host is present, use the runner for that capture without changing the app-generation circuit." runSurfaceFallback runs before the circuit check and changes no circuit.
  • Runner README: the incident narrative is replaced by "Mute XCTest's AX-server rejection in both fetch wordings so a recorded failure does not end the runner session." The one-app-class observation ("On the observed app class, each allElementsBoundByIndex query-sweep read failed with kAXErrorIllegalArgument …") moved to the evidence file.
  • Modality: the physical-device line and the mode-ownership line are "should" recommendations again. No code enforces either.
  • RunnerTests+Interaction.swift:291 now cites apple/runner/README.md "XCTest accessibility channel policy".
  • Routing inventories (preparing, wedged reader, window and coordinate-space refusals) are replaced by a pointer to ADR 0038 and the refusal fixture. The remote-content reason stays.
  • Acquisition/presentation mechanics stay in the presentation README. The ADR keeps "acquisition publishes facts; presentation interprets them".
  • Round-1 threads: the tree-changed rule needs fewer children than both bounds (SnapshotBridgeCapture.m:106); a wedged reader without a session id is a capture-scoped bridge-read-busy; probe matching prefers frameOnScreen; all three resolveIosViewportEvidenceFromRoots callers are named; the tree walk serializes frames but uses no geometry for descent; the modal evidence names the dimming views; the layering test cites ADR 0004 "Ownership".
  • Round-3 threads: the carrier path is @agent-device/contracts/ios-snapshot (the old contracts/snapshot-presentation subpath left in #2357); a muted rejection is not recorded; the distance reason is back; the query-sweep observation cites #2621.
  • The presentation README gains the Android carrier, the unscoped quality payload, and the comparison key. The bridge README gains the surface-proof order and "a local read never starts or replaces a runner" (readyLocalSession).
Migration ledger

Old ADR 0004 (before this PR) → outcome. Abbreviations: PR = apple/snapshot-presentation/README.md, BR = apple/snapshot-bridge/README.md, RR = apple/runner/README.md, EV = the evidence file, FX = contracts/fixtures/ios-snapshot-bridge-refusals.json.

Old sectionOutcomeOwner or reason
Status: 0038 owns routing; two modes; bridge is a routeKeptStatus, Decision
Status: amendment history; "fallback warnings observable"DeletedGit history; ADR 0038 owns warnings
Rules at a glanceKept (merged)Each rule is one Decision sentence
Context: two readers; XCTest failures; filters cannot recover nodesKept (short)Context, Rationale
Context: Bluesky exampleDeletedExample app, not a fact
Two strategies; no cutoff; no raw truncation; preserve AX error; sparse verdictKeptDecision: Two public modes
Regular strategy: scroll intersection, hidden-content hintsMovedPR "Visibility fold"
Bridge for local Simulators; one typed fallback; generation circuitKept (cancellation and generation-scope made explicit)Decision: Fallback
Physical devices only with a supported Apple channelKept ("should")Decision: Acquisition
preparing, bridge-reader-wedged, window and coordinate-space refusals, snapshot-tree-changed, foreground-owner-changedMovedFX; BR sections
Runner must not end itselfMovedBR "Runner-served reader"
remote-content-boundary rule and reasonKept (short)Decision; BR "Remote content" keeps leaf conditions
0.20.x hybrid-app historyDeletedHistory; Rationale keeps the cost
Ownership: carrier, capture-kit, daemon order, new consumers use the facetKeptDecision: Ownership (carrier path corrected to contracts/ios-snapshot)
capture-kit facet list; Wave 4 freshness/timeout/overlay inventory; layering test; call-site auditDeletedscripts/layering/architecture-ownership.ts snapshot-policy and the test own them
Android carrier; macOS bypass; Swift/TS twin enginesMoved / kept (short)PR; ADR keeps "shared fixtures, not shared code"
Acquired vs presented vocabularies; CaptureHint; projections; IOS_SNAPSHOT_PROJECTION_MISMATCHMovedPR "Acquisition and presentation", "Projections"
Comparison key; unscoped quality payloadMovedPR
Compaction never deletes an identifier; hittable: false (#2638)KeptDecision: Presentation
Eligibility, scope, clip fold, viewport, de-duplication, depth capabilityMovedPR sections
Regular --depth is a presentation cutKeptDecision; reasoning in PR "Depth"
Regression notes: channel penalty, probes, deadlines, abandoned workMovedRR "XCTest accessibility channel policy"
Regression notes: muted AX-server rejectionMoved (instruction)RR; app-class observation in EV (#2621)
Regression notes: #639, #1105 historyDeletedIssues own it
Recovery conformance; accepted-depth hintsMoved (already owned)BR; ADR keeps the rejected shared engine
Consequences: mode use; mode ownershipKept ("should")Consequences
Future AX-service backendKeptRejected alternative + open gap
Migration steps 1–2DeletedHistory; ADR keeps the seam
#3801 clip decisionKeptDecision history line; mechanics in PR "Cumulative clip"
Acquisition-side limitsKeptOpen gaps
#2438 never activate; registry; session stays on app; surface beats bundle id; presence predicate; provenance and lineageKeptDecision: System surfaces
#2438 proof steps; local read never starts a runner; provider proof starts oneMovedADR 0038 "Surface proof"; BR "System surface hosts", "Runner-served reader"
#2438 failed proof keeps the circuitKept (fixed: circuit unchanged)Decision
#2438 rejected verdict caches and pgrepKept (grouped)Rejected; 45 ms in EV
#2438 Apple Pay hostDeletedRegistry fixture notes own each host
#2612 one space; window declares space; fail openKeptDecision: Geometry
#2612 rotation table, fixture replay; reported-geometry producersMoved / kept (one line)PR "Coordinate space"; Consequences
#2612 refusal scope reasoningKept (merged)Decision: Fallback reason
#2659 orientation spike factsMovedBR "Coordinate space"
#3899 placed window publishes screen pointsKeptDecision; mechanics in PR "Placed windows", RR "Screen space"
#2612, #3899, #2638, #2661, #2751, #4182 measurementsMovedEV
Modal containment ruleKept (short)Decision; conditions in PR "Modal containment"
Modal rejected seams; iPad rejected alternatives; runner-served rejected alternativesKept (grouped by trade-off)Rejected alternatives
iPad trait typing; runner full-screen gestureMovedBR "Trait typing"; RR
Runner-served mechanics (queue, budgets, target verb, cached targets)MovedBR "Runner-served reader"
Observe-only capture skips the routeKeptDecision: Acquisition
Open gaps (#3899 windowing, provider main thread, JSON-in-JSON)KeptOpen gaps
Drift list

Old ADR (before this PR) → new ADR. Modality, scope, and exceptions only.

  1. Physical devices: "should use … only if Apple exposes a supported channel" → "should get … only if …". Modality unchanged.

  2. Mode ownership: "maintainers … should first decide which strategy owns it" → "they should first decide which mode owns it". Modality unchanged.

  3. Future AX-service backend: "should be added as a platform backend … should not be added inside the XCTest runner" → a rejected alternative ("another producer inside the runner") plus an open gap. The should not is now a rejection, so it is a little stronger.

  4. Semantic compaction: "may not delete one" → "must not delete one". Same meaning in STE.

  5. Mode definitions: "regular visible strategy: use recursive XCTest snapshots" is gone. The bridge acquires regular snapshots on Simulators. The runner README keeps the XCTest capture-plan rules.

  6. Comparison key: moved to the presentation README and worded for all producers, as the old text already worded it. ADR 0038 Identity agrees.

  7. Status "fallback warnings make recovered output observable end to end": dropped. ADR 0038 owns warnings. The sparse-quality verdict stays.

  8. Fallback now names the cancellation exception and the generation-scoped condition. Both match the code and ADR 0038; the old text left them implicit.

  9. Remote content: the ADR states "content that can be on screen" for the leaf conditions (reaches the viewport, or reports no frame). The bridge README keeps the exact conditions.

  10. Modal containment: the ADR states the rule only. The conditions and fail-closed cases are unchanged in the presentation README.

  11. Ownership: "@agent-device/contracts/snapshot-presentation holds the neutral carrier and the clip-fold geometry contract" named a subpath that #2357 removed. It now reads "@agent-device/contracts/ios-snapshot holds the acquisition carrier; capture-kit owns … the clip fold", which matches the code. This corrects a stale fact, not a decision.

No exception or scope was widened.


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.