Document Phase 0 scaffold and architecture

This commit is contained in:
2026-07-10 22:48:08 +03:00
parent 39fec46ab6
commit d646c51b90
26 changed files with 8222 additions and 1 deletions

View File

@@ -0,0 +1,232 @@
# Max Index V1 Implementation Plan
**Intent:** превратить пустой репозиторий в воспроизводимое кроссплатформенное desktop-приложение, которое локально распознаёт русскую речь и один раз увеличивает индекс при подтверждённой фразе.
**Current Behavior:** Phase 0 уже даёт packaged Electron/React shell, закреплённый dependency graph и чистые index/face primitives; захват микрофона, recognizer, оркестрация сессии и редактируемые правила ещё не реализованы.
**Expected Outcome:** разработчик поднимает проект через `pnpm`, а пользователь получает честно обозначенное локальное прослушивание, реакцию персонажа и индекс `0..100` без записи сырого звука и полной стенограммы.
**Target-Perspective Output:** пользователь запускает приложение, выбирает микрофон, нажимает Start, произносит настроенную фразу и видит ровно одно изменение индекса и короткую реакцию; Pause действительно освобождает микрофон, повторный запуск после загрузки модели работает offline.
**Truth Owner:** требования принадлежат корневому `AGENTS.md`; зависимости — `package.json` и `pnpm-lock.yaml`; желаемое состояние сессии, индекс и история — экземпляр `SessionController` в main-процессе поверх чистого domain reducer; MediaStream — renderer; готовность recognizer, очередь и границы utterance — utility process.
**Contract Boundary:** узкий типизированный preload API для управления и versioned snapshots; `RecognitionEvent` с `sessionId`, `utteranceId`, `sequence`, `kind` и `text`; отдельный bounded `MessagePort` для будущего PCM data plane.
**Cutover:** greenfield; mock recognizer сначала доказывает настоящую межпроцессную границу, затем `sherpa-onnx` становится единственным production recognizer.
**Displaced Path:** удалить любой Forge demo-код; после подключения sherpa оставить mock только как test/dev dependency injection; после появления оригинальных ассетов удалить CSS-заглушку лица.
**Value Density:** первый законченный срез проходит через renderer, preload, main, utility и domain, доказывая главную инварианту partial → final без двойного увеличения.
**Acceptance Evidence:** на целевой платформе виден сценарий «микрофон → русская фраза → одно увеличение → Pause останавливает tracks → offline restart»; unit/integration/smoke проверки покрывают границы процессов и отказ recognizer.
**Evidence Lane:** детерминированные Vitest-тесты, packaged smoke на текущем хосте, ручной сценарий с реальным микрофоном и отдельная release-матрица по OS/arch.
**Kill Criteria:** packaged build не содержит доступного пользователю mock-пути; PCM не проходит через control IPC; renderer не хранит канонический индекс; сырые аудиофайлы и полный transcript не появляются на диске; system loopback не смешан с microphone provider.
**Architecture Slice:** `Renderer capture → MessagePort → Utility recognizer → RecognitionEvent → Main SessionController/domain → versioned snapshot → Renderer projection`.
**Plan Review Gate:** PRE passed for Phase 0 on 2026-07-10; later phases require task-level PRE review before execution.
## Границы результата
В V1 входят микрофон, локальный streaming ASR, правила, индекс, история текущей сессии, лицо, Start/Pause/Reset, выбор устройства, tray и установщики. Аккаунты, backend, полная стенограмма, запись встреч, speaker identification и system loopback остаются вне V1.
Текущий запрос реализует только Phase 0. Он создаёт запускаемый фундамент и не выдаёт статическую оболочку за работающий recognizer.
## Карта архитектуры
### Source of truth
- `AGENTS.md` — продуктовые и инженерные ограничения.
- `package.json` + `pnpm-lock.yaml` — воспроизводимый dependency graph.
- `SessionController` в main — desired state, `sessionId`, versioned snapshot, индекс и короткая история текущей сессии.
- Pure domain reducer — matching, dedupe, cooldown, clamp и выбор face level.
- Renderer — только MediaStream handle/capture health и отображение snapshot.
- Utility process — recognizer readiness, endpointing, `utteranceId`, sequence и bounded PCM queue.
- Main model manager — версия модели, URL, SHA-256, проверенный локальный путь.
- Versioned `src/domain/triggers/default-rules.ts` — заводской набор правил; main-owned settings repository хранит только пользовательскую копию/изменения и мигрирует их по stable rule id.
`Listening` является observed state только после подтверждений renderer capture и utility readiness. Main хранит desired state, но не объявляет прослушивание активным без этих acknowledgements.
### Read path
```text
navigator.mediaDevices.getUserMedia
→ AudioWorklet
→ mono PCM chunks (100300 ms)
→ transferable MessagePort with backpressure
→ utilityProcess / SpeechRecognizer adapter
→ RecognitionEvent
→ SessionController + pure domain reducer
→ versioned SessionSnapshot
→ renderer
```
### Write path
- Rules, device choice and model metadata: main-owned repository under `app.getPath('userData')` with schema validation and atomic writes.
- Index and trigger history: memory only for the current session.
- Model: temporary download, SHA-256 verification, atomic rename, then offline reuse.
- Raw audio and full transcript: never persisted in normal mode.
### Files to avoid until their phase
- `resources/models/**` and any large model binary in Git.
- `node-cpal`: microphone capture belongs to Web Audio in renderer.
- Redux/Zustand, `electron-store` and animation frameworks without a demonstrated need.
- Universal `send(channel, payload)` preload APIs.
- `ipcRenderer.invoke('acceptPcm')` as an audio transport.
- Playwright before a stable Electron flow makes its maintenance worthwhile.
- Fake implementations for the project skills named in `AGENTS.md`.
## Dependency baseline
Phase 0 pins pnpm `10.34.5` and uses Node `>=22.13 <23`, Electron `43.1.0`, Forge `7.11.2`, React `19.2.7`, TypeScript `5.9.3`, Vite `5.4.21` and Vitest `3.2.7`. Forge packages stay on one version. `.npmrc` uses `node-linker=hoisted` because Forge packaging walks physical `node_modules`; `package.json` allows build scripts only for Electron, esbuild and the pinned macOS packaging helpers.
`sherpa-onnx-node` is deliberately introduced in Phase 3, not treated as a harmless scaffold package. It is a native runtime dependency with platform packages and shared libraries. The spike must prove Vite externalization, ASAR unpack rules for both `.node` and `.dylib/.so/.dll`, runtime library lookup and a packaged-app smoke test before the dependency becomes part of the default runtime.
The first model is `sherpa-onnx-streaming-t-one-russian-2025-09-08`. Its manifest owns the expected sample rate, files, source URL, version and SHA-256. The model is not an npm dependency.
## Phase 0: reproducible application foundation
**Outcome:** a clean checkout installs, type-checks, tests, packages and opens a secure Russian-language Max Index shell on the current macOS arm64 host.
**Status:** completed on 2026-07-10; POST review passed after navigation, CSP and macOS metadata hardening.
**Files:**
- Copy `AGENTS.md` byte-for-byte from `/Users/dokril/Downloads/AGENTS.md`; preserve the already-created `PRODUCT.md` and goal documents; create `.npmrc`, `.gitignore`, `package.json`, `pnpm-lock.yaml`, `tsconfig.json`, `eslint.config.mjs`, `forge.env.d.ts`.
- Create `forge.config.ts`, `vite.main.config.ts`, `vite.preload.config.ts`, `vite.renderer.config.ts`, `vitest.config.mts`.
- Create `src/main/main.ts`, `src/preload/preload.ts`, `src/renderer/index.html`, `src/renderer/index.tsx`, `src/renderer/App.tsx`, `src/renderer/styles.css`.
- Create only the first used pure domain files for index clamping and face-level selection plus tests.
- Update `README.md` with commands, architecture status and explicit non-capabilities.
**Allowed scope:** secure `BrowserWindow`, build/release makers, static paused snapshot and original CSS placeholder. No microphone, fake listening, recognizer, persistence, tray or settings.
**Verification:**
```bash
pnpm install --frozen-lockfile
cmp /Users/dokril/Downloads/AGENTS.md AGENTS.md
pnpm lint
pnpm typecheck
pnpm test
pnpm package
pnpm start
```
**Acceptance evidence:** package succeeds on macOS arm64 and a real Electron window states that the microphone is not connected yet. Windows and Linux remain unverified.
**Captured evidence:** frozen-lockfile install, lint, strict typecheck and 14 Vitest tests pass; `pnpm package` produces `out/Max Index-darwin-arm64/Max Index.app`; the packaged process loads `file://.../app.asar/.vite/renderer/main_window/index.html` and the Paused shell was visually inspected. A forced `https://example.com` navigation remained on the allowlisted file URL. Production CSP disables connections and inline styles; `Info.plist` has ATS arbitrary loads disabled and only the microphone usage description. Source and project `AGENTS.md` share SHA-256 `c66e1d562ee2fb1a9ffbba597081508b5318b7c0770e919d73b811cac8ff9985`.
**Parallel:** renderer shell and build configuration can proceed in parallel after package/config contracts are fixed.
## Phase 1: real mock utility vertical slice
**Outcome:** `Start → mock partial/final → exactly one TriggerMatched → reaction/index → Pause → Reset` crosses the intended process boundaries.
**Files:**
- Create `src/shared/app-contract.ts` and `src/shared/recognition-contract.ts` with runtime guards.
- Create `src/domain/triggers/default-rules.ts`, `normalize-text.ts`, `match-trigger.ts`, `trigger-engine.ts`; create focused siblings under `src/domain/index/` and `src/domain/face/`, with tests next to each module.
- Create `src/main/session-controller.ts` and `src/main/utility-recognizer.ts`.
- Create `src/audio/recognition/speech-recognizer.ts`.
- Create `src/utility/recognition.ts` as a separate Forge/Vite build entry.
- Replace the static renderer snapshot with a read-only projection over typed preload commands `start`, `pause`, `reset`, `getSnapshot`, `subscribe → unsubscribe`.
**Contract details:** partial and final for one utterance share `sessionId + utteranceId`, `sequence` is monotonic, and match identity includes `ruleId + phrase position`. Cooldown is evaluated after dedupe. `SessionSnapshot.stateVersion` resolves snapshot/subscription races.
**Default-rules cutover:** Phase 1 ships the canonical defaults in code and initializes the in-memory session from them. Phase 4 introduces persisted user rules keyed by the same stable ids; there is no second editable defaults file and no renderer-owned rule source.
**Utility lifecycle:** fork only after `app.whenReady()`, use explicit dev/package artifact paths, handle ready/exit/crash/restart/shutdown, and ignore old-session events.
**Verification:** unit tests for normalization, `ё/е`, word boundaries, overlapping rules, partial/final dedupe, cooldown, clamp and Reset; integration tests for a real utility child, stale sessions and exit handling; manual mock user flow in dev and an explicitly internal, non-release packaged test artifact.
**Acceptance evidence:** one and only one visible increment for partial + final of the same utterance.
**Production cutover gate:** release packaging cannot expose or select the mock recognizer. Phase 1's packaged mock is built only under an explicit internal test flag; Phase 3 removes that flag from release configuration before sherpa becomes the default production adapter.
**Parallel:** domain reducer and renderer projection can proceed in parallel after shared contracts are fixed.
## Phase 2: microphone and bounded PCM transport
**Outcome:** Start acquires the selected microphone, Pause releases all tracks, and bounded PCM reaches the utility process without freezing renderer or main.
**Files:**
- Create `src/audio/capture/audio-capture.ts` and the AudioWorklet processor.
- Extend main/preload/utility orchestration for a transferred `MessagePort` data plane.
- Add device enumeration, capture acknowledgements and observable queue health.
**Contract details:** aggregate 128-sample worklet frames into 100300 ms mono chunks; report actual input sample rate; resample in one utility-owned location; cap the queue and emit overload state instead of growing memory; stop tracks on Pause, device switch, window shutdown and app quit.
**Verification:** Start → Listening acknowledgement, Pause track shutdown, device switch/removal, stale-session rejection, bounded queue and proof that no audio file is created.
**Acceptance evidence:** a synthetic/mock inference path consumes live microphone PCM, while UI remains responsive under a deliberately slow consumer.
**Parallel:** capture and utility queue implementation can proceed in parallel only after the MessagePort protocol is reviewed.
## Phase 3: sherpa-onnx and T-one native packaging spike
**Outcome:** the same `SpeechRecognizer` contract runs the T-one streaming model locally in both dev and packaged macOS arm64 builds.
**Files:**
- Add `sherpa-onnx-node` as a runtime dependency.
- Create `src/audio/recognition/sherpa-recognizer.ts` and model manifest/types.
- Create main-owned model download/integrity/path management.
- Update Vite externalization and Forge ASAR unpack configuration for the addon and shared libraries.
- Add a licensed short audio fixture, expected transcript assertions and packaged native-load smoke.
**Contract details:** sample rate comes from the model manifest (T-one currently expects 8 kHz); adapter synthesizes stable utterance identity from endpointing; errors distinguish recoverable runtime failure from fatal model failure; transcript text is redacted from normal logs.
**Verification:** checksum failure, interrupted download, offline reuse, real-time factor/latency sample, native addon load in packaged app and exact-phrase fixture quality.
**Acceptance evidence:** bundled app starts offline after the first verified model acquisition and recognizes the agreed Russian fixture without a cloud call.
**Parallel:** model manager and sherpa adapter may proceed in parallel after manifest and error contracts are fixed.
## Phase 4: rules, settings, tray and complete experience
**Outcome:** users can configure rules and devices, see all face states, recover from common errors and keep the app in tray.
**Files:**
- Add main-owned settings repository and schema migrations.
- Add renderer settings view, rule editor, mic test and explicitly temporary transcript preview.
- Add tray lifecycle and idempotent Start/Pause/Reset orchestration.
- Replace CSS face with an original PNG/WebP sprite sheet and a small state machine.
**Verification:** persisted rules and device fallback, all six face levels, transient reaction, reduced motion, permission/model/device errors, tray reopen, recognizer restart and no leaked listeners.
**Acceptance evidence:** a non-technical user can recover from denied permission, missing model and vanished device using one clear action per state.
**Parallel:** settings UI and original asset production can proceed in parallel after the settings and face-state contracts are fixed.
## Phase 5: release evidence
**Outcome:** signed/installable artifacts and honest support claims for Windows x64, macOS arm64/x64 and Linux x64.
**Files:** release workflow, maker settings, icons, signing/notarization configuration, artifact checksums and operator runbook.
**Verification:** clean install/upgrade/uninstall, permissions, native addon/model path, tray, microphone, offline restart and one-real-utterance flow on every claimed OS/arch.
**Acceptance evidence:** archived per-platform run with artifact hash, app version, model version, OS/arch and user-flow result. A host-only package is not cross-platform evidence.
**Parallel:** platform lanes run independently after Phase 4 is stable.
## Phase 6: system audio as a separate provider
**Outcome:** opt-in meeting audio capture for headphone scenarios without destabilizing microphone mode.
**Allowed scope:** separate Windows, macOS and Linux providers behind the audio-source boundary; explicit permissions and capability detection.
**Kill criteria:** no universal loopback promise, no hidden fallback, and microphone remains the default known-good provider.
## Project completion gate
V1 is complete only when all of the following are proven on every supported target:
1. Active listening is unmistakable and Pause releases capture resources.
2. A configured Russian phrase changes the index exactly once per utterance.
3. Index stays within `0..100`; Reset is idempotent.
4. Renderer remains responsive during slow recognition and utility restart.
5. No raw audio or full transcript is persisted by default.
6. A verified model works offline after first acquisition.
7. Installed artifacts pass the per-platform user flow; unsupported targets are not claimed.
## Primary references
- [Electron Forge Vite template](https://www.electronforge.io/templates/vite)
- [Electron Forge pnpm packaging requirement](https://www.electronforge.io/)
- [Electron utilityProcess API](https://www.electronjs.org/docs/latest/api/utility-process)
- [sherpa-onnx Node addon installation](https://k2-fsa.github.io/sherpa/onnx/javascript-api/install.html)
- [sherpa-onnx T-one model](https://k2-fsa.github.io/sherpa/onnx/pretrained_models/online-ctc/t-one-ctc-models.html)