|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +## Project Context |
| 4 | + |
| 5 | +ObservableDefaults is a Swift 6 package that integrates SwiftUI Observation with two storage backends: |
| 6 | + |
| 7 | +- `@ObservableDefaults` for `UserDefaults` |
| 8 | +- `@ObservableCloud` for `NSUbiquitousKeyValueStore` |
| 9 | + |
| 10 | +The package has two core targets: |
| 11 | + |
| 12 | +- `Sources/ObservableDefaults`: public macros, runtime wrappers, test-mode cloud mock |
| 13 | +- `Sources/ObservableDefaultsMacros`: SwiftSyntax macro implementations and diagnostics |
| 14 | + |
| 15 | +Most changes are correctness-sensitive because public behavior is generated by macros and then executed through runtime wrappers. When changing behavior, inspect both the macro expansion logic and the runtime wrapper behavior before editing. |
| 16 | + |
| 17 | +## Key Files |
| 18 | + |
| 19 | +- `Package.swift`: package targets and SwiftSyntax dependency range |
| 20 | +- `Sources/ObservableDefaults/Macros.swift`: public macro declarations and user-facing parameter docs |
| 21 | +- `Sources/ObservableDefaults/UserDefaults/UserDefaultsWrapper.swift`: `UserDefaults` read/write resolution rules |
| 22 | +- `Sources/ObservableDefaults/NSUbiquitousKeyValueStore/NSUbiquitousKeyValueStoreWrapper.swift`: cloud read/write resolution rules and DEBUG test-mode isolation |
| 23 | +- `Sources/ObservableDefaultsMacros/Macros/ObservableDefaultsMacro.swift`: generated `UserDefaults` observation and external change handling |
| 24 | +- `Sources/ObservableDefaultsMacros/Macros/ObservableCloudMacro.swift`: generated cloud observation logic |
| 25 | +- `Sources/ObservableDefaultsMacros/Macros/DefaultsBackedMacro.swift`: generated `UserDefaults` accessor behavior and declaration-time default capture |
| 26 | +- `Sources/ObservableDefaultsMacros/Macros/CloudBackedMacro.swift`: generated cloud accessor behavior |
| 27 | +- `Tests/ObservableDefaultsTests`: authoritative behavior coverage |
| 28 | +- `README.md` and `README_zh.md`: public semantics and usage docs |
| 29 | + |
| 30 | +## Development Rules |
| 31 | + |
| 32 | +### Macro and Runtime Semantics |
| 33 | + |
| 34 | +- Do not mix `@ObservableDefaults` and `@ObservableCloud` on the same class. Keep storage concerns split across separate types. |
| 35 | +- Both top-level macros are class-only. Do not introduce examples or tests that suggest struct support. |
| 36 | +- `@DefaultsBacked` and `@CloudBacked` do not support `willSet` / `didSet`. `@ObservableOnly` does. |
| 37 | +- In `observeFirst: true` mode, only explicitly backed properties are persistent. Observable-only properties must not appear in external storage notification handling. |
| 38 | +- Prefix values are trimmed and must not contain `"."`. Preserve this invariant. |
| 39 | +- Static properties must remain ignored by persistence/observation generation. |
| 40 | + |
| 41 | +### Default and Fallback Behavior |
| 42 | + |
| 43 | +- Declaration-time defaults are immutable model defaults. They are not the same as current runtime values. |
| 44 | +- `@ObservableDefaults` fallback order is: |
| 45 | + 1. persisted value in the selected `UserDefaults` domain |
| 46 | + 2. value from `UserDefaults.register(defaults:)` |
| 47 | + 3. declaration-time model default captured by the macro |
| 48 | +- `@ObservableCloud` fallback order is: |
| 49 | + 1. persisted cloud value |
| 50 | + 2. declaration-time model default |
| 51 | +- When fixing key-removal or missing-key behavior, verify both non-optional and optional properties. |
| 52 | +- Never use the current cached property value as the fallback when recomputing external `UserDefaults` changes. |
| 53 | + |
| 54 | +### Storage Resolution Rules |
| 55 | + |
| 56 | +- RawRepresentable-based storage takes priority over Codable JSON storage when multiple conformances match. |
| 57 | +- For hybrid `RawRepresentable & PropertyListValue` types, maintain raw-value write behavior and compatibility fallback on reads. |
| 58 | +- If you change overload ordering or wrapper dispatch, run the ambiguity and compatibility suites before considering the change complete. |
| 59 | + |
| 60 | +## Testing Expectations |
| 61 | + |
| 62 | +- Run `swift test` before finishing any behavioral change. |
| 63 | +- If a change touches external notifications, fallback logic, or equality checks, at minimum run: |
| 64 | + - `swift test --filter ExternalChangeEqualityTests` |
| 65 | + - `swift test --filter ObservableDefaultsTests` |
| 66 | +- If a change touches cloud test-mode behavior, mock storage, or notification handling, also run: |
| 67 | + - `swift test --filter ObservableCloudTests` |
| 68 | + - `swift test --filter Issue26OverloadAmbiguityTests` |
| 69 | +- If a change touches prefixes, suite parsing, or macro string parameters, run: |
| 70 | + - `swift test --filter PrefixTests` |
| 71 | + - `swift test --filter WhitespaceTests` |
| 72 | + - `swift test --filter StringParameterTests` |
| 73 | +- If a change touches storage format or overload resolution, run: |
| 74 | + - `swift test --filter RawRepresentableCodableTests` |
| 75 | + - `swift test --filter Issue26OverloadAmbiguityTests` |
| 76 | + |
| 77 | +### Test Isolation Rules |
| 78 | + |
| 79 | +- `UserDefaults` tests should use isolated suite names. Prefer `UserDefaults.getTestInstance(suiteName:)`, and use a unique suite per test when cross-test interference is possible. |
| 80 | +- Cloud `.testMode` tests use per-test mock suite isolation through `NSUbiquitousKeyValueStoreWrapper.testSuiteName`. Preserve that mechanism. |
| 81 | +- If a suite relies on shared notifications or shared storage state, make it `.serialized`. |
| 82 | +- Do not “fix” flaky tests by weakening expectations if the real issue is shared storage or notification interference. |
| 83 | + |
| 84 | +## Documentation Rules |
| 85 | + |
| 86 | +- If you change public behavior, update `README.md` and `README_zh.md` in the same change. |
| 87 | +- If you change public macro parameters, update: |
| 88 | + - `Sources/ObservableDefaults/Macros.swift` |
| 89 | + - `README.md` |
| 90 | + - `README_zh.md` |
| 91 | +- If you change fallback semantics, `observeFirst`, App Group behavior, or development/test mode behavior, update the README examples and explanatory sections, not just inline comments. |
| 92 | + |
| 93 | +## Review Priorities |
| 94 | + |
| 95 | +When reviewing changes, prioritize: |
| 96 | + |
| 97 | +1. behavioral regressions in generated code |
| 98 | +2. missing tests for external changes and fallback paths |
| 99 | +3. `MainActor` and `defaultIsolationIsMainActor` coverage |
| 100 | +4. `observeFirst` correctness |
| 101 | +5. storage format compatibility for RawRepresentable/Codable hybrids |
| 102 | +6. test isolation and notification scope |
| 103 | + |
| 104 | +## Release Workflow |
| 105 | + |
| 106 | +- Bug fixes should normally increment the patch version. |
| 107 | +- Current release style uses plain semantic version tags such as `1.8.3`, not `v1.8.3`. |
| 108 | +- Before tagging a release: |
| 109 | + - merge the intended commits into `main` |
| 110 | + - run full `swift test` |
| 111 | + - confirm `README.md` and `README_zh.md` reflect any public semantic changes |
| 112 | +- After tagging: |
| 113 | + - push `main` |
| 114 | + - push the tag |
| 115 | + - create a GitHub release with notes |
| 116 | +- Release notes should summarize: |
| 117 | + - the user-visible fix |
| 118 | + - any fallback or compatibility semantics that changed or were clarified |
| 119 | + - test coverage added |
| 120 | + - documentation updates |
| 121 | +- If a release includes an external PR, explicitly thank the original contributor in the release notes. |
| 122 | + |
| 123 | +## Commands |
| 124 | + |
| 125 | +Common commands used in this repository: |
| 126 | + |
| 127 | +```bash |
| 128 | +swift test |
| 129 | +swift test --filter ExternalChangeEqualityTests |
| 130 | +swift test --filter ObservableDefaultsTests |
| 131 | +swift test --filter ObservableCloudTests |
| 132 | +swift test --filter Issue26OverloadAmbiguityTests |
| 133 | +git switch main |
| 134 | +git merge --ff-only <branch> |
| 135 | +git tag -a 1.8.3 -m "1.8.3" |
| 136 | +git push origin main |
| 137 | +git push origin 1.8.3 |
| 138 | +gh release create 1.8.3 --generate-notes --title "1.8.3" |
| 139 | +``` |
0 commit comments