Files
transcriber/openspec/changes/archive/2026-08-14-record-centric-model/tasks.md
T
av 1576d06735 внутренняя модель перестроена вокруг аудиозаписи
- audiorecords вместо transcribe_jobs: приложения (texts, structures,
  recognitions, record_events, topics) живут своими коллекциями, ссылки на
  исходник и на приведённую копию перестали переставляться
- рубеж называет достигнутое, отказ стал признаком остановки с причиной, а
  сторожей стало двое: число отказов и время в рубеже
- воркеры потеряли специализацию, их число задаётся [pipeline] workers, шаг
  выбирается по рубежу, а захват отдаёт идентификатор и признак захвата
2026-08-14 20:20:33 +03:00

17 KiB

Критерии приёмки

Дословно из записи задачи 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, пункты, не покрытые критериями выше. Приёмка судится по одному списку.

  1. Держатель захвата отличим значением, а не занятостью записи. Оракул — тест: шаг A держит запись, признак остановки снимает человек, запись захватывает шаг B; запись результата шагом A не проходит, и отправителю от него ничего не уходит.
  2. Всякий способ вывести запись из работы сообщает отправителю. Оракул — тест по перечню причин остановки: у каждой отправитель получает сообщение.
  3. Повтор шага не создаёт второго приложения. Оракул — тест: шаг завершения оборван после записи текста и повторён с прежнего рубежа; строка расшифровки у записи одна.
  4. Штатная остановка сервиса не тратит отказ. Оракул — тест: шаг прерван отменой контекста, число отказов записи прежнее, признака остановки нет.
  5. Содержимое записи закрыто во всех коллекциях, где лежит. Оракул — тест: ссылка на сохранённый ответ провайдера без сессии отдаёт отказ, с чужой сессией — тоже.
  6. Отказ виден с разрезом по шагу. Оракул — тест: отказ шага приведения растит счётчик с меткой своего рубежа.

1. Сущности и рубежи

  • 1.1 Завести entity.AudioRecord с рубежом, временем входа в рубеж, признаком остановки и её причиной, полями очереди и ссылками на приложения
  • 1.2 Дескриптор рубежа одним объявлением: имя, шаг, чья работа, срок захвата, предел времени, берётся ли в работу. Таблица выбора шага, список отбора захвата и пределы выводятся из него, а не перечисляются порознь
  • 1.3 Развести MoveToState и Postpone: первый двигает рубеж и время входа в него, второй ставит паузу и снимает захват, рубежа не трогая
  • 1.4 Заменить Fail и Die на Halt(причина, текст) и Resume(); Resume сбрасывает отказы, паузу и время входа в рубеж, рубеж сохраняет
  • 1.5 Завести сущности приложения: файл, текст с видом, структура реплик с версией, тема, событие журнала, попытка распознавания
  • 1.6 Тест: Postpone не двигает время входа в рубеж и не сбрасывает отсчёт
  • 1.7 Тест: Halt сохраняет рубеж, Resume возвращает на него же и заново ставит время входа (критерий 1)

2. Шаг схемы

  • 2.1 Новым файлом шага завести коллекции audio_records, texts, structures, recognitions, record_events, topics
  • 2.2 Дописать files полями формата и длительности
  • 2.3 Уникальность: тема по паре «владелец и название», текст по паре «запись и вид» (texts.kind), структура по паре «запись и версия»
  • 2.4 Индексы под выборку захвата: рубеж, пауза, срок захвата, признак остановки
  • 2.5 Правила доступа новых коллекций: просмотр только владельцем связанной записи; поле вложения в recognitions помечено защищённым
  • 2.6 Тот же шаг удаляет прежнюю коллекцию transcribe_jobs: данных под ней нет, а пустая коллекция висела бы в панели вторым домом для того же понятия
  • 2.7 Тест шага: на чистом каталоге поднимаются все коллекции новой модели, и принятая следом запись доходит до конечного рубежа
  • 2.8 Тест: ссылка на сохранённый ответ без сессии и с чужой сессией даёт отказ (критерий 10)
  • 2.9 Обновить docs/database.md тем же изменением: гейт сверяет шаг схемы с правкой этого документа

3. Контракты и репозитории

  • 3.1 AudioRecognizer отдаёт доменный результат — реплики со временем, плоский текст, байты на хранение — вместо строки; заливка и отправка разделены
  • 3.2 Контракты репозиториев: запись, файлы, тексты, структура, попытки распознавания, журнал событий, темы
  • 3.3 Захват возвращает идентификатор записи и признак этого захвата; признак уникален для каждого захвата; acquireColumns и acquiredRow уходят, срок протухания пишется числом при захвате
  • 3.4 Запрос захвата: отбор по рубежам из дескриптора, паузе, сроку захвата и отсутствию признака остановки, порядок по времени заведения и ключу
  • 3.5 Запись результата условна по признаку этого захвата, а не по занятости; владелец, заголовок, краткое описание и темы шагом не переписываются
  • 3.6 Перенацелить сканеры internal/archrules с колонок захвата на перечень рубежей: дескриптор против списка отбора против значений шага схемы
  • 3.7 Тест: захват отдаёт идентификатор и признак, троим одновременным достаётся одному
  • 3.8 Тест: остановленная запись захвату не выдаётся
  • 3.9 Тест: шаг A теряет захват после снятия остановки человеком и записи не проводит (критерий 6)

4. Адаптер распознавания

  • 4.1 Разбор потока GetRecognition в реплики со временем внутри адаптера
  • 4.2 Раздельные заливка в Object Storage и отправка операции; строка попытки заводится до обращения к провайдеру
  • 4.3 Заливка проверяет объект нужного размера и не повторяется; отправка не повторяется при заведённом идентификаторе операции
  • 4.4 Адаптер отдаёт сырые байты ответа на хранение вложением
  • 4.5 Тест: структура собрана из сохранённого вложения, клиент SpeechKit не позван ни разу (критерий 4)
  • 4.6 Тест: объект на месте — заливка не повторяется; идентификатор операции на месте — отправка не повторяется

5. Конвейер

  • 5.1 Выбор шага по рубежу из дескриптора; шаги перестают быть привязаны к воркеру
  • 5.2 Шаг приведения: две отдельные ссылки на файлы вместо одной переставляемой
  • 5.3 Шаг отправки: строка попытки распознавания, идентификатор операции и ключ объекта уезжают туда
  • 5.4 Шаг опроса зовёт Postpone, а не переход в то же состояние
  • 5.5 Шаг завершения: текст строкой texts, структура строкой structures, сырой ответ вложением; повтор не заводит второго комплекта
  • 5.6 Предел времени в рубеже из дескриптора: остановка с причиной «застряла», идентификатор операции при этом сохраняется
  • 5.7 Единое место ответа отправителю на всякую остановку, независимо от причины
  • 5.8 Отмена по остановке сервиса возвращает число отказов назад и приговора не выносит
  • 5.9 Журнал событий пишется на смену рубежа, на остановку и на снятие остановки; на откладывание — нет; колонка текста отказа зовётся outcome_text, чтобы error_text осталось именем одной колонки
  • 5.10 Тест полного прохода: обе ссылки на файлы заполнены и обе открываются (критерий 2)
  • 5.11 Тест: остановка на normalized, снятие признака, следующим идёт отправка (критерий 1)
  • 5.12 Тест на подставных часах: сотня откладываний не двигает state_entered_at, по истечении предела запись останавливается с причиной «застряла» (критерий 5)
  • 5.13 Тест по перечню причин остановки: у каждой отправитель получает сообщение (критерий 7)
  • 5.14 Тест: повтор шага завершения не заводит второй расшифровки (критерий 8)
  • 5.15 Тест: отмена контекста не тратит отказ (критерий 9)

6. Воркеры, метрики и настройки

  • 6.1 Пул одинаковых воркеров вместо трёх именованных в main.go
  • 6.2 Метка счётчика transcriber_worker_job_count — рубеж, а не имя потока
  • 6.3 Число воркеров и два предела времени — в internal/config и config.example.toml с умолчаниями; имена ключей названы в дизайне
  • 6.4 N = 0 поднимает сервис без движения записей
  • 6.5 Тест конвейера при N=1 и N=4 — запись доходит до done; при N=0 остаётся в uploaded (критерий 3)
  • 6.6 Тест: отказ шага приведения растит счётчик с меткой своего рубежа (критерий 11)

7. Поверхность и панель

  • 7.1 Ответ приёма отдаёт рубеж uploaded; ответ опроса — рубеж из нового перечня плюс поле halted
  • 7.2 Текст расшифровки в ответе опроса читается из texts по виду «сырая расшифровка», а не по последней записи
  • 7.3 Правила панели: снятие признака остановки и правка рубежа очищают захват, срок, паузу и отказы и заново ставят время входа в рубеж
  • 7.4 Запрет удаления учётной записи считает audio_records и files
  • 7.5 Тесты транспорта под новые значения status и поле halted

8. Документы и гейт

  • 8.1 Инвариант CLAUDE.md о колонках очереди: перечень колонок съёживается, предмет правила переезжает на перечень рубежей
  • 8.2 Инвариант CLAUDE.md о держателе захвата: держатель отличим значением признака захвата, а не занятостью записи
  • 8.3 docs/architecture.md: компоненты, цепочка рубежей, единые точки, таблица внешних зависимостей, раздел «Эксплуатация» — чем владелец теперь замечает отказ
  • 8.4 docs/database.md: коллекции, представление данных, настройки с числовым значением (число воркеров, два предела времени, сроки захвата)
  • 8.5 docs/review.md: снять ложноположительное «гонка захвата по построению невозможна» — построение снято пулом одинаковых воркеров
  • 8.6 Разделы Purpose спек pipeline и storage при архивации: цепочка рубежей перестала быть «сознательно не описанной», а строка про непереносимые прежние данные — верной
  • 8.7 task gate зелёный целиком
  • 8.8 Поведенческая проверка живым запуском: подъём с telegram.enabled = false и подставным распознавателем, запись доходит до done