# R5 Capability-Maturity Audit — platform modules vs mature donors

> **RESOLVED 2026-06-16 (read before acting on any "UNBUILT" below):** the `realtime /server` (DO/Queue) prerequisite this audit flagged as "UNBUILT / load-bearing / promote from intentionally-unbuilt" (lines ~131/195/226) **was built per this audit's recommendation** — `@platform-modules/realtime` is now `shipped`, `/server` built + unit-proven (`registry.json`). The UNBUILT references below are the **as-of-2026-06-15 state that triggered the build**, preserved as the historical record — not a current claim. Live status: `registry.json`.

> Audience: AI coding agents first. Optimize for activation, not prose.
> Governs: which `@platform-modules/*` are **over-thinned** (shipped below their mature donor form) and the per-capability **rebuild backlog**. Doctrine: `docs/standards/coding-standard.md` §5A (R5) · `CLAUDE.md` §3 (Capability altitude).
> Status: IN PROGRESS (started 2026-06-15). Pilot pair (ai, i18n) **harvested + SHIPPED to main** (i18n `b74012a`, ai `4fd36d2` — clean-4 + supportsImage seam; ai deferred-6 specced/unbuilt by DAG); 11 cull modules pending donor census.
> Consumed by: the multideal migration (`/home/user/Projects/multideal/docs/platform_modules_report.md` Phase 0.5) — a module is not "ready to import" until it carries its donor's mature capability set.

## Method (the R5 decomposition, per capability)

1. **Module surface** (platform side — read directly, accurate, cheap): the shipped `exports` subpaths = the capability surface. Table below.
2. **Donor capability sweep** (fan-out — cursor census, read-only): what the *mature donor* has in this domain, with `file:line` + one-line "what it does". NO verdicts (census never decides).
3. **DELTA** = donor capability NOT in the module surface.
4. **R5 verdict per DELTA item** (main-loop only — never delegated). Stop at first rung:
   - GATE: mature donor has it (or cross-class forward-needed)? no → drop (YAGNI).
   - app-specific residue (this app's entities/rules/schema/wiring) → **HOST**
   - owned by / composable from an EXISTING module → **COMPOSE** (route, don't re-subpath)
   - generic, class-wide, stable contract → **IMPORTABLE** subpath
   - universal → **CORE**
5. **Maturity rule:** adopt the *most mature donor form* — cross-check non-multideal donors (zync.is, ForumZone, Press.zone, CommentPressZone, trance-shop-israel, fewtok) where the domain suggests a richer form elsewhere.

## Module shipped surface (platform side — captured 2026-06-15)

| Module | Shipped subpaths | peerDeps | Surface prior (hypothesis — confirm at census) |
|---|---|---|---|
| `util` | `.` `crypto` `tokens` `password` `datetime` `timezone` `fts/{sanitize-tsquery,safe-headline,rank}` | none | layered; donor (multideal `lib/`) has more leaves — `geo` `url` `validation` `observability` `enums` `deals` `image-upload`. Generic leaves (url/validation/geo?) → importable; app-domain (deals/enums) → host. |
| `db` | `.` `neon-http` `neon-serverless` `postgres-js` `pglite` | drizzle, neon, postgres, pglite | type contract; driver adapters mature. Likely already-correct (thin BY NATURE, not over-thinned). |
| `auth` | `.` `engine-custom/*` `engine-better-auth` `otp-email` | db, mail, util, better-auth, drizzle | engine-custom mature (password/refresh/session/schema). DELTA risk: csrf/guards — but those are HOST middleware (§8), confirm not a module gap. |
| `jobs` | `.` `cf-queues` `db-poll` `outbox` `do-runner` | db, workers-types, drizzle | mature runner surface. **This is the module `ai` must COMPOSE** (rung 2). Confirm it covers idempotent retry + status machine. |
| `mail` | `.` `resend` `ses` `postmark` `brevo` | resend, postmark, ses-sdk | provider adapters mature. DELTA risk: bulk-mailer / templating (donor multideal `server/email` has bulk-mailer). |
| `notifications` | `.` `email` `webpush` | db, mail | **THIN vs donor** — donor multideal = `notifications`+`push`+`workflows`. DELTA risk: in-app channel, workflow/digest engine, email→push fallback. R5 candidate. |
| `ledger` | `.` `vesting` `lifetime` `balance-after` | db, drizzle | rich (append/balance/debit/debit-with-read/vesting/lifetime/errors/types). Likely well-layered. Confirm `tx()`-context seam (known migration blocker). |
| `tax` | `rates-table` ONLY (no `.`) | db | thin BY DESIGN (PARTIAL — rates-table only); engine/vat-id/stripe forward-declared, 0 donors. Zero tests (known defect). |
| `uploads` | `magic-bytes` `image-dimensions` `size-limit` `quota` `presign-s3compat` `r2-binding` | aws4fetch | mature server seam. DELTA: client image-encoder (known — host WASM in multideal; decide subpath vs host). |
| `billing` | `.` `stripe` `sumit` | db, jobs, ledger, tax, drizzle, stripe | DELTA risk: webhook dedup, platform-fee routing, Connect payouts (last two likely host per affiliate tie). |
| `helpdesk` | `.` ONLY (case/messages/attachments/sla/transitions/transition-validator/schema) | db, drizzle | has `case`; **DELTA risk: `ticket` + `kb` (knowledge base)** — donor multideal has support_tickets + support_kb + ai-interventions; `ai`'s bucket-2 tools COMPOSE this, so the surface gap matters. |

## Verdicts

### PILOT — done first-hand (see multideal report R5 decomposition for the full table)
- **`ai`** — over-thinned. Harvest → module: `google`/Gemini adapter (broken promise), LLM job lifecycle (**composes `jobs`**), `JobKind` interface, tool-use agent harness + registry, usage→price layer (opt-in subpath; core `usage` stays token-only), PII redactor. Tools split 3 ways: **harness `ai/agent`** = tool-contract + run-loop + registry/dispatch + terminal-tool + max-iters + `AgentDeps` + result-envelope + autonomy-gate; **generic tool primitives → per-leaf `ai/agent/tools/<tool>`** (`ai/agent/tools/read-image`, `ai/agent/tools/search-kb`) — per-leaf NOT a single `tools` blob, because deps diverge (`read-image` → gateway + `util/ssrf-guard`; `search-kb` → composes `search`/`helpdesk-kb`), so grouping would defeat tree-shaking/swap-survival; ~9 helpdesk ops (`get-ticket`/`get-case`/`post-message`/`close-ticket`/`set-category`/`set-priority`/`list-attachments`/`request-evidence`/`escalate-to-human`) → **compose `helpdesk`** (NOT module subpaths); ~4-5 app-domain (`issue-refund`/`get-transaction`/`get-redemption-log`/`get-user-history`/`get-vendor-history`) → host. Kinds: `translation` → importable; `deal-moderation`/`vendor-violation`/`image-approval` rules → host (all via importable `JobKind`).
  - **BUILD SCOPE (disposition ≠ buildability):** this bullet is the *durable design surface*; the **DAG gates which subpaths build now.** Pilot build = **clean 4 only** (`ai/google`, `ai/mock`, `ai/pricing`, `ai/redact` — census-deps:none, pure ports/mechanisms, `/google` fixes a live `getAdapter` throw). **Build-deferred** (spec kept): `ai/agent`, `ai/agent/tools/read-image` (← `util/ssrf-guard` unbuilt), `ai/agent/tools/search-kb` (← `helpdesk/kb` unbuilt), `ai/job` (← `jobs` runner/outbox under-built), `ai/kind`, `ai/kind/translate` (×1 donor-only seam-extractions, freeze-blocked until a non-donor consumer). Gate + unblock conditions: spec "Build scope (DAG-gated)".
  - **SHIPPED 2026-06-15 (main `4fd36d2`):** clean-4 built + seam-reviewed + merged — `ai/google` (closes the live `getAdapter('google')` `FatalError`), `ai/mock`, `ai/pricing` (host rate table, no baked rates R1), `ai/redact` (PII security floor). Plus one additive core seam reconciliation: `supportsImage?: boolean` on `AIAdapter` (MINOR; boundary `assertUsageBoundary` token-only/no-cost unchanged), set truthfully — `true` on the 3 image-mapping adapters (google/openai-compat/anthropic), `false` on the `mock` fixture (maps no image content). This core delta (`4fd36d2`+`ee7b71b`) initially bypassed the per-diff seam-gate → ran a **post-hoc seam-reviewer pass (PASS 2026-06-15)**, all 5 invariants (boundary-freeze · honesty both directions · seam-visibility via `getAdapter()` · spec↔code · changeset) verified by code. 41/41 tests, bundle-isolation holds (`@google/genai` external, core excludes adapters). Deferred-6 remain specced/unbuilt (no stubs).
- **`i18n` / `i18n-react`** — over-thinned. **SHIPPED 2026-06-15 (main `b74012a`):** harvested + seam-reviewed (2 real bugs fixed: DirectionProvider conditional-hook, zod peer `>=4`) + merged. Harvest → module: namespaced+nested keys, lazy bundle-split loader (serves CF-Workers ceiling), `pickLocalized` (field NAMES are host config), `createZodErrorMap`; react: `useT(ns)`+memoized, zustand-persist store, #418 hydration parity, `DirectionProvider`. NOT i18n: a11y prefs (`usePrefs`) → own module/host. BUG (not capability): multideal locale-default ignores Accept-Language → fix in host (module order is correct).

### BATCH 1 verdicts (util/db/auth/jobs/mail) — rendered 2026-06-15 from `r5-evidence-batch1.md`
Verdict codes: **CORE** (universal) · **IMPORT** (generic → importable subpath) · **COMPOSE** (route to existing module) · **HOST** (app-domain/request-lifecycle) · **-REACT** (framework-axis sibling, never a core subpath §6) · **GATE** (needs a Gate-1 decision).

**Headline:** every batch-1 module except `db` (a type contract, mostly correct) is **under-built** vs the *combined* multideal+zync donor maturity — `auth`, `jobs`, `mail` substantially. Strongly validates R5.

#### util (L0, zero-dep — importable leaves stay zero-dep or peer)
| Capability (donor) | R5 | Target |
|---|---|---|
| minor-unit money format (`money.ts`) | IMPORT | `util/money` — currency a param, NO default (R1) |
| Intl currency formatter (`format.ts`) | COMPOSE | dedup into `i18n` (already has `formatCurrency`; ILS-default = R1 bug) |
| fixed-zone civil-date wrappers (`datetime.ts`) | CORE | confirm already in shipped `util/datetime`+`timezone`; strip Israel default |
| base64url/hex CF-safe (`encoding.ts`) | IMPORT | `util/encoding` (or fold into `crypto`) |
| open-redirect sanitizer | IMPORT | `util/sanitize-redirect` (security floor) |
| cross-tab event bus | IMPORT | `util/cross-tab` (browser leaf, zero-dep) |
| SSRF outbound guard (zync) | IMPORT | `util/ssrf-guard` (security; Workers-critical; zync donor) |
| stable object hash (`canonicalFilterHash`) | IMPORT | `util/stable-hash` (Web Crypto) |
| URL query codec (`queryCodec`) | IMPORT | `util/url` |
| zod first-issue (`validation/zod`) | IMPORT | `util/validation` (zod peer) |
| display initials (`string.ts`) | IMPORT | `util/string` |
| phone display format | IMPORT/HOST | `util/phone` if generic; else host (borderline) |
| client error capture (`observability`) | GATE | Sentry-peer leaf vs an observability module (×2 donors?) |
| image encode/upload pipeline | COMPOSE | → `uploads` (`./image-encoder` decision) |
| URL-filter hook / optimistic-mut / paginated-hook | -REACT | client React utils → a `-react` util sibling, NOT util core |

#### db (L0 — type contract + drivers; mostly correct already)
| tx-union `Querier` type | CORE | confirm matches module's `Database`/`TransactionalDatabase`/`Querier` spine |
| PII column encrypt (pgcrypto) + blind-index HMAC | IMPORT | `db/crypto` — generic PII + searchable index (security; high value) |
| outbox SKIP-LOCKED + retry + dead-letter + redrive | COMPOSE | → `jobs/outbox` (module outbox lacks dead/redrive) |
| cron cursor / run-heartbeat / DLQ counter | COMPOSE | → `jobs` |
| guest access-token queries | COMPOSE | → `auth` (guest-token engine) |
| `drizzle-kit migrate` | HOST | tooling/CLI, consumer-run; documented pattern, not an export |
| fractional-index ordering (zync) | IMPORT | `util` or `db` pure-math ordering helper |
| keyset cursor codec (zync) | IMPORT | `db/pagination` (or util) |

#### auth (L1) — SIGNIFICANTLY under-built vs multideal+zync combined
| session create/rotate/revoke-all | CORE | engine-custom — confirm revoke-all + RT-grace present |
| magic-link issue/consume | IMPORT | `auth/magic-link` |
| email-verification token pair | IMPORT | `auth/email-verification` |
| CSRF token engine (HMAC/double-submit) | IMPORT | `auth/csrf` |
| auth cookie builders | IMPORT | `auth/cookies` |
| guest/anon resource token | IMPORT | `auth/guest-token` |
| RBAC claim helpers (zync `rbac`) | IMPORT | `auth/rbac` |
| 2FA pending-session + backup-code (zync) | IMPORT | `auth/two-factor` |
| Firebase ID-token verify | IMPORT | `auth/firebase` adapter (peer) |
| per-user argon2id DO lockout (Workers) | IMPORT | `auth/password-do` — Workers CPU-budget pattern (high value, runtime lock) |
| DB token-bucket + CF-binding rate-limit (2 shapes) | GATE | rate-limit is its OWN seam (≥2 consumers, not auth-owned) |
| CSRF middleware / guard-redirect / Hono guards | HOST | request-lifecycle (§8) |
| case-role RBAC (`determineCaseRole`) | HOST | app-domain (or helpdesk) |

#### jobs (L1) — outbox under-built; no cron; no generic runner
| outbox retry-backoff + dead-letter + redrive + handler registry | CORE | harvest into `jobs/outbox` ("lacks dead/redrive") |
| generic runJob lifecycle (ok/retry/fatal) + stale-RUNNING reaper | IMPORT | `jobs/runner` — the generic runner **`ai` COMPOSES** (rung-2 confirmed both sides) |
| cron expr→handler dispatch + heartbeat + cursor | IMPORT | `jobs/cron` (standalone `cron` module already Gate-1 killed → jobs subpath) |
| DLQ metrics counter | IMPORT | `jobs` dlq metrics |
| typed batch queue-consumer ergonomics (zync) | IMPORT | extend `jobs/cf-queues` |
| HTTP self-fetch cron router (zync) | IMPORT | `jobs/cron` adapter variant (2 donor shapes → seam) |

#### mail (L1) — only adapters shipped; templates/bulk/suppression UNBUILT
| zero-dep `{{var}}` template render + escape (zync) | IMPORT | `mail/templates` (core; escaping = security floor) |
| React email render + primitives (multideal) | -REACT | new `mail-react` sibling (framework axis ≠ core, §6) |
| bulk mailer seam + Brevo + campaign-stats + list ops | IMPORT | `mail/bulk` |
| signed unsubscribe tokens | IMPORT | `mail/unsubscribe` (HMAC; uses `util/tokens`) |
| dev mock capture | IMPORT | `mail/mock` (test aid) |
| suppression/bounce store (TYPE exists, impl MISSING) | IMPORT | `mail/suppression` — build the declared seam |
| multi-channel deliver (zync) | COMPOSE | → `notifications` (hub owns channel routing; mail = email channel) |

#### Cross-cutting findings (batch 1)
- **Rate-limit is its own capability** — multideal DB-bucket + zync CF-binding = 2 adapter shapes → **Gate-1: a dedicated rate-limit seam, NOT an auth subpath.**
- **A client/React util sibling is implied** — 3+ React hooks surfaced in util's donor → a `-react` util leaf (§6), never util core.
- **`fractional-index` + keyset-cursor** → importable ordering/pagination helpers (util-or-db).
- **Generic job runner → `jobs`** — confirms `ai` composes `jobs` (rung-2 validated from the jobs side too).
- **multi-channel deliver → `notifications`**; **`mail-react`** is a new sibling candidate.

### BATCH 2 verdicts (notifications/ledger/tax/uploads/billing/helpdesk) — rendered 2026-06-15 from `r5-evidence-batch2.md`
Same codes: **CORE** · **IMPORT** · **COMPOSE** · **HOST** · **-REACT** · **GATE/WATCH**.

**Headline:** `notifications` is the **most over-thinned module in the platform** (alongside `auth`) — it shipped 2 channel *adapters* and is missing the entire hub (deliver/fallback core, in-app channel, inbox+read-state, tiered registry, prefs gate). By contrast `tax` is **correctly thin** — its whole DELTA relocates to an (unbuilt) `invoicing` module, not to tax. `ledger`/`billing`/`uploads`/`helpdesk` each have a few sharp, real gaps. The ai↔helpdesk↔search↔notifications **COMPOSE chain** is confirmed from every side.

#### notifications (L2) — SHIPPED 2 ADAPTERS; THE HUB ITSELF IS UNBUILT (worst R5 gap)
| Capability (donor) | R5 | Target |
|---|---|---|
| multi-channel deliver + email→push fallback orchestration | CORE | harvest into `.` — the hub's *reason to exist*; registry `pushFallback` flag drives it (zync `deliver.ts` is the mature form) |
| inbox persistence + read-state (markRead/unreadCount/listInbox) | IMPORT | `notifications/inbox` (composes `db`; schema host, queries generic) |
| tiered event registry (critical/optional/marketing + durable/pushFallback) | IMPORT+HOST | `notifications/registry` = the **mechanism/metadata shape** (generic); the ~80 concrete event entries = HOST (i18n/ai pattern: generic mechanism + host config) |
| tier-aware preference gate (`allowsEvent`) | IMPORT+HOST | `notifications/prefs` gate generic; JSONB column shape = HOST |
| Telegram channel adapter (zync) | IMPORT | `notifications/telegram` (bot-token peer) — same channel-adapter contract as email/webpush |
| in-app/live delivery (WS/DO) + live topic routing | COMPOSE | → `realtime` — **but realtime `/server` (DO/Queue) is UNBUILT** → this channel is blocked on it (sequencing dep, see cross-cutting) |
| per-channel subscribe authz (`canSubscribe`) | HOST | app channel rules (chat/ticket/case/admin) — app-domain |
| encrypted per-tenant adapter credentials | IMPORT | `notifications/credentials` (composes `db/crypto` from batch-1) |
| digest/batching workflow | COMPOSE+HOST | scheduling → `jobs`; digest content/query → HOST |
| SMS channel | WATCH | 0 donors — channel-adapter seam accommodates it; don't build |

#### ledger (L2) — well-layered; 4 sharp seam gaps (×1 donor → do NOT freeze contract)
| Capability (donor) | R5 | Target |
|---|---|---|
| self-wrapped tx entrypoint (append opens own tx when no outer tx) | CORE | **the `tx()`-context seam** (known migration blocker) — add optional-outer-tx overload to append/debit |
| ledger→wallet reconcile CTE (recompute from source-of-truth) | IMPORT | `ledger/reconcile` (cron-safe); scheduling → COMPOSE `jobs` |
| negative-balance carry-forward (donor allows on matured; module throws) | CORE-config | balance-floor must be a **policy param** per entry-class, not a hard-coded throw |
| 4-bucket wallet projection (balance/pending/matured/withdrawable) | IMPORT | confirm vs shipped `vesting`/`lifetime`/`balance-after`; harvest the projection mechanism |
| TOCTOU-safe payout-debit (lock+check+append) | CORE | generic; eligibility *window* policy = HOST |
| withdrawable sweep | COMPOSE | → `jobs` (scheduled) |
| typed debit-reason enum (referral_reward/redemption/…) | HOST | app entry-type values; module stays generic (R1 — no donor defaults) |
| vesting hold scheduling (`computeMatureAt`) | CORE+HOST | mechanism in shipped `vesting`; deal-kind→hold mapping = HOST |
| double-entry | WATCH | donor is single-entry signed-delta — drop (YAGNI) |

#### tax (L2) — CORRECTLY THIN; DELTA relocates to `invoicing`, NOT tax
| Capability (donor) | R5 | Target |
|---|---|---|
| rate-resolution / inclusive+exclusive math | CORE | module HAS it (`rates-table`) — Pattern C confirmed |
| build the `.` root export (shipped `rates-table` only) | GATE | tax is **PARTIAL** — root entry not exported; small real gap |
| schedule storage: static dataset vs DB-backed admin-mutable | GATE | 2 donor shapes (multideal `system_config`, zync `vat_rates`) → a `tax/schedule-store` adapter seam decision |
| tax-document persistence + issuance + per-line taxable + doc sides | COMPOSE | → **`invoicing`** (unbuilt L3) — these are invoicing concerns, NOT tax |
| VAT-id validation | WATCH | 0 donors (donors store but don't validate) — stays forward-declared |
| exemptions (non-taxable lines) | COMPOSE | → `invoicing` (line-level) |

#### uploads (L2) — server seam mature; client encoder is the big gap (×1 donor → don't freeze)
| Capability (donor) | R5 | Target |
|---|---|---|
| client image-encoder/resizer + variant generation (Web Worker + jsquash WASM) | IMPORT | **`uploads/image-encoder`** — browser subpath, jsquash peer (resolves batch-1 "image-encoder decision"); framework-agnostic, NOT a `-react` concern |
| worker-proxied upload flow (HMAC token + server stream) | IMPORT | `uploads/proxied-upload` — **CF-Workers-native** (runtime lock §5), 2nd adapter alongside shipped `presign-s3compat` |
| upload lifecycle orchestration (initiate/finalize) | IMPORT+COMPOSE | `uploads/lifecycle` seam; persistence → `db`; schema → HOST |
| AI/abuse scan gate on finalize | COMPOSE | → `ai` (IMAGE_APPROVAL) + `jobs` (enqueue) |
| purpose→dimension constraint enforcement | CORE+HOST | check mechanism in shipped `image-dimensions`; purpose→constraint map = HOST |
| approval/scan status columns | HOST | app schema |

#### billing (L2, pilot/last) — primitives mature; do NOT dump Connect/subscriptions to host
| Capability (donor) | R5 | Target |
|---|---|---|
| Stripe Connect (onboarding + transfer + payout) | IMPORT | **`billing/connect`** — generic marketplace Stripe surface; only the affiliate *wiring* is host (R5: don't discard mature generic capability) |
| subscription/recurring billing (zync — richer) | IMPORT | **`billing/subscriptions`** — forward cross-class SaaS need + mature zync donor |
| platform-fee split (application_fee/transfer_data) | IMPORT+HOST | split mechanism generic; fee % source = HOST |
| webhook verify + dedup/idempotent-finalize | CORE | confirm shipped `stripe` ingestWebhook covers single-winner dedup |
| 3DS/SCA client-action branch (`requires_client_action`) | CORE | confirm clientSecret return path in shipped adapter |
| purchase-finalize chokepoint (QR/tax/referral/push/outbox) | HOST | app orchestration composing `ledger`+`tax` |
| domain refund-policy calculator (deal-type splits) | HOST | app rules; refund *execution* is CORE/shipped |
| concrete Stripe webhook route | HOST | app handler over generic `ingestWebhook` |
| dunning / failed-payment retry | WATCH | 0 donors; → `jobs` when a subscription consumer needs it |

#### helpdesk (L3 — now ×2 donors: multideal+zync; lacks `ticket`+`kb`)
| Capability (donor) | R5 | Target |
|---|---|---|
| ticket entity (opener/assignee/reopen/metadata-mutation) | IMPORT | **`helpdesk/ticket`** — module has `case` only; this is the surface ai `get-ticket`/`set-category` COMPOSE |
| knowledge base (articles/slug/category/locale + search) | IMPORT+COMPOSE | **`helpdesk/kb`** entity; search → COMPOSE `search`+`util/fts` (the ai `search-kb` tool's substrate) |
| AI agent tools (get-ticket/post-message/close/set-cat/list-attach/escalate/request-evidence) | COMPOSE | → these are **ai-side** (pilot bucket-2); helpdesk exports the *ops* they wrap (close+transition ✓, messages ✓, attachments ✓; **add metadata-mutation**) |
| escalate side-effects | COMPOSE | → `notifications` |
| transaction case + dual SLA (ai-vs-human targets) | CORE | module HAS `case`+`sla` — confirm `sla` covers dual-target |
| zync tenant ticket CRM (cursor pagination, tenant guards) | COMPOSE | tenant scope → `tenancy`; paging → `db/pagination` (batch-1 keyset); model → `helpdesk/ticket` |

#### Cross-cutting findings (batch 2)
- **`notifications` is the platform's worst over-thinning** (tie with `auth`): it shipped channel adapters but **not the hub** — multi-channel deliver+fallback is the CORE that justifies the module's existence and it is absent. zync's `deliver.ts` is the mature form to harvest.
- **`realtime /server` (DO/Queue) being UNBUILT is now load-bearing** — `notifications`' in-app channel + live topic routing COMPOSE it. Sequencing dependency: notifications' in-app channel cannot ship until realtime `/server` exists. (Core is types-only by intent; this is the first hard consumer.)
- **`invoicing` is the missing owner** — most of `tax`'s DELTA (doc persistence/issuance/per-line/exemptions) is *invoicing*, confirming tax is correctly thin and surfacing `invoicing` (L3 WATCH) as a real future module, NOT a tax expansion.
- **`helpdesk` crosses the disposition floor** — now ×2 donors (multideal cases+tickets+kb, zync tenant CRM) → `ticket`+`kb` move from WATCH toward designed; the ai support agent is their first consumer.
- **×1-donor freeze guardrail binds `ledger` + `uploads`** (multideal sole donor) — harvest the mature form now, but **do not freeze the contract** until a non-donor app confirms it (R5 guardrail).
- **R5 "don't dump to host" bites hardest in `billing`** — Connect + subscriptions are mature generic capabilities (the surface-prior hypothesis "likely host per affiliate tie" was itself over-thinning) → importable subpaths; only app wiring stays host.
- **The COMPOSE chain is fully confirmed**: ai bucket-2 tools → `helpdesk` ops; `helpdesk/kb` → `search`; `helpdesk` escalate + `notifications` → realtime; `ai` LLM lifecycle → `jobs`. Every rung-2 route validated from both ends.

## Full PLATFORM-MATURITY backlog (assembled from batch 1 + 2)

> **SCOPE WARNING — this is NOT the multideal migration gate.** This is the per-module R5 backlog: what each module needs to reach its mature *combined-donor* form. It includes **zync-only** surface multideal never consumes (`billing/subscriptions`, `notifications/telegram`, `auth/firebase`, …).
>
> **The multideal migration gate = this backlog ∩ multideal's adopt-only-needed cull** (decision A — the 8 import + 3 partial in `platform_modules_report.md`). Only the *intersection* blocks "multideal first." Building the rest now violates YAGNI ("build the exact seam the need requires, never a speculative framework"). The R5 mandate is a **design rule applied when a module is built/adopted** — not an order to bring all 13 modules to full surface before migrating. Cross this list against the cull before committing effort past the ai+i18n pilot.

Ordered by layer (build bottom-up), tagged by effort signal.

**L0:**
- `util`: +`money` `encoding` `sanitize-redirect` `cross-tab` `ssrf-guard` `stable-hash` `url` `validation` `string` (+ a `-react` util sibling for the client hooks). Dedup `format` currency → `i18n`.
- `db`: +`db/crypto` (PII encrypt + blind-index) · +`db/pagination` (keyset) · +fractional-index helper. Mostly correct otherwise.

**L1:**
- `auth` (under-built): +`magic-link` `email-verification` `csrf` `cookies` `guest-token` `rbac` `two-factor` `firebase` `password-do`. Rate-limit = **its own Gate-1 seam** (not an auth subpath).
- `jobs` (under-built): harvest `outbox` dead-letter/redrive/registry [CORE] · +`runner` (the generic lifecycle `ai` composes) · +`cron` · DLQ metrics · queue-consumer ergonomics.
- `mail` (only adapters shipped): +`templates` `bulk` `unsubscribe` `mock` `suppression` (+ a `mail-react` sibling). multi-channel deliver → `notifications`.

**L2:**
- `notifications` (WORST): harvest deliver+fallback [CORE] · +`inbox` `registry` `prefs` `telegram` `credentials`. In-app channel BLOCKED on realtime `/server`.
- `ledger`: tx-context seam [CORE] · negative-balance policy param [CORE] · +`reconcile` · confirm 4-bucket projection. (×1 — don't freeze.)
- `tax`: build `.` root [GATE] · `schedule-store` adapter [GATE]. Rest → invoicing.
- `uploads`: +`image-encoder` (browser/jsquash) · +`proxied-upload` (Workers) · +`lifecycle`. (×1 — don't freeze.)
- `billing` (pilot/last): +`connect` `subscriptions` · platform-fee split.

**L2 prerequisite surfaced:** `realtime/server` (DO/Queue) — promote from intentionally-unbuilt now that `notifications` is a hard consumer.

**L3 (WATCH→designed):**
- `helpdesk`: +`ticket` `kb` (kb composes `search`). Now ×2 donors.
- `invoicing`: NEW — owns tax-doc persistence/issuance/per-line/exemptions. ×2 donors (multideal + zync).

**Pilot rebuild target (user's pick): `ai` + `i18n`** — verdicts in PILOT section above; deep harvest-rebuild next.
