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

127 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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** после текущего прохода выполняется ровно один дополнительный проход