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

63 lines
7.4 KiB
Markdown
Raw 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.

# 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`