Files
transcriber/openspec/changes/archive/2026-08-15-remove-telegram-intake/specs/pipeline/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

19 KiB

ADDED Requirements

Requirement: Конвейер ответа отправителю не шлёт

Шаг конвейера SHALL доводить запись до достигнутого рубежа и MUST не обращаться к отправителю вовсе — ни с готовым текстом, ни с сообщением о неудаче. Исход своей записи отправитель узнаёт опросом готовности и в панели владельца; адрес опроса и содержимое ответа нормирует capability intake.

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

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

Записи, которой этот канал недоступен, не бывает: у каждой записи есть владелец, и опрос отдаёт ему её исход. Держится это обязательностью владельца в схеме хранилища — норму держит capability storage.

Scenario: Готовый текст отправителю не уходит

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

Scenario: Остановка видна опросом, а не сообщением

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

MODIFIED Requirements

Requirement: Захват задачи неделим

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

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

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

Одна и та же запись MUST доставаться ровно одному захватившему. Двум вызывающим, пришедшим за работой одновременно, запись MUST достаться одному, а второй MUST получить признак «работы сейчас нет».

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

Порядок выборки MUST быть определён однозначно: сравнения по неуникальному значению для этого мало, и к нему MUST добавляться ключ записи. Иначе порядок обработки невоспроизводим, а проверка, опирающаяся на «следующую» запись, зелена через раз.

Требование стоит на инварианте проекта «Принятая запись не теряется молча»: захват, разделённый на два шага, отдаёт одну запись двум воркерам, и работа одного из них теряется без следа.

Признак «работы нет» этим требованием не переопределяется — его нормирует требование «Пустой прогон воркера — не отказ».

Scenario: За работой пришли трое разом

  • GIVEN к работе пригодна ровно одна запись
  • WHEN три захвата идут одновременно
  • THEN запись получает ровно один из них
  • AND двое остальных получают признак «работы сейчас нет»

Scenario: Захваченная запись не выдаётся второй раз

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

Scenario: Захват отдаёт идентификатор и свой признак

  • GIVEN к работе пригодна запись
  • WHEN воркер её захватывает
  • THEN захват возвращает идентификатор записи и признак этого захвата
  • AND колонки записи шаг читает отдельным чтением

Scenario: Признак перевыданного захвата отличается от прежнего

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

Requirement: Результат пишет только держатель захвата

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

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

Довод про два ответа отправителю из требования ушёл вместе с доставкой: обращений наружу шаг не делает. Требование от этого не ослабло — порча записи двумя пишущими остаётся его предметом целиком.

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

Scenario: Правка владельца пережила сохранение шага

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

Scenario: Захват ушёл под работающим шагом

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

Scenario: Человек снял остановку под работающим шагом

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

Requirement: Число отказов ограничивает повторы шага

У аудиозаписи SHALL быть число отказов. Оно MUST расти при каждом захвате и MUST возвращаться к нулю, когда шаг завершился без отказа либо отложил работу. Рост при захвате, а не при отказе, засчитывает попытку и записи, брошенной на середине: шаг, уносящий с собой процесс, до объявления отказа не доходит никогда.

Остановка сервиса отказом не считается. Шаг, прерванный отменой по собственной остановке сервиса, MUST возвращать число отказов назад и MUST не выносить записи приговора: запись не виновата в том, что нас перезапустили, и несколько выкладок подряд иначе останавливают здоровую многочасовую запись с приговором «отказы исчерпаны». Всякая другая причина, по которой шаг не дошёл до объявления исхода, отказ тратит.

Запись, захваченная с числом отказов сверх заданного предела, MUST останавливаться признаком тем, кто её захватил, и MUST не отдаваться шагу в работу. Остановка эта видна отправителю опросом готовности наравне с прочими — норму держит capability intake.

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

Scenario: Запись отказывает на каждой попытке

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

Scenario: Шаг уносит процесс, не объявив отказа

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

Scenario: Остановка сервиса отказа не тратит

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

Scenario: Прошедшая запись отказов не копит

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

Requirement: Выборка воркера владельцем не сужается

Воркер SHALL брать записи всех владельцев подряд и MUST не учитывать владельца при выборе очередной записи.

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

Оговорка про записи без владельца из требования ушла: заводить их стало нечем — колонка владельца пустого значения не принимает, и норму держит capability storage.

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

Scenario: Записи двух владельцев проходят одним воркером

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

Scenario: Шаг конвейера владельца не затирает

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

REMOVED Requirements

Requirement: Недоставленный ответ не роняет шаг

Reason: Доставка ответа отправителю убрана вместе с входом Telegram, и недоставке взяться неоткуда: обращения наружу шаг больше не делает. Обе прежние причины недоставки — неподнятый вход отправителя и неназванный адресат записи — описывали именно этот вход.

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

Requirement: Всякая остановка сообщает отправителю

Reason: Обязанность сообщить требовала адресата, а адресатом был чат Telegram. С убранным входом сообщать стало нечем и некуда, и обязанность переходит к опросу готовности — её держит требование «Конвейер ответа отправителю не шлёт» вместе с capability intake.

Migration: Отправитель узнаёт об остановке признаком в ответе опроса готовности. Владелец сервиса видит остановку записью журнала и полем причины у самой записи — как и прежде.