Running a custom instruction (translate, summarise, rewrite, explain code,
free prompt) inserted the instruction's own wording instead of the result.
Two faults stacked:
The instruction was passed as the text to process, leaving the system-prompt
argument empty. `BASE_SYSTEM_PROMPTS` has no `custom` key, so resolution fell
back to `refine` without saying so, and the model dutifully polished the
instruction it had been handed. The transcript never reached it.
And only `{{text}}` was substituted, which none of the five built-in
instructions use — they carry `{{targetLanguage}}`, `{{userPrompt}}`, or no
placeholder at all. The substitution was a no-op from the day it was written:
the presets landed ten hours before the code that expected them.
- Instruction prompts now go to the system-prompt argument and the transcript
to the text argument. Instructions that spell out `{{text}}` keep their old
meaning, so hand-written ones still work.
- `renderInstructionPrompt` resolves `{{text}}`, `{{userPrompt}}` and
`{{targetLanguage}}` in one place, and warns by name when a placeholder is
left standing rather than letting it reach the model.
- `resolveSystemPrompt` no longer drops silently to `refine` for `custom`.
- Voice shortcuts no longer die at the `defaultLLMAction === 'none'` gate; an
explicitly named instruction outranks the default. Without one, `none` still
passes the transcript through untouched.
- `translate` receives its target language instead of relying on a default two
call frames away. It is still always English — `AppConfig` has no key for it,
and neither `language` (UI locale) nor `sttLanguage` (source language) can
stand in. Choosing a target language needs a setting and is not in this fix.
- Chains ran instructions with placeholders intact; they share the same
resolution now.
- The command screen's pipeline bench called `llm.generate`, which preload does
not expose, so every run threw and the catch showed the input back as if it
had succeeded. It uses `llm.process` now, over the same path production
takes, and a failure reads as a failure.
Present since the feature shipped: the custom-instruction path has never
worked. Plain actions (refine, summarise, grammar, expand) were unaffected and
are now covered by tests so they stay that way.
229 lines
18 KiB
Markdown
229 lines
18 KiB
Markdown
# 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 check:desktop-renderer[:test] # built renderer pages reference only assets on disk
|
|
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), playwright e2e; local STT engine: `npm --prefix apps/desktop run sidecar:setup` then `sidecar:build` (PyInstaller → `sidecar-dist/sidecar`), full local Windows package: `npm --prefix apps/desktop run dist:win:full`; `setup:sox` re-downloads the bundled SoX |
|
|
| 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, key-binding SSOT (`./keybinding`), constants, `crypto-license`, `pii-redactor`, `secure-memory`, `supabase-config`, `meeting-markdown`, `markdown-to-docx`; has vitest tests (`packages/core/vitest.config.ts`, `npm run test --workspace=@d3ro/core`) |
|
|
| `@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**. `package-windows`/`package-macos` build the faster-whisper sidecar (`sidecar:setup` → `sidecar:build`) and run `scripts/ci/verify-sidecar-bundle.mjs` before electron-builder, so a release can never ship without the local STT engine. Every pipeline that runs `npm run build --workspace=@d3ro/desktop` (`.forgejo` release/portable, `.github` CI/release) then runs `scripts/ci/verify-desktop-renderer-bundles.mjs`, which fails packaging when a renderer page references an asset the build did not emit (GAP-INFRA-05).
|
|
|
|
### Forgejo Actions (`.forgejo/workflows/`)
|
|
`portable.yml` — 태그/수동 실행으로 **서명 없이** portable 채널(95MiB 7z 분할 볼륨 + Scoop 매니페스트 + 설치 스크립트)을 게시한다. `WIN_CSC_*` 불필요, updater feed는 건드리지 않는다.
|
|
|
|
`deploy-site.yml` / `deploy-site-windows.yml` — build `site`, write release identity, deploy to Cloudflare Pages `d3ro` (`d3ro.pages.dev`), verify live commit/version, app-links, legal URLs. 커스텀 도메인 `d3ro.chanpaca.net` 은 Pages 커스텀 도메인이 DNS CNAME을 요구하므로, DNS를 건드릴 수 없는 동안은 Workers 라우트 브리지 `server/cloudflare-site-bridge/`(`d3ro.chanpaca.net/*` → Pages 프록시, 수동 `npx wrangler deploy`)가 서빙한다. CNAME을 추가한 뒤 브리지를 삭제하면 Pages 커스텀 도메인으로 직접 서빙된다(GAP-REL-09b).
|
|
`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` — **랜딩/다운로드 센터**(2026-09-19부터 Pages `d3ro` 배포본을 Workers 라우트 브리지가 서빙; 그 이전에는 바인딩이 없어 빈 404였다), `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` (NAS 포털/API는 현재 이 호스트네임에 바인딩되어 있지 않다).
|
|
|
|
---
|
|
|
|
## 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.4.0`, `androidVersionCode`/`iosBuildNumber` `1041000`, releaseDate `2026-09-21`, 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 + `@ffmpeg-installer`, extraResources (icons, sounds, sox, **sidecar**, ffmpeg, 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/web/src/lib/desktop-release.ts`, `site/src/release.ts` | Download-center desktop release contract (installer filename + release date); version and date are kept on the SSOT by `npm run version:sync` (drifted to 1.2.0 once — GAP-REL-08) |
|
|
| `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/ffmpeg/` — optional bundled ffmpeg (CI or manual); `getFfmpegPath()` also resolves the `@ffmpeg-installer/ffmpeg` binary from `app.asar.unpacked`.
|
|
- `apps/desktop/sidecar-dist/` — PyInstaller sidecar bundle consumed by `extraResources` (gitignored; built by `npm --prefix apps/desktop run sidecar:build`).
|
|
- `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).
|