Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .specsync/change-sequence.json
Original file line number Diff line number Diff line change
@@ -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": []
}
Original file line number Diff line number Diff line change
@@ -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": []
}
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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).
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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": []
}
]
}
Original file line number Diff line number Diff line change
@@ -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": []
}
86 changes: 86 additions & 0 deletions docs/spikes/syncstate-feasibility.md
Original file line number Diff line number Diff line change
@@ -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.
Loading