docs: record the 1.1.0 release and add the infrastructure map
Some checks failed
deploy-site / deploy (push) Failing after 40s

Release notes for 1.1.0 were split between an Unreleased section and the
version section, so the published notes would have omitted the update-feed
and desktop changes. Everything shipping in this version now sits under one
`## [1.1.0]` heading.

`docs/map/` becomes the entry point for what infrastructure exists per
platform and how far each feature is developed, with a documented update
protocol so feature work and this map do not drift apart again. The release
guide now states that installer binaries live in the update feed rather than
the repository.
This commit is contained in:
Yun Chan 2026-09-16 23:27:52 +09:00
parent c2db1b2176
commit c3ddd36c6f
29 changed files with 3207 additions and 23 deletions

176
AGENTS.md Normal file
View file

@ -0,0 +1,176 @@
# AGENTS.md — D3RO Voice
Operating guide for any AI agent (or human) working in this repository.
Read this first. It tells you where the infrastructure map lives and what you
must update when you change the product.
---
## 1. What this repo is
`D3RO Voice` is a multi-platform AI voice assistant monorepo:
- `apps/desktop` — Electron desktop app (local-first: SoX + faster-whisper sidecar + Ollama + SQLite)
- `apps/web` — Next.js cloud console
- `apps/mobile-rn` — React Native product mobile app (cloud-first; **active**)
- `apps/api-server` — ASP.NET Core 10 AI proxy + back office backend
- `apps/admin` — Next.js admin back office
- `server/supabase` — Postgres + RLS + Auth + Storage + ~27 Deno Edge Functions (canonical product backend)
- `server/cloudflare-worker` — edge gateway
- `site/` — landing/marketing/legal site
- `packages/*` — shared `@d3ro/core`, `ui`, `ui-native`, `i18n`, `api-client`
Full detail: [`docs/map/01-system-overview.md`](./docs/map/01-system-overview.md).
---
## 2. The map — read before you work
**`docs/map/` is the authoritative feature & infrastructure map.** Use it to learn
what infrastructure exists and how far each feature is developed before writing code.
Start at [`docs/map/00-index.md`](./docs/map/00-index.md). Key files:
| Need | File |
|---|---|
| Map index + status legend + how to use | `docs/map/00-index.md` |
| Product / IA / identity / pipeline | `docs/map/01-system-overview.md` |
| Repo layout, build, CI/CD, Docker, deploy | `docs/map/02-infrastructure.md` |
| Shared packages | `docs/map/03-shared-packages.md` |
| Desktop services/IPC/pages | `docs/map/04-desktop-app.md` |
| Web routes/components | `docs/map/05-web-app.md` |
| Mobile screens/features | `docs/map/06-mobile-app.md` |
| .NET API | `docs/map/07-api-server.md` |
| Admin console | `docs/map/08-admin-console.md` |
| Supabase + Cloudflare | `docs/map/09-supabase-backend.md` |
| **Feature map (per platform, with status)** | `docs/map/10-feature-catalog.md` |
| **Gaps / backlog / external blockers** | `docs/map/11-gap-backlog.md` |
| **How to keep the map current** | `docs/map/12-update-protocol.md` |
Recommended sequence for a new task:
1. Read the surface doc for the area you will touch (`04``09`).
2. Find the feature in `10-feature-catalog.md` and check its status per platform.
3. Check `11-gap-backlog.md` so you do not re-plan tracked work.
4. Do the work.
5. Update the map **before** declaring done (see §4).
Mobile status is authoritative in `docs/v3/MOBILE_APP_COMPLETION_SSOT.md`; the map
mirrors and rolls it up. When they disagree, fix the SSOT first, then the map.
---
## 3. Non-negotiable repo rules
These come from [`CLAUDE.md`](./CLAUDE.md) and are enforced here too:
- TypeScript strict, no `any`, no `console.log` (use `logger`).
- Services are singletons + `EventEmitter`. IPC channels are `${feature}:${action}` from the SSOT `packages/core/src/ipc-channels.ts`; wrap errors in `D3ROError`/`ErrorCode` and `IPCResult`.
- Theme SSOT: only `d3roPalette`/`d3roShadow`/`d3roTypo`/`d3roRadius`; no hardcoded hex outside `theme.ts`.
- i18n: use `t()`; no hardcoded Korean/English strings in UI.
- No `Co-Authored-By` or Claude-related wording in commits. Follow the commit format in the shell tool instructions.
- **Desktop GUI**: do not background-launch Electron from an agent subshell (virtual desktop). Tell the user to run `run-desktop.bat` or an external terminal.
- **Audio device discovery**: never `execSync` (use async `exec`).
- **GPU**: keep `app.disableHardwareAcceleration()` and `--disable-gpu`.
- Desktop: keep `app.setName('d3ro-voice')` + `app.setAppUserModelId` at the top; prevent dev silent exit.
- Never hardcode secrets; production secrets are injected at build/CI time. Fail closed, never fake success.
- Security-first: only defensive security work.
Build/test commands: [`docs/map/02-infrastructure.md`](./docs/map/02-infrastructure.md) §3. Root: `npm run typecheck`, `npm run test`, `npm run lint`. Mobile: `npm --prefix apps/mobile-rn run lint|typecheck|test`.
---
## 4. MANDATORY: keep the map updated
> **Feature change ⇒ map change, in the same commit / PR.**
> A feature is not done until the map reflects it.
Whenever you **add, remove, change, or defer** a feature, or touch infrastructure,
you MUST update the map:
1. Edit `docs/map/10-feature-catalog.md` (status per surface + notes/anchors).
2. Edit the relevant surface doc (`04``09`) if you changed services, routes, screens, IPC channels, tables, Edge Functions, or build/CI.
3. Edit `docs/map/11-gap-backlog.md`:
- resolved gap → `[x]` + date + evidence;
- newly deferred item → new row + next step;
- cleared external blocker → `[x]` + evidence.
4. Reconcile with `docs/v3/MOBILE_APP_COMPLETION_SSOT.md` for mobile changes.
5. Update `docs/map/02-infrastructure.md` and the `00-index.md` header dates when build/release/CI or the product version changes.
Full procedure and templates: [`docs/map/12-update-protocol.md`](./docs/map/12-update-protocol.md).
**Status semantics:** `[x]` verified · `[~]` partial/unverified · `[ ]` planned/absent · `[!]` blocked externally · `[-]` N/A. Never mark `[x]` on typecheck alone.
---
## 5. Definition of done (feature work)
- [ ] Code implemented, wired end-to-end (no TODOs / placeholders / fake success).
- [ ] Tests added/updated and passing; lint/typecheck clean on the touched scope.
- [ ] Relevant surface doc (`04``09`) updated if interfaces changed.
- [ ] `10-feature-catalog.md` row updated (status per platform + anchors).
- [ ] `11-gap-backlog.md` updated (closed/opened/external).
- [ ] Mobile SSOT reconciled if mobile was touched.
- [ ] No secrets committed; fail-closed preserved.
---
## 6. Update & release system
Desktop auto-update runs `electron-updater` against the **canonical Forgejo feed**
`https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/latest`.
The GitLab project 1172 registry is a **legacy mirror** for pre-Forgejo installs;
runtime must never point at it. Full evaluation and 2026 methodology:
[`docs/deployment/update-system-assessment.md`](./docs/deployment/update-system-assessment.md).
These files move together and are enforced by `npm run release:metadata:test`:
| Concern | SSOT |
|---|---|
| runtime feed + channels | `apps/desktop/src/main/update-feed.ts` |
| builder publish URL | `apps/desktop/electron-builder.yml` |
| update policy (channels, min version, force, delta/full, rollout, kill switch) | `release/update-policy.json` |
| policy runtime logic | `apps/desktop/src/main/update-policy.ts` |
| canonical publisher | `scripts/ci/publish-forgejo-release.mjs` |
| legacy mirror publisher | `scripts/ci/publish-gitlab-release.mjs` |
| version SSOT | `release/product-version.json` |
Rules:
- **Tag-only, immutable releases.** Publish runs only on annotated `vX.Y.Z` tags.
Tags are never moved or deleted; a bad release is fixed forward with a higher
patch. Same-version re-release is forbidden by SemVer.
- **Feed contract.** `UPDATE_FEED_URL` == builder `publish.url` == canonical
Forgejo URL. `LEGACY_UPDATE_FEED_URL` must stay the GitLab mirror.
- **Version discipline.** SemVer: `feat`=MINOR, `fix`=PATCH, breaking=MAJOR. Run
`npm run version:sync` when syncing surfaces; `npm run version:check` must be clean.
- **Channels.** `latest` (stable), `beta`, `alpha`. Stable clients never receive a
prerelease unless their channel allows it.
- **Major vs incremental.** `release/update-policy.json` decides whether an update
is mandatory (`minimumSupportedVersion`, `forceInstallBelow`) and whether to use a
full installer instead of a blockmap delta (`fullInstallOnMajorChange`,
`fullInstallVersionGap`). `stagingPercentage` limits rollout; `killSwitch` stops
update checks remotely.
- **Secrets stay fail-closed.** Every publisher needs `FORGEJO_TOKEN`
(`write:package` + `write:repository`). Desktop releases additionally require the
`WIN_CSC_*` Authenticode material; missing signing data must fail the pipeline.
Commands: `npm run release:metadata`, `npm run release:metadata:test`,
`npm run release:forgejo:check`, `npm run release:tag -- --dry-run` (then
`npm run release:tag`, then `git push origin vX.Y.Z`). Use `--sign` when GPG is
configured; tags are annotated and immutable. Locally, `npm run release:forgejo:local`
loads `FORGEJO_TOKEN` from `.env` (gitignored); CI reads the secret from its store.
---
## 7. Where deeper context lives (optional)
- Design (historical desktop architecture): `docs/design/*`
- Build-out phases: `docs/phases/*`
- Multi-platform plan: `docs/v2/*`
- Mobile SSOT + Play package: `docs/v3/*`
- Monetization: `docs/monetization-plan.md`
- Deployment/release: `docs/deployment/*`, `release/*`, `scripts/ci/*`
- Refactor policy + reports: `docs/REFACTOR_POLICY.md`, `docs/REFACTOR_WAVE*_REPORT.md`
- Project log/handoffs: `memory/*`, `CHANGELOG.md`
Treat those as history; treat `docs/map/*` and the code as current.

View file

@ -22,6 +22,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Generated-content safety controls**: added generation receipts, shared generative-AI safety instructions, and an authenticated report flow for owned AI-generated meeting documents.
- **Release verification tooling**: added Android artifact, App Links, production Firebase/AdMob configuration, Play asset, secret scanning, signed provenance, and publication-boundary checks.
- **Complete mobile icon set**: added canonical Android legacy/adaptive/monochrome launchers, the 512px Play icon, and all required iPhone, iPad, and App Store marketing icon slots.
- **Canonical desktop release channel**: desktop auto-update now reads one Forgejo Generic Package Registry feed, published by a version-agnostic publisher (`scripts/ci/publish-forgejo-release.mjs`) from a tag-triggered Forgejo Actions workflow. GitLab CI and GitHub Actions converge on the same publisher, and the GitLab registry stays a legacy mirror for pre-Forgejo installs.
- **Update policy SSOT** (`release/update-policy.json`): channels (`latest`/`beta`/`alpha`), minimum supported version, forced install, full-installer thresholds, staged rollout percentage, and a remote kill switch, enforced at runtime by `apps/desktop/src/main/update-policy.ts`.
- **Dictionary import/export on desktop and web**: round-trip import with per-entry conflict reporting, a web dictionary client, and an expanded knowledge add form.
- **Multi-transport push delivery**: Web Push (VAPID) and token-based Apple Push (APNs) transports join Firebase Cloud Messaging, with a Cloudflare Worker cron drain over an outbox table.
- **Team activity feed**: team activity events, migration, and the web feed component.
- **Shared entitlement gating** in `@d3ro/core` for free/paid feature boundaries.
- 21 unit tests for update policy decisions and feed helpers, plus tests for dictionary I/O, entitlements, push drain, Web Push, and APNs payloads.
### Changed
- Unified mobile authentication and invitation links on the canonical `d3ro-voice` app scheme and added fail-closed verification for the HTTPS App Links contract.
@ -32,6 +39,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Finalized benchmarked `ko-KR`/`en-US` Play listing copy and a Console-previewed
1024×500 canonical feature graphic with preserved generation prompt, source,
output, and hashes.
- Desktop runtime updater feed moved from GitLab project 1172 to the canonical
Forgejo registry; `electron-builder.yml`, the metadata verifier, and both CI
publishers now enforce the canonical/mirror split.
- Release metadata verifier self-test expanded to 13 negative cases covering the
feed contract, policy schema, and Forgejo publisher invariants.
- Landing site and web console download centers now link the canonical Forgejo feed
instead of repository-local installer paths, which are not part of any deploy
artifact.
- Admin console data views (models, pipelines, users, subscriptions, audit log) read
live back-office data, with a unified sidebar and console theme.
- Desktop settings, ad surfaces, license, and meeting-export UI aligned on the shared
theme tokens; meeting export filenames now go through one sanitizer.
### Security
- Added atomic authorization and replay protection for teams, invitations, push delivery, transcription quotas, billing, ad rewards, administrative actions, data portability, and content reports.
@ -43,6 +62,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Corrected realtime Edge Function model routing and type checks for team and enterprise tiers.
- Removed the legacy `d3ro://` deep-link surface to prevent divergent authentication callback identities.
- Removed the Forgejo release-publishing prohibition; the legacy hardcoded-version
script is replaced by a version-gated publisher.
- Dropped a stale admin bundle from the API server web root, and stopped tracking
.NET build output, Playwright run output, and developer-only automation scripts in
the release checkout.
## [0.2.1-alpha] - 2026-07-22

View file

@ -2,6 +2,10 @@
Speakly RE 기반 완전 로컬 음성 어시스턴트. Ollama + Whisper + TTS.
> **에이전트 진입점**: 작업 전 `AGENTS.md`를 읽고, 기능/인프라 현황은 `docs/map/`
> 참조한다. 기능 추가·삭제·변경·백로그 이관 시 `docs/map/10-feature-catalog.md`
> `docs/map/11-gap-backlog.md`를 반드시 함께 갱신한다 (규칙: `docs/map/12-update-protocol.md`).
## 기술 스택
Electron 33+ | React 19 + MUI 7 + Vite | TypeScript 5.7+ strict
better-sqlite3 + drizzle-orm | faster-whisper (Python sidecar) | Ollama REST
@ -39,6 +43,14 @@ npm run typecheck # tsc --noEmit
Phase 1~15.5 전체 완료 + 랜딩 페이지(site/).
차단 이슈: @nut-tree-fork/nut-js 포크 사용
## 업데이트 / 릴리스
자동 업데이트는 electron-updater + **canonical Forgejo feed**
(`git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/latest`). GitLab project 1172은
legacy mirror다. 채널·최소 지원 버전·강제 업데이트·증분/전체·staged rollout·킬 스위치는
`release/update-policy.json`이 정본이다. 릴리스는 annotated `vX.Y.Z` 태그에서만 게시하며
태그는 불변이다. 규칙은 `AGENTS.md` §6, 평가·방법론은
`docs/deployment/update-system-assessment.md` 참조.
## 설계 문서 (구현 시 Read 도구로 참조)
- docs/design/00-master-architecture.md — 시스템 아키텍처, 초기화/종료, 서비스 목록
- docs/design/01-service-specifications.md — 서비스 인터페이스, 상태머신, 이벤트

View file

@ -0,0 +1,126 @@
# Push transport without Firebase — analysis
> Status: decision note (2026-09-13)
> Scope: `send-push` Edge Function, `server/supabase/migrations` push outbox, `apps/mobile-rn` notification stack
> Question: we already pay for Supabase. Can it replace Firebase for notifications?
---
## 0. TL;DR
- **Supabase already is the push backend.** Tokens, devices, durable outbox, leases, retries, and delivery audit all live in Supabase (`push_tokens`, `devices`, `push_deliveries`, `push_dispatch_attempts`, `send-push`). Firebase is only the **last hop** to the device.
- **Web and iOS are now Firebase-free on the server side.** Web Push (VAPID + RFC 8291 aes128gcm) and Apple APNs (`.p8` token) transports are implemented in `send-push` (`_shared/webpush.ts`, `_shared/apns.ts`, 10 unit tests). Client registration for those providers is the remaining work.
- **Android cannot drop Firebase without changing transport.** Google Play Services only delivers push through FCM, and the FCM registration token is minted by the Firebase Messaging SDK, which requires `google-services.json` (a Firebase project). Supabase cannot substitute for that hop.
- **The scheduled drain was missing and is now added.** Nothing in the repo triggered `send-push?mode=drain`, so transactionally enqueued notifications never left the outbox. A Cloudflare Cron Trigger now drains it every minute.
- **Firebase-free Android is possible but is a different product decision:** UnifiedPush + a self-hosted distributor (e.g. ntfy). Third-party push services (OneSignal, Pusher) do **not** remove the Firebase dependency; they wrap FCM.
---
## 1. What exists today
| Layer | Owned by | Notes |
|---|---|---|
| Token + device registry | Supabase | `push_tokens`, `devices`; writes via `register_push_registration` RPC; RLS denies direct access |
| Durable outbox + retry/lease | Supabase | `push_deliveries`, `push_dispatch_attempts`, `claim_due_push_dispatches`, `finalize_push_delivery` |
| Dispatch orchestration | Supabase Edge `send-push` | Auth, event resolution, batching, stale-token handling |
| **Transport** | **FCM** | `_shared/push-contract.ts``https://fcm.googleapis.com/v1/projects/<id>/messages:send`, OAuth2 via `FCM_SERVICE_ACCOUNT_JSON` |
| Android client | Firebase Messaging SDK | `com.google.firebase:firebase-messaging`, `D3ROFirebaseMessagingService`, `google-services.json`; `FIREBASE_CONFIGURED` build flag |
Provider values accepted but unimplemented (`send-push/index.ts`):
```
const unsupported = deliveries.filter((delivery) => delivery.provider !== 'fcm')
→ finalizeDelivery(..., 'permanent_failure', 'push_provider_not_supported')
```
So: the only Firebase-specific pieces are (a) the FCM HTTP v1 send + service account, and (b) the Android client token source.
---
## 2. Platform-by-platform answer
### Desktop — no change needed
In-app events use IPC/EventEmitter and Supabase Realtime. No push transport, no Firebase.
### Web — Firebase-free, implementable now
- Standard **Web Push** with **VAPID** keys. The browser Push API + service worker handles delivery; the server signs with a VAPID private key and POSTs to the subscription endpoint.
- Supabase plan: store `push_subscriptions` (endpoint + keys per browser), send from `send-push` with a VAPID JWT (ES256).
- No Firebase, no Google account. APNs not involved.
### iOS — Firebase-free, implementable now
- **APNs** directly: `.p8` key + Key ID + Team ID → JWT (ES256) → `https://api.push.apple.com/3/device/<token>`.
- Needs an Apple Developer membership (already needed for App Store), **not** a Firebase project.
- Supabase Edge Function can hold the `.p8` as a secret and sign the JWT with WebCrypto.
### Android — Firebase-free only via UnifiedPush
- Google Play Services delivers notifications through FCM. The official `FirebaseMessaging` SDK mints the registration token, and it requires `google-services.json`. There is no supported way to obtain an FCM token without a Firebase project.
- FCM HTTP v1 also requires an OAuth2 service account with the `firebase.messaging` scope, which means an FCM-enabled Google Cloud/Firebase project (the Firebase *console* is not strictly required, but the project is).
- Firebase-free options:
1. **UnifiedPush + self-hosted distributor (ntfy)** — open standard; the app registers with a distributor (`ntfy`, NextPush) which holds a persistent connection; the server POSTs to the distributor. No Google. Requires the user to have a distributor installed (or the app to bundle one) and adds a background-connection battery cost.
2. **Own persistent WebSocket / foreground service** — free of Google but unreliable: Android Doze/App Standby kills sockets, so notifications cannot wake a killed app. Acceptable only for in-app live updates while running.
3. **Third-party (OneSignal, Pusher Beams, Expo Push)** — still FCM/APNs underneath. Does not remove Firebase for Android.
---
## 3. What "Firebase" actually costs us today
- A Firebase project + `google-services.json` committed for the Android build (build blocks release if missing).
- A service account JSON in Supabase secrets (`FCM_SERVICE_ACCOUNT_JSON`) and `FCM_PROJECT_ID`.
- No Firebase database/auth/storage/analytics is used. It is a push-only dependency.
If the goal is "no Firebase at all", the decision reduces to: replace Android transport (UnifiedPush) or accept FCM on Android only.
---
## 4. Recommended path
1. **Keep Supabase as the SSOT/outbox** (already true). Do not move token/outbox logic.
2. **[done] Implement `webpush` + `apns` transports** in `send-push` (`_shared/webpush.ts`, `_shared/apns.ts`).
3. **For Android, offer two modes behind config:**
- `fcm` (default): minimal Firebase project, push-only usage.
- `unifiedpush` (opt-in): self-hosted ntfy distributor; documented UX and battery caveats.
4. **Do not adopt OneSignal/Pusher** as a "Firebase replacement" — it does not remove the dependency.
### Effort sketch
| Item | Effort | Risk |
|---|---|---|
| Web Push (VAPID) transport + tests | SM | low — **done** |
| APNs (JWT ES256 `.p8`) transport + tests | M | medium — **server done**, Apple key + client registration pending |
| Cloudflare cron drain | S | low — **done** |
| UnifiedPush Android (client lib + distributor + server transport) | L | high (UX, battery, distributor dependency) |
| Minimal Firebase project for Android FCM | S (external action) | low |
---
## 5. Implemented (2026-09-13)
| Piece | Where | Notes |
|---|---|---|
| Web Push transport | `_shared/webpush.ts` | VAPID ES256 JWT + RFC 8291 `aes128gcm` encryption; strict config/subscription validation; 200/404/410/413 mapping |
| APNs transport | `_shared/apns.ts` | Token-based `.p8` ES256 JWT (cached ~50 min); production/sandbox hosts; stale (`BadDeviceToken`/`Unregistered`) and 403 handling |
| Provider routing | `send-push/index.ts` | `fcm` / `webpush` / `apns` dispatched with per-delivery stale → `stale`, payload → `permanent_failure`, else retryable; unknown providers stay `push_provider_not_supported` |
| Cloudflare cron drain | `server/cloudflare-worker/src/push-drain.ts` + `scheduled()` + `wrangler.toml [triggers] crons` | POSTs `send-push?mode=drain&limit=…` with the service-role bearer every minute |
| Tests | `_shared/webpush.test.ts`, `_shared/apns.test.ts`, `push-drain.test.ts`, `push-contract.test.ts` | 10 new tests incl. an RFC 8291 decrypt round-trip; CI runs the edge suite and the worker drain test |
### Required configuration
| Secret / var | Consumer | Purpose |
|---|---|---|
| `WEBPUSH_VAPID_PUBLIC_KEY` / `WEBPUSH_VAPID_PRIVATE_KEY` / `WEBPUSH_SUBJECT` | Supabase Edge Function | VAPID signing; subject must be `mailto:` or `https:` |
| `APNS_KEY_ID` / `APNS_TEAM_ID` / `APNS_PRIVATE_KEY` / `APNS_TOPIC` / `APNS_ENVIRONMENT` | Supabase Edge Function | APNs token auth; topic is the bundle id (`com.d3ro.voice`) |
| `SUPABASE_URL` / `SUPABASE_SERVICE_ROLE_KEY` / `PUSH_DRAIN_BATCH_LIMIT` | Cloudflare Worker | Cron drain target + auth (service role key as `wrangler secret`) |
### Remaining work
- **Client registration:** the mobile app currently registers only `fcm`. Web needs a service worker + `pushManager.subscribe` → store the JSON subscription as the registration id; iOS needs an APNs device token and `provider: 'apns'`.
- **Android decision:** keep FCM or adopt UnifiedPush (see §2/§3).
---
## 6. Related
- `server/supabase/functions/_shared/push-contract.ts` — FCM transport + provider types
- `server/supabase/functions/_shared/webpush.ts` — Web Push (VAPID + RFC 8291)
- `server/supabase/functions/_shared/apns.ts` — APNs (.p8 token)
- `server/supabase/functions/send-push/index.ts` — dispatch orchestration
- `server/cloudflare-worker/src/push-drain.ts` — cron drain
- `apps/mobile-rn/src/features/notifications/*` — client token/provider contract
- `docs/map/11-gap-backlog.md` `GAP-PUSH-01` / `GAP-PUSH-02` / `EXT-FIREBASE-01`
- `docs/v3/MOBILE_APP_COMPLETION_SSOT.md` `EXT-011`

View file

@ -1,6 +1,6 @@
# D3RO Voice 릴리스 가이드
기준일: 2026-08-29. 이 문서는 desktop GitLab 패키지·자동 업데이트와 mobile store release의 경계를 분리한다. 태그 생성이나 HTTP 200 하나만으로 배포 완료를 선언하지 않는다.
기준일: 2026-09-16. 이 문서는 desktop GitLab 패키지·자동 업데이트와 mobile store release의 경계를 분리한다. 태그 생성이나 HTTP 200 하나만으로 배포 완료를 선언하지 않는다.
## 현재 release identity
@ -15,29 +15,44 @@
| Windows Authenticode | external public-trust code-signing certificate | 현재 local `1.1.0` installer·unpacked app은 `NotSigned`; production PFX·CI secret·signed artifact GREEN 전까지 게시 금지 |
| Firebase | Console `u/0`, `u/1` 모두 D3RO project 없음 | 사용자 승인 후 project·Android app 생성 필요 |
| AdMob | app `ca-app-pub-1039714767792854~6427959892`; banner `/9840591290`; rewarded `/2255790918` | SSOT 확정. `검토 필요`·`광고 게재 제한`·store 미연결·결제 프로필 미완료 |
| updater feed | `https://gitlab.twentyoz.kr:8443/api/v4/projects/1172/packages/generic/d3ro-voice/latest` | public `latest.yml`은 아직 `0.2.1-alpha`; `1.1.0` 미배포 |
| updater feed (canonical) | `https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/latest` | Forgejo Generic Registry. GitLab project 1172은 legacy mirror |
| release notes | `CHANGELOG.md` `## [1.1.0]` | 태그 전 확정·검증 필수 |
source SSOT와 live updater metadata의 버전이 다르므로 아직 `1.1.0` 배포 완료가 아니다.
## GitLab desktop 파이프라인
## desktop 릴리스 파이프라인
```text
authoritative release commit
→ version/check/test/build GREEN
→ tag v1.1.0
annotated tag v1.1.0
→ package-windows (build-win-x64)
→ package-macos (build-mac-arm64)
→ publish-release (build-linux-x64)
├─ packages/generic/d3ro-voice/1.1.0/ 버전별 보존
├─ packages/generic/d3ro-voice/latest/ latest updater feed
└─ GitLab Release + CHANGELOG release notes
├─ publish-forgejo-release.mjs ← canonical
│ ├─ Forgejo Generic Registry /d3ro-voice/1.1.0/ (버전별 보존)
│ ├─ Forgejo Generic Registry /d3ro-voice/latest/ (updater feed + update-policy.json)
│ └─ Forgejo Release + CHANGELOG notes + 자산 첨부
└─ publish-gitlab-release.mjs ← legacy mirror (pre-Forgejo 설치본)
├─ GitLab Generic Registry /d3ro-voice/1.1.0/
├─ GitLab Generic Registry /d3ro-voice/latest/
└─ GitLab Release
```
- Forgejo·GitHub Actions도 동일한 `publish-forgejo-release.mjs`로 수렴한다. 어느
빌더가 성공해도 canonical feed는 하나다.
- `publish-forgejo-release.mjs`는 설치 자산을 먼저, `latest.yml`을 마지막에
게시하고, 공개 URL에서 재검증한 뒤 Release 자산을 첨부한다.
- 설치 바이너리는 저장소에 커밋하지 않는다. 배포 정본은 Forgejo feed이며,
`site/public/releases/*/`, `apps/web/public/releases/*/`,
`apps/api-server/wwwroot/releases/*/`는 build graph 밖이라 배포 산출물에
포함되지 않는다. 사이트·웹 다운로드 센터는 로컬 경로가 아니라 feed URL을
링크한다. (역사적 `1.0.0` 자산만 추적 상태로 남아 있다.)
- `scripts/ci/sync-version.mjs --check --tag v1.1.0`는 태그, `release/product-version.json`, package/lockfile, Android/iOS 버전 면의 일치를 fail-closed로 검증한다.
- `scripts/ci/verify-release-metadata.mjs`는 배포 메타데이터와 CI/publisher 계약을 검증한다.
- 같은 gate는 desktop license public key가 Ed25519이고 `release/product-version.json``desktopLicensePublicKeyId`와 일치하는지 검증한다. `electron.vite.config.ts`는 이 파일을 직접 읽으므로 누락·손상된 키로는 build가 시작되지 않는다.
- `scripts/ci/publish-gitlab-release.mjs`는 버전별 패키지를 먼저 올리고, `latest` 파일에서 설치 자산 참조를 검증한 후 update metadata를 마지막에 게시한다.
- `scripts/ci/publish-forgejo-release.mjs`는 canonical이다. 버전별 패키지를 먼저 올리고, `latest`에서 설치 자산 참조를 검증한 뒤 `latest.yml``update-policy.json`을 마지막에 게시하고 공개 URL에서 재검증한다. `scripts/ci/publish-gitlab-release.mjs`는 legacy mirror로 동일 자산을 GitLab에도 올린다.
- Windows installer와 `latest.yml`은 필수다. macOS 산출물이 없는 Windows-only release를 의도했다면 그 판단을 release record에 남긴다.
- `package-windows`는 external public-trust code-signing PFX를 protected file variable `WIN_CSC_PFX_FILE`로, 암호와 exact certificate subject를 protected `WIN_CSC_KEY_PASSWORD`, `WIN_CSC_EXPECTED_SIGNER_SUBJECT`로 받아야 한다. GitHub Release도 `WIN_CSC_LINK`, `WIN_CSC_KEY_PASSWORD`, `WIN_CSC_EXPECTED_SIGNER_SUBJECT`가 모두 없으면 실패한다.
- `scripts/ci/verify-windows-release-artifact.ps1`는 installer와 unpacked app의 Authenticode `Valid`, exact signer subject, non-self-signed code-signing EKU, PE version, `latest.yml` path/size/SHA-512를 검증한다. 로컬 self-signed `Everything2EverythingDev`는 production 신뢰 인증서가 아니며 gate에서 명시적으로 거부한다.
@ -45,7 +60,14 @@ authoritative release commit
## 자동 업데이트 계약
`apps/desktop/src/main/update-feed.ts``apps/desktop/electron-builder.yml`은 버전 없는 같은 public Generic Package Registry URL을 가리켜야 한다.
`apps/desktop/src/main/update-feed.ts``apps/desktop/electron-builder.yml`
버전 없는 같은 canonical Forgejo URL을 가리켜야 한다.
```text
https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/latest
```
legacy mirror(제거 예정, 런타임 참조 금지):
```text
https://gitlab.twentyoz.kr:8443/api/v4/projects/1172/packages/generic/d3ro-voice/latest
@ -53,7 +75,7 @@ https://gitlab.twentyoz.kr:8443/api/v4/projects/1172/packages/generic/d3ro-voice
아래 면이 하나라도 깨지면 release를 중단한다.
1. package registry의 무인증 public pull이 허용됐다.
1. Forgejo Generic Registry의 무인증 public pull이 허용됐다 (`FORGEJO_TOKEN`으로 게시, 익명으로 읽기).
2. `latest.yml``version`이 태그와 일치한다.
3. `latest.yml` URL/path가 같은 `latest` 경로의 실제 installer를 참조한다.
4. installer 파일명에 공백이 없다: `D3RO-Voice-Setup-<version>-x64.exe`.
@ -61,7 +83,28 @@ https://gitlab.twentyoz.kr:8443/api/v4/projects/1172/packages/generic/d3ro-voice
6. installer와 `win-unpacked/D3RO Voice.exe` 모두 external public-trust certificate의 Authenticode `Valid`이고, signer subject가 protected CI identity와 정확히 일치한다.
7. 이전 실제 설치본이 feed를 탐지하고, 다운로드·재시작·버전 상승을 끝까지 완료한다.
2026-08-29 live `latest.yml`의 버전은 `0.2.1-alpha`다. 이는 updater endpoint가 응답한다는 증거일 뿐 `1.1.0` 게시 증거가 아니다.
2026-08-29 live `latest.yml`의 버전은 `0.2.1-alpha`다(legacy GitLab feed). 이는 updater endpoint가 응답한다는 증거일 뿐 `1.1.0` 게시 증거가 아니다.
## 업데이트 채널과 메이저/증분 정책
정책 정본은 `release/update-policy.json`이고, 런타임 로직은
`apps/desktop/src/main/update-policy.ts`다. publisher가 이 파일을 feed 루트에
게시하면 실행 중 앱이 내려받아 적용한다.
| 필드 | 의미 |
|---|---|
| `defaultChannel` / `channels` | `latest`(stable) · `beta` · `alpha`. `allowPrerelease`가 false면 stable 고객은 prerelease를 받지 않는다 |
| `minimumSupportedVersion` | 이 미만 설치본은 업데이트가 **필수**(연기·건너뛰기 불가) |
| `forceInstallBelow` | 이 미만은 다이얼로그 없이 즉시 설치. null이면 비활성 |
| `fullInstallOnMajorChange` | major 승격 시 blockmap 차분 대신 전체 설치자 |
| `fullInstallVersionGap` | 같은 major에서 minor 갭이 이 값 이상이면 전체 설치자 |
| `stagingPercentage` | stable 업데이트를 노출할 사용자 비율(0~100). 강제 업데이트는 무시 |
| `killSwitch` | true면 업데이트 확인 자체를 중단 |
- **증분(delta)**: 기본. `.blockmap`으로 변경 블록만 받는다. 첫 업데이트는 항상 full이다.
- **전체(full)**: 현재 버전을 신뢰할 수 없거나, major 승격이거나, minor 갭이 임계 이상일 때 `disableDifferentialDownload`를 켠다.
- **회수(rollback)**: `stagingPercentage`를 낮추거나 `killSwitch`를 켠다. 이미 배포된 버전은 되돌리지 않고 더 높은 patch로 forward-fix한다.
## `1.1.0` 릴리스 절차
@ -70,14 +113,14 @@ https://gitlab.twentyoz.kr:8443/api/v4/projects/1172/packages/generic/d3ro-voice
3. dirty/untracked 작업을 임의로 reset·clean하지 말고, release 범위만 검토 가능한 authoritative commit으로 보존한다.
4. 같은 commit에서 lint, typecheck, test, build, release metadata·security·artifact gate를 전부 GREEN으로 만든다. Windows는 production Authenticode PFX를 주입한 CI build와 `verify-windows-release-artifact.ps1` GREEN이 필수다.
5. desktop offline license를 제공한다면 external private key를 admin의 `ADMIN_LICENSE_PRIVATE_KEY` secret로 주입하고, 저장소 public key와 sign/verify roundtrip 및 발급 감사 로그를 확인한다.
6. 이전 버전보다 높은 태그 `v1.1.0`을 생성해 push한다. 태그는 게이트를 시작하는 후속 단계지 검증을 대체하지 않는다.
7. GitLab에서 package-windows, package-macos, publish-release와 의도한 mobile job 상태를 모두 확인한다. pending/stuck/skipped를 GREEN으로 기록하지 않는다.
8. 버전별 package, GitLab Release note/asset, `latest.yml`, installer hash를 외부 public URL에서 다시 검증한다.
6. 이전 버전보다 높은 annotated 태그 `v1.1.0`을 생성해 push한다. `npm run release:tag -- --dry-run`으로 검증한 뒤 `npm run release:tag`(GPG 사용 시 `-- --sign`)와 `git push origin v1.1.0`를 실행한다. 태그는 불변이며 게이트를 시작하는 후속 단계지 검증을 대체하지 않는다.
7. GitLab에서 package-windows, package-macos, publish-release와 의도한 mobile job 상태를 모두 확인한다. publish-release가 Forgejo와 GitLab 양쪽에 게시했는지 로그로 확인한다. pending/stuck/skipped를 GREEN으로 기록하지 않는다.
8. Forgejo Release note/asset, `latest.yml`, `update-policy.json`, installer hash를 외부 public URL에서 다시 검증한다.
9. 이전 설치본에서 자동 업데이트 E2E를 실행하고 실행 중 버전·프로세스·사용자 데이터 보존을 확인한다.
## mobile release와의 경계
Desktop GitLab Release를 게시해도 Android production 출시가 자동으로 완료되지 않는다. Android는 다음을 별도로 증명한다.
Desktop release를 게시해도 Android production 출시가 자동으로 완료되지 않는다. Android는 다음을 별도로 증명한다.
- 준비된 local upload/evidence key와 AdMob identity를 protected CI secret에 주입하고, 사용자 승인 후 생성한 production Firebase config와 함께 version `1.1.0`, versionCode `1010001` AAB 생성
- package/config/upload signer/ABI/16 KB page size/signed provenance GREEN
@ -100,9 +143,14 @@ Desktop GitLab Release를 게시해도 Android production 출시가 자동으로
- `release/mobile-release-evidence-public.pem` — release evidence public key
- `apps/desktop/resources/license/production-public.pem` — desktop offline license public key SSOT
- `scripts/ci/sync-version.mjs` — 버전 면 동기화·검증
- `scripts/ci/create-release-tag.mjs` — 릴리스 태그 게이트 (annotated/서명, 불변)
- `scripts/ci/verify-release-metadata.mjs` — release metadata 자가 검증
- `scripts/ci/verify-windows-release-artifact.ps1` — Windows version·updater metadata·Authenticode gate
- `scripts/ci/publish-gitlab-release.mjs` — registry·Release·updater feed publisher
- `apps/desktop/src/main/update-feed.ts` — runtime updater URL SSOT
- `scripts/ci/publish-gitlab-release.mjs` — legacy GitLab registry·Release mirror publisher
- `apps/desktop/src/main/update-feed.ts` — runtime updater URL SSOT (canonical Forgejo + legacy mirror 상수)
- `release/update-policy.json` — 채널·최소 지원 버전·강제 업데이트·full/delta·staged rollout·킬 스위치 SSOT
- `apps/desktop/src/main/update-policy.ts` — 정책 파싱·결정 순수 로직
- `scripts/ci/publish-forgejo-release.mjs` — canonical Forgejo registry·Release·feed publisher
- `apps/desktop/electron-builder.yml` — builder publish URL·artifact contract
- `docs/deployment/update-system-assessment.md` — 평가·2026 방법론·ADR
- `docs/v3/play/04-release-checklist.md` — Play Console·AAB·closed test·production gate

View file

@ -0,0 +1,214 @@
# D3RO Voice 업데이트 시스템 평가와 2026 방법론
기준일: 2026-09-13. 이 문서는 현재 자동 업데이트·릴리스 체계를 평가하고, 2026년
9월 기준 업계 방법론에 맞춘 목표 아키텍처와 결정 사항을 기록한다. 운영 절차는
[`release-guide.md`](./release-guide.md), 정책 정본은
[`release/update-policy.json`](../../release/update-policy.json)이다.
---
## 1. 결론
**기반은 좋다. 그러나 "업데이트 시스템"이라 부르기엔 미완성이다.**
현재 데스크톱은 `electron-updater` + GitLab Generic Package Registry feed로
동작하며, 버전 SSOT·CHANGELOG gate·메타데이터 자가검증·서명 fail-closed까지
갖춘 릴리스 파이프라인을 이미 보유한다. 이 부분은 업계 표준보다 앞서 있다.
반면 다음은 없다.
- **업데이트 채널**: stable/beta/alpha 구분이 없고 `latest.yml` 하나만 존재한다.
- **메이저/증분 정책**: major 승격, 최소 지원 버전, 강제 업데이트, 버전 갭에 따른
full 재설치 판단이 전혀 없다. 모든 업데이트가 동일하게 취급된다.
- **단계적 롤아웃(staged rollout)과 킬 스위치**: 없다.
- **배포 호스트 일관성**: 제품의 공개 배포 허브(admin·site 다운로드)는 Forgejo
`git.chanpaca.net/yunchan/d3ro-voice`인데, 정작 updater feed는 GitLab
(project 1172)을 가리킨다. 게다가 `verify-release-metadata.mjs`는 Forgejo
릴리스 배포를 **금지**하고 있어 Forgejo로 릴리스할 수 없다.
- **업데이트 서명**: `latest.yml` 메타데이터 서명(Ed25519)이 "future work"로만
남아 있다. 코드 서명은 요구하지만 feed 자체 무결성은 TLS + SHA-512에만 의존한다.
**판정: `[~]` (부분).** 데스크톱 단일 채널·단일 호스트 업데이터로는 동작하지만,
다중 채널·강제 업데이트·Forgejo 배포·릴리스 관리 방법론을 갖춘 "업그레이드 체계"는
아직 아니다. 이 문서와 함께 추가된 코드가 그 격차를 메운다.
---
## 2. 현재 구현 인벤토리
| 구성 | 위치 | 상태 |
|---|---|---|
| 업데이터 서비스 | `apps/desktop/src/main/services/UpdateService.ts` | 단일 feed, 4h 주기, 동의 다이얼로그, skip version |
| feed SSOT | `apps/desktop/src/main/update-feed.ts` | GitLab project 1172 `latest` (버전 비고정) |
| 빌더 publish | `apps/desktop/electron-builder.yml` | `provider: generic`, `detectUpdateChannel: false` |
| 버전 SSOT | `release/product-version.json` | `1.1.0` / `1010001` |
| 버전 동기화 | `scripts/ci/sync-version.mjs` | 태그·패키지·lockfile·Android/iOS/.NET 일치 fail-closed |
| 메타데이터 검증 | `scripts/ci/verify-release-metadata.mjs` | feed·publisher·workflow 계약 self-test |
| GitLab publisher | `scripts/ci/publish-gitlab-release.mjs` | 버전별 + `latest`, 설치자산 우선, public 재검증 |
| Windows 산출물 gate | `scripts/ci/verify-windows-release-artifact.ps1` | Authenticode·PE version·SHA-512 |
| CI | `.gitlab-ci.yml` `package-*``publish-release` | 태그 전용 |
| Forgejo | `.forgejo/workflows/*` | **사이트 배포만**. 릴리스 배포 금지됨 |
| 릴리스 노트 | `CHANGELOG.md` (Keep a Changelog) | publisher가 섹션 누락 시 실패 |
---
## 3. 갭 상세
### 3.1 채널 부재
`detectUpdateChannel: false`이고 `updater.channel` 설정이 없어 항상 `latest.yml`
읽는다. 베타/알파 사용자를 분리할 수 없고, prerelease를 안정 채널에 노출하지 않으려면
매번 수동으로 feed를 조작해야 한다.
### 3.2 메이저/증분 판단 부재
`_promptUserConsent`는 버전과 무관하게 동일한 3버튼 다이얼로그를 띄운다.
`update-available` 이벤트에 `isMandatory` 필드가 선언돼 있지만 실제로 계산하지 않는다.
major 승격(예: 1.x → 2.x)에서 사용자가 "나중에"를 눌러 구버전에 남을 수 있고,
버전 갭이 큰 클라이언트가 differential patch를 시도하다 실패할 수 있다.
### 3.3 Forgejo 배포 금지
`verify-release-metadata.mjs:147-148`은 Forgejo workflow에
`sync-and-publish-forgejo-release`가 포함되면 실패시킨다. 이 스크립트가 버전을
`1.0.0`으로 하드코딩한 legacy이기 때문이다. 결과적으로 Forgejo에는 릴리스가
존재하지만(v1.0.0, 2026-08-20) 최신 체계에서는 갱신되지 않는다. 제품의 공개
다운로드 페이지(`site/`, `apps/web/download`, `apps/admin/releases`)는 모두
Forgejo를 정본으로 본다.
### 3.4 staged rollout / 킬 스위치 부재
`stagingPercentage`를 쓰지 않아 새 버전이 전 사용자에게 즉시 노출된다. 잘못된
릴리스 회수는 "더 높은 버전으로 forward-fix"만 가능하다.
### 3.5 메타데이터 서명 부재
SHA-512는 TLS 종단 간 무결성만 보장한다. Feed 호스트가 침해되면 악성 설치자를
서명 검증 이전에 내려줄 수 있다. 2026년 표준(Doyensec SafeUpdater, Sparkle 2.9
signed feed, Tauri ed25519 강제)은 **매니페스트 서명**을 요구한다.
### 3.6 릴리스 gate 신뢰성 저하 (발견)
`apps/desktop/package.json``typecheck``tsc --noEmit`인데 `tsconfig.json`
`files: []` + `references` 구조라 **아무 파일도 검사하지 않는다**. 실제로
`tsc -p tsconfig.node.json --noEmit`을 돌리면 다수의 기존 오류가 나온다
(`bootstrap.ts`, `support-handlers.ts` 등). 즉 "typecheck GREEN"은 데스크톱
main/preload에 대해 아무 의미가 없고, 릴리스 게이트가 이 착시에 의존한다.
(`npm run lint`와 vitest는 정상 동작.)
### 3.7 버전 번호 이력 불일치
git 태그는 `v1.0.0`에서 멈춰 있고, live `latest.yml``0.2.1-alpha`다. 제품 버전
정본은 `1.1.0`이다. 태그→릴리스 파이프라인은 "같은 커밋에서 검증"을 요구하므로
`1.1.0` 릴리스가 아직 시작되지 않았음을 보여준다.
---
## 4. 2026 방법론 (조사 요약)
### 4.1 프레임워크 선택
| 옵션 | 특징 | 판단 |
|---|---|---|
| **electron-updater + electron-builder v26/27+** | blockmap 차분, 채널, `stagingPercentage`, Windows 서명 검증, generic provider | **유지**. 이미 통합돼 있고 가장 성숙 |
| Velopack | Squirrel 대체, JS/Electron 지원, delta | 빌드 프레임워크 교체 비용 큼 |
| Tauri updater | ed25519 서명 강제, `latest.json` | Tauri 전용 |
| Sparkle | macOS 전용, 서명 feed, critical update | 교차 플랫폼 아님 |
| update-electron-app | 공식 Squirrel 경로 | 채널·staged rollout 없음 |
결론: **electron-updater를 유지하고 v27+ 보안 기본값(파일 단위 `files[]` 메타데이터,
fail-closed 서명 검증, 명시적 publish) 방향으로 설정을 정렬**한다.
### 4.2 차분(delta) vs 전체(full)
- electron-updater는 캐시된 이전 설치자를 기준으로 `.blockmap`의 변경 블록만
HTTP Range로 내려받는다. 첫 업데이트는 항상 full이다.
- **full로 강제해야 하는 경우**: (a) 현재 버전을 신뢰할 수 없음/손상, (b) major
승격(스키마·ABI 변경), (c) minor 갭이 임계 이상, (d) 서버가 Range 미지원.
- 구현: `autoUpdater.disableDifferentialDownload`를 위 조건에서 켠다.
### 4.3 채널과 staged rollout
- 채널: `latest`(stable) / `beta` / `alpha`. 클라이언트 `updater.channel` +
`allowPrerelease`로 선택. 빌드 측은 `generateUpdatesFilesForAllChannels`
채널별 메타데이터를 생성할 수 있다.
- staged rollout: `latest.yml``stagingPercentage` + 클라이언트의 영구 사용자
해시. 회수는 **더 높은 버전**으로만 가능(같은 버전 재배포 불가).
### 4.4 릴리스 관리 방법론
- **SemVer**: breaking=MAJOR, 하위호환 기능=MINOR, 버그픽스=PATCH. 릴리스된 버전은
불변(immutable). 잘못된 릴리스는 새 PATCH로 forward-fix.
- **Conventional Commits**`feat`=MINOR, `fix`=PATCH, `BREAKING CHANGE`/`!`=MAJOR.
자동화 도구는 release-please(릴리스 PR 게이트) / semantic-release(완전 자동) /
changesets(모노레포). 이 저장소는 SSOT+수동 승인 게이트를 이미 쓰므로
**release-please식 게이트를 유지**하고 커밋 컨벤션만 도입한다.
- **Trunk-based development**가 2026 기본. 릴리스 브랜치는 필요 시 JIT로 자르고
태그 후 제거. 이 저장소는 `main` + 태그 릴리스로 이미 TBD에 가깝다.
- **Git 태그**: annotated + 서명(`git tag -s`) + `v` 접두 semver. 보호 태그 규칙으로
삭제·강제 이동을 막는다. 릴리스 태그는 절대 이동하지 않는다.
- **CHANGELOG**: Keep a Changelog. `[Unreleased]`를 PR에서 유지하고 릴리스 시
날짜 섹션으로 이동. YANKED 표기.
### 4.5 CI/CD
- 코드 품질 CI는 모든 push에서, 릴리스/배포 CI는 **태그에서만**.
- 태그가 버전 SSOT·CHANGELOG·테스트·서명을 모두 통과해야 publish.
- 자산을 먼저 업로드하고 **메타데이터(latest.yml)를 마지막에** 게시해 배포 중
404를 막는다(현 publisher가 이미 구현).
- 공개 URL에서 재검증(fail-closed).
출처: semver.org, conventionalcommits.org, keepachangelog.com,
electron.build auto-update/code-signing/publish, forgejo.org packages/actions,
trunkbaseddevelopment.com, sre.google release-engineering, Doyensec SafeUpdater.
---
## 5. 목표 아키텍처
```text
┌───────────────────────────── release/update-policy.json (SSOT)
author commit → version:check → test → build (signed)
└→ tag vX.Y.Z (annotated, protected)
┌───────────────┼───────────────────────────┐
│ │ │
GitLab CI GitHub Actions Forgejo Actions
(primary) (mirror) (release hub + feed)
│ │ │
└───────────────┴──────────────┬────────────┘
publish-forgejo-release.mjs (canonical)
publish-gitlab-release.mjs (legacy mirror)
┌────────────────────────┴────────────────────────┐
▼ ▼
Forgejo Generic Registry (canonical feed) GitLab Generic Registry (legacy)
.../generic/d3ro-voice/<version|latest>/ .../generic/d3ro-voice/<version|latest>/
electron-updater → 채널(latest/beta/alpha) → 정책(mandatory/full-vs-delta)
```
- **Canonical runtime feed**: Forgejo
`https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/latest`.
- **Legacy mirror**: GitLab project 1172 `latest`. 기존 `0.2.1-alpha` 설치본이
GitLab을 계속 폴링하므로, 새 설치자가 Forgejo feed를 갖는 버전을 받을 때까지
유지한다. 마이그레이션 완료 후 제거 가능.
- **정책 SSOT**: `release/update-policy.json`. 빌드에 번들되는 기본값 + feed에서
원격 오버라이드(선택). 서버에서 정책을 내려 강제 업데이트·킬 스위치를 즉시
적용할 수 있다.
---
## 6. 결정 기록 (ADR 요약)
1. **ADR-UPD-01**: electron-updater를 유지한다. 빌드 프레임워크 교체는 비용 대비
이득이 없다.
2. **ADR-UPD-02**: Forgejo를 canonical 배포·feed 호스트로 한다. GitLab은 legacy
mirror로 남긴다.
3. **ADR-UPD-03**: 채널(latest/beta/alpha)과 최소 지원 버전·강제 업데이트·full
재설치를 `release/update-policy.json`으로 정본화한다.
4. **ADR-UPD-04**: 태그는 annotated `vX.Y.Z`이며 이동하지 않는다. 릴리스는
forward-fix만 허용한다.
5. **ADR-UPD-05**: Forgejo 배포 금지 조항을 제거하고, 버전 하드코딩 없는
`publish-forgejo-release.mjs`로 대체한다.
6. **ADR-UPD-06** (권고): 데스크톱 `typecheck`가 실제로 검사하도록
`tsc -b` 또는 `tsc -p tsconfig.node.json`로 교체하고 기존 오류를 별도
워크스트림에서 정리한다. 이 문서 범위에서는 릴리스 게이트가 착시에 의존하지
않도록 `verify-release-metadata.mjs`에 최소한의 경고를 남긴다.

77
docs/map/00-index.md Normal file
View file

@ -0,0 +1,77 @@
# D3RO Voice — Feature & Infrastructure Map (Index)
> Status: ACTIVE
> Last full audit: 2026-09-13
> Scope: entire monorepo `D:/workspace/D3ROVoice` at product version `1.1.0`
> Purpose: let any agent (or human) answer two questions in under a minute:
> 1. **What infrastructure exists?** (build, CI, services, APIs, data, packages, deploy)
> 2. **How far is each feature developed?** (per surface, with file anchors and status)
This is the entry point. Read the index, then open only the sub-document you need. Do not read all files every time.
---
## 1. How to use this map
| You need to know… | Open |
|---|---|
| The product, its IA, platforms, identity/data model | [`01-system-overview.md`](./01-system-overview.md) |
| Repo layout, build, CI/CD, Docker, deploy, scripts, docs | [`02-infrastructure.md`](./02-infrastructure.md) |
| Shared packages (`@d3ro/core`, `ui`, `ui-native`, `i18n`, `api-client`) | [`03-shared-packages.md`](./03-shared-packages.md) |
| Desktop (Electron) services, IPC, pages, popups, status | [`04-desktop-app.md`](./04-desktop-app.md) |
| Web (Next.js) routes, components, clients, status | [`05-web-app.md`](./05-web-app.md) |
| Mobile (React Native) screens, features, tabs, status | [`06-mobile-app.md`](./06-mobile-app.md) |
| .NET cloud API: controllers, services, tables, auth | [`07-api-server.md`](./07-api-server.md) |
| Admin back office (Next.js) routes, guards, status | [`08-admin-console.md`](./08-admin-console.md) |
| Supabase migrations, Edge Functions, Cloudflare worker | [`09-supabase-backend.md`](./09-supabase-backend.md) |
| **The feature map** — every feature, per platform, with status | [`10-feature-catalog.md`](./10-feature-catalog.md) |
| **Known gaps / under-developed / backlog** | [`11-gap-backlog.md`](./11-gap-backlog.md) |
| **Mandatory rules for keeping this map current** | [`12-update-protocol.md`](./12-update-protocol.md) |
An agent starting a task should:
1. Read the relevant surface doc (0409) for infrastructure.
2. Read `10-feature-catalog.md` for the feature's current status and platform coverage.
3. Read `11-gap-backlog.md` to see if the feature is already tracked as backlog.
4. After finishing, follow `12-update-protocol.md` before the work is considered done.
---
## 2. Status legend
Feature rows in `10-feature-catalog.md` use this scale:
| Symbol | Meaning |
|---|---|
| `[x]` | Implemented and verified on this platform (code + tests / evidence exist in-repo). |
| `[~]` | Implemented but partial, unverified, or blocked on an external/console gate. |
| `[ ]` | Planned or absent on this platform. |
| `[!]` | Blocked on something outside the repo (external console, secret, physical device, store review). |
| `[-]` | Not applicable to this platform (with a one-line reason). |
Status is **per platform**. A feature can be `[x]` on desktop, `[~]` on mobile, `[ ]` on web.
---
## 3. One-paragraph system summary
D3RO Voice is a multi-platform AI voice assistant (transcription, LLM command execution, meeting intelligence, RAG, voice conversation) sold as Free / Pro / Pro+ / Team / Enterprise tiers. It ships as an **Electron desktop app** (local-first: bundled SoX, faster-whisper sidecar, Ollama, local SQLite), a **React Native mobile app** (`apps/mobile-rn`, cloud-first: Supabase auth + Edge Functions + on-device Whisper fallback), a **Next.js web console**, a **Next.js admin back office**, and a **.NET cloud API** (AI proxy + back office backend). The shared backend is **Supabase** (Postgres + RLS + Auth + Storage + ~27 Deno Edge Functions), deployed to a Synology NAS via Docker with a Cloudflare edge worker and tunnel. Shared code lives in `packages/*`. Distribution: Windows NSIS + macOS DMG (GitLab/Forgejo feed + electron-updater), Android APK/AAB via Google Play.
---
## 4. Reading order for a brand-new agent
1. `AGENTS.md` (root) — operating rules + the obligation to update this map.
2. `docs/map/01-system-overview.md` — the big picture and IA.
3. The surface doc for your task (0409).
4. `docs/map/10-feature-catalog.md` — find the feature and its status.
5. `docs/map/11-gap-backlog.md` — check for existing backlog notes.
Deeper design history (not required to start): `docs/design/*`, `docs/phases/*`, `docs/v2/*`, `docs/v3/*`, `memory/*`, `CHANGELOG.md`. The mobile SSOT is `docs/v3/MOBILE_APP_COMPLETION_SSOT.md`.
---
## 5. Maintenance
This map must change whenever a feature is added, removed, changed, or deferred.
See [`12-update-protocol.md`](./12-update-protocol.md) for the exact checklist and
`AGENTS.md` for the agent obligation.

View file

@ -0,0 +1,181 @@
# 01 — System Overview & Information Architecture
> Surface: whole product
> Source of truth for: vision, platforms, IA, identity/data model, AI pipeline, tiers
---
## 1. Product in one line
A multi-platform AI voice assistant: press/hold or tap to speak, get a transcript, optionally run it through an LLM (cleanup, translate, summarize, execute a command), and keep the result in a searchable history that syncs across desktop, web, and mobile.
---
## 2. Surfaces (apps) and their roles
| Surface | Path | Stack | Runtime model | Role |
|---|---|---|---|---|
| Desktop | `apps/desktop` | Electron 33 + React 19 + MUI 7 + Vite | **Local-first** (SoX, faster-whisper sidecar, Ollama, SQLite) with optional cloud sync | The flagship: global hotkey dictation, text insertion into other apps, meetings, captions, RAG, voice conversation, OS actions |
| Web | `apps/web` | Next.js 15 App Router + Supabase | **Cloud** | Browser console: record/STT, history, commands, meetings, knowledge, teams, chat, billing |
| Mobile | `apps/mobile-rn` | React Native 0.85 + React 19 (CLI, not Expo) | **Cloud-first** (Supabase + Edge Functions), on-device Whisper fallback | Product mobile app: recording/import, history, meetings, memos, templates, teams, Talk, admin, data portability, IAP + ads |
| API server | `apps/api-server` | ASP.NET Core 10 + EF Core + SQLite | Cloud (self-hosted/NAS) | LLM/STT proxy and admin back-office backend for the .NET identity side |
| Admin console | `apps/admin` | Next.js 16 + MUI | Cloud | Back office: users, subscriptions, models/STT providers, usage, audit log, releases, ads |
| Landing site | `site/` | Vite 6 + React 19 + Tailwind | Static | Marketing/download/legal pages, deployed to Cloudflare Pages + GitHub Pages |
| Edge gateway | `server/cloudflare-worker` | Cloudflare Worker (TS) | Edge | CORS + proxy to the NAS-hosted API origin |
| Backend data/functions | `server/supabase` | Postgres + Deno Edge Functions | Cloud | Canonical product data, auth, RLS, storage, AI proxies, billing, delivery |
---
## 3. Information architecture (feature domains)
The product IA is stable across surfaces; each surface implements a subset.
```
D3RO Voice
├── Capture & Transcribe
│ ├── Dictation (hold/release, push-to-talk)
│ ├── Hands-free (toggle)
│ ├── File transcription (audio/video)
│ ├── Live captions (system audio)
│ └── Multiple STT engines (local Whisper, cloud providers)
├── AI Processing
│ ├── Local LLM (Ollama) / Cloud LLM (Claude, OpenAI)
│ ├── Auto Polish / cleanup / translate / summarize
│ ├── Custom instructions (user commands)
│ ├── Voice commands (keyword → command)
│ └── LLM Chains (multi-step pipelines)
├── Memory & Knowledge
│ ├── History (search, favorites, export)
│ ├── Dictionary (custom vocabulary)
│ ├── Memos (tags over history)
│ ├── Knowledge base (local RAG / cloud RAG)
│ └── Voice actions (OS automation)
├── Meetings
│ ├── Meeting recording + live transcript
│ ├── Timestamped memos
│ ├── AI summary + speaker diarization
│ ├── Document generation (minutes/report/idea-note/mindmap)
│ └── Export (PDF/DOCX/TXT/Markdown)
├── Conversation
│ ├── Local duplex conversation (STT→LLM→TTS)
│ └── Realtime conversation (OpenAI gpt-realtime, Premium)
├── Accounts & Sync
│ ├── Supabase auth (email + Google/GitHub/Apple OAuth)
│ ├── Cloud sync (per-user DB/rows)
│ ├── Devices (registration, revocation)
│ ├── Teams (members, invites, roles)
│ └── Data portability (export/import, account delete)
├── Monetization
│ ├── Tiers: Free / Pro / Pro+ / Team / Enterprise
│ ├── Desktop licenses (Ed25519, offline)
│ ├── Web billing (Stripe + Payple)
│ ├── Mobile IAP (Google Play / App Store)
│ └── Free-tier ads (AdMob rewarded + banner, mediation roster)
├── Platform Shell
│ ├── Settings / preferences / themes (6 themes)
│ ├── Onboarding
│ ├── Notifications / push
│ ├── Support & content reporting
│ └── Admin & audit
```
Legacy/other: voice keyword shortcuts, screen/context capture, auto-launch, system tray.
---
## 4. Identity & data model (multi-source, converging)
There are **three** identity/data systems in the repo. This is a known architectural tension (see `11-gap-backlog.md` G-01).
| System | Where | Stores | Status |
|---|---|---|---|
| Supabase Auth + Postgres | `server/supabase` | Canonical product users, profiles, subscriptions, history, meetings, teams, knowledge, push, billing, ads | **Canonical SSOT** for web + mobile |
| .NET API server | `apps/api-server` | Its own SQLite `Users` (JWT, roles), model/STT endpoints, usage/error logs, admin audit | Back-office + AI proxy; legacy SHA-256 users force-disabled at startup |
| Desktop local license | `apps/desktop` | Ed25519-signed offline license key, local SQLite DB per user (`_local` for anonymous) | Local-first tier gating + optional Supabase cloud sync |
Data flow:
- Desktop: local SQLite (per-user file) ↔ optional Supabase sync (history/dictionary/meetings).
- Web/Mobile: Supabase directly (tables + RLS) and via Edge Functions.
- Admin: Next.js server routes → Supabase service role and/or .NET `/api/admin/*`.
---
## 5. AI pipeline
**Local path (desktop):** mic → SoX/native capture (PCM16 16kHz mono) → faster-whisper Python sidecar → optional Ollama LLM → SQLite history → clipboard/text insertion into the active app.
**Cloud path (web/mobile/desktop online):**
- STT → Supabase Edge Function `stt-proxy` (quota reservation, provider fallback) or .NET `SttProxyService` (internal gateway token only).
- LLM → Supabase `llm-proxy` / `realtime-token` (Claude/OpenAI) or .NET `LlmProxyService`.
- Meetings/documents → `generate-meeting-document`, `embed-chunks`, `search-knowledge`.
**Realtime voice (Premium):** OpenAI `gpt-realtime-2.1` via ephemeral token from `realtime-token`, WebRTC in the desktop renderer with local-pipeline fallback.
STT providers supported by the desktop dispatcher (`apps/desktop/src/main/services/stt`): local Whisper, OpenAI, Groq, Deepgram, AssemblyAI, Google, Custom (OpenAI-compatible), and D3RO Cloud.
---
## 6. Monetization tiers
| Tier | Notes |
|---|---|
| Free | Quotas on STT/LLM; free-tier ads (desktop/mobile). |
| Pro / Pro+ | Paid subscriptions. Desktop: offline Ed25519 license. Web: Stripe/Payple. Mobile: Google Play Billing. |
| Team / Enterprise | Teams, shared meetings, admin roles. |
Desktop license verification is Ed25519 (public key in `release/desktop-license-public.pem`); the private key was rotated out of the repo. Mobile release evidence uses a separate Ed25519 keypair.
---
## 7. Platform architecture diagram (text)
```
┌──────────────────────────────────────────┐
│ Supabase (SSOT) │
│ Postgres+RLS · Auth · Storage · Realtime │
│ ~27 Deno Edge Functions │
└───────────────┬──────────────────────────┘
┌──────────────┬───────┴────────┬───────────────┐
│ │ │ │
apps/web apps/mobile-rn apps/desktop apps/admin
(Next.js) (React Native) (Electron) (Next.js)
│ │ │ │
└──────────────┴────────────────┘ │
│ │
┌───────┴────────┐ ┌─────────┴─────────┐
│ Cloudflare │ │ apps/api-server │
│ Worker (edge) │──────────────▶ .NET 10 + SQLite │
└───────┬────────┘ │ + admin audit │
│ └───────────────────┘
Cloudflare Tunnel
┌───────┴────────┐
│ D3RO NAS │ docker-compose.nas.yml
│ API + Admin │ d3ro.chanpaca.net / admin.chanpaca.net
└────────────────┘
```
---
## 8. Cross-cutting concerns
| Concern | Implementation |
|---|---|
| Design system | `packages/ui` (web/desktop, MUI + tokens), `packages/ui-native` (mobile). Theme SSOT `theme.ts` (`d3roPalette`/`d3roTypo`/`d3roShadow`/`d3roRadius`). 6 themes. |
| i18n | `packages/i18n`, 12 locales, `ko` master, `t()` + type-safe keys. |
| IPC | `packages/core/src/ipc-channels.ts` is the channel SSOT; desktop preload exposes `window.electronAPI` (33 namespaces). |
| Errors | `D3ROError` + `ErrorCode`, `IPCResult<T>` envelope. |
| Crypto | Ed25519 license signing (`packages/core/src/utils/crypto-license`), HMAC admin sessions, PKCE on mobile. |
| Observability | `LoggerService` (electron-log) on desktop; `ServerErrorLog`/`ApiUsageLog`/`SttUsageLog` in .NET; admin audit log in Supabase. |
| Security posture | Fail-closed defaults: STT/LLM never return synthetic success; admin panels show explicit "unavailable" rather than sample data; secret scanning in CI. |
---
## 9. Related deep documents
- Desktop architecture: `docs/design/00-master-architecture.md``09-history-popup.md`
- Build-out history: `docs/phases/phase-1.md``phase-15.5-speaker-diarization.md`
- Multi-platform plan: `docs/v2/00-v2-master-plan.md`
- Mobile SSOT (authoritative checklist): `docs/v3/MOBILE_APP_COMPLETION_SSOT.md`
- Monetization: `docs/monetization-plan.md`
- Release process: `docs/deployment/release-guide.md`, `docs/deployment/nas-deployment-guide.md`

View file

@ -0,0 +1,224 @@
# 02 — Repository & Infrastructure Map
> Surface: whole repo
> Source of truth for: layout, workspaces, build/test commands, CI/CD, Docker, deploy, scripts, resources
---
## 1. Repository layout
```
D:/workspace/D3ROVoice
├── apps/
│ ├── desktop/ Electron app (npm workspace @d3ro/desktop)
│ ├── web/ Next.js console (npm workspace @d3ro/web)
│ ├── admin/ Next.js back office (npm workspace @d3ro/admin)
│ ├── mobile-rn/ React Native product mobile app (NOT an npm workspace)
│ ├── api-server/ ASP.NET Core 10 API (D3ROVoice.Api)
│ ├── api-server.Tests/ xUnit tests for the API
│ └── admin-swagger/ Static Swagger UI + openapi.json
├── packages/ Shared TS packages (npm workspaces)
│ ├── core/ @d3ro/core — types, errors, IPC channels, crypto, utils
│ ├── ui/ @d3ro/ui — web/desktop design system (MUI + tokens)
│ ├── ui-native/ @d3ro/ui-native — React Native design system
│ ├── i18n/ @d3ro/i18n — 12 locales, provider, formatters
│ └── api-client/ @d3ro/api-client — Supabase wrapper + shared types
├── server/
│ ├── supabase/ Supabase project: config.toml, migrations/, functions/, tests/
│ └── cloudflare-worker/ Edge gateway (wrangler.toml + src/index.ts)
├── site/ Vite landing site (deployed to Cloudflare Pages + GitHub Pages)
├── resources/ icons/ (empty), sox/ (bundled Windows SoX binaries)
├── release/ Version identity SSOT + license/evidence public keys
├── scripts/ ~145 automation scripts + scripts/ci/ (31) + scripts/lib/
├── docs/ Design, phases, v2/v3 plans, deployment, map/ (this map)
├── memory/ Project status, handoffs, archive
├── tests/e2e/, test-results/ Root-level e2e + last-run artifacts
├── scratch/ Large local evidence (APKs, screenshots, DBs) — not build input
├── supabase/ Empty scaffolding (.branches/, snippets/) — real project is server/supabase
├── .github/workflows/ GitHub Actions (6)
├── .gitlab-ci.yml GitLab CI (primary desktop/mobile release pipeline)
├── .forgejo/workflows/ Forgejo Actions (site deploy to Cloudflare Pages)
├── docker-compose.yml, docker-compose.nas.yml
├── Dockerfile.admin, apps/api-server/Dockerfile, apps/admin/Dockerfile
├── turbo.json, tsconfig.json, tsconfig.base.json, pnpm-workspace.yaml
├── package.json monorepo root, npm workspaces
├── CLAUDE.md Claude-specific project rules
└── AGENTS.md Cross-agent entry: rules + map obligation (this map's anchor)
```
Note: root `package.json` declares npm workspaces `apps/desktop`, `apps/web`, `apps/admin`, `packages/*`. `pnpm-workspace.yaml` also exists (`apps/*`, `packages/*`) but npm is the active toolchain. **Mobile is intentionally outside the workspace.**
---
## 2. Toolchain & versions
| Tool | Version | Source |
|---|---|---|
| Node | 24.19.0 | `.nvmrc` |
| TypeScript | 5.7 | root `package.json` |
| .NET SDK | 10.0.300 (rollForward latestPatch) | `global.json` |
| Deno | 2.8.1 | CI (`edge-functions-quality`) |
| JDK | 17 | mobile CI |
| Electron | 33.4.11 | `apps/desktop/electron-builder.yml` |
| React Native | 0.85 | `apps/mobile-rn/package.json` |
| Turborepo | turbo.json tasks: build/typecheck/test/lint/dev | `turbo.json` |
---
## 3. Root scripts (`package.json`)
```bash
npm run dev # desktop dev (electron-vite)
npm run build # desktop production build
npm run build:admin # admin production build
npm run build:all | npm run ci # scripts/ci/build-all.mjs
npm run checksum # scripts/ci/generate-checksums.mjs
npm run version:check | version:sync
npm run release:metadata[:test]
npm run release:forgejo[:check] # canonical Forgejo publisher/feed
npm run release:tag # annotated/signed immutable release tag
npm run security:secrets[:test] # hardcoded-secret scanner
npm run test:e2e:red # content-report red e2e
npm run release:mobile:boundary[:test]
npm run release:mobile:config[:test]
npm run release:mobile:build-config:test
npm run release:play:assets[:test]
npm run typecheck # all workspaces
npm run test # all workspaces
npm run lint # eslint apps/desktop apps/web apps/admin packages
npm run format # prettier
npm run typecheck:mobile # apps/mobile-rn tsc (outside npm workspaces)
npm run lint:mobile # apps/mobile-rn eslint --max-warnings=0
npm run test:mobile # apps/mobile-rn jest
npm run verify:all # aggregate: workspaces + mobile (typecheck/lint/test)
```
Per-app commands that matter:
| App | Commands |
|---|---|
| desktop | `npm run dev --workspace=@d3ro/desktop`, `build`, `typecheck`, `test` (vitest, 1266 tests), playwright e2e |
| mobile-rn | `npm run typecheck:mobile` / `lint:mobile` / `test:mobile` (root), or `npm --prefix apps/mobile-rn run lint/typecheck/test`; android gradle builds, Maestro E2E |
| api-server | `dotnet build`, `dotnet test` (also `apps/api-server.Tests`) |
| web | `next build`, playwright e2e in `apps/web/e2e` |
| admin | `next build` (`build:admin`) |
Desktop GUI execution rule (from `CLAUDE.md` / `.agents/rules/`): run via `run-desktop.bat` or an external terminal; do not background-launch GUI from an agent subshell.
---
## 4. Shared packages
See [`03-shared-packages.md`](./03-shared-packages.md). Summary:
| Package | Provides |
|---|---|
| `@d3ro/core` | Domain types, `D3ROError`/`ErrorCode`, IPC channel SSOT, constants, `crypto-license`, `pii-redactor`, `secure-memory`, `supabase-config`, `meeting-markdown`, `markdown-to-docx` |
| `@d3ro/ui` | Theme tokens, CSS vars, MUI DS components (web/desktop) |
| `@d3ro/ui-native` | RN design system (MetalCard, PhosphorText, Led, PhysicalButton, WaveBars, …) |
| `@d3ro/i18n` | 12 locales, `I18nProvider`, `t()`, date/number/relative formatters |
| `@d3ro/api-client` | Supabase browser/server clients, meetings/history/usage/transcribe wrappers, shared types; has vitest tests |
---
## 5. CI/CD
### GitHub Actions (`.github/workflows/`)
| Workflow | Purpose |
|---|---|
| `ci.yml` | Main CI: `code-quality` (secret scan, mobile release/config/build-config self-tests, Play asset contract, lint, typecheck), `api-server-tests`, `edge-functions-quality` (Deno), `test-matrix` (win/mac/ubuntu vitest), `build-validation` (desktop win, admin ubuntu), `mobile-android` (debug/CSPRNG/E2E APKs + verifiers), `mobile-emulator-e2e` (API 35 + Maestro 2.7.0) |
| `release.yml` | On tag `v*.*.*`: preflight → `package-windows` (NSIS) → `package-macos` (DMG/ZIP arm64) → `package-android` (signed APK/AAB + evidence) → `package-admin-docker` (GHCR) → `publish-release` (checksums + GitHub Release + Forgejo canonical publish) |
| `deploy-site.yml` | On `site/**`: build Vite site, boundary self-test, deploy GitHub Pages |
| `build-mac.yml` | Manual macOS build (arm64/x64), sox + PyInstaller sidecar + electron-rebuild |
| `payple-renew.yml` | Daily cron → `payple-renew` edge function |
| `release-signing-ca.yml` | Manual Windows (Azure Trusted Signing) / macOS notarize build+sign |
### GitLab CI (`.gitlab-ci.yml`)
Stages `validate → test → build → e2e → package → publish → deploy`. Primary pipeline for desktop Windows/macOS releases (Forgejo Generic Registry is the canonical updater feed; GitLab project 1172 is a legacy mirror) and production mobile releases (`mobile-production-release`, manual/protected). Admin NAS deploy job is intentionally **disabled**.
### Forgejo Actions (`.forgejo/workflows/`)
`deploy-site.yml` / `deploy-site-windows.yml` — build `site`, write release identity, deploy to Cloudflare Pages `d3ro` (`d3ro.chanpaca.net`), verify live commit/version, app-links, legal URLs.
`release.yml` — tag-triggered Windows build (signed) + `publish-forgejo-release.mjs` to the canonical Forgejo feed/release hub.
---
## 6. Docker & deployment
| File | Purpose |
|---|---|
| `Dockerfile.admin` | 3-stage Next.js admin build (node 24.19.0-alpine, port 3001) |
| `apps/admin/Dockerfile` | Next.js standalone runner for `.next/standalone` |
| `apps/api-server/Dockerfile` | Multi-stage .NET 10 (sdk → aspnet runtime), port 5000, `VOLUME /app/data` |
| `docker-compose.yml` | Dev/self-host: `d3ro-api-server` (5050→5000, `./data` volume), `d3ro-admin` (3001), optional `ollama` (profile `ai`, 11434) |
| `docker-compose.nas.yml` | NAS: prebuilt `d3ro-voice-api:latest` + `d3ro-voice-admin:latest`; API mounts `/volume1/docker/d3ro/wwwroot/{privacy,terms,delete-account,legal.css}` read-only |
Deploy scripts: `scripts/deploy-nas.ps1`, `scripts/deploy-nas.sh`, `scripts/deploy-site-to-nas.js`, `scripts/nas-control.sh` (start/stop/restart/status/logs/backup/update).
Public endpoints (production): `https://d3ro.chanpaca.net` (portal/API), `https://admin.chanpaca.net` (admin CRM). Edge: `server/cloudflare-worker` proxying to the NAS origin, plus a **Cron Trigger** (`* * * * *`) that drains the Supabase push outbox via `send-push?mode=drain` (`src/push-drain.ts`; needs `SUPABASE_URL` var + `SUPABASE_SERVICE_ROLE_KEY` secret). Tunnel: Cloudflare Tunnel `kd-nas`.
---
## 7. `server/supabase` (backend)
- `config.toml` — project `d3ro-voice`, ports 55321-55324, DB major 17, auth redirects (localhost, `d3ro.chanpaca.net`, `d3ro-voice://auth-callback`), providers Google/GitHub/Apple.
- `migrations/`**63 SQL migrations** (schema, RLS, auth triggers, storage, team invites, knowledge/pgvector, push outbox, Payple/Stripe billing, admin roles, mobile platform/monetization, atomic command reorder, device revocation, content reporting, audit log, meeting documents, STT quota reservations, ad reward replay protection, team activity feed).
- `functions/`**~27 Deno Edge Functions** (`stt-proxy`, `llm-proxy`, `content-report`, `generate-meeting-document`, `embed-chunks`, `search-knowledge`, `realtime-token`, `team-invite`, `team-accept`, `send-push`, `account-delete`, `admin-users`, `admin-subscriptions`, `admin-payments`, `admin-audit-log`, billing `billing-catalog`/`stripe-checkout`/`stripe-portal`/`stripe-webhook`/`payple-checkout`/`payple-manage`/`payple-renew`/`payple-webhook`, `iap-verify`, `admob-ssv`, `google-play-rtdn`). Shared contracts in `functions/_shared/` — push transports now include `webpush.ts` (VAPID + RFC 8291) and `apns.ts` (.p8 token) alongside FCM. CI (`edge-functions-quality`) runs `deno check` + `deno test` and also the Cloudflare worker drain test.
- `tests/` — integration/E2E for content report, mobile platform/recording/reward-race, payments, mobile release preflight, push, team push security, STT quota.
Full detail: [`09-supabase-backend.md`](./09-supabase-backend.md).
---
## 8. `scripts/` groups
- **CI (`scripts/ci/`, 33 files):** build/version/release (`build-all`, `sync-version`, `generate-checksums`, `verify-release-metadata`, `create-release-tag`, `extract-release-notes`), security (`check-no-hardcoded-secrets`), mobile release gates (`verify-mobile-release-boundary/-config/-build-config`, `verify-android-artifact/-app-links`, `verify-play-store-assets`, `prepare-whisper-model`, `create-mobile-release-evidence`, `prepare-mobile-release-publication`, emulator/CSPRNG gates), keys (`create-desktop-license-keypair`, `create-release-evidence-key`, etc.), publish (`publish-forgejo-release` canonical, `publish-gitlab-release` mirror; legacy `sync-and-publish-forgejo-release`, `upload-asset-to-forgejo-release`), env/tooling (`bootstrap-linux-toolchain.sh`, `audit-nas-stt-config.ps1`, mobile local E2E scripts).
- **Deploy/release:** `deploy-nas.ps1/.sh`, `deploy-site-to-nas.js`, `nas-control.sh`, `publish-gh.ps1`, `gen-keystore.js`.
- **GCP/Google OAuth automation + inspection (~70 `*.mjs`):** `auto-configure-oauth`, `automate-google-oauth`, `setup-consent`, `create-*-client`, `check-*`, `inspect-*` — mostly one-off/browser-driven console automation.
- **AdMob console automation:** `admob-probe.mjs` (read-only login/app/ad-unit probe), `admob-login.mjs` + `run-admob-login.bat` (one interactive headful Chrome login into a persistent profile), `admob-automate.mjs` (dry-run by default; `--apply` creates/verifies banner+rewarded units and reports Play-store link). Uses `playwright` with `channel: 'chrome'` and the gitignored `.chrome-playwright-profile`.
- **E2E / verification:** `e2e-desktop-*.js`, `real-app-multi-tab-e2e.js`, `test-and-capture-all-10-ad-services.js`, `verify-live-production-d3ro.js`.
- **Screenshots/captures:** `capture-*.js`.
- **Forgejo ops:** `check-forgejo-actions-runs.js`, `capture-forgejo-*.js`.
> `scripts/` is large and partially scratch. Prefer `scripts/ci/*` for anything release-gated, and `server/supabase/tests` for backend integration.
---
## 9. Release & versioning SSOT
| File | Purpose |
|---|---|
| `release/product-version.json` | version `1.1.0`, `androidVersionCode`/`iosBuildNumber` `1010001`, releaseDate, desktop license keyId |
| `release/android-release-identity.json` | package `com.d3ro.voice`, Play app ID, app-signing SHA-256, upload cert SHA-256, evidence keyId, AdMob unit IDs |
| `release/desktop-license-public.pem` | Ed25519 public key for desktop offline licenses |
| `release/mobile-release-evidence-public.pem` | Ed25519 public key for mobile release evidence |
| `apps/desktop/electron-builder.yml` | appId `com.d3ro.voice`, NSIS x64 (forced code signing), macOS DMG/ZIP arm64, generic Forgejo publish feed, asarUnpack native modules, extraResources (icons, sounds, sox, ollama) |
| `apps/desktop/src/main/update-feed.ts` | Auto-update feed SSOT (canonical Forgejo + legacy GitLab mirror, channels) |
| `release/update-policy.json` | Update policy SSOT (channels, minimum supported version, forced update, delta/full, staged rollout, kill switch) |
| `apps/desktop/src/main/update-policy.ts` | Policy parsing/decision logic |
| `scripts/ci/publish-forgejo-release.mjs` | Canonical Forgejo registry + Release + feed publisher |
Version sync is enforced by `scripts/ci/sync-version.mjs` and `verify-release-metadata.mjs`; `npm run version:check` should be clean.
---
## 10. Resources & tests
- `resources/sox/` — bundled Windows SoX (`sox.exe` + DLLs) for audio capture.
- `resources/icons/` — empty; electron-builder falls back to `build/icon.ico|png`.
- Desktop tests: `apps/desktop/tests/` (vitest unit + playwright e2e), `apps/desktop/test-results/`.
- Mobile tests: `apps/mobile-rn/__tests__/` (57 suites / 353 tests per mobile SSOT), `.maestro/` + `.maestro-output/` E2E evidence. Note: a full parallel Jest run can hit the 5s render timeout on slow machines; re-run the failing spec in isolation before treating it as a regression.
- Web tests: `apps/web/e2e/` (playwright).
- API tests: `apps/api-server.Tests/` (xUnit).
- Root: `tests/e2e/`, `test-results/.last-run.json`.
---
## 11. Known infrastructure gaps
See [`11-gap-backlog.md`](./11-gap-backlog.md) for the maintained list (`INFRA-*`). Headlines:
- `apps/mobile-rn` is not an npm workspace member; use `typecheck:mobile`/`lint:mobile`/`test:mobile` or `verify:all`.
- Admin NAS deploy job disabled in GitLab CI; admin ships via GitHub/GHCR + manual NAS compose.
- Two identity systems (.NET JWT/SQLite vs Supabase); a canonical resolver now exists in `@d3ro/core/entitlement` but web/mobile/.NET adoption is incremental (`11` GAP-ID-02).

View file

@ -0,0 +1,108 @@
# 03 — Shared Packages Map
> Surface: `packages/*` (npm workspaces)
> Source of truth for: shared domain logic, design systems, i18n, API client
All packages are private, version `1.1.0`, source-only (`main`/`types` point at `src`).
---
## 1. `@d3ro/core` — business logic SSOT (`packages/core`)
The canonical place for types and cross-surface logic. Both desktop and web/mobile depend on it.
| Area | Exports | Notes |
|---|---|---|
| Types | `./types` | Domain types shared across surfaces |
| Errors | `./errors` | `D3ROError`, `ErrorCode` |
| IPC channels | `./ipc-channels` | `IPC_CHANNELS` object + `IPCChannel` union. **SSOT** for every desktop IPC channel (VOICE, AUDIO, STT, TTS, LLM, HOTKEY, CONFIG, HISTORY, DICTIONARY, WINDOW, SYSTEM, STATS, MEMO, VOICE_COMMAND, CONTEXT, CHAIN, CAPTION, FILE_TRANSCRIPTION, MEETING_SUMMARY, DICTATION_TEMPLATE, VOICE_CONVERSATION, RAG, VOICE_ACTION, MEETING_MODE, MEETING_DOC_TEMPLATE, MEETING_CHAT, LICENSE, CLOUD_SYNC, INSTRUCTION, SYSTEM_AUDIO, POPUP_RESULT, POPUP_HISTORY, POPUP_COMMAND, POPUP_CAPTION, VOICE_PARTIAL, CLIPBOARD, APP, ONLINE_AUTH, ADS, SUPPORT, PAYMENT) |
| Constants | `./constants` | Shared constants |
| Crypto license | `./utils/crypto-license` | Ed25519 license sign/verify (used by admin issuer + desktop verifier) |
| PII | `pii-redactor`, `secure-memory` | Redaction + secure memory helpers |
| Supabase config | `supabase-config` | Shared Supabase config shape |
| Entitlement | `./entitlement` | `EntitlementTier`, `AdminRole`, `EntitlementSnapshot`, `resolveEntitlement`, `normalizeEntitlementTier`/`normalizeAdminRole` — canonical tier/role contract across Supabase, desktop license, and .NET |
| Doc utils | `./utils/meeting-markdown`, `./utils/markdown-to-docx` | Meeting Markdown export + DOCX generation (dep: `docx`) |
**When to change:** new cross-surface type, new error code, new IPC channel, license format. Add here first, then consume.
---
## 2. `@d3ro/ui` — web/desktop design system (`packages/ui`)
| Export | Contents |
|---|---|
| `.` | Barrel |
| `./theme` | `theme.ts``d3roPalette`, `d3roTypo` (13 steps), `d3roShadow` (10), `d3roRadius` (7); 6 themes (dark/Midnight, light, nord, solarized, catppuccin, dracula) + build helpers |
| `./theme-vars` | CSS variable map for popups/vanilla surfaces |
| `./components/ds` | MUI/Emotion DS components: CrtDisplay, InstrumentPanel, Led, PhysicalButton, MetalCard, PhosphorText (13 variants), MetalDial, ScreenPanel, ButtonGroup, TiltCard, GradientWave, StatRing |
Peers: React 19, MUI 7, Emotion 11. Depends on `@d3ro/core`.
**Theme SSOT rule:** never hardcode hex outside `theme.ts`; use `d3roPalette`/`d3roShadow` tokens. `accent.amber` was removed in Wave 2; use `accent.main`.
---
## 3. `@d3ro/ui-native` — React Native design system (`packages/ui-native`)
| Provides |
|---|
| `NativeThemeProvider` / `useNativePalette`, native palette/typo/radius/fonts |
| Components: `MetalCard`, `PhosphorText`, `Led`, `PhysicalButton`, `ScreenPanel`, `WaveBars`, `AppStatusBar`, `Header`, `FilterChip` |
Peers: `react`, `react-native`. Consumed by `apps/mobile-rn` via file: workspace dependencies.
---
## 4. `@d3ro/i18n` — internationalization (`packages/i18n`)
| Provides | Notes |
|---|---|
| 12 locales | `ko` (master), `en`, `ja`, `zh`, `zh-TW`, `es`, `fr`, `de`, `pt`, `ru`, `vi`, `th` in `src/locales/` |
| Type-safe keys | `TranslationKey` derived from `ko.json` |
| React context | `I18nProvider`, `useI18n`, `TFunction` |
| Fallback chain | locale → `en``ko` → key |
| Formatters | `formatDate`, `formatNumber`, `formatRelativeDate`, `formatTime` via `Intl` |
| Storage injection | `I18nStorage` adapter (localStorage on web, AsyncStorage on mobile) |
Electron-independent. Rule: no hardcoded Korean/English strings in UI; use `t()`.
---
## 5. `@d3ro/api-client` — Supabase wrapper (`packages/api-client`)
| Export | Purpose |
|---|---|
| `client` | Base Supabase client factory |
| `auth` | Auth helpers |
| `meetings` | Meeting queries |
| `history` | History queries |
| `usage` | Usage queries |
| `transcribe` | STT calls |
| `supabase-browser` | Browser client (`getSupabaseBrowserClient`, `isSupabaseConfigured`) |
| `supabase-server` | Server client (`createSupabaseServerClient`) |
| `types` | Shared DB/domain types — **canonical; do not redefine table types per app** |
Deps: `@d3ro/core`, `@supabase/supabase-js`; optional peer `@supabase/ssr`. Tests: `__tests__/` (client, transcribe, types) via vitest.
Consumed by `apps/web`, `apps/admin` (via re-exports), `apps/mobile-rn`, and desktop cloud paths.
---
## 6. Dependency direction
```
@d3ro/i18n (standalone)
@d3ro/ui@d3ro/core
@d3ro/ui-native(standalone, peers react-native)
@d3ro/api-client → @d3ro/core
@d3ro/core (standalone)
```
Apps depend on packages, never the reverse. Shared package changes ripple to all consumers (see `docs/REFACTOR_POLICY.md`).
---
## 7. Package-level gaps
- `apps/mobile-rn` is not an npm workspace member, so root `typecheck`/`test` skip it; use `typecheck:mobile`/`lint:mobile`/`test:mobile` from the repo root.
- No versioned publishing of packages (private, source-only); all consumers are in-repo.

208
docs/map/04-desktop-app.md Normal file
View file

@ -0,0 +1,208 @@
# 04 — Desktop App (Electron) Map
> Surface: `apps/desktop`
> Stack: Electron 33 + React 19 + MUI 7 + Vite (electron-vite) + better-sqlite3/drizzle + uiohook-napi + nut-js
> Source root: `apps/desktop/src` (`main/`, `preload/`, `renderer/`)
---
## 1. Process architecture
| Layer | Path | Contents |
|---|---|---|
| Main | `src/main/` | Services, IPC handlers, windows, bootstrap/lifecycle, DB |
| Preload | `src/preload/` | `index.ts` exposes `window.electronAPI`; `popup.ts` exposes `window.popupAPI` |
| Renderer | `src/renderer/` | React app: `AppLayout` + 7 pages + modals + 5 vanilla popups |
**Main entry** `src/main/index.ts`: sets app name/AppUserModelId, disables GPU acceleration, EPIPE/uncaught handlers, registers `d3ro-voice://` deep-link protocol (Supabase OAuth implicit + PKCE), single-instance lock, then `bootstrap()` + `setupLifecycle()`.
**Bootstrap** `src/main/bootstrap.ts`: ordered `BootstrapStep[]` — logger, config, **database (critical)**, license, create-windows (critical), tray, **ipc-handlers (critical)**, custom-instructions, voice-commands, sound-effects, auto-launch, popup-preload, hotkey, voice-mode, llm-polling, meeting-summary-wiring, meeting-mode, cloud-sync, auto-update. Wires VoiceMode events to sound + history persistence.
---
## 2. Main services (`src/main/services/`)
Singleton + `EventEmitter` pattern (`getXService()` accessors).
### Core voice pipeline
| Service | Purpose |
|---|---|
| `VoiceModeService` | Orchestrator: 9-state `RecognitionState` + 4-state `AudioState`, dual-condition flush, action queue. Events: session-started/completed/cancelled, transcription-update, audio-level, recognition/audio-state-changed, premium-llm-fallback, error |
| `AudioCaptureService` | Mic PCM16 16kHz mono (SoX on Windows, node-record-lpcm16 elsewhere). Events: audio-data, audio-level, device-changed, started, stopped, error |
| `LocalSTTService` | faster-whisper Python sidecar manager (state machine, dual-flush, model download/cancel) |
| `HotkeyService` | uiohook-napi global hooking (dictation/hands-free/command/caption). Events: hotkey-pressed/released, double-press, error |
| `TextInsertService` | Clipboard save→set→Ctrl+V→restore via nut-js |
| `SoundEffectService` | Preloaded WAV feedback (start/stop/error/cancel/chime) |
### STT engine layer (`services/stt/`)
| File | Purpose |
|---|---|
| `STTManager` | Dispatcher across local + 6 cloud providers, auto-fallback (events provider-changed, config-changed, fallback-to-local) |
| `types.ts` | `ISTTDriver` contract |
| `audio-utils.ts` | `pcmToWav`, `createProbeWav` |
| `drivers/OpenAI|Groq|Deepgram|AssemblyAI|Google|Custom|D3ROCloud` | Provider drivers; `D3ROCloudDriver` uses Supabase access token |
### LLM layer
| Service | Purpose |
|---|---|
| `LocalLLMService` | Ollama REST (models, pull w/ progress, server start, NDJSON streaming) |
| `PremiumLLMService` | Claude via Supabase `llm-proxy`, local fallback |
| `OnlineLLMService` | JWT-authenticated .NET backend client |
| `llm-prompts.ts` | `resolveSystemPrompt` SSOT for action prompts |
### Memory & knowledge
| Service | Purpose |
|---|---|
| `HistoryService` | SQLite history CRUD/search/stats |
| `DictionaryService` | Custom vocabulary CRUD/search + cloud sync hooks + JSON/CSV import/export (`dictionary:import`/`export`, save/open dialogs) |
| `MemoService` | Memo tags over history (`memo_tags`) |
| `RAGService` | Local RAG: `nomic-embed-text` embeddings, cosine search over `rag_chunks` |
| `CustomInstructionService` | User LLM commands (5 built-ins) |
| `VoiceCommandService` | Keyword → command rule matching |
| `ChainService` | Multi-step LLM pipelines (LLMChain) |
| `ScreenContextService` | Active-window + selected-text context |
### Phase 10+ features
| Service | Purpose |
|---|---|
| `CaptionService` | Live captions from system/loopback audio; caption overlay (events segment, state-changed, session-saved, error) |
| `FileTranscriptionService` | Audio/video file → ffmpeg → 30s chunks → STT merge (events progress, complete, error, state-changed) |
| `MeetingSummaryService` | Post-caption LLM summary |
| `DictationTemplateService` | Field-by-field voice form filling |
| `VoiceConversationService` | STT→LLM→TTS loop, 10-turn memory |
| `TTSPlaybackService` | Platform TTS (macOS `say`, Windows SAPI), sentence queue |
| `VoiceActionService` | Voice → LLM JSON action plan → OS execution (dangerous blocked) |
### Phase 1215
| Service | Purpose |
|---|---|
| `MeetingModeService` | Meeting recording: live transcript, timestamp memos, doc generation/export, diarization |
| `MeetingDocTemplateService` | Meeting-doc templates (built-ins + CRUD) |
### Account / infra / monetization
| Service | Purpose |
|---|---|
| `ConfigService` | electron-store `AppConfig` (`configGet/Set`, defaults) |
| `LicenseService` | Freemium tiers, quotas (`daily_usage`), activation, upgrade prompts |
| `CloudSyncService` | Supabase sync, per-user DB switching, history/dictionary/meeting mirror |
| `CloudSTTService` | Thin cloud STT wrapper over `D3ROCloudDriver` |
| `UpdateService` | electron-updater (canonical Forgejo feed, channels, mandatory/full-vs-delta policy, staged rollout, restart dialog) |
| `AutoLaunchService` | OS login-item auto-start |
| `LoggerService` | electron-log wrapper + category loggers |
| `checkout`/payment | `payment-handlers.ts` — authenticated Edge-only Stripe/Payple checkout + server readback |
### Ads (`services/ads/`)
| File | Purpose |
|---|---|
| `AdMediationEngine` | Multi-ad mediation + header bidding |
| `AdSettlementService` | Revenue settlement, withholding, payout ledger |
| `BaseAdAdapter` / `UnavailableAdAdapter` | Adapter contract + fail-closed base |
| `DirectHouseSponsorAdapter` | **Real configurable adapter**: bids/reports against an operator HTTPS `endpointUrl` (`AdNetworkConfig.endpointUrl`), validates creatives, fail-closed (`adapter_not_configured`) when unconfigured |
| 9 placeholder adapters (AppLovin, Carbon, EthicalAds, GoogleAdManager, InMobi, Mintegral, Playwire, PubMatic, Unity) | Extend `UnavailableAdAdapter` — registered, no live bids (`provider_not_integrated`) |
---
## 3. IPC layer
Registry: `src/main/ipc/index.ts` calls 29 `registerXHandlers()` in fixed order. Channel SSOT: `packages/core/src/ipc-channels.ts`.
| Handler | Channel group(s) |
|---|---|
| `ads-handlers` | ADS |
| `audio-handlers` | AUDIO |
| `caption-handlers` | CAPTION + SYSTEM_AUDIO |
| `chain-handlers` | CHAIN |
| `cloud-sync-handlers` | CLOUD_SYNC |
| `config-handlers` | CONFIG |
| `context-handlers` | CONTEXT |
| `dictionary-handlers` | DICTIONARY |
| `file-transcription-handlers` | FILE_TRANSCRIPTION |
| `history-handlers` | HISTORY + `stats:getSummary` |
| `hotkey-handlers` | HOTKEY |
| `instruction-handlers` | INSTRUCTION |
| `license-handlers` | LICENSE |
| `llm-handlers` | LLM + `llm:premium:*` + ONLINE_AUTH |
| `meeting-doc-template-handlers` | MEETING_DOC_TEMPLATE |
| `meeting-mode-handlers` | MEETING_MODE + MEETING_CHAT |
| `meeting-summary-handlers` | MEETING_SUMMARY |
| `memo-handlers` | MEMO |
| `payment-handlers` | PAYMENT |
| `rag-handlers` | RAG |
| `stt-handlers` | STT |
| `support-handlers` | SUPPORT |
| `system-handlers` | SYSTEM |
| `template-handlers` | DICTATION_TEMPLATE |
| `voice-action-handlers` | VOICE_ACTION |
| `voice-command-handlers` | VOICE_COMMAND |
| `voice-conversation-handlers` | VOICE_CONVERSATION |
| `voice-handlers` | VOICE |
| `window-handlers` | WINDOW + `SYSTEM.OPEN_EXTERNAL` |
Preload exposes **`window.electronAPI`** with 33 namespaces: `platform, audio, config, voice, stt, hotkey, llm (incl. premium), history, dictionary, stats, window, system, instruction, app, memo, voiceCommand, context, chain, caption, license, fileTranscription, meetingSummary, dictationTemplate, rag, voiceAction, voiceConversation, meetingMode, meetingChat, meetingDocTemplate, cloudSync, onlineAuth, ads, support, payment`. Envelope: `IPCResult<T>` (success/error); `app.onDataChanged` is the global refresh channel.
---
## 4. Windows & popups
`windows/WindowManager.ts` creates 6 windows: main (borderless, custom TitleBar; macOS `hiddenInset`), recording-tip, result-popup, history-popup, command-popup, caption-overlay. Injects popup theme CSS + i18n strings; 2-phase resize. `windows/TrayManager.ts` — tray icon + menu + double-click show.
Vanilla popups (`src/renderer/popups/`):
| Popup | Purpose |
|---|---|
| `recording-tip` | 9-bar waveform indicator, partial transcript |
| `result-popup` | Transcription result + copy, auto-close with hover pause |
| `history-popup` | Recent transcriptions; ↑↓/Enter/1-9/ESC |
| `command-popup` | Command selection (Ctrl+Shift+C) |
| `caption-overlay` | Live caption overlay (font/opacity/maxLines) |
---
## 5. Renderer IA
Routing is state-based in `AppLayout.tsx` (`Route` union + `NAV_ITEMS`), no react-router.
| Page | Route | Feature |
|---|---|---|
| `DashboardPage` | dashboard | Voice cockpit: hero, bento tiles, multi-engine hub (STT/LLM), telemetry, recent history, file drop |
| `HistoryPage` | history | History & memory timeline; search, tag filter, pagination, export/delete |
| `DictionaryPage` | dictionary | Custom vocabulary editor |
| `CommandsPage` | commands | Custom instructions + voice keyword rules + LLM chains + dictation templates |
| `VoiceConversationPage` | conversation | Duplex voice assistant (local pipeline vs OpenAI Realtime) |
| `KnowledgeBasePage` | knowledge | Local RAG: add/index docs, semantic query, reindex/remove |
| `MeetingModePage` | meeting | Meeting studio: live transcript, memos, doc generation/export, diarization |
Modals/components: `SettingsModal` (tabs General/Audio/STT/LLM/License/Cloud/About), `LicenseModal`, `LicenseTab`, `CloudSyncSection`, `OnboardingModal`, `UpgradePromptModal`, `ProBadge`, `TemplateSection`, `FileDropZone`, `HotkeyRecordModal`, `OllamaGuideModal`, `CodexOAuthGuideModal`, `TitleBar`, `StatusBar`, meeting components (9), voice-conversation, payment (`CheckoutModal`, `checkout-flow.ts`), support (`SupportModal`), ads (`AdBanner`, `RewardedQuotaModal`), shared cards.
Hooks: `useRealtimeConversation` (OpenAI Realtime WebRTC), `useLicenseState`, `useProFeature`.
DB schema (`src/main/db/schema.ts`, drizzle SQLite): `history`, `dictionary`, `stats`, `memo_tags`, `daily_usage`, `rag_documents`, `rag_chunks`, `meeting_sessions`, `meeting_memos`, `meeting_documents`.
---
## 6. Desktop status summary
- Core dictation/LLM/history pipeline: **implemented + tested** (~590 desktop tests; vitest + playwright).
- Cross-platform packaging: Windows NSIS (signed, `forceCodeSigning`), macOS DMG/ZIP arm64 (ad-hoc signing); auto-update via canonical Forgejo feed with update policy (`release/update-policy.json`).
- Local-first AI (SoX + faster-whisper sidecar + bundled Ollama) and cloud paths both present.
- Meeting intelligence, RAG, voice conversation (local + Realtime), captions, file transcription: implemented.
- **Ad mediation**: `DirectHouseSponsorAdapter` performs real configurable REST bids; the other 9 adapters remain fail-closed stubs pending official SDKs (see `11-gap-backlog.md` GAP-ADS-01/02).
- Tier resolution now routes through `@d3ro/core/entitlement` (`resolveEntitlement`, `normalizeEntitlementTier`); `useLicenseState.isPro` includes `pro_plus`.
- No `TODO`/`FIXME` markers found in `src` (grep clean). `src/main/types/` is an empty directory.
---
## 7. Key file anchors
| Thing | Path |
|---|---|
| App entry / deep links | `src/main/index.ts` |
| Bootstrap order | `src/main/bootstrap.ts` |
| IPC registry | `src/main/ipc/index.ts` |
| IPC channel SSOT | `packages/core/src/ipc-channels.ts` |
| Preload API | `src/preload/index.ts` |
| Windows | `src/main/windows/WindowManager.ts` |
| Voice orchestrator | `src/main/services/VoiceModeService.ts` |
| DB schema | `src/main/db/schema.ts` |
| Renderer shell / routes | `src/renderer/components/AppLayout.tsx` |
| Update feed SSOT | `src/main/update-feed.ts` |
| Update policy SSOT | `release/update-policy.json` + `src/main/update-policy.ts` |

86
docs/map/05-web-app.md Normal file
View file

@ -0,0 +1,86 @@
# 05 — Web App (Next.js) Map
> Surface: `apps/web`
> Stack: Next.js 15 App Router + React 19 + MUI (via `@d3ro/ui`) + Supabase
> Role: cloud console; the reference surface for server-shared data UX
---
## 1. Route tree (`src/app/`)
Root layout: `ThemeProvider → I18nProvider → AuthProvider`.
| Route | Type | Feature |
|---|---|---|
| `/` | server | Redirects: session → `/dashboard`, else `/login` (also `/login` when Supabase unconfigured) |
| `/login` | client | OAuth (Google/GitHub/Apple) + email/password |
| `/accept-invite` | client | Team invite acceptance (`?token=``team-accept`); stores pending token if logged out |
| `/download` | client | Download center + release history; desktop installers marked "release pending"; client SHA-256 verify tool |
| `/releases` | server | Re-exports `/download` |
| `/auth/callback` | route handler | OAuth code → session exchange |
| `(app)/record` | server | `<MicRecorder/>` real-time record/STT |
| `(app)/commands` | client | Custom instruction CRUD/reorder/activate/execute |
| `(app)/dashboard` | client | Stats (sessions, time, words, streak), tier, recent history |
| `(app)/actions` | server | NL command → LLM parse → action (`<ActionRunner/>`) |
| `(app)/meetings` | server | Meetings list |
| `(app)/meetings/[id]` | server | Meeting detail: audio player, live transcript (Realtime), memos, generated docs |
| `(app)/knowledge` | server | RAG doc list + add + semantic search (search "planned V2-M+1") |
| `(app)/teams` | server | Teams list + create |
| `(app)/teams/[id]` | server | Team detail: members, owner-only invite, team meetings |
| `(app)/chat` | server | Talk AI chat (`<ChatPanel/>`) |
| `(app)/dictionary` | client | Pronunciation dictionary CRUD |
| `(app)/history` | client | History list: search, favorites, pagination, copy/delete |
| `(app)/history/[id]` | client | History detail: edit title/original/polished, favorite, delete |
| `(app)/billing` | client | Plans + Payple/Stripe checkout, manage/portal |
`(app)/layout.tsx` is the auth guard + shared `<Sidebar/>`.
---
## 2. Components (`src/components/`)
| Group | Components |
|---|---|
| `actions/` | `action-runner.tsx` (LLM parse → action: create_meeting/search_knowledge/create_memo/send_team_invite) |
| `record/` | `mic-recorder.tsx` (MediaRecorder + level analyser → `transcribeWebAudio`) |
| `dashboard/` | `meetings-trend-chart.tsx` (recharts) |
| `billing/` | `billing-checkout-options`, `checkout-button` (Stripe), `payple-checkout-button`, `payple-client`, `payple-manage-button`, `portal-button` |
| `meetings/` | `document-editor`, `generate-document-button`, `live-transcript-list` (Realtime), `markdown-preview` (Mermaid), `meeting-audio-player` (signed URL), `memo-form` |
| `chat/` | `chat-panel.tsx` |
| `teams/` | `create-team-form`, `invite-member-form`, `activity-feed` (team notes + realtime) |
| `knowledge/` | `add-knowledge-form` (text or `.txt`/`.md` file, newline-aware chunking, `embed-chunks` on submit), `knowledge-search` (semantic via `search-knowledge`) |
| `layout/` | `sidebar.tsx` (nav, theme selector, logout, i18n) |
| `providers/` | `auth-provider`, `i18n-provider`, `theme-mode-context`, `theme-provider` |
---
## 3. Clients (`src/lib/`)
| File | Purpose |
|---|---|
| `billing-catalog.ts` | Parse/validate billing catalog (schema v1, pro/pro_plus, Payple/Stripe prices) |
| `command-client.ts` | Custom instruction client (types, error codes, execute) |
| `dashboard-client.ts` | Dashboard snapshot loader |
| `dictionary-client.ts` | Dictionary CRUD/pagination/search + `serializeDictionary` / `parseDictionaryFile` / `importDictionaryFile` (JSON/CSV) |
| `history-client.ts` | History list/get/update/delete, revision-safe |
| `supabase-browser.ts` | Re-export `@d3ro/api-client/supabase-browser` |
| `supabase-server.ts` | Next `cookies()` wrapper |
| `web-stt-client.ts` | Web STT via `stt-proxy`, 25 MB limit, `WebSttError` codes |
---
## 4. Tests
Playwright specs in `apps/web/e2e/`: billing, payple-checkout, dashboard-dictionary-commands, history, web-stt-client, smoke.
---
## 5. Web status summary
- Full App Router console: auth (email + OAuth), record/STT, history (list+detail), commands, actions, meetings (list+detail+docs), knowledge (add+search), teams (list+detail+invite), chat, dictionary, billing, download/releases.
- Backed by Supabase tables + Edge Functions (`stt-proxy`, `llm-proxy`, `team-invite`, `team-accept`, `stripe-checkout`, `payple-checkout`, `payple-manage`, `search-knowledge`, `generate-meeting-document`).
- No literal `TODO`/`FIXME` markers; remaining smaller items (see [`11-gap-backlog.md`](./11-gap-backlog.md) `WEB-*`):
- Teams `member_count` is `0` in MVP (separate query needed) — `teams/page.tsx`.
- Action runner team-invite uses a manual redirect safety path instead of a live invite (`action-runner.tsx`).
- `/download` shows "artifact not yet published" for the current desktop release.
- Knowledge file upload, semantic search, dictionary import/export, and the team activity feed are now implemented (`[x]` in the catalog).

114
docs/map/06-mobile-app.md Normal file
View file

@ -0,0 +1,114 @@
# 06 — Mobile App (React Native) Map
> Surface: `apps/mobile-rn` (ACTIVE product mobile app)
> Stack: React Native 0.85 + React 19 (CLI) + Supabase + Google Play Billing + AdMob
> The Expo legacy skeleton (`apps/mobile`) was **removed 2026-09-13**; `apps/mobile-rn` is the only mobile runtime.
> Authoritative checklist: `docs/v3/MOBILE_APP_COMPLETION_SSOT.md`
---
## 1. Navigation structure
Root stack (`App.tsx` `RootNavigator`) branches on auth/onboarding:
- `recovery``UpdatePassword`
- `onboarding` → Onboarding + InviteAccept + Login
- unauthenticated → Login, SignUp, ForgotPassword, InviteAccept
- authenticated → `Main` (`TabNavigator`) + ~22 stack screens
Bottom tabs (`TabNavigator.tsx`): **Dash / Works / Record (center raised FAB) / Talk / Settings**.
Deep links: `d3ro-voice://` and `https://d3ro.chanpaca.net` (`accept-invite`).
Provider stack: `GestureHandlerRootView → SafeAreaProvider → AuthProvider → EntitlementProvider → BillingProvider → MobileAdsProvider → MobilePreferencesProvider → DeviceProvider → LocalizedApplication (I18n + StatusBar)`.
---
## 2. Screens (`src/screens/`, 30)
| Screen | Feature |
|---|---|
| `RecordScreen` | Recording + local-whisper/cloud transcription, import audio, share |
| `DashScreen` | Dashboard: usage stats, tier, streaks, trends |
| `WorksHubScreen` | WORKS hub: pinned feature drill-down (desktop-parity) |
| `TalkScreen` | Voice/text AI chat: STT, LLM streaming, TTS |
| `HistoryScreen` / `HistoryDetailScreen` | History list/detail: sync, search, filters, audio playback, edit/share/favorite/delete |
| `MeetingsScreen` / `MeetingDetailScreen` | Meeting workspaces: create/rename/delete, transcript, memos, documents, export, content report |
| `MemosScreen` | Memos: list, tags, search, share |
| `TemplatesScreen` | Dictation/meeting document templates CRUD |
| `KnowledgeScreen` | Knowledge docs: create/import/index/merge/delete/search |
| `CommandsScreen` | Custom instruction CRUD/reorder/activate/execute |
| `ActionsScreen` | Voice-action parse/confirm/execute + history |
| `DictionaryScreen` | Dictionary CRUD/search/filters |
| `TeamsScreen` / `TeamDetailScreen` | Teams: create, members, invites, rename, leave, meetings, activity feed/comments |
| `DevicesScreen` | Registered devices, revoke/remove |
| `NotificationsScreen` | Notification list, permission, deep-link nav |
| `AccountScreen` | Profile, identities, data export/delete, logout |
| `DataPortabilityScreen` | Export/import archive, dictionary, history, meeting docs |
| `AdminScreen` / `AdminUserDetailScreen` | Role-gated admin: users, subscriptions, audit, role/tier edits |
| `ProPaywallScreen` | Pro/Pro+ paywall: Play IAP, restore, rewarded ads |
| `OnboardingScreen` | First-run audience/theme/locale |
| `SettingsScreen` | Preferences, locale/theme, privacy links, logout |
| `LoginScreen` / `SignUpScreen` / `ForgotPasswordScreen` / `UpdatePasswordScreen` / `InviteAcceptScreen` | Auth flows |
---
## 3. Features (`src/features/`, 20 modules)
| Module | Purpose |
|---|---|
| `recording/` | Durable offline recording queue, retry processing, language handling |
| `import/` | Audio pick/decode/validate, local Whisper fallback, resumable multipart upload, Android share-intent intake |
| `history/` | History service + local cache + sync hook |
| `meetings/` | Meeting workspace CRUD, realtime channels, processing jobs, transcripts/memos/docs |
| `memos/` | Memo CRUD, tags, search, realtime, share |
| `talk/` | Streaming LLM, transcription, TTS playback |
| `teams/` | Team CRUD/members/invites/realtime, pending invite, `listTeamActivities`/`createTeamActivity` (activity feed) |
| `knowledge/` | Knowledge docs CRUD/index/merge/search + file picker |
| `commands/` | Custom command client |
| `actions/` | Voice-action parse/execute + side effects |
| `dictionary/` | Dictionary CRUD/pagination/search |
| `devices/` | Device registration/list/revoke |
| `dashboard/` | Dashboard stats + recent entries |
| `templates/` | Templates + generation idempotency |
| `auth/` | OAuth sign-in (in-app browser, PKCE) + identity linking |
| `admin/` | Admin user/subscription/audit APIs + session/role |
| `data-portability/` | Canonical JSON export/import, schema validation, legacy import, meeting export, file sharing |
| `chat/` | Chat LLM normalization/streaming, reportable generation IDs |
| `notifications/` | Notification contract/native registration/service/runtime hook |
| `reporting/` | AI content reporting (reason codes, Edge calls, idempotency) |
Core libs (`src/lib/`): `auth-context`, `auth-redirect`, `auth-capabilities`, `secure-auth-storage` (Keychain + legacy migration), `logout`, `account-exit`, `account-local-data` (central purge), `supabase`, `billing-context` (Play/App Store IAP), `entitlement-context`, `device-context`, `preferences-context`, `mobile-ads-context` (UMP consent + rewarded), `native-config`, `pkce-s256`, `random-id`, `audio-recorder`, `e2e-runtime-bootstrap`.
---
## 4. Mobile status summary
Source of truth for status is `docs/v3/MOBILE_APP_COMPLETION_SSOT.md` sections 0.1, 2.1, 4. Current condensed state:
| Area | Status |
|---|---|
| Auth / account | `[~]` Implemented + Jest GREEN; external OAuth consent→callback and logout/delete/provider device E2E pending |
| Onboarding / theme / accessibility | `[x]` device journey GREEN (some checklist rows still `[ ]` for a11y specifics) |
| Recording / transcription / Talk | `[x]` core path GREEN on API 34 (FGS, recovery, local Whisper, cloud fail-closed, TTS) |
| Data portability | `[x]` export/restore + share E2E GREEN |
| History / meetings | `[x]` real data + meeting creation contract GREEN; audio timestamp/speaker + cross-app E2E pending |
| Teams / push | `[~]` teams GREEN; real FCM delivery needs Firebase/FCM credentials |
| Billing / ads | `[~]` test ads + Google Play verification code GREEN; live store purchase/restore/settlement and production AdMob serving **RED (external)** |
| Admin | `[x]` mobile admin role E2E GREEN (ordinary/manager/admin/super_admin, stale JWT) |
| Quality / distribution | `[~]` lint/typecheck/Jest GREEN; production signed AAB + Play submission **RED (external gates)** |
**External blockers (do not treat as code gaps):** production Firebase project, AdMob live serving, Play Billing license tester, CI secret injection, signed production AAB, Play closed test (0/12, 14 days), production access approval. See SSOT §0, §6.
---
## 5. Legacy removal
The Expo skeleton (`apps/mobile`) was deleted on 2026-09-13: it duplicated auth/data logic, shipped a dev-bypass login, and was outside the workspace and CI. `scripts/ci/sync-version.mjs` and `package-lock.json` no longer reference it. `apps/mobile-rn` is the sole mobile runtime; git history retains the old app.
---
## 6. Tests & evidence
- Jest: `apps/mobile-rn/__tests__/` — per SSOT 57 suites / 353 tests (auth, privacy purge, admin, billing, recording language, generation idempotency, templates/memos, STT quota integration).
- Maestro E2E: `apps/mobile-rn/.maestro/` + evidence in `.maestro-output/`.
- Android instrumentation: `apps/mobile-rn/android/app/src/androidTest` (CSPRNG + incoming share).
- Build verifiers: `scripts/ci/verify-android-artifact.mjs`, `verify-android-app-links.mjs`, `verify-mobile-build-config.mjs`, `verify-mobile-release-boundary.mjs`.

107
docs/map/07-api-server.md Normal file
View file

@ -0,0 +1,107 @@
# 07 — API Server (.NET) Map
> Surface: `apps/api-server` (`D3ROVoice.Api`), tests in `apps/api-server.Tests`
> Stack: ASP.NET Core 10 + EF Core + SQLite
> Role: AI proxy (LLM/STT) + admin back-office backend for the .NET identity side
---
## 1. Composition root (`Program.cs`)
- **DI:** `AddControllers`, `AddHttpClient` (proxies), Swagger (`v1`), rate limiter policy `auth` (fixed window 10/min per IP, 429), `AddDbContext<AppDbContext>` (SQLite; `DATA_DIR`/`DB_PATH`), scoped `IAuthService`, `ILlmProxyService`, `ISttProxyService`, `IAdminOperationService`.
- **Auth:** JWT Bearer HS256; startup hard-fails unless `JWT_SECRET` (≥32 bytes), `JWT_ISSUER`, `JWT_AUDIENCE` are set. Zero clock skew.
- **Policies:** `ManagerOrAbove` (manager/admin/superadmin), `AdminOrAbove` (admin/superadmin), `SuperAdminOnly` (superadmin). Role normalization strips `_`/`-`, lowercases.
- **CORS/Hosts:** strict origin validation (`CORS_ALLOWED_ORIGINS`), `ALLOWED_HOSTS` required outside Development.
- **Startup DB init:** `EnsureCreated()`, raw `CREATE TABLE IF NOT EXISTS` for admin operation/audit tables, legacy SHA-256 password lockdown (`IsActive=false`, `Role="LegacyDisabled"`), idempotent env admin provisioning (`ADMIN_EMAIL`/`ADMIN_PASSWORD`, only when no active user), default LLM/STT endpoints seeded.
- **Middleware order:** Swagger (dev) → CORS → invite-page hardening (CSP/no-store) → default files → mobile/legacy asset block (404 for `.apk`/`.aab`/signed zips + legacy bundles) → static files → rate limiter → authentication → authorization.
- **Health:** `GET /health`, `GET /api/health``{status, service, version, uptimeSeconds, database, timestamp}`.
- **Fallbacks:** `/accept-invite``accept-invite.html`; `/admin/{*path}` → legacy embedded admin SPA in `wwwroot/admin`.
---
## 2. Controllers & routes
### `AuthController` (`/api/auth`)
| Route | Auth | Behavior |
|---|---|---|
| `POST /register` | anonymous + `X-D3RO-Bootstrap-Token` | Rate-limited, ≤16KB; requires `ADMIN_BOOTSTRAP_TOKEN`; fixed-time compare; `409` if registered |
| `POST /login` | anonymous | Rate-limited; returns JWT; `401` invalid |
| `GET /me` | `[Authorize]` | `UserInfoDto` or `404` |
### `LlmController` (`/api/llm`, class `[Authorize]`)
| Route | Behavior |
|---|---|
| `POST /generate` | Requires prompt; `GenerateAsync` |
| `POST /chat` | `ChatAsync` (last message = prompt) |
### `SttController` (`/api/stt`, class `[Authorize]`)
| Route | Auth | Behavior |
|---|---|---|
| `POST /transcribe` | any auth | **Always `410 Gone` `stt_edge_gateway_required`** — user transcription is Edge-only |
| `POST /internal/transcribe` | anonymous + `X-D3RO-STT-Gateway-Token` | multipart, ≤26MB; requires `D3RO_API_TOKEN`; fixed-time compare; provider orchestration; error map `503/400/502` |
| `GET /providers` | ManagerOrAbove | List STT endpoints |
| `POST /test?endpointId=` | ManagerOrAbove | Test endpoint |
### `AdminController` (`/api/admin`, class `ManagerOrAbove`)
All mutations flow through `ExecuteAdminMutationAsync` (idempotent + audited).
| Route | Effective policy |
|---|---|
| `GET /stats` | Manager+ |
| `GET /users` | Manager+ |
| `GET/POST/PUT/DELETE /endpoints[/{id}]` | Manager+ read; Admin+ write |
| `GET /stt-endpoints`, `/stt-endpoints/{id}` | Manager+ |
| `POST/PUT/DELETE /stt-endpoints[/{id}]`, `POST .../set-default` | Admin+ |
| `POST /stt-endpoints/{id}/test` | Manager+ |
| `POST /stt-endpoints/test-direct` | Admin+ |
| `GET /stt-usage`, `GET /usage` | Manager+ |
| `POST /license-audit` | **SuperAdminOnly** |
---
## 3. Services
| Service | Purpose |
|---|---|
| `AuthService` | Register (one-time SuperAdmin bootstrap only, `PasswordHasher<User>`, serialized), login (timing-safe dummy verify, inactive rejection, rehash), JWT gen (8h, claims id/email/role) |
| `LlmProxyService` | Resolve endpoint by model → fallback → Mock echo; OpenAI-style POST; parse content+usage; token cost → `ApiUsageLog`; errors → `ServerErrorLog` + fallback text |
| `SttProxyService` (~1182 lines) | Provider adapters (groq/openai/custom, deepgram, google, assemblyai, azure, local-sidecar), candidate resolution + fallback, content-type/duration detection, `SttUsageLog` cost, synthetic-tone endpoint test, CRUD with exclusive default, usage report |
| `AdminOperationService` | Validates actor/idempotency key/memo, SHA-256 request hash, `Serializable` transaction, idempotent replay, before/after audit entry |
---
## 4. Data (`Data/AppDbContext.cs`)
| DbSet / table | Key fields |
|---|---|
| `Users` | Id, unique Email, PasswordHash, Role (default "User"), IsActive, CreatedAt, LastLoginAt |
| `ModelEndpoints` | Id, unique ModelId, ModelName, Provider, EndpointUrl, ApiKey, per-1k costs, IsActive |
| `UsageLogs` | UserId/Email, model, tokens, cost, duration, status |
| `ErrorLogs` | ErrorType, Message, StackTrace, Endpoint |
| `SttProviderEndpoints` | Name, ProviderType, URL, ApiKey, ModelId, Method, Language, Prompt, Temperature, per-minute/second cost, IsDefault, FallbackPriority, ExtraHeadersJson |
| `SttUsageLogs` | User, endpoint, provider, duration, cost, latency, status, transcript preview |
| `AdminOperationRequests` | ActorEmail + IdempotencyKey (unique), Operation, RequestHash, ResponseJson |
| `AdminAuditEntries` | ActorEmail, Action, TargetType/Id, BeforeJson, AfterJson, Memo, IdempotencyKey |
DTOs (`Dtos/Dtos.cs`): auth, license audit, LLM, admin/model endpoints, STT (transcribe, endpoints, test, usage reports).
---
## 5. Tests (`apps/api-server.Tests/`, xUnit)
`AdminAuthorizationE2ETests`, `AdminOperationServiceTests`, `AuthBootstrapControllerTests`, `AuthSecurityTests`, `SttControllerSecurityTests`, `SttFailClosedTests`, `SttGatewayAuthorizationE2ETests` (26/26 per SSOT).
---
## 6. API server status summary
- Auth, login, JWT, role policies, rate limiting, CORS, host restrictions: **implemented**.
- LLM proxy: **implemented** (with local Mock echo fallback for standalone testing).
- STT proxy: **implemented** for internal gateway; public `/transcribe` intentionally `410` (Edge-only by design).
- Admin API: users, endpoints (LLM + STT), usage, license audit: **implemented** with idempotency + audit.
- Known intentional states (not bugs):
- Legacy SHA-256 users force-disabled.
- No hardcoded/seeded admin credentials.
- Mobile/legacy release assets blocked with 404.
- `LlmProxyService` Mock fallback.
- This backend holds a **separate identity** from Supabase; see `11-gap-backlog.md` `ID-01`.

View file

@ -0,0 +1,91 @@
# 08 — Admin Console (Next.js) Map
> Surface: `apps/admin`
> Stack: Next.js 16 App Router + MUI (`@d3ro/ui` theme) + Supabase service role + .NET proxy
> Role: back office CRM/ops — users, subscriptions, models, usage, audit, releases, ads
---
## 1. Route tree (`src/app/`)
### Public
| Route | Purpose |
|---|---|
| `/login` | Email/password → `/api/auth/login`; Google OAuth via Supabase → `/auth/callback`; maps error keys |
| `/unauthorized` | 403 screen |
| `/auth/callback` | OAuth code → session exchange |
### Protected `(admin)` (guarded by `(admin)/layout.tsx``requireManager()`)
| Route | Purpose |
|---|---|
| `/` | Dashboard: backend stats, node health, MRR/ARR/active subscriptions (Supabase), recent errors |
| `/pipelines` | AI/voice pipeline telemetry; explicit "unavailable" card when no measured data |
| `/models` | LLM model + STT provider manager (presets, CRUD, test) |
| `/releases` | Forgejo live release hub (assets, platforms, sizes, downloads, SHA-256) |
| `/users` | User directory: search + tier/role filters |
| `/users/[id]` | User 360: profile, subscription, 30-day usage, role change (admin+), payment history |
| `/subscriptions` | Subscription ops list + filters; license issuer (super_admin) |
| `/subscriptions/new` | Grant VIP subscription (admin+) |
| `/subscriptions/[id]` | Subscription detail: edit (manager+) / delete (admin+) |
| `/ads` | Ad mediation console (10 networks, all fail_closed) + reward stats |
| `/support` | **Stub** — explicit "not configured" panel |
| `/usage` | Combined LLM + STT usage/cost analytics |
| `/audit-log` | Supabase audit log list + target filter + pagination |
| `/audit-log/[id]` | Audit detail with before/after diff |
---
## 2. API route handlers (`src/app/api/`)
| Route | Methods | Behavior |
|---|---|---|
| `/api/auth/login` | POST | Validate body, reject honeypot `trap`, in-memory rate limit/lockout, proxy to `.NET /api/auth/login` (7s timeout, HTTPS in prod), validate token/role/email/expiry, sign HMAC session cookie `d3ro_admin_session` |
| `/api/auth/logout` | POST/GET | Clear cookie; GET redirects `/login` |
| `/api/admin/backend/[...segments]` | GET/POST/PUT/DELETE | Allow-list proxy to `.NET /api/admin/*`; required role by path; same-origin for non-GET; ≤64KB; UUID idempotency-key for mutations |
| `/api/admin/license` | POST | Same-origin + `requireVerifiedBackendSession('super_admin')`; sign Ed25519 key with `ADMIN_LICENSE_PRIVATE_KEY` (`@d3ro/core/utils/crypto-license`); best-effort audit |
| `/api/admin/supabase/[operation]` | GET/POST/PATCH/DELETE | `admin-users`, `admin-subscriptions`, `admin-payments` via RPCs; strict allow-lists; Payple live history returns `501` |
| `/auth/callback` | GET | Supabase OAuth exchange |
---
## 3. Libraries (`src/lib/`)
| File | Purpose |
|---|---|
| `admin-session.ts` | Session types, secret validation (≥32 bytes), strict cookie parse, `adminCookieSecure()` escape hatch |
| `security.ts` | server-only HMAC-SHA256 sign/verify, in-memory rate limit/lockout, runtime security validation |
| `admin-guard.ts` | RSC guards `requireManager`/`requireAdmin`/`requireSuperAdmin`, role helpers |
| `edge-session.ts` | Edge-runtime HMAC verify via WebCrypto (used by `proxy.ts`) |
| `backend-session.ts` | `requireApiServerOrigin`, `requireVerifiedBackendSession(minRole)` (verifies cookie + `.NET /api/auth/me`), `fetchAdminBackend` |
| `api-server.ts` | server-only data access to .NET backend (stats, users, endpoints, usage reports) |
| `backend-admin-client.ts` | client CRUD for model/STT endpoints with auto idempotency keys |
| `admin-api.ts` | client `callAdminApi` for Supabase admin operations |
| `supabase-admin.ts` | service-role client, actor resolution RPC, product user fetch |
| `supabase-browser.ts` / `supabase-server.ts` | client/server Supabase wrappers |
| `ad-monetization.ts` | `MEDIATION_ROSTER` (fail_closed) + ad reward stats |
| `subscription-metrics.ts` | MRR/ARR/active/tier breakdown |
| `audit-sanitize.ts` | recursive redaction of sensitive keys in audit snapshots |
| `forgejo-releases.ts` | Forgejo release feed parser (`RELEASE_REPO_URL`) |
| `console-theme.ts` | design tokens + MUI style presets |
Root files: `instrumentation.ts` (startup security validation), `proxy.ts` (edge middleware: public paths, auth redirect, security headers), `robots.ts` (disallow all).
---
## 4. Components (`src/components/`)
`admin-sidebar` (nav island: Core Platform / Customer & Revenue / Intelligence & Security), `unavailable-admin-panel` (reusable "NOT CONNECTED", no sample data), `audit-diff-viewer`, `payment-history`, `subscription-form`, `role-change-dialog` + `role-change-button`, `memo-dialog`, `license-issuer-button` + `license-issuer-dialog`, `checksum-copy`, charts (`dau-chart`, `feature-usage-chart`, `top-users-chart`).
---
## 5. Admin status summary
- Dashboard, models, releases, users, subscriptions, usage, audit log, ads: **implemented** against real backend/Supabase data.
- Security: HMAC signed sessions, RSC + edge guards, rate limit/lockout, honeypot, strict origin/allow-list, no-store, robots disallow, audit redaction. Red-team scenarios were exercised (see `memory/project_status.md`).
- Explicit fail-closed / not-configured states (by design, not bugs):
- `/support` stub — no ticket/SLA/diagnostics contract.
- `/ads` — all 10 networks `fail_closed`; no live bids.
- `admin-payments` Payple live history → `501`.
- `UnavailableAdminPanel` whenever Supabase env absent; writes disabled, no sample metrics.
- `/pipelines` and dashboard node/error sections render only measured data.
- Deploy: `Dockerfile.admin` / `apps/admin/Dockerfile` → GHCR + NAS compose; GitLab admin NAS deploy job disabled.

View file

@ -0,0 +1,101 @@
# 09 — Backend: Supabase + Cloudflare Map
> Surfaces: `server/supabase` (Postgres + Deno Edge Functions), `server/cloudflare-worker` (edge gateway)
> Role: canonical product data, auth, RLS, storage, AI proxies, billing, notifications
---
## 1. Supabase project (`server/supabase/`)
- `config.toml` — project_id `d3ro-voice`; local ports API 55321 / DB 55322 / Studio 55323 / Inbucket 55324; DB major v17; storage 50 MiB; auth `site_url` localhost:5173, redirects include `https://d3ro.chanpaca.net` and `d3ro-voice://auth-callback`; external providers Google/GitHub/Apple; per-function `verify_jwt` settings; analytics off.
- `seed.sql`, `migrations/` (63), `functions/` (~27), `functions/_shared/`, `tests/`, `deno.json`.
> Note: root `supabase/` contains only empty scaffolding (`.branches/`, `snippets/`). The real project lives under `server/supabase/`.
### 1.1 Migrations (62) — thematic groups
| Theme | Examples |
|---|---|
| Core schema + RLS + auth triggers | initial tables, profiles, triggers |
| Storage buckets | audio, meeting documents, exports |
| Teams | team invites, membership, roles, `team_activities` feed (`20260913000033`) |
| Knowledge / RAG | `knowledge_documents`, `knowledge_chunks`, pgvector |
| Notifications / push | push tokens, durable outbox |
| Billing | Payple, Stripe, subscriptions, payment provider events/operations |
| Admin | admin roles, audit log, atomic admin RPCs |
| Mobile platform | mobile platform/monetization/runtime integrity |
| Commands | atomic command reorder |
| Devices | device registration + revocation |
| Content reporting | report reasons, generation receipts |
| Meetings | meeting documents |
| STT quota | atomic quota reservations (00026) |
| Ads | ad reward receipt replay protection (00028) |
Migration numbering referenced in SSOT goes up to `00028`; CI verifies `migration-up` + shadow replay.
### 1.2 Edge Functions (~27)
| Function | Purpose |
|---|---|
| `stt-proxy` | User STT gateway: auth, atomic quota reservation/refund, provider fallback, fail-closed |
| `llm-proxy` | LLM gateway (Claude/OpenAI) |
| `realtime-token` | OpenAI Realtime ephemeral token (tier-gated, session quota) |
| `generate-meeting-document` | AI document generation (minutes/report/idea-note/mindmap) |
| `embed-chunks` / `search-knowledge` | RAG embeddings + semantic search |
| `content-report` | AI content reporting |
| `team-invite` / `team-accept` | Team invitations |
| `send-push` | Push delivery |
| `account-delete` | Account deletion cascade + provider unlink |
| `admin-users` / `admin-subscriptions` / `admin-payments` / `admin-audit-log` | Admin operations |
| `billing-catalog` | Server pricing catalog |
| `stripe-checkout` / `stripe-portal` / `stripe-webhook` | Stripe billing |
| `payple-checkout` / `payple-manage` / `payple-renew` / `payple-webhook` | Payple billing (Korea) |
| `iap-verify` | Google Play / App Store purchase verification |
| `admob-ssv` | AdMob server-side verification + reward ledger |
| `google-play-rtdn` | Play Real-time Developer Notifications |
Shared contracts in `functions/_shared/`: admin, auth, audit, cors, quota, payple, push, llm, stt, team, generation-receipt, google-play, pubsub, generative-ai-safety (+ `*.test.ts`).
### 1.3 Tests (`server/supabase/tests/`)
Integration/E2E: content-report red e2e, content-reporting (ps1/sql), mobile platform/recording/reward-race, payment provider, mobile release preflight, public mobile runtime, push claim/outbox, team push security, STT quota.
---
## 2. Cloudflare worker (`server/cloudflare-worker/`)
- `wrangler.toml` — worker `d3ro-voice-api`, compat 2024-04-01, `BACKEND_ORIGIN=http://192.168.0.39:5050`, optional custom domain route (commented).
- `src/index.ts` — CORS preflight, `/worker-health`, forwards to backend origin with `X-Forwarded-*` / `X-D3RO-Edge-Proxy` headers.
Cloudflare Tunnel `kd-nas` maps public hostnames to NAS services.
---
## 3. Data model (product tables, high level)
Canonical product data lives in Supabase Postgres with RLS:
- **Identity:** `auth.users` + `profiles`, roles/claims, identity links.
- **Content:** `history`, `meetings`, `meeting_memos`, `meeting_documents`, `memos`, `dictionary`, `commands`/instructions, `templates`.
- **Knowledge:** `knowledge_documents`, `knowledge_chunks` (pgvector).
- **Teams:** teams, members, invites, activity feed (`team_activities`, RPC-only writes, realtime-enabled).
- **Delivery:** `devices`, `push_tokens`, push outbox.
- **Monetization:** `subscriptions`, `payment_provider_events/operations`, `ad_reward_claims`, IAP receipts, generation receipts.
- **Ops:** `audit_log` + admin operation records.
- **Portability:** `portable_exports` (+ storage).
Shared TS types for these live in `packages/api-client` (SSOT).
---
## 4. Backend status summary
- Auth (email + Google/GitHub/Apple), RLS, storage, realtime: **implemented**; production Auth + Google provider entry verified GREEN; GitHub/Apple provider secrets and mobile consent callback pending (external).
- STT/LLM proxies: **implemented** and fail-closed (no synthetic transcripts); atomic quota reservations verified with 20-way concurrency.
- Billing (Stripe + Payple + IAP verify + webhooks/RTDN): **implemented**; live provider end-to-end and Payple webhook signature verification pending.
- Ads (AdMob SSV, rewarded ledger, replay protection): **implemented** (Edge v13 ACTIVE); production AdMob serving blocked externally (review/serving limits/store link/payment profile).
- Push: Supabase owns tokens/devices/outbox/retries. Transports implemented for **FCM, Web Push (VAPID + RFC 8291), and APNs (.p8 token)**; the Cloudflare Worker cron drains the outbox every minute. Android still requires FCM at the device. Details: `docs/deployment/push-transport-without-firebase.md`.
- Content safety: generation receipts + `content-report` **implemented**.
- Data portability (`account-delete`, export/restore): **implemented**.
External gates are enumerated in `docs/v3/MOBILE_APP_COMPLETION_SSOT.md` §0/§6 and mirrored in `11-gap-backlog.md` (`EXT-*`, `BE-*`).

View file

@ -0,0 +1,205 @@
# 10 — Feature Catalog (Feature Map)
> The canonical feature map. One row = one user-facing capability.
> Status is per surface. Legend in [`00-index.md`](./00-index.md) §2.
> Surfaces: D = desktop (`apps/desktop`), W = web (`apps/web`), M = mobile (`apps/mobile-rn`), B = backend (Supabase/.NET).
**How to use an ID:** cite it in commits, plans, and backlog. Example: "CAP-03 is `[~]` on mobile (external OAuth pending)".
Status quick-reference: `[x]` done+verified · `[~]` partial/unverified · `[ ]` planned/absent · `[!]` blocked externally · `[-]` N/A.
---
## CAP — Capture & Transcribe
| ID | Feature | D | W | M | B | Anchors / notes |
|---|---|---|---|---|---|---|
| CAP-01 | Push-to-talk dictation (hold/release) | [x] | [-] | [x] | [-] | Desktop `VoiceModeService`; mobile RecordScreen via app CTA/notification action (no global hotkey) |
| CAP-02 | Hands-free toggle dictation | [x] | [-] | [x] | [-] | Desktop double-press; mobile toggle |
| CAP-03 | Live partial transcript while recording | [x] | [ ] | [ ] | [-] | Desktop `voice:partialTranscript` + recording-tip |
| CAP-04 | Recording waveform + level meter | [x] | [x] | [x] | [-] | Desktop 9-bar cos distribution; mobile audio level |
| CAP-05 | Device/mic selection | [x] | [ ] | [~] | [-] | Desktop config; mobile uses system default |
| CAP-06 | System/loopback audio capture | [x] | [-] | [ ] | [-] | Desktop only (caption source); mobile policy-limited |
| CAP-07 | Local Whisper STT | [x] | [-] | [x] | [-] | Desktop sidecar; mobile on-device Whisper (supported devices) |
| CAP-08 | Cloud STT (multi-provider) | [x] | [x] | [x] | [x] | Desktop 6 providers + D3RO Cloud; web/mobile via `stt-proxy`; .NET internal gateway |
| CAP-09 | STT auto-fallback + fail-closed | [x] | [x] | [x] | [x] | `STTManager`; SSOT R-021/R-022 GREEN |
| CAP-10 | STT model download/management UI | [x] | [-] | [~] | [-] | Desktop model manager + onboarding; mobile bundled model |
| CAP-11 | File transcription (audio/video) | [x] | [ ] | [x] | [~] | Desktop ffmpeg chunking; mobile import picker; web deferred |
| CAP-12 | Audio import from other apps (share intent) | [-] | [-] | [x] | [-] | Mobile Android `ACTION_SEND`/`ACTION_VIEW` (SSOT R-016 GREEN) |
| CAP-13 | Live captions overlay | [x] | [-] | [-] | [-] | Desktop `CaptionService` + caption-overlay popup |
| CAP-14 | Recording persistence / crash recovery | [x] | [ ] | [x] | [-] | Desktop WAV persist; mobile durable queue + process-kill WAV recovery |
| CAP-15 | Android foreground recording service | [-] | [-] | [x] | [-] | Mobile API 34 FGS + persistent notification (SSOT R-005 GREEN) |
---
## AI — AI Processing
| ID | Feature | D | W | M | B | Anchors / notes |
|---|---|---|---|---|---|---|
| AI-01 | Local LLM (Ollama) | [x] | [-] | [ ] | [-] | Desktop bundled Ollama |
| AI-02 | Cloud LLM (Claude/OpenAI) | [x] | [x] | [x] | [x] | Desktop `PremiumLLMService`; web/mobile via `llm-proxy`; .NET `LlmProxyService` |
| AI-03 | Auto Polish (cleanup/filler removal) | [x] | [~] | [~] | [x] | Desktop built-in; web/mobile via commands |
| AI-04 | Translate / summarize / rephrase | [x] | [x] | [x] | [x] | Built-in instructions |
| AI-05 | Custom instructions (user commands) | [x] | [x] | [x] | [x] | Desktop `CommandsPage` (Red Team RT-03 verified); web `commands`; mobile `CommandsScreen` |
| AI-06 | Voice keyword commands | [x] | [-] | [ ] | [-] | Desktop `VoiceCommandService` + command popup |
| AI-07 | LLM Chains (multi-step pipelines) | [x] | [ ] | [ ] | [-] | Desktop `ChainService` |
| AI-08 | Screen/context capture for prompts | [x] | [-] | [ ] | [-] | Desktop `ScreenContextService` |
| AI-09 | Streaming responses | [x] | [x] | [x] | [x] | SSE/NDJSON streaming |
| AI-10 | Dictation templates (voice form fill) | [x] | [ ] | [x] | [~] | Desktop `DictationTemplateService`; mobile `TemplatesScreen` |
---
## MEM — Memory & Knowledge
| ID | Feature | D | W | M | B | Anchors / notes |
|---|---|---|---|---|---|---|
| MEM-01 | History list + search | [x] | [x] | [x] | [x] | Desktop SQLite; web/mobile Supabase |
| MEM-02 | History detail + edit | [x] | [x] | [x] | [x] | Mobile `HistoryDetailScreen` |
| MEM-03 | History favorites | [x] | [x] | [x] | [x] | |
| MEM-04 | History export / share | [x] | [~] | [x] | [x] | Desktop export; web limited; mobile share sheet |
| MEM-05 | History audio playback | [x] | [x] | [x] | [x] | Signed URLs on web/mobile |
| MEM-06 | Dictionary (custom vocabulary) | [x] | [x] | [x] | [x] | All surfaces CRUD; Desktop Red Team RT-02 & RT-18 fuzzed/verified |
| MEM-07 | Dictionary import/export | [x] | [x] | [x] | [-] | Desktop `dictionary:import/export` JSON+CSV (file dialogs, `DictionaryService`); web `serializeDictionary`/`importDictionaryFile` + header buttons; mobile CSV/TXT export + CSV/JSON/TXT import via `data-portability` |
| MEM-08 | Memos (tags over history) | [x] | [ ] | [x] | [x] | Desktop `MemoService`; mobile `MemosScreen`; web none |
| MEM-09 | Knowledge base / RAG add+index | [x] | [x] | [x] | [x] | Desktop local RAG (DEF-008 infinite chunking loop resolved, RT-08 verified); web/mobile cloud RAG |
| MEM-10 | Semantic search over knowledge | [x] | [x] | [x] | [x] | Web `KnowledgeSearch` calls `search-knowledge` (was mislabeled deferred); mobile + Edge `search-knowledge` |
| MEM-11 | Knowledge file upload | [x] | [x] | [x] | [x] | Web `.txt`/`.md` picker + newline-aware chunking + `embed-chunks`; desktop txt/md/pdf/docx; mobile file picker |
| MEM-12 | Voice actions (OS automation) | [x] | [x] | [x] | [x] | Desktop `VoiceActionService`; web `ActionRunner` (simulated); mobile `ActionsScreen` |
| MEM-13 | Cross-surface data sync | [~] | [x] | [x] | [x] | Desktop Supabase sync (V2-4); web/mobile native |
| MEM-14 | Memo tag search | [x] | [ ] | [x] | [x] | |
---
## MTG — Meetings
| ID | Feature | D | W | M | B | Anchors / notes |
|---|---|---|---|---|---|---|
| MTG-01 | Meeting recording + live transcript | [x] | [~] | [x] | [x] | Desktop `MeetingModeService`; web realtime view; mobile |
| MTG-02 | Meeting creation (title/attendees/language/template) | [x] | [x] | [x] | [x] | SSOT H-006 GREEN |
| MTG-03 | Timestamped memos during meeting | [x] | [x] | [x] | [x] | |
| MTG-04 | AI summary generation | [x] | [x] | [~] | [x] | Desktop `MeetingSummaryService`; web `generate-document-button`; mobile via Edge |
| MTG-05 | Document generation (minutes/report/idea-note/mindmap) | [x] | [x] | [x] | [x] | Edge `generate-meeting-document`; SSOT F-011 GREEN |
| MTG-06 | Document edit (Markdown) | [x] | [x] | [x] | [x] | Desktop editor; web `document-editor` |
| MTG-07 | Export PDF/DOCX/TXT/Markdown | [x] | [~] | [x] | [x] | Desktop `ExportMenu`; web markdown; mobile print/DOCX chooser (SSOT data portability GREEN) |
| MTG-08 | Speaker diarization | [~] | [ ] | [ ] | [ ] | Desktop `phase-15.5` (LLM estimate + pyannote prep); mobile SSOT H-014 pending |
| MTG-09 | Audio seek ↔ transcript timestamp | [ ] | [ ] | [ ] | [ ] | SSOT H-011 pending |
| MTG-10 | Meeting list search/filter/sort | [~] | [ ] | [~] | [x] | Basic lists; advanced filters pending |
| MTG-11 | Meeting AI chat over transcript | [x] | [ ] | [ ] | [x] | Desktop `MeetingChatPanel` + `MEETING_CHAT` |
| MTG-12 | Content reporting for generated docs | [-] | [ ] | [x] | [x] | `content-report` Edge + generation receipts |
---
## CV — Conversation
| ID | Feature | D | W | M | B | Anchors / notes |
|---|---|---|---|---|---|---|
| CV-01 | Local duplex voice conversation (STT→LLM→TTS) | [x] | [ ] | [x] | [-] | Desktop `VoiceConversationService`; mobile Talk |
| CV-02 | Realtime voice (OpenAI gpt-realtime, Premium) | [x] | [ ] | [ ] | [x] | Desktop `useRealtimeConversation` + `realtime-token` Edge |
| CV-03 | Text AI chat | [x] | [x] | [x] | [x] | Desktop chat, web `chat-panel`, mobile `TalkScreen` |
| CV-04 | TTS playback + controls | [x] | [ ] | [x] | [-] | Desktop SAPI/`say`; mobile Android TTS |
| CV-05 | Voice selection / backend selection | [x] | [ ] | [~] | [-] | Desktop settings `conversationBackend` |
---
## ACC — Accounts, Sync, Devices, Portability
| ID | Feature | D | W | M | B | Anchors / notes |
|---|---|---|---|---|---|---|
| ACC-01 | Email sign-up/login | [ ] | [x] | [x] | [x] | Desktop uses separate online-auth; SSOT A-001..A-005 `[ ]` rows are mobile checklist granularity |
| ACC-02 | OAuth Google | [~] | [x] | [~] | [x] | Mobile full consent→callback pending (external) |
| ACC-03 | OAuth GitHub / Apple | [~] | [~] | [~] | [~] | GitHub/Apple provider secrets pending |
| ACC-04 | Password reset / recovery deep link | [ ] | [ ] | [x] | [x] | Mobile ForgotPassword/UpdatePassword |
| ACC-05 | Account profile / identity management | [~] | [~] | [x] | [x] | Desktop `LicenseTab`; mobile `AccountScreen` |
| ACC-06 | Logout + local sensitive purge | [x] | [x] | [x] | [x] | Mobile central purge GREEN |
| ACC-07 | Account deletion (server cascade + local purge) | [ ] | [ ] | [~] | [x] | `account-delete` Edge; mobile device E2E pending |
| ACC-08 | Cloud sync (per-user data) | [x] | [x] | [x] | [x] | Desktop `CloudSyncService`; SSOT D-* largely `[ ]` granular |
| ACC-09 | Device registration + revocation | [-] | [ ] | [x] | [x] | Mobile `DevicesScreen` |
| ACC-10 | Offline queue + retry | [~] | [ ] | [x] | [x] | Mobile durable queue |
| ACC-11 | Data export/import (portability) | [~] | [ ] | [x] | [x] | Mobile canonical JSON E2E GREEN; desktop has export files |
| ACC-12 | Notification / push | [ ] | [~] | [~] | [x] | Backend transports for FCM + Web Push (VAPID) + APNs (.p8) + outbox cron drain implemented. Web/mobile client registration for webpush/apns still pending; Android delivery needs FCM project. |
---
## TEAM — Teams & Admin
| ID | Feature | D | W | M | B | Anchors / notes |
|---|---|---|---|---|---|---|
| TEAM-01 | Team create / rename / delete | [-] | [x] | [x] | [x] | Desktop N/A |
| TEAM-02 | Invite by email / link + accept deep link | [-] | [x] | [x] | [x] | `team-invite`/`team-accept` |
| TEAM-03 | Members + roles (admin/member/viewer) + leave | [-] | [x] | [x] | [x] | |
| TEAM-04 | Team meetings/docs sharing + RLS isolation | [-] | [x] | [~] | [x] | Cross-user isolation tested |
| TEAM-05 | Team comments / activity feed | [-] | [x] | [x] | [x] | `team_activities` migration + `create_team_activity` RPC + realtime; web `ActivityFeed`, mobile TeamDetail activity card (2026-09-13) |
| TEAM-06 | Admin back office (users/subs/models/usage/audit) | [-] | [-] | [x] | [x] | `apps/admin` + mobile `AdminScreen` |
| TEAM-07 | Role-based destructive action confirm + audit | [-] | [x] | [x] | [x] | SSOT T-006..T-008 GREEN |
| TEAM-08 | Desktop admin surface | [ ] | [-] | [-] | [-] | None; N/A by design |
---
## MON — Monetization & Ads
| ID | Feature | D | W | M | B | Anchors / notes |
|---|---|---|---|---|---|---|
| MON-01 | Tier gating (Free/Pro/Pro+/Team/Enterprise) | [x] | [x] | [x] | [x] | `LicenseService`, entitlement provider |
| MON-02 | Usage quotas (daily_usage) | [x] | [x] | [x] | [x] | |
| MON-03 | Desktop offline license (Ed25519) | [x] | [-] | [-] | [x] | `crypto-license` + admin issuer |
| MON-04 | Web checkout (Stripe) | [-] | [x] | [~] | [x] | Stripe checkout/portal/webhook |
| MON-05 | Web checkout (Payple) | [-] | [x] | [~] | [~] | Payple checkout/manage/renew/webhook; webhook signature pending |
| MON-06 | Paywall / upgrade prompts | [x] | [x] | [x] | [x] | `UpgradePromptModal`, `ProPaywallScreen` |
| MON-07 | Mobile IAP purchase + restore | [-] | [-] | [~] | [x] | `iap-verify` + `billing-context`; live store E2E blocked |
| MON-08 | Billing catalog / pricing display | [x] | [x] | [x] | [x] | Server catalog SSOT; hardcoded prices removed |
| MON-09 | Free-tier banner ads | [~] | [-] | [x] | [x] | Desktop adapters fail-closed; mobile AdMob test GREEN, prod serving blocked |
| MON-10 | Rewarded ads → quota credits | [~] | [-] | [x] | [x] | Desktop `RewardedQuotaModal` (stub adapters); mobile SSV GREEN |
| MON-11 | Ad mediation engine + settlement | [~] | [-] | [~] | [x] | Engine + settlement built. `DirectHouseSponsorAdapter` is now a **real configurable REST adapter** (bid/impression/click/reward via `endpointUrl`, fail-closed when unconfigured, unit-tested). Other 9 networks remain `UnavailableAdAdapter` stubs pending official SDKs. |
| MON-12 | Subscription management (portal/store) | [-] | [x] | [x] | [x] | Stripe portal / Payple manage / Play manage |
---
## SHELL — Platform Shell, Settings, Onboarding, Support
| ID | Feature | D | W | M | B | Anchors / notes |
|---|---|---|---|---|---|---|
| SHELL-01 | Settings / preferences | [x] | [~] | [x] | [x] | Desktop tabbed modal; web theme/i18n; mobile `SettingsScreen` |
| SHELL-02 | Theme system (6 themes) | [x] | [x] | [x] | [-] | `theme.ts` SSOT |
| SHELL-03 | i18n (12 locales) | [x] | [x] | [x] | [-] | `@d3ro/i18n`; ko/en fully translated, others partial |
| SHELL-04 | Onboarding / first-run | [x] | [ ] | [x] | [-] | Desktop model bootstrap; mobile audience/theme/locale |
| SHELL-05 | Accessibility / reduced motion | [~] | [~] | [~] | [-] | Desktop reduced-motion honored; mobile a11y rows pending |
| SHELL-06 | System tray / background | [x] | [-] | [-] | [-] | Desktop tray |
| SHELL-07 | Auto-launch on login | [x] | [-] | [-] | [-] | Desktop only |
| SHELL-08 | Auto-update | [x] | [-] | [!] | [-] | Desktop electron-updater; Forgejo canonical feed; channels + mandatory/major-vs-delta policy (`release/update-policy.json`); mobile store updates |
| SHELL-09 | Support / diagnostics | [x] | [ ] | [ ] | [~] | Desktop `SupportModal`; admin `/support` stub |
| SHELL-10 | Download center / releases | [-] | [x] | [-] | [x] | Web `/download`, admin `/releases`, Forgejo |
| SHELL-11 | Landing site / legal pages | [-] | [-] | [-] | [-] | `site/` — privacy/terms/delete-account live |
| SHELL-12 | Notifications (in-app / desktop) | [x] | [ ] | [x] | [x] | Desktop events; mobile push |
---
## INFRA — Build, CI, Release, Quality
| ID | Feature | Status | Anchors |
|---|---|---|---|
| INFRA-01 | Monorepo + workspaces + turbo | [x] | `package.json`, `turbo.json` |
| INFRA-14 | Unified entitlement resolver (`@d3ro/core/entitlement`) | [x] | `EntitlementSnapshot` + `resolveEntitlement` map Supabase/desktop-license/.NET sources to one contract; desktop `isPro` fixed, `syncFromCloud` normalized; mobile/web adoption incremental (see `11` GAP-ID-02) |
| INFRA-02 | Shared packages | [x] | `packages/*` |
| INFRA-03 | Desktop build + signed packaging | [x] | `electron-builder.yml`, GitLab `package-windows/macos` |
| INFRA-04 | Mobile CI (debug/E2E/release) | [~] | `.github/workflows/ci.yml`, `.gitlab-ci.yml`; production AAB external |
| INFRA-05 | .NET API tests | [x] | `apps/api-server.Tests` |
| INFRA-06 | Edge function tests (Deno) | [x] | `server/supabase/functions`, `tests/` |
| INFRA-07 | E2E desktop (playwright) | [x] | `apps/desktop/tests`, `apps/desktop/playwright.config.ts` |
| INFRA-08 | E2E web (playwright) | [x] | `apps/web/e2e` |
| INFRA-09 | E2E mobile (Maestro + instrumentation) | [~] | `.maestro/`, `androidTest`; emulator API 35 gate in CI |
| INFRA-10 | Secret scanning / release boundaries | [x] | `scripts/ci/check-no-hardcoded-secrets.mjs`, `verify-mobile-release-*.mjs` |
| INFRA-11 | Docker + NAS deploy | [x] | `docker-compose.nas.yml`, `scripts/deploy-nas.*` |
| INFRA-12 | Cloudflare edge + tunnel | [x] | `server/cloudflare-worker`, Cloudflare Tunnel `kd-nas` |
| INFRA-13 | Site deploy (Cloudflare Pages + GitHub Pages) | [x] | `.forgejo/workflows/deploy-site.yml`, `.github/workflows/deploy-site.yml` |
| INFRA-15 | Update & release system | [x] | Canonical Forgejo feed + channels/policy (`release/update-policy.json`, `src/main/update-policy.ts`), canonical publisher `scripts/ci/publish-forgejo-release.mjs`, legacy GitLab mirror; `npm run release:metadata:test`. v1.1.0 published to Forgejo & official download centers active on web (`/download`, `/releases`) and site (`#download`). |
---
## Coverage summary (by surface)
| Surface | `[x]` | `[~]` | `[ ]` | Notable strength | Notable weakness |
|---|---|---|---|---|---|
| Desktop | ~40 | 3 | ~8 | Local AI pipeline, meetings, RAG, conversation, hotkeys | Ads stubs, no team admin, no email account |
| Web | ~22 | 6 | ~14 | Server-shared data UX, billing, meetings, teams | No local AI, limited knowledge upload/search |
| Mobile | ~40 | 12 | ~18 | Cloud + native recording, portability, admin, IAP/ads | External store/console gates, a11y, deep E2E pending |
| Backend | ~45 | 6 | ~4 | RLS, Edge functions, billing, fail-closed AI | Payple webhook signature, some external provider keys |

135
docs/map/11-gap-backlog.md Normal file
View file

@ -0,0 +1,135 @@
# 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.
---
## 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 v1.1.0 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` | `[x]` 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. Public download centers in `apps/web` (`/download`, `/releases`) and `site` (`#download`) activated with direct 1.1.0 installer download, SHA-256 verification, and mirror links. Playwright E2E tests verified 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`. |
---
## 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 | Live payment E2E | 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:** yes, tested. Ads: one real adapter, rest stubs.
- **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.

View file

@ -0,0 +1,105 @@
# 12 — Map Update Protocol (Mandatory)
> This is not optional. If a change alters what the product can do, which
> infrastructure exists, or how developed something is, the map must be updated
> **in the same unit of work**. A feature is not "done" until the map reflects it.
>
> The agent obligation is stated in [`../../AGENTS.md`](../../AGENTS.md).
---
## 1. The rule in one line
> **Feature change ⇒ map change, in the same commit / PR.**
No feature work is complete if `docs/map/10-feature-catalog.md` (and, when relevant,
`docs/map/11-gap-backlog.md`) is stale.
---
## 2. When to trigger an update
Trigger on any of these events:
| Event | Must update |
|---|---|
| **New feature** added on any surface | Feature catalog row (`10`), surface doc (`04``09`) if infra changed, remove matching backlog row (`11`) |
| **Feature changed** (behavior, platform coverage, tier, provider) | Feature catalog status/notes, surface doc if interfaces changed |
| **Feature deleted / de-scoped** | Flip catalog row to reflect removal (delete row or mark `[-]` with reason), surface doc cleanup, note in `11` §4 if documentation moved |
| **Feature deferred to backlog** | Add row to `11-gap-backlog.md` (§13) and mark catalog `[ ]` |
| **Backlog item resolved** | Flip `11` row to `[x]` with date + evidence, update catalog to `[x]`, prune after one cycle |
| **External blocker cleared** | Flip `EXT-*` to `[x]` with evidence, update the affected catalog rows |
| **New app / package / service / Edge Function / CI workflow** | `02-infrastructure.md` and the relevant surface doc (`04``09`) |
| **Version bump** (`release/product-version.json`) | `00-index.md` header "Last full audit", `02` §9 |
| **Surface architecture change** (new IPC group, new route, new provider) | Surface doc + `03-shared-packages.md` if shared |
---
## 3. The update checklist
Run this before declaring any feature task complete:
1. **Locate the feature** in `10-feature-catalog.md`. If it has no ID, add a row with a new ID in the right domain (`CAP/AI/MEM/MTG/CV/ACC/TEAM/MON/SHELL/INFRA`).
2. **Set status per surface** (`D/W/M/B`) using the legend (`00-index.md` §2). Status is per platform, not global.
3. **Add file anchors** in the row notes (short `path:line` where useful) so the next agent can verify.
4. **Update the surface doc** (`04``09`) if you added/removed a service, route, screen, IPC channel, table, or function.
5. **Update `02-infrastructure.md`** if you touched build, CI, Docker, deploy, scripts, or release identity.
6. **Update `11-gap-backlog.md`**:
- Closing an open gap → `[x]` + date + evidence path.
- Creating a newly deferred item → new row with ID, gap, evidence, next step.
- Clearing external blocker → `[x]` + evidence.
7. **Reconcile with the mobile SSOT** if the change touches mobile: `docs/v3/MOBILE_APP_COMPLETION_SSOT.md` remains authoritative for mobile checklist rows. If the SSOT and this map disagree, fix the SSOT first, then this map.
8. **Bump the header**: update `00-index.md` "Last full audit" date only for a full re-audit; for incremental changes update the per-file `> Last updated` line if you add one.
9. **Commit the docs with the code** (same commit or same PR), with a clear message.
---
## 4. Status semantics (do not abuse)
| Status | Requires |
|---|---|
| `[x]` | Code exists **and** is verified (tests/evidence in repo or an evidenced manual run recorded in the map/SSOT). |
| `[~]` | Code exists but is partial, unverified, or blocked. Say *what* is missing in the notes. |
| `[ ]` | Planned or absent. If planned, ensure a `11` backlog row exists. |
| `[!]` | Code is done but "done" is blocked by something outside the repo. Name the external gate. |
| `[-]` | Genuinely N/A for that surface. Add a one-line reason. |
Never mark `[x]` to reflect "typecheck passed" alone. Typecheck ≠ runtime ≠ verified feature.
---
## 5. Ownership & cadence
- **Every feature task** updates its own rows (no batching).
- **Every refactor wave / phase close** triggers a reconcile pass over `10` and `11`.
- **Weekly or per-release** do a light sweep: scan for rows whose status no longer matches code; fix drift.
- **Per release** (`release/product-version.json` bump) do a full re-audit of `02`, `09`, and the header dates.
---
## 6. Anti-drift rules
- Do not create a second feature list elsewhere. This catalog is the SSOT for "what exists and how done".
- Do not describe status in prose only. Use the tables and the legend.
- Do not leave a `[~]`/`[!]` row without a note explaining the remaining work.
- Do not add a feature to the product without adding it here.
- If you genuinely cannot determine status, mark `[~]` and add a backlog row "verify status of X" rather than guessing or leaving it blank.
---
## 7. Quick templates
**New feature row (catalog):**
```
| CAP-16 | Feature name | [x] | [ ] | [~] | [x] | short anchors; what's partial |
```
**New gap row (backlog §1):**
```
| GAP-<AREA>-NN | Area | What's missing | file anchors | Suggested next step |
```
**New external blocker (backlog §3):**
```
| EXT-<SYSTEM>-NN | Blocker | What is needed | where tracked |
```

77
docs/tdd-red/00-README.md Normal file
View file

@ -0,0 +1,77 @@
# TDD-RED 프레임워크 · 2026
**"RED" 를 원론·증거·의견 기준으로 총체적으로 파악하고, 의미 있는 RED와 의미 없는 RED를 분리하고,
없는 것(가짜/의례적 RED)을 삭제하고, 흩어진 테크닉을 통폐합·구조화하고, 긴 시나리오로 재구성한 실행 프레임워크.**
> 작성 기준: Sep 2026. 2026년 전반~9월 자료(핵심은 2026-08 사전인쇄·Spec Kit v1.0.0) + 원론(Beck/Fowler/DHH 등) + 2026 생성형 AI 에이전트 시대 자료.
> 리서치 물량: **약 94개 외부 출처 정보 항목**(정확히 100개 번호, 일부 종합 링크 포함) / 14개 원문 1차 소스는 Playwright(headless → headful 폴백)로 직접 스크랩.
> **레드팀 검증 반영**: 5방면(원론·수치·실용성·일관성·2026최신성)을 거쳐 v1.1로 개정 — 보고서는 `scratch/tdd-red/redteam/01~05*.md`.
---
## 문서 인덱스
| 문서 | 내용 | 한 줄 요약 |
|---|---|---|
| `01-principles-and-sources.md` | RED의 원론·이론·논문·주요 인물 의견 전부 파악 | "RED는 무엇인가"의 땅다지기 |
| `02-meaningful-vs-meaningless-red.md` | **의미 있는 RED vs 의미 없는 RED** 분류표 | 핵심 산출물 1 |
| `03-cleanup-and-consolidation.md` | 없는/가짜 RED **삭제**, 테크닉 **통폐합**·구조화 | 핵심 산출물 2 |
| `04-long-scenarios.md` | RED를 **긴 시나리오**(행위 사양)로 재구성 | 핵심 산출물 3 |
| `05-framework.md` | 실행 프레임워크: 제어 루프·AGENTS.md·체크리스트·게이트 | 핵심 산출물 4 |
| `SOURCES.md` | 100개 정보 항목(약 94 외부) + 1차 소스 14개 | 증거 자료실 |
---
## 핵심 요약 (4줄)
1. **RED는 "실패하는 테스트를 먼저 쓰는 것"이 아니라, "한 가지 관찰 가능한 행위를 사양으로 명세하고, 그 행위가 없어서 **예상된 이유로** 실패함을 증명하는 것"**이다 (회귀=결함 재현, 특성화=레거시 전처리는 명시적 예외).
2. **의미 있는 RED** = 행위(behavior)를 하나만, 실제 결함을 잡을 수 있는 단정(assertion)으로, 구현이 아니라 인터페이스/결과를 검증하는 것. **의미 없는 RED** = 단정 없는 가짜 테스트, 구현 세부(내부) 테스트, 전부 목(mock)한 인공 시스템, 커버리지 채우기, 의례적 RED.
3. **2계층 실행**: 내부 루프(초 단위: RED→GREEN→REFACTOR) + 외부 게이트(GATE=VERIFY+REVIEW+DONE). 레드팀 검증으로 8단계 단일 목록을 폐기하고 단계 순서 모순을 해소했다.
4. **2026 에이전트 시대**: RED를 "에이전트에게 테스트-퍼스트를 프롬프트로 말하는 것"이 아니라 "**인간이 결정한 수용 기준을 보호 경로로 강제하는 완료 게이트**"로 격상한다. '숨김(held-out)'은 이 환경에서 실행 불가하여 '**보호(protected)**'(고치지 못하게)로 다운그레이드했다.
---
## 근본 정의 (Canon RED, Beck 2023 재확인 + 2026 + 레드팀 정정)
- 테스트 목록(행위 시나리오)을 먼저 **전부** 나열하라.
- 목록에서 **딱 하나**를 구체적이고 실행 가능한 테스트로 만들고, **실패를 확인하라**(실패 관찰 명문화는 Beck 『TDD By Example』·2026 가이드 결합 — Canon 본문은 실패를 전제만 한다).
- 그 테스트(+이전 모든 테스트)가 통과하도록 코드를 바꾸되, **가장 단순한 변경**으로.
- 선택적으로 리팩터(구현 설계 개선) — 행위는 바꾸지 않는다.
- 목록이 비울 때까지 반복 → **공포가 지루함으로 바뀔 때까지**.
- *Beck, "Canon TDD", Dec 2023 / 스크랩 `kentbeck_canon_tdd.txt`* (참고: Canon 본문은 2단계가 '하나를 구체 테스트로'까지이고 실패는 전제만 됨)
---
## 2026 자료 프레이밍 — 정정 (레드팀 실측 반영)
원래 "최근 4개월(2026-05~09)"로 과장됐으나, **실측상 4개월 창 안에 드는 외부 항목은 약 8개(8%)**에 불과하다.
다수(Google/TotT 03-10, Meta 02-11, Alderson 01-25, Drew Cain 04-09)는 2026년 1~4월 자료다. 아래 표는 실제 시기를 그대로 둔다.
| 항목 | 출처(년) | 핵심 | 증거 등급 |
|---|---|---|---|
| VS Code + Copilot 전용 **Red/Green/Refactor 에이전트** 가이드 | 2026(Living) | IDE가 TDD 3상을 에이전트 핸드오프로 공식화 | 공식 문서 |
| **TDD-Agent** (테스트 먼저 + 실행 피드백 반복) | 2026-08-17 | 저장소 수준 정답률·커버리지·뮤테이션 개선 보고 | **사전인쇄(비피어리뷰)** |
| **Spec-Driven Development(SDD)** 등장 | 2025~2026 | Thoughtworks Radar 2025 · GitHub Spec Kit · Amazon Kiro · Red Hat | **2차 인용·벤더 수치** |
| **Google "The Way of TDD"** (TotT) | 2026-03-10 | TDD가 은탄환이 아니라는 공식 인정 | 공식 블로그 |
| **Meta "Death of Traditional Testing / JiTTests"** | 2026-02-11 | 전통 테스트 유지보수 붕괴 → 즉시생성 테스트 — **RED 승격과 반대 방향(분기)** | 공식 블로그 |
| **TDD-Agent / TDAD / Spec-driven test gen** 사전인쇄 | 2026-08 | 테스트 품질·독립성·실행 피드백이 양보다 중요 | **사전인쇄** (TDAD 실재 제출은 2026-03) |
| Alderson "Turns out I was wrong about TDD" | 2026-01-25 | 에이전트가 TDD 경제성을 뒤집음 (단 test-first 순서는 명시 안 함) | n=1 실무 후기 |
| Emily Bache × Nizar "TDD Guard / Probity" | 2026-07-27 | 에이전트의 TDD 위반(과잉구현·테스트약화)을 **도구로 강제** | 인터뷰 |
| Drew Cain "TDD Is Out, SDD Is In" | 2026-04-09 | 테스트는 검증, 스펙은 의도 — "올바른 것을 만드는가" (저자·날짜 확인 필요) | 2차 인용 사례 |
| **합의(2026-09 시점)** | — | "RED를 행위 명세·독립 게이트로 승격"은 2026 신호 **일부 + 원론·실증의 결합** — 전부의 수렴은 아님(Meta JiTTests 등은 분기) | — |
---
## 디렉터리
```
docs/tdd-red/
├── 00-README.md ← 지금 이 파일
├── 01-principles-and-sources.md
├── 02-meaningful-vs-meaningless-red.md
├── 03-cleanup-and-consolidation.md
├── 04-long-scenarios.md
├── 05-framework.md
└── SOURCES.md (100개 정보 항목: 약 94 외부 출처)
└── 레드팀 검증 → `scratch/tdd-red/redteam/01~05*.md` (5방면)
```

View file

@ -0,0 +1,91 @@
# 01 · 원론·이론·논문·주요인물 — RED 전모 파악
"RED가 무엇인가"를 원론 정의 → 이론·실증 → 주요 인물 견해 → 2026 요약 순서로 정리한다.
(근거 출처는 `SOURCES.md`의 1~100번 참조. 괄호 안 숫자는 해당 항목 번호.)
---
## 1. RED의 원론 정의 (가장 좁고 정확한 정의)
**RED = "구현 전에, 한 가지 관찰 가능한 행위를 사양으로 명세한 자동화 테스트가, 그 행위의 부재로 인해 **예상된 이유로 실패**함을 증명하는 단계."**
(종합 정의다 — "예상된 이유로 실패" 표현은 Beck 『TDD By Example』과 2026 VS Code 가이드의 "fail for the right reason"를 결합한 것. Fowler는 실패 관찰을 직접 명문화하지 않았다 — 레드팀 H-1 정정.)
핵심 요소 (3):
- **사양**: 테스트 = 실행 가능한 요구사항 (실행 가능 사양, executable specification)
- **방법**: 실패를 **직접 관찰·검증** (실수로 통과가 아님)
- **결과**: 인터페이스 설계 결정 (Beck 8, 14) 및 회귀 누적
Beck의 Canon TDD (5): 테스트 목록 나열 → 하나만 구체 테스트→실패 → 최소 구현으로 초록 → (선택) 리팩터 → 목록 빌 때까지.
*"공포가 지루함으로 바뀔 때까지."*
RED는 "테스트를 먼저 쓰는 일(order)" 이상이다 — **행위 분석(1단계 목록)+실패 증명(2단계)**이 함께 정의된다 (9, 10).
---
## 2. 이론·실증 (무엇이 효과를 만드는가)
**프로세스 메커니즘 (이론)**:
- RED: 요구를 구현 전 실행 가능 예제로 → 요구 명확화·테스트 가능성
- GREEN: 작은 행위 증분 → 인지 부하↓·피드백↑
- REFACTOR: 중복 제거 + 회귀 안전망 → 유지보수성↑
- 반복: 빈번한 검증 → 조기 결함 검출
**핵심 실증 결론 — "test-first 순서 자체의 효과는 제한적"** (32, 33, 36, 37):
- 짧고 규칙적인 피드백 주기 + 테스트 강도 + 회귀 누적이 대부분의 이점을 설명.
- 메타분석: 외부품질에 작은 긍정 효과, 생산성 효과 미미 (34).
- 6개월 종단: 최종 품질 차이는 없어도 **더 많고 더 나은(결함검출력) 테스트**를 생산·유지 (37).
- **결론**: RED가 유효한 이유는 '순서'가 아니라 '행위 명세 + 빠른 실패 + 단정 강도 + 회귀'.
**TDD 위협 요인**:
- 리팩터 생략 (35; Fowler 21)
- 테스트 품질(단정·경계) 저하 — 개수·커버리지가 아님
- 과제 복잡성·경험·환경 (36)
**ROI 기준**: 투자 모델(손익분기)이며 절대 상수가 아님 (39, 40). ERICSSON 총비용 56%↓ (42, 단 수치 확인 필요), Microsoft+IBM 4팀 결함 4090%↓·시간 1535%↑ (41 — 실제 Microsoft 3팀+IBM 1팀).
---
## 3. 주요 인물 견해 지도
| 인물 | 입장 | 한 줄 |
|---|---|---|
| **Kent Beck** (창시자) | Canon TDD 정의 제공, 독단 반대, 맥락 의존 | "정의는 정의일 뿐. 행위 하나씩 공포→지루함으로." |
| **Martin Fowler** | RGR 정의, 자기검증≠TDD, 리팩터 중시 | "테스트를 먼저 쓰고, 깨지면 수정한다"(실패관찰은 Beck·2026 가이드 의 것 — 직접 인용 아님) |
| **David Heinemeier Hansson** | 독단·목-헤비·설계 왜곡 반대 | "TDD는 죽었다, 테스팅 만세" (2014) |
| **James Shore** | 프로그래머 몫 테스트 옹호, 수용은 대화 | "수용을 이진 테스트로 환원하면 거짓 확신." |
| **Marco Arment** | 소규모 제품 관점, 테스트 비용 사례별 정당화 | "과잉 테스트 경계" (2012) |
| **Vivek Haldar** | 장기 회귀·모듈성 이점 강조 | Arment에 반박 |
| **John Ousterhout** | TDD가 설계에 역행한다는 비판 | "작은 설계 증분은 best design을 막는다" |
| **Beck ↔ Ousterhout 논쟁** | 설계는 '언제'의 문제, 균형이 best | "일찍 설계할수록 덜 informed, 늦게 하라" (15) |
| **Emily Bache × Nizar** (2026) | 에이전트는 TDD 위반 → 도구 강제 | "TDD Guard/Probity" (7779) |
| **Martin Alderson** (2026) | 회의론자였다가 에이전트 경제로 전향 (단 test-first 순서는 명시 안 함) | "TDD 쪽이 맞았다, 로봇 주니어 덕분에"; 정작 '테스트를 먼저 쓰라고는 안 했다' (7376) |
| **Drew Cain** (2026) | TDD→SDD 전환 | "테스트는 검증, 스펙은 의도" (8084) |
| **Birgitta Boeckeler** (Thoughtworks) | SDD | "에이전트는 코드엔 강하고 장기 일관성엔 약하다"(Boeckeler 단독 발언, oleaedge 경유) |
| **Dave Farley** | ATDD | Bache 글에서 ATDD 관련 간접 언급 (SOURCES 근거 약함) |
| **Google(TotT 2026)** | TDD 옹호하되 비-은탄환 명시 | "The Way of TDD" — Bartosz Papis (72) |
**역사적 궤적**: 1994 SUnit·1995 OOPSLA 데모(Beck) → 2012 Arment 논쟁 → 2014 "Is TDD Dead"(DHH×Beck×Fowler) → 20172025 실증 다기화 → **20252026 SDD·에이전트 게이트 시대**.
---
## 4. 2026 (2026년 전반~9월) 합의 — 이전 관점과의 차이
| 차원 | 전통적 관점 | 2026 관점 |
|---|---|---|
| RED의 역할 | 프로그래머의 설계 훈련 | **행위 명세 + 독립 검증 + 완료 게이트** |
| 주체 | 인간 개발자 | 인간이 사양·리뷰, 에이전트가 구현·테스트 생성 |
| 순서의 강조 | test-first 강조 | **보호 수용 테스트 + 독립 검증 + 짧은 주기** (92, 8788) |
| 테스트 생산 | 수작업 | 에이전트가 초 단위 생성 (73) — 품질·독립성만 필수 |
| 테스트 유지비 | 상시 유지 | JiT 즉시 테스트로 유지비 제거 시도 (Meta 71) |
| 도구 | IDE·프레임워크 | Spec Kit(SDD·립 계) · VS Code 3-에이전트 · Probity 가드레일 |
| 실증 | 순서 효과 논쟁 | **품질·독립성·실행 피드백 > 수량** (6163, 8687) |
---
## 5. 하나의 종합 정의 (이 프레임워크의 RED)
> **"의미 있는 RED" = 긴 시나리오(행위 사양)에서 하나를 뽑아, 관찰 가능한 결과를 강한 단정으로 명세하고,
> 구현 없는 상태에서 **예상된 이유로 실패함을 증명**하여 (a) 사양을 명확히 하고 (b) 인터페이스를 설계하며 (c) 이후 구현·회귀를 **게이트**하는 단계.
> — 에이전트 시대에는 이 '게이트'가 인간이 결정한 수용 기준으로서 **구현 에이전트 밖**에서 강제된다.**
상세 산출물: `02`(분류) / `03`(정리·통합) / `04`(긴 시나리오) / `05`(프레임워크).

View file

@ -0,0 +1,76 @@
# 02 · 의미 있는 RED vs 의미 없는 RED
**목적**: 'RED'라는 이름 아래 흔히 섞여 있는 것들을 "행위를 정말 명세·검증하는가"라는 단일 기준으로 **의미 있음/의미 없음**으로 분리한다.
판정 원칙 한 줄: *"이 테스트가, 구현을 고치지 않았는데 통과할 수 있는가? 통과해도 아무것도 증명하지 않는가? 그렇다면 의미 없는 RED다."*
---
## 판정 축 (5 기준)
| # | 축 | 의미 있는 RED 는… | 의미 없는 RED 는… |
|---|---|---|---|
| 1 | **행위(behavior)** | 공개 인터페이스/관찰 가능한 결과 하나를 명세 | 내부 구현·프라이빗 메서드·호출 순서·데이터 구조 세부 |
| 2 | **단정(assertion)** | 실제 결함을 잡을 수 있는 강한 단정 | 단정 없음 / 상수 단정 / 커버리지만 채움 |
| 3 | **실패 이유** | "행위 부재 **또는 결함 재현**" 예상된 이유로 실패 | 컴파일·셋업·문법·무관한 이유로 실패(그래도 빨강) |
| 4 | **결정성** | 결정적·저비용·재현 가능, 시간/랜덤/네트워크 제어 | 비결정적, flaky, 외부 인프라 의존, 목으로 전부 인공화 |
| 5 | **의도(설계)** | 테스트를 쓰며 인터페이스 설계를 의도적으로 결정 | 테스트 목록 없이 즉흥, 구현 세부를 그대로 돌려보기 |
> 한 축이라도 "의미 없음"이면 그 RED는 빈 껍데기다. 특히 **(2) 단정**과 **(3) 실패 이유**가 핵심.
---
## 의미 있는 RED (KEEP) — 12가지 **패턴 카탈로그**
> **레드팀 정정**: 아래 12가지는 상호배타적인 '유형'이 아니라, **5축(판정 축)을 충족하는 사례의 패턴 카탈로그**다.
> 하나의 좋은 테스트는 여러 패턴을 동시에 충족한다(예: #1+#8+#10). "12 중 하나"라기보다 **5축을 모두 충족해야** 의미 있으며
> 12가지는 그 판정을 돕는 예시다. 특성화(#12)·회귀(#3)는 "행위 부재로 실패" 정의의 **명시적 예외**(레거시/결함)다.
| 패턴 | 설명 | 근거 |
|---|---|---|
| 1. **행위 우선 RED** | 한 가지 비즈니스 규칙/관찰 결과를 먼저 명세, 그 행위 없음으로 실패 | Fowler, Canon TDD |
| 2. **경계·엣지 RED** | 경계값(50원, 49.99원), 빈 입력, null, malformed, 오류 경로, 상태전이 | Microsoft, spec-driven |
| 3. **회귀 RED** | 결함을 재현(실패 이유=결함 재현) — fix 전에 실패 확인, 정의 예외 | Fowler, Alderson |
| 4. **인터페이스 설계 RED** | 테스트 작성 시점에 API/시그니처를 의도적으로 결정 | Beck Design-in-TDD |
| 5. **삼각측량 RED** | 예제를 하나씩 추가하며 일반화를 강제 (Fake→Triangulate→Obvious) | TDD By Example |
| 6. **결함-검출 잠재력 RED** | "구현이 미묘하게 틀려도 통과할까?"에 NO가 나오는 테스트 | Microsoft, 뮤테이션 |
| 7. **외부 강제 수용 RED (AI)** | 인간이 쓴 보호 수용 테스트로 에이전트의 완료를 게이트 | 2026 TDD-Agent, TDAD, TDFlow |
| 8. **블랙박스 RED** | 결과·상태변화·예외·외부 상호작용만 단정, 세부 구현 비의존 → 리팩터에 생존 | fowler_test_pyramid |
| 9. **Traceable RED** | 요구→수용 시나리오→테스트 ID→파일 추적 가능 | SDD traceability |
| 10. **설명력 있는 RED** | 이름이 `Withdraw_BalanceInsufficient_Throws` — 실패 시 무엇이 깨졌는지 즉시 | Microsoft, AAA |
| 11. **독립 검증 RED (AI)** | 구현 에이전트와 분리된 검증자/모델·신선 컨텍스트가 작성 | 2026 권장 루프 |
| 12. **특성화 (레거시 전처리)** | 기존 동작을 승인/캡처해 리팩터 안전망으로 — **RED 정의의 예외**: 즉시 통과(행위 없음이 아님) | Characterization/Approval |
---
## 의미 없는 RED (DELETE) — 10가지
| 유형 | 증상 | 왜 나쁜가 | 근거 |
|---|---|---|---|
| 1. **가짜 RED (fake)** | `expect(true).toBe(true)`, 단정 없음 | 커버리지만 높이고 결함 0 검출 | survey, Microsoft |
| 2. **사소한 RED (trivial)** | getter·언어기능·프레임워크 반복 테스트 | 결함-검출 가치 없음 | survey |
| 3. **구현-세부 RED** | 프라이빗 메서드·내부 호출순서·데이터 구조 단정 | 리팩터 때마다 깨져 재작성 → 리팩터를 막음 | fowler_test_pyramid |
| 4. **인공 시스템 RED** | 전부 목/스텁으로 인공화, 실제 통합 경계 미검증 | 실제 결함을 놓침, DHH의 'test-induced damage' | Is TDD Dead |
| 5. **복붙 기대값 RED** | 계산 결과를 그대로 기대값에 복붙 | 이중검증 파괴, 자신의 버그를 '정답'으로 고정 | Beck Canon TDD |
| 6. **커버리지-채움 RED** | 단정 삭제/축소로 위장 초록, %만 올리기 | 은탄환이 아닌 커버리지 | Beck, Google |
| 7. **의례적(all-at-once) RED** | (단위 계층) 테스트를 전부 먼저 쓰고 구현 — **수용·보호 계층의 사전 작성은 예외** | 첫 테스트가 후속 사양을 무효화하면 대량 낭비 | Beck Canon TDD |
| 8. **비결정 RED** | 시간·랜덤·네트워크·DB·셰어드 상태 의존 | flaky → 신뢰 상실, zero-tolerance 대상 | Azure WAF, Microsoft |
| 9. **비대(broad) RED** | 수많은 시나리오를 전 계층에 중복 반복 | 유지보수 폭증, 신호 희석 | Google "No more E2E" |
| 10. **에이전트 약화 RED** | 테스트 삭제·스킵·비활성·광범위 목 대체·참조 없는 과잉 구현 | 독립 검증 붕괴, 에이전트가 지름길 (주의: #10은 '삭제 대상 테스트'가 아니라 **금지 프로세스 행동** — 판정 워크플로로는 잡히지 않음) | Alderson, Emily Bache/Probity |
---
## 판정 워크플로 (이 테스트를 유지/삭제할까?)
```
1. 이름이 행위를 명세하는가? (아니오 → 수정 또는 삭제)
2. 단정이 결함을 잡을 수 있는가? (아니오 → 뮤테이션으로 확인 후 삭제)
3. 깨졌을 때 이유가 명확한가? (아니오 → 세부 구현 의존 의심)
4. 구현을 고치지 않고 통과할 수 있는가? (예 → 가짜/사소, 삭제)
5. 구현 세부(프라이빗/호출순서)를 검증? (예 → 블랙박스로 재작성)
6. 결정적이고 저비용인가? (아니오 → 하위 계층으로 이동)
7. 리팩터(행위 보존)를 견디는가? (아니오 → 행위 단정으로 재작성)
전부 YES → 유지 (의미 있는 RED)
```
> **뮤테이션은 '판정 보조 도구'** (05 §4 정책): 개별 테스트를 매번 뮤테이션으로 판정하면 비용 과다. 대신 리뷰에서 단정이 상수 교체·경계 반전을 잡는지 눈으로 검사하고, 정리(cleanup) 국면·금융/인가/안전 로직에서만 뮤테이션(변경 파일 증분)으로 확인한다.
> (레드팀 정정: 기존 "최종 판정기" 표현은 05와 모순 — 통일)

View file

@ -0,0 +1,90 @@
# 03 · 삭제(Cleanup)와 통폐합(Consolidation)·구조화
**목적**: 02에서 '의미 없는 RED'로 분류된 것을 **삭제/수정**하고, 흩어진 테크닉·용어·도구적 접근을 **하나의 구조**로 통폐합한다.
---
## 1단계 · 삭제 (없는 것/가짜 것을 제거)
"없는 것" = 행위가 없어서 **존재 이유가 없는** RED. 다음을 명시적으로 제거한다.
- [ ] [02-#1] 단정 없는 `expect(true)` 류 · 태어나서 한 번도 실제 결함을 잡은 적 없는 테스트 삭제
- [ ] [02-#3] 프라이빗 메서드/내부 구현 세부 테스트 → 제거 또는 블랙박스 행위 테스트로 재작성
- [ ] [02-#4] 전부 목으로 만든 인공 시스템 테스트 → 실제 경계 통합 테스트로 교체 (단, 순수 로직 계층에 한정 — 네이티브/경계 서비스는 예외)
- [ ] [02-#5] 복붙 기대값(자기검증 파괴) 테스트 → 손으로 쓴 기대값으로 교체
- [ ] [02-#6] 커버리지 %만 위한 테스트(단정 축소) 제거
- [ ] [02-#9] 전 계층에 중복된 동일 시나리오 테스트 → 최저 충분 계층 하나로 축소
- [ ] [02-#8] 비결정/flaky 테스트 → 격리(owner 지정)·수리·**수리 불가 시 삭제**
- [ ] [02-추가] 죽은 테스트(실행 안 됨·스킵·영구 스킵·대상 코드 소멸) 삭제 — 02 DELETE에 11번째로 승격
- [ ] [02-추가] AI 에이전트가 추가한, 이유를 설명할 수 없는 '검증용 더미' 테스트 삭제
- [ ] [02-#2/#5] 테스트 목록에 없는 즉흥·무의도 구현 세부 테스트 제거
> 삭제 후 **정리 국면**에서는 뮤테이션 점수 회귀 없음을 변경 파일 증분으로 확인(커버리지 말고). 삭제가 행위 보호를 깨면 유지한 채 재작성.
---
## 2단계 · 통폐합 (흩어진 것들을 하나의 축으로)
기존에 별개로 통용되던 개념들을 "출처(사양/행위)"라는 단일 축으로 묶는다.
### 2-1. 테스트 전략 통폐합 → 하나의 포트폴리오
| 통폐합 전 (산재) | 통폐합 후 (하나의 축: 검증 계층) |
|---|---|
| unit / integration / e2e / system | **테스트 피라미드**(많은 단위 + 적당한 통합 + 소수 E2E) |
| TDD / ATDD / BDD / SDD | **Spec → 수용 기준 → 테스트**의 단일 사양 흐름 |
| approval / golden master / snapshot | **승인 테스트**(대용량 출력 비교) — 용도만 다름 |
| characterization test | **특성화** = 기존 동작 보호용 승인 (RED 정의 예외) |
| example / property / contract / mutation | **검증 보강 세트**: 예(가독성)+프로퍼티(넓이)+계약(경계)+뮤테이션(강도) |
### 2-2. TDD 3상 → "RED가 중심인 단일 피드백 루프"로 통합
RED를 독립된 절차가 아니라 **루프의 첫 반복 게이트**로 재배치:
```
[Spec] → [RED: 행위 하나 명세+실패증명] → [GREEN: 최소구현] → [REFACTOR: 행위 불변 정리]
↑__________________ TRACE/회귀 누적 ___________________
```
- 여기서 "RED"는 02의 12가지 **패턴 카탈로그**(5축 충족)에 맞는 테스트만 허용한다.
- GREEN에서 단정·테스트를 고치지 않는다(two hats).
- REFACTOR는 행위를 바꾸지 않으며 항상 전체 스위트 초록 유지.
### 2-3. 빨강의 3가지 구현 전략 → 하나의 '진행 규칙'으로
`Fake it → Triangulate → Obvious`를 "**불확실함이 크면 작게, 확신이 있으면 일반 구현**"이라는 하나의 규칙으로 통합. (Beck: Obvious가 확실하면 바로 그것.)
### 2-4. 에이전트 시대 프레임워크와 인간 TDD 통합 → SHORT-SPEC + LONG-TRACE 루프
- **빠른 계층(short loop)**: 단위·통합 테스트 — 에이전트가 로컬에서 초 단위로 실행 (Alderson)
- **느린 계층(long gate)**: E2E·수용 테스트 — PR/CI에서 실행, **보호 경로**·독립 (Alderson, 2026 TDD-Agent)
- **사양 계층(spec)**: 수용 시나리오(긴 시나리오) — 올바른 것을 만드는가 (SDD)
---
## 3단계 · 구조화 (용어·책임·소유권 정리)
| 항목 | 확정 |
|---|---|
| "**의미 있는 RED**" = 행위·단정·예상된 실패·결정성·의도 5축 충족 (02) | KEEP |
| "**의미 없는 RED**" = 5축 위반(8종) + 별도 프로세스 금지(2종: 의례적·에이전트 약화) — 02-10가지 | DELETE/수정 |
| RED의 목적 = **사양 명세 + 독립 검증 + 완료 게이트** (2026 재정의) | 정의 |
| 테스트의 책임 주체 = **프로그래머** (고객/에이전트 부담 전가 금지) | Shore |
| 커버리지 = 진단, 뮤테이션 = 단정 강도, 시나리오 커버리지 = 행위 완성 (5052) | 매트릭스 |
| 에이전트 테스트 = 초안, 인간 리뷰 필수 (95) | 가드레일 |
| 테스트 이름 규칙 = `동작_시나리오_기대` (02: 설명력 패턴; SOURCES 46) | 표준 |
| 구조 = AAA / Given-When-Then (47) | 표준 |
---
## 산출물 요약
```
의미 있는 RED 12개 ← KEEP
의미 없는 RED 10개 ← DELETE/REWRITE
전략 5묶음 ← 통폐합 (피라미드/사양/승인/특성화/검증보강 — 특성화는 승인과 별도 행 유지)
TDD 3상 ← 단일 루프로 통합
빨강 3전략 ← 하나의 진행 규칙
빠른/느린/사양 ← 3-계층 구조화
8개 용어 ← 확정(구조화)
```

View file

@ -0,0 +1,99 @@
# 04 · 긴 시나리오(Long Scenario)로 RED 재구성
**목적**: 무수한 미시(단위) RED를 "사용자가 겪는 **긴 여정(장면)**" 단위로 재구성한다.
RED(테스트)는 단일 행위의 단일 테스트로 고정하되, 테스트를 **긴 시나리오(행위 사양)에서 파생**시킨다. 장면 단위를 "시나리오/행위 사양", 그에서 나온 테스트를 "미시 RED"로 용어 구분한다.
이로써 미시 RED 난립을 막고, 행위 완성도(traceability)와 에이전트 제어성을 동시에 얻는다.
---
## 왜 긴 시나리오인가
- **Beck(Canon TDD)의 1단계가 곧 긴 시나리오**: "며칠 동안 안 쓰던 기능까지 전부(행위 변형 목록) 나열하라." 마이크로부터 시작하면 완성도를 놓친다.
- **Alderson(2026)**: 티켓마다 "구현 전에 **테스트 계획**(통상 사용 + 엣지 + 커버 방식)"을 논의 — 이것이 곧 시나리오 명세.
- **SDD(2026)**: 수용 시나리오 → 테스트 매트릭스 → Test ID → 파일(추적).
- **테스트 피라미드(56)**: 행위는 **최저 충분 계층**에서 검증 — 같은 시나리오를 전 계층에 중복 금지.
- **Shore**: 수용은 "대화"가 본질 — 시나리오는 그 대화를 구조화한 산출물.
---
## 긴 시나리오 템플릿 (Gherkin 계열)
```gherkin
Feature: (사용자 가치를 하나의 문장으로)
Scenario: (한 장면의 제목)
Given <초기 상태 / 사전조건>
And <추가 상태>
When <행위/사건>
Then <관찰 가능한 결과 1>
And <관찰 가능한 결과 2>
And <부수 효과/경계/오류 투명성>
```
**좋은 예** (올바른 것을 만드는가 + 만드는 방법 모두 명세):
```gherkin
Feature: 계정 잠금
Scenario: 반복 실패 후 계정 잠금
Given 활성 계정
And 4회 연속 로그인 실패
When 잘못된 비밀번호 1개 더 제출
Then 인증은 실패해야 한다
And 계정은 15분 잠금
And 보안 이벤트가 기록된다
```
**원칙**: `Then`은 내부 구현이 아니라 **사용자/외부 시스템이 보는 결과**여야 한다(44).
=> `freeShippingFlag` 같은 내부 플래그 대신 `배송비 = 0`을 단정.
---
## 시나리오 → 테스트 매트릭스 (long → short 전개)
한 긴 시나리오를 **가장 낮은 충분 계층**의 테스트로 분해하되, 각 미시 RED에 추적 ID를 단다.
| Scenario (long) | 계층 | RED (short) | Test ID |
|---|---|---|---|
| 정상 주문 결제 | 단위 | 할인율 계산 정확 | T-101 |
| 정상 주문 결제 | 단위 | 배송비 경계(49.99/50/100, 음수 오류) | T-102 |
| 정상 주문 결제 | 통합 | 주문 저장→조회 왕복 | T-201 |
| 정상 주문 결제 | 계약 | 결제 게이트웨이 계약 | T-301 |
| 반복 실패 잠금 | 단위 | 시도 카운터·잠금 임계 | T-110 |
| 반복 실패 잠금 | 통합 | 보안 이벤트 기록 | T-210 |
| 정상 주문 결제 | E2E(PR) | 로그인→장바구니→결제→확인 (임계 여정만) | T-401 |
- **각 미시 RED는 02의 '의미 있는 RED' 12가지 중 하나여야** 한다.
- **같은 시나리오를 모든 계층에 중복하지 않는다**(57). E2E에서 결함 발견 시 **최저 계층**에 회귀 추가.
- **프로퍼티/계약/뮤테이션**을 필요한 곳에 보강(예: 파서·인코더·금융·인가).
---
## 긴 시나리오 × 에이전트 조율 (2026)
```
1. 스펙(스토리) 작성 ← 인간이 "올바른 것" 결정 (SDD)
2. 긴 수용 시나리오 명세 ← Given/When/Then, 인간 + 에이전트 논의(Alderson)
3. 보호(protected) 수용 테스트 ← 보호 경로에 배치·에이전트 수정 차단(92; 숨김은 이 환경에서 실행 불가 — 05 §2 다운그레이드)
4. RED: 에이전트가 시나리오별 의미 있는 단위 RED 생성
· "실제 이 브랜치가 발동하는 실례" 요구(76) — expect(1+1) 방지
5. GREEN: 최소 구현, 테스트 수정 금지(two hats, 11/100), 가장 단순한 변경(Probity, 78)
6. VERIFY: 풀 스위트+타입+린트+보안(+뮤테이션)
7. REFACTOR: 행위 불변
8. REVIEW: 인간이 **테스트 파일부터** 리뷰(74) → 지름길/삭제 탐지
9. DONE: 결정적 검사 통과 시에만 — 에이전트 "완료" 주장 불신 (63, 100)
```
이 긴 시나리오 단위가 곧 **하나의 리뷰 가능한 PR/커밋 크기**가 되도록 태스크를 나눈다. (참고: 한 발 단계의 PR 크기 근거로 쓸 만한 SOURCES 항목은 없음 — 운영 규약으로 결정)
---
## 긴 시나리오 체크리스트 (완성 게이트)
- [ ] 시나리오가 **사용자 가치**를 한 문장으로 담는다
- [ ] 정상 경로 + 경계 + 오류 + 상태전이 + 인가/보안이 매트릭스에 있다 (53, 89)
- [ ] `Then`이 관찰 가능한 결과만 단정 (44)
- [ ] 모든 행위가 추적 ID로 연결된다 (90)
- [ ] 각 RED가 '의미 있는 RED' 5축 충족 (02)
- [ ] 보호 수용 테스트가 보호 경로에 있고 에이전트가 수정 못 한다 (92, 05 §2)
- [ ] 최저 충분 계층에서만 검증, E2E는 임계 여정만 (57)
- [ ] 결함 발견 시 하위 계층 회귀 추가 (20)

View file

@ -0,0 +1,201 @@
# 05 · 실행 프레임워크 (Framework) — v1.1 (레드팀 반영)
**TDD-RED 프레임워크 v1.1** — Sep 2026 기준. 원론(Beck/Fowler)·증거·2026 에이전트 시대 합의 + **레드팀 검증(5방면) 반영**.
핵심 변화(v1.0 대비): ① 루프를 **2계층**(내부 루프 + 외부 게이트)으로 분리해 단계 순서 모순 해소, ② '숨김'→'보호'로 다운그레이드, ③ FREEZE를 루프 단계가 아닌 **상비 인프라**로 이동, ④ 뮤테이션을 내부 루프에서 제거·주기 게이트로, ⑤ 인용·수치 오류 전수 교정.
---
## 1. 제어 구조 — 2계층 (정본, 모든 문서의 기준)
```
[외부] SPEC ── 프로젝트별 사양(수용 기준/긴 시나리오) + 수용 테스트는 보호 경로로
[내부 루프 ×N] RED → GREEN → (REFACTOR) ← 초 단위, 에이전트 로컬, "가장 단순한 변경"
[외부] GATE ── VERIFY(기계 검사) + REVIEW(테스트 diff) + DONE(결정적 완료 판정)
```
- **내부 루프**는 Beck 원형 그대로 초 단위로 회복한다. 실패 증거 → 최소 구현 → 선택 리팩터.
- **외부 게이트**는 PR/CI 단위. 기계 검사·테스트 diff 리뷰·완료 판정을 한 게이트로 통합.
- 하나의 8단계 번호 목록을 폐기한다 — 단계 수가 문서마다 4~9로 갈라지던 원인.
### 세부 절차
| 단계 | 내용 | 수준 |
|---|---|---|
| **SPEC** | 요구를 관찰 가능한 수용 기준(긴 시나리오)으로. 이 프로젝트는 `docs/design/*` 을 정본으로 그로부터 Gherkin을 파생(충돌 시 design이 우선). 수용 테스트는 보호 경로에 1회 배치(상비 인프라, 매 루프 아님) | 외부 |
| **RED** | 행위 하나를 의미 있는 테스트로, 구현 전 **예상된 이유로 실패함을 증명**. 단위 계층은 실패 증거를 먼저 | 내부 |
| **GREEN** | 가장 단순한 변경. 실패한 테스트를 수정/약화/삭제/스킵하지 말 것 | 내부 |
| **REFACTOR** | 행위 불변 구조 정리(선택). 내부 루프 내에서 수행 | 내부 |
| **VERIFY** | 기계 검사: 영향받은 워크스페이스 스위트+타입체크+린트+보안. **풀 스위트는 CI 전담**. 증분 뮤테이션(변경 파일만, 주기/게이트) | 게이트 |
| **REVIEW** | **테스트 파일부터** 리뷰 — 약화·삭제·스킵·하드코딩·무관변경 탐지. 독립 모델/신선 컨텍스트 권장 | 게이트 |
| **DONE** | 결정적 검사 통과 시에만 완료 인정. 에이전트 "완료" 주장 불신 | 게이트 |
---
## 2. 보호(Protected) 수용 테스트 — '숨김'의 현실적 다운그레이드 (레드팀 공격 2·3 반영)
**문제**: "숨김(held-out) 수용 테스트를 에이전트 컨텍스트 밖에"는 솔로+에이전트 환경에서 **실행 불가**
같은 repo면 Grep으로 읽히고, 별도 repo는 유지비가 가치를 초과하며, 추적성(매트릭스 공개)과 상호모순.
**해법 — '숨김' → '보호': 목표를 "읽지 못하게"가 아니라 "**고치지 못하게**"로 재정의.**
구현(1회 설정, 상비 인프라):
1. `tests/acceptance/**` 에 대해 Claude/Codex 거버넌스 `deny` 규칙(Edit/Write 차단).
2. CODEOWNERS + 브랜치 보호로 해당 경로 변경 시 인간 승인 강제.
3. CI에서 "acceptance 경로 diff가 있고 스펙 커밋 태그가 없으면 실패" 검사.
4. 보호 경로 무결성 체크를 게이트에 포함.
범위 차등화: 임계 여정 3~5개(라이선스, 결제/정산 등)만 보호, 나머지는 일반 스위트.
추적성 규칙: 보호 테스트는 T-### 매트릭스에 **존재·ID만** 기록하고 Then 상세는 기록하지 않는다.
---
## 3. 의미 있는 RED 스펙 (단일 테스트 최소 기준)
| 항목 | 실제 값 |
|---|---|
| 이름 | `동작_시나리오_기대결과` (예: `Withdraw_BalanceInsufficient_Throws`) |
| 구조 | AAA 또는 Given-When-Then |
| 단정 | 결함-검출 가능, 관찰 결과만 (내부 X) |
| 계층 | 최저 충분 계층 (E2E는 임계 여정만) |
| 결정성 | 시간·랜덤·네트워크·DB 주입/제어, 저비용 |
| 실패 이유 | "행위 부재 **또는 결함 재현**" 하나만 (회귀·특성화 예외 포함) |
**판정**: 테스트 별개 유형이 아니라 **5축(행위·단정·실패이유·결정성·의도, +계층 경제성) 충족 여부**로 판정하며,
아래 12가지는 이 5축을 충족하는 **패턴 카탈로그**(상호배타 분류가 아님)다.
**의미 있는 RED 패턴 카탈로그 (KEEP 12)**: 행위 우선 · 경계·엣지 · 회귀(결함 재현) · 인터페이스 설계 · 삼각측량 ·
결함-검출 잠재력 · 외부 강제 수용 · 블랙박스 · Traceable · 설명력 · 독립 검증 · 특성화(레거시 전처리 예외).
**의미 없는 RED (DELETE)**: 가짜(단정 없음 `expect(true)`) · 사소 · 구현-세부 · 전부 목 인공화 · 복붙 기대값 ·
커버리지 채움 · 의례적(단위 계층 한정; 수용 계층 사전 작성은 예외) · 비결정/flaky · 전 계층 중복 ·
에이전트 약화/과잉 구현(+프로세스 금지 행동 2종 분리).
> 특성화 RED는 엄밀히 "RED 이전의 레거시 안전망(전처리)"이다 — 기존 동작 캡처로 **즉시 통과**하므로
> "행위 부재로 실패" 정의의 예외로 처리한다.
---
## 4. 뮤테이션 정책 (레드팀 공격 4 반영)
- **내부 루프에서 제거.** RED 1건마다 뮤테이션은 비용이 10~100배 과소평가(파일당 뮤턴트 30~200 × 수십 초).
- **주기 게이트로 이동**: PR 게이트에서 **변경된 파일만** 증분 뮤테이션(`--mutate` diff 스코프, incremental), 전체는 nightly.
- **적용 범위**: vitest/Jest 워크스페이스에 한정, .NET(Stryker.NET)·Deno는 도구 성숙도로 **명시 제외**.
- §8 지표 철학과 정합: 뮤테이션은 "측정 도구"지 "완료 게이트"가 아니다. §3 최소 기준의 뮤테이션 행 삭제 —
대신 리뷰에서 단정이 상수 교체·경계 반전을 잡는지 **눈으로 검사**하는 저비용 대체.
---
## 5. 긴 시나리오 → 테스트 매트릭스 (구현 주문)
```gherkin
Feature: 계정 잠금
Scenario: 반복 실패 후 잠금
Given 활성 계정 And 시도 4회 실패
When 잘못된 비밀번호 1개 더 제출
Then 인증 실패 And 15분 잠금 And 보안 이벤트 기록
```
| Scenario | 계층 | RED | ID |
|---|---|---|---|
| 반복 실패 잠금 | 단위 | 카운터·임계 | T-110 |
| 반복 실패 잠금 | 통합 | 보안 이벤트 기록 | T-210 |
| 정상 로그인 | E2E | 로그인 여정 | T-410 |
ID 규칙: 백의 자리=계층(1단위 2통합 3계약 4E2E), 십의 자리=시나리오. 정본 매트릭스는 `04-long-scenarios.md`
있으며(05는 발췌·확장), 같은 시나리오를 전 계층에 중복하지 않는다.
---
## 6. 2026 에이전트 보강
- **보호 수용 테스트**를 에이전트가 수정 못 하게 강제 (§2).
- **독립 검증**: 테스트 작성/리뷰는 구현과 다른 모델·신선 컨텍스트 권장(조용히 동시 작성은 금지 — 단위 RED 생성 후
GREEN 수행하는 표준 플로우는 실패 증거만 먼저 제시하면 허용).
- **추가 검증 보강**: 프로퍼티(넓이) · 계약(경계, Pact S-96) · 뮤테이션(단정 강도) 필요 시.
- **TDD 위반 도구 강제** (Probity/TDD Guard류): 스킵·과잉구현·테스트 약화 자동 감지.
- **에이전트 생성 테스트 = 초안**: 인간 리뷰 필수.
> 주의: 2026 신호는 방향이 **수렴만**이 아니다. Meta JiTTests(항목 71)는 "전통 테스트의 죽음 → 유지보수 없는
> 즉시생성 테스트"로 **반대 방향**이다. 이 프레임워크는 그중에서도 "인간이 결정한 수용 기준을 게이트로"라는
> 견해를 선택한 것임을 명시한다(증거 등급: TDD-Agent/+9.8pp/TDFlow 등은 2026-08 사전인쇄·n=1·2차 인용 — 피어리뷰
> 연구와 별개 등급으로 취급).
---
## 7. TRIAGE — 모든 변경이 RED 대상인가? (선별 규칙, 계약 0단계)
"모든 행위 변경에 강제"라는 무조건 문구를 폐기하고, 아래 선별 표를 AGENTS.md 첫 항목으로 둔다.
| 상황 | 방법 |
|---|---|
| 행위 명확·구현 어려움 | TDD-RED (내부 루프) |
| 프로덕션 버그 | 실패하는 회귀 RED 먼저 |
| 안정 도메인 규칙 | RED/예제 우선 |
| 설계·기술 미지 | 스파이크 후 정리하고 테스트 |
| 시각 UI 탐색 | test-last(컴포넌트·접근성·E2E 강조) |
| **린트/타입으로 집행되는 정적 규칙 준수 변경** (console.log 제거, 토큰 교체 등) | RED 면제 — VERIFY(린트)만 통과 (D3RO 규칙) |
| AI가 코드·테스트 둘 다 생성 | 독립 테스트 + 리뷰 + 뮤테이션/프로퍼티 |
| 레거시 대규모 변경 | 특성화(characterization) 우선 |
| 네이티브/프로세스 경계 서비스 (Electron 메인, STT/TTS) | 어댑터 계약 테스트(경계만 목) + 스모크 — "전부 목 금지"는 순수 로직 계층에만 적용 |
| UI 텍스트 단정 | t-key 수준에서 (i18n SSOT 하의 '관찰 가능한 결과' 대리자) |
| 싱글톤/EventEmitter 서비스 | Determinism 위해 설계서 차원의 `resetForTest()` 훅 추가 |
---
## 8. AGENTS.md 정책 (저장소 계약 — 2계층 + 게이트 반영)
```markdown
# TDD-RED 정책 (v1.1)
## 원칙
- 테스트는 행위(behavior)를 검증한다. 구현 세부·프라이빗·호출순서·데이터 구조는 금지.
- RED는 "테스트가 이미 구현된 상태에서 통과하거나, 예상된 이유 외로 실패해도" 무효.
- 같은 턴에서 테스트와 구현을 "조용히 동시에" 쓰는 것을 금지 — 반드시 실패 증거를 먼저 제시.
- 특성화(레거시)·회귀(결함)는 RED 정의의 명시적 예외.
## TRIAGE (0단계): §7 선별 표에 따라 이 변경이 RED 대상인지 판단.
- 린트/타입으로 집행되는 규칙 준수 변경·시각 UI 탐색 등은 RED 면제(사유 기록).
## 프로세스
1. SPEC : 요구를 Given/When/Then 수용 시나리오로. docs/design이 정본이면 그로부터 파생.
2. RED : 행위 하나를 의미 있는 테스트로. 실행해 예상된 이유로 실패함을 출력으로 증명.
3. GREEN : 가장 단순한 변경. 실패한 테스트를 수정/약화/삭제/스킵하지 말 것.
4. REFACTOR: 행위 보존, 영향받은 스위트 초록 유지.
5. GATE :
a. VERIFY: 영향받은 워크스페이스 스위트+타입체크+린트+보안 (풀 스위트는 CI 전담). 증분 뮤테이션(변경 파일만, 게이트).
b. 보호 경로(`tests/acceptance/**`) 무결성 — 에이전트는 해당 경로를 수정·삭제 금지.
c. REVIEW: 테스트 파일부터 diff 리뷰(약화·삭제·스킵·하드코딩·무관변경 확인).
d. DONE : 결정적 검사 통과 시에만. "완료" 주장은 검사 없이는 인정 안 됨.
6. REPORT: 변경 파일 / 실행 명령 / 구현 전 실패 증거 / 최종 결과 / 미해결 위험·가정.
## 금지(가드레일)
- 기존 테스트 삭제·스킵·비활성·광범위 목 대체. 보호 경로 수정.
- 참조되지 않는(과잉) 구현 추가. `expect(true)` 식 단정, 복붙 기대값.
- "완료" 주장 — 결정적 검사 통과만 인정.
```
---
## 9. 성공 기준 (ROI / 품질)
실증 코어(34, 36, 37, 42 계열): TDD는 "품질 향상은 확률적, 생산성은 맥락 의존". **절대 수치가 아닌 내부 파일럿**으로 ROI 판단.
- 결함-검출 능력(뮤테이션 점수) · 회귀 미검출 · flaky율 · 리드타임 · 리뷰 시간 단축을 측정.
- 커버리지% 가 아니라 "이 테스트가 뮤턴트를 죽이는가"를 **측정 지표**로(게이트는 아님).
- 특정 벤더·개인 수치(Kiro, Red Hat, n=1)는 2차·마케팅 증거로 무게 차등.
---
## 부록 · 즉시 적용용 미니 프롬프트
```
지시: 작업을 2계층 TDD로 수행. (RED 대상이면)
1. 저장소 컨벤션·테스트 명령 확인.
2. 요구를 observable 수용 기준으로 재진술 (docs/design이 정본이면 그로부터 파생).
3. RED: 테스트 하나 추가하고 실행 → 예상된 이유로 실패함을 보여라.
4. GREEN: 가장 단순한 최소 변경. 테스트를 수정/약화/삭제하지 말 것.
5. REFACTOR: 행위 불변.
6. GATE: 영향받은 스위트+타입+린트 실행 → 보호 경로(tests/acceptance) 미접촉 확인 → 테스트 diff 리뷰 → 결정적 완료.
7. 보고: 변경 파일/명령/구현 전 실패 증거/최종 결과/위험·가정.
```

View file

@ -0,0 +1,67 @@
# 레드팀 검증 · 종합 보고 & 조치 로그
- 검증일: 2026-09-02
- 방식: Orca 멀티에이전트 감독 오케스트레이션 — 5개 독립 claude 워커를 같은 워크트리에 디스패치
- 워커 산출물: `scratch/tdd-red/redteam/01~05*.md`
- 본 문서는 5개 리포트를 종합하고, v1.1 문서에 **반영한 사항 / 보류한 사항 / 후속 조치**를 기록한다.
---
## 1차 산출 (레드팀 워커 리포트)
| # | 리포트 | 파일 | 핵심 판정 |
|---|---|---|---|
| 01 | 원론·논문 정확성 | `01-theory.md` | HIGH 4 / MED 10 / LOW 8. 논지는 옳으나 원론 인용 위생 약함 (Fowler 오귀속 2건, 날조성 인용 2건) |
| 02 | 수치·인용 정밀성 | `02-numbers.md` | HIGH 2 / MED 8 / LOW 5. 저자 오귀속(Madeyski), TDAD 날짜 오류 등 |
| 03 | 실용성·실행성 | `03-practicality.md` | **채택 불가 판정**. 8단계 순서 불일치, 숨김/FREEZE 실행 불가, 뮤테이션 비용 과소평가 |
| 04 | 내부 일관성 | `04-consistency.md` | HIGH 5 / MED 14 / LOW 12. 제어 루프 순서가 문서마다 4~9단계, 분류 체계 자기모순, 인용번호 8+건 |
| 05 | 2026 최신성·타당성 | `05-recency.md` | 출처 환각 0건. 단 "4개월 안" 과장(실측 8%), 증거 등급 평탄화 |
---
## 반영 완료 (v1.1 문서 수정)
### A. 구조적 (핵심)
1. **제어 루프 2계층으로 재정의** (`05` §1): 내부 루프(RED→GREEN→REFACTOR, 초 단위) + 외부 게이트(GATE=VERIFY+REVIEW+DONE). 단일 8단계 목록 폐기 → 문서 간 순서 모순 해소. `00` 핵심 요약·`00` 표·`04` 조율·`05` §2/부록 모두 이 구조 기준으로 정렬.
2. **'숨김(held-out)' → '보호(protected)'** (`05` §2, `04`, `02` #7): 읽기 차단은 이 환경 불가 → 쓰기 차단(deny 규칙+CODEOWNERS+CI 가드)으로. 임계 여정 3~5개만 보호.
3. **FREEZE는 단계가 아닌 상비 인프라로** (`05`, `00`): 루프에서 제거, SPEC 시 수용 테스트 보호 경로 배치로 종결.
4. **뮤테이션을 내부 루프에서 제거** (`05` §4, `02` 판정, `03` 1단계): PR 게이트에서 변경 파일 증분으로, vitest/Jest만. "최종 판정기"→"판정 보조 도구"로 통일.
5. **TRIAGE를 AGENTS.md 첫 단계로** (`05` §7): "모든 변경 강제" 폐기, §7 선별 표(UI/s파이크/린트 규칙 준수 등 RED 면제)를 계약 0단계로.
### B. 인용·사실 (원론+수치)
6. Fowler "실패 관찰" 오귀속 정정 → Beck·2026 가이드 종합임을 명시 (`SOURCES` 1~2, `01` §1).
7. "Practical Test Pyramid" 실제 저자 Ham Vocke·개념 Mike Cohn으로 정정 (`SOURCES` 44/47/56).
8. Madeyski → **Pančur & Ciglarič (2011)** (`SOURCES` 33).
9. "IBM 연구" → **Microsoft 3팀 + IBM 1팀** Nagappan et al. (`SOURCES` 41, `01`).
10. TDAD 날짜 → 2026-03-18 (기존 02-08은 2602.07900과 혼동) (`SOURCES` 62).
11. Beck "차를 탓" 직접 인용 → 취지만, 직접 인용 아님 (`SOURCES` 25).
12. Alderson "테스트-퍼스트가 맞았다" 각색 → "TDD 쪽이 맞았다, 정작 test-first는 명시 안 함" (`01` 표, `00` 표).
13. Thoughtworks "가장 중요한" → "one of the most important" (`SOURCES` 82).
14. Harness-IF는 TDD 전용 아님 → 지시 준수 일반으로 (`SOURCES` 63).
15. 항목 87 "≫" 근거 보강: TDFlow 94.3% vs 68.0% 추가 (`SOURCES` 87).
16. Ericsson 56% 수치 미확인 표시 (`SOURCES` 42).
17. George & Williams "24팀" → 24명(12쌍) (`SOURCES` 31).
### C. 문맥·표시
18. "최근 4개월(2026-05~09)" 과장 → "2026년 전반~9월"로 정정, 실측표 제공 (`00`, `01` §4).
19. "100+" → "100개(약 94 외부)" (`00`, `SOURCES`).
20. Google "총리" → "TotT" 오타 수정 (`01`).
21. "데베이트" → "논의" 오타 수정 (`04`).
22. 무출처 `(*)` 항목 52·54에 출처 부여 (`SOURCES`).
23. 04 인용번호 정정: 46→56, 74→76, 7→11/100, 87→63/100, 무근거 PR크기 인용 제거.
24. 05 §6 인용번호: (53,47,49)→(53,96,49), §8 (37,45,42)→실증 코어 재조정.
25. 1차 소스 표 URL 절단 복구 (`SOURCES`).
---
## 보류·후속 조치 (문서만으로는 해결 안 되는 것)
| 항목 | 이유 | 권장 |
|---|---|---|
| **D3RO VOICE 착륙 컨벤션** (i18n t-key 단정, resetForTest 훅, 네이티브 경계 계약 테스트, 5중 런타임, 싱글톤 SSOT) | 실용성 레드팀 공격 5 — 프로젝트 고유 결정 | 실제 채택 시 별도 `docs/tdd-red/d3ro-adoption.md` 또는 설계 문서 반영 |
| **기존 `tests/red/` 27개 파일 이행** | 마이그레이션 경로 미정 | 감사 → 수용급만 `tests/acceptance/` 보호, 나머지 일반 스위트로 |
| **뮤테이션 도구 설치·범위 확정** | Stryker 미설치, .NET/Deno 제외 | 채택 시 도입 여부 결정 |
| **oleaedge 저자·날짜, Kiro/Red Hat 원출처, Boeckeler Radar 원문** | 2차 인용, 원문 미확인 | 필요 시 원출처 대조 후 재인용 |
| **TDD-Agent/TDFlow 사전인쇄 등급** | 비피어리뷰 | 등급 라벨 유지, 피어리뷰 후 갱신 |
> **총평**: 레드팀은 핵심 논지(행위 명세·실패 증명·독립 게이트)와 2026 소스의 실재성을 확인했다(HIGH 대다수가 "원문/이 환경과의 정합성" 문제이지 방향 오류 아님). 수정으로 구조적 결함(순서 불일치·숨김 실행불가·뮤테이션 비용)과 인용 위생(오귀속·날짜·저자)을 해소했다. 초기 상태에서 "채택 불가"였던 프레임워크는 v1.1에서 이 저장소에 적용 가능한 형태로 개정됐다.

135
docs/tdd-red/SOURCES.md Normal file
View file

@ -0,0 +1,135 @@
# SOURCES.md — 100개 정보 항목 (약 94개 외부 출처 기반)
1차 소스 14개는 Playwright(headless → headful 폴백)로 직접 스크랩했고, 나머지는 2026-05~09 최신 및 원론 검색으로 수집.
각 항목은 "정보 항목(사실/주장/원칙)" 단위로 번호를 부여했다.
---
## A. 원론 · 이론 · 논문 (academic / canonical)
1. REDGREENREFACTOR: 실패 테스트를 먼저 쓰고 → 최소 구현으로 초록 → 리팩터(행위 불변)를 반복한다. *(Fowler)*
2. RED의 본질은 "테스트가 실패함을 **직접 관찰**하는 것" — 실수로 통과하는 게 아님을 검증. *(레드팀 정정: Fowler 직접 언급 아님 — Beck 『TDD By Example』 + 2026 가이드의 'fail for the right reason' 결합)*
3. Beck의 제1규칙: "자동화 테스트가 실패한 뒤에만 새 코드를 쓴다." *(TDD By Example)*
4. Beck의 제2규칙: "중복(duplication)을 제거한다." *(TDD By Example)*
5. Canon TDD 정의: (1) 행위 시나리오 목록 → (2) 목록에서 하나만 구체 테스트로 → (3) 그 테스트+이전 전부가 통과하도록 변경 → (4) 선택 리팩터 → (5) 목록 비울 때까지 반복. "공포가 지루함이 될 때까지." *(Beck, Canon TDD 2023)*
6. Canon TDD는 "이렇게 해야 함"이 아니라 정의(ref)다 — 다른 방식이 통하면 그걸 써라. *(Beck)*
7. TDD가 만들어야 할 결과: (a) 이전 동작 유지 (b) 새 동작 정상 (c) 다음 변경 준비 (d) 개발자·동료의 **정당한 확신**. *(Beck)*
8. 인터페이스 설계 vs 구현 설계 구분 — 테스트를 쓰며 주로 인터페이스 설계 결정. *(Beck)*
9. 테스트 목록 단계를 빼먹는 실수 — "TDD는 바로 코딩으로 들어간다"는 오해. 행위 분석이 먼저. *(Beck)*
10. "테스트를 전부 다 써놓고 하나씩 통과시키기"는 퇴행적 — 첫 테스트가 후속 테스트를 무효화하면 낭비. *(Beck)*
11. 실패하게 만들 때의 실수 3가지: 단정 삭제로 위장 통과, 계산값을 그대로 기대값에 복붙(이중검증 파괴), 초록 만들면서 리팩터 섞기(two hats 위반). *(Beck)*
12. 리팩터는 "이 세션에 필요한 만큼만", 추상화는 너무 일찍 하지 말 것 — "중복은 힌트지 명령이 아니다." *(Beck)*
13. 테스트 순서가 경험·결과에 영향을 준다 — "코드는 초기조건에 민감한가?"라는 열린 질문. *(Beck)*
14. TDD와 설계: 테스트 성공 후 "이 구현을 쉽게 했을 설계는 무엇인가"를 질문 → API 설계 결정 → 어색한 API는 지금 다듬는다. *(Beck, Design in TDD 2025)*
15. "Best possible design"보다 **균형(Balance)** — 설계는 '언제'의 문제, '하는가 말아야 하는가'가 아님. 일찍 설계할수록 덜 informed. *(Beck)*
16. TDD Prerequisites(Beck, 'TDD Prerequisites' 2026): 예측 가능한 IO, 중요 시나리오 예측 가능, 빠르고 집중된 테스트, 결정적·저비용 셋업일 때 효율적 — 탐색적/시뮬레이션/5분 변경은 다른 방식. *(스크랩 밖 — Beck 뉴스레터, 정확한 게시일 미확인)*
17. "Passing tests bore me" — 통과 테스트가 공허하면 구현 세부를 확인하는 것. *(Beck 요약)*
18. 자기검증 코드(self-testing code) ≠ TDD — 이후에 테스트를 써도 자기검증은 가능하지만 그것은 TDD가 아님. *(Fowler)*
19. TDD는 단순한 기법이 아니라 프로그래밍·설계 훈련 — 사양+구현+검증+지속 설계 개선의 결합. *(Fowler)*
20. 모든 결함에 대해 먼저 실패하는 회귀 테스트를 추가하라. *(Fowler)*
21. 리팩터 스킵은 흔한 TDD 실패 원인 — 통과 코드가 서서히 나빠질 수 있음. *(Fowler)*
22. DHH "TDD is dead" (2014-04-23): 반대한 것은 **독단(dogma)**, 테스트 자체가 아님. "TDD는 죽었다, 테스팅 만세." *(DHH)*
23. test-induced design damage: 격리 단위 테스트를 위해 과도한 DI·서비스 객체·인터페이스·어댑터·목 → 이해·변경 어려움. *(DHH, Fowler 전재)*
24. Mock-헤비 테스트가 인공 시스템을 테스트 — 실제 DB·통합 경계가 더 중요할 수 있음. *(DHH)*
25. Beck의 반박 취지: 설계 품질 문제는 TDD보다 설계 판단의 탓 — '차를 탓하지 마라'식 직접 인용은 원문에 없음(레드팀 정정). *(Is TDD Dead?)*
26. 최종 합의: 세 사람 모두 자동 회귀 테스트 지지, TDD는 맥락 의존 — "양쪽을 맹목적으로 따르지 마라." *(Is TDD Dead? 2014)*
27. Marco Arment (2012): 소규모 제품 개발자 관점 — 테스트 비용을 사례별로 정당화하라. *(Build & Analyze #107)*
28. Vivek Haldar 반박 (2012-12-12): 회귀 방지·모듈성·자신감의 장기 이점 과소평가. *(Testing Redux)*
29. James Shore: 수용(acceptance)은 모호하고 사회적이고 협상 가능 — 이진 실행 테스트로 환원하면 거짓 확신. *(2012)*
30. Shore: 수용은 대화여야, 테스트는 프로그래머 몫(TDD) — Cucumber 처럼 고객에게 부담 전가하는 건 실수. *(2012)*
31. George & Williams (2004): 프로그래머 24명(=12쌍) 실험 — TDD가 기능 테스트 ~18% 더 통과, 개발시간 ~16% 증가. *(IST 46(5))*
32. Erdogmus et al. (2005): test-first 참가자가 더 많은 테스트 작성, 테스트 수↑가 생산성↑와 상관 — **test-first 순서 자체보다 테스트 강도**일 수 있다. *(TSE 31(3))*
33. **Pančur & Ciglarič (2011)**: TDD vs 반복 test-last 통제비교 — 생산성·복잡도·수용성능·커버리지·뮤테이션 점수 유의차 없음 → 이점은 **짧은 주기**에서. *(IST 53(6)). 레드팀 저자 정정: 기존 'Madeyski' 표기는 오류 — Madeyski의 실험은 IST 52(2) 2010으로 별개*
34. Rafique & Mišić (2013): 27개 연구 메타분석 — 외부품질에 작은 긍정 효과, 생산성 효과는 미미. *(TSE 39(6))*
35. Fucci et al. (2017): 참가자들이 지시만큼 리팩터를 안 함 — 교재 TDD와 실제 TDD의 괴리. *(IST 89)*
36. Tosun et al. (2017): 단순 과제에선 TDD 생산성 우위, 복잡 brownfield에선 하락 — 과제 복잡성 조절변수. *(EMSE 22(6))*
37. Romano et al. (2021): 6개월 종단 — 외부품질·생산성 유의차 없음, 그러나 **더 많은·더 나은 결함검출 테스트**를 생산하고 6개월 유지. *(JSS 176)*
38. 2016 체계적 리뷰(27편): 내부품질 개선 76%, 외부품질 개선 88%, 생산성 하락 44%. *(IST)*
39. 2025 tertiary 분석: 8개 TDD 체계적 리뷰 중 모든 리뷰에 공통 포함된 1차 연구는 3%에 불과 → "결과는 선택에 따라 달라진다." *(IST 2025)*
40. Müller & Padberg ROI 모델: TDD는 투자이며 손익분기점은 생산성 불리 vs 결함제거 효율로 결정. *(KIT)*
41. Nagappan et al. (2008) 산업 사례: Microsoft 3팀 + IBM 1팀 — 사전 릴리스 결함밀도 4090% 감소, 초기 개발시간 1535% 증가. *(EMSE 13(3))*
42. Ericsson 소개 사례(Damm & Lundberg): 컴포넌트 테스트+TDD로 컴포넌트 결함비율 6070%→20% 미만(2개 제품·6개 프로젝트). 총비용 56%↓는 **전문 미확인(레드팀 M-7)** — 확인 전까지 보류. *(JSS 79, 2006)*
43. 커플링 효과(coupling effect): 단순 인공결함을 잡는 테스트는 복잡 결함도 자주 잡음 → 뮤테이션의 근거. *(TOSEM)*
44. The Practical Test Pyramid: 행위를 공개 인터페이스로 검증, 프라이빗/구현세부 금지, 리팩터 시 테스트 재작성 방지. *(레드팀 정정: 실제 저자는 Ham Vocke(Thoughtworks), 개념 원조는 Mike Cohn 'Succeeding with Agile' — martinfowler.com 게재일 뿐 Fowler 저작 아님)*
45. 테스트 품질 질문: "구현이 미묘하게 틀려도 이 테스트는 통과할까?" → 결함을 잡을 수 없으면 가치 낮음. *(Microsoft)*
46. 하나의 테스트에 하나의 실패 이유 — 이름·단정으로 무엇이 깨졌는지 즉시 파악. *(Microsoft)*
47. AAA(ArrangeActAssert) 구조 — 긴 Arrange=복잡설계, 긴 Act=여러 행위, 긴 Assert=부실 테스트 지표. *(Microsoft; Given-When-Then은 Fowler)*
48. 테스트 스멜 용어: 의미 없음(meaningless)/죽은(dead)/사소(trivial)/가짜(fake). *(survey)*
49. 뮤테이션 점수 = killed / (valid mutants) — 커버리지 대비 "정말 검증하는가"의 지표. *(PIT/Stryker)*
50. **테스트 강도(test strength)** = killed/(killed+survived) — 커버리지 제외한 단정 강도. *(PIT)*
51. Oracle gap: 커버리지↑뮤테이션↓ = 코드는 실행하지만 단정·입력이 약함. *(arXiv 2309.02395)*
52. 커버리지는 주사이고 뮤테이션은 단정 검증 — 절대 수치 강요 금지. *(*)
53. Property-based test vs example test: 예는 가독성·정확한 기대값, 프로퍼티는 넓은 입력공간 탐색+자동 최소축소. *(QuickCheck)*
54. 프로퍼티 예: round-trip, idempotence, ordering, conservation. 발견된 반례는 명시 회귀 테스트로. *(QuickCheck/PBT 종합)*
55. Characterization(기존 동작 고정) vs test-first approval(목표 동작 정의) — 승인 테스트는 두 용도 모두의 도구. *(ApprovalTests)*
56. Test pyramid: 많은 단위, 적당한 통합, 소수의 E2E(임계 여정만). *(Mike Cohn 개념 / Ham Vocke·Google)*
57. "더 이상 E2E는 그만" — E2E를 임계 여정에만, 결함 발견 시 하위 레벨 회귀 추가. *(Google TotT)*
58. Beck "테스트를 얼마나?" → "같은 확신에 도달하는 **최소한만**." 커버리지 % 목표 없음. *(StackOverflow)*
59. 테스트 리팩터: 테스트 코드도 프로덕션처럼 품질 관리, 공유 픽스처 과용 금지. *(Microsoft)*
60. 비결정성·플레이키 금지 — 시간·랜덤·네트워크·DB는 주입/제어. zero-tolerance for flaky. *(Microsoft/Azure WAF)*
## B. 2026 에이전트 시대 (2026년 전반~9월)
61. **TDD-Agent** (2026-08-17): 구현 전 실행가능 테스트 생성 → 테스트·코드를 함께 반복 정제 → 저장소 정답률·커버리지·뮤테이션 개선. *(arXiv 2608.16742)*
62. **TDAD** (실제 제출 2026-03-18): 프롬프트로 "TDD를 써라"만 주면 회귀 증가 경향; 의존성·테스트영향 컨텍스트(TDAD 적용 시 6.08%→1.82%)가 회귀 감소. *(arXiv 2603.17973). 레드팀 날짜 정정: 기존 '2026-02-08'은 항목 86(2602.07900)과 혼동*
63. **Harness-IF**: 실행 트레이스로 **지시(인스트럭션) 준수**를 평가 — 최종 통과만으론 절차(그중 TDD 포함) 준수 증명 불가. *(arXiv 2608.11727). 레드팀 범위 정정: TDD 전용 평가 논문이 아님*
64. **Spec-driven test generation** (2026-08): 전제/사후/불변/비정의 행위/오류경계 문서화 후 테스트 생성 → 버그 검출 +9.8pp, 브랜치 커버리지 +2.5pp. *(arXiv 2608.17177)*
65. **GitHub Spec Kit**: Spec→Plan→Tasks→Implement→Verify; v1.0.0 (2026-08-21); 테스트-우선 거버넌스 프리셋. *(GitHub)*
66. **OpenSpec**: AI 코딩 어시스턴트용 오픈소스 스펙 레이어. *(Fission-AI)*
67. **VS Code TDD 가이드**: TDD Red / TDD Green / TDD Refactor 3개 커스텀 에이전트 + 핸드오프 + 자동 테스트 실행. *(code.visualstudio.com, 2026)*
68. **OpenAI Harness engineering**: 아키텍처·코딩규칙을 긴 프롬프트보다 **린터/구조 테스트로 기계적 강제**가 더 신뢰성. *(openai.com)*
69. **Running Codex safely**: 샌드박스, 네트워크·자격증명 제한, 고위험 액션 승인, 명령·테스트 로그 보존. *(openai.com)*
70. **Anthropic evals**: 안정적·재현 가능 환경과 결정적 그레이더. *(anthropic.com)*
71. **Meta Catching JiTTests** (2026-02-11): 코드 도착→의도 추론→뮤턴트 생성→테스트 실행→신호 집중; 유지보수·리뷰 불필요, 실제 버그 시에만 인간 개입. *(engineering.fb.com)*
72. **Google "The Way of TDD"** (2026-03-10): red-green-refactor 옹호하되 "은탄환이 아니다" 명시. *(testing.googleblog.com)*
73. **Alderson** (2026-01-25): 에이전트가 테스트를 '초' 단위로 쓰니 경제성 역전 — 티켓마다 "테스트 계획을 구현 전에" 요구, 단위·통합 우선(목 인프라), E2E는 PR 시 CI. *(martinalderson.com)*
74. Alderson: 리뷰를 **구현 코드가 아닌 테스트 파일부터** — 에이전트가 테스트를 지우면 즉시 드러남. *(martinalderson.com)*
75. Alderson: 버그 수정 시 "왜 테스트가 못 잡았는지" 설명하게 하고 그 엣지용 테스트 추가. *(martinalderson.com)*
76. Alderson: `expect(1+1).toBe(2)` 식 단정을 막기 위해 "이 브랜치가 실제 언제 발동하는지 실례"를 요구. *(martinalderson.com)*
77. **Emily Bache x Nizar** (2026-07-27): 에이전트는 TDD 지시를 받아도 지름길(스킵·과잉구현·린트 비활성·체크 전 커밋)을 찾음 → **TDD Guard/Probity 도구로 강제**. *(coding-is-like-cooking.info)*
78. Probity: 참조 안 되는 메서드 추가(과잉구현)를 잡아 "테스트를 통과시키는 가장 단순한 변경"을 재요구. *(Emily Bache)*
79. "테스트 행위지, 구현 형태가 아니다" — 테스트가 리팩터를 견디고, 재작성 불필요, 자신감이 목적. *(Nizar/Emily Bache)*
80. **Drew Cain "TDD Is Out, SDD Is In"** (2026-04-09): 테스트는 검증, 스펙은 의도; 올바른 것을 만드는가. 스펙=구현가이드+테스트플랜+문서 3-in-1. *(oleaedge.com)*
81. SDD 수치: 8주에 56개 스펙, 181 커밋, 869 파일, 5.6만 줄; 29%는 spec-to-code 자동, 71%는 인간 검토·판단. *(oleaedge.com)*
82. Thoughtworks Radar 2025: spec-driven development를 **가장 중요한 실천 중 하나(one of)**로 선정(oleaedge 2차 전언). Birgitta Boeckeler 인용문도 oleaedge 경유 — Radar 원문 대조 필요. *(oleaedge.com)*
83. Amazon Kiro: spec-driven IDE, 작업 제품까지 58% 빠르고 프로덕션 버그 65% 감소 보도. *(oleaedge.com)*
84. Red Hat: spec-driven AI 코딩 95%+ 정확도 보도. *(oleaedge.com)*
85. Harness-bench (2026): 즉시·잘 정의된 과제에선 중장비 낭비, 긴 지평·회귀수정·모호 저장소에서 가치. *(github.com/tdrml/harness-bench)*
86. "Agent가 테스트를 많이 쓴다고 이슈 해결이 안 좋아짐" (2026-02-08, 6개 모델 SWE-bench Verified): 에이전트 테스트량은 결과에 유의한 영향 없음. *(arXiv 2602.07900)*
87. 핵심 결론: **인간 테스트+에이전트 구현****에이전트 테스트+에이전트 구현** (독립성 부재). *(TDFlow, arXiv 2510.23761/EACL 2026: 인간 작성 테스트 제공 시 SWE-bench Verified 94.3% vs 에이전트 생성 테스트 68.0%. 단 2602.07900 자체는 '테스트량 무효과'가 결론 — '≫'는 TDFlow+원리 종합)*
88. AI 에이전트용 권장 루프: SPEC→RED→FREEZE(숨김 수용 테스트)→GREEN→VERIFY(풀 스위트+타입+린트+정적/보안+뮤테이션)→REFACTOR→REVIEW(테스트 약화·삭제·하드코딩·스킵·무관 변경 검사)→DONE 게이트. *(종합)*
89. Jackson의 테스트-영향/의존성 컨텍스트: 스키마·계약·상태전이·경계·인가·동시성·멱등성 테스트 매트릭스를 사양에서 도출. *(spec-driven)*
90. Traceability: (Requirement→Acceptance scenario→Test ID→Test file→Result)로 유지. *(spec-driven)*
91. 커버리지 도구를 에이전트로 돌려 미검증 브랜치 개선 + 실례 요구 (Alderson식 단정 방지). *(Alderson)*
92. "숨겨진(held-out) 수용 테스트가 에이전트 컨텍스트 밖에" — 에이전트가 만든 테스트로 독립 검증 불가. *(종합)*
93. 에이전트가 기존 테스트를 약화·삭제·스킵·광범위 목으로 대체하는 것을 금지하는 가드레일. *(종합)*
94. 커버리지는 진단용, 시나리오 커버리지·뮤테이션과 결합. *(2026 자동 테스트 생성 산업 규모)*
95. 에이전트 생성 테스트는 초안(draft)이지 증거(evidence)가 아님 — GitHub 공식 가이드도 리뷰·누락 시나리오 추가 요구. *(docs.github.com)*
96. 계약 테스트(Pact), 프로퍼티 테스트는 결정적 오라클 — 자연어보다 강한. *(Pact)*
97. Google(테스트 인프라로 결함 예방) vs Meta(고신호 자동화+실험) 문화 비교; 둘 다 2026엔 AI 지원 테스트로 수렴. *(Google/Meta 블로그)*
98. 2026 팀 정책: 새 비즈니스 로직 단위 테스트 필수, PR마다 빠른 단위 스위트, 단위 스테이지엔 네트워크·영구 인프라 금지, 변경코드 커버리지 모니터하되 맹목 목표 금지, 플레이키 격리+소유자, 중요 경계 통합/계약, 금융·인가·안전 로직 뮤테이션. *(종합)*
99. 지속 TDD-RED 지시문 템플릿(AGENTS.md용) — 05-framework.md에 수록.
100. "에이전트가 같은 테스트와 구현을 **조용히 함께 쓰면 안 됨**" — 시각적 증거(먼저 실패) 요구, GREEN에서 테스트 수정 금지, CI가 최종 판결. *(VS Code/Codex/Qodo 종합)*
---
## 1차 소스 스크랩 목록 (Playwright, headless→headful)
| id | url | 크기 | 비고 |
|---|---|---|---|
| kentbeck_canon_tdd | newsletter.kentbeck.com/p/canon-tdd | 7.8KB | Canon TDD 정의 |
| kentbeck_design_in_tdd | newsletter.kentbeck.com/p/design-in-tdd | 5.2KB | TDD와 설계, Ousterhout 논쟁 |
| kentbeck_passing_tests_bore_me | kentbeck.com/summaries/passing-tests-bore-me/ | 1.0KB | 공허한 통과 |
| fowler_tdd | martinfowler.com/bliki/TestDrivenDevelopment.html | 2.4KB | RGR 정의 |
| fowler_is_tdd_dead | martinfowler.com/articles/is-tdd-dead/ | 2.6KB | DHH 대담 |
| dhh_tdd_is_dead | dhh.dk/2014/tdd-is-dead-long-live-testing.html | 5.2KB | DHH 원문 |
| fowler_test_pyramid | martinfowler.com/articles/practical-test-pyramid.html | 80KB | 테스트 피라미드 실무 |
| james_shore_acceptance_testing | jamesshore.com/v2/blog/2012/acceptance-testing-revisited | 2.1KB | 수용은 대화 |
| alderson_wrong_about_tdd | martinalderson.com/posts/turns-out-i-was-wrong-about-tdd/ | 10KB | 2026 에이전트 경제성 |
| google_way_of_tdd | testing.googleblog.com/2026/03/the-way-of-tdd.html | 2.3KB | 2026-03 공식 |
| oleaedge_sdd | oleaedge.com/blog/tdd-is-out-sdd-is-in | 7.3KB | SDD 사례·수치 |
| emily_bache_ai_tdd | coding-is-like-cooking.info/2026/07/the-most-important-ai-coding-advice-you-havent-heard-yet/ | 13KB | TDD Guard/Probity |
| vscode_tdd_guide | code.visualstudio.com/docs/agents/guides/test-driven-development-guide | 14KB | 3-에이전트 TDD |
| meta_jit_testing | engineering.fb.com/2026/02/11/developer-tools/the-death-of-traditional-testing-agentic-development-jit-testing-revival/ | 7.2KB | Catching JiTTests |
원문 파일: `scratch/tdd-red/raw/*.txt` (스크래퍼: `scratch/tdd-red/scrape.mjs`, `retry.mjs`)

View file

@ -99,8 +99,14 @@ Apple Developer Program 계정 필요. iOS 출시 전에 등록.
```bash
cd server/supabase
# 시크릿 설정 (배포 전)
supabase secrets set GOOGLE_CLOUD_STT_KEY=<google_api_key>
# 시크릿 설정 (배포 전) — functions/stt-proxy/index.ts가 실제로 읽는 변수.
# 아무것도 설정하지 않으면 엣지함수가 503 stt_provider_unavailable를 반환한다.
# (과거 문서의 GOOGLE_CLOUD_STT_KEY는 현 함수에서 사용하지 않는다)
supabase secrets set D3RO_API_URL=<api-server-origin>
supabase secrets set D3RO_API_TOKEN=<stt-gateway-token>
supabase secrets set GROQ_API_KEY=<groq_api_key> # 직접 Whisper 폴백
supabase secrets set OPENAI_API_KEY=<openai_api_key> # 선택
supabase secrets set DEEPGRAM_API_KEY=<deepgram_api_key> # 선택
supabase secrets set ANTHROPIC_API_KEY=<anthropic_api_key>
# 함수 배포

View file

@ -44,7 +44,7 @@
## 1. 정본 경계와 보존 규칙
- `apps/mobile-rn`: Android/iOS 제품 모바일 앱. 모든 신규 구현은 여기에 한다.
- `apps/mobile`: Expo 51 기반 과거 스켈레톤. 참고만 하며 제품 기능을 이쪽에 이중 구현하지 않는다.
- `apps/mobile`: Expo 51 기반 과거 스켈레톤. **2026-09-13 제거 완료** — 이중 구현 금지, 이력은 git에 보존.
- `apps/web`: 서버 공유 기능과 사용자 데이터 UX의 비교 기준.
- `apps/desktop`: 데스크톱 기능 목록과 로컬 전용 기능의 의미 기준.
- `packages/api-client`: Supabase 타입과 공용 API 호출의 정본. 모바일에서 임의 테이블 타입을 중복 작성하지 않는다.
@ -116,7 +116,7 @@
### G — 거버넌스·기반
- [x] G-001 제품 모바일 런타임을 `apps/mobile-rn`으로 확정하고 본 문서에 기록
- [ ] G-002 `apps/mobile` 참조 코드에서 재사용할 기능을 선별하고 이중 런타임 의존 제거
- [x] G-002 `apps/mobile` 참조 코드에서 재사용할 기능을 선별하고 이중 런타임 의존 제거 — 2026-09-13 `apps/mobile` 삭제, `sync-version.mjs`/`package-lock.json` 정리, `version:check` GREEN
- [x] G-003 모바일 환경 설정 스키마 작성: Supabase URL/anon key, API origin, OAuth redirect, AdMob app/unit IDs, Play product IDs
- [x] G-004 debug/test/prod 설정 분리와 시작 시 구성 진단 화면 구현
- [x] G-005 하드코딩된 release signing 자격 증명을 로컬/CI secret 주입으로 이동
@ -227,7 +227,7 @@
### F — 데스크톱/Web 전체 기능의 모바일 화면
- [ ] F-001 Dashboard 통계·사용량·처리 job·구독·동기화 상태를 실제 데이터로 표시
- [ ] F-002 Dictionary 목록/검색/추가/편집/삭제/import/export/sync
- [x] F-002 Dictionary 목록/검색/추가/편집/삭제/import/export/sync — CRUD/sync GREEN, export는 `data-portability` CSV/TXT, import는 CSV/JSON/TXT
- [ ] F-003 Custom instructions 목록/활성화/CRUD/reorder/sync
- [ ] F-004 Voice commands 목록/활성화/CRUD와 모바일 실행 가능한 action 구분
- [ ] F-005 Actions 실행, 권한 확인, 실행 결과·실패 기록
@ -247,7 +247,7 @@
- [ ] T-002 이메일/링크 초대, 딥링크 수락, 만료·중복·다른 계정 처리
- [ ] T-003 팀원 목록, admin/member/viewer 권한, 제거·나가기
- [ ] T-004 팀 회의·문서 공유와 RLS 교차 사용자 음성/문서 접근 차단
- [ ] T-005 팀 코멘트·활동 feed·알림
- [x] T-005 팀 코멘트·활동 feed — `team_activities` + `create_team_activity` RPC + Realtime, TeamDetail 활동 카드 (알림 연동은 T-010/011 범위)
- [x] T-006 일반 사용자의 본인 데이터 관리와 관리자 전용 기능을 분리 — ordinary UI에서 Admin 숨김·관리 API request 0, manager/admin/super 분기 API 34 E2E PASS
- [x] T-007 admin/manager role 사용자는 사용자·구독·결제 감사 정보를 모바일에서도 안전 조회 — manager read/subscription update, stale JWT 403와 target/modal purge API 34 E2E PASS
- [x] T-008 role 변경·구독 수정 등 파괴적 관리 동작은 재확인·감사 로그·서버 권한 검증 — admin role 범위·delete confirm/cancel, super manager→admin, RPC readback과 cleanup 0 E2E PASS