19 KiB
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
navigator.mediaDevices.getUserMedia
→ AudioWorklet
→ mono PCM chunks (100–300 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-storeand 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.mdbyte-for-byte from/Users/dokril/Downloads/AGENTS.md; preserve the already-createdPRODUCT.mdand 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.mdwith 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:
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.tsandsrc/shared/recognition-contract.tswith runtime guards. - Create
src/domain/triggers/default-rules.ts,normalize-text.ts,match-trigger.ts,trigger-engine.ts; create focused siblings undersrc/domain/index/andsrc/domain/face/, with tests next to each module. - Create
src/main/session-controller.tsandsrc/main/utility-recognizer.ts. - Create
src/audio/recognition/speech-recognizer.ts. - Create
src/utility/recognition.tsas 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.tsand the AudioWorklet processor. - Extend main/preload/utility orchestration for a transferred
MessagePortdata plane. - Add device enumeration, capture acknowledgements and observable queue health.
Contract details: aggregate 128-sample worklet frames into 100–300 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-nodeas a runtime dependency. - Create
src/audio/recognition/sherpa-recognizer.tsand 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:
- Active listening is unmistakable and Pause releases capture resources.
- A configured Russian phrase changes the index exactly once per utterance.
- Index stays within
0..100; Reset is idempotent. - Renderer remains responsive during slow recognition and utility restart.
- No raw audio or full transcript is persisted by default.
- A verified model works offline after first acquisition.
- Installed artifacts pass the per-platform user flow; unsupported targets are not claimed.