232 lines
74 KiB
Markdown
232 lines
74 KiB
Markdown
# 11 — Gap & Backlog Register
|
||
|
||
> The maintained list of what is **under-developed, deferred, or externally blocked**.
|
||
> Status: living document. Every feature change updates this file (see [`12-update-protocol.md`](./12-update-protocol.md)).
|
||
> External items are marked `EXT`; they block "done" but must not block code, tests, or local fixtures.
|
||
|
||
Legend: `[ ]` open · `[~]` in progress · `[!]` blocked externally · `[x]` resolved (keep for one cycle, then prune).
|
||
|
||
---
|
||
|
||
## 0. How to read this
|
||
|
||
- An item here is **not** a failure. It is a known state with an owner and a next step.
|
||
- When you close an item, flip it to `[x]`, add the date + evidence path, and also update `10-feature-catalog.md`.
|
||
- Grandfathered detail lives in `docs/v3/MOBILE_APP_COMPLETION_SSOT.md`; this file is the cross-surface roll-up. When the two disagree, the SSOT wins for mobile and must be reconciled here.
|
||
- **Not everything imperfect is a gap.** Trade-offs that were reviewed and deliberately kept live in §7 as constraints, not in §1. Check §7 before opening a row for one.
|
||
|
||
---
|
||
|
||
## 1. High impact — real capability gaps (no external blocker)
|
||
|
||
| ID | Area | Gap | Evidence | Suggested next step |
|
||
|---|---|---|---|---|
|
||
| GAP-QA-01 | Quality | Extreme Red Team: headful end-to-end bug hunting across real desktop Electron, Web Next.js, and CI pipelines. | `red_team_log.md`, `tests/e2e/red_team_cycle*.spec.ts`, `apps/web/e2e/red_team_cycle4_web.spec.ts` | `[x]` 2026-09-15: 18 scenarios executed, 14 defects caught and 100% resolved (infinite chunking loop DEF-008, IPC signature mismatch DEF-004, markdown editor typing rollback DEF-006, Web RSC Link serialization DEF-012, secret scanner lookahead DEF-013, etc.). All 18 scenarios GREEN with zero regressions. |
|
||
| GAP-REL-01 | Release | Official release publication to Forgejo and active public download center deployment. | `scripts/ci/publish-forgejo-release.mjs`, `apps/web/src/app/download/page.tsx`, `site/src/sections/Download.tsx`, `apps/web/e2e/red_team_cycle4_web.spec.ts` | `[~]` 2026-09-15: v1.1.0 release assets (`D3RO-Voice-Setup-1.1.0-x64.exe`, `.blockmap`, `latest.yml`, `update-policy.json`) published to canonical Forgejo registry and release hub. 2026-09-16: the published 1.1.0 installer carries no Authenticode signature, so it does not satisfy the release policy; product version moved to `1.2.0` and publication must come from CI with the signing gate GREEN. Download centers in `apps/web` (`/download`) and `site` (`#download`) link the canonical Forgejo feed. 2026-09-19: `1.3.7` (overlay fix) is published to the canonical updater feed (`latest.yml` = 1.3.7, 90.6MiB); the CI signing gate that blocks tag-driven publication is still unresolved, so this went out through the local updater path (GAP-REL-06). 2026-09-23: `1.5.0` published the same way (updater feed `latest.yml` = 1.5.0, installer 91.2MiB / 95,612,108 bytes, sha512 matches the local build and the feed serves it anonymously); the runtime and portable channels were republished first. |
|
||
| GAP-REL-08 | Release | 다운로드 센터가 **존재하지 않는 설치 파일**을 가리켰다. `apps/web/src/lib/desktop-release.ts`와 `site/src/release.ts`의 `DESKTOP_VERSION`이 `1.2.0`에 멈춰 있어 설치 URL이 `D3RO-Voice-Setup-1.2.0-x64.exe`였고, 그 경로는 피드에서 404다(실측: 1.2.0=404, 1.3.7=206). `version:sync`가 이 두 표면을 덮지 않아 계속 어긋났다. | `scripts/ci/sync-version.mjs`, `apps/web/src/lib/desktop-release.ts`, `site/src/release.ts` | `[x]` 2026-09-19: 두 다운로드 계약 파일을 `sync-version.mjs` 대상에 추가해 버전·릴리스일이 SSOT에서 자동 반영되도록 하고, 현재 값(1.3.7 / 2026-09-19)으로 정정했다. `version:check`·typecheck·site 빌드 GREEN. |
|
||
| GAP-REL-09 | Release | 랜딩 사이트가 **재배포되지 않는다**. `deploy` 워크플로가 main push마다 실패한다. 실측 원인(run#66 로그): `site/src/sections/Hero.tsx`가 타이머 ref를 `NodeJS.Timeout`으로 타이핑해 `@types/node` 네임스페이스가 필요했고, 배포 잡은 `npm ci --prefix site`만 하므로 조상 `node_modules`의 hoisted 타입이 없어 `tsc -b`가 `TS2503: Cannot find namespace 'NodeJS'`로 실패한다. 그래서 `https://d3ro.chanpaca.net/release-identity.json`이 404다(공개 버전 검증 불가). | `.forgejo/workflows/deploy-site.yml`, `site/src/sections/Hero.tsx` | `[x]` 2026-09-19: ref를 `ReturnType<typeof setTimeout>`으로 바꿔 hoisted 타입 의존을 제거했다(격리 `--typeRoots`로 CI 조건 재현 → 수정 전 TS2503, 수정 후 clean). 같은 수정을 push하자 `deploy` run#67이 사이트 빌드를 통과해 `dist/`를 만들었고, 실패는 다음 단계(Cloudflare)로 이동했다. |
|
||
| GAP-REL-09b | Release | `d3ro.chanpaca.net`이 404였던 직접 원인: 이 Cloudflare 계정에 Pages 프로젝트 `d3ro`/`d3ro-voice`가 **존재하지 않아** 커스텀 도메인 바인딩이 없었다(빈 본문 404, `cf-ray`만 반환). Pages 커스텀 도메인은 존 DNS CNAME(`d3ro → d3ro.pages.dev`)을 요구하는데 기존 `d3ro` 레코드가 남아 있어 `CNAME record not set`으로 pending에 머물렀고, 로컬 wrangler 자격증명에는 DNS 스코프가 없다(403 Authentication error). `deploy-site.yml`은 `CF_API_TOKEN` 시크릿이 없어 마지막 게시 단계에서도 `exit 1`이다. | `server/cloudflare-site-bridge/`, `.forgejo/workflows/deploy-site.yml`, `docs/map/02-infrastructure.md` | `[x]` 2026-09-19: Pages 프로젝트 `d3ro` 생성 + `site/dist` production 배포(`d3ro.pages.dev` 200, `release-identity.json` = commit `2407f5a` / 1.3.7) + 커스텀 도메인 연결. DNS 없이 도메인을 살리기 위해 Workers 라우트 브리지(`server/cloudflare-site-bridge`, `d3ro.chanpaca.net/*` → Pages 프록시, `npx wrangler deploy`)를 배포 → 라이브 확인: `/`·`/privacy/`·`/terms/`·`/delete-account/` 200, 라이브 번들이 설치 파일명을 `1.3.7`로 계산, `/download.html` → `/#download`. 남은 정리 2건: (1) 대시보드에 CNAME을 추가한 뒤 브리지 워커 삭제, (2) CI 자동 게시를 위해 `CF_API_TOKEN`(Pages/Workers Edit) + `CF_ACCOUNT_ID`=`8e83cc130e7329c160cf2b88d6b4c20a`를 Forgejo 시크릿에 등록. **2026-09-23:** CI `deploy`(deploy-site.yml)는 `CF_API_TOKEN` 부재로 run#71까지 여전히 실패한다. 로컬 인증 wrangler(pages:write)로 `npm run build --prefix site` → release-identity 작성 → `npx wrangler pages deploy site/dist --project-name d3ro --branch main`을 수동 배포했고, 라이브 `https://d3ro.chanpaca.net/release-identity.json`이 commit `5c11ee2` / version `1.5.0`을, 라이브 번들이 설치 파일명 `D3RO-Voice-Setup-1.5.0-x64.exe`를 보고한다. 자동 게시에는 여전히 시크릿 등록이 필요하다. |
|
||
| GAP-BILL-01 | Billing | 사이트 가격과 서버 청구 금액·결제 진입 URL이 제각각이었다. | `packages/core/src/plan-catalog.ts`, `packages/core/src/web-urls.ts`, `scripts/ci/sync-core-contract.mjs` | `[x]` 2026-09-26 (Wave 3, 88f24d8·b6fe588): 가격·쿼터·공개 URL 정본을 core 두 파일로 합치고 Deno는 생성 사본(`contract:check`). Payple 함수 4종+billing-catalog 운영 재배포(payple-* v9, billing-catalog v5) → 신규·갱신 모두 ₩2,900/₩8,900. 결제 진입은 `/app/billing` 하나. |
|
||
| GAP-BILL-02 | Billing | **Payple 정기 갱신이 한 번도 실행되지 않았다.** 갱신 cron(`payple-renew`)이 `.github/workflows`에만 있었는데 GitHub 원격이 없다. 웹 Payple 결제용 `NEXT_PUBLIC_PAYPLE_CLIENT_KEY`는 설정된 적이 없다. | `.forgejo/workflows/payple-renew.yml`, `apps/web/src/components/billing/payple-checkout-button.tsx`, `apps/web/Dockerfile` | `[~]` 2026-09-26: 워크플로 Forgejo 이식(dc43884), `CRON_SECRET` 새로 발급해 Supabase 함수 시크릿·Forgejo 시크릿에 등록, Forgejo `SUPABASE_URL` 등록 — main에 push되면 스케줄이 등록된다. **백로그(사용자 결정, 2026-09-27):** 아직 Payple 계약·계정이 없다. 웹 Payple 클라이언트 키(`NEXT_PUBLIC_PAYPLE_CLIENT_KEY`)는 계약 후 `.env`에 넣고 웹 이미지를 다시 빌드한다. 그 전까지 웹 결제 버튼은 꺼진 상태로 둔다. Stripe는 쓰지 않고(2026-09-26 저장소에서 제거, GAP-BILL-03), Google Play 가격은 스토어가 정본이라 사이트에 표시하지 않는다. |
|
||
| GAP-BILL-03 | Billing | **Stripe 결제 제거 — 운영 잔여물 정리.** 사용자 결정(2026-09-26)으로 저장소에서 Stripe 코드·설정·테스트를 모두 지웠다(함수 3종, 웹 checkout/portal 버튼, `billing-catalog`의 Stripe 가격 조회, 데스크톱 `CheckoutModal`·`payment:*` IPC, `billingUrl()`의 `success`/`canceled` 복귀 쿼리). 배포된 Supabase 함수와 시크릿은 아직 운영에 남아 있다. 과거 `provider='stripe'` 행·`stripe_*` 컬럼·마이그레이션은 이력 호환으로 유지한다. | `server/supabase/config.toml`, `server/supabase/functions/billing-catalog/index.ts`, `apps/web/src/lib/billing-catalog.ts` | `[ ]` 운영: `supabase functions delete stripe-checkout`·`stripe-portal`·`stripe-webhook`, 시크릿 `STRIPE_SECRET_KEY`·`STRIPE_WEBHOOK_SECRET`·`STRIPE_PRICE_PRO`·`STRIPE_PRICE_PRO_PLUS`·`STRIPE_PRICE_TEAM` unset, Stripe 대시보드 웹훅 엔드포인트 비활성화. `billing-catalog` 재배포는 웹앱 배포보다 먼저(웹 파서가 Stripe 가격을 거부). |
|
||
| GAP-WEB-01 | Web | 웹앱(`apps/web`)이 공개되지 않았다(배포 경로가 없었음). | `apps/web/Dockerfile`, `docker-compose.nas.yml`, `server/cloudflare-site-bridge` | `[x]` 2026-09-26: `/app` basePath·NAS `d3ro_voice_web`(3002). 터널 `kd-nas` ingress에 `d3ro.chanpaca.net` path `^/app` → NAS 3002를 추가하고 브리지 워커가 `/app`·`/api`·`/health`를 도메인 원본(터널)으로 흘린다(4aadee8). Supabase `site_url`을 `https://d3ro.chanpaca.net/app`으로(기존 localhost:3000), 허용 목록에 `/app/**` 추가. 라이브 `/app/login` 200. 같은 수정으로 9/19부터 끊겨 있던 API(`/api`, 관리자 로그인·stt-proxy)도 복구. |
|
||
| GAP-OPS-01 | Ops | NAS 운영 compose가 저장소와 어긋나 있었다(JWT 기본값 폴백, 법률 wwwroot 마운트, 필수 변수 4개 누락). | `docker-compose.nas.yml`, NAS `/volume1/docker/d3ro/.env` | `[x]` 2026-09-26: NAS `.env`에 `SUPABASE_URL`·`SUPABASE_SERVICE_ROLE_KEY`(운영 service_role)·`ADMIN_BOOTSTRAP_TOKEN`(신규)·`API_SERVER_URL` 추가, 저장소 compose로 교체 후 `docker compose up -d`. 4개 컨테이너 정상, 백업 `*.before-wave3-20260926`. |
|
||
| GAP-OPS-02 | Ops | 관리자 주소가 두 개였다(`admin.chanpaca.net`, `d3ro-admin.chanpaca.net`). `admin.d3ro.chanpaca.net`은 2단계 서브도메인이라 고급 인증서(ACM, 월 $10)가 필요하다. | Cloudflare 터널 kd-nas ingress, chanpaca.net DNS | `[x]` 2026-09-26: 사용자 결정 — 관리자 주소는 `d3ro-admin.chanpaca.net` 하나(chanpaca.net에 다른 서비스가 많다). `admin.chanpaca.net` 터널 규칙·DNS 삭제로 이름을 풀었다. ACM은 지금 구독하지 않는다. 참고: `*.kite`·`*.voice` 고급 인증서는 ACM이 꺼져 2026-11-02 만료 후 갱신되지 않는다(다른 프로젝트). |
|
||
| GAP-CI-01 | CI | macOS 빌드·서명 러너가 없다. `.github`의 build-mac·release-signing-ca는 실행된 적 없이 삭제됐다. | `.forgejo/workflows/*`, `.gitlab-ci.yml` | `[!]` EXT: Mac 호스트에 Forgejo runner(`macos` 라벨)를 붙이거나 GitLab `package-macos` 사용. 서명된 Android 릴리스는 GitLab `mobile-production-release`가 정본. |
|
||
| GAP-I18N-02 | Desktop | 녹음 캡슐의 "처리 중..."이 `recording-tip/index.html`에 한국어로 고정돼 앱 언어를 따르지 않는다. | `apps/desktop/src/renderer/popups/recording-tip/*`, `WindowManager.getPopupI18nStrings` | `[ ]` 2026-09-26 발견. |
|
||
| GAP-TEAM-02 | Team | 브라우저에서 초대를 수락하는 경로가 없다. 웹 `accept-invite` 페이지는 발급 링크가 가리키지 않고 로그인 후 토큰을 읽는 곳이 없어 끊겨 있었으므로 삭제했다(사이트 `/accept-invite/`는 앱 딥링크만). | `site/public/accept-invite/`, `server/supabase/functions/team-accept` | `[ ]` 2026-09-26: 데스크톱 전용 사용자를 위한 웹 수락 흐름이 필요하면 `/app` 아래에 다시 설계. |
|
||
| GAP-REL-10 | Release | `release-windows`(태그 파이프라인)는 서명 가드에 도달하기 **전에** sidecar 단계에서 죽는다. 이 러너 컨텍스트에서는 `sidecar:setup`이 Python 3.11+를 찾지 못한다(`Python 3.11+ 를 찾을 수 없습니다`) → `sidecar:build` → `verify-sidecar-bundle.mjs` 연쇄 실패(실측: run#65 `v1.3.7`, run#61 `v1.3.6`). 같은 러너의 portable 잡은 `py -3.11 → Python 3.11.9`를 찾아 사이드카 빌드에 성공하므로, 워크플로/컨테이너 간 PATH 차이다. | `.forgejo/workflows/release.yml`, `apps/desktop/scripts/setup-sidecar.mjs` | `[!]` 2026-09-19: 러너에 Python 3.11+(`py` 런처 포함)를 보장하거나 워크플로에 `actions/setup-python` 단계를 추가한다. 그 전까지 서명 게시는 불가능하다(GAP-REL-02와 별개 선행 차단). |
|
||
| GAP-REL-11 | Release | portable 워크플로의 마지막 `actions/upload-artifact@v4` 단계가 Forgejo 러너에서 `GHESNotSupportedError`로 실패한다(증거 보존만 실패, 게시는 성공). | `.forgejo/workflows/portable.yml` | `[x]` 2026-09-19: `v1.3.7` portable 게시는 run#64에서 성공(7z 단일 볼륨 83.7MB + zip 2부, `portable-latest/portable.json`이 1.3.7 보고). 남은 조치: upload-artifact 단계를 제거하거나 v3/다른 보존 방식으로 바꿔 워크플로를 GREEN으로 만든다. |
|
||
| GAP-REL-12 | Release | Forgejo generic registry는 파일 단위 덮어쓰기를 거부(HTTP 409)하는데, portable 게시 스크립트의 "동일 파일 건너뛰기"가 1MiB 초과 파일을 **크기만** 비교했다. 그래서 새 runtime의 sidecar 볼륨 `.001`이 이전 볼륨과 정확히 같은 94,371,840 bytes였고, 내용이 다른데도 "동일"로 오판해 `runtime-latest`가 낡은 `.001` + 새 `.002`로 갈라졌다(다운로드 시 손상). | `scripts/ci/publish-portable-release.mjs` | `[x]` 2026-09-23: 파일 목록 API(`GET /api/v1/packages/{owner}/generic/{name}/{version}/files`)의 sha256으로 정확히 비교하고, 내용이 다른 파일이 하나라도 있으면 해당 별칭 버전 전체를 삭제 후 재업로드하도록 수정했다. 이미 발생한 `runtime-latest`는 버전을 지우고 재게시해 4개 파일 모두 로컬 해시와 일치함을 확인했다. |
|
||
| GAP-REL-02 | Release | Windows stable publication needs an external public-trust Authenticode PFX, its password, the exact signer subject, and a Forgejo token, none of which live in the repository. | `.forgejo/workflows/release.yml`, `.gitlab-ci.yml`, `scripts/ci/set-forgejo-secrets.mjs`, `scripts/ci/verify-windows-release-artifact.ps1` | `[!]` 2026-09-18 measured: the Forgejo repo had **zero** Actions secrets; `FORGEJO_TOKEN` is registered now (2026-09-18) but `WIN_CSC_*` still have no values, so `v1.2.0` (run 49) and `v1.3.0` (run 51) both failed at the signing guard and **no updater-feed release has been published since `1.1.0`**. Inject the four secrets (`WIN_CSC_LINK`, `WIN_CSC_KEY_PASSWORD`, `WIN_CSC_EXPECTED_SIGNER_SUBJECT`, `FORGEJO_TOKEN`) with `npm run release:secrets` (check: `npm run release:secrets:check`), then re-run `release.yml` for the `v1.3.0` tag via `workflow_dispatch` (tags are immutable). **2026-09-19 정정**: CI 서명 게이트는 여전히 막혀 있지만, updater feed에는 `1.3.2`~`1.3.7`이 로컬 `release:updater` 경로로 게시되어 있다(GAP-REL-06). **2026-09-23 정정**: `1.4.0`은 게시되지 않았고(GAP-REL-06), `1.5.0`도 로컬 무서명 경로로 게시했다. CI 서명 게이트는 여전히 `WIN_CSC_*` 부재로 막혀 있다(GAP-REL-10의 Python 선행 문제와 별개). |
|
||
| GAP-REL-06 | Release | 서명 인증서가 없어 stable(`latest`) 채널에 **무서명** 설치본을 게시했다. electron-updater는 `app-update.yml`에 `publisherName`이 없으면 서명 검증을 건너뛰므로 설치는 동작하지만, SmartScreen 평판은 버전마다 0부터 시작한다. | `scripts/ci/publish-updater-release.mjs`, `release/update-policy.json`, `.forgejo/workflows/release.yml` | `[!]` 2026-09-18: `1.3.2`를 `--ack-unsigned`(명시적 승인 플래그)로 게시. **2026-09-19: `1.3.7`도 같은 경로로 게시**(`npm run release:updater -- --ack-unsigned`, 설치본 90.6MiB, `latest.yml`=1.3.7, 설치본 sha512가 피드 메타데이터와 일치). 인증서 확보 시 더 높은 버전으로 서명 게시하여 대체하고, 이 예외를 제거한다. **2026-09-23: `1.5.0`도 같은 경로로 게시**(`npm run release:updater -- --build --ack-unsigned`, 설치본 91.2MiB, `latest.yml`=1.5.0, sha512 검증). |
|
||
| GAP-REL-07 | Release | 패키징된 `better-sqlite3`가 호스트 Node ABI여서 `1.3.2` 설치본이 시작 즉시 죽었다(NODE_MODULE_VERSION 131 vs 130). 원인: 로컬 `npm install`이 네이티브 모듈을 Node용으로 재빌드했고 패키징이 재빌드를 건너었다. | `scripts/ci/verify-native-abi.mjs`, `scripts/ci/fix-native-abi.mjs`, `.gitlab-ci.yml`/`.forgejo`/`.github` 패키징 단계 | `[x]` 2026-09-18: 패키징 후 Electron ABI를 검증하고, 검증된 트리에서만 설치본을 생성(`--prepackaged`)한다. `1.3.3`은 설치본에서 추출한 바이너리로 재검증 GREEN. |
|
||
| GAP-ADS-01 | Ads | 9 of 10 desktop ad adapters still extend `UnavailableAdAdapter` (`provider_not_integrated`). | `apps/desktop/src/main/services/ads/*` | `[~]` 2026-09-13: `DirectHouseSponsorAdapter` is now a real configurable REST adapter (bid/impression/click/reward via `endpointUrl`; fail-closed when unconfigured; 22 unit tests GREEN). Remaining 9 need official SDKs/authenticated endpoints. |
|
||
| GAP-ADS-02 | Ads | Desktop mediation reward accounting is not wired to license quota (`claimReward` still returns no tokens). | `AdMediationEngine.ts`, `AppLayout.tsx` | Wire verified `reportRewardCompletion` to `LicenseService` quota after the direct sponsor endpoint exists. |
|
||
| GAP-ID-01 | Identity | Supabase, .NET JWT/SQLite, and the desktop offline license each had their own tier/role shape. | `@d3ro/core/entitlement`, `LicenseService`, `entitlement-context` | `[~]` 2026-09-13: canonical `EntitlementSnapshot` + `resolveEntitlement` added with tests; desktop tier normalization + `isPro` fixed. Full adoption tracked as GAP-ID-02. |
|
||
| GAP-ID-02 | Identity | Web and mobile still hand-roll tier/role normalization instead of consuming the canonical resolver; .NET identity is still a separate store. | `apps/web/src/lib`, `apps/mobile-rn/src/lib/entitlement-context.tsx` | Adopt `resolveEntitlement` in web/mobile; decide whether to retire the .NET user store or keep explicit mapping. |
|
||
| GAP-TEAM-01 | Teams | Team comments / activity feed was not implemented. | `server/supabase/migrations/20260913000033_team_activities.sql`, web `activity-feed.tsx`, mobile `team-service.ts` | `[x]` 2026-09-13: `team_activities` table + `create_team_activity` RPC + RLS + realtime publication; web + mobile UI. |
|
||
| GAP-MEM-01 | Knowledge | Web file upload was deferred and semantic search was mislabeled future. | `apps/web/src/components/knowledge/*` | `[x]` 2026-09-13: `.txt`/`.md` picker, newline-aware chunking, `embed-chunks` on submit, `search-knowledge` confirmed live. |
|
||
| GAP-MEM-02 | Dictionary | No dictionary import/export on any surface. | catalog MEM-07 | `[x]` 2026-09-13: desktop `dictionary:import/export` (JSON/CSV), web serialize/parse + download/upload, mobile via `data-portability`; 8 new unit tests. |
|
||
| GAP-MTG-01 | Meetings | Audio seek ↔ transcript timestamp sync missing. | SSOT H-011 | Store segment timestamps; wire player seek. |
|
||
| GAP-MTG-02 | Meetings | Speaker diarization only partially done on desktop; absent web/mobile. | `docs/phases/phase-15.5-speaker-diarization.md`, SSOT H-014 | Finish desktop pyannote path; expose speaker labels cross-surface. |
|
||
| GAP-INFRA-01 | Build | `apps/mobile-rn` is outside npm workspaces, so root `typecheck`/`test`/`lint` skip it. `typecheck:mobile`/`lint:mobile`/`test:mobile`/`verify:all` root scripts added 2026-09-13 (`package.json`), but membership/CI integration is still open. | `package.json` | Decide: add mobile to workspaces, or wire `verify:all` into CI. |
|
||
| GAP-MOB-01 | Legacy | `apps/mobile` Expo skeleton duplicated auth/data code and got version-sync edits. | SSOT G-002 | `[x]` 2026-09-13: deleted `apps/mobile`, removed from `sync-version.mjs` and `package-lock.json`; `version:check` GREEN. |
|
||
| GAP-INFRA-02 | CI | GitLab admin NAS deploy job is disabled; admin deploy is manual/GHCR. | `.gitlab-ci.yml` (admin job comments) | Re-enable with a protected environment, or document the manual runbook as canonical. |
|
||
| GAP-INFRA-03 | Release | Desktop auto-update was single-channel and pointed at GitLab while the public hub was Forgejo; no channel, mandatory-update, major-vs-delta, staging, or kill-switch policy. | `apps/desktop/src/main/update-feed.ts`, `electron-builder.yml`, `.forgejo/workflows` | `[x]` 2026-09-13: canonical Forgejo feed + legacy GitLab mirror, `release/update-policy.json` + runtime policy, `publish-forgejo-release.mjs` + Forgejo release workflow, 21 policy tests, verifier self-test 13 cases. See `docs/deployment/update-system-assessment.md`. |
|
||
| GAP-INFRA-04 | Quality | Desktop `typecheck` is a no-op: `tsconfig.json` is `files: []` + references, so `tsc --noEmit` checks nothing. Real `tsc -p tsconfig.node.json --noEmit` surfaces many pre-existing errors. | `apps/desktop/package.json`, `apps/desktop/tsconfig.json` | Switch to `tsc -b` (or per-project `-p`) and clear the existing errors in a dedicated workstream; do not treat "typecheck GREEN" as evidence until then. |
|
||
| GAP-SHELL-01 | Support | Admin `/support` is a stub panel; desktop-only `SupportModal`. No shared ticket contract. | `apps/admin/src/app/(admin)/support/page.tsx` | Define a ticket/diagnostics contract or keep stub and mark N/A in catalog. |
|
||
| GAP-PUSH-01 | Push | `send-push` accepted `webpush`/`apns` but marked them `push_provider_not_supported`. | `_shared/webpush.ts`, `_shared/apns.ts`, `send-push/index.ts` | `[x]` 2026-09-13: VAPID Web Push (RFC 8291) + APNs `.p8` transports implemented and routed; 10 new tests incl. an encryption round-trip. Client registration for those providers still pending (see GAP-PUSH-03). |
|
||
| GAP-PUSH-02 | Push | Nothing triggered `send-push?mode=drain`; enqueued notifications never left the outbox. | `server/cloudflare-worker/src/push-drain.ts`, `wrangler.toml` | `[x]` 2026-09-13: Cloudflare Cron Trigger (`* * * * *`) drains the outbox; tests in CI. Requires `SUPABASE_URL` + `SUPABASE_SERVICE_ROLE_KEY` secret on the worker. |
|
||
| GAP-PUSH-03 | Push | Mobile/web clients register only `fcm`; no service worker subscription or APNs device token. | `apps/mobile-rn/src/features/notifications/*`, `apps/web` | Add web service worker + `pushManager.subscribe` (store JSON subscription) and iOS APNs token registration. |
|
||
| GAP-PUSH-04 | Push | Android still depends on FCM (`google-services.json`). | `apps/mobile-rn/android`, `send-push` | Decide: minimal Firebase project, or UnifiedPush/ntfy. See `docs/deployment/push-transport-without-firebase.md`. |
|
||
| GAP-STT-01 | Local STT | Packaged desktop builds shipped **no** faster-whisper sidecar: `electron-builder.yml` had no `extraResources` entry for it and no pipeline job built it. Local transcription was impossible on any installed build; the app fell back to a system Python without the runtime. | `apps/desktop/electron-builder.yml`, `.gitlab-ci.yml`, `apps/desktop/scripts/build-sidecar.mjs` | `[x]` 2026-09-18: sidecar bundled via `extraResources` (`sidecar-dist/sidecar` → `resources/sidecar`), `sidecar:setup`/`sidecar:build` scripts, and `scripts/ci/verify-sidecar-bundle.mjs` fails packaging when the engine or its Silero VAD data is missing. Verified on the real 242 MB bundle. |
|
||
| GAP-STT-02 | Local STT | Dev resolved the sidecar/SoX paths against the Vite output dir (`out/main`), so the sidecar ran a Python without faster-whisper and SoX recording died with `spawn sox ENOENT`. | `apps/desktop/src/main/utils/paths.ts`, `apps/desktop/tests/main/utils/paths.test.ts` | `[x]` 2026-09-18: app root is discovered by walking up for app markers; packaged mode fails loudly instead of silently falling back. Unit tests pin both branches. |
|
||
| GAP-STT-03 | Local engines | On hosts where `localhost` resolves only to IPv6, every local engine call (STT sidecar and Ollama) was refused. Audio capture and local LLM appeared dead. | `apps/desktop/src/main/utils/loopback.ts`, `LocalSTTService`, `LocalLLMService`, `RAGService`, `OnlineLLMService`, `STTManager` | `[x]` 2026-09-18: loopback normalization to `127.0.0.1` for all local engine URLs; defaults updated; 9 unit tests. Verified against the live sidecar and Ollama on a host with an IPv6-only `localhost`. |
|
||
| GAP-STT-04 | Local STT | Live partial transcript (`CAP-03`, `voice:partialTranscript`) was marked done but had **no producer**: the channel, popup UI, and preload existed, nothing ever emitted. | `apps/desktop/src/main/services/VoiceModeService.ts`, `LocalSTTService.transcribePartial`, `STTManager.transcribePartial` | `[x]` 2026-09-18: 1.5 s cadence over a 7.5 s trailing window, greedy decode, drained before the final transcription; never inserted. |
|
||
| GAP-STT-05 | Local STT | The bundled sidecar lacked faster-whisper's Silero VAD data, so `vad_filter=true` transcription would have failed at runtime even with the engine bundled. | `apps/desktop/scripts/build-sidecar.mjs`, `scripts/ci/verify-sidecar-bundle.mjs` | `[x]` 2026-09-18: `--collect-all faster_whisper` plus a packaging-time presence check for `assets/silero_vad_v6.onnx`. |
|
||
| GAP-REL-03 | Release | 서명이 없어 설치할 수 있는 경로가 없다(인증서 발급 전 공백). | `.forgejo/workflows/portable.yml`, `scripts/ci/build-portable.mjs`, `scripts/local/install-d3ro-voice.ps1`, `bucket/d3ro-voice.json` | `[x]` 2026-09-18: 서명 없는 portable 채널 구현 — 95MiB 7z 분할 볼륨(688MB → 162MiB) + Scoop 버킷 + 수동 설치 스크립트를 Forgejo에 게시. 실제 설치 스크립트 end-to-end 검증(볼륨 다운로드 → SHA-256 → 결합 → 해제 → 엔진 포함 확인). updater feed는 건드리지 않음. **2026-09-19: `1.3.7`이 CI portable run#64에서 `portable-latest`에 게시됨**(7z 단일 볼륨 83.7MB + zip 2부, `portable.json`=1.3.7) — 즉 portable 경로는 CI만으로 동작하고, 실패한 것은 마지막 증거 업로드 단계다(GAP-REL-11). |
|
||
| GAP-REL-04 | Release | canonical feed는 Cloudflare 뒤에 있어 업로드 본문이 **100MiB**를 넘으면 HTTP 413으로 거부한다. | `scripts/ci/publish-updater-release.mjs`, `docs/deployment/unsigned-distribution.md` | `[~]` 2026-09-18: 설치본을 90.6MiB로 줄여 업데이트 피드 게시를 복구했다(GAP-STT-07). 휴대용/Scoop 채널은 여전히 95MiB 분할이 필요하다. |
|
||
| GAP-STT-07 | Local STT | 진(사이드카)을 앱 번들에 넣으면 설치본이 100MiB를 넘고 매 업데이트마다 162MiB를 다시 받는다. | `apps/desktop/src/main/services/RuntimeProvisioner.ts`, `apps/desktop/electron-builder.yml` | `[x]` 2026-09-18: 설치본에서 엔진/ffmpeg를 제거하고 처음 필요할 때 `runtime-latest`에서 내려받는다(부품별 + 결합본 SHA-256 검증). 설치본 189MB → 90.6MiB, 런타임 1회 116MiB(엔진 94.4 + ffmpeg 21.7). 실제 feed로 통합 검증 완료. |
|
||
| GAP-STT-06 | Local STT | Decode settings were untuned: previous-text conditioning let repeated hallucinations compound, and no VAD parameters meant slow, uneven segments. | `apps/desktop/sidecar/main.py` | `[x]` 2026-09-18: `condition_on_previous_text=false`, bounded low-temperature fallback, `no_speech`/`compression_ratio`/`log_prob` thresholds, 300 ms silence trimming. Same transcript, ~5x faster on the reference machine (7.7 s audio: 1609 ms → 303 ms). |
|
||
| GAP-KEY-01 | Key bindings | 키바인딩 전면 개편(CAP-16)이 실앱 구동으로 검증되지 않은 상태였다. uiohook 전역 후킹, 더블프레스 타이밍, 마우스 버튼 수신을 실행 중인 Electron 에서 확인한 적이 없었고, 증거가 계약 수준(`packages/core` 117개 통과, 키바인딩 파일 타입 에러 0건)뿐이었다. 에이전트는 데스크톱 GUI 를 띄울 수 없다(`AGENTS.md` §3). | `apps/desktop/src/main/services/KeyBindingService.ts`, `packages/core/__tests__/keybinding.test.ts`, `%APPDATA%/d3ro-voice/logs/main.log`, `%APPDATA%/d3ro-voice/d3ro-voice-config.json` | `[x]` 2026-09-21: 사용자가 Windows 에서 앱을 띄워 검증 완료. 로그(12:53–13:06)에 `Migrated 4 legacy shortcut(s) to keyBindings` → `Loaded 6 key binding(s) from config for 6 action(s)` → `uiohook started, global keyboard/mouse hook active` → `key-bindings initialized` → `Key binding events connected` 가 순서대로 남았고 부팅 에러 0건, STT 모델 로딩까지 정상. 키보드·마우스(MB4/MB5) 바인딩을 UI 로 실제 조작해 동작을 확인했고, 같은 세션의 `Loaded 7 key binding(s) … for 6 action(s)` 가 다중 바인딩이 실동작함을 보인다. 설정 파일 되읽기로 마이그레이션 결과를 확인 — 사용자 커스텀 값이 그대로 보존됐고(dictation `code:49,alt` / hands-free `code:49,alt+shift` / command `code:165,ctrl` / caption `code:49,ctrl+alt+shift`, 팝업 2종은 신규 기본값), 구 `*Shortcut` 4개와 `displayLabel` 은 모두 사라졌다. **남은 것**: `KeyBindingService` 자체의 유닛 테스트는 여전히 없다(실행 검증이 유닛 테스트를 대체하지 않는다). macOS/Linux 는 GAP-KEY-02 로 계속 열려 있다. |
|
||
| GAP-KEY-02 | Key bindings | 마우스 버튼 지원이 **Windows 기준으로만** 설계·확인됐다. `KeyBindingService` 에는 마우스 관련 플랫폼 분기가 없고(`process.platform` 은 meta 수정자 라벨 표기에만 쓰인다), macOS/Linux 에서 uiohook 이 보고하는 X1/X2 버튼 번호와 OS 기본 "뒤로/앞으로" 동작과의 간섭은 확인하지 않았다. 마우스 이벤트는 suppress 가 불가능하므로 원래 동작이 항상 함께 실행된다. | `KeyBindingService.ts:235`(`readMouseButton`), `:301`(meta 라벨 분기), `packages/core/src/keybinding.ts:561-612`(마우스 카탈로그 5종). 2026-09-21 실앱 검증(GAP-KEY-01)은 **Windows 에서만** 이뤄졌고 거기서는 MB4/MB5 가 정상 동작했다. | macOS/Linux 에서 MB2~MB5 수신 여부와 버튼 번호 매핑을 확인하고, 다르면 카탈로그를 플랫폼별로 분기한다. |
|
||
| GAP-KEY-03 | Key bindings | `command` 액션에 전용 핸들러가 없다. 이번에 처음으로 설정 UI 에 노출됐지만, 트리거되면 dictation 파이프라인으로 fallback 하며 `KEYBINDING_ACTIONS` 의 `holdMode:false` 대신 dictation 과 같은 hold-to-talk 로 강제된다. 개편 이전부터 같은 동작이었고 이번 작업은 그 사실을 코드에 명시화만 했다(기능 변화 없음). | `apps/desktop/src/main/services/VoiceModeService.ts:1071`(`_resolveHoldMode`), `packages/core/src/keybinding.ts:740`(액션 정의) | `command` 전용 동작을 정의하고 `_resolveHoldMode` 의 예외를 제거하거나, 액션을 카탈로그에서 뺀다. |
|
||
| GAP-QA-02 | Quality | 캡션 테스트 2건이 **개발 머신에 사이드카 venv 가 있는지에 따라 결과가 갈린다**. `LocalSTTService.initialize()`(`:239`) → `_ensureSidecarRunning()`(`:583`) → `_spawnSidecar()`(`:650`) → `_waitForHealth()`(`:794`) 경로에서 venv 가 존재하면 실제 Python 프로세스를 띄우고 health 폴링이 vitest 기본 타임아웃 10초를 넘긴다. venv 가 없으면 `getSidecarCommand()`(`apps/desktop/src/main/utils/paths.ts:174`)가 즉시 throw 해서 같은 테스트가 빠르게 통과한다. 테스트가 로컬 환경을 격리하지 못한 것이 결함이다. | `tests/red/ipc-surfaces.usecase.test.ts`(`캡션 시작 실패는 success:false 로 나온다`), `tests/red/silent-errors.usecase.test.ts:48`. **키바인딩 개편의 회귀가 아니다** — 2026-09-21 에 HEAD(`0ca9e24`) 무수정 코드를 같은 환경(venv 연결)에서 돌려 동일하게 재현했다. 같은 날 같은 머신에서도 실행 방식에 따라 결과가 갈렸다: 전체 실행은 `3 failed / 1311 passed (1314)`(`rag.usecase` + `silent-errors` 캡션 + `paths.test`)이고 `ipc-surfaces` 캡션 케이스는 통과했는데, 그 파일만 단독 실행하면 같은 케이스가 10초 타임아웃으로 실패한다. 테스트 총수 1314 는 어느 실행에서나 같고, 새로 깨진 테스트는 0건이다. | 사이드카 기동을 테스트 경계에서 주입·모킹해 환경 의존을 끊는다. 함께 실패하는 `rag.usecase`(임베딩 서버 부재)도 같은 성격이다. `tests/main/utils/paths.test.ts:78` 은 성격이 다르다 — 기대 정규식이 `사이드카를 찾을 수 없습니다` 인데 실제 메시지는 `로컬 음성 엔진이 아직 설치되지 않았습니다…` 로 바뀌어 테스트가 문구를 따라가지 못한 것이다. |
|
||
| GAP-I18N-01 | i18n | 로케일별 키 수가 크게 어긋난다. 2026-09-21 실측: `ko` 1716 / `en` 1709 / 나머지 10개 로케일 각 327. `keybinding.*` 55개는 12개 로케일 전부에 동일하게 들어갔지만, 그 밖 약 1,380개 키가 비영어 로케일에 없어 폴백 체인(locale → `en` → `ko`)으로 표시된다. 키바인딩 작업 이전부터 있던 부채이며 그 작업 범위 밖이었다. | `packages/i18n/src/locales/*.json`, 카탈로그 SHELL-03. **구체 사례 (2026-09-21 실측)**: `popup.error.default` 가 `en.json`·`ko.json` 에만 있고 나머지 10개 로케일에 없다. 소비처는 `WindowManager.ts:59`(팝업 문자열 주입, 선재)와 `CommandsPage.tsx:198`·`:201`(LLM 수정으로 추가된 파이프라인 벤치 오류 표시) 두 곳이며, 비영어 사용자에게는 오류 메시지가 영어로 폴백된다. 새 갭이 아니라 이 행이 세는 약 1,380개 중 하나다 — 별도 행을 열지 마라. | 로케일 간 키 diff 를 내는 커버리지 게이트를 만들어 회귀를 막고, 누락 키를 채운다. |
|
||
| GAP-I18N-02 | i18n | 렌더러가 `ko.json` 에 없는 `license.*` 키를 쓴다. `TranslationKey` 가 `ko.json` 에서 파생되므로 누락은 타입 에러로 드러난다. 타입 에러로만 끝나지 않는다 — 폴백 체인이 `locale → en → ko → 키 문자열` 이므로 마스터 로케일에도 없으면 **`license.team` 같은 키가 화면에 그대로 노출된다**. 2026-09-21 실측: `license.feature.premium_llm`·`license.team`·`license.enterprise` 가 없고 이로 인한 TS2345 가 4건이다. HEAD 에서도 없던 키이므로 선재 결함이며 키바인딩 작업과 무관하다. | `apps/desktop/src/renderer/components/UpgradePromptModal.tsx:47`·`:192`, `apps/desktop/src/renderer/pages/DashboardPage.tsx:481`·`:529`, `packages/i18n/src/locales/ko.json` | 세 키를 `ko.json` 에 추가하고 12개 로케일에 반영한다. 같은 타입체크에 잡히는 `LicenseTab.tsx`(6건)·`LicenseModal.tsx`(2건)는 원인이 다르다 — `TFunction` 을 `(k: string) => string` 에 넘기는 TS2322 4건과 `currentTier` 미정의 TS2304 2건으로, 후자는 컴파일이 깨지는 별개 결함이다(GAP-INFRA-04 범위). |
|
||
| GAP-LLM-01 | LLM | **번역 대상 언어를 사용자가 고를 수 없다.** 항상 `English` 고정이다. `AppConfig` 에 대상 언어 키가 없고, 기존 두 키 모두 대용할 수 없다 — `language` 는 UI 로케일이라 `'ko'` 같은 코드가 프롬프트에 그대로 들어가 문장이 깨지고, `sttLanguage` 는 입력(원문) 언어라 그 값으로 번역하면 원문이 그대로 나온다. 2026-09-21 LLM 수정(`9c2b4d4`)은 대상 언어가 호출 프레임 두 단계 밖의 기본값에 의존하던 것을 명시 인자로 바로잡았을 뿐, 선택지를 만들지는 않았다(설정 키 + 설정 UI + i18n 이 필요해 patch 범위 밖으로 뒀다). | `apps/desktop/src/main/services/llm-prompts.ts:37`(`DEFAULT_TARGET_LANGUAGE`)·`:50`(`resolveTargetLanguage`), `packages/core/src/types.ts`(`AppConfig` 에 키 없음), 내장 프리셋 `CustomInstructionService.ts:26` | `AppConfig` 에 대상 언어 키를 추가하고, 설정 UI(LLM 탭)에 노출하고, `resolveTargetLanguage()` 가 설정을 읽게 한다. 언어 목록과 라벨은 i18n 키가 필요하다. |
|
||
| GAP-LLM-02 | LLM | **2026-09-21 LLM 지시문 수정(`9c2b4d4`)이 실앱 구동으로 검증되지 않았다.** 유닛 테스트는 통과하지만(`llm-prompts.test.ts` 21, `llm-handlers.test.ts` 7, `VoiceModeService.test.ts` 22, `ChainService.test.ts` 5 — 수정 4건을 각각 되돌려 실제로 실패하는 것까지 확인), 실행 중인 Electron 에서 실제 지시문을 돌려 결과가 삽입되는 것을 본 적이 없다. 에이전트는 데스크톱 GUI 를 띄울 수 없다(`AGENTS.md` §3). **게다가 이 수정과 직접 관련된 usecase 테스트 4개가 실행조차 되지 않았다** — `tests/red/{instruction,chain,voice,config}.usecase.test.ts` 가 `better-sqlite3` ABI 불일치로 DB 생성 단계에서 먼저 죽는다(GAP-INFRA-06). 즉 그 범위는 통과도 실패도 아닌 **미검증**이다. 영향 받는 카탈로그 행: AI-04, AI-05, AI-06, AI-07(전부 데스크톱 `[~]`). | `apps/desktop/src/main/services/llm-prompts.ts`, `VoiceModeService.ts:779`·`:827-870`, `ChainService.ts:196`, `src/main/ipc/llm-handlers.ts:96`, `src/renderer/pages/CommandsPage.tsx:180-205` | 사용자가 `run-desktop.bat` 로 앱을 띄워 (1) 명령 페이지에서 내장 프리셋(번역/요약/전문 리라이트/코드 설명/자유 프롬프트)을 활성화한 뒤 받아쓰기, (2) 명령 팝업에서 선택 후 받아쓰기, (3) 음성 키워드로 명령 호출, (4) 체인 실행, (5) 명령 페이지 파이프라인 벤치를 각각 돌려 **지시문 문구가 아니라 처리 결과가** 삽입되는지 확인한다. GAP-INFRA-06 의 ABI 전환 스크립트가 생기면 usecase 4종을 함께 돌린다. |
|
||
| GAP-INFRA-06 | Dev env | `better-sqlite3` 네이티브 ABI 가 **앱 실행과 로컬 테스트에서 서로 다른 값을 요구**한다. Electron 33 은 ABI 130, 호스트 Node 23 은 ABI 131 이라 한쪽에 맞추면 다른 쪽이 깨진다. 2026-09-21 실측: `electron-rebuild -f -w better-sqlite3` 직후 vitest 가 `366 failed / 948 passed` 로 무너졌고, 리빌드 전에는 `1311 passed` 였다. 같은 날 확인한 현재 워크스페이스는 Node ABI 쪽(호스트 `node -e "require('better-sqlite3')"` 성공)이라 테스트는 돌고 앱 실행에는 재리빌드가 필요하다. **배포 차단 이슈가 아니다** — `node_modules/` 는 gitignore(`.gitignore:1`)이고 패키징 경로는 `scripts/ci/verify-native-abi.mjs` 가 이미 막는다(GAP-REL-07 `[x]`). 순수하게 로컬 개발 환경 전환 비용 문제다. **다만 전환 비용으로 끝나지 않는다 — 검증을 가린다.** Electron ABI 쪽으로 리빌드된 상태에서는 `tests/red/*.usecase.test.ts` 가 DB 생성 단계에서 먼저 죽어 그 안의 케이스가 통과도 실패도 하지 않는다. 2026-09-21 LLM 지시문 수정(`9c2b4d4`)이 그 사례다: 전체 실행이 `366 failed / 994 passed (1360)` 였고 실패 366건 중 365건이 이 ABI 로 죽은 usecase 파일들인데, 하필 `instruction`·`chain`·`voice`·`config` usecase 가 그 수정의 직접 영향 범위였다(GAP-LLM-02). 참고로 같은 날 clean tree 베이스라인은 `366 failed / 948 passed (1314)` 로 실패 수가 동일해 신규 실패는 0건이다. | `scripts/ci/verify-native-abi.mjs`, `scripts/ci/fix-native-abi.mjs`, `package.json`(현재 리빌드용 스크립트 없음), `apps/desktop/tests/red/*.usecase.test.ts` | 두 ABI 를 오가는 npm 스크립트를 둔다(예: `rebuild:app` = Electron ABI, `rebuild:test` = Node ABI). 지금은 전환 방법이 문서화도 스크립트화도 되어 있지 않아 매번 수동으로 알아내야 한다. |
|
||
| GAP-STT-08 | Local STT | 1.3.5 설치본에서 엔진 설치가 "런타임 아카이브 해시 불일치 (sidecar)"로 항상 실패했다. 부품 검증은 **메모리 스트림**에서 센 값으로, 결합 검증은 **디스크 파일**에서 계산해 기준이 서로 달랐다. 디스크 쓰기가 잘려도 부품 검사를 통과하고 결합 단계에서만 터지므로 원인 파악도 불가능했다. 재시도가 없어 전송이 한 번 끊기면 곧바로 설치 실패였다. | `apps/desktop/src/main/services/RuntimeProvisioner.ts` | `[x]` 2026-09-18: 부품 크기·해시를 디스크 파일 기준으로 통일하고, 결합본은 크기를 먼저 검사한 뒤 해시를 본다(오류 메시지에 실제/기대값 포함). 부품 다운로드는 실패 시 해당 파일을 지우고 최대 3회 재시도한다. 서버 아티팩트는 무결함을 확인했고(부품 2개 해시 일치, 결합본 `e203aa53…` = 인덱스 기대값), 실제 feed로 설치를 재현해 18초 만에 성공. **1.3.6으로 게시 완료** — `latest.yml`이 1.3.6/90.6MiB를 서빙하고 설치본 sha512가 피드 메타데이터와 일치. 설치본 asar에 수정 코드가 포함되고 구버전 `archiveHash` 경로는 제거됨을 확인. |
|
||
|
||
| GAP-INFRA-05 | Build | 패키징된 렌더러 팝업 스크립트가 번들에 없었다. 팝업 HTML이 classic `<script src="./script.js">`를 참조해 Vite가 처리하지 않았고, dev에서는 로드되지만 설치본에는 파일이 없었다. 그래서 녹음 오버레이가 0:00에서 멈추고 웨이브 바가 뜨지 않았으며 실시간 자막이 렌더되지 않았다. 로드 전 `webContents.send`가 조용히 버려지는 문제와 `hide()` 이후 재표시의 z-order/repaint 유실도 함께 있었다. | `apps/desktop/src/renderer/popups/*/index.html`, `apps/desktop/src/main/windows/WindowManager.ts`, `scripts/ci/verify-desktop-renderer-bundles.mjs` | `[x]` 2026-09-19: 팝업 5종을 `type="module"`로 전환해 Vite가 해시된 번들로 방출하도록 고쳤고, 빌드 HTML이 참조하는 모든 로컬 asset이 디스크에 있는지 검사하는 `verify-desktop-renderer-bundles.mjs`(+ self-test)를 `.forgejo`/`.github` 패키징 파이프라인에 연결했다. WindowManager는 렌더러 준비 전 IPC를 `did-finish-load`까지 보관하고, 팝업을 표시할 때마다 topmost 재선언 + 강제 repaint를 수행하며, 팝업 렌더러 콘솔/로드 실패를 main 로그로 승격한다. |
|
||
|
||
| GAP-INPUT-01 | Input intelligence | **2026-09-22 10:57 실사용 검증 성공** — 카톡 타이핑 중 제안 5개 생성(1436ms)이 로그로 확인됐고(조합 중·idle 25ms에서도), 50초 관찰에 스냅샷 67건/생성 3회/실패 0건이었다. 남은 확인: 오버레이 위치·외관, 수락 삽입, 비밀번호 차단, 주간 수치 48개 유닛 케이스는 정책/집계/프롬프트 같은 순수 함수만 덮는다. 검증되지 않은 것: (1) `Alt+Shift+←` 수락이 실제 앱에 문장을 삽입하는지, (2) 오버레이가 케어렛 옆에 붙는지(케어렛 rect Chromium 에서만 UIA 로 얻어지고 네이티브 Win32 앱은 `GetGUIThreadInfo` 경로가 아예 없다), (3) `TypeError` 없이 Electron 메인에서 `koffi` 가 로드되는지(현재 호스트 Node 에서만 확인), (4) PyInstaller 번들에 `uiautomation`/`comtypes` 가 실제로 들어가는지(`build-sidecar.mjs` 에 `--collect-all` 을 추가했지만 번들을 다시 빌드해 확인하지 않았다). 에이전트는 데스크톱 GUI 를 띄울 수 없다(`AGENTS.md` §3). | `apps/desktop/src/main/services/{InputTelemetryService,SuggestionService,UiaContextService}.ts`, `apps/desktop/src/main/utils/win32-foreground.ts`, `apps/desktop/sidecar/{uia_bridge.py,requirements.txt}`, `apps/desktop/scripts/build-sidecar.mjs`, `apps/desktop/tests/main/services/input-intelligence.test.ts`(45) + `llm-prompts.test.ts`(+4) | **2026-09-21 23:13 부분 검증 완료** (사용자가 실제로 Notepad 에 타이핑한 로그): 텔레메트리 기동, uiohook 후킹, UIA 스냅샷(Notepad — `edit=true pw=false comp=false src=value`), WindowsTerminal 의 문서형 컨트롤을 `not-editable` 로 정확히 거부, 판단 로그까지 전부 실동작 확인. 이 과정에서 결함 4건을 잡아 고쳤다: (a) 트리거 지연(1000ms)이 스냅샷 디바운스(700ms)보다 커서 **"멈춘 뒤" 게이트가 결코 열리지 않던 문제**(settle 패스 추가), (b) 켜 둔 상태로 앱을 켜면 워밍업이 한 번도 안 돌던 문제(부팅 시 워밍업 + 가용성 폴링 대기 재시도), (c) 케어렛 오프셋을 못 주는 앱에서 문서 전체를 접두로 쓰던 문제(tail 폴백), (d) 후보가 도착해야 오버레이가 떠서 "아무것도 안 나옴" 으로 보이던 문제(요청 즉시 "생성 중" 표시). **2026-09-22 09:00~09:17 추가 실측** (사용자가 KakaoTalk·WindowsTerminal·Agent Switchboard 에 타이핑한 17개 스냅샷): 편집 가능으로 판정된 것은 3건뿐이고, 그중 KakaoTalk 입력창은 `edit=true src=value` 인데도 `len=0` 이라 실제 내용을 못 읽었다 — 커스텀 렌더 앱(카톡·터미널·에이전트 UI)에서는 UIA 가 텍스트를 노출하지 않는다. 즉 "아무것도 안 나옴" 의 상당 부분은 결함이 아니라 **읽을 수 없는 앱에서의 정상 동작**이며, 이 구분이 사용자에게 보이지 않던 것이 문제였다. 그래서 설정 > 입력에 **실시간 진단 줄**(포커스 앱 · 읽기 가부 · 소스 · 글자 수 · 비밀번호/케어렛 폴백 표시)을 추가하고 12개 로케일에 문구를 넣었다. 남은 확인: 오버레이의 실제 위치·외관, `Alt+Shift+←` 수락 삽입, 비밀번호 필드 차단, 주간 수치 정확도. 함께: `sidecar:build` 후 `GET /uia/focus` 응답을 확인하고 그 결과를 `scripts/ci/verify-sidecar-bundle.mjs` 의 필수 항목에 반영한다. **검증 도구 주의**: 데스크톱 `npm run typecheck` 는 문서화된 대로 no-op 이라(GAP-INFRA-04) 이번 작업에서도 거짓 통과를 냈다 — `typecheck:strict` 로 다시 돌려 이 작업이 만든 타입 오류 7건(core 에 없는 `InputTelemetryState` 참조 5건 등)을 찾아 고쳤다. 현재 strict 기준 이 작업 파일들의 오류는 0건이다(main 13 / renderer 35 는 전부 선재). |
|
||
| GAP-INPUT-07 | Input intelligence | `[~]` **2026-09-23 focused automated evidence:** five test files / 80 tests passed; desktop typecheck/lint, Python `py_compile`, and `git diff --check` exited 0 (the core 131-test pass is earlier independent evidence). The overlay lifecycle and `windowsHide:true` child-process audit are code/automation evidence only, not Electron GUI or external-app insertion proof. | `InputTelemetryService.ts`, `SuggestionService.ts`, `KeyBindingService.ts`, `components/input-insights/{InputConsentPanel,InputInsightsView}.tsx`, `popups/suggestion-overlay/`, `tests/main/services/{input-flow-domain,input-flow-services,windows-child-process-hide}.test.ts` | From an external terminal restart with `run-desktop.bat`, verify: (1) click X during generation closes immediately and never reappears; (2) selecting text closes the overlay; (3) input-focus loss closes it; (4) TTS, voice action, audio enumeration, screen context and file transcription do not revive a cmd/PowerShell window. Retain the existing editable/read-unavailable/password, Flow Radar/Edit Friction/App Quality, receipt, Smart Exclusion, provenance/fallback, shortcut-audit and narrow-geometry checks. Automated tests do **not** replace this manual proof. |
|
||
| GAP-INPUT-02 | Input intelligence | **IME 조합 중 텍스트(preedit)는 수집되지 않는다.** 우리는 조합 중에는 통계·제안을 모두 억제하고 조합이 끝난 커밋 텍스트만 UIA 로 읽는다. 조합 문자열 자체를 읽으려면 `IUIAutomationTextEditPattern::GetActiveComposition`(또는 레거시 앱은 IME `ImmGetCompositionStringW`)이 필요하다. KeyType.Windows 도 이 부분을 명시적으로 다음 사이클로 미룬다 — 즉 업계 공통 미해결 지점이다. 조합 중 억제 자체는 MS Learn 의 IME 문서상 필수 조치이며 구현돼 있다. | `apps/desktop/sidecar/uia_bridge.py`(`_caret_rect_and_offset` 의 TextEditPattern 블록), `packages/core/src/input-intelligence.ts`(`decideSuggestion` 의 `composing` 분기) | 조합 범위를 읽어 "조합 중 미리보기"를 제안 후보로 쓸지 검토한다. 지금은 억제만 하고 있어 한국어 사용자는 조합을 끝내야 제안이 뜬다. |
|
||
| GAP-INPUT-03 | Input intelligence | **공백이 없는 언어(중국어)는 단어 수가 과소 집계된다.** `countWords` 가 공백 분리 토큰을 세므로 "今天开会讨论了三件事" 는 1단어로 계산된다. 한국어/영어/일본어(공백 사용)는 정상이다. | `packages/core/src/input-intelligence.ts`(`countWords`), 카탈로그 INPUT-03, 테스트 `input-intelligence.test.ts` | CJK 연속 구간을 문자 단위로 세는 분기를 추가하거나, 통계 라벨을 "단어" 대신 "어절"로 바꾼다. |
|
||
| GAP-INPUT-04 | Input intelligence | **UIA 브리지가 Windows 전용이다.** `uiautomation` 은 Windows UI Automation 래퍼이므로 macOS 는 AX API, Linux 는 AT-SPI 구현이 따로 필요하다. 그래서 입력 인텔리전스는 카탈로그에서 데스크톱(`D`)만 표기하고 나머지 표면은 `[-]` 다. 또한 Chromium 138 미만 앱은 접근성 트리가 켜져 있으면 입력창 텍스트를 노출하지 않는데, 그 앱들을 `--force-renderer-accessibility` 로 켜도록 강제할 방법이 없다(우리 자신은 `app.setAccessibilitySupportEnabled(true)` 로 처리했다 — `bootstrap.ts`). | `apps/desktop/sidecar/uia_bridge.py`, `apps/desktop/src/main/bootstrap.ts`(`initInputIntelligence`) | macOS AX 경로를 붙일지 결정한다. 붙이지 않으면 카탈로그에서 macOS 를 명시적 N/A 로 유지한다. |
|
||
| GAP-INPUT-05 | Input intelligence | **수락/닫기 키가 사용자 습관과 충돌할 수 있다.** 기본값을 `Alt+Shift+←/↓/↑` 로 둔 이유는 Tab·Escape·Ctrl+Space 같은 관례 키를 뺏지 않기 위해서다(우리는 키를 삼키지 않으므로 원래 동작이 함께 실행된다). 그 대가는 "Tab 으로 수락" 같은 자연스러운 조작이 아니라는 점이고, 인라인 컴플리션 도구 대부분(Tab)과 다르다. | `packages/core/src/keybinding.ts`(`suggestion-accept`/`next`/`dismiss`), `apps/desktop/src/main/bootstrap.ts`(트리거 구독) | 오버레이가 보일 때만 Tab 을 삼키는 경로(전역 후킹에서 조건부 suppress)를 검토한다. 지금은 불가능하지 않지만 포커스 없는 창에서 키를 가로채는 설계가 필요하다. |
|
||
| GAP-LLM-03 | LLM | `[x]` **2026-09-22 코드 구현 완료.** 공유 `_abortController`를 요청별 controller로 대체하고 외부 signal을 각 요청에 연결/정리한다. generate/stream은 2048 tokens / 120 s, chat은 512 / 60 s로 제한하며, stream은 `done` frame 없이는 성공 처리하지 않고 불완전 응답을 정리한다. VoiceConversation은 single-flight와 request signal을 통해 local cancel을 실제 chat 취소로 전달한다. | `apps/desktop/src/main/services/LocalLLMService.ts`, `VoiceConversationService.ts` | 독립 표적 검증: 6 test files / 69 tests passed / 0 failed. 변경 코드·테스트 ESLint exit 0, `git diff --check` exit 0. 이는 전체 strict typecheck 또는 GUI 검증이 아니다. 앱 재시작 뒤 동시 제안·음성 요청의 요청별 취소와 `done` 누락 실패 처리는 GAP-LLM-04에서 계속 확인한다. |
|
||
| GAP-LLM-04 | LLM | `[~]` **Gemma/Ollama residency·burst·unbounded chat root cause (2026-09-22).** 19:14:12 boot warmup의 `keep_alive: 30m`가 `gemma4:e4b`를 expiry 19:44:12까지 VRAM 3,226,342,521 bytes / context 4096으로 상주시켰다. Windows GPU Engine PID 표본에는 활성 Ollama compute가 없어 무한 추론이 아니라 강제 residency였다. 19:11:17–19:11:58 자동 제안 반복은 기존 900 ms / 12 per min / 5 candidates / 128 tokens / 1-char growth가 허용한 burst였다. 위험 경로는 chat의 무제한 `num_predict`(Ollama 기본 `-1`), timeout·외부 abort 부재, 공유 취소, `done` 없는 EOF 성공, voice cancel 미전파, watchdog의 비취소였다. | `apps/desktop/src/main/services/{LocalLLMService,SuggestionService,VoiceConversationService}.ts`, Ollama `/api/ps` and Windows GPU Engine observations | 구현 계약: boot warmup 제거, suggestion `keep_alive: 2m`; 600 ms debounce / 5 s interval / 6 per min (hard max 12) / 3 candidates / 64 tokens / 12-char growth / 8 s timeout; 요청별 cancellation, bounded requests, done/cleanup, voice single-flight, spawn/poll dedupe와 dispose. Raw Ollama 증거: cold bounded 요청은 client hard timeout 15.044 s에 취소된 뒤 `/api/ps` empty와 `/api/version` 80 ms 회복을 보였고, explicit warmup HTTP 200은 16.639 s, 후속 `num_predict=1` / `keep_alive='2m'` 요청은 553 ms HTTP 200 / `done:true` / `eval_count:1` / `response=OK` / `done_reason:length`였으며 `/api/ps` expiry는 약 119.9 s였다. 이어 19:48:59 +09:00에는 새 generate/unload/kill/retry 없이 충분히 지난 뒤 단일 `/api/ps`가 HTTP 200 / 45.8 ms / `{models:[]}`였고 `/api/version`은 HTTP 200 / 7.3 ms / `0.32.13`이었다. 이는 raw API 수준의 expiry 뒤 unload 확인이다. **남은 조건:** 앱 재시작·GUI·실제 자동제안 타이핑 증거는 아니므로 그 경로에서 rate limit, timeout/cancel, 2분 residency를 확인한다. |
|
||
| GAP-INPUT-06 | Input intelligence | `[~]` **제안 폭주 상한 및 runtime 확인.** 이전 24.7 s / 4.9 s, boot warmup, `keep_alive: 30m`, 48/96/128 tokens 및 2/5 candidates 기록은 역사적 가설/완화 이력이며 현재 정책이 아니다. 현 정책은 600 ms debounce, 5 s minimum interval, 6 requests/min default (hard max 12), 3 candidates, 64 output tokens, 12-character growth, 8 s timeout, `keep_alive: 2m` 및 boot warmup 제거다. | `apps/desktop/src/main/services/SuggestionService.ts`, `packages/core/src/input-intelligence.ts`, `LocalLLMService.ts` | 표적 자동 검증은 6 files / 69 passed / 0 failed이며 raw Ollama의 15.044 s cold timeout cleanup, 약 119.9 s residency expiry, 그리고 19:48:59 +09:00에 새 generate/unload/kill/retry 없이 확인한 `/api/ps` HTTP 200 / 45.8 ms / `{models:[]}` (`/api/version` HTTP 200 / 7.3 ms / `0.32.13`)도 확인됐다. 전체 strict typecheck는 green이 아니며, 이 raw API unload 증거는 앱 재시작·GUI·실제 자동제안 타이핑 검증이 아니다. 그 경로에서 rate limit, 8 s abort, 3-candidate/64-token 상한을 확인한다. |
|
||
|
||
| GAP-SYNC-01 | Sync | Desktop↔mobile sync was one-way and lossy. (1) `pushAll` set the cursor to *now* before `pullAll` ran, so the first pull after sign-in fetched nothing — mobile-created data never reached the desktop except via Realtime while running (which never connected, GAP-SYNC-02). (2) `_inArray` used only the first meeting id, so memos/documents of every other meeting were never pushed. (3) No deletion propagation either way. (4) Pull had no `user_id` filter — team meetings of other users landed in the personal DB and a desktop re-push cleared `team_id`. (5) One global, local-clock cursor across accounts; no pagination (1,000-row cap). (6) Desktop writes did not move `history.revision`, so mobile's optimistic concurrency silently overwrote desktop edits. (7) One case-duplicate dictionary word failed the whole batch. (8) Memo tags, custom commands, templates, favorites and history titles were not synced at all; anonymous local-mode data was never uploaded after sign-in. | `apps/desktop/src/main/services/sync/*`, `CloudSyncService.ts`, `server/supabase/migrations/20260927000034_cross_device_sync_parity.sql` | `[x]` 2026-09-27: engine rewritten (outbox + per-user server-clock keyset cursors + tombstones + per-row isolation + dictionary adoption + natural-key memo tags + client-keyed template RPCs + local-mode import). Evidence: `apps/desktop/tests/main/sync/SyncEngine.test.ts` (20) + `apps/desktop/tests/integration/cross-device-sync.supabase.test.ts` (5, local Supabase stack); full desktop suite 1499/1500 (1 = known caption env timeout); SQL checks for revision bump, tombstones, account-delete cascade. |
|
||
| GAP-SYNC-02 | Sync | **Desktop Realtime never connected.** Electron 33 main (Node 20.18) has no global `WebSocket`; supabase-js Realtime only ever logged `TIMED_OUT`/`CLOSED` (installed-app log 2026-09-16) and gave up after three retries. Also: filtered `postgres_changes` channels never deliver DELETE (Supabase limitation). | `CloudSyncService.ts`, `services/sync/realtime-transport.ts`, integration test "Realtime" case | `[x]` 2026-09-27: `ws` injected as the Realtime transport (`ws` now a declared desktop dependency, externalised in the main bundle); deletions arrive as `sync_tombstones` INSERT events; the heartbeat re-subscribes a channel that is not `joined`. Verified live against the local stack (INSERT/UPDATE + tombstone events received). |
|
||
| GAP-SYNC-03 | Portability | Mobile/cloud data export failed for any account with meetings or generated documents: `export_account_portability` used `to_jsonb(row)` while the v1 archive contract (server restore + mobile `exactKeys`) is a frozen key set, and later migrations added columns. | `server/supabase/migrations/20260927000034_cross_device_sync_parity.sql` §7 | `[x]` 2026-09-27: export projects each dataset onto its v1 keys; integration test asserts the memo/meeting key sets. |
|
||
| GAP-SYNC-04 | Mobile devices | Mobile `normalizeRegisteredDevice` rejected every platform except `android`/`ios`, so the first desktop/web device on an account would have made the whole Devices screen fail to load. | `apps/mobile-rn/src/features/devices/device-service.ts`, `screens/DevicesScreen.tsx` | `[x]` 2026-09-27: accepts the server platform list (`android/ios/web/windows/macos`), card shows the platform; `__tests__/devices.test.ts` 3/3, full mobile Jest 372/372. |
|
||
| GAP-SYNC-05 | Sync | **Not yet in production.** Migration `20260927000034` is applied only to the local stack; the rewritten desktop sync ships with the next desktop release. Until the migration is deployed, desktop tombstone pulls and template RPCs fail against production (they are isolated per step, other entities keep syncing). | migration file, `apps/desktop/src/main/services/sync/` | `[~]` 2026-09-27: production `supabase db push` applied `0029`–`0035` (backup dump taken first; see GAP-OPS-01), all 23 Edge Functions redeployed, v1.7.0 published (runtime/portable first, then the updater feed) and installed on the owner PC. **Remaining:** the desktop on that PC is in local mode — one Google sign-in in Settings > Cloud, then edit/delete on the phone and confirm the desktop updates within seconds and appears under Devices. |
|
||
| GAP-SYNC-06 | Sync | Still desktop-local (not mirrored): knowledge base (desktop local RAG embeddings vs cloud pgvector — different embedding spaces), audio (desktop WAV files vs mobile Storage uploads), `user_settings` (locale, auto-polish, active command), desktop preset-command prompt edits, meeting `transcripts` segments (desktop uses `raw_transcript`, which mobile also fills). | `docs/map/10-feature-catalog.md` MEM-09/MEM-05/SHELL rows | `[x]` 2026-09-27: knowledge docs sync as source text and are re-embedded per surface; recordings upload to the mobile storage contract (≤50 MiB per file, opt-out toggle) and remote recordings play through signed URLs; `user_settings` maps the shared fields. 2026-09-27 (later): the two items first left out are done too — meeting `transcripts` segments are rebuilt from the desktop transcript (the phone renders segments before `edited_transcript`, so desktop edits were invisible there) and preset prompt edits reach the phone through `sync_set_builtin_instruction_prompt_v1` (migration `0036`, production applied; `tests/main/sync/SyncTranscriptsPresets.test.ts` (4) + local-stack integration case "전사 구간·프리셋"). Evidence: `tests/main/sync/SyncExtensions.test.ts` (9) + integration case "지식 문서·설정·녹음" against the local Supabase stack. |
|
||
| GAP-SYNC-07 | Sync | `sync_tombstones` grows without a schedule. `prune_sync_tombstones_v1(interval)` (service_role) exists; nothing calls it. | migration §1 | `[x]` 2026-09-27: migration `0035` enables `pg_cron` and schedules `prune-sync-tombstones` daily at 03:17 UTC (production `cron.job` active=true). A client whose cursor is older than 180 days must fall back to a full resync. |
|
||
|
||
| GAP-OPS-01 | Ops | **Production Supabase was five migrations behind** (`0029` content reporting, `0030` profile column ACL — users could raise their own `role`/`tier` through PostgREST, `0031` audit-log actor unlink, `0032` meeting-document reporting, `0033` team activity feed) and `content-report` was never deployed, while the map listed those features GREEN from local runs. Most functions still ran the 2026-08-21 build. | `server/supabase/migrations/`, Management API `schema_migrations`, `functions` list | `[x]` 2026-09-27: schema+data dump taken, `db push` applied `0029`–`0035`, all functions redeployed (unauthenticated calls return 401, i.e. live and gated); `authenticated` can no longer UPDATE `profiles.tier`; real-account export returns exactly the v1 keys. Next: add a CI check that compares `supabase migration list --linked` with the repo so production cannot silently lag again. |
|
||
---
|
||
|
||
## 2. Mobile checklist roll-up (from `MOBILE_APP_COMPLETION_SSOT.md` §4)
|
||
|
||
These are the mobile SSOT rows still `[ ]` / `[~]`. Do not duplicate the full text here; open the SSOT for detail.
|
||
|
||
| Group | Open rows | Theme |
|
||
|---|---|---|
|
||
| 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 |
|
||
| 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 |
|
||
| 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 |
|
||
|
||
---
|
||
|
||
## 3. External blockers (`EXT`) — require action outside the repo
|
||
|
||
| ID | Blocker | What is needed | Where tracked |
|
||
|---|---|---|---|
|
||
| EXT-FIREBASE-01 | No production Firebase project | Android FCM only; web/iOS can avoid Firebase (see push transport doc). Create project + Android app + Play fingerprint + FCM, or adopt UnifiedPush for Android. | SSOT EXT-011, `docs/deployment/push-transport-without-firebase.md` |
|
||
| EXT-ADMOB-01 | AdMob `검토 필요` / `광고 게재 제한` / store not linked / payment profile incomplete | Complete console review + store link + payment profile. Playwright tooling ready: `scripts/admob-login.mjs` (one interactive login on a visible desktop) then `scripts/admob-automate.mjs --apply` creates/verifies units and reports store link. Neither persisted profile is signed in yet. | SSOT §0, EXT-006 |
|
||
| EXT-PLAY-01 | Play Billing license tester + test payment method | Configure license testers | SSOT EXT-004 |
|
||
| EXT-PLAY-02 | Play product/offer/base-plan + tracks | Create Pro/Pro+ products and tracks | SSOT EXT-003 |
|
||
| EXT-PLAY-03 | Production AAB + CI secret injection + recovery backups | Inject Firebase/AdMob/signing/evidence CI secrets | SSOT EXT-008 |
|
||
| EXT-PLAY-04 | Closed test 0/12 members, 14 days; production access disabled | Run closed test, request production | SSOT EXT-010 |
|
||
| EXT-OAUTH-01 | GitHub/Apple provider secrets; real mobile Google consent→callback | Configure providers, verify consent | SSOT EXT-001 |
|
||
| EXT-APPSIGN-01 | Live App Links still old certificate | Deploy updated `assetlinks.json`, re-verify live | SSOT App Links row |
|
||
| EXT-PHYS-01 | Physical Fold6 install/OAuth/purchase evidence | User runs the artifact on device | SSOT EXT-009 |
|
||
| EXT-PAY-01 | Payple live history + webhook signature verification | Provider contract + signature scheme | SSOT M-027 |
|
||
| EXT-STRIPE-01 | ~~Production Stripe/Payple cross-verification~~ | **삭제됨 2026-09-26** — Stripe 제거. Payple 라이브 검증은 EXT-PAY-01 | SSOT M-013 |
|
||
| EXT-STT-01 | Production provider keys (Groq/OpenAI/Deepgram/Gemini) | Inject provider secrets | `apps/api-server/Program.cs` env docs |
|
||
|
||
---
|
||
|
||
## 4. Documentation & drift watch
|
||
|
||
| ID | Item | Note |
|
||
|---|---|---|
|
||
| DOC-01 | `CLAUDE.md` describes only Phase 1-15.5 desktop; does not mention web/mobile/admin/api-server. | This map supersedes it for IA; consider trimming CLAUDE.md to rules + map pointer. |
|
||
| DOC-02 | `docs/design/*` reflects an older desktop-only architecture and contains known naming drift (`06-gap-analysis.md`). | Treat this map + code as current; design docs are historical. |
|
||
| DOC-03 | `memory/project_status.md` is a chronological log, not current state. | Use `docs/map/*` for current state. |
|
||
| DOC-04 | `docs/v2/00-v2-master-plan.md` targets Expo for mobile. | Actual mobile is RN CLI (`apps/mobile-rn`); the Expo app was removed 2026-09-13. v2 plan is historical. |
|
||
| DOC-05 | This map itself must be regenerated after large refactors. | See update protocol. |
|
||
|
||
---
|
||
|
||
## 5. Quick "is X done?" lookup
|
||
|
||
- **Desktop local dictation / LLM / history / meetings / RAG / conversation:** local
|
||
dictation works in dev **and** in packaged builds as of `1.3.0` (engine bundled, paths
|
||
fixed, IPv4 loopback). Ads: one real adapter, rest stubs.
|
||
- **Desktop LLM post-processing:** split the question. The **plain actions**
|
||
(`refine`/`summarize`/`grammar`/`expand`, Settings → `defaultLLMAction`) always worked
|
||
and are now regression-tested. The **custom-instruction path** — built-in presets,
|
||
user commands, voice keyword commands, chains — **never worked in any shipped release**
|
||
(`v0.1.0-alpha`..`v1.4.0`): it inserted the instruction's own wording instead of the
|
||
result. Fixed 2026-09-21 in `9c2b4d4` with unit tests, but **not yet verified in a
|
||
running app**, and the four related usecase test files could not execute (GAP-LLM-02,
|
||
GAP-INFRA-06). Translate is still hardcoded to English (GAP-LLM-01).
|
||
- **Desktop key bindings (CAP-16):** rewritten onto one SSOT with multiple bindings per
|
||
action and mouse-button support; **verified on Windows** by a manual run on 2026-09-21
|
||
(legacy migration, 6 actions loaded, keyboard + mouse hook live, multi-binding exercised
|
||
— GAP-KEY-01 `[x]`). macOS/Linux mouse behavior is still unconfirmed (GAP-KEY-02) and
|
||
the service has no unit test of its own.
|
||
- **Web console:** yes, feature-complete for server-shared data; knowledge upload/search and team feed implemented.
|
||
- **Mobile:** code complete for most flows and tested locally; blocked mainly by external store/console gates, plus a11y and some E2E depth.
|
||
- **Backend:** fail-closed AI proxies, RLS, billing, ads SSV implemented; push transports (FCM + webpush + APNs) and cron drain implemented.
|
||
- **Admin:** complete with deliberate "unavailable" states; support page is a stub.
|
||
|
||
---
|
||
|
||
## 6. Immediate TODO — push transports, drain, AdMob (2026-09-13)
|
||
|
||
Actionable checklist for the work started this session. Fields to fill are blank in
|
||
`.env` (git-ignored) and mirrored in `.env.example`.
|
||
|
||
**Push — server (code done, config pending)**
|
||
- [ ] Set Supabase Edge secrets for the transports you deploy: `WEBPUSH_VAPID_PUBLIC_KEY`, `WEBPUSH_VAPID_PRIVATE_KEY`, `WEBPUSH_SUBJECT` (web); `APNS_KEY_ID`, `APNS_TEAM_ID`, `APNS_PRIVATE_KEY`, `APNS_TOPIC`, `APNS_ENVIRONMENT` (iOS); `FCM_SERVICE_ACCOUNT_JSON`, `FCM_PROJECT_ID` (Android).
|
||
- [ ] Deploy the updated `send-push` (`supabase functions deploy send-push`).
|
||
- [ ] Generate a VAPID keypair (P-256) and store the private/public pair; subject must be `mailto:` or `https:`.
|
||
- [ ] Create the APNs `.p8` key in the Apple Developer portal and record Key ID + Team ID.
|
||
|
||
**Push — Cloudflare cron drain**
|
||
- [ ] Set worker vars: `SUPABASE_URL` (`wrangler secret`/`[vars]`) and batch limit.
|
||
- [ ] Set worker secret: `wrangler secret put SUPABASE_SERVICE_ROLE_KEY`.
|
||
- [ ] `wrangler deploy` from `server/cloudflare-worker` and confirm the `scheduled` runs (Cloudflare dashboard → Cron Triggers).
|
||
- [ ] Verify end-to-end: enqueue a test event (e.g. team invite) → drain → delivery row `delivered`.
|
||
|
||
**Push — clients**
|
||
- [ ] Web: add a service worker + `pushManager.subscribe`, store the JSON subscription as the registration id, register `provider: 'webpush'`. (GAP-PUSH-03)
|
||
- [ ] iOS: register the APNs device token with `provider: 'apns'`. (GAP-PUSH-03)
|
||
- [ ] Android: decide FCM vs UnifiedPush/ntfy. (GAP-PUSH-04)
|
||
|
||
**AdMob (Playwright tooling ready)**
|
||
- [ ] Run `run-admob-login.bat` (or `node scripts/admob-login.mjs`) once on a visible desktop to persist the Google session.
|
||
- [ ] Run `node scripts/admob-automate.mjs --apply` to create/verify the banner + rewarded units and report the Play-store link.
|
||
- [ ] Complete AdMob console gates externally: review, serving limits, store link, payment profile. (EXT-ADMOB-01)
|
||
|
||
**Docs**
|
||
- [ ] Keep `docs/deployment/push-transport-without-firebase.md` and this file in sync when transports or clients change.
|
||
|
||
---
|
||
|
||
## 7. 알려진 설계 제약 (수용됨 — 결함 아님)
|
||
|
||
### 7.x 기기 간 동기화 (2026-09-27 수용)
|
||
|
||
- **충돌 규칙은 "마지막 쓰기 우선 + 로컬 미전송 보호"다.** 데스크톱은 아직 올리지 않은 로컬 변경이 있는 행을 pull이 덮지 않고, 올릴 때는 서버 행을 통째로 덮는다. 모바일은 `revision`으로 충돌을 감지한다(데스크톱 쓰기도 이제 revision을 올린다). 필드 단위 병합은 하지 않는다.
|
||
- **원격 삭제가 로컬 미전송 수정보다 우선한다.** 다른 기기에서 지운 행은 데스크톱에서 편집 중이었어도 지워진다.
|
||
- **업그레이드 후 첫 동기화는 예전 삭제를 되살릴 수 있다.** 이 버전 이전에는 삭제 기록이 없었으므로, 데스크톱에만 남아 있던 행(모바일에서 지웠지만 데스크톱이 몰랐던 행)은 첫 대조에서 서버로 다시 올라간다. 데이터 유실보다 되살림을 택했다.
|
||
- **데스크톱 사용 횟수(`dictionary.usage_count`)는 자주 바뀌어 매번 올리지 않는다.** 단어를 추가·수정할 때 함께 올라간다.
|
||
- **회의 전사 구간은 데스크톱 전사에서 다시 만든다.** 폰은 구간이 있으면 구간을 그리므로, 데스크톱 전사가 바뀔 때마다 구간을 통째로 맞춘다(시각 표기가 없는 줄은 앞 줄의 시각을 잇는다). 폰은 전사를 고치지 않으므로 서버→데스크톱 방향은 `raw_transcript`로 충분하다.
|
||
- **프리셋 명령은 짝이 있는 4개만 맞춘다.** 폰은 프리셋 행을 직접 고칠 수 없고(RLS) 데스크톱에서 고친 프롬프트만 전용 RPC로 올린다. 번역의 `{{targetLanguage}}`는 English로 바꿔 올린다(양쪽 모두 영어 번역). `자유 프롬프트`는 폰에 대응이 없어 로컬이다.
|
||
- **설정은 뜻이 같은 것만 맞춘다.** 언어는 모바일이 지원하는 ko/en, 테마는 시스템/라이트/다크만(데스크톱 전용 테마는 로컬), 자동 다듬기는 켬/끔, 활성 명령은 양쪽에 있는 사용자 명령일 때만. 서버에 설정 행이 이미 있으면 첫 동기화에서 서버 값이 이긴다.
|
||
- **지식 문서는 원문만 옮긴다.** 기기마다 임베딩 모델·차원이 달라(데스크톱 768, 서버 1536) 벡터는 각자 만든다. 다른 기기에서 온 문서에는 원본 파일이 없다.
|
||
|
||
여기 있는 항목은 고쳐야 할 갭이 아니라 **대안을 검토한 뒤 의도적으로 유지하기로 한 절충**이다.
|
||
§1 에 갭으로 재등록하지 마라.
|
||
|
||
| ID | 제약 | 왜 이대로 두는가 | 완화 장치 |
|
||
|---|---|---|---|
|
||
| CONSTRAINT-INPUT-01 | 입력 레메트리는 **키 내용을 저장하지 않는다.** 카운터(키 입력·클릭·스크롤)와 마우스 이동 거리, 그리고 **사용자가 명시적으로 동의한 경우에만** 읽은 텍스트(학습 DB)만 남긴다. 키코드→문자 복원은 시도하지 않는다. | ActivityWatch `aw-watcher-input` 이 같은 이유로 같은 선택을 한다 — 전역 키 내용 수집은 보안·프라이버시 파급이 크고, 신뢰를 잃으면 기능 자체가 사라진다(README 원문: "This does not track which keys you press … This is not a keylogger, and never will be"). 게다가 키코드 복원은 한/일 IME 에서 원리적으로 불가능하다(조합 결과가 텍스트다). | 수집은 옵트인(`inputTelemetryEnabled`/`inputLearnTypedText` 기본 false), 비밀번호 필드는 `IsPassword` 로 fail-closed 차단, 앱별 제외, 30일 보존 후 자동 삭제, "수집된 데이터 삭제" 버튼(동의 철회 시 즉시 삭제 + 학습 플래그 해제), 전송 없음(로컬 SQLite 전용). |
|
||
| CONSTRAINT-I18N-01 | `packages/core/src/keybinding.ts` 는 i18n 키를 평범한 `string` 으로 노출한다. 렌더러가 `asTranslationKey()`(`apps/desktop/src/renderer/components/keybinding/translation-key.ts:7`)로 경계에서 캐스팅하므로, 존재하지 않는 키를 넘겨도 컴파일러가 잡지 못한다. | core 가 로케일 패키지에 의존하지 않게 하려는 의도적 설계다. 검토한 대안 둘 다 성립하지 않는다 — (A) 키 필드를 리터럴 유니온으로 좁히는 방식은 `KEY_CATALOG` 가 `letterEntries()` 같은 함수 생성부를 포함해 불가능하고, (B) core 가 `@d3ro/i18n` 의 타입 가드를 쓰는 방식은 의존 방향을 core → i18n 으로 역전시켜 `03-shared-packages.md` §6 의 전제를 깬다. 2026-09-21 결정: 현행 유지. | `packages/core/__tests__/keybinding-i18n.test.ts` (14 케이스). core 가 참조하는 키가 12개 로케일 전부에 있는지, 값이 빈 문자열이 아닌지, core 가 렌더러 전용 `keybinding.ui.*` 를 참조하지 않는지 검사한다. 거부 사유 키는 하드코딩 목록이 아니라 실제 `validateBinding` 경로를 태워 수집하므로 새 사유가 생기면 자동으로 커버된다. |
|