diff --git a/.specsync/change-sequence.json b/.specsync/change-sequence.json index 6f645df..7b9138a 100644 --- a/.specsync/change-sequence.json +++ b/.specsync/change-sequence.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "sequence": 7, - "id": "CHG-0007-hermetic-userdefaults-in-tests", + "sequence": 8, + "id": "CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19", "acknowledged_collisions": [] } diff --git a/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/approvals.json b/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/approvals.json new file mode 100644 index 0000000..0fd5e09 --- /dev/null +++ b/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/approvals.json @@ -0,0 +1,19 @@ +{ + "approvals": [ + { + "gate": "definition", + "actor": "agent:cursor", + "timestamp": 1784405587, + "digest": "b5c82d28f1c53ec2ac954f7d8d0882709dcbe58e0e94c495ff9442d1687c037a", + "note": "Approve SyncState spike findings documentation." + }, + { + "gate": "acceptance", + "actor": "agent:cursor", + "timestamp": 1784405631, + "digest": "69534223835cb3a4a8ec741e98b24a023320e33c872e34f34220c26718572dd0", + "note": "Accept SyncState spike findings." + } + ], + "reopenings": [] +} diff --git a/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/change.md b/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/change.md new file mode 100644 index 0000000..fbfce53 --- /dev/null +++ b/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/change.md @@ -0,0 +1,24 @@ +--- +id: CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19 +state: accepted +type: documentation +base_commit: ecc0e2f1eb30504cb9a98b596389e1cd2de00e21 +--- + +# Spike: SyncState (iCloud) feasibility findings for issue 19 + +## Intent + +Spike: SyncState (iCloud) feasibility findings for issue 19 + +## Affected Canonical Specs + +- None + +## Acceptance Criteria + +- Findings doc at docs/spikes/syncstate-feasibility.md with go/no-go recommendation for SyncState in a bare CLI. + +## No-spec Rationale + +Documentation-only spike findings; no module API changes. diff --git a/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/context.md b/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/context.md new file mode 100644 index 0000000..f50f983 --- /dev/null +++ b/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/context.md @@ -0,0 +1,5 @@ +# Context + +Issue #19 asked whether AppState SyncState (NSUbiquitousKeyValueStore) can work from a bare CLI. + +This change records findings only; no production SyncState dogfood. diff --git a/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/docs.md b/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/docs.md new file mode 100644 index 0000000..86dcdd9 --- /dev/null +++ b/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/docs.md @@ -0,0 +1,5 @@ +# Docs + +See `docs/spikes/syncstate-feasibility.md` for the full SyncState (iCloud) feasibility spike. + +Verdict: no-go for a bare SPM CLI (entitlements, short-lived process, CI cannot prove iCloud). diff --git a/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/state.json b/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/state.json new file mode 100644 index 0000000..5ba7143 --- /dev/null +++ b/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/state.json @@ -0,0 +1,32 @@ +{ + "schema_version": 1, + "id": "CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19", + "slug": "spike-syncstate-icloud-feasibility-findings-for-issue-19", + "title": "Spike: SyncState (iCloud) feasibility findings for issue 19", + "description": "Spike: SyncState (iCloud) feasibility findings for issue 19", + "kind": "documentation", + "state": "accepted", + "canonical_applied": true, + "base_commit": "ecc0e2f1eb30504cb9a98b596389e1cd2de00e21", + "created_at": 1784405580, + "updated_at": 1784405631, + "affected_specs": [], + "affected_paths": [ + "docs/spikes/syncstate-feasibility.md", + ".specsync/change-sequence.json" + ], + "no_spec_change": true, + "no_spec_change_rationale": "Documentation-only spike findings; no module API changes.", + "acceptance_criteria": [ + "Findings doc at docs/spikes/syncstate-feasibility.md with go/no-go recommendation for SyncState in a bare CLI." + ], + "selected_artifacts": [ + "context", + "docs" + ], + "dependencies": [], + "answers": { + "architecture_risk": "no", + "public_contract": "no" + } +} diff --git a/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/verification-attempts.json b/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/verification-attempts.json new file mode 100644 index 0000000..2114caf --- /dev/null +++ b/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/verification-attempts.json @@ -0,0 +1,20 @@ +{ + "schema_version": 1, + "attempts": [ + { + "timestamp": 1784405625, + "commit": "ecc0e2f1eb30504cb9a98b596389e1cd2de00e21", + "contract_digest": "b5c82d28f1c53ec2ac954f7d8d0882709dcbe58e0e94c495ff9442d1687c037a", + "workspace_digest": "7ec8add955a67b6f79c9213fdfa45cddff2d68f7b3a0f4c5ea57c9560a0f8d21", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [] + } + ] +} diff --git a/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/verification.json b/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/verification.json new file mode 100644 index 0000000..10d8fee --- /dev/null +++ b/.specsync/changes/CHG-0008-spike-syncstate-icloud-feasibility-findings-for-issue-19/verification.json @@ -0,0 +1,41 @@ +{ + "timestamp": 1784405625, + "commit": "ecc0e2f1eb30504cb9a98b596389e1cd2de00e21", + "contract_digest": "b5c82d28f1c53ec2ac954f7d8d0882709dcbe58e0e94c495ff9442d1687c037a", + "workspace_digest": "7ec8add955a67b6f79c9213fdfa45cddff2d68f7b3a0f4c5ea57c9560a0f8d21", + "acceptance_input_digest": "8f29a941462274a04f64754be7e276d4263174ae15311d9f3b8ff9a88d7aa256", + "acceptance_manifest": { + "schema_version": 1, + "entries": [ + { + "path": ".specsync/change-sequence.json", + "kind": "file", + "mode": 33188, + "payload_digest": "cf6815bd4aff15dd98e5b68b1baf960876fc7b62b1439c9f84e3008f9475be83", + "entry_digest": "53425a8cbd12f59d00ac30e388d0ef8b439898d7578cc92f90500f74e91464f3", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "docs/spikes/syncstate-feasibility.md", + "kind": "file", + "mode": 33188, + "payload_digest": "3ee20c0f792877051f80f12a2afa1e26cf76345c3cf65a489112292d0484a900", + "entry_digest": "51023ff91d431468ce0fe5bb840f7baaa8a3e74c9d5698b47de188cda4b5f37d", + "owners": [ + "@exact:delivery" + ] + } + ] + }, + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [] +} diff --git a/docs/spikes/syncstate-feasibility.md b/docs/spikes/syncstate-feasibility.md new file mode 100644 index 0000000..e8329d5 --- /dev/null +++ b/docs/spikes/syncstate-feasibility.md @@ -0,0 +1,86 @@ +# Spike: SyncState (iCloud) feasibility for `aps` + +Issue: [#19](https://github.com/0xLeif/aps-cli/issues/19) + +Status: investigation only. No production SyncState code. + +## Verdict + +**No-go** for shipping or dogfooding `SyncState` in the `aps` CLI during 0.x (and likely beyond while `aps` remains a bare SPM executable). + +Local API calls without entitlements do not meaningfully sync. A demo that proves real iCloud round-trips is possible only with a signed, provisioned macOS identity and a non-headless environment; that packaging model conflicts with how `aps` is built, tested, and run today. + +## What AppState `SyncState` actually does + +Inspected AppState sources under SPM checkout `AppState` (`SyncState.swift`, `Application+SyncState.swift`): + +- Backing store is `NSUbiquitousKeyValueStore.default`. +- Values are JSON-encoded `Data` written through a small `UbiquitousKeyValueStoreManaging` adapter. +- A local `StoredState` fallback is used when the iCloud store has no value. +- Apple-platform only (`#if canImport(Security)`); unavailable on Linux. +- AppState docs mirror Apple's requirement: request `com.apple.developer.ubiquity-kvstore-identifier` and configure iCloud Key-Value storage in an Xcode / signed app project. +- The `icloudStore` dependency registers for `didChangeExternallyNotification`. External updates need a process that stays alive long enough to receive them. +- AppState's own `SyncStateTests` exercise encode/decode and the local fallback path; they do not prove cross-device iCloud sync. + +Apple documents that writes stay in memory then flush asynchronously; `synchronize()` only hints that data is ready, does not force an upload, and returns `false` when entitlements are missing. + +## Bare CLI experiment (local) + +A throwaway unsigned Swift executable (no Info.plist, no entitlements) was run on this machine: + +```text +synchronize=false +bundleID=nil +synchronize_after_set=false +read_back=nil +``` + +The existing `swift build` `aps` binary only carries the debug entitlement `com.apple.security.get-task-allow`. It has no ubiquity KVS entitlement and no stable bundle identifier. + +Conclusion: from a bare CLI process, `NSUbiquitousKeyValueStore` does not attach to a real iCloud store. Any "it compiled" or "AppState fell back to StoredState" result is not SyncState dogfooding. + +## Requirements for a meaningful demo + +To demonstrate real SyncState behavior (write on machine A / process 1, observe on machine B / process 2 via iCloud), you would need all of the following: + +| Requirement | Why | +| --- | --- | +| Apple Developer Program team | Provision iCloud Key-Value storage on an App ID | +| Stable bundle identifier | KVS identity is `TeamID` + store id (usually bundle id) | +| Entitlement `com.apple.developer.ubiquity-kvstore-identifier` | Without it, `synchronize()` fails and the store is unknown | +| Code signing that embeds that entitlement | SPM `swift build` / `swift run` do not apply restricted entitlements | +| Likely an app-bundle or Xcode-managed target | Restricted entitlements are authorized by provisioning profiles; bare tools are a poor fit | +| Interactive macOS session signed into iCloud | Headless runners typically have no iCloud account | +| Long enough process lifetime | System sync and `didChangeExternally` are async; short CLI exits race the daemon | + +Developer ID distribution can work for macOS apps with KVS in some setups (Apple's "App Store only" wording is outdated for some cases), but that still does not make SPM `swift run aps` a viable dogfood path. + +## Headless / CI behavior + +| Environment | Expectation | +| --- | --- | +| Linux smoke CI | SyncState does not compile into AppState's Apple-only surface; cannot be a cross-platform demo key | +| macOS GitHub Actions | No developer-provisioned iCloud KVS for this open repo, no signed-in iCloud user, no reliable sync | +| Local unsigned `swift test` / `swift run` | At best exercises AppState's local fallback; false confidence | + +Automated CI cannot be the proof surface for SyncState. Manual, signed, two-device (or two-Mac) testing would be required. + +## Fit with `aps` goals + +`GOAL.md` already lists SyncState as out of scope for 0.x. That remains correct: + +- `aps` dogfoods AppState as a hermetic CLI with `APS_HOME` / `--state-dir` isolation. +- iCloud KVS is account-global, entitlement-bound, and not path-isolated like FileState. +- Agents and CI need deterministic, local state. SyncState is the opposite: eventual, account-scoped, and environment-dependent. +- Packaging cost (Xcode project, signing secrets, Apple ID) is disproportionate to the demo value for a 0.x harness. + +## Go / no-go recommendation + +| Option | Recommendation | +| --- | --- | +| Add a SyncState demo key to `aps` in 0.x | **No-go** | +| Rely on local fallback as "dogfooding SyncState" | **No-go** (misleading) | +| Separate signed macOS sample app outside this CLI | Optional later research; not required for `aps` 0.x | +| Keep SyncState out of scope until packaging model changes | **Go** (status quo) | + +**Final recommendation: no-go.** Close the spike without production code. Revisit only if `aps` gains a signed macOS app-bundle distribution path and an explicit non-CI dogfood story.