65 KiB
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). External items are markedEXT; 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 update10-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되면 스케줄이 등록된다. 백로그(사용자 결정): 웹 Payple 클라이언트 키는 아직 넣지 않는다. 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 — 둘 다 터널 kd-nas → NAS 3001). 원하는 이름 admin.d3ro.chanpaca.net은 2단계 서브도메인이라 Cloudflare Universal SSL이 덮지 못해 고급 인증서(ACM, 월 $10)가 필요하다. |
Cloudflare 터널 kd-nas ingress, chanpaca.net DNS | [ ] 2026-09-26 사용자 결정: ACM은 지금 구독하지 않는다. 주소 하나로 합칠지(무료인 d3ro-admin.chanpaca.net)는 미정. 참고: chanpaca.net의 기존 고급 인증서(*.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 상한을 확인한다. |
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 | 삭제됨 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 in9c2b4d4with 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:orhttps:. - Create the APNs
.p8key 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 deployfromserver/cloudflare-workerand confirm thescheduledruns (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, registerprovider: '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(ornode scripts/admob-login.mjs) once on a visible desktop to persist the Google session. - Run
node scripts/admob-automate.mjs --applyto 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.mdand this file in sync when transports or clients change.
7. 알려진 설계 제약 (수용됨 — 결함 아님)
여기 있는 항목은 고쳐야 할 갭이 아니라 대안을 검토한 뒤 의도적으로 유지하기로 한 절충이다. §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 경로를 태워 수집하므로 새 사유가 생기면 자동으로 커버된다. |