feat: core recording lifecycle (#1) #2

Merged
aep merged 22 commits from feat/1-core-recording-lifecycle into main 2026-09-27 18:24:26 +03:00
Owner

Closes #1

Ядро жизненного цикла записи (src/core/, без импортов obsidian и Node API) по плану docs/superpowers/plans/2026-09-27-core-recording-lifecycle.md и спеке openspec/changes/core-recording-lifecycle/:

  • машина состояний записи, именование, frontmatter и тело заметки, расписание повторов;
  • saveCapture, QueueRunner.tick, ручные команды retry и claimAll;
  • golden-тест test-01.

Проверка: npm test — 10 файлов, 125/125; npm run build — exit 0. Каждая задача прошла ревью через subagent-driven-development, финальное ревью всей ветки — на Opus (Critical нет; 1 Important и 3 Minor исправлены одной волной, повторная проверка — все 7 пунктов ADDRESSED).

Rulings I made

Все записаны в openspec/changes/core-recording-lifecycle/design.md, раздел «Decisions made without discussion», №29–36.

  1. P1 (#29) — модели implementer'ов. Haiku для задач 2–7 и 9, Sonnet для 1, 8, 10 — вместо моделей из плана (и решения 26), по правилам Model Selection SDD. Цена ошибки: лишний раунд исправлений при ошибке слабой модели; брака в ветке нет, каждая задача прошла ревью. На практике Haiku дважды подписал коммит своим именем — исправлено amend'ом сообщения.
  2. P2 (#30) — дублирование тестовых хелперов. setup/rec/seed/fields в tests/queue.test.ts и tests/queue-commands.test.ts оставлены как в плане, не вынесены в tests/fakes.ts. Цена ошибки: около 20 строк дублирующейся подготовки держать синхронными; перенос позже механический.
  3. P3 (#31) — куда записываются решения. Implementer'ы вписывают свои в design.md в коммите задачи, решения контроллера — отдельными docs-коммитами. Цена ошибки: шум в нумерации/формате design.md; на деле implementer'ы решений не добавили.
  4. T8-1 (#32) — после dispose() очередь не отправляет и не пишет в vault. tick() не начинает проход, цикл отправки прерывается, send() выходит до записи sent; запрос в полёте завершается как временная ошибка. Отступление от дословного кода плана, добавлен тест. Причина: код плана сжигал попытку у каждой оставшейся записи при выгрузке и писал в vault после неё — больше, чем принятая в решении 24 цена в одну попытку. Цена ошибки: несколько строк проверок и тест, лишние, если обёртка никогда не вызывает tick() после dispose().
  5. F-1 (#33) — «Распознать все ожидающие здесь» сразу отправляет и свои отложенные записи (retry этого устройства и sent старше 10 минут), как требует спека; абзац «Ручные команды» в design.md обновлён. Цена ошибки: свои повторы уходят до 30 минут раньше срока (один лишний запрос на запись, без потери данных). Поведение подтверждено человеком.
  6. F-2 (#34) — ошибка одной записи не останавливает проход pass(). Первая ошибка пробрасывается в конце, так что tick() по-прежнему отклоняется. Цена ошибки: pass() чуть сложнее; retry()/claimAll() могут отклониться, уже изменив свою запись.
  7. F-3 (#35) — tests/fixtures/*.webm убран из .gitignore. Решения 9 и 25 требуют коммитить аудио-фикстуры; правило давало ложный зелёный локально и красный на чистом клоне. Цена ошибки: можно случайно закоммитить большой webm.
  8. F-4 (#36) — мелкие правки надёжности из финального ревью в единственной волне исправлений: запоздавший результат пишется, только если запись всё ещё sent этого устройства с тем же voice-sent-at; после dispose() не работают восстановление зависших sent, retry() и claimAll(); тест на sent на диске во время запроса; тест, что retry() для sent ничего не делает. M-3, T1, T4 оставлены отложенными. Цена ошибки: около 20 лишних строк; если проверка слишком строгая — законный поздний результат отбрасывается и запись повторяется через 10 минут (один лишний запрос).

Отложенные мелочи

Финальное ревью сочло, что ни одна не блокирует merge.

  • M-3: esbuild.config.mjs жёстко задаёт абсолютный путь к тестовому vault (решение 19). Путь теперь вписан в CLAUDE.md проекта; переопределение через env остаётся отложенным.
  • T1: types: ["node"] в tsconfig позволяет Node-глобалам (process, Buffer) проходить проверку типов в src/core; защитный тест ловит только import/require. Рекомендация ревьюера — закрыть до issue про обёртку (отдельный tsconfig для src или расширить регулярку).
  • T4: в note.ts нет обработки CRLF — такая заметка молча пропускается и остаётся saved, данные не теряются.
  • T4: пустые voice-error/voice-next-attempt/voice-sent-at читаются как отсутствующие (согласуется с решением 23).
  • T3: переход claim из saved не покрыт юнит-тестом (покрыт сквозным).
  • T5: renderNote(inserted) без text молча вставляет пустую строку.
  • T5: запасное значение '—' в статусе retry недостижимо.
  • T5: ветка default в statusLine неявно покрывает inserted.
  • T7: notePath вычисляется дважды в capture.ts.
  • T7: нет проверки коллизии пути аудио; ревьюер советует обёртке использовать createBinary, который бросает исключение вместо перезаписи.
  • T8: контракт отклонения tick() нужно учесть в обёртке (doc-комментарий добавлен, сторона обёртки открыта).
  • Версии зависимостей не зафиксированы (TypeScript ^7.0.2, vitest ^5.0.2, esbuild ^0.28.2, obsidian ^1.13.1, @types/node ^26.6.3).

🤖 Generated with Claude Code

https://claude.ai/code/session_01QKHeGuAYh4W1jNdjMConZa

Closes #1 Ядро жизненного цикла записи (`src/core/`, без импортов `obsidian` и Node API) по плану `docs/superpowers/plans/2026-09-27-core-recording-lifecycle.md` и спеке `openspec/changes/core-recording-lifecycle/`: - машина состояний записи, именование, frontmatter и тело заметки, расписание повторов; - `saveCapture`, `QueueRunner.tick`, ручные команды `retry` и `claimAll`; - golden-тест `test-01`. Проверка: `npm test` — 10 файлов, 125/125; `npm run build` — exit 0. Каждая задача прошла ревью через subagent-driven-development, финальное ревью всей ветки — на Opus (Critical нет; 1 Important и 3 Minor исправлены одной волной, повторная проверка — все 7 пунктов ADDRESSED). ## Rulings I made Все записаны в `openspec/changes/core-recording-lifecycle/design.md`, раздел «Decisions made without discussion», №29–36. 1. **P1 (#29) — модели implementer'ов.** Haiku для задач 2–7 и 9, Sonnet для 1, 8, 10 — вместо моделей из плана (и решения 26), по правилам Model Selection SDD. *Цена ошибки:* лишний раунд исправлений при ошибке слабой модели; брака в ветке нет, каждая задача прошла ревью. На практике Haiku дважды подписал коммит своим именем — исправлено amend'ом сообщения. 2. **P2 (#30) — дублирование тестовых хелперов.** setup/rec/seed/fields в `tests/queue.test.ts` и `tests/queue-commands.test.ts` оставлены как в плане, не вынесены в `tests/fakes.ts`. *Цена ошибки:* около 20 строк дублирующейся подготовки держать синхронными; перенос позже механический. 3. **P3 (#31) — куда записываются решения.** Implementer'ы вписывают свои в design.md в коммите задачи, решения контроллера — отдельными docs-коммитами. *Цена ошибки:* шум в нумерации/формате design.md; на деле implementer'ы решений не добавили. 4. **T8-1 (#32) — после `dispose()` очередь не отправляет и не пишет в vault.** `tick()` не начинает проход, цикл отправки прерывается, `send()` выходит до записи `sent`; запрос в полёте завершается как временная ошибка. Отступление от дословного кода плана, добавлен тест. Причина: код плана сжигал попытку у каждой оставшейся записи при выгрузке и писал в vault после неё — больше, чем принятая в решении 24 цена в одну попытку. *Цена ошибки:* несколько строк проверок и тест, лишние, если обёртка никогда не вызывает `tick()` после `dispose()`. 5. **F-1 (#33) — «Распознать все ожидающие здесь» сразу отправляет и свои отложенные записи** (`retry` этого устройства и `sent` старше 10 минут), как требует спека; абзац «Ручные команды» в design.md обновлён. *Цена ошибки:* свои повторы уходят до 30 минут раньше срока (один лишний запрос на запись, без потери данных). Поведение подтверждено человеком. 6. **F-2 (#34) — ошибка одной записи не останавливает проход `pass()`.** Первая ошибка пробрасывается в конце, так что `tick()` по-прежнему отклоняется. *Цена ошибки:* `pass()` чуть сложнее; `retry()`/`claimAll()` могут отклониться, уже изменив свою запись. 7. **F-3 (#35) — `tests/fixtures/*.webm` убран из `.gitignore`.** Решения 9 и 25 требуют коммитить аудио-фикстуры; правило давало ложный зелёный локально и красный на чистом клоне. *Цена ошибки:* можно случайно закоммитить большой webm. 8. **F-4 (#36) — мелкие правки надёжности из финального ревью** в единственной волне исправлений: запоздавший результат пишется, только если запись всё ещё `sent` этого устройства с тем же `voice-sent-at`; после `dispose()` не работают восстановление зависших `sent`, `retry()` и `claimAll()`; тест на `sent` на диске во время запроса; тест, что `retry()` для `sent` ничего не делает. M-3, T1, T4 оставлены отложенными. *Цена ошибки:* около 20 лишних строк; если проверка слишком строгая — законный поздний результат отбрасывается и запись повторяется через 10 минут (один лишний запрос). ## Отложенные мелочи Финальное ревью сочло, что ни одна не блокирует merge. - **M-3:** `esbuild.config.mjs` жёстко задаёт абсолютный путь к тестовому vault (решение 19). Путь теперь вписан в CLAUDE.md проекта; переопределение через env остаётся отложенным. - **T1:** `types: ["node"]` в tsconfig позволяет Node-глобалам (`process`, `Buffer`) проходить проверку типов в `src/core`; защитный тест ловит только import/require. Рекомендация ревьюера — закрыть до issue про обёртку (отдельный tsconfig для `src` или расширить регулярку). - **T4:** в `note.ts` нет обработки CRLF — такая заметка молча пропускается и остаётся `saved`, данные не теряются. - **T4:** пустые `voice-error`/`voice-next-attempt`/`voice-sent-at` читаются как отсутствующие (согласуется с решением 23). - **T3:** переход `claim` из `saved` не покрыт юнит-тестом (покрыт сквозным). - **T5:** `renderNote(inserted)` без `text` молча вставляет пустую строку. - **T5:** запасное значение `'—'` в статусе retry недостижимо. - **T5:** ветка default в `statusLine` неявно покрывает `inserted`. - **T7:** `notePath` вычисляется дважды в `capture.ts`. - **T7:** нет проверки коллизии пути аудио; ревьюер советует обёртке использовать `createBinary`, который бросает исключение вместо перезаписи. - **T8:** контракт отклонения `tick()` нужно учесть в обёртке (doc-комментарий добавлен, сторона обёртки открыта). - Версии зависимостей не зафиксированы (TypeScript ^7.0.2, vitest ^5.0.2, esbuild ^0.28.2, obsidian ^1.13.1, @types/node ^26.6.3). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QKHeGuAYh4W1jNdjMConZa
aep added 21 commits 2026-09-27 16:59:31 +03:00
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKHeGuAYh4W1jNdjMConZa
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKHeGuAYh4W1jNdjMConZa
- I-1 (F-1): claimAll() now also moves this device's own `retry` records
  and own stale `sent` records to `saved` via the `claim` transition
  (device = own), same as it already does for other devices' claimable
  records, then runs a normal tick() so everything sends without waiting
  for the next retry time. The returned count still only reflects records
  reassigned from other devices. Adds `ownReclaimable()` to schedule.ts.
- M-1 (F-2): pass() wraps each per-record operation (staleOwn update and
  each send) in try/catch, remembers the first error, keeps processing the
  rest of the due queue, and rethrows the first error after both loops
  complete — so one permanently failing record no longer blocks newer
  records behind it. list()/load() failures still reject tick()
  immediately, as before.
- F-4a: send()'s post-request update now also requires
  `r.device === env.deviceId && r.sentAt === sent.sentAt`, so a late
  transcription result can never overwrite a record another device (or a
  newer send) has since taken over.
- F-4b: pass()'s staleOwn loop now also stops when the abort signal fires;
  retry() and claimAll() return early (false / 0) without writing once
  disposed.
- F-4d: retry() no-op it.each now also covers `sent` (own, fresh).

Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decision 25 allows .webm/.ogg golden cases and decision 9 requires audio
fixtures to be committed; the ignore rule silently let a webm golden case
pass locally while failing on a clean clone (the fixture would never reach
git). Removes the `tests/fixtures/*.webm` line from .gitignore.

Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aep added 1 commit 2026-09-27 16:59:53 +03:00
aep merged commit 001ba8af80 into main 2026-09-27 18:24:26 +03:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: aep/obsidian-voice-note#2
No description provided.