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
This commit is contained in:
parent
001ba8af80
commit
9b4ea0a3c9
126
openspec/specs/recording-lifecycle/spec.md
Normal file
126
openspec/specs/recording-lifecycle/spec.md
Normal file
@ -0,0 +1,126 @@
|
||||
# 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** после текущего прохода выполняется ровно один дополнительный проход
|
||||
62
openspec/specs/voice-note-format/spec.md
Normal file
62
openspec/specs/voice-note-format/spec.md
Normal file
@ -0,0 +1,62 @@
|
||||
# voice-note-format Specification
|
||||
|
||||
## Purpose
|
||||
Определяет, где и под какими именами лежат аудиофайлы и заметки голосовых записей, как в заметке хранится состояние записи и как плагин обновляет заметку, не затрагивая текст пользователя.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Идентификатор и имена файлов
|
||||
|
||||
Идентификатор записи MUST иметь вид `YYYYMMDD-HHMMSS-xxxx` (время начала записи в UTC и 4 случайных шестнадцатеричных символа в нижнем регистре). Аудио MUST сохраняться как `voice/source/<id>.<ext>`, где расширение определяется по mime без параметров: `audio/mp4` → `m4a`, `audio/webm` → `webm`, `audio/ogg` → `ogg`, любой другой → `bin`. Заметка MUST создаваться как `voice/inbox/YYYY-MM-DD HH-mm-ss.md` по локальному времени начала записи; при занятом имени MUST добавляться суффикс ` (2)`, ` (3)` и т.д.
|
||||
|
||||
#### Scenario: Mime с параметрами
|
||||
- **WHEN** запись в формате `audio/webm;codecs=opus` начата 27.09.2026 в 14:15:03 на устройстве в поясе UTC+3
|
||||
- **THEN** аудио сохранено как `voice/source/20260927-111503-<4 hex>.webm`, заметка — `voice/inbox/2026-09-27 14-15-03.md`
|
||||
|
||||
#### Scenario: Коллизия имени заметки
|
||||
- **WHEN** заметка `voice/inbox/2026-09-27 14-15-03.md` уже существует
|
||||
- **THEN** новая заметка создана как `voice/inbox/2026-09-27 14-15-03 (2).md`
|
||||
|
||||
### Requirement: Ключи frontmatter
|
||||
|
||||
Состояние записи MUST храниться в плоских ключах frontmatter: `voice-id`, `voice-status`, `voice-device`, `voice-audio` (wiki-ссылка на аудио), `voice-mime` (исходный mime с параметрами), `voice-attempts`, а также, когда применимо, `voice-next-attempt` и `voice-sent-at` (ISO 8601 UTC), `voice-error`, `voice-interrupted`. Плагин MUST изменять только ключи с префиксом `voice-`, а остальные ключи и их порядок MUST оставаться нетронутыми. При переходе в `inserted` ключи `voice-next-attempt`, `voice-sent-at` и `voice-error` MUST удаляться. Заметка с отсутствующим или некорректным `voice-status` MUST игнорироваться очередью.
|
||||
|
||||
#### Scenario: Пользовательские свойства сохраняются
|
||||
- **WHEN** пользователь добавил в заметку свойство `tags: [idea]`, а затем запись перешла в `inserted`
|
||||
- **THEN** `tags: [idea]` на месте в прежней позиции, ключи `voice-*` обновлены
|
||||
|
||||
#### Scenario: Испорченный статус
|
||||
- **WHEN** пользователь вручную записал `voice-status: foo`
|
||||
- **THEN** очередь не обрабатывает заметку и не падает
|
||||
|
||||
### Requirement: Тело заметки до распознавания
|
||||
|
||||
Новая заметка MUST содержать вложение аудио `![[<путь к аудио>]]`, затем управляемую область, ограниченную маркерами-комментариями `%%voice-note:<id>%%` и `%%/voice-note%%`. Внутри области MUST отображаться состояние: для `saved`/`sent` — `> [!info] Распознаётся…`; для `retry` — `> [!info] Не удалось отправить, повтор в HH:mm (попытка N/5)` по локальному времени; для `failed` — `> [!warning] Не распознано: <причина>. Команда «Повторить распознавание»`, кроме причины «нет речи», для которой — `> [!info] В записи не распознано речи` (без слова «ошибка» и без призыва повторить). Для прерванной записи над областью MUST стоять строка `> [!note] Запись прервана, сохранено M:SS`, которая остаётся и после вставки текста.
|
||||
|
||||
#### Scenario: Заметка в ожидании повтора
|
||||
- **WHEN** запись перешла в `retry` после второй неудачи со следующей попыткой в 14:17 по местному времени
|
||||
- **THEN** область содержит `> [!info] Не удалось отправить, повтор в 14:17 (попытка 2/5)`
|
||||
|
||||
### Requirement: Вставка транскрипта
|
||||
|
||||
При переходе в `inserted` управляемая область вместе с маркерами MUST заменяться текстом транскрипта. Весь текст за пределами области MUST оставаться без изменений. Если маркеры не найдены, транскрипт MUST дописываться в конец заметки через пустую строку. Если запись уже в `inserted`, повторная вставка MUST не менять заметку. Обновление статуса и тела MUST выполняться одной операцией записи.
|
||||
|
||||
#### Scenario: Пользователь дописал текст под заметкой
|
||||
- **WHEN** под управляемой областью пользователь написал «купить молоко», затем пришёл транскрипт
|
||||
- **THEN** область заменена транскриптом, «купить молоко» осталось на месте
|
||||
|
||||
#### Scenario: Маркеры удалены вручную
|
||||
- **WHEN** пользователь стёр маркеры, затем пришёл транскрипт
|
||||
- **THEN** транскрипт дописан в конец заметки, статус `inserted`
|
||||
|
||||
#### Scenario: Дубль ответа
|
||||
- **WHEN** транскрипт приходит для записи, уже находящейся в `inserted`
|
||||
- **THEN** содержимое заметки не изменилось
|
||||
|
||||
### Requirement: Golden-эталоны
|
||||
|
||||
Для каждой пары фикстур `tests/fixtures/<name>.<audio-ext>` + `<name>.txt` с эталоном `<name>.note.md` полный путь «захват → очередь → вставка» с провайдером, отвечающим содержимым `<name>.txt`, при фиксированных времени, идентификаторе устройства и источнике случайности MUST давать заметку, побайтно совпадающую с `<name>.note.md`. Провайдер MUST получать байты аудиофикстуры без изменений и исходный mime.
|
||||
|
||||
#### Scenario: test-01
|
||||
- **WHEN** прогоняется фикстура `test-01.m4a` с mime `audio/mp4` и ответом из `test-01.txt`
|
||||
- **THEN** итоговая заметка совпадает с `test-01.note.md`
|
||||
Loading…
x
Reference in New Issue
Block a user