# AGENTS.md — BalanceWatch Этот файл — точка входа для AI-агентов и разработчиков. Прочитай его целиком перед любым изменением в репозитории. ## Что это за проект **BalanceWatch** — самостоятельное (watch-only) приложение для учёта личных финансов под **watchOS 10+**. Это клиент экосистемы Balance: те же сущности и тот же протокол синхронизации используют iOS/macOS-, Android- и web-клиенты, а синхронизируются все они через self-hosted сервер BalanceServer. Проект выделен из монорепозитория Balance: туда входили таргет `BalanceWatch` и общий код, который этот таргет компилировал. Здесь этот код живёт как **копия** (см. раздел «Общий контракт»). - UI: SwiftUI, только системные компоненты. - Хранилище: SwiftData, только локально (`cloudKitDatabase: .none`). - Синхронизация: HTTP(S) к BalanceServer, сессия хранится в Keychain. - Внешних зависимостей (SPM/CocoaPods) нет — не добавляй их без явной необходимости. ## Структура ```text BalanceWatch/ ├── AGENTS.md ├── README.md ├── .github/workflows/build.yml # CI: сборка для симулятора и устройства ├── docs/CONTRACT_V2.md # общий контракт экосистемы v2 (копия) ├── branding/ # icon.svg, avatar, banner, render.sh; не входит в бандл ├── docs/ # CONTRACT_V2.md, скриншоты для баннера ├── BalanceWatch.xcodeproj/ # один таргет и одна схема — BalanceWatch └── BalanceWatch/ ├── App/ │ ├── BalanceWatchApp.swift # @main, ModelContainer, тема │ ├── WatchRootView.swift # главный экран: баланс, месяц, навигация │ ├── WatchAddTransactionView.swift # добавление операции (+ выбор счёта) │ ├── WatchAccountsView.swift # CurrencyContext в окружении, счета, │ │ # выбор пространства (ledger) │ └── WatchFeatures.swift # аналитика, операции, бюджеты, │ # категории, корректировка баланса, настройки ├── Models/ # SwiftData-модели и системные категории; │ # V2Records.swift — счета, переводы, курсы, SyncRecord ├── Shared/ # BalanceModelContainer, AppTheme, │ # CategoryDesignOptions, ServerSync (ServerAccountStore) ├── Utilities/ # FinanceCalculations, Money (сотые, курсы, конвертация), │ # форматирование валют и дат ├── Views/Components/CategoryIconView.swift └── Assets.xcassets ``` ### Xcode-проект `project.pbxproj` использует **папки, синхронизируемые с файловой системой** (`PBXFileSystemSynchronizedRootGroup`, objectVersion 77, нужен Xcode 16+). Любой файл, положенный в `BalanceWatch/`, сам попадает в таргет: - чтобы добавить файл, просто создай его в нужной папке, `project.pbxproj` трогать не нужно; - `project.pbxproj` редактируй только ради build settings; - не клади в `BalanceWatch/` файлы, которые не должны попасть в бандл (скрипты, документацию, тестовые данные). Ключевые build settings: `SDKROOT = watchos`, `WATCHOS_DEPLOYMENT_TARGET = 10.0`, `INFOPLIST_KEY_WKWatchOnly = YES`, `GENERATE_INFOPLIST_FILE = YES` (своего Info.plist нет — ключи задаются через `INFOPLIST_KEY_*`), bundle id `com.tolamironcenko.Balance.watchkitapp`, `SWIFT_VERSION = 5.0`. ## Правила разработки ### 1. Где что менять | Что | Где | | --- | --- | | Экраны часов | `App/` | | Модель данных | `Models/` + `Shared/BalanceModelContainer.swift` | | Финансовые правила | `Utilities/FinanceCalculations.swift` | | Авторизация и синхронизация | `Shared/ServerSync.swift` | | Переиспользуемые view | `Views/Components/` | Не дублируй доменную логику во view: расчёты — в `FinanceCalculations`, сеть — в `ServerAccountStore`. ### 2. SwiftData Схема зарегистрирована в `BalanceModelContainer.schema`: `FinanceTransaction`, `MonthlyBudget`, `CustomCategory`, `SyncTombstone`, `FinanceAccount`, `FinanceTransfer`, `ExchangeRate`, `SyncRecord`. - Каждое пространство данных (§7 контракта) — отдельное хранилище: личное — прежнее хранилище по умолчанию, общее — `BalanceLedger-`, где scope — хеш сервер + пользователь + ledger (`LedgerContainers`, `LedgerSelection`). Корневой view пересоздаётся через `.id(scope)`. - `syncID` — id записи на проводе. У записей, созданных до v2, он `nil`, и отправляется прежний `id.uuidString` (иначе на сервере появятся дубли); новые записи получают UUID в нижнем регистре, полученные — id с сервера. - `extraPayload` хранит незнакомые поля payload и отправляет их обратно (§0.2). `SyncRecord` хранит `recurring`, `goal`, `debt`, `attachment`, `setting` целиком. - У пользователей уже есть локальные базы. Не удаляй и не переименовывай persisted-поля без стратегии миграции; новые поля делай со значением по умолчанию или опциональными. - Если основное хранилище не открылось, `make()` откатывается на in-memory контейнер `BalanceRecovery`. Не убирай этот fallback. - CloudKit не используется и не должен включаться: единственный источник синхронизации — BalanceServer. ### 3. Изменение и удаление записей - Любое изменение синхронизируемой сущности обязано обновлять `syncUpdatedAt`, иначе изменение не уйдёт на сервер. - Удаляй синхронизируемые сущности через `modelContext.deleteForSync(...)`, а не через `modelContext.delete(...)`: первый вариант пишет tombstone в `SyncDeletionStore`, и удаление доходит до других устройств. - Операции и бюджеты хранят snapshot категории (имя, иконка, emoji, цвет). При переименовании пользовательской категории связанные операции и бюджеты нужно обновить — сохраняй это поведение. ### 4. Финансовые правила Центральные правила — в `FinanceCalculations`: - суммы считаются в целых сотых (`Money`), курсы — десятичные строки, округление половиной от нуля; интервалы полуоткрытые `[начало, конец)`; - общий баланс = доходы − расходы + переводы, всё в базовой валюте (`CurrencyContext`: `setting baseCurrency`, иначе `@AppStorage("currencyCode")`); нет курса — сумма не входит, показывается «нет курса C→B»; - баланс счёта — в его валюте без конвертации; - операции с `isBalanceAdjustment == true` входят в общий баланс, но **не** входят в периодическую аналитику (месяц/3 мес./12 мес.); - расходы группируются по `categoryName`. Эти правила **одинаковы во всех клиентах Balance** (Apple, Android, web) и описаны в `docs/CONTRACT_V2.md` §2–3. Не меняй их только здесь. ### 5. Синхронизация (общий контракт) `ServerAccountStore.shared` — единственная точка входа для авторизации и синхронизации (`register`, `login`, `logout`, `sync`). Эндпоинты — `v1/...` на BalanceServer; sync устроен как push изменений после последнего push (транзакции → категории → бюджеты → tombstones), затем pull по `cursor` пачками (`syncBatchSize`, `hasMore`). - Формат JSON (`SyncChange`, `SyncRequest`, `SyncResponse`, payload'ы сущностей), семантика cursor/sequence и auth-заголовки — **общий контракт со всеми клиентами и сервером**. Изменение здесь без изменения сервера и остальных клиентов сломает синхронизацию. - При смене URL сервера сессия очищается, cursor/push-state сбрасываются, пользователь логинится заново (`saveServer`). Сохраняй это поведение. Смена сервера и выход возвращают личное пространство. - v2: перед синхронизацией запрашивается `GET /v1/capabilities`; 404 — сервер v1, тогда `supportsV2 == false`: счета и общие бюджеты скрыты, `setting` не отправляется. Незнакомые сущности при pull пропускаются. - Общее пространство передаётся заголовком `X-Balance-Ledger` на `v1/sync`; cursor, push-state и журнал удалений имеют суффикс `LedgerSelection.keySuffix`. - Из сущностей v2 часы отправляют только `setting` (базовая валюта); счета, переводы, курсы, регулярные операции и прочее только читают. Регулярные операции часы не создают. ### 6. Безопасность - Пароли не сохраняются нигде. Access/refresh токены — только в Keychain (`KeychainStore`), никогда не в `UserDefaults`, SwiftData или файлах. - Для удалённого сервера — только HTTPS. HTTP разрешён только для `localhost`, `127.0.0.1`, `::1`. - Не логируй пароли, токены и заголовок `Authorization`. - Не коммить секреты, production-URL и credentials. ### 7. UI на часах - Предпочитай `NavigationStack`, `List`, `Form`, короткие списки и компактные действия; крупный текст с `minimumScaleFactor`. - Строки интерфейса — на русском (`developmentRegion = ru`). - Акцентный цвет — `.indigo`; тема берётся из `@AppStorage("appTheme")` через `AppTheme`. Валюта — `@AppStorage("currencyCode")`. - Ключи `@AppStorage`/`UserDefaults` не переименовывай: это сломает настройки у существующих пользователей. ## Сборка и проверка ```bash xcodebuild -project BalanceWatch.xcodeproj -scheme BalanceWatch \ -destination 'generic/platform=watchOS Simulator' \ CODE_SIGNING_ALLOWED=NO build ``` CI (`.github/workflows/build.yml`) собирает проект на `macos-15` для симулятора и для устройства без подписи. Перед PR сборка должна быть зелёной в обоих вариантах. Тестового таргета пока нет. `Utilities/Money.swift` не зависит от SwiftData/SwiftUI, а `FinanceCalculations` работает через протоколы `LedgerEntry`/`LedgerTransfer`, поэтому денежную математику можно проверить отдельным `swiftc`-прогоном на macOS (с заглушкой `FinanceTransaction`). Если добавляешь тесты, не завязывай их на текущую дату и локаль — явно фиксируй `Calendar`, `Date` и formatter'ы. Добавив тестовый таргет, добавь и шаг `test` в CI. ## Чек-лист перед PR - [ ] Изменение лежит в правильном слое (`App/`, `Models/`, `Shared/`, `Utilities/`). - [ ] Persisted-поля SwiftData не сломаны, миграция продумана. - [ ] Изменённые синхронизируемые сущности обновляют `syncUpdatedAt`. - [ ] Удаления идут через `deleteForSync`. - [ ] Финансовые правила и sync-контракт не разошлись с остальными клиентами Balance. - [ ] Секреты и токены не попали в репозиторий и логи. - [ ] CI собирает проект для симулятора и для устройства. - [ ] README/AGENTS.md обновлены, если поменялась структура или сборка. ## Оформление - Цвета интерфейса — только из `BalancePalette` (`Shared/AppTheme.swift`), палитра «Лес», общая с BalanceFlutter. Не возвращай indigo/purple. - Иконка, аватар и баннер рендерятся из SVG: `bash branding/render.sh` (нужен `resvg`). PNG в `AppIcon.appiconset` руками не правь. - Аватар репозитория в Gitea обновляется из `branding/avatar.png`.