## 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**: Отправитель узнаёт об остановке признаком в ответе опроса готовности. Владелец сервиса видит остановку записью журнала и полем причины у самой записи — как и прежде.