# 08 — Требования к схеме БД: ORCH-088 (Serial gate, freeze-хранилище) Work Item: **ORCH-088** · Repo: **orchestrator** · Стадия: architecture Связь: ADR `06-adr/ADR-001-serial-gate.md` (D2/D3/D4), ТЗ `02-trz.md` §5. > Общая прод-БД (self-hosting обслуживает enduro-trails из того же инстанса). Все миграции — > **только аддитивные и идемпотентные** (`CREATE TABLE IF NOT EXISTS` / `_ensure_column`). Изменение > существующих таблиц-контрактов (`tasks`, `jobs`, `job_deps`, `agent_runs`) запрещено. --- ## 1. Новая таблица `repo_freeze` (FR-5) Durable per-repo признак заморозки пакета после post-deploy `DEGRADED`/rollback. Append-only журнал: активная заморозка ⇔ существует строка репо с `cleared_at IS NULL`. ```sql CREATE TABLE IF NOT EXISTS repo_freeze ( id INTEGER PRIMARY KEY AUTOINCREMENT, repo TEXT NOT NULL, -- ключ области (per-repo) frozen_at TEXT NOT NULL DEFAULT (datetime('now')), reason TEXT, -- напр. "post-deploy DEGRADED 3/5" work_item_id TEXT, -- задача-источник деградации (уже stage='done') cleared_at TEXT -- NULL = freeze активен; снят оператором → datetime ); CREATE INDEX IF NOT EXISTS idx_repo_freeze_active ON repo_freeze (repo, cleared_at); ``` ### Семантика - **Активный freeze репо `R`:** `EXISTS (SELECT 1 FROM repo_freeze WHERE repo=R AND cleared_at IS NULL)`. - **Выставление** (`set_repo_freeze`): INSERT новой строки (`cleared_at=NULL`). Повторный DEGRADED при уже активном freeze — допускается доп. строка (журнал) либо no-op при существующей активной (выбор разработчика; на gate не влияет — `EXISTS` идемпотентен). - **Снятие** (`clear_repo_freeze`): `UPDATE repo_freeze SET cleared_at=datetime('now') WHERE repo=? AND cleared_at IS NULL` (закрывает все активные строки репо). Идемпотентно (повтор → 0 rows). - **Read (gate/snapshot):** только `cleared_at IS NULL`-строки; `is_repo_frozen` — **fail-closed** (ошибка чтения → `True`, AC-9). ### Использование в горячем claim `db.claim_next_job` читает `repo_freeze` инлайн внутри `serial_gate`-фрагмента (только локальная БД, offline — NFR-2): ``` OR EXISTS (SELECT 1 FROM repo_freeze f WHERE f.repo = jobs.repo AND f.cleared_at IS NULL) ``` (внутри `AND NOT ( jobs.agent='analyst' AND … )` — см. ADR D1). Тотальный сбой построения фрагмента → fail-open для claim (AC-8); реально выставленная строка блокирует через сам SQL. --- ## 2. Активная задача репо — без новых колонок «Репо занят» вычисляется из существующих столбцов `tasks(repo, stage)`: ```sql EXISTS (SELECT 1 FROM tasks WHERE repo=? AND id != ? AND stage != 'done') ``` Новых колонок/таблиц для «активной задачи» и «очереди ожидания» **не вводится**: ожидание = существующий `jobs.status='queued'` analyst-job + gate в claim (ТЗ §5). --- ## 3. Идемпотентность и restart-safety - Миграция `repo_freeze` выполняется в общем init-пути схемы (`db.init_db`/`_ensure_*`), безопасна к повторному запуску (`IF NOT EXISTS`). - Всё состояние gate/freeze — в БД (нет in-memory) ⇒ после рестарта поведение идентично (NFR-3/AC-3): активная задача определяется из `tasks`, freeze — из `repo_freeze`, ожидающая задача — `queued` job. - При выключенных флагах (`serial_gate_enabled=False` / `serial_gate_freeze_enabled=False`) таблица инертна; enduro и существующие контракты не затрагиваются (NFR-4/AC-11). --- ## 4. Неизменяемые контракты `tasks`, `jobs`, `job_deps`, `agent_runs`, `tracker_messages` — схема **без изменений**. `STAGE_TRANSITIONS` / `QG_CHECKS` — не БД, но также не меняются (NFR-5).