# Критерии приёмки Дословно из записи задачи `record-centric-model`. Файл задачи закрытие удалит — критерии обязаны его пережить. 1. Запись, остановленная на шаге, перезапускается снятием признака и продолжает с того рубежа, где стояла. Оракул — тест: шаг останавливает запись на `normalized`, снятие `halted_at` возвращает её в работу, и следующим идёт отправка на распознавание, а не повторная нормализация. 2. У прошедшей конвейер записи ссылки на исходник и на opus ведут на разные существующие копии. Оракул — тест полного прохода: обе ссылки заполнены и обе открываются. 3. Число воркеров задаётся конфигом, и поведение от него не зависит. Оракул — прогон теста конвейера при `N=1` и `N=4`: запись доходит до `done` в обоих; при `N=0` она остаётся в `uploaded` и не теряется. 4. Структура реплик строится из сохранённого ответа провайдера без обращения к нему. Оракул — тест на сохранённом вложении: структура собрана, клиент SpeechKit не позван ни разу. 5. Запись, застрявшая в рубеже дольше предела, останавливается, а откладывание опроса предела не сдвигает. Оракул — тест на подставных часах: сотня откладываний подряд не двигает `state_entered_at` и не обнуляет отсчёт, а по истечении предела запись получает признак остановки с причиной «застряла». ## Приёмочные критерии из ревью дизайна Рубрика прохода `review-rubric`, пункты, не покрытые критериями выше. Приёмка судится по одному списку. 6. Держатель захвата отличим **значением**, а не занятостью записи. Оракул — тест: шаг A держит запись, признак остановки снимает человек, запись захватывает шаг B; запись результата шагом A не проходит, и отправителю от него ничего не уходит. 7. Всякий способ вывести запись из работы сообщает отправителю. Оракул — тест по перечню причин остановки: у каждой отправитель получает сообщение. 8. Повтор шага не создаёт второго приложения. Оракул — тест: шаг завершения оборван после записи текста и повторён с прежнего рубежа; строка расшифровки у записи одна. 9. Штатная остановка сервиса не тратит отказ. Оракул — тест: шаг прерван отменой контекста, число отказов записи прежнее, признака остановки нет. 10. Содержимое записи закрыто во всех коллекциях, где лежит. Оракул — тест: ссылка на сохранённый ответ провайдера без сессии отдаёт отказ, с чужой сессией — тоже. 11. Отказ виден с разрезом по шагу. Оракул — тест: отказ шага приведения растит счётчик с меткой своего рубежа. ## 1. Сущности и рубежи - [x] 1.1 Завести `entity.AudioRecord` с рубежом, временем входа в рубеж, признаком остановки и её причиной, полями очереди и ссылками на приложения - [x] 1.2 Дескриптор рубежа одним объявлением: имя, шаг, чья работа, срок захвата, предел времени, берётся ли в работу. Таблица выбора шага, список отбора захвата и пределы **выводятся** из него, а не перечисляются порознь - [x] 1.3 Развести `MoveToState` и `Postpone`: первый двигает рубеж и время входа в него, второй ставит паузу и снимает захват, рубежа не трогая - [x] 1.4 Заменить `Fail` и `Die` на `Halt(причина, текст)` и `Resume()`; `Resume` сбрасывает отказы, паузу **и время входа в рубеж**, рубеж сохраняет - [x] 1.5 Завести сущности приложения: файл, текст с видом, структура реплик с версией, тема, событие журнала, попытка распознавания - [x] 1.6 Тест: `Postpone` не двигает время входа в рубеж и не сбрасывает отсчёт - [x] 1.7 Тест: `Halt` сохраняет рубеж, `Resume` возвращает на него же и заново ставит время входа **(критерий 1)** ## 2. Шаг схемы - [x] 2.1 Новым файлом шага завести коллекции `audio_records`, `texts`, `structures`, `recognitions`, `record_events`, `topics` - [x] 2.2 Дописать `files` полями формата и длительности - [x] 2.3 Уникальность: тема по паре «владелец и название», текст по паре «запись и вид» (`texts.kind`), структура по паре «запись и версия» - [x] 2.4 Индексы под выборку захвата: рубеж, пауза, срок захвата, признак остановки - [x] 2.5 Правила доступа новых коллекций: просмотр только владельцем связанной записи; поле вложения в `recognitions` помечено защищённым - [x] 2.6 Тот же шаг удаляет прежнюю коллекцию `transcribe_jobs`: данных под ней нет, а пустая коллекция висела бы в панели вторым домом для того же понятия - [x] 2.7 Тест шага: на чистом каталоге поднимаются все коллекции новой модели, и принятая следом запись доходит до конечного рубежа - [x] 2.8 Тест: ссылка на сохранённый ответ без сессии и с чужой сессией даёт отказ **(критерий 10)** - [x] 2.9 Обновить `docs/database.md` тем же изменением: гейт сверяет шаг схемы с правкой этого документа ## 3. Контракты и репозитории - [x] 3.1 `AudioRecognizer` отдаёт доменный результат — реплики со временем, плоский текст, байты на хранение — вместо строки; заливка и отправка разделены - [x] 3.2 Контракты репозиториев: запись, файлы, тексты, структура, попытки распознавания, журнал событий, темы - [x] 3.3 Захват возвращает **идентификатор записи и признак этого захвата**; признак уникален для каждого захвата; `acquireColumns` и `acquiredRow` уходят, срок протухания пишется числом при захвате - [x] 3.4 Запрос захвата: отбор по рубежам из дескриптора, паузе, сроку захвата и отсутствию признака остановки, порядок по времени заведения и ключу - [x] 3.5 Запись результата условна по признаку **этого** захвата, а не по занятости; владелец, заголовок, краткое описание и темы шагом не переписываются - [x] 3.6 Перенацелить сканеры `internal/archrules` с колонок захвата на перечень рубежей: дескриптор против списка отбора против значений шага схемы - [x] 3.7 Тест: захват отдаёт идентификатор и признак, троим одновременным достаётся одному - [x] 3.8 Тест: остановленная запись захвату не выдаётся - [x] 3.9 Тест: шаг A теряет захват после снятия остановки человеком и записи не проводит **(критерий 6)** ## 4. Адаптер распознавания - [x] 4.1 Разбор потока `GetRecognition` в реплики со временем внутри адаптера - [x] 4.2 Раздельные заливка в Object Storage и отправка операции; строка попытки заводится до обращения к провайдеру - [x] 4.3 Заливка проверяет объект нужного размера и не повторяется; отправка не повторяется при заведённом идентификаторе операции - [x] 4.4 Адаптер отдаёт сырые байты ответа на хранение вложением - [x] 4.5 Тест: структура собрана из сохранённого вложения, клиент SpeechKit не позван ни разу **(критерий 4)** - [x] 4.6 Тест: объект на месте — заливка не повторяется; идентификатор операции на месте — отправка не повторяется ## 5. Конвейер - [x] 5.1 Выбор шага по рубежу из дескриптора; шаги перестают быть привязаны к воркеру - [x] 5.2 Шаг приведения: две отдельные ссылки на файлы вместо одной переставляемой - [x] 5.3 Шаг отправки: строка попытки распознавания, идентификатор операции и ключ объекта уезжают туда - [x] 5.4 Шаг опроса зовёт `Postpone`, а не переход в то же состояние - [x] 5.5 Шаг завершения: текст строкой `texts`, структура строкой `structures`, сырой ответ вложением; повтор не заводит второго комплекта - [x] 5.6 Предел времени в рубеже из дескриптора: остановка с причиной «застряла», идентификатор операции при этом сохраняется - [x] 5.7 Единое место ответа отправителю на всякую остановку, независимо от причины - [x] 5.8 Отмена по остановке сервиса возвращает число отказов назад и приговора не выносит - [x] 5.9 Журнал событий пишется на смену рубежа, на остановку и на снятие остановки; на откладывание — нет; колонка текста отказа зовётся `outcome_text`, чтобы `error_text` осталось именем одной колонки - [x] 5.10 Тест полного прохода: обе ссылки на файлы заполнены и обе открываются **(критерий 2)** - [x] 5.11 Тест: остановка на `normalized`, снятие признака, следующим идёт отправка **(критерий 1)** - [x] 5.12 Тест на подставных часах: сотня откладываний не двигает `state_entered_at`, по истечении предела запись останавливается с причиной «застряла» **(критерий 5)** - [x] 5.13 Тест по перечню причин остановки: у каждой отправитель получает сообщение **(критерий 7)** - [x] 5.14 Тест: повтор шага завершения не заводит второй расшифровки **(критерий 8)** - [x] 5.15 Тест: отмена контекста не тратит отказ **(критерий 9)** ## 6. Воркеры, метрики и настройки - [x] 6.1 Пул одинаковых воркеров вместо трёх именованных в `main.go` - [x] 6.2 Метка счётчика `transcriber_worker_job_count` — рубеж, а не имя потока - [x] 6.3 Число воркеров и два предела времени — в `internal/config` и `config.example.toml` с умолчаниями; имена ключей названы в дизайне - [x] 6.4 `N = 0` поднимает сервис без движения записей - [x] 6.5 Тест конвейера при `N=1` и `N=4` — запись доходит до `done`; при `N=0` остаётся в `uploaded` **(критерий 3)** - [x] 6.6 Тест: отказ шага приведения растит счётчик с меткой своего рубежа **(критерий 11)** ## 7. Поверхность и панель - [x] 7.1 Ответ приёма отдаёт рубеж `uploaded`; ответ опроса — рубеж из нового перечня плюс поле `halted` - [x] 7.2 Текст расшифровки в ответе опроса читается из `texts` по **виду** «сырая расшифровка», а не по последней записи - [x] 7.3 Правила панели: снятие признака остановки и правка рубежа очищают захват, срок, паузу и отказы и заново ставят время входа в рубеж - [x] 7.4 Запрет удаления учётной записи считает `audio_records` и `files` - [x] 7.5 Тесты транспорта под новые значения `status` и поле `halted` ## 8. Документы и гейт - [x] 8.1 Инвариант `CLAUDE.md` о колонках очереди: перечень колонок съёживается, предмет правила переезжает на перечень рубежей - [x] 8.2 Инвариант `CLAUDE.md` о держателе захвата: держатель отличим значением признака захвата, а не занятостью записи - [x] 8.3 `docs/architecture.md`: компоненты, цепочка рубежей, единые точки, таблица внешних зависимостей, раздел «Эксплуатация» — чем владелец теперь замечает отказ - [x] 8.4 `docs/database.md`: коллекции, представление данных, настройки с числовым значением (число воркеров, два предела времени, сроки захвата) - [x] 8.5 `docs/review.md`: снять ложноположительное «гонка захвата по построению невозможна» — построение снято пулом одинаковых воркеров - [x] 8.6 Разделы `Purpose` спек `pipeline` и `storage` при архивации: цепочка рубежей перестала быть «сознательно не описанной», а строка про непереносимые прежние данные — верной - [x] 8.7 `task gate` зелёный целиком - [x] 8.8 Поведенческая проверка живым запуском: подъём с `telegram.enabled = false` и подставным распознавателем, запись доходит до `done`