Skip to content

feat(Where): gate synced history by data compatibility version - #495

Open
kyleve wants to merge 3 commits into
mainfrom
codex/store-compatibility-lockout
Open

kyleve wants to merge 3 commits into
mainfrom
codex/store-compatibility-lockout

Conversation

@kyleve

@kyleve kyleve commented Nov 14, 2026 •

Copy link
Copy Markdown
Owner

Posted by an AI agent on kyleve's behalf.

Summary

Gate Where's synced history with an independent data compatibility version before opening SwiftData. A newer shared version shows a lockout screen. If compatibility changes during use, Where hides history, stops GPS, drains the old scope, and returns to the launch gate.

Only an installation with Automatic Recording On and Always location permission can advance the shared version. A viewer on a newer build waits for its recording device. Version 1 introduces the gate without changing the existing history format.

Product behavior

Situation Before After
Shared version matches this build Open history Open history after verification
Shared version is newer Open potentially incompatible history Show Update Where; no store open
This build is newer, recording is Off Open history Wait for the recording device; no version write
This build is newer, recording is On with Always permission Open history Conditionally publish the version before opening history
A newer version appears while running Keep the session alive Hide history, stop GPS, retire the scope, and show lockout
Network fails after same-account, same-version verification No compatibility decision Continue offline using the local receipt
Intent or share arrives during lockout Can access history Intent fails with recovery guidance; share asks the user to open Where

A first installation can choose recording without opening history. Established viewers cannot enable recording from the lockout screen. Local-only Development builds and demo scopes bypass CloudKit verification.

Architecture

API Responsibility
DataCompatibilityCoordinator One process-owned admission decision, a monotonic receipt, recording eligibility, and coalesced checks
CloudKitDataCompatibilityTransport Account-bound reads and conditional saves in a private control zone outside SwiftData
WhereCompatibilityModel Launch gate, foreground/push/import/Retry monitoring, and whole-scope retirement
WhereServices.shutdown() Stop recording before slow cleanup and drain owned observers without erasing consent or the outbox
IntentServices.block(_:) / share receipt check Fail new entry-point work before it obtains or opens the store

The runtime creates the coordinator once and injects it into the UI model and every bootstrap:

let coordinator = DataCompatibilityCoordinator(
    transport: CloudKitDataCompatibilityTransport(containerIdentifier: "iCloud.com.stuff.where"),
    receipts: AppGroupDataCompatibilityReceipt(appGroupIdentifier: appGroupIdentifier),
    version: .current,
)
let compatibility = WhereCompatibilityModel(coordinator: coordinator)
// The runtime injects these same instances into WhereModel and WhereBootstrap.
flowchart TD
    Trigger[Boot / foreground / push / remote import / Retry / poll] --> Check[Verify independent CloudKit version]
    Check -->|Equal or verified offline receipt| Gate[Compatibility launch gate]
    Check -->|Older and recording On with Always permission| Publish[Conditional version advance]
    Publish -->|Verified| Gate
    Check -->|Newer or cannot verify/advance| Lock[Show lockout]
    Lock --> Retire[Stop GPS and drain existing scope]
    Retire --> Gate
    Gate -->|Allowed| Open[Open one fresh scope]
Loading

The foreground poll runs every 60 seconds, including while locked. Push delivery is advisory. Metadata and SwiftData propagation are not atomic across devices. Existing operations can finish; the boundary is the whole scope.

Screenshots

Update required: an older build hides history and asks for an app update. UpdateRequired fixture at 0126a337a57db48f3e8b9665b6106179cbd61d83, iPhone, light appearance; Standard and Flat themes.

Update Where lockout in Standard and Flat themes

Waiting for the recording device: a newer viewer cannot advance the data version. RecordingDevice fixture at 0126a337a57db48f3e8b9665b6106179cbd61d83, iPhone, light appearance; Standard and Flat themes.

Viewer waiting for its recording device to update

Review focus

Read the Core decision, lifecycle integration, and launch regressions.

An independent review covered the complete change and affected callers. Its findings led to regression coverage for cached-floor preservation, malformed receipt versions, retired recording after suspended startup/import, pending intent rejection, and onboarding ownership. No actionable findings remain after the final pass. A separate integration review of main c954dd31e also found no regressions: both architecture guards, compatibility masking, adaptive inset wiring, Flyover worlds, localization entries, and required guidance remain intact.

The share extension receives only a local receipt reader. It has no CloudKit capability. Missing, unreadable, blocked, or mismatched receipts fail before SwiftDataStore.make.

Compatibility and rollout

Deploy the WhereStoreCompatibility record schema (requiredVersion: Int64, accountID: String) to Production before distributing cloud-enabled builds. Ship gate version 1 to supported installations before increasing DataModelVersion.current for an incompatible change. Builds that predate the gate cannot enforce it.

The atomic App Group receipt is excluded from backups and retains the highest observed version. Offline access requires the same account and version, plus CloudKit's ability to resolve that account identity. Reset, lockout, and backup restore do not lower the compatibility floor.

The device acceptance procedure covers rollout, two-device propagation, account changes, offline launches, and entry points.

Testing

  • ./test --all after merging main c954dd31e: 3,000 iOS tests passed, plus 28 host macro tests, 30 architecture tests, architecture lint, and the host helper regressions. SnapshotKitTesting's existing known-issue cases remain marked as expected.

  • Focused regression run before the main merge (ac42349b0): 73 tests passed across compatibility, recording retirement, onboarding recovery, and intents.

  • ./test --snapshots --only WhereUISnapshotTests/DataCompatibilityViewSnapshotTests --only WhereUISnapshotTests/RootViewSnapshotTests --only WhereUISnapshotTests/OnboardingViewSnapshotTests --record never --no-generate: 3 suites / 33 reference images passed. Reviewed all 16 new lockout references; root and onboarding references remain unchanged.

  • ./swiftformat --lint, ./xcstrings --lint, ./attribution --check, ./sync-agents --check, and git diff --check passed.

  • ./sf-symbols --lint reports six pre-existing raw-symbol violations in Shared/SnapshotKitTesting/Tests/SnapshotImageRenderingTests.swift; all six also exist on merged main c954dd31e.

  • Not verified on physical devices: two-device CloudKit propagation, offline cold-launch account identity availability, and live Siri/share-extension host UI. No production schema was deployed. Beta/App Store audience builds remain CI validation.

  • Browser access is disabled in this session. Snapshot PNGs were reviewed locally; the published image bytes and GitHub-rendered HTML are checked through the API. Live PR layout and Mermaid rendering were not visually inspected.

…ment

Groundwork for the app launch gate: use an independent conditional CloudKit record, an account-bound offline receipt, and a recording-only version advance. Add permanent recording retirement and drain owned service observations without erasing the location outbox. The app wiring follows in the next commit.

Validation: 73 focused Core, UI, and intent regression tests passed, including 58 Core tests; 29 architecture tests and architecture lint passed. Catalog and attribution checks passed.
Verify the independent data version before bootstrap, onboarding discovery,
and backup recovery can open history. Keep foreground, push, and import
monitoring outside the disposable data scope. On lockout, hide history,
stop recording, drain owned work, and recover through the launch gate.

Only recording installations with Always permission may advance the shared
version. Block intent admission and require a matching local receipt in the
share extension. Add lockout UI, regression coverage, snapshots, ownership
lint, and deployment guidance.

Validation: full iOS, host, and architecture suites passed; focused lockout,
root, and onboarding snapshots passed. Physical two-device CloudKit checks
and production schema deployment remain release validation.

@kyleve kyleve Nov 14, 2026 •

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kyleve's behalf.

Data compatibility lockout: the older build asks for an app update; a newer viewer waits for its recording device. Both fixtures are from 0126a337a57db48f3e8b9665b6106179cbd61d83, iPhone, light appearance, with Standard and Flat themes side by side.

Update Where lockout

Waiting for the recording device

All 16 references passed, including dark appearance, iPad, large text, and accessibility overlays. These fixtures show layout and copy; live VoiceOver and cross-device CloudKit behavior remain device validation.

@kyleve kyleve Nov 14, 2026 •

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kyleve's behalf.

Onboarding recording choice, PhoneRecordingChoice fixture at 0126a337a57db48f3e8b9665b6106179cbd61d83, iPhone, light appearance. The existing reference passes unchanged. Continue now owns one completion task and verifies compatibility before opening history; the image demonstrates the retained choice UI, not that asynchronous ordering.

Onboarding recording choice

@kyleve kyleve Nov 14, 2026 •

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kyleve's behalf.

Root screen after admission, LoggedIn fixture at 0126a337a57db48f3e8b9665b6106179cbd61d83, iPhone, light appearance. The existing reference passes unchanged. A blocked decision replaces this content with DataCompatibilityView across launch phases; its snapshots are attached to that source file.

Root screen after compatibility admission

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kyleve's behalf.

Review focus: offline continuity still requires CKContainer.userRecordID() to resolve the account before the coordinator can use a verified receipt. The deterministic tests cover account-bound fallback and stale replies; airplane-mode cold launch needs the physical-device acceptance check in the rollout guide.

The separate control record and SwiftData propagation are not atomic. Push, foreground, import, Retry, and polling detect changes, after which the entire scope retires.

Retain both architecture guards and their tests. Move compatibility guidance
into the reorganized instruction owners. Preserve the compatibility mask
and monitoring with main's adaptive developer inset wiring.

Validate the merged result with the full unit/host/architecture suites,
focused lockout/root/onboarding snapshots, formatting, catalogs, attribution,
and offline agent-guidance checks.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant