docs(adr): slim ADR 0004 to a decision record
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, andcontracts/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.tsrethrows a cancelled acquisition (rethrowCancelledFailure). - Circuit: "After a generation-scoped fallback, disable the bridge for that app generation."
fallbackAfterFailureadds the generation only forscope === 'generation'. - Surface proof: "If the proof fails or a host is present, use the runner for that capture without changing the app-generation circuit."
runSurfaceFallbackruns 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
allElementsBoundByIndexquery-sweep read failed withkAXErrorIllegalArgument…") 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:291now citesapple/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-scopedbridge-read-busy; probe matching prefersframeOnScreen; all threeresolveIosViewportEvidenceFromRootscallers 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 oldcontracts/snapshot-presentationsubpath 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 section | Outcome | Owner or reason |
|---|---|---|
| Status: 0038 owns routing; two modes; bridge is a route | Kept | Status, Decision |
| Status: amendment history; "fallback warnings observable" | Deleted | Git history; ADR 0038 owns warnings |
| Rules at a glance | Kept (merged) | Each rule is one Decision sentence |
| Context: two readers; XCTest failures; filters cannot recover nodes | Kept (short) | Context, Rationale |
| Context: Bluesky example | Deleted | Example app, not a fact |
| Two strategies; no cutoff; no raw truncation; preserve AX error; sparse verdict | Kept | Decision: Two public modes |
| Regular strategy: scroll intersection, hidden-content hints | Moved | PR "Visibility fold" |
| Bridge for local Simulators; one typed fallback; generation circuit | Kept (cancellation and generation-scope made explicit) | Decision: Fallback |
| Physical devices only with a supported Apple channel | Kept ("should") | Decision: Acquisition |
preparing, bridge-reader-wedged, window and coordinate-space refusals, snapshot-tree-changed, foreground-owner-changed | Moved | FX; BR sections |
| Runner must not end itself | Moved | BR "Runner-served reader" |
remote-content-boundary rule and reason | Kept (short) | Decision; BR "Remote content" keeps leaf conditions |
| 0.20.x hybrid-app history | Deleted | History; Rationale keeps the cost |
| Ownership: carrier, capture-kit, daemon order, new consumers use the facet | Kept | Decision: Ownership (carrier path corrected to contracts/ios-snapshot) |
| capture-kit facet list; Wave 4 freshness/timeout/overlay inventory; layering test; call-site audit | Deleted | scripts/layering/architecture-ownership.ts snapshot-policy and the test own them |
| Android carrier; macOS bypass; Swift/TS twin engines | Moved / kept (short) | PR; ADR keeps "shared fixtures, not shared code" |
Acquired vs presented vocabularies; CaptureHint; projections; IOS_SNAPSHOT_PROJECTION_MISMATCH | Moved | PR "Acquisition and presentation", "Projections" |
| Comparison key; unscoped quality payload | Moved | PR |
Compaction never deletes an identifier; hittable: false (#2638) | Kept | Decision: Presentation |
| Eligibility, scope, clip fold, viewport, de-duplication, depth capability | Moved | PR sections |
Regular --depth is a presentation cut | Kept | Decision; reasoning in PR "Depth" |
| Regression notes: channel penalty, probes, deadlines, abandoned work | Moved | RR "XCTest accessibility channel policy" |
| Regression notes: muted AX-server rejection | Moved (instruction) | RR; app-class observation in EV (#2621) |
| Regression notes: #639, #1105 history | Deleted | Issues own it |
| Recovery conformance; accepted-depth hints | Moved (already owned) | BR; ADR keeps the rejected shared engine |
| Consequences: mode use; mode ownership | Kept ("should") | Consequences |
| Future AX-service backend | Kept | Rejected alternative + open gap |
| Migration steps 1–2 | Deleted | History; ADR keeps the seam |
| #3801 clip decision | Kept | Decision history line; mechanics in PR "Cumulative clip" |
| Acquisition-side limits | Kept | Open gaps |
| #2438 never activate; registry; session stays on app; surface beats bundle id; presence predicate; provenance and lineage | Kept | Decision: System surfaces |
| #2438 proof steps; local read never starts a runner; provider proof starts one | Moved | ADR 0038 "Surface proof"; BR "System surface hosts", "Runner-served reader" |
| #2438 failed proof keeps the circuit | Kept (fixed: circuit unchanged) | Decision |
#2438 rejected verdict caches and pgrep | Kept (grouped) | Rejected; 45 ms in EV |
| #2438 Apple Pay host | Deleted | Registry fixture notes own each host |
| #2612 one space; window declares space; fail open | Kept | Decision: Geometry |
| #2612 rotation table, fixture replay; reported-geometry producers | Moved / kept (one line) | PR "Coordinate space"; Consequences |
| #2612 refusal scope reasoning | Kept (merged) | Decision: Fallback reason |
| #2659 orientation spike facts | Moved | BR "Coordinate space" |
| #3899 placed window publishes screen points | Kept | Decision; mechanics in PR "Placed windows", RR "Screen space" |
| #2612, #3899, #2638, #2661, #2751, #4182 measurements | Moved | EV |
| Modal containment rule | Kept (short) | Decision; conditions in PR "Modal containment" |
| Modal rejected seams; iPad rejected alternatives; runner-served rejected alternatives | Kept (grouped by trade-off) | Rejected alternatives |
| iPad trait typing; runner full-screen gesture | Moved | BR "Trait typing"; RR |
Runner-served mechanics (queue, budgets, target verb, cached targets) | Moved | BR "Runner-served reader" |
| Observe-only capture skips the route | Kept | Decision: Acquisition |
| Open gaps (#3899 windowing, provider main thread, JSON-in-JSON) | Kept | Open gaps |
Drift list
Old ADR (before this PR) → new ADR. Modality, scope, and exceptions only.
-
Physical devices: "should use … only if Apple exposes a supported channel" → "should get … only if …". Modality unchanged.
-
Mode ownership: "maintainers … should first decide which strategy owns it" → "they should first decide which mode owns it". Modality unchanged.
-
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.
-
Semantic compaction: "may not delete one" → "must not delete one". Same meaning in STE.
-
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.
-
Comparison key: moved to the presentation README and worded for all producers, as the old text already worded it. ADR 0038 Identity agrees.
-
Status "fallback warnings make recovered output observable end to end": dropped. ADR 0038 owns warnings. The sparse-quality verdict stays.
-
Fallback now names the cancellation exception and the generation-scoped condition. Both match the code and ADR 0038; the old text left them implicit.
-
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.
-
Modal containment: the ADR states the rule only. The conditions and fail-closed cases are unchanged in the presentation README.
-
Ownership: "
@agent-device/contracts/snapshot-presentationholds the neutral carrier and the clip-fold geometry contract" named a subpath that #2357 removed. It now reads "@agent-device/contracts/ios-snapshotholds 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](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.