# Backend Migration Capability Parity Matrix

Audience: AI coding agents first. Treat `GAP` as required migration work unless row says `DROPPED:<reason>`.

Sources: demand contract `docs/contracts/plugin-client-contract.md`; old behavior `api/src/routes/*.ts`; current supply `/home/user/Projects/platform/apps/press-zone/src/`; locked design `/home/user/Projects/platform/docs/specs/2026-07-03-plugin-backend-core-design.md` (byte-identical to local `docs/specs/2026-07-03-plugin-backend-core-design.md`).

| ID (CAP-xx) | Capability | Old surface (endpoints) | Plugin caller (file) | New surface (route/mechanism or GAP or DROPPED:&lt;reason&gt;) | Wire-compat? (identical / capability-equivalent-different-wire / missing) | L1 test ID (T-CAP-xx, to be written) | Status (GAP / EXISTS-UNTESTED / DROPPED) |
|---|---|---|---|---|---|---|---|
| CAP-01 | Synchronous translation | `POST /v1/translate`; `POST /v1/translate/bulk` (`api/src/routes/translate.ts`) | `includes/Translation/TranslationService.php`; `includes/Integration/TranslationAPI.php`; `includes/Translation/MetaBox.php`; `includes/Translation/BulkActions.php` | **GAP.** `routes/plugin.ts` exposes metered plugin scaffold only; runtime `index.ts` injects `execute: async () => ({ok:true})`. Core spec §6.4 assigns translate handler to Spec 2. | missing | T-CAP-01 | GAP |
| CAP-02 | Async job submission, callback delivery, and polling | `POST /v1/jobs`; `POST /v1/jobs/bulk-strings`; `GET /v1/jobs/{api_job_id}` (`api/src/routes/jobs.ts`) | `includes/Translation/TranslationService.php`; `includes/Translation/JobSender.php`; `includes/API/TranslateController.php`; `includes/API/StringTranslateController.php`; `includes/API/TranslateJobsController.php` | **GAP.** No job HTTP routes, queue submission, status store, or signed callback delivery in current app. Core spec §9 describes queue architecture only; handler belongs to Spec 2. | missing | T-CAP-02 | GAP |
| CAP-03 | Character estimate | `POST /v1/estimate` (`api/src/routes/estimate.ts`) | `includes/Translation/CharacterEstimator.php` | **GAP.** No estimate route/handler under `src/routes/`. | missing | T-CAP-03 | GAP |
| CAP-04 | Exception dictionary push/pull sync | `POST /v1/exceptions/sync`; `GET /v1/exceptions?limit=100` (`api/src/routes/exceptions.ts`) | `includes/Translation/TranslationService.php` | **GAP.** No exception persistence or plugin route in current app. | missing | T-CAP-04 | GAP |
| CAP-05 | Site registration and seat consumption | `POST /v1/sites/register` (`api/src/routes/sites.ts`) | `includes/Translation/SiteRegistrar.php` | `POST /oauth/token` creates `sites` + `credentials` rows (`routes/oauth.ts`), returning OAuth token envelope. **GAP:** current app code does not enforce seat availability during authorize/token persistence; old metadata payload is not accepted. | missing | T-CAP-05 | GAP |
| CAP-06 | Site heartbeat and deactivation | `PATCH /v1/sites/{site_id}` (`api/src/routes/sites.ts`) | `includes/Translation/SiteRegistrar.php` | **GAP.** `GET /api/account` and `GET /api/account/:accountId` read sites (`routes/account.ts`); no plugin-facing update/disconnect route exists. | missing | T-CAP-06 | GAP |
| CAP-07 | Plan listing | `GET /v1/onboarding/plans?plugin=international` (`api/src/routes/onboarding.ts`; pricing reference `api/src/routes/pricing.ts`) | `includes/Translation/OnboardingApi.php` | **GAP.** Subscription packages exist in DB/schema and are consumed internally by `POST /api/subscribe` (`routes/subscribe.ts`), but no public plan-list route exists. | missing | T-CAP-07 | GAP |
| CAP-08 | Account registration and login | `POST /v1/auth/register`; `POST /v1/auth/login` (`api/src/routes/auth.ts`) | `includes/Translation/OnboardingApi.php` | **DROPPED: replaced by platform session login plus OAuth2 authorization-code/PKCE connect flow.** App exposes `GET/POST /oauth/authorize`, `GET /oauth/callback`, `POST /oauth/token` (`routes/oauth.ts`); plugin must stop consuming JWT `accessToken` responses. | capability-equivalent-different-wire | T-CAP-08 | DROPPED |
| CAP-09 | Legacy/mock checkout and upgrade | `POST /v1/onboarding/checkout`; `POST /v1/onboarding/upgrade` (`api/src/routes/onboarding.ts`) | `includes/Translation/OnboardingApi.php` | **DROPPED: non-production mock purchase paths replaced by real subscription creation and OAuth credential provisioning.** Locked core spec §§6.1–6.2; current `routes/subscribe.ts` + `routes/oauth.ts`. | capability-equivalent-different-wire | T-CAP-09 | DROPPED |
| CAP-10 | Checkout initiation | `POST /v1/subscriptions/checkout` (`api/src/routes/account.ts`) | `includes/Translation/OnboardingApi.php`; `admin/src/pages/onboarding.js` | `POST /api/subscribe` under session auth creates PayPal subscription, entitlement, and initial wallet (`routes/subscribe.ts`; mount in `index.ts`). Response has subscription/wallet identifiers, not old `approval_url` + `checkout_session_id`. | capability-equivalent-different-wire | T-CAP-10 | BLOCKED-ON-INFRA |
| CAP-11 | Checkout completion polling and one-time credential retrieval | `GET /v1/subscriptions/checkout-sessions/{id}` (`api/src/routes/account.ts`) | `includes/Translation/OnboardingApi.php`; `admin/src/pages/onboarding.js` | **DROPPED: checkout-session polling removed; PayPal webhooks update subscription state (`routes/webhooks.ts`) and OAuth `POST /oauth/token` provisions per-site credential (`routes/oauth.ts`).** No replacement polling route. | capability-equivalent-different-wire | T-CAP-11 | DROPPED |
| CAP-12 | License activate/deactivate/validate/status | `POST /v1/international/license/activate`; `POST /v1/international/license/deactivate`; `POST /v1/international/license/validate`; `GET /v1/international/license/status` (`api/src/routes/multilingualLicense.ts`; analogous `api/src/routes/translateLicense.ts`) | `includes/Licensing/LicenseClient.php`; `includes/Licensing/ActivationManager.php`; `includes/Licensing/LicenseValidator.php` | **DROPPED: license keys are retired product-wide. Site lifecycle is covered by CAP-05/CAP-06 and credential lifecycle by the OAuth connect flow; no license surface is a cutover target.** | capability-equivalent-different-wire | T-CAP-12 | DROPPED |
| CAP-13 | Plugin update check | `GET /v1/international/updates/check` (old licensing service; caller contract) | `includes/Licensing/LicenseClient.php`; `includes/Licensing/UpdateChecker.php` | **GAP.** No update metadata route or mechanism in current app/spec. | missing | T-CAP-13 | GAP |
| CAP-14 | Health probe | `GET /health` (`api/src/routes/health.ts`) | `includes/Licensing/LicenseClient.php`; `includes/API/SettingsController.php` | `GET /health` → `{data:{ok:true}}` (`routes/health.ts`; mounted by `index.ts`). Status-only caller works; legacy caller expecting old decoded body and sending `X-API-Key` sees different body but header is harmless. | capability-equivalent-different-wire | T-CAP-14 | PASS |
| CAP-15 | Site credential issuance and Bearer authentication | Old API-key/JWT/license-key mechanisms across `POST /v1/auth/*`, onboarding, sites, translation, jobs, and license routes | `includes/Translation/Settings.php`; `includes/Translation/OnboardingApi.php`; `includes/Licensing/LicenseClient.php` | OAuth2 PKCE routes issue per-site opaque token; `mw/auth.ts` accepts `Authorization: Bearer <prefix>.<secret>` through `verifyApiKey`; `lib/credential.ts` resolves account/site/provider linkage. Plugin credential storage and connect flow require migration. | capability-equivalent-different-wire | T-CAP-15 | BLOCKED-ON-INFRA |
| CAP-16 | Entitlement and permission enforcement | Old per-route API-key/JWT/license checks (`api/src/routes/*.ts`) | All authenticated callers above | `mw/auth.ts` resolves principal/scopes; `mw/access.ts` enforces entitlement + `translate.write`; `routes/plugin.ts` composes guards. Error envelope is `{error:{code,message}}`. | capability-equivalent-different-wire | T-CAP-16 | BLOCKED-ON-INFRA |
| CAP-17 | Credits balance, debit, and insufficient-credit rejection | Translation/job execution debits/returns usage; account credit implementation in `api/src/routes/account.ts`; stats reference `api/src/routes/stats.ts` | Translation/job callers consume `characters_used`, `cost_usd`, and HTTP 402; no standalone balance endpoint in client contract | Period wallet seeding/debit exists in `lib/wallet.ts`; `routes/plugin.ts` atomically debits and returns 402 `INSUFFICIENT_CREDITS`; account dashboard routes expose wallet balance (`routes/account.ts`). **Translation-specific cost calculation/usage response remains CAP-01/CAP-02 GAP.** | capability-equivalent-different-wire | T-CAP-17 | BLOCKED-ON-INFRA |
| CAP-18 | Rate limiting | Old public/API/license route limiters (`api/src/routes/auth.ts`, `translate.ts`, `jobs.ts`, `multilingualLicense.ts`, `translateLicense.ts`) | Plugin handles 429 on translation/jobs/estimate/license paths | Global per-IP 60/min KV limiter (`mw/rate-limit.ts`, installed before routes in `index.ts`), fail-open when binding absent. No per-credential or per-account job-concurrency limiter exists despite core spec §9. | capability-equivalent-different-wire | T-CAP-18 | FAIL |

## Endpoint Coverage

- Contract outbound endpoints: **23**.
- Mapped exactly once across CAP-01–CAP-14: **23**.
- Unmapped: **none**.
- Inbound WordPress callbacks are requirements within CAP-02, not outbound endpoint-count members.

## Live L1 Summary

- GAP / NOT-IMPLEMENTED: **8** capabilities — CAP-01–CAP-07, CAP-13.
- BLOCKED-ON-INFRA: **4** capabilities — CAP-10, CAP-15–CAP-17.
- FAIL: **1** capability — CAP-18.
- PASS: **1** capability — CAP-14.
- DROPPED: **4** capabilities — CAP-08, CAP-09, CAP-11, CAP-12.
- Total: **18** capabilities.

## Spec 2 Backlog

- **CAP-01:** accept both contract translation request dialects/shapes; return every consumed translation/usage field or migrate all plugin callers atomically.
- **CAP-02:** submit content and bulk-string jobs, persist/poll status, deliver every signed WordPress callback dialect, and preserve idempotent recovery behavior.
- **CAP-03:** estimate token/character usage for all target languages with contract error envelope and local-fallback-compatible statuses.
- **CAP-04:** full-replace and fetch account/site-scoped exception dictionaries with `limit=100` support.
- **CAP-05:** create/update site from plugin metadata, enforce subscription seat cap before persistence, and return stable site ID.
- **CAP-06:** accept daily metadata heartbeat and idempotent deactivation for issued site ID.
- **CAP-07:** expose purchasable International plugin packages with fields consumed by onboarding UI.
- **CAP-13:** serve signed/versioned plugin update metadata containing at least `update_available`, `version`, and package `url`.

## Uncertainties

- Route double-prefix **NOT-REPRODUCED**: `createApp()` mounts at `/api`; `createPluginRoute()` declares `/plugin/:plugin`; canonical route is `/api/plugin/:plugin`.
- OAuth seat-cap defect **CONFIRMED**: authorize/token flow has no active-site count or `seats_config` guard before site persistence.
- Subscribe approval URL defect **CONFIRMED**: `SubscribeAccountResult` omits provider approval URL.
- Two incompatible plugin handlers register the same WordPress callback path; Spec 2 must select one contract or support both (`docs/contracts/plugin-client-contract.md`, inbound webhook section).
- `/v1/jobs` and `/v1/translate` each have incompatible active request/response dialects. No dialect may be silently discarded before caller consolidation.
- License-status call has no in-repository response-field consumer; compatibility requirement is successful valid JSON only.
