# Требования к модулю: m03-program-store > Модуль — сервис `ProgramStore` в `Server.WatchBody.Core`: единая точка > работы с программами атлета — версионный JSON, жизненный цикл версий > `draft → active → archived`, гарантия «`active` — ровно одна». > Самодостаточная копия требований (d-dev-6). ## 1. Назначение и границы **В границах модуля:** - класс `ProgramStore` (`Server.WatchBody.Core`, каталог `Programs\`): сервис над репозиториями m01 (`ProgramRepository`, `ProgramVersionRepository`) — единственный вход для всех потребителей (REST API m02/m08, ProgressionRules m05); - жизненный цикл версий: создание draft (новая версия на основе актуальной), активация, архивация; - чтение: активная версия (с body), список версий (без body), версия по id (с body); - тесты сервиса (интеграционные, docker `watchbody-pg`). **Вне границ модуля:** правила прогрессии и делоада (m05 — создают draft-версии **через** ProgramStore), импорт пакета программы (m06 — тоже через ProgramStore), редактор программы на телефоне (m09), запись версий из sync в m02 (остаётся на репозиториях — уже работает; переход на ProgramStore — кандидат рефакторинга, не в v1). Схема тела программы (недели → тренировки → упражнения → подходы-план, дизайн LLM-планирования разд. 2) в v1 сервером не валидируется — хранится как есть (jsonb). ## 2. Стек и соглашения - C# / .NET 10, `Server.WatchBody.Core`; доступ к БД — только репозитории m01 (Kit.Helpers), прямого SQL нет. - Конфиг `Progression.*`/`Deload.*` в модуле не потребляется (это m05). - Логи — Kit.Helpers `Log.Logger` (уровень Info). ## 3. Инварианты (🛡 — нарушать запрещено) 1. **Программы принадлежат атлету** — все операции принимают `athleteId`; чужие программы/версии недоступны. 2. **Неизменяемость версии** — после создания `body`, `author`, `based_on_version` не меняются; меняется только `status`. 3. **Ровно одна `active` на программу** — активация переводит прежнюю активную в `archived` (гарантия БД — частичный уникальный индекс; ProgramStore использует только переходы, разрешённые БД). 4. **Переходы статусов только `draft → active → archived`** — повторная активация archived-версии запрещена (явное действие пользователя — Ф-10.4); 🛡 автоматической смены активной версии нет (vision 2.4). 5. **Тренировки ссылаются на версию, активную на их начало** — ProgramStore не перепривязывает ссылки (история не «поплывёт»). ## 4. Состав сервиса (контракты, шаг 1) | # | Метод | Назначение | |---|---|---| | 1 | `EnsureProgram(athleteId, title)` | программа атлета (создаёт при отсутствии; UNIQUE(athlete_id)) | | 2 | `CreateDraft(athleteId, body, author, rationale, basedOnVersion = null)` | новая draft-версия: `version = max+1` (БД), `based_on_version` = актуальная максимальная (или явная); author ∈ user/admin/rules (llm — v2) | | 3 | `Activate(athleteId, versionId)` | draft → active; прежняя active → archived; не-draft → ошибка | | 4 | `Archive(athleteId, versionId)` | active → archived (смена активной = активация другой версии) | | 5 | `GetActive(athleteId)` | активная версия с body (или null) | | 6 | `ListVersions(athleteId)` | версии программы без body (статусы, номера, rationale) | | 7 | `GetVersion(athleteId, versionId)` | версия с body | ## 5. Критерии приёмки модуля 1. Жизненный цикл: CreateDraft → Activate → (новая draft) → Activate — активна ровно одна, прежняя archived; повторная активация archived — ошибка. 2. Изоляция: операции одного атлета не видят/не трогают программы другого. 3. Неизменяемость: после создания body/author/based_on_version не меняются ни одним методом сервиса. 4. CreateDraft фиксирует `based_on_version` = актуальная максимальная версия (история версий — цепочка). 5. Тесты интеграционные против docker `watchbody-pg` — зелёные; сборка решения без ошибок.