---
phase: 35-psyche-sync-cross-machine-context-backup-via-private-gh-repo
plan: 01
subsystem: infra
tags: [serde, serde_json, settings, sync, owlery, rust]

# Dependency graph
requires:
  - phase: 18.2-spacetime-filesystem
    provides: spt_home() resolver ($SPT_HOME runtime root, separate from ~/.claude)
provides:
  - SyncState enum (5 D-11 variants, kebab-case serde)
  - SyncSettings struct (10 fields, all #[serde(default)])
  - sync_settings_path() -> $SPT_HOME/settings.json
  - read_sync_settings() (soft-fail to default on any parse error)
  - write_sync_settings() (sibling-preserving splice of the sync key only)
affects: [35-02, 35-03, 35-04, sync.rs, hook-dispatch, doctor, session-start, accept_flow]

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "Sibling-preserving JSON splice via serde_json::Map::insert (only the sync key mutates; preserve_order keeps siblings)"
    - "Hot-path soft-fail read: any I/O or parse error returns Default, never panics, never prints stderr (D-14)"

key-files:
  created: []
  modified:
    - src/common/owlery.rs

key-decisions:
  - "Sync namespace persists at $SPT_HOME/settings.json, NOT ~/.claude/settings.json (Pitfall 7) — keeps SPT runtime state out of the claude-code-managed file owned by auto_setup"
  - "Timestamps stay Option<String> (ISO-8601), not chrono::DateTime — later plans compare lexicographically"
  - "Every SyncSettings field carries #[serde(default)] so partial/older JSON loads cleanly"

patterns-established:
  - "Read-modify-write that splices a single top-level namespace key, preserving unknown siblings"
  - "Graceful-degrade read returning Default on missing-file / invalid-json / missing-key / shape-mismatch"

requirements-completed: [SYNC-BOOTSTRAP-01]

# Metrics
duration: 3min
completed: 2026-05-26
---

# Phase 35 Plan 01: Sync Settings Data Layer Summary

**SyncState enum + SyncSettings struct + 3 sibling-preserving helpers persisting the Phase 35 sync namespace under $SPT_HOME/settings.json, with 9 inline serde/file round-trip tests.**

## Performance

- **Duration:** ~3 min
- **Started:** 2026-05-26T23:26:00Z
- **Completed:** 2026-05-26T23:27:49Z
- **Tasks:** 2 (TDD)
- **Files modified:** 1

## Accomplishments
- `pub enum SyncState` — the five D-11 variants (`Unset`, `Enabled`, `Declined`, `RemindLater`, `Failing`) with `#[serde(rename_all = "kebab-case")]` so `RemindLater <-> "remind-later"`; `Default = Unset`.
- `pub struct SyncSettings` — exactly the 10 D-11 fields in order, every field `#[serde(default)]`, manual `Default` impl (`state = Unset`, `consecutive_failures = 0`, all `Option` timestamps `None`).
- `pub fn sync_settings_path()` — returns `spt_home().join("settings.json")` with a doc comment explicitly stating the target is `$SPT_HOME/settings.json` (NOT `~/.claude/settings.json` — Pitfall 7), separate from the claude-code file maintained by `crate::common::auto_setup`.
- `pub fn read_sync_settings()` — soft-fails to `SyncSettings::default()` on missing file, invalid JSON, missing `sync` key, or shape mismatch; never panics, never prints stderr (D-14).
- `pub fn write_sync_settings()` — splices ONLY the `sync` key via `serde_json::Map::insert`, preserving sibling top-level keys; best-effort parent-dir creation; surfaces real I/O errors to the caller.

## Task Commits

Both TDD tasks landed in a single atomic commit because the helpers (Task 2) reference the `SyncSettings` type (Task 1) and there is no independently-compilable intermediate state within one file (cross-symbol coupling — see Deviations):

1. **Task 1 + Task 2: sync settings data layer** - `9ce41f7` (feat)

**Plan metadata:** committed separately with this SUMMARY.

_Note: tests landed alongside implementation in the same commit; for a pure data-layer struct the type and its tests are compile-coupled._

## Files Created/Modified
- `src/common/owlery.rs` - Added the `SYNC_SETTINGS_KEY` const, `SyncState` enum + `Default`, `SyncSettings` struct + `Default`, and `sync_settings_path()` / `read_sync_settings()` / `write_sync_settings()` helpers, plus 9 inline unit tests in the existing `#[cfg(test)] mod tests` block.

## Public Symbols Added (5)
- `pub enum SyncState` (owlery.rs:142)
- `pub struct SyncSettings` (owlery.rs:167)
- `pub fn sync_settings_path() -> PathBuf` (owlery.rs:210)
- `pub fn read_sync_settings() -> SyncSettings` (owlery.rs:219)
- `pub fn write_sync_settings(s: &SyncSettings) -> std::io::Result<()>` (owlery.rs:246)

## Test Count: 9
`sync_settings_default`, `sync_state_serde`, `sync_settings_round_trip`, `sync_settings_path_under_spt_home`, `sync_settings_read_default`, `sync_settings_round_trip_file`, `sync_settings_preserves_siblings`, `sync_settings_inserts_alongside_siblings`, `sync_settings_invalid_json_returns_default`. All pass under `cargo test --lib owlery::tests::sync_ -- --test-threads=1` (env-mutating tests serialize via the existing `ENV_LOCK` + `EnvSnapshot` idiom).

## sync_settings_path Doc-Comment (Pitfall 7 distinction)
> Target is `$SPT_HOME/settings.json` (NOT `~/.claude/settings.json` — see Phase 35 RESEARCH Pitfall 7). The sync namespace lives in the SPT runtime root, separate from the claude-code-managed file maintained by `crate::common::auto_setup`.

## Decisions Made
- None beyond the plan — schema, field order, target path, and serde conventions all followed D-11 / Pitfall 7 as specified.

## Deviations from Plan

### Combined task commits (structural, not a content deviation)
- **Found during:** Task 2.
- **Issue:** Task 2's helpers reference the `SyncSettings` type from Task 1; both live in `src/common/owlery.rs`. Splitting into two commits would have produced a non-compiling intermediate (or required an artificial split of one file's diff).
- **Resolution:** Both tasks committed atomically in `9ce41f7` per the cross-symbol-coupling guidance ("Combined tasks into single commit when intermediate state doesn't compile").
- **Impact:** None on plan scope. All planned behavior delivered.

### One extra test (additive)
- The plan named 8 tests; this plan ships 9. The added `sync_settings_path_under_spt_home` test asserts `sync_settings_path() == spt_home().join("settings.json")` under an env-overridden `SPT_HOME` tempdir (Task 2 Test 1), which the plan's verify line referenced as `owlery::sync_settings_path`. Net: stronger coverage, no scope creep.

---

**Total deviations:** 2 (1 commit-structure, 1 additive test). No content/scope deviations.
**Impact on plan:** None — all must-haves, artifacts, and key-links satisfied.

## Issues Encountered
None. Tests required `--test-threads=1` (already the established convention for env-mutating tests in this module via `ENV_LOCK`).

## Known Stubs
None. All three helpers are fully wired and exercised by tests. No external callers yet (Plan 35-02 onward consumes them — by design).

## User Setup Required
None - no external service configuration required.

## Next Phase Readiness
- Data layer is a dependency-free leaf — Wave 2+ (sync.rs primitives, hook dispatch, doctor surface, session-start emission, accept_flow) can now read/mutate sync state via the three helpers.
- `serde_json` `preserve_order` feature (already in Cargo.toml) guarantees sibling-key stability on rewrite — downstream plans can rely on it.

## Self-Check: PASSED
- `src/common/owlery.rs` modified — FOUND (5 public symbols at lines 142/167/210/219/246).
- Commit `9ce41f7` — FOUND in git log.
- 9 unit tests pass; `cargo build --release` clean.

---
*Phase: 35-psyche-sync-cross-machine-context-backup-via-private-gh-repo*
*Completed: 2026-05-26*
