# Спецификация: жизненный цикл программы (EnsureProgram, CreateDraft, Activate, Archive) > Сервис `ProgramStore` (`Server.WatchBody.Core\Programs\`); работа — > через репозитории m01. Ошибки — `ProgramStoreException(message)`. ## 1. `EnsureProgram(int athleteId, string title)` → `Program` Создаёт программу атлета при отсутствии (UNIQUE(athlete_id) — одна на атлета), иначе возвращает существующую. `title` — только при создании. | # | Тест | Ожидание | |---|---|---| | 1 | первый вызов | программа создана (title как задан) | | 2 | повторный вызов с другим title | возвращена та же программа (id не меняется, title не перезаписывается) | ## 2. `CreateDraft(int athleteId, string body, string author, string rationale, int? basedOnVersion = null)` → `ProgramVersion` Новая draft-версия. `basedOnVersion = null` → явное значение = актуальная максимальная версия (цепочка истории); число валидируется против существующих. `author` ∈ `user | admin | rules` (`llm` — v2, иначе ошибка). `body` — непустой (не валидируется по схеме, v1). Тело после создания не меняется (🛡); `version = max+1` считает БД. | # | Тест | Ожидание | |---|---|---| | 3 | первая версия атлета без программы | созданы `program` и версия 1 (draft, based_on = NULL) | | 4 | вторая версия | version = 2, based_on_version = 1 | | 5 | author = "llm" | ошибка (v2) | | 6 | author = "rules" | создана (для m05) | | 7 | basedOnVersion = 5 при max = 2 | ошибка (цепочка нарушена) | | 8 | пустой body | ошибка | | 9 | два draft подряд | version 3 и 4, обе draft (активация — отдельно) | ## 3. `Activate(int athleteId, Guid versionId)` → `void` `draft → active`; прежняя активная версия программы (если была) → `archived` (БД гарантирует ровно одну). Не-draft версия → ошибка; чужая версия атлета → ошибка (изоляция). | # | Тест | Ожидание | |---|---|---| | 10 | активация draft при активной v1 | v1 → archived, v2 → active; `GetActive` = v2 | | 11 | активация первой версии (активной не было) | единственная active | | 12 | активация archived-версии | ошибка (🛡 переходы только вперёд) | | 13 | повторная активация уже активной | ошибка (не draft) | | 14 | активация чужой версии | ошибка | ## 4. `Archive(int athleteId, Guid versionId)` → `void` `active → archived` (например, атлет хочет остаться без активной программы). Не-active → ошибка. | # | Тест | Ожидание | |---|---|---| | 15 | архивация активной | archived, `GetActive` → null | | 16 | архивация draft | ошибка |