Files
transcriber/openspec/changes/archive/2026-08-15-remove-telegram-intake/specs/storage/spec.md
T
av 8f7c3a057a удалён вход Telegram, владелец записи стал обязателен в схеме
- убраны клиент бота, транспорт обновлений, отправитель сообщений, сборка
  входа при старте, секция настроек и зависимость go-telegram-bot-api; из
  конвейера ушла доставка ответа отправителю — исход виден опросом готовности.
  Колонки адресата и значение источника остались в схеме: применённые шаги не
  переписываются
- шаг 202608140003 запрещает пустого владельца у аудиозаписи и у файла;
  существующие строки он не проверяет, и это принято сознательно — искать их
  надо запросом до выкладки
- ревью нашло два пред-существующих дефекта, оба закрыты: пустой второй ответ
  распознавателя стирал сохранённую расшифровку, а пустая расшифровка перестала
  быть заметной вместе с убранной доставкой. Попутно поднят golang.org/x/image
  до v0.45.0 — красный шаг vulns, воспроизводился и на чистом master
2026-08-15 07:24:35 +03:00

13 KiB

ADDED Requirements

Requirement: Пустой результат не кладётся поверх сохранённого

Хранилище SHALL не заменять сохранённое содержимое приложения записи — текст и структуру реплик — пустым. Замена пустым MUST оставлять прежнее значение и считаться сделанной работой, а не отказом.

Требование стоит на повторном опросе одной и той же операции распознавания. Повтор — обычное дело: держатель захвата умер, сохранение рубежа отказало, человек снял признак остановки в панели. Провайдер при этом вправе ответить пустым потоком, отказом это не считается, и безусловная замена стирала бы расшифровку живого человека — без следа и без возврата, потому что сервис объявлен архивом и удаления по требованию не знает.

Та же защита MUST стоять у сырого ответа провайдера: разное правило у двух хранителей одного результата читается как недосмотр, и один из них молча теряет то, ради чего второй заведён.

Норма записана со стороны хранилища, а не шага: шагов, кладущих текст, больше одного, и правило, записанное у одного из них, у остальных читалось бы как снятое.

Scenario: Пустой второй ответ не стирает расшифровку

  • GIVEN расшифровка записи сохранена
  • WHEN ту же операцию опрашивают снова, и провайдер отвечает пустым
  • THEN сохранённая расшифровка остаётся прежней
  • AND шаг завершается без отказа

MODIFIED Requirements

Requirement: Сервис поднимается на чистом каталоге данных

Сервис SHALL приводить хранилище в рабочий вид сам: на пустом каталоге данных он MUST завести свою схему и принимать записи своим входом — приёмом по HTTP — без единого ручного шага до первого запуска.

Прежние данные не переносятся. Каталог, оставшийся от прежней раскладки, MUST не читаться и не считаться источником: сервис начинает с чистого листа, и это решение задачи, а не следствие отказа.

Схема MUST заводиться версионированными шагами, а применённый шаг MUST не переписываться — только новым шагом. Иначе повторный запуск на уже заведённом каталоге разошёлся бы с первым молча.

Каталог данных у сервиса MUST быть один: база и файлы записей лежат под ним вместе, и второго пути к ним не заводится.

Scenario: Первый запуск на пустом каталоге

  • GIVEN каталог данных пуст
  • WHEN сервис запускается
  • THEN он заводит своё хранилище и продолжает работу
  • AND принятая следом запись доходит до состояния done

Scenario: Повторный запуск на заведённом каталоге

  • GIVEN сервис уже запускался на этом каталоге и завёл хранилище
  • WHEN он запускается снова
  • THEN он не заводит схему второй раз и не теряет прежние записи

Requirement: Пароль владельца от панели не лежит в конфигурации

Сервис SHALL не заводить в конфигурации ключа под пароль владельца от панели. Пароль MUST задаваться самим владельцем, а хранилище MUST держать только его отпечаток.

Требование стоит на инварианте проекта «Секрет не покидает конфиг» с другой стороны: секрет, которого в конфигурации нет, не утекает вместе с ней и не уезжает в выкладку третьим путём. Пароль от панели открывает все записи и все файлы разом — это самое чувствительное, что есть у сервиса.

Приглашение завести владельца сервис MUST печатать только пока владельца нет, и оно MUST истекать по времени. Приглашение равносильно паролю от панели, а печатается оно в журнал контейнера, откуда строку не убрать: бессрочное отдало бы панель всякому читателю логов навсегда.

Пока владелец пароля не задал, сервис MUST принимать записи: панель без владельца приёму не мешает.

Scenario: Владелец пароля ещё не задал

  • GIVEN каталог данных пуст и владелец панели не заведён
  • WHEN сервис запускается
  • THEN он принимает записи
  • AND ни один ключ конфигурации не несёт пароля от панели

Scenario: Владелец заведён, приглашение больше не печатается

  • GIVEN владелец панели заведён
  • WHEN сервис запускается снова
  • THEN приглашения завести владельца в журнале нет

Requirement: Владелец задачи лежит связью с учётной записью

Хранилище SHALL держать владельца аудиозаписи отдельной колонкой — связью с учётной записью, — и эта колонка MUST не иметь умолчания: запись, чей владелец не назван, не достаётся никому по недосмотру схемы.

Колонка MUST не допускать пустого значения. Прежде допускала, и цену платили за записи, принятые ботом: связи чата с учётной записью сервис не вёл. С убранным входом заводить ничью запись стало некому, и обязательность переезжает из одного лишь приёма в схему — туда, где её держит хранилище, а не договорённость. Разница не косметическая: пока обязательность жила в приёме, ничью запись заводили руками в панели, и она уходила в конвейер, стоила денег на распознавание и не доставалась потом никому.

Владелец MUST не назначаться и не меняться конвейером.

Scenario: Колонка появляется на пустой базе

  • WHEN сервис поднимается на чистом каталоге данных
  • THEN у аудиозаписи есть колонка владельца
  • AND умолчания у неё нет
  • AND пустого значения она не принимает

Scenario: Запись без владельца не сохраняется

  • GIVEN сервис поднят
  • WHEN аудиозапись пытаются сохранить с пустым владельцем — приёмом, конвейером или руками в панели
  • THEN хранилище её не сохраняет

Scenario: Конвейер владельца не назначает

  • GIVEN запись с владельцем прошла шаг конвейера
  • WHEN смотрят её владельца
  • THEN он прежний

Requirement: Файл записи сужается владельцем наравне с задачей

Хранилище SHALL держать владельца и у файла записи — той же связью с учётной записью, — и правило просмотра файлов MUST пускать к файлу только его владельца.

Владелец файла MUST назначаться при приёме, из предъявленной сессии, а колонка файла MUST не допускать пустого значения наравне с колонкой записи. Прежде пустое значение оставалось у файлов, заведённых конвейером для записи без владельца; таких записей больше не заводится, и разное правило у записи и у её файла читалось бы как недосмотр.

Файл, заведённый шагом конвейера, — приведённую копию заводит именно он — MUST получать владельца своей записи. Иного источника владельца у файла нет, и шаг, оставивший его пустым, упрётся в отказ сохранения: запись накопит отказы и остановится признаком на первом же приведении.

Ссылки на файлы у записи две — на принятую копию и на приведённую, — и обе живут до конца, но владелец файла MUST по-прежнему лежать своей колонкой, а не выводиться через запись: файл переживает свою запись, и заведённый шагом до сохранения записи он остаётся с владельцем и без ссылки.

Отказ наступает на переходе по ссылке, а не на выдаче токена файла: токен хранилище выдаёт на предъявителя, а не на файл, и о файле при выдаче не спрашивает вовсе. Требовать отказа при выдаче значит требовать механизма, которого нет, — а проверка, написанная под такое требование, зеленела бы, не касаясь пути, по которому аудио и уходит.

Scenario: Чужой файл не отдаётся

  • GIVEN запись принята одним вошедшим
  • WHEN другой вошедший идёт по ссылке на файл этой записи со своим токеном
  • THEN содержимого он не получает

Scenario: Свой файл отдаётся

  • GIVEN человек принял запись
  • WHEN он идёт по ссылке на файл своей записи со своим токеном
  • THEN содержимое отдаётся

Scenario: Файл без владельца не сохраняется

  • GIVEN сервис поднят
  • WHEN файл записи пытаются сохранить с пустым владельцем
  • THEN хранилище его не сохраняет

Scenario: Приведённая копия получает владельца записи

  • GIVEN запись с владельцем дошла до приведения
  • WHEN шаг заводит приведённую копию файла
  • THEN владельцем копии стоит владелец записи
  • AND шаг завершается без отказа