## 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 нести время остановки, причину и машинный текст отказа. Снятие признака SHALL возвращать запись в работу **с того рубежа, где она стояла**, и MUST сбрасывать **всё, чем прошлый прогон её удерживал**: - признак остановки — время остановки, причину и машинный текст отказа; - признак захвата и срок его протухания; - число отказов; - паузу перед повтором; - время входа в рубеж. Перечень назван целиком и в одном месте, потому что забытое поле не даёт ни отказа, ни строки в журнале. Оставленный признак захвата держит запись занятой до протухания срока и отдаёт её потом чужому шагу — тому, чей результат условен по прежнему значению. Оставленное время входа в рубеж останавливает запись снова первым же захватом, если остановленной она простояла дольше предела, и перезапуск не работает вовсе. Оставленные отказы и пауза откладывают первый же прогон на накопленный срок. Возврат в работу MUST идти **через домен**: тот, кто его делает, называет запись, а поля выше сбрасывает домен одним действием. Правка колонок мимо домена повторяет перечень вторым местом, и второе место расходится с первым молча. Возврат в работу MUST писать событие журнала записи с происхождением «человек». Иначе запись, побывавшая остановленной и вернувшаяся в работу, неотличима в журнале от записи, которую конвейер вёл без остановок, а происхождение события перестаёт различать что-либо: другого писателя, кроме конвейера, у журнала не остаётся. Инструментом возврата сегодня служит подкоманда набора инструментов разработчика: панели у сервиса нет, а экраны владельца приносят отдельные задачи. Норма написана про поля и про домен, а не про инструмент, и появление экрана её не трогает. Прежние состояния отказа и смерти MUST не заводиться заново: обе причины восстанавливаются одинаково — снятием признака, — и различие между ними перестаёт быть структурным, оставаясь причиной остановки. Состояние, называющее отказ, стирает достигнутый рубеж, и продолжение с места остановки становится невозможным. **Способ вывести запись из выборки MUST быть один — этот признак.** Второго признака, исключающего запись из работы помимо рубежа и паузы, MUST не заводиться: два способа расходятся, и молчаливо теряется тот, который забыли проверить. Условие отбора MUST не выводить запись из выборки молча — запись, переставшая браться в работу, обязана нести признак остановки с причиной. Остановку MUST ставить тот, кто запись захватил. Перевод принадлежит одному месту: условие отбора, молча пропускающее запись мимо выборки, оставило бы её без следа. Остановленная запись MUST не выдаваться захвату. #### Scenario: Остановленная запись продолжает с места остановки - **GIVEN** шаг остановил запись на рубеже приведения - **WHEN** признак остановки снимают - **THEN** следующим идёт отправка на распознавание, а не повторное приведение #### Scenario: Остановленная запись не выдаётся захвату - **GIVEN** у записи стоит признак остановки - **WHEN** за её рубежом приходит захват - **THEN** запись ему не выдаётся #### Scenario: Снятие признака сбрасывает всех сторожей - **GIVEN** запись остановлена с накопленными отказами и паузой - **AND** остановленной она простояла дольше предела времени в рубеже - **WHEN** признак остановки снимают - **THEN** число отказов, пауза и время входа в рубеж сброшены - **AND** ближайший захват выдаёт запись, а не останавливает её снова #### Scenario: Возврат в работу освобождает захват - **GIVEN** запись остановлена, и признак прошлого захвата на ней стоит - **WHEN** признак остановки снимают - **THEN** признака захвата и срока его протухания на записи нет - **AND** ближайший захват выдаёт запись, не дожидаясь протухания прежнего срока #### Scenario: Возврат в работу виден в журнале записи - **GIVEN** запись остановлена - **WHEN** её возвращают в работу - **THEN** в журнале событий записи есть событие с происхождением «человек» ### Requirement: Конвейер ответа отправителю не шлёт Шаг конвейера SHALL доводить запись до достигнутого рубежа и MUST не обращаться к отправителю вовсе — ни с готовым текстом, ни с сообщением о неудаче. Исход своей записи владелец узнаёт **карточкой записи**; адрес карточки и содержимое ответа нормирует capability `archive`. Владелец сервиса узнаёт исход журналом и журналом событий записи. Панели, где он видел бы то же строкой таблицы, у сервиса нет — она ушла вместе со встроенным хранилищем, и второго канала наблюдения это не отняло: журнал событий пишется по-прежнему, а читается запросом к базе, пока экрана нет. Держатель нормы сменился вместе с убранным опросом готовности: прежде исход отдавал адрес опроса, нормированный capability `intake`, и адреса этого больше нет. Обязанность при этом не изменилась — изменилось только то, каким адресом она исполняется. Требование заведено взамен доставки в чат, убранной вместе с входом Telegram. Без него молчание конвейера читалось бы как недоделка: прежде ответ уходил, и всякий, кто помнит это, ищет в шаге отправку, а её отсутствие принимает за потерянную ветку. Инвариант проекта «Принятая запись не теряется молча» держится теперь карточкой записи — там остановка видна признаком и причиной — и журналом владельца, где у неё стоит причина. Обязанность при этом сменила направление: прежде об отказе сообщали, теперь отказ доступен спросившему. Отправитель, который не спрашивает, об остановке не узнаёт. Записи, которой этот канал недоступен, не бывает: у каждой записи есть владелец, и карточка отдаёт ему её исход. Держится это обязательностью владельца в схеме — норму держит capability `storage`. #### Scenario: Готовый текст отправителю не уходит - **GIVEN** запись дошла до конечного рубежа - **WHEN** шаг конвейера её завершает - **THEN** ни одного обращения наружу с текстом расшифровки не уходит - **AND** текст достаётся отдельным адресом текста записи #### Scenario: Остановка видна карточкой, а не сообщением - **GIVEN** запись остановлена по исчерпании отказов - **WHEN** владелец записи спрашивает её карточку - **THEN** ответ несёт достигнутый рубеж, признак остановки и её причину - **AND** в журнале владельца сервиса есть запись об остановке с причиной