feat: add mobile shell and unified search
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Failing after 50s
ci / 워크스페이스 빌드 검증 (push) Has been skipped
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 20s
ci / .NET API 서버 테스트 (push) Successful in 12s
deploy-site / deploy (push) Successful in 39s
ci / 모바일 린트·타입·Jest (push) Failing after 3h10m31s

This commit is contained in:
Yun Chan 2026-09-28 23:23:36 +09:00
parent f505a03d34
commit 5ca3709b32
98 changed files with 5168 additions and 2093 deletions

View file

@ -0,0 +1,52 @@
# Wave 4/5 모바일 셸·통합 검색 검증 요약 — 2026-09-28
이 문서는 `.gitignore` 대상인 `scratch/design-audit/wave45/`의 로컬 원본을 대신해 커밋 가능한 메타데이터와 판정 경계를 남긴다. 비밀값이 들어갈 수 있는 Maestro debug bundle과 로그는 복사하지 않았고, 아래에는 테스트 자격 증명이 없다.
## 실행 대상
| 항목 | 값 |
|---|---|
| package | `com.d3ro.voice` |
| versionName | `1.9.0-e2e.20260928.2` |
| versionCode | `202609282` |
| APK | `apps/mobile-rn/android/app/build/outputs/apk/e2e/app-e2e.apk` |
| 크기 | 147063583 bytes |
| SHA-256 | `96EBA2A47E54033EB0DEFCE4E4D282F1D06F162A5F7DF9457E99609D98745D58` |
이 APK는 로컬 Wave 4/5 E2E artifact다. production 서명 APK/AAB, Play track, Play 설치 또는 production release provenance 증거가 아니다.
## 자동·런타임 결과
| 게이트 | 결과 | 원본 |
|---|---|---|
| 모바일 전체 Jest | **86 suites / 589 tests PASS** | `npm --prefix apps/mobile-rn test` |
| 320dp dark journey | **1/1 PASS, 0 failure, 88.0 s** | `scratch/design-audit/wave45/navigation-shell-search-320-dark-verified.xml` |
| 411dp light journey | **1/1 PASS, 0 failure, 100.0 s** | `scratch/design-audit/wave45/navigation-shell-search-411-light-verified.xml` |
원본 XML 무결성:
- 320dp dark: 440 bytes, SHA-256 `55AA0A3FDAF07B9FD4CB4F67341C798A3A952D7C6E76B829D382AC5390CCEA38`
- 411dp light: 442 bytes, SHA-256 `017B882E24150F66362ACF367A5F4DB9EB98E56ED743359B01BDDE68DCF3337D`
두 canonical run은 visible Home/Records/Meetings/More 4탭, FAB 열기와 Android Back 닫기, Home 통합 검색의 record/meeting 결과와 각 상세 이동, Records/Meetings/More 도착을 검사했다. 파일명이 run의 viewport/theme를 보존하지만 XML 본문에는 viewport/theme, APK digest/version, Android/API 또는 Maestro version이 내장되지 않으므로 APK identity와 XML 사이를 암호학적으로 묶은 provenance로 읽으면 안 된다. 이전 `*-final.xml` 실패본은 canonical이 아니다.
## 상태 판정
- `GAP-UX-04`, 모바일 `SHELL-13`, 모바일 `SHELL-14`: `[x]`. 셸 계약과 두 대표 viewport/theme journey가 통과했다.
- `Q-021`, 모바일 `SHELL-15`: `[~]`. 실제 TalkBack/focus order, font scale, rotation 증거는 없다. 48dp와 theme contrast는 source/Jest 증거다.
- `GAP-UX-05`, `MEM-15`, `H-016`: `[~]`. 모바일 결과→상세 journey가 두 run에서 통과했어도 검색 구현·성능 계약은 아직 닫히지 않았다.
`ScreenHeader`는 새 `UnifiedSearch`와 이번 v4에서 전환한 화면에 적용했다. 모든 역사적 root-stack 화면이 공용 헤더로 전환됐다는 주장은 하지 않는다. 런타임 journey는 세 FAB action 각각의 실제 route, hidden `Record` route의 탭 바 비노출, 48dp geometry도 직접 assertion하지 않으며 이 계약은 Jest와 소스 증거에 의존한다. Maestro 로그의 부수 heartbeat 기록 오류 때문에 이 run을 Q-023의 “로그 오류 0” 증거로도 사용하지 않는다.
## 의도적 제거
`DashScreen`과 `WorksHubScreen`은 v4 셸에서 제거했다. Home이 dashboard 역할을 맡고, WorksHub의 목적지는 1급 탭과 More에서 계속 접근한다. WorksHub 전용 로컬 도구 검색·고정은 중복 navigation taxonomy와 local ordering state를 줄이기 위해 의도적으로 종료했다. 사용자 기록·회의·도구 콘텐츠를 삭제한 작업은 아니며, 업그레이드 설치에는 사용하지 않는 `works-hub-pins.v1` AsyncStorage 키가 남을 수 있다.
## 남은 게이트
- 데스크톱 Electron에서 `Ctrl+K`와 record/meeting 결과 이동 수동 검증
- 별도 `transcripts` segment 검색 설계·구현
- 모바일 raw-text overfetch 제거와 in-flight request cancellation
- 양 표면 FTS/index 및 실제 대규모 데이터 성능 기준·측정
- TalkBack/focus order, font scale, rotation 실기기 검증
- production Firebase/CI secret, signed AAB, Play track, Fold6 Play-installed artifact 검증

View file

@ -13,9 +13,9 @@ The canonical place for types and cross-surface logic. Both desktop and web/mobi
| Area | Exports | Notes |
|---|---|---|
| Types | `./types` | Domain types shared across surfaces |
| Types | `./types` | Domain types shared across surfaces, including the record+meeting `UnifiedSearchSource` / `UnifiedSearchMatchedField` / `UnifiedSearchResult` / `UnifiedSearchParams` / `UnifiedSearchResponse` contract |
| Errors | `./errors` | `D3ROError`, `ErrorCode` |
| IPC channels | `./ipc-channels` | `IPC_CHANNELS` object + `IPCChannel` union. **SSOT** for every desktop IPC channel (VOICE, AUDIO, STT, TTS, LLM, KEYBINDING, CONFIG, HISTORY, DICTIONARY, WINDOW, SYSTEM, STATS, MEMO, VOICE_COMMAND, CONTEXT, CHAIN, CAPTION, FILE_TRANSCRIPTION, MEETING_SUMMARY, DICTATION_TEMPLATE, VOICE_CONVERSATION, RAG, VOICE_ACTION, MEETING_MODE, MEETING_DOC_TEMPLATE, MEETING_CHAT, LICENSE, CLOUD_SYNC, INSTRUCTION, SYSTEM_AUDIO, POPUP_RESULT, POPUP_HISTORY, POPUP_COMMAND, POPUP_CAPTION, VOICE_PARTIAL, CLIPBOARD, APP, ONLINE_AUTH, ADS, SUPPORT, INPUT_TELEMETRY, SUGGESTION, POPUP_SUGGESTION) |
| IPC channels | `./ipc-channels` | `IPC_CHANNELS` object + `IPCChannel` union. **SSOT** for every desktop IPC channel (VOICE, AUDIO, STT, TTS, LLM, KEYBINDING, CONFIG, HISTORY, UNIFIED_SEARCH, DICTIONARY, WINDOW, SYSTEM, STATS, MEMO, VOICE_COMMAND, CONTEXT, CHAIN, CAPTION, FILE_TRANSCRIPTION, MEETING_SUMMARY, DICTATION_TEMPLATE, VOICE_CONVERSATION, RAG, VOICE_ACTION, MEETING_MODE, MEETING_DOC_TEMPLATE, MEETING_CHAT, LICENSE, CLOUD_SYNC, INSTRUCTION, SYSTEM_AUDIO, POPUP_RESULT, POPUP_HISTORY, POPUP_COMMAND, POPUP_CAPTION, VOICE_PARTIAL, CLIPBOARD, APP, ONLINE_AUTH, ADS, SUPPORT, INPUT_TELEMETRY, SUGGESTION, POPUP_SUGGESTION) |
| Key bindings | `./keybinding` | **SSOT** for every global shortcut in the app: `KeyBinding` (`device`/`code`/`ctrl`/`alt`/`shift`/`meta`), `KEY_CATALOG` (10 selectable groups incl. mouse), `KEYBINDING_ACTIONS` (9 rebindable actions incl. the three suggestion actions), `bindingKey`/`normalizeBinding`/`validateBinding`/`detectBindingConflicts`/`formatBindingSegments`/`searchKeyCatalog`/`parseBindingMap`. Persisted as `AppConfig.keyBindings`. i18n keys are exposed as plain `string` so core stays independent of `@d3ro/i18n`; consumers narrow at the boundary (`asTranslationKey`) and a contract test guards the keys — accepted constraint, `11` §7 CONSTRAINT-I18N-01. Tests: `__tests__/keybinding*.test.ts` via `vitest.config.ts` (`npm run test --workspace=@d3ro/core`), 117 cases as of 2026-09-21 |
| Input intelligence | `./input-intelligence` | **SSOT** for the input-telemetry and next-sentence-suggestion domain (added 2026-09-21): key classification (`classifyKeyStroke`), text metrics (`countWords`/`countSentences`/`endsSentence`/`textBeforeCaret`), `computeTypedDelta` (UIA snapshot diff — the IME-safe way to count typed text), `decideSuggestion` + `isAppExcluded` (when to request / skip / clear), `parseSuggestionCandidates`/`sanitizeSuggestionLine` (prompt-leak and prefix-echo defence), `anchorFloatingPanel` (caret-anchored overlay placement), `mergeActivityBucket`/`summarizeActivity`/`pixelsToMeters`, `extractPhrases`/`selectPhraseHints`, `INPUT_TELEMETRY_DEFAULTS`/`SUGGESTION_DEFAULTS` |
| Constants | `./constants` | Shared constants |
@ -48,10 +48,11 @@ Peers: React 19, MUI 7, Emotion 11. Depends on `@d3ro/core`.
| Provides |
|---|
| `NativeThemeProvider` / `useNativePalette`, native palette/typo/radius/fonts |
| Components: `MetalCard`, `PhosphorText`, `Led`, `PhysicalButton`, `ScreenPanel`, `WaveBars`, `AppStatusBar`, `Header`, `FilterChip` |
| `NativeThemeProvider` / `useNativePalette`; dark/light/get-palette, raw/status, typo/radius/fonts/space/control/weight/motion/numeral tokens. The v4 semantic colors mirror `packages/ui/src/theme-raw.ts`; `d3roNativeControl.h` is the 48dp control floor |
| Components: `MetalCard`, `PhosphorText`, `Led`, `PhysicalButton`, `ScreenPanel`, `WaveBars`, `FilterChip` |
Peers: `react`, `react-native`. Consumed by `apps/mobile-rn` via file: workspace dependencies.
App-specific `ScreenHeader`, `RecordFab`, and `UnifiedSearchResultRow` stay under `apps/mobile-rn/src/components`; they are not shared package exports.
---

View file

@ -54,6 +54,7 @@ Singleton + `EventEmitter` pattern (`getXService()` accessors).
| Service | Purpose |
|---|---|
| `HistoryService` | SQLite history CRUD/search/stats |
| `UnifiedSearchService` | Read-only SQLite search across history title/content and meeting title/transcript; merges newest-first results without changing either schema |
| `DictionaryService` | Custom vocabulary CRUD/search + cloud sync hooks + JSON/CSV import/export (`dictionary:import`/`export`, save/open dialogs) |
| `MemoService` | Memo tags over history (`memo_tags`) |
| `RAGService` | Local RAG: `nomic-embed-text` embeddings, cosine search over `rag_chunks` |
@ -131,6 +132,7 @@ Registry: `src/main/ipc/index.ts` calls 31 `registerXHandlers()` in fixed order.
| `dictionary-handlers` | DICTIONARY |
| `file-transcription-handlers` | FILE_TRANSCRIPTION |
| `history-handlers` | HISTORY (incl. `history:setFavorite`, `history:getAudio` → local bytes or signed URL) + `stats:getSummary` |
| `unified-search-handlers` | UNIFIED_SEARCH (`unifiedSearch:query`), returning `IPCResult<UnifiedSearchResponse>` |
| `input-telemetry-handlers` | INPUT_TELEMETRY |
| `instruction-handlers` | INSTRUCTION |
| `keybinding-handlers` | KEYBINDING |
@ -156,7 +158,7 @@ The **`KEYBINDING`** group replaced the old per-action `HOTKEY` group. `HOTKEY`
**`LLM.PROCESS` normalizes at the IPC boundary.** The handler runs `buildInstructionInvocation` itself when `action === 'custom'` with a `customPrompt` (`llm-handlers.ts:94-108`), so the renderer passes the **raw instruction text** and never duplicates the substitution or argument-placement rules. This is what makes `VoiceModeService`, `ChainService`, and `LLM.PROCESS` literally share one implementation. No channel or type changed for this; `LLMProcessParams` is unchanged.
Preload exposes **`window.electronAPI`** with 35 namespaces: `platform, audio, config, voice, stt, keybinding, llm (incl. premium), history, dictionary, stats, window, system, instruction, app, memo, voiceCommand, context, chain, caption, license, fileTranscription, meetingSummary, dictationTemplate, rag, voiceAction, voiceConversation, meetingMode, meetingChat, meetingDocTemplate, cloudSync, onlineAuth, ads, support, inputTelemetry, suggestion`. The `keybinding` bridge is 9 methods mirroring the channels above (`src/preload/index.ts:323`), replacing the 11-method `hotkey` bridge. Envelope: `IPCResult<T>` (success/error); `app.onDataChanged` is the global refresh channel.
Preload exposes **`window.electronAPI`** with 37 namespaces: `platform, audio, config, voice, stt, runtime, keybinding, llm (incl. premium), history, unifiedSearch, dictionary, stats, window, system, instruction, app, memo, voiceCommand, context, chain, caption, license, fileTranscription, meetingSummary, dictationTemplate, rag, voiceAction, voiceConversation, meetingMode, meetingChat, meetingDocTemplate, cloudSync, onlineAuth, ads, support, inputTelemetry, suggestion`. The `keybinding` bridge is 9 methods mirroring the channels above (`src/preload/index.ts:323`), replacing the 11-method `hotkey` bridge. Envelope: `IPCResult<T>` (success/error); `app.onDataChanged` is the global refresh channel.
---
@ -185,17 +187,18 @@ Vanilla popups (`src/renderer/popups/`):
## 5. Renderer IA
Routing is state-based in `AppLayout.tsx` (`Route` union + `NAV_ITEMS`), no react-router.
Routing is state-based in `renderer/navigation/app-navigation.tsx` (`AppRoute`, params, history/back), no react-router. `Ctrl+K` opens the search route and `Ctrl+1..3` opens Home/Records/Meetings.
| Page | Route | Feature |
|---|---|---|
| `DashboardPage` | dashboard | Voice cockpit: hero, bento tiles, multi-engine hub (STT/LLM), telemetry, recent history, file drop |
| `HistoryPage` | history | History & memory timeline; search, tag filter, pagination, export/delete |
| `HomePage` | home | Today summary, recording/meeting/caption starts, current blockers, recent records/meetings, file transcription, bounded free usage |
| `UnifiedSearchPage` | search | Record+meeting search, newest-first merged results, source-aware detail navigation; source/tests complete, Electron manual proof pending |
| `HistoryPage` | records | History & memory timeline; search, tag/mode/favorite filters, pagination, export/delete |
| `DictionaryPage` | dictionary | Custom vocabulary editor |
| `CommandsPage` | commands | Custom instructions + voice keyword rules + LLM chains + dictation templates |
| `VoiceConversationPage` | conversation | Duplex voice assistant (local pipeline vs OpenAI Realtime) |
| `KnowledgeBasePage` | knowledge | Local RAG: add/index docs, semantic query, reindex/remove |
| `MeetingModePage` | meeting | Meeting studio: live transcript, memos, doc generation/export, diarization |
| `MeetingModePage` | meetings | Meeting studio: live transcript, memos, doc generation/export, diarization |
Modals/components: `SettingsModal` (tabs General/Audio/STT/LLM/Input/License/Cloud/About), `LicenseModal`, `LicenseTab`, `CloudSyncSection`, `InputInsightsPanel` (consent + suggestion policy + weekly insights + learned phrases), `InputConsentPanel` (receipt + current-app exclusion recommendation), `InputInsightsView` (flow, friction and app-quality summaries), `OnboardingModal`, `UpgradePromptModal`, `ProBadge`, `TemplateSection`, `FileDropZone`, `OllamaGuideModal`, `CodexOAuthGuideModal`, `TitleBar`, `StatusBar`, meeting components (9), voice-conversation, payment (`CheckoutModal`, `checkout-flow.ts`), support (`SupportModal`), ads (`AdBanner`, `RewardedQuotaModal`), shared cards.
@ -220,6 +223,7 @@ DB schema (`src/main/db/schema.ts`, drizzle SQLite): `history`, `dictionary`, `s
- **Local STT is packaged** (`1.3.0`): `electron-builder.yml` `extraResources` copies `sidecar-dist/sidecar` → `resources/sidecar` and `resources/ffmpeg` → `resources/ffmpeg`; `scripts/ci/verify-sidecar-bundle.mjs` gates packaging. Build locally with `npm --prefix apps/desktop run sidecar:setup && npm --prefix apps/desktop run sidecar:build`. The sidecar stays in console mode so `stdout`/`stderr` reach the app log (UTF-8, line-buffered); a packaged sidecar **must** exist or startup fails loudly instead of silently falling back to a system Python.
- All local engine URLs (`LocalSTTService`, `LocalLLMService`, `RAGService`, `OnlineLLMService`, `STTManager`) pass through `src/main/utils/loopback.ts`, which rewrites `localhost` to `127.0.0.1`, because some Windows hosts resolve `localhost` to IPv6 only and local engines bind IPv4.
- Meeting intelligence, RAG, voice conversation (local + Realtime), captions, file transcription: implemented.
- **Unified search (2026-09-28): source and automated tests only.** `UnifiedSearchService` + `unifiedSearch:query` IPC + preload bridge + `UnifiedSearchPage` search records and meetings and route to their detail views. The focused service and IPC tests pass; the renderer is source/type-gated, not headful-tested. No external-terminal Electron run has yet proven `Ctrl+K`, result rendering, or both detail transitions; treat the desktop capability as `[~]` until that run is retained.
- **LLM instruction prompts: fixed 2026-09-21 (`9c2b4d4`), not yet verified in a running app.** Running a custom instruction inserted the instruction's own wording instead of the processed result. Two faults stacked: the instruction was passed in the `text` argument with the system-prompt argument left empty, and `BASE_SYSTEM_PROMPTS` has no `custom` key so resolution fell back to `refine` **silently** — the model polished the instruction and the transcript never reached it; separately, only `{{text}}` was substituted and none of the five built-in presets use it (`{{targetLanguage}}`, `{{userPrompt}}`, or no placeholder), so the substitution was a no-op from the day it was written. Introduced in `fea923d` (2026-04-05) and present in every release `v0.1.0-alpha`..`v1.4.0` — **the path never worked; this is not a regression.** Plain actions (`refine`/`summarize`/`grammar`/`expand`) were unaffected and are now pinned by regression cases. The fix routes all three entry paths through `llm-prompts.ts` (see §2) and additionally corrects two things found alongside it: a voice shortcut naming an instruction was nullified by the `defaultLLMAction === 'none'` gate (`VoiceModeService.ts:779`), and the commands-page pipeline bench called `llm.generate`, which preload does not expose, so every run threw and the `catch` displayed the **input** as if it had succeeded — a fail-closed violation that is the reason the bug went unnoticed for five months (`CommandsPage.tsx:180-205`, now on `llm.process` with failures rendered as failures).
- **Verification limits — do not read this as verified.** Unit tests pass (`llm-prompts.test.ts` 21, `llm-handlers.test.ts` 7, `VoiceModeService.test.ts` 22, `ChainService.test.ts` 5), and each of the four fixes was reverted individually to confirm the tests actually fail without it. `npm run lint` (apps/desktop scope) passes; `tsconfig.check.json` errors went 36 → 35 (the `llm.generate` error is gone) with no errors in the touched files. But there is **no running-app run**, and `tests/red/{instruction,chain,voice,config}.usecase.test.ts` — precisely the related paths — never executed because of the `better-sqlite3` ABI mismatch. That range is neither passing nor failing; it is untested (`11` GAP-LLM-02, GAP-INFRA-06).
- **Key bindings: implemented and verified on Windows.** Every global shortcut now comes from one contract (`@d3ro/core/keybinding`) with multiple bindings per action, mouse-button support, and no hardcoded accelerators left in `bootstrap.ts`. A manual run on 2026-09-21 confirmed legacy migration (custom values preserved), 6 actions loaded, the uiohook keyboard **and** mouse hook active with zero boot errors, and multi-binding working; contract side is `packages/core` 117 tests GREEN with no type errors in the key-binding files (`11` GAP-KEY-01 `[x]`). Two things remain open: `KeyBindingService` has no unit test of its own, and macOS/Linux mouse behavior is unconfirmed (`11` GAP-KEY-02). The rewrite also fixed a dead hands-free double-press path, an order-dependent reserved-combo check, a `globalShortcut.unregisterAll()` that wiped the popup accelerators, and a `setEnabled(true)` that re-enabled hooking with an empty binding set.
@ -252,6 +256,7 @@ DB schema (`src/main/db/schema.ts`, drizzle SQLite): `history`, `dictionary`, `s
| LLM prompt / placeholder SSOT | `src/main/services/llm-prompts.ts` (shared by `VoiceModeService`, `ChainService`, `ipc/llm-handlers.ts`) |
| DB schema | `src/main/db/schema.ts` |
| Renderer shell / routes | `src/renderer/components/AppLayout.tsx` |
| Unified search | `src/main/services/UnifiedSearchService.ts`, `src/main/ipc/unified-search-handlers.ts`, `src/renderer/pages/UnifiedSearchPage.tsx` |
| Update feed SSOT | `src/main/update-feed.ts` |
| Update policy SSOT | `release/update-policy.json` + `src/main/update-policy.ts` |
| Path/loopback resolution | `src/main/utils/paths.ts`, `src/main/utils/loopback.ts` |

View file

@ -13,23 +13,25 @@ Root stack (`App.tsx` `RootNavigator`) branches on auth/onboarding:
- `recovery` → `UpdatePassword`
- `onboarding` → Onboarding + InviteAccept + Login
- unauthenticated → Login, SignUp, ForgotPassword, InviteAccept
- authenticated → `Main` (`TabNavigator`) + ~22 stack screens
- authenticated → `Main` (`TabNavigator`) + 23 stack screens
Bottom tabs (`TabNavigator.tsx`): **Dash / Works / Record (center raised FAB) / Talk / Settings**.
Bottom tabs (`TabNavigator.tsx`): **Home / Records / Meetings / More**. `Record` remains a hidden tab route so an active recording stays mounted; it never occupies a visible tab slot. A floating `RecordFab` appears on Home/Records/Meetings (not More/Record) and routes **dictation → Record**, **meeting recording → Meetings + `createRequest`**, **audio import → Record + `importRequest`**. Android back, scrim press, and action selection close its menu; tab back behavior is history-based.
Home opens `UnifiedSearch` on the root stack. New and converted v4 flows use the app-level `ScreenHeader` with a 48dp back action; tab roots such as Records and Meetings show a title without a back action. Historical stack screens that were not part of this conversion may still use their own header.
Deep links: `d3ro-voice://` and `https://d3ro.chanpaca.net` (`accept-invite`).
Provider stack: `GestureHandlerRootView → SafeAreaProvider → AuthProvider → EntitlementProvider → BillingProvider → MobileAdsProvider → MobilePreferencesProvider → DeviceProvider → LocalizedApplication (I18n + StatusBar)`.
---
## 2. Screens (`src/screens/`, 30)
## 2. Screens (`src/screens/`: 31 active source files)
| Screen | Feature |
|---|---|
| `RecordScreen` | Recording + local-whisper/cloud transcription, import audio, share |
| `DashScreen` | Dashboard: usage stats, tier, streaks, trends |
| `WorksHubScreen` | WORKS hub: pinned feature drill-down (desktop-parity) |
| `TalkScreen` | Voice/text AI chat: STT, LLM streaming, TTS |
| `HomeScreen` | Today summary, recent 5 records / 3 meetings, unified-search entry, Free plan entry |
| `MoreScreen` | Tools and account routes; Admin entry is role-gated |
| `UnifiedSearchScreen` | Authenticated record+meeting search, newest-first merged results, source-aware detail navigation |
| `RecordScreen` | Recording + local-whisper/cloud transcription, import audio, share; entered from the hidden Record route/FAB |
| `TalkScreen` | Voice/text AI chat: STT, LLM streaming, TTS; opened from More on the root stack |
| `HistoryScreen` / `HistoryDetailScreen` | History list/detail: sync, search, filters, audio playback, edit/share/favorite/delete |
| `MeetingsScreen` / `MeetingDetailScreen` | Meeting workspaces: create/rename/delete, transcript, memos, documents, export, content report |
| `MemosScreen` | Memos: list, tags, search, share |
@ -46,16 +48,17 @@ Provider stack: `GestureHandlerRootView → SafeAreaProvider → AuthProvider
| `AdminScreen` / `AdminUserDetailScreen` | Role-gated admin: users, subscriptions, audit, role/tier edits |
| `ProPaywallScreen` | Pro/Pro+ paywall: Play IAP, restore, rewarded ads |
| `OnboardingScreen` | First-run audience/theme/locale |
| `SettingsScreen` | Preferences, locale/theme, privacy links, logout |
| `SettingsScreen` | Preferences, locale/theme, privacy links, logout; opened from More on the root stack |
| `LoginScreen` / `SignUpScreen` / `ForgotPasswordScreen` / `UpdatePasswordScreen` / `InviteAcceptScreen` | Auth flows |
---
## 3. Features (`src/features/`, 20 modules)
## 3. Features (`src/features/`, 23 modules)
| Module | Purpose |
|---|---|
| `recording/` | Durable offline recording queue, retry processing, language handling |
| `audio/` | Linked recording/audio cleanup |
| `import/` | Audio pick/decode/validate, local Whisper fallback, resumable multipart upload, Android share-intent intake |
| `history/` | History service + local cache + sync hook |
| `meetings/` | Meeting workspace CRUD, realtime channels, processing jobs, transcripts/memos/docs |
@ -70,11 +73,13 @@ Provider stack: `GestureHandlerRootView → SafeAreaProvider → AuthProvider
| `dashboard/` | Dashboard stats + recent entries |
| `templates/` | Templates + generation idempotency |
| `auth/` | OAuth sign-in (in-app browser, PKCE) + identity linking |
| `account/` | Account deletion service and server-confirmed exit boundary |
| `admin/` | Admin user/subscription/audit APIs + session/role |
| `data-portability/` | Canonical JSON export/import, schema validation, legacy import, meeting export, file sharing |
| `chat/` | Chat LLM normalization/streaming, reportable generation IDs |
| `notifications/` | Notification contract/native registration/service/runtime hook |
| `reporting/` | AI content reporting (reason codes, Edge calls, idempotency) |
| `search/` | User-scoped record title/original/polished/summary + meeting title/raw/edited transcript search; sanitization, parallel queries, newest-first merge |
Core libs (`src/lib/`): `auth-context`, `auth-redirect`, `auth-capabilities`, `secure-auth-storage` (Keychain + legacy migration), `logout`, `account-exit`, `account-local-data` (central purge), `supabase`, `billing-context` (Play/App Store IAP), `entitlement-context`, `device-context`, `preferences-context`, `mobile-ads-context` (UMP consent + rewarded), `native-config`, `pkce-s256`, `random-id`, `audio-recorder`, `e2e-runtime-bootstrap`.
@ -87,10 +92,12 @@ Source of truth for status is `docs/v3/MOBILE_APP_COMPLETION_SSOT.md` sections 0
| Area | Status |
|---|---|
| Auth / account | `[~]` Implemented + Jest GREEN; external OAuth consent→callback and logout/delete/provider device E2E pending |
| Onboarding / theme / accessibility | `[x]` device journey GREEN (some checklist rows still `[ ]` for a11y specifics) |
| Onboarding | `[x]` previous first-run device journey GREEN; this does not verify the new v4 shell on every viewport/theme |
| v4 shell / theme / accessibility | `[x]` shell journey at 320dp dark and 411dp light; `[~]` broader design-system/a11y proof because TalkBack/font-scale/rotation remain |
| Recording / transcription / Talk | `[x]` core path GREEN on API 34 (FGS, recovery, local Whisper, cloud fail-closed, TTS) |
| Data portability | `[x]` export/restore + share E2E GREEN |
| History / meetings | `[x]` real data + meeting creation contract GREEN; audio timestamp/speaker + cross-app E2E pending |
| Record+meeting unified search | `[~]` service/row tests and both canonical runtime journeys (both source results → details) GREEN; mobile raw-text overfetch/no cancellation and FTS/index/large-data performance remain unproved |
| Teams / push | `[~]` teams GREEN; real FCM delivery needs Firebase/FCM credentials |
| Billing / ads | `[~]` test ads + Google Play verification code GREEN; live store purchase/restore/settlement and production AdMob serving **RED (external)** |
| Admin | `[x]` mobile admin role E2E GREEN (ordinary/manager/admin/super_admin, stale JWT) |
@ -104,11 +111,16 @@ Source of truth for status is `docs/v3/MOBILE_APP_COMPLETION_SSOT.md` sections 0
The Expo skeleton (`apps/mobile`) was deleted on 2026-09-13: it duplicated auth/data logic, shipped a dev-bypass login, and was outside the workspace and CI. `scripts/ci/sync-version.mjs` and `package-lock.json` no longer reference it. `apps/mobile-rn` is the sole mobile runtime; git history retains the old app.
The v4 shell also removed `DashScreen` and `WorksHubScreen`. Home replaces the dashboard entry; first-class tabs and More preserve the former WorksHub destinations. The WorksHub-only local tool filter and pinning were intentionally retired to avoid a second navigation taxonomy and local ordering state. This removed navigation UI, not record/meeting/tool content; an unused `works-hub-pins.v1` AsyncStorage key may remain on upgraded installs.
---
## 6. Tests & evidence
- Jest: `apps/mobile-rn/__tests__/` — per SSOT 57 suites / 353 tests (auth, privacy purge, admin, billing, recording language, generation idempotency, templates/memos, STT quota integration).
- Jest: `npm --prefix apps/mobile-rn test` — current full tree **86 suites / 589 tests PASS** (2026-09-28). v4-focused coverage includes `navigation-shell.test.tsx`, `screen-header.test.tsx`, `theme-parity.test.ts`, `ui-native-theme.test.tsx`, `unified-search-service.test.ts`, and `unified-search-result-row.test.tsx`.
- Maestro E2E: `apps/mobile-rn/.maestro/` + evidence in `.maestro-output/`.
- v4 local runtime: `apps/mobile-rn/.maestro/navigation-shell-search-local.yaml`; 320dp dark **1/1 PASS, 88 s** at `scratch/design-audit/wave45/navigation-shell-search-320-dark-verified.xml`, and 411dp light **1/1 PASS, 100 s** at `scratch/design-audit/wave45/navigation-shell-search-411-light-verified.xml`.
- Runtime APK: `com.d3ro.voice`, versionName `1.9.0-e2e.20260928.2`, versionCode `202609282`, 147063583 bytes, SHA-256 `96EBA2A47E54033EB0DEFCE4E4D282F1D06F162A5F7DF9457E99609D98745D58`. This is a local E2E artifact, not a production-signed APK/AAB or Play release.
- Committed evidence summary: [`docs/evidence/wave45-mobile-shell-search-2026-09-28.md`](../evidence/wave45-mobile-shell-search-2026-09-28.md).
- Android instrumentation: `apps/mobile-rn/android/app/src/androidTest` (CSPRNG + incoming share).
- Build verifiers: `scripts/ci/verify-android-artifact.mjs`, `verify-android-app-links.mjs`, `verify-mobile-build-config.mjs`, `verify-mobile-release-boundary.mjs`.

View file

@ -68,6 +68,7 @@ Status quick-reference: `[x]` done+verified · `[~]` partial/unverified · `[ ]`
| MEM-12 | Voice actions (OS automation) | [x] | [x] | [x] | [x] | Desktop `VoiceActionService`; web `ActionRunner` (simulated); mobile `ActionsScreen` |
| MEM-13 | Cross-surface data sync | [~] | [x] | [x] | [x] | **Desktop rewritten 2026-09-27** (`services/sync/SyncEngine.ts`): persistent outbox (offline/restart safe), server-clock keyset cursors per user DB with pagination, pending local edits never overwritten by pull, deletions both ways through `sync_tombstones`, per-row failure isolation, Realtime that actually connects (`ws` transport). Covers history (+title/favorite), dictionary, meetings, meeting memos/documents, memo tags, custom commands, dictation + meeting-doc templates; plus device registration. Evidence: `apps/desktop/tests/main/sync/SyncEngine.test.ts` (20) + `apps/desktop/tests/integration/cross-device-sync.supabase.test.ts` (5, local Supabase stack). `[~]` until migration `20260927000034` is deployed to production and a desktop build is exercised against a phone (GAP-SYNC-05); knowledge/audio/settings still local (GAP-SYNC-06) **2026-09-27 second batch:** knowledge base, recordings (upload + remote playback) and the mobile `user_settings` fields that mean the same thing on both sides (ko/en locale, system/light/dark theme, auto-polish on/off, active user command) now sync too; remote setting changes reach the open window (`app:dataChanged`, `config:changed`). Production DB is at migration `0035` and all Edge Functions are redeployed from the repo |
| MEM-14 | Memo tag search | [x] | [ ] | [x] | [x] | |
| MEM-15 | Unified record + meeting search | [~] | [ ] | [~] | [-] | Shared `@d3ro/core/types` result contract. Desktop: SQLite `UnifiedSearchService` → `unifiedSearch:query` → `UnifiedSearchPage`, source/focused tests GREEN but no Electron manual proof. Mobile: user-scoped Supabase history+meetings search, service/row tests plus 320dp dark and 411dp light runtime journeys (both result types → details) GREEN. `[~]` remains because separate `transcripts` segments are absent, mobile fetches raw text without in-flight cancellation, and neither surface has FTS/index or large-data performance proof (`11` GAP-UX-05; `docs/evidence/wave45-mobile-shell-search-2026-09-28.md`). |
---
@ -201,9 +202,9 @@ end-to-end behaviour has **not been verified by typing in a real app** (`11` GAP
| SHELL-10 | Download center / releases | [-] | [x] | [-] | [x] | Web `/download`, admin `/releases`, Forgejo |
| SHELL-11 | Landing site / legal pages | [-] | [-] | [-] | [-] | `site/` — the only landing/download/legal/invite/assetlinks surface (Wave 3, 2026-09-26: copies in api-server wwwroot and apps/web removed; 404.html; hero loops a copy of the app recording capsule). Prices/URLs from `packages/core` plan-catalog/web-urls — see `design.md` |
| SHELL-12 | Notifications (in-app / desktop) | [x] | [ ] | [x] | [x] | Desktop events; mobile push |
| SHELL-13 | Desktop information architecture (2026-09-28) | [x] | [-] | [ ] | [-] | 1급 홈·기록·회의 + 2급 "도구"(AI 대화·명령어·사전·지식) + 설정. `renderer/navigation/app-navigation.tsx`(라우트·params·back, Ctrl+1..3 / Ctrl+,), `components/shell/AppSidebar.tsx`(버튼 + aria-current, 아래 한 줄 = 조회한 상태만), 상태바 삭제. 모바일 동일 구조는 물결 ④(`11` GAP-UX-04). 결정 정본: 루트 `design.md` 2026-09-28 |
| SHELL-14 | Home ("오늘") | [x] | [-] | [ ] | [-] | 대시보드를 대신한다: 받아쓰기 키 · 새 회의·자막 시작 · 조회된 문제만 · 한 줄 요약 · 최근 기록 6/회의 4 · 파일 전사 · 무료 한도. `pages/HomePage.tsx` |
| SHELL-15 | Design system v4 "조용한 계기" | [x] | [~] | [~] | [-] | `packages/ui/src/theme-raw.ts` 원시값 SSOT(렌더러 theme.ts·팝업 theme-vars.ts 가 파생), 평면 표면·텍스트 3단 AA·accent.solid/ink. 웹·관리자는 토큰 파생으로 평면화만 적용, 모바일(ui-native) 동기화는 물결 ④ |
| SHELL-13 | Desktop/mobile information architecture (2026-09-28) | [x] | [-] | [x] | [-] | 1급 홈·기록·회의 + 2급 도구. Desktop: `renderer/navigation/app-navigation.tsx`, `AppSidebar`, status bar removed. Mobile: visible Home/Records/Meetings/More tabs, hidden Record route, floating three-action Record FAB, More tool/account grouping; full Jest and the canonical 320dp dark/411dp light runtime journeys are GREEN (`11` GAP-UX-04). `DashScreen`/`WorksHubScreen` were removed; WorksHub-only local filter/pinning was intentionally retired while every destination remains in tabs/More and user content is untouched. Decision SSOT: root `design.md`. |
| SHELL-14 | Home ("오늘") | [x] | [-] | [x] | [-] | Desktop `pages/HomePage.tsx`: recording key, starts, blockers, summary, recent 6/4, file transcription, bounded usage. Mobile `HomeScreen`: today summary, recent records 5/meetings 3, unified-search and plan entry; canonical 320dp dark and 411dp light runtime journeys GREEN. |
| SHELL-15 | Design system v4 "조용한 계기" | [x] | [~] | [~] | [-] | `packages/ui/src/theme-raw.ts` raw-value SSOT; flat surfaces, three text levels with AA contrast, accent.solid/ink. `packages/ui-native` mirrors dark/light semantics and exposes 48dp control/numeral/motion tokens; focused parity/provider/header tests plus 320dp dark/411dp light runtime are GREEN. `[~]` remains because actual TalkBack, font-scale, and rotation proof is absent; `ScreenHeader` covers new/converted v4 flows, not every historical stack screen. |
---

View file

@ -37,8 +37,8 @@ Legend: `[ ]` open · `[~]` in progress · `[!]` blocked externally · `[x]` res
| GAP-UX-01 | Desktop | 회의 "내 메모"(상세 탭)는 이 기기 localStorage 에만 저장된다 — 기기 간 동기화되지 않는다. 끝난 회의에는 addMemo 가 거부된다(`MeetingModeService.ts:383`). | `components/meeting/meeting-notepad-storage.ts`, `MeetingModeService` | `[ ]` 2026-09-28: 끝난 회의의 사용자 노트를 받을 필드(또는 종료 후 addMemo)와 동기화 어댑터가 필요. |
| GAP-UX-02 | Desktop | 기록 조회에 모드·즐겨찾기 조건, 태그+검색 조합이 없어 화면이 불러온 페이지 위에서 거른다(조건이 까다로우면 여러 페이지를 불러와야 보인다). 오프셋 페이지라 사이 삽입·삭제 때 누락·중복 가능. 기록 제목·본문 편집 없음. 마크다운 내보내기는 태그 붙은 기록만·저장 위치 선택 불가. | `HistoryService`, `history:getAll`, `MemoService.exportMarkdown` | `[ ]` 2026-09-28 |
| GAP-UX-03 | Desktop | 회의 요약 항목 → 전사 구간 근거 링크 없음(요약·전사를 잇는 인용 데이터가 없다). | `MeetingSummaryService`, 회의 문서 생성 | `[ ]` 2026-09-28: 레퍼런스(Granola·Otter)의 핵심 패턴 — 생성 시 구간 인용을 함께 저장해야 한다. |
| GAP-UX-04 | Mobile | 모바일 정보 구조 개편(탭 4 홈·기록·회의·더보기 + 떠 있는 녹음 버튼의 시작 메뉴) 미착수. | `apps/mobile-rn/src/navigation/TabNavigator.tsx` | `[ ]` 2026-09-28: 물결 ④. |
| GAP-UX-05 | Desktop+Mobile | 기록+회의 통합 검색 없음(회의 전사는 어떤 검색으로도 못 찾는다). | 렌더러·모바일 검색, `MeetingModeService` | `[ ]` 2026-09-28: 물결 ⑤. 스키마 변경 없이 조회 추가. |
| GAP-UX-04 | Mobile | 모바일 정보 구조 개편: visible Home/Records/Meetings/More 4탭, 숨은 Record route, 세 동작 FAB, Home/More, 새/전환된 v4 흐름의 `ScreenHeader`와 48dp back target. | `apps/mobile-rn/src/navigation/TabNavigator.tsx`, `components/{RecordFab,ScreenHeader}.tsx`, `screens/{Home,More}Screen.tsx` | `[x]` 2026-09-28: 전체 Jest 86 suites/589 tests, 320dp dark 1/1 PASS(88초), 411dp light 1/1 PASS(100초). Canonical 보고서는 `scratch/design-audit/wave45/navigation-shell-search-{320-dark,411-light}-verified.xml`, 추적 요약은 `docs/evidence/wave45-mobile-shell-search-2026-09-28.md`. `DashScreen`/`WorksHubScreen`은 삭제했고 WorksHub의 로컬 도구 검색·고정은 복잡도 축소를 위해 의도적으로 종료했다. 목적지는 탭/More에 보존됐고 콘텐츠 삭제는 없으며 비활성 `works-hub-pins.v1` 키는 남을 수 있다. TalkBack/font-scale/rotation은 이 셸 gap을 다시 열지 않고 Q-021 `[~]`로 추적한다. |
| GAP-UX-05 | Desktop+Mobile | 기록+회의 통합 검색. 두 원본은 분리 유지하고 결과만 `source + id`로 상세 화면에 연결한다. | `@d3ro/core/types`, desktop `UnifiedSearchService`/`UnifiedSearchPage`, mobile `features/search`/`UnifiedSearchScreen` | `[~]` 2026-09-28: 모바일 service/row tests와 320dp dark·411dp light에서 기록·회의 결과→각 상세 경로가 GREEN. 데스크톱은 서비스·IPC·preload·페이지와 focused tests까지만 있으며 Electron `Ctrl+K`/결과 이동 수동 증거가 없다. 회의 검색은 `meetings.raw_transcript`/`edited_transcript` 집계 필드까지라 별도 `transcripts` 구간이 빠져 있다. 모바일은 일치 확인에 원문 필드를 내려받아 과전송하고 실행된 요청 취소가 없다. 두 표면 모두 FTS/index와 실제 대규모 데이터 성능 증거가 없다. Electron 수동 검증, 구간 검색 설계, 모바일 projection/RPC·취소, 검색 규모 기준과 perf proof 뒤에 표면별 `[x]`를 판단한다. |
| GAP-UX-06 | Desktop | main 쪽 정리 필요: `STTManager.ts:53-129` 공급자 설명 한국어 하드코딩·마케팅 문구("100% 완전 오프라인", "Industry Benchmark") — 렌더러는 더 이상 쓰지 않는다 / audio TEST_DEVICE 가 deviceId 무시 / 사전 `incrementUsage` 호출처 없음(사용 횟수 항상 0) / 사전 발음 필드가 인식 힌트·치환에 쓰이지 않음 / 대화 세션 다시 듣기 IPC 없음 / preload `instruction.getAll` 이 `unknown[]` / 지식 추가 실패 판정이 문구 비교. | 각 파일 | `[ ]` 2026-09-28 (도구·설정 개편 에이전트 보고) |
| GAP-UX-07 | Desktop | 2026-09-28 개편 문구는 ko·en 만 — 다른 8개 로케일은 en 폴백. 온보딩 Ollama 모델 크기 표기(`onboarding.llmModelSize` 9.6GB)와 안내 모달의 크기가 다르다. | `packages/i18n/src/locales/*` | `[ ]` 2026-09-28 |
| GAP-TEAM-02 | Team | 브라우저에서 초대를 수락하는 경로가 없다. 웹 `accept-invite` 페이지는 발급 링크가 가리키지 않고 로그인 후 토큰을 읽는 곳이 없어 끊겨 있었으므로 삭제했다(사이트 `/accept-invite/`는 앱 딥링크만). | `site/public/accept-invite/`, `server/supabase/functions/team-accept` | `[ ]` 2026-09-26: 데스크톱 전용 사용자를 위한 웹 수락 흐름이 필요하면 `/app` 아래에 다시 설계. |
@ -119,14 +119,14 @@ These are the mobile SSOT rows still `[ ]` / `[~]`. Do not duplicate the full te
|---|---|---|
| G (governance) | G-002, G-006, G-007, G-008, G-009, G-010, G-011 | Type/error/identity consolidation, legacy bundle separation, credential rotation |
| A (auth/account) | A-001..A-013, A-018; A-014..A-017 `[~]` | Session restore, signup, deep links, OAuth, profile, provider E2E |
| O (onboarding/a11y) | O-001..O-012 | Onboarding branches, permissions, tutorial, a11y, full ko/en |
| O (onboarding/a11y) | O-001..O-007, O-009, O-011; O-008/O-010/O-012 `[~]` | Onboarding branches, permissions, tutorial; app-wide theme coverage, TalkBack/font-scale/rotation; full-app ko/en audit |
| D (data/sync/offline) | D-001..D-014 | Schema/RLS, devices, cross-surface read/write, offline queue, conflict policy |
| R (record/transcribe) | R-001..R-009, R-011..R-015, R-017..R-020; R-016 `[x]` | Permissions, recorder, FGS, upload queue, job state, sharing |
| H (history/meetings) | H-001..H-005, H-007..H-015; H-006 `[x]` | Pagination, filters, detail, meeting timeline, docs, diarization, cross-app |
| F (feature parity) | F-001..F-010, F-013, F-014; F-011/F-012 `[x]` | Dashboard, dictionary, commands, actions, chat, conversation, knowledge |
| H (history/meetings) | H-001..H-005, H-007..H-015; H-016 `[~]`; H-006 `[x]` | Pagination, filters, detail, meeting timeline/docs/diarization/cross-app; unified-search query/cancellation/performance gaps |
| F (feature parity) | F-003..F-010, F-013, F-014; F-001 `[~]`; F-002/F-011/F-012 `[x]` | Mobile Home dashboard subset, commands/actions/chat/conversation/knowledge |
| T (teams/admin/notify) | T-001..T-005, T-009..T-012; T-006..T-008, T-013 `[x]` | Team CRUD/roles, push delivery/deep links, notification settings |
| M (monetization) | M-025, M-027; M-013 `[~]` | Play Billing license test, Payple webhook signature |
| Q (quality/release) | Q-012..Q-014, Q-019..Q-022, Q-026; Q-010/Q-011/Q-023/Q-027/Q-028 `[~]` | Emulator scripts, OAuth E2E, billing E2E, visual/a11y gates, env unification, production AAB |
| Q (quality/release) | Q-012..Q-014, Q-019, Q-020, Q-022, Q-026; Q-010/Q-011/Q-021/Q-023/Q-027/Q-028 `[~]` | Emulator scripts, OAuth/billing E2E, full journey; TalkBack/font-scale/rotation, env unification, production AAB |
---

View file

@ -9,6 +9,7 @@
> 제품화 재개 핸드오프: [`MOBILE_PRODUCTIZATION_HANDOFF_2026-08-21.md`](./MOBILE_PRODUCTIZATION_HANDOFF_2026-08-21.md)
> 이 문서는 이전 `docs/v3/00-mobile-master-plan.md`의 미래 로드맵을 대체하는 구현·검증 정본이다.
> 2026-09-26: `.github/workflows`는 삭제되었고 CI는 `.forgejo/workflows`가 정본이다 — 아래 날짜별 기록에 남은 `.github/workflows` 언급은 해당 시점의 스냅샷이다.
> 2026-09-28 Wave 4/5 로컬 증거: [`../evidence/wave45-mobile-shell-search-2026-09-28.md`](../evidence/wave45-mobile-shell-search-2026-09-28.md). 이 E2E APK와 런타임 보고서는 production release 증거가 아니다.
## 0. 2026-09-16 출시 게이트 스냅샷
@ -94,14 +95,18 @@
| 묶음 | 현재 판정 | 확인된 증거 / 남은 게이트 |
|---|---|---|
| Auth·계정 | 구현·자동화 GREEN, 외부 OAuth·privacy purge 기기 E2E 대기 | auth/signup/invite 경계는 최신 exact APK `.11`에서 PASS. Keychain 세션, 중앙 `purgeAllAccountLocalData`, cold no-session·SIGNED_OUT·refresh session loss·A→B 전환·실패 재시도 경계는 전체 모바일 57 suites/353 tests에 포함돼 GREEN. 실제 token-expiry 강제 로그아웃·logout/delete/provider 연결/해제 기기 E2E와 Google/GitHub/Apple 동의→callback은 남음 |
| 온보딩·테마 | 구현·기기 journey GREEN | 신규/기존 분기, skip/replay, 계정별 preference merge, system/light/dark, reduced motion, 알림·마이크 권한 상태 구현. production preference `0→skip→1`, Android PID 유지, 5.8초 내 Main, navigation warning 0을 실측 |
| 온보딩 | 기존 구현·기기 journey GREEN | 신규/기존 분기, skip/replay, 계정별 preference merge, 알림·마이크 권한 상태 구현. production preference `0→skip→1`, Android PID 유지, 5.8초 내 Main, navigation warning 0을 실측. 이 과거 증거를 2026-09-28 v4 셸의 전 테마·viewport 증거로 재사용하지 않는다 |
| v4 셸·테마·접근성 | 셸 `[x]`, 디자인 시스템·접근성 `[~]` | visible Home/Records/Meetings/More 4탭, hidden Record route, 세 동작 FAB, Home/More, 새/전환된 v4 흐름의 `ScreenHeader`, ui-native dark/light 의미 토큰과 48dp control을 구현했다. 전체 Jest 86 suites/589 tests, 320dp dark 1/1 PASS(88초), 411dp light 1/1 PASS(100초). 기존 stack 화면 전부의 헤더 전환을 뜻하지 않으며 실제 TalkBack/font-scale/rotation은 남음 |
| 녹음·전사·Talk | 핵심 경로 GREEN | API 34 microphone FGS, 지속 알림, pause/resume/discard, 실제 통화 audio-focus, 프로세스 kill 후 WAV 복구, 파일 import, cloud STT fail-closed, on-device Whisper, Claude SSE, Android TTS를 통합 APK에서 확인. STT provider 호출 전 원자 quota reservation·실패 환불은 20-way 동시성으로 검증. Android `ACTION_SEND` 수신은 허용 MIME·크기·provider 예외·MP4 AAC 추출을 fail-closed로 구현했고 API 34 instrumentation 4/4(CSPRNG 1 + share 3)와 별도 UID 외부 ContentProvider APK→`ACTION_SEND`→Record→durable queue→cancel cleanup E2E가 GREEN |
| 데이터 portability | 구현·공유 E2E GREEN | 00021 atomic export/restore, 손상/타 사용자/충돌 rollback, legacy import를 local integration으로 확인. `portable_exports`→FileProvider `shared_exports` 경계와 PDF Print/DOCX chooser·30초 cleanup을 실제 기기에서 확인 |
| History·회의 | 실제 데이터·회의 생성 계약 GREEN | Supabase history pagination/search/favorite/CAS, Web↔mobile history E2E, meetings/memos/documents RLS/CAS, meeting recording lifecycle 구현. 00025의 제목·참석자·4개 언어·소유 템플릿 원자 생성은 2계정 43 assertions와 competing-payload PT409로 검증. speaker/audio timestamp와 최종 교차 앱 E2E는 남음 |
| 기록+회의 통합 검색 | `[~]` source/full Jest + 320dp dark·411dp light runtime GREEN | 사용자 범위 history title/original/polished/summary와 meetings title/raw/edited transcript를 병렬 검색해 최신순으로 합치고 각 상세로 이동한다. 두 viewport/theme에서 두 source→detail을 실측했다. 다만 별도 `transcripts` 구간은 검색하지 않고, 모바일 raw-text 과전송·실행 요청 미취소, FTS/index·대규모 성능 미검증이 남음 |
| Teams·Push | 팀 GREEN, 외부 Push 대기 | production 17 assertions와 local 36 assertions로 원자 팀/초대/RLS/role/device token 계약 확인. durable push outbox 00015~00020과 logout device `1→0` 확인. 실제 FCM 수신은 Firebase/FCM 자격 증명·cron 배포가 필요 |
| Billing·Ads | test 광고와 production AdMob identity/SSV 준비, store·실제 지급 E2E RED | Google Play restore는 실제 store list→모든 token 서버 검증→entitlement snapshot 일치 후에만 성공. Google 공식 test adaptive banner와 rewarded `AdActivity` 완주를 실측했다. production app `ca-app-pub-1039714767792854~6427959892`, banner `/9840591290`, rewarded `/2255790918`, `50 cloud_ai_tokens`, SSV URL, Edge v13, durable receipt 원장은 SSOT에 반영했다. 그러나 live는 `검토 필요`, `광고 게재 제한`, store 미연결, 결제 프로필 미완료다 |
| Admin | 서버·Web·모바일 E2E와 production DB·Edge 배포 GREEN | .NET 21/21, 00023 관리 integration 25 assertions, Edge/JWT 47 assertions와 TLS Chromium E2E GREEN. 모바일 API 34에서 ordinary/manager/admin/super_admin 및 stale 강등을 55 assertions로 실측: Free test 광고, Pro 광고 미요청, 관리자 메뉴/API 차단, 구독 수정, 역할 경계·변경, 삭제 확인·취소, stale JWT 403와 민감 state purge가 모두 PASS. fixture 잔존 0. production 00023 및 admin Edge 4개 배포 후 익명 GET은 모두 `401 authentication_required` + `Cache-Control: no-store`로 재검증 |
| 품질·배포 | exact-APK 에뮬레이터 gate GREEN, production 배포 RED | mobile ESLint 0, typecheck GREEN, 전체 Jest 57 suites/353 tests GREEN은 2026-08-24 실행 증거다. universal non-debuggable TEST-DEMO `0.0.0-e2e.20260821.11`/`202608221`을 API 34에 fresh install해 cold launch·auth/signup/invite를 검증했지만 production artifact가 아니다. 2026-08-29 release identity `1.1.0`/`1010001`, local upload key, Ed25519 evidence keypair, AdMob identity는 확정했지만 production Firebase·CI secret 주입·AAB·Play track·Fold6 Play 설치 증거는 없다 |
| 품질·배포 | current local source/runtime gate GREEN, production 배포 RED | 2026-09-28 현재 tree 전체 Jest 86 suites/589 tests GREEN. 로컬 E2E APK는 `com.d3ro.voice`, `1.9.0-e2e.20260928.2`/`202609282`, 147063583 B, SHA-256 `96EBA2A47E54033EB0DEFCE4E4D282F1D06F162A5F7DF9457E99609D98745D58`이며 두 canonical Maestro 보고서가 GREEN이다. 이것은 production-signed artifact가 아니므로 Firebase·CI secret 주입·AAB·Play track·Fold6 Play 설치 게이트는 그대로 RED다 |
**v4 제거 범위:** `DashScreen`과 `WorksHubScreen`을 삭제했다. Home과 1급 탭/More가 기존 목적지를 보존하며 사용자 콘텐츠는 삭제하지 않았다. WorksHub 전용 로컬 도구 검색·고정은 복잡도 축소를 위해 의도적으로 종료했고, 비활성 `works-hub-pins.v1` AsyncStorage 키는 업그레이드 설치에 남을 수 있다.
## 3. 플랫폼 동등성 원칙
@ -164,11 +169,11 @@
- [ ] O-005 첫 녹음→전사→공유를 안내하는 단계별 튜토리얼
- [ ] O-006 주요 탭과 파일 가져오기·동기화·결제 관리 튜토리얼
- [ ] O-007 튜토리얼 다시 보기·건너뛰기·완료 상태 동기화
- [ ] O-008 system/light/dark 테마와 D3RO 토큰 기반 렌더링
- [~] O-008 system/light/dark 테마와 D3RO 토큰 기반 렌더링 — ui-native v4 dark/light palette parity·provider/component Jest, 320dp dark·411dp light runtime GREEN. 새/전환된 v4 흐름 밖의 앱 전체 테마 적용 감사까지 완료했다는 뜻은 아님
- [ ] O-009 테마·언어·햅틱·자동 교정·STT 옵션 저장 및 기기간 동기화
- [ ] O-010 글자 확대, TalkBack label/role, focus order, 48dp 터치 영역, 색 대비 검증
- [~] O-010 글자 확대, TalkBack label/role, focus order, 48dp 터치 영역, 색 대비 검증 — touched controls/ScreenHeader 48dp와 theme contrast source tests GREEN, 실제 TalkBack·focus order·font scale·rotation은 대기
- [ ] O-011 reduced motion과 애니메이션 비활성 경로
- [ ] O-012 한국어/영어 필수 전체 번역과 하드코딩 문자열 제거
- [~] O-012 한국어/영어 필수 전체 번역과 하드코딩 문자열 제거 — v4 셸·FAB·통합 검색과 touched enum label은 ko/en typed keys 사용, 앱 전체 감사 대기
### D — 데이터 가져오기·동기화·오프라인
@ -229,10 +234,11 @@
- [ ] H-013 회의 목록 search/filter/sort/delete/share
- [ ] H-014 화자 구분 상태·화자명 편집·미지원 fallback
- [ ] H-015 Web↔모바일 동일 회의 교차 편집 E2E
- [~] H-016 홈 기록+회의 통합 검색 — 사용자 범위 history/meetings 병렬 조회, 입력 정규화·최신순 merge·record/meeting 상세 이동 service/row Jest와 320dp dark·411dp light runtime GREEN. 별도 transcript segment 미검색, 모바일 raw-text 과전송·실행 요청 미취소, FTS/index·대규모 성능 미검증 때문에 부분 완료
### F — 데스크톱/Web 전체 기능의 모바일 화면
- [ ] F-001 Dashboard 통계·사용량·처리 job·구독·동기화 상태를 실제 데이터로 표시
- [~] F-001 Home 통계·사용량·처리 job·구독·동기화 상태를 실제 데이터로 표시 — 실제 dashboard stats + 최근 기록 5/회의 3 + Free plan 진입 구현, 320dp dark·411dp light runtime GREEN. 처리 job·동기화 상태 전체 범위는 대기
- [x] F-002 Dictionary 목록/검색/추가/편집/삭제/import/export/sync — CRUD/sync GREEN, export는 `data-portability` CSV/TXT, import는 CSV/JSON/TXT
- [ ] F-003 Custom instructions 목록/활성화/CRUD/reorder/sync
- [ ] F-004 Voice commands 목록/활성화/CRUD와 모바일 실행 가능한 action 구분
@ -316,8 +322,8 @@
- [x] Q-017 Web seeded 데이터→모바일 조회/편집→Web 확인 sync E2E
- [x] Q-018 Free banner/rewarded test ad와 Pro 광고 미요청 E2E — official Google test ad 실측, ordinary Free 광고 노출과 manager/admin Pro 광고 비노출·미요청 API 34 PASS
- [ ] Q-019 Play Billing test purchase/restore/manage E2E
- [ ] Q-020 모든 탭·설정·팀·관리·실패 복구 journey E2E
- [ ] Q-021 회전·폰트 확대·TalkBack·dark/light·작은/큰 화면 visual gate
- [ ] Q-020 모든 탭·설정·팀·관리·실패 복구 journey E2E — v4 4탭/FAB/More/통합 검색의 320dp dark·411dp light 부분 journey만 GREEN; 전체 범위를 대신하지 않음
- [~] Q-021 회전·폰트 확대·TalkBack·dark/light·작은/큰 화면 visual gate — 이 v4 journey의 320dp dark·411dp light는 GREEN; 회전·폰트 확대·TalkBack 실기기 증거는 대기
- [ ] Q-022 airplane mode·process kill·token expiry·server 5xx·중복 tap 회복 E2E
- [~] Q-023 앱 로그 crash/ANR/React error/민감정보 0건 확인 — exact `.9` auth/signup/invite/small-screen 및 admin role journeys에서 fatal/ANR/React fatal 0, production provider journey 민감정보 로그 감사 대기
- [x] Q-024 APK package/version/signature/SHA-256와 설치된 앱 버전 증거
@ -361,10 +367,14 @@
| Historical baseline | `git status --short --branch` | 2026-08-21 당시 `main`이 remote보다 2 commit 앞서고 dirty 변경이 다수였다. 현재 release SHA로 재사용하지 않는 역사 증거다 | 본 문서 1절 | 2026-08-21 |
| Mobile lint | `npm --prefix apps/mobile-rn run lint` | GREEN, warning/error 0 (`--max-warnings=0`) | `apps/mobile-rn/.eslintrc.js`, `package.json` | 2026-08-21 |
| Mobile typecheck | `npm --prefix apps/mobile-rn run typecheck` | GREEN | `apps/mobile-rn/tsconfig.json` | 2026-08-21 |
| Mobile unit | `npm --prefix apps/mobile-rn test` | 전체 통합 tree 57 suites / 353 tests GREEN; 중앙 account-local privacy purge, cold/session-loss/A→B auth 경계, secure logout, identity safety, mobile admin, rewarded/banner retry·template/memo·meeting creation·recording language·generation idempotency 포함 | `apps/mobile-rn/__tests__` | 2026-08-24 |
| Historical mobile unit | `npm --prefix apps/mobile-rn test` | 당시 전체 통합 tree 57 suites / 353 tests GREEN; 현재 tree 수치가 아님 | `apps/mobile-rn/__tests__` | 2026-08-24 |
| Current mobile unit | `npm --prefix apps/mobile-rn test` | 전체 tree **86 suites / 589 tests PASS**. navigation shell/header/theme와 unified-search service/result-row 계약을 포함 | `apps/mobile-rn/__tests__`, `docs/evidence/wave45-mobile-shell-search-2026-09-28.md` | 2026-09-28 |
| Wave 4/5 local runtime APK | output metadata + file size/SHA-256 + installed/runtime identity 확인 | `com.d3ro.voice`, versionName `1.9.0-e2e.20260928.2`, versionCode `202609282`, 147063583 B, SHA-256 `96EBA2A47E54033EB0DEFCE4E4D282F1D06F162A5F7DF9457E99609D98745D58`. 로컬 E2E artifact이며 production-signed APK/AAB가 아니다 | `apps/mobile-rn/android/app/build/outputs/apk/e2e/{app-e2e.apk,output-metadata.json}`, `docs/evidence/wave45-mobile-shell-search-2026-09-28.md` | 2026-09-28 |
| Mobile v4 320dp dark runtime | `apps/mobile-rn/.maestro/navigation-shell-search-local.yaml` | **1/1 PASS, 0 failure, 88초**: visible 4 tabs, FAB open/back close, Home unified search, record+meeting results→both details, Records/Meetings/More navigation | `scratch/design-audit/wave45/navigation-shell-search-320-dark-verified.xml`, `docs/evidence/wave45-mobile-shell-search-2026-09-28.md` | 2026-09-28 |
| Mobile v4 411dp light runtime | same journey on 411dp light | **1/1 PASS, 0 failure, 100초**. 두 보고서는 shell/viewport/theme runtime proof이며 TalkBack/font-scale/rotation 또는 production release proof가 아니다 | `scratch/design-audit/wave45/navigation-shell-search-411-light-verified.xml`, `docs/evidence/wave45-mobile-shell-search-2026-09-28.md` | 2026-09-28 |
| Supabase local | migration-up + shadow replay 00001~00028 및 domain integration | 기존 domain integration에 더해 AdMob receipt 원장의 unknown/ineligible/cooldown replay, account-delete unlink, ACL, 교차 사용자·동시 요청을 검증했다. mobile-platform 85/85, reward race 2회 연속, SSV Deno 6/6, db lint error 0, shadow diff empty, fixture cleanup 0 | `server/supabase/migrations/20260821000028_ad_reward_receipt_replay_protection.sql`, `server/supabase/tests`, `server/supabase/functions/_shared/admob-ssv.test.ts` | 2026-08-24 |
| Android build | `:app:assembleDebug -PreactNativeArchitectures=arm64-v8a,x86_64` | universal BUILD SUCCESSFUL; 두 ABI의 screens/gesture/Nitro/Whisper SO 완전성 확인 후 x86_64 clean cold launch GREEN, SHA-256 `E4647A4C041174DF2B2F094EF7BB61B4B01DD10A379DB32CCBE4105231E04A7C` | `apps/mobile-rn/android/app/build/outputs/apk/debug/app-debug.apk`, `scratch/demo/d3ro-mobile-dual-abi-installed.png` | 2026-08-21 |
| Latest Android E2E test artifact | JDK 17 `:app:assembleE2e -PreactNativeArchitectures=arm64-v8a,x86_64` + artifact/build-config verifiers | BUILD/verifier GREEN; `com.d3ro.voice`, version `0.0.0-e2e.20260821.11`/`202608221`, non-debuggable, embedded bundle 3,808,068 B·Whisper model 77,691,713 B/SHA 검증, Google test AdMob ID, arm64+x86_64. APK 146,739,935 B, SHA-256 `74035B69DB0A564846DF5ED4B85CA5D0E2289D5BB7D73DEE02C35DFD57380C2B`. API 34 exact-APK fresh install와 cold launch GREEN. production artifact가 아니다 | `apps/mobile-rn/android/app/build/outputs/apk/e2e/app-e2e.apk`, `scripts/ci/verify-android-artifact.mjs`, `scripts/ci/verify-mobile-build-config.mjs` | 2026-08-21 |
| Historical Android E2E test artifact | JDK 17 `:app:assembleE2e -PreactNativeArchitectures=arm64-v8a,x86_64` + artifact/build-config verifiers | 당시 BUILD/verifier GREEN; `com.d3ro.voice`, version `0.0.0-e2e.20260821.11`/`202608221`, APK 146,739,935 B, SHA-256 `74035B69DB0A564846DF5ED4B85CA5D0E2289D5BB7D73DEE02C35DFD57380C2B`. 위 2026-09-28 artifact로 대체됐지만 당시 auth/cold-launch 증거의 identity로 보존한다. production artifact가 아니다 | historical installed/runtime evidence, `scripts/ci/verify-android-artifact.mjs`, `scripts/ci/verify-mobile-build-config.mjs` | 2026-08-21 |
| Public Forgejo distribution | 최신 E2E TEST-DEMO 고유 asset upload + anonymous redownload hash 대조 | PENDING; `.11` SHA-256 `74035B69…380C2B` 공개 업로드·anonymous redownload 검증은 수행하지 않음 | Forgejo release asset | 2026-08-21 |
| Mobile release boundary | `npm run release:mobile:boundary:test` + source-contract/security/syntax/YAML checks | GREEN; release mode·expected version/package/cert/prod AdMob·Ed25519 signed evidence와 artifact hash 일치, immutable snapshot, hardlink/reparse/path-swap/static APK 우회·overwrite·test AdMob/debug signer/nonrelease 거부를 검증. 실제 production APK/AAB에는 아직 실행하지 않음 | `scripts/ci/mobile-release-evidence-lib.mjs`, `scripts/ci/verify-mobile-release-boundary.mjs`, `.github/workflows/release.yml` | 2026-08-21 |
| Production Android release gate | release/config/artifact/provenance self-tests + Play/Firebase/AdMob live 재확인 | release identity `1.2.0`/`1020001`, Play app ID `4976102237698469110`, package `com.d3ro.voice`, app-signing SHA-256, local upload/evidence key, AdMob identity를 확인했다. Play는 `임시`, 설정 0/11, AAB 0건, closed tester 0/12명, production access disabled다. Firebase project·CI secret 주입·AAB가 없으므로 현재 판정은 RED다 | `release/product-version.json`, `release/android-release-identity.json`, `scripts/ci/verify-mobile-release-config.mjs`, `scripts/ci/verify-android-artifact.mjs`, live consoles | 2026-08-29 |