docs: apply plan review — retry status wording, TZ note, stale-sent and rename decisions
Refs #1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
cb9a691931
commit
944a6594ec
@ -20,7 +20,7 @@
|
||||
- `MAX_ATTEMPTS = 5`; паузы после 1–4 неудачи: 30 с, 2 мин, 10 мин, 30 мин; зависший `sent` — строго больше 10 минут.
|
||||
- Пустой транскрипт → `failed` с `voice-error: no-speech`, в заметке `> [!info] В записи не распознано речи`.
|
||||
- Маркеры области: `%%voice-note:<id>%%` … `%%/voice-note%%`.
|
||||
- Тексты статусов (дословно): `> [!info] Распознаётся…`; `> [!info] Нет связи, повтор в HH:mm (попытка N/5)`; `> [!warning] Не распознано: <причина>. Команда «Повторить распознавание»`; `> [!note] Запись прервана, сохранено M:SS`.
|
||||
- Тексты статусов (дословно): `> [!info] Распознаётся…`; `> [!info] Не удалось отправить, повтор в HH:mm (попытка N/5)`; `> [!warning] Не распознано: <причина>. Команда «Повторить распознавание»`; `> [!note] Запись прервана, сохранено M:SS`.
|
||||
- Строковые значения `voice-*` во frontmatter — в двойных кавычках (JSON-экранирование), числа и `true` — без кавычек.
|
||||
- Тесты запускаются с `TZ=Europe/Moscow` (UTC+3, без летнего времени) — выставляется в `vitest.config.ts`.
|
||||
- `npm test` — весь набор, зелёным считается только весь набор.
|
||||
@ -1176,7 +1176,7 @@ describe('renderNote: status region', () => {
|
||||
it('retry shows local time and attempt', () => {
|
||||
const r: VoiceRecord = { ...saved, status: 'retry', attempts: 2, nextAttempt: '2026-09-27T11:17:03.000Z', error: 't' };
|
||||
const out = renderNote(initialNote(saved, 0), r);
|
||||
expect(out).toContain('> [!info] Нет связи, повтор в 14:17 (попытка 2/5)\n');
|
||||
expect(out).toContain('> [!info] Не удалось отправить, повтор в 14:17 (попытка 2/5)\n');
|
||||
expect(out).not.toContain('Распознаётся');
|
||||
expect(readVoiceFields(out)['voice-status']).toBe('retry');
|
||||
});
|
||||
@ -1267,7 +1267,7 @@ export function statusLine(r: VoiceRecord): string {
|
||||
switch (r.status) {
|
||||
case 'retry': {
|
||||
const at = r.nextAttempt ? formatLocalTime(r.nextAttempt) : '—';
|
||||
return `> [!info] Нет связи, повтор в ${at} (попытка ${r.attempts}/${MAX_ATTEMPTS})`;
|
||||
return `> [!info] Не удалось отправить, повтор в ${at} (попытка ${r.attempts}/${MAX_ATTEMPTS})`;
|
||||
}
|
||||
case 'failed': {
|
||||
if (r.error === NO_SPEECH) return '> [!info] В записи не распознано речи';
|
||||
@ -1851,7 +1851,7 @@ describe('QueueRunner.tick', () => {
|
||||
'voice-next-attempt': '2026-09-27T11:15:33.000Z',
|
||||
'voice-error': 'offline',
|
||||
});
|
||||
expect(ctx.notes.files.get(path)).toContain('Нет связи, повтор в 14:15 (попытка 1/5)');
|
||||
expect(ctx.notes.files.get(path)).toContain('Не удалось отправить, повтор в 14:15 (попытка 1/5)');
|
||||
|
||||
ctx.env.advance(17_000); // 11:15:20 — рано
|
||||
await ctx.runner.tick();
|
||||
|
||||
@ -83,7 +83,7 @@ interface Env { now(): Date; random(): number; deviceId: string; }
|
||||
|
||||
### Тестовая инфраструктура (Task 1)
|
||||
|
||||
Шаблон obsidian-sample-plugin (esbuild, `manifest.json` с `isDesktopOnly: false`), vitest. Тест-страж сканирует `src/core/**/*.ts` и падает на импорт `obsidian`, `fs`, `path`, `child_process`, `node:*`. Фейки в `tests/fakes/`: in-memory `RecordingStore`/`NoteStore`, скриптуемый `Transcriber`, `Env` с управляемыми часами.
|
||||
Шаблон obsidian-sample-plugin (esbuild, `manifest.json` с `isDesktopOnly: false`), vitest. Тест-страж сканирует `src/core/**/*.ts` и падает на импорт `obsidian`, `fs`, `path`, `child_process`, `node:*`. Тесты запускаются с фиксированным `TZ=Europe/Moscow` (задаётся в `vitest.config.ts`): имена заметок и строка статуса `retry` зависят от локального времени, а golden-сравнение побайтное. Фейки в `tests/fakes.ts`: in-memory `RecordingStore`/`NoteStore`, скриптуемый `Transcriber`, `Env` с управляемыми часами.
|
||||
|
||||
Аудиофикстуры `*.m4a` лежат в git (`test-01.m4a` ~85 КБ), так что `npm test` зелёный на чистом клоне. Если для эталона `<name>.note.md` нет аудиофайла, golden-тест MUST падать с сообщением «нет аудиофикстуры tests/fixtures/<name>.m4a», а не пропускаться молча.
|
||||
|
||||
@ -125,3 +125,5 @@ interface Env { now(): Date; random(): number; deviceId: string; }
|
||||
24. 2026-09-27 (план) — `QueueRunner.dispose()` отменяет `AbortSignal`, переданный провайдеру; прерванный запрос обрабатывается как любое исключение — временная ошибка. *Контракт `Transcriber` принимает signal; выгрузка плагина должна обрывать загрузку.* **Выгрузка посреди запроса тратит одну попытку из пяти.**
|
||||
25. 2026-09-27 (план) — Golden-кейсы находятся по файлам `tests/fixtures/*.note.md`; аудио ищется как `.m4a`, `.webm`, `.ogg` с mime `audio/mp4`, `audio/webm`, `audio/ogg`. *Добавить кейс — положить три файла, без правки теста.* **Нулевая.**
|
||||
26. 2026-09-27 (план) — Модели исполнителей: haiku — `naming`, `schedule` (механика по готовому коду); opus — `QueueRunner.tick` (асинхронность, гонки); sonnet — остальное. Режим — subagent-driven-development (10 задач > 5). *Правила Model Selection SDD и глобальный CLAUDE.md.* **Слабая модель на механической задаче — лишний круг ревью, не дефект в ветке.**
|
||||
27. 2026-09-27 (план, зафиксировано на ревью) — Зависший `sent` засчитывается как попытка (временная ошибка «отправка прервана»). *Иначе запись, на которой Obsidian каждый раз выгружается, повторялась бы бесконечно.* **Пять выгрузок Obsidian подряд во время отправки дадут `failed` без единого ответа сервиса; спасает ручной повтор.**
|
||||
28. 2026-09-27 (план, зафиксировано на ревью) — Результат запроса пишется по пути, полученному из `list()`. Если заметку переименовали или перенесли между первым `process` (`sent`) и вторым (результат), второй `process` не находит файл и результат отбрасывается. *Порт `NoteStore` адресует заметки путём; отслеживание переименований — усложнение обёртки ради редкого случая.* **Запись остаётся в `sent`, через 10 минут уходит в повтор — один лишний запрос.**
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
Перед отправкой запись MUST перейти в `sent` с отметкой времени начала отправки, записанной в заметку. Очередь MUST обрабатывать записи строго по одной. Результат провайдера — успех с текстом, временная ошибка или постоянная ошибка. Успех с непустым текстом MUST переводить запись в `inserted` с текстом в заметке. Успех с пустым (после обрезки пробелов) текстом MUST переводить запись в `failed` с причиной «нет речи»: это свойство записи, а не ошибка сервиса, повтор бесполезен.
|
||||
|
||||
#### Scenario: Успешное распознавание
|
||||
- **WHEN** провайдер вернул текст для записи в `saved`
|
||||
- **WHEN** провайдер вернул текст, а запись в момент ответа в `sent`
|
||||
- **THEN** заметка содержит текст и статус `inserted`
|
||||
|
||||
#### Scenario: В записи нет речи
|
||||
|
||||
@ -32,11 +32,11 @@
|
||||
|
||||
### Requirement: Тело заметки до распознавания
|
||||
|
||||
Новая заметка MUST содержать вложение аудио `![[<путь к аудио>]]`, затем управляемую область, ограниченную маркерами-комментариями `%%voice-note:<id>%%` и `%%/voice-note%%`. Внутри области MUST отображаться состояние: для `saved`/`sent` — `> [!info] Распознаётся…`; для `retry` — `> [!info] Нет связи, повтор в HH:mm (попытка N/5)` по локальному времени; для `failed` — `> [!warning] Не распознано: <причина>. Команда «Повторить распознавание»`, кроме причины «нет речи», для которой — `> [!info] В записи не распознано речи` (без слова «ошибка» и без призыва повторить). Для прерванной записи над областью MUST стоять строка `> [!note] Запись прервана, сохранено M:SS`, которая остаётся и после вставки текста.
|
||||
Новая заметка 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)`
|
||||
- **THEN** область содержит `> [!info] Не удалось отправить, повтор в 14:17 (попытка 2/5)`
|
||||
|
||||
### Requirement: Вставка транскрипта
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user