Andrey Epifancev 9b4ea0a3c9 docs(openspec): archive core-recording-lifecycle, promote specs (#1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKHeGuAYh4W1jNdjMConZa
2026-09-27 19:26:25 +04:00

13 KiB
Raw Blame History

recording-lifecycle Specification

Purpose

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

Requirements

Requirement: Состояния записи

Запись MUST находиться ровно в одном из состояний: created (идёт запись, данные только в памяти), saved (аудио и заметка сохранены, ожидает отправки), sent (запрос в полёте), transcribed (текст получен, не записан; не сохраняется), retry (ожидает повтора после временной ошибки), failed (требуется ручное действие), inserted (финал). Во frontmatter заметки MUST сохраняться только saved, sent, retry, failed, inserted. Переход, не описанный в этой спецификации, MUST отклоняться ошибкой без изменения записи.

Scenario: Недопустимый переход отклонён

  • WHEN к записи в состоянии inserted применяется событие «отправка начата»
  • THEN операция завершается ошибкой, запись не изменена

Scenario: Транзитное состояние не попадает в заметку

  • WHEN получен текст транскрипта
  • THEN заметка за одну запись переходит из sent сразу в inserted, значение transcribed во frontmatter не появляется

Requirement: Сохранение захваченного аудио

По окончании записи плагин MUST сохранить аудио в voice/source/ и создать заметку в voice/inbox/ в состоянии saved до любой сетевой операции. Если захвачено 0 байт, MUST NOT создаваться ни аудиофайл, ни заметка. Если запись была прервана (событие остановки не от пользователя), сохраняется всё накопленное, а запись помечается как прерванная.

Scenario: Обычная запись

  • WHEN пользователь остановил запись длиной 0:47 с непустыми данными в формате audio/mp4
  • THEN в voice/source/ появился файл <id>.m4a, в voice/inbox/ — заметка со статусом saved, и это произошло без сетевых запросов

Scenario: Прерванная запись

  • WHEN запись прервалась системой после 0:47 с непустыми данными
  • THEN аудио и заметка сохранены, заметка помечена как прерванная с длительностью 0:47, и запись участвует в очереди как обычная

Scenario: Пустая запись

  • WHEN запись завершилась (штатно или обрывом) с 0 байт данных
  • THEN никаких файлов не создано

Requirement: Отправка и результат

Перед отправкой запись MUST перейти в sent с отметкой времени начала отправки, записанной в заметку. Очередь MUST обрабатывать записи строго по одной. Результат провайдера — успех с текстом, временная ошибка или постоянная ошибка. Успех с непустым текстом MUST переводить запись в inserted с текстом в заметке. Успех с пустым (после обрезки пробелов) текстом MUST переводить запись в failed с причиной «нет речи»: это свойство записи, а не ошибка сервиса, повтор бесполезен.

Scenario: Успешное распознавание

  • WHEN провайдер вернул текст, а запись в момент ответа в sent
  • THEN заметка содержит текст и статус inserted

Scenario: В записи нет речи

  • WHEN провайдер вернул успех с текстом из одних пробелов
  • THEN запись в failed с причиной «нет речи», в заметке — «В записи не распознано речи»

Scenario: Последовательная обработка

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

Requirement: Повторы после временных ошибок

Временная ошибка MUST увеличивать счётчик попыток и переводить запись в retry со временем следующей попытки через 30 с, 2 мин, 10 мин и 30 мин после 1-й, 2-й, 3-й и 4-й неудачи соответственно. Пятая неудача подряд MUST переводить запись в failed. Постоянная ошибка MUST переводить запись в failed сразу, независимо от счётчика. Запись в retry MUST NOT отправляться раньше своего времени следующей попытки.

Scenario: Сеть пропала и вернулась

  • WHEN первая отправка завершилась временной ошибкой в 14:15:03, а следующая попытка после 14:15:33 — успехом
  • THEN после первой попытки запись в retry со следующей попыткой 14:15:33, после второй — в inserted

Scenario: Исчерпание попыток

  • WHEN пять отправок подряд завершились временной ошибкой
  • THEN запись в failed, причина — последняя ошибка

Scenario: Постоянная ошибка

  • WHEN первая отправка завершилась постоянной ошибкой «401 Unauthorized»
  • THEN запись сразу в failed с этой причиной

Scenario: Время повтора не наступило

  • WHEN очередь запускается в 14:15:20 для записи со следующей попыткой 14:15:33
  • THEN запрос к провайдеру не отправляется

Requirement: Восстановление после прерванной отправки

Запись текущего устройства в sent, у которой с начала отправки прошло больше 10 минут, MUST считаться завершившейся временной ошибкой «отправка прервана» и обрабатываться по правилам повторов.

Scenario: Obsidian выгружен во время запроса

  • WHEN очередь запускается в 14:30:00 и находит запись в sent с началом отправки 14:19:00
  • THEN запись учитывается как временная неудача и планируется к повтору

Scenario: Запрос ещё может идти

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

Requirement: Принадлежность устройству

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

Scenario: Чужая запись не отправляется автоматически

  • WHEN на маке запускается очередь, а в vault есть запись в saved, созданная на iPhone
  • THEN мак не отправляет её

Requirement: Команда «Распознать все ожидающие здесь»

Команда MUST переназначить текущему устройству все записи других устройств в состояниях saved, retry и sent старше 10 минут, после чего обработать очередь без ожидания времени следующей попытки. Записи в failed и inserted, а также sent не старше 10 минут, MUST NOT затрагиваться.

Scenario: Мак забирает запись телефона

  • WHEN на маке выполнена команда, а в vault есть запись iPhone в retry со следующей попыткой через 20 минут
  • THEN запись переназначена маку и отправлена сейчас

Requirement: Команда «Повторить распознавание»

Команда для записи в failed или retry MUST сбросить счётчик попыток в 0, перевести запись в saved и запустить очередь. Для записей в других состояниях команда MUST ничего не менять.

Scenario: Повтор после исправления ключа

  • WHEN запись в failed после 401, пользователь исправил ключ и выполнил команду
  • THEN запись отправлена заново со счётчиком попыток, начатым с нуля

Requirement: Удалённая заметка отменяет запись

Состояние записи живёт только во frontmatter её заметки; очередь находит записи по наличию ключа voice-id в любом месте vault, а не по папке. Если заметку удалили, запись MUST считаться отменённой, и её аудиофайл MUST NOT удаляться. Если заметку удалили во время запроса, пришедший результат MUST отбрасываться без ошибки и без воссоздания заметки.

Scenario: Заметка удалена во время запроса

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

Scenario: Заметка перенесена в другую папку

  • WHEN пользователь перенёс заметку в saved из voice/inbox/ в projects/
  • THEN запись по-прежнему обрабатывается очередью

Requirement: Повторный запуск очереди

Если запуск очереди запрошен, пока она уже работает, новый параллельный проход MUST NOT начинаться; вместо этого MUST выполниться ровно один дополнительный проход после завершения текущего.

Scenario: Три запроса во время работы

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