# Seller Management — Backend Migration Plan Documentation only. No code, no implementation. This plan synthesizes the three prior audits — do not re-derive facts already established there, read them for detail: - [Seller-Management.md](Seller-Management.md) — capability overview, Implemented/Planned/Future legend - [Seller-Management-Backoffice-Readiness-Audit.md](Seller-Management-Backoffice-Readiness-Audit.md) — per-admin-module scoping facts - [Seller-Management-Storefront-Audit.md](Seller-Management-Storefront-Audit.md) — storefront/subdomain facts - `BACKEND.md` §11 — the backend documentation this plan assumes as its starting contract **Non-negotiable constraint repeated from the mission, and load-bearing for every classification below:** existing marketplaces without sellers must continue working exactly as today. Seller Management stays optional forever, not just at launch. No endpoint may require seller support unless `modules.sellerManagement.enabled` is true. Every classification and every migration strategy in this document is written to satisfy that constraint first — where a module can't satisfy it without a structural rework, that's called out explicitly, not glossed over. ## Endpoint classification legend - **No change** — endpoint's request/response/behavior is untouched. - **Minor change** — an optional field/parameter added (e.g. `sellerId?` in a filters object or DTO); absent value behaves identically to today. - **Major change** — data model or endpoint semantics change beyond an optional field (e.g. a new join, a new required decision like split orders, a new aggregation dimension). - **New endpoint** — doesn't exist today, net-new surface. ## Module-by-module ### Authentication - **Current:** Two live mechanisms (`BACKEND.md` §4) — Telegram/QR customer session login (live), Ed25519 admin challenge/response (built client-side, not wired live). No seller concept in either. - **Future:** `SellerPermissionRole` (`marketplaceOwner`/`seller`/ `sellerStaff`/`platformAdmin`) as a role a session can carry, and a decision on whether Seller/Seller Staff authenticate via a third mechanism or reuse Telegram/Ed25519 with a different claim. - **Migration strategy:** additive only — a new optional claim/role field on the existing JWT structure (§4 §3), never a new auth mechanism forced onto existing flows. Existing customer and admin login must not gain any new required step. - **Backward compatibility:** total, if done as an additive claim. A session with no seller claim today is indistinguishable from a session before this work existed. - **Risk:** **Medium** — auth is the most consequence-sensitive surface in the app (ADR-010 froze it once already); any change here needs security review, not just a schema add. - **Effort:** Medium (claim/role addition) once the role-vs-`AdminRole` reconciliation decision (§11.4) is made; that decision itself is the bigger unknown, not the wiring. - **Endpoint classification:** existing login/verify/refresh/logout — **Minor change** (optional claim), contingent on the role decision above. New seller-specific login/enrollment flow (if Seller/Seller Staff need one) — **New endpoint**, Future, not designed. ### Authorization - **Current:** Coarse `AdminRole` (Owner/Manager/Support/ReadOnly) mapped to `backoffice.read`/`backoffice.write`/`builder.read`/`builder.write`/ `settings.manage` (§4 §9.1). No route currently enforces even this — the admin role model exists but nothing gates on it yet (confirmed by the Backoffice audit: "no admin module anywhere does role-based hiding of buttons or data today"). - **Future:** `SellerPermissionRole` layered in, deciding whether it extends or sits alongside `AdminRole`. - **Migration strategy:** since authorization enforcement doesn't exist yet even for the current roles, there is no existing enforcement to break — this is genuinely additive design space, not a migration of live behavior. - **Backward compatibility:** trivial today (nothing to preserve because nothing enforces roles yet) but becomes a real compatibility question the moment `AdminRole` enforcement is eventually added — sequence matters: whichever role model ships first should be designed with the other in mind, or the second one becomes a breaking migration of the first. - **Risk:** **Low today, Medium if sequenced wrong** — the risk is entirely about doing `AdminRole` enforcement and `SellerPermissionRole` design in the wrong order, not about either individually. - **Effort:** Low to design (no live behavior to preserve); Medium to implement once both role systems' relationship is decided. - **Endpoint classification:** all admin endpoints — **No change** today (no enforcement exists to change); **Major change** whenever role-enforcement is added generally, seller-aware or not. ### Bootstrap - **Current:** `GET /bootstrap`, backend resolves tenant by Host (ADR-001, §1). `BootstrapConfig` already has optional `modules?`/`seller?` fields (§11.6), always absent/false — no backend populates them. - **Future:** backend resolving `{seller}.{marketplace-domain}` (§11.5) and populating `modules.sellerManagement.enabled` + `seller` for a real seller-scoped request. - **Migration strategy:** the fields already exist and are optional — the only backend work is *populating* them correctly for a resolved seller scope, not adding new frontend-facing shape. Existing marketplaces' bootstrap responses need zero changes; they simply never populate the new fields. - **Backward compatibility:** already verified — these are optional fields added to a live contract with zero consumer changes required (confirmed `tsc --noEmit` clean at the time they were added). - **Risk:** **Low** — this is the best-prepared module in the whole migration, precisely because the frontend contract was already extended in advance without needing backend cooperation. - **Effort:** Medium — the resolution logic (subdomain → seller lookup) is new backend work, but the response shape is already spoken for. - **Endpoint classification:** `GET /bootstrap` — **Minor change** (two new optional response fields to populate, conditionally). ### Products - **Current:** No real backend yet (§8) — mock gateway. `AdminProductListFilters` already has `search/categoryId/visibility/stock/includeArchived/sort/page/pageSize`. `Item`/`AdminProduct` already carry optional `sellerId?` (§11.8), read/written by nobody today. - **Future:** `sellerId` filter on list endpoints; ownership enforcement on create/update/delete once a real seller session exists. - **Migration strategy:** add `sellerId` as one more optional field to the existing filters object and DTOs — the field already exists in the frontend type, so there's no frontend change required at all, only backend query logic (`WHERE seller_id = ? OR seller_id IS NULL` pattern or equivalent) gated behind the feature flag. - **Backward compatibility:** guaranteed at the frontend type level already; backend guarantee depends on making the new column/filter nullable and defaulting existing rows to `NULL` (marketplace-owned) — see Database changes below. - **Risk:** **Low** for the filter addition; **Medium** for ownership *enforcement* (a bug here could hide a marketplace owner's own products from themselves, or leak a seller's products to another seller). - **Effort:** Small (list/filter) once the DI-token seam this domain currently lacks (per Backoffice audit) is added — that seam is independent prerequisite work, not seller-specific. - **Endpoint classification:** list/get — **Minor change**. Create/update (ownership assignment) — **Minor change** if `sellerId` is just an optional write field; **Major change** if ownership transfer or seller-side write restrictions are added. ### Categories - **Current:** The most backend-mature domain — real HTTP already (`AdminCategoriesApiGateway`, DI-token-bound, §3.2). Facade currently hardcodes a full unfiltered fetch and does all real filtering client-side to preserve tree parent/child chains (Backoffice audit finding). - **Future:** categories are conceptually marketplace-wide (shared taxonomy) — the real future question is scoping *products within* a category by seller, not scoping categories themselves. - **Migration strategy:** likely **no backend change at all** for Categories proper; ownership scoping happens one level down, at Products. - **Backward compatibility:** trivial — no change anticipated. - **Risk:** **Low.** - **Effort:** Low (frontend-only fix: the facade's hardcoded full-fetch pattern, unrelated to backend). - **Endpoint classification:** **No change** anticipated. ### Orders - **Current:** `AdminOrderListFilters` has `search/status/page/pageSize`; `AdminOrder` has optional `sellerId?` (§11.8) at the order level. `AdminOrderItem` (per-line-item) has **no seller attribution field at all** — the concrete gap behind Checkout Modes (§11.7). - **Future:** per-item seller attribution, plus the Unified-vs-Split-Orders decision (§11.7) — the single most consequential undecided item in this entire plan. - **Migration strategy:** cannot be resolved by an additive field alone. **Unified Order** path: add optional `sellerId` to each order-item row (minor, additive). **Split Orders** path: checkout must decide, at creation time, whether to write N order records instead of one for a multi-seller cart — that changes `POST /orders`' semantics for any cart containing mixed-seller items, which is a **major** change regardless of how carefully it's gated, because "how many order records does this checkout produce" is not optional-field-shaped. - **Backward compatibility:** guaranteed for any cart containing only marketplace-owned items (the only kind that exists today) under either path. The compatibility risk is entirely about *new* multi-seller carts, which cannot exist until Products have real seller ownership — so there is a natural sequencing safety net here, not just a promise. - **Risk:** **High** — this is the highest-risk item in the whole plan. Payments, refunds, and financial reporting all depend on the Unified-vs-Split decision; getting it wrong after sellers exist means a breaking change to live financial data, not a code refactor. - **Effort:** Large — this decision should be made and locked before any seller onboarding is possible, not discovered mid-rollout. - **Endpoint classification:** `GET /orders` (list/filter) — **Minor change**. `POST /orders` (create) — **Major change** (semantics change based on the Unified/Split decision). `PATCH` status transitions — likely **Minor change** if orders stay 1:1 with a single seller scope (Split path) or **Major change** if partial per-seller status exists within one unified order. ### Payments - **Current:** **Frozen** (ADR-010) — Telegram QR/card payment flow, `POST /cart` (`CartPaymentRequest` → `QrCreateResponse`), status polling. Explicitly preserved unchanged through every prior platform-refactoring pass in this codebase's history. - **Future:** if Split Orders is chosen (§ Orders above), a single checkout may need to create multiple payment records or split a captured payment across sellers — a genuinely new payment-flow shape, not a parameter addition. - **Migration strategy:** **do not touch the frozen flow for the Unified Order path** — a unified order with mixed-seller items can still use today's exact single-payment flow, only the *order* record differs internally. Only the Split Orders path forces any payment-flow change, and even then, the existing flow should remain the path for seller-disabled marketplaces with zero exceptions. - **Backward compatibility:** absolute requirement, restated from ADR-010 — this is the one area of the whole codebase with an explicit prior "freeze, do not redesign" decision, and Seller Management does not override it. - **Risk:** **High** if Split Orders is chosen — payment/financial flows are the least forgiving place for a design misstep in this entire system. - **Effort:** Large, only if Split Orders is chosen; **zero** for Unified Order. - **Endpoint classification:** **No change** under Unified Order. **Major change or New endpoint** under Split Orders (undecided, Future). ### Transactions - **Current:** Derived entirely from the same unscoped Orders list (`AdminTransactionsLocalGateway` mirrors `AdminOrdersLocalGateway`'s pattern, Backoffice audit finding). `AdminTransactionListFilters` has `search/status/type/page/pageSize`. - **Future:** per-seller transaction filtering/reporting. - **Migration strategy:** inherits whatever Orders decides — Transactions cannot be scoped correctly until Orders resolves per-item seller attribution (§ Orders above). Adding a `sellerId` filter here today would be cosmetic without that prerequisite. - **Backward compatibility:** same guarantee as Orders — blocked on the same prerequisite, not an independent risk. - **Risk:** **Medium** — inherits Orders' risk one level removed (financial reporting, not the payment flow itself). - **Effort:** Small once Orders is resolved; not independently schedulable before that. - **Endpoint classification:** **Minor change** (filter field), contingent on Orders' Major change landing first. ### Reviews - **Current:** Storefront submission is live; admin moderation (`AdminModerationGateway`) is mock-only. `AdminReviewListFilters` has `search/status/rating/page/pageSize`. Reviews carry `productId` — no direct seller field. - **Future:** a seller sees reviews on their own products, via a join through Products' `sellerId`, not a new field on the review itself. - **Migration strategy:** add `sellerId` as a filter that internally joins through the product's ownership — additive at the API surface, but implemented as a join, not a stored column on reviews. - **Backward compatibility:** total — the filter is purely additive and optional. - **Risk:** **Low.** - **Effort:** Small, contingent on Products having real `sellerId` data to join against. - **Endpoint classification:** review list/detail — **Minor change**. Reports (`loadReports()`, currently bare no-arg, no filters object at all) — **Minor change** too, but requires adding a filters parameter that doesn't exist on the endpoint today, so slightly more surface than the reviews list. ### Analytics - **Current:** No aggregation endpoint exists at all (§3.20) — every number (revenue, top products, health, recommendations) is computed client-side by summing the *entire* orders/products/categories/reviews corpus. This gap exists independent of Seller Management. - **Future:** per-seller analytics, requiring the underlying domains (Orders, Products, Reviews) to be seller-scoped first, then a real aggregation endpoint partitioned by seller. - **Migration strategy:** do not attempt to seller-scope analytics before a real aggregation endpoint exists for the marketplace as a whole — that is a prerequisite gap, not a Seller Management task. Once it exists, seller-partitioning is an additional dimension on the same aggregation query, not a new endpoint family. - **Backward compatibility:** unaffected — a marketplace with no sellers gets marketplace-wide aggregates exactly as it always has, whenever the real aggregation endpoint is eventually built. - **Risk:** **Medium** — mostly the risk of building the wrong aggregation shape before seller-partitioning is even a consideration, and having to redo it. - **Effort:** Large — this is explicitly the last domain recommended for real backend work in `BACKEND.md` §9, and Seller Management adds a further dimension on top of an already-large lift. - **Endpoint classification:** **New endpoint** (the aggregation endpoint itself doesn't exist yet, seller-aware or not). ### Media - **Current:** `MediaRepository` abstract class, DI-token-bound already (mock live, `ApiMediaRepository` not yet written). `MediaListParams` already accepts `page/pageSize/search/folder/kind/sort`. `MediaAsset` has no owner/uploader field at all. - **Future:** `sellerId` filter, requiring an owner field added to `MediaAsset` first. - **Migration strategy:** two small additive changes — a new optional `sellerId`/`ownerId` field on the asset model, and a matching optional filter field on `MediaListParams`. This module and Categories are the two best-positioned in the entire audit for a low-risk addition. - **Backward compatibility:** total — both changes are optional-field additions to an already-flexible params object. - **Risk:** **Low.** - **Effort:** Small. - **Endpoint classification:** list — **Minor change**. Upload/update (§7) — **Minor change** (one new optional field in the request body). ### Search - **Current:** Standard search contract exists (§2.5) as a framework convention (keyword, no dedicated backend endpoint beyond catalog list filtering — `/search` reuses the same catalog container/endpoint as `/catalog` per the Storefront audit). No autocomplete/trending endpoint exists; explicitly flagged as its own open "Requires backend decision" in §2.12. - **Future:** filtering search results by seller scope, same mechanism as Products (search is not architecturally distinct from catalog browsing today). - **Migration strategy:** inherits Products' migration entirely — no separate search-specific backend work anticipated beyond what Products already needs. - **Backward compatibility:** total, same guarantee as Products. - **Risk:** **Low.** - **Effort:** None beyond Products — not independently schedulable. - **Endpoint classification:** **Minor change**, identical to Products' classification (same underlying endpoint). ### CMS (Static Pages) - **Current:** No backend call at all — reads/writes `BootstrapConfig.staticPages` in-memory (§1, §3.8). Marketplace-wide content (legal, about) by nature. - **Future:** **conceptually does not apply** — no per-seller static page concept exists in this document or in product intent. If sellers ever need their own content pages, that is new product surface, not an extension of this module. - **Migration strategy:** none anticipated. Flag if product strategy later decides otherwise — treat as a new capability, not a CMS migration. - **Backward compatibility:** unaffected — no change proposed. - **Risk:** **None.** - **Effort:** **None.** - **Endpoint classification:** **No change.** ### Builder - **Current:** No backend write path at all (§1.10, §8) — draft/publish is `localStorage`-only, editing one global `BootstrapConfig` document per marketplace. The single strongest one-owner assumption in the codebase (Backoffice audit finding). - **Future:** a seller-scoped builder (per-seller storefront layout/ branding) would be an entirely new product surface built on a different premise than "one config document per marketplace" — not an extension of the existing builder. - **Migration strategy:** none proposed for the existing Builder. If a seller-facing builder is ever wanted, it should be scoped as its own ADR and its own backend surface, not bolted onto the marketplace Builder's existing draft/publish contract. - **Backward compatibility:** unaffected — no change proposed to the existing module. - **Risk:** **None** for the existing module; **High** if a future team attempts to retrofit seller-awareness into the existing single-document model rather than building fresh — flagged explicitly as an anti-pattern to avoid. - **Effort:** **None** now; a seller-facing builder, if ever built, is a large, independent effort, not a migration of this one. - **Endpoint classification:** **No change** to the existing (still nonexistent) builder write path. Any future seller-builder surface is **New endpoint**, Future, not designed. ### Settings - **Current:** No route exists — `comingSoon: true` in the admin nav, no component backs it (confirmed, Backoffice audit). - **Future:** if Settings is ever built, it's the natural home for marketplace-level Seller Management configuration (e.g. enabling the module, default seller policies) — speculative, not designed. - **Migration strategy:** none — there's nothing to migrate. - **Backward compatibility:** not applicable. - **Risk:** **None.** - **Effort:** **None** attributable to Seller Management specifically. - **Endpoint classification:** **No change** — no endpoint exists. ### Notifications - **Current:** A marketplace-facing feature flag exists (`FeatureFlagsConfig.notifications: boolean`) but this gates a storefront UI feature, not a backend notification/email service — no such service exists in this codebase for anyone today. - **Future:** seller onboarding (invitation accepted, application approved/rejected) would need real notification delivery — entirely net-new, not an extension of the existing flag. - **Migration strategy:** none — nothing exists to migrate. - **Backward compatibility:** not applicable. - **Risk:** **Low** (net-new work carries normal build risk, not migration risk). - **Effort:** Medium, whenever built — but zero today and not a prerequisite for anything else in this plan. - **Endpoint classification:** **New endpoint**, Future, not designed. ### Emails - **Current:** No email-sending capability exists anywhere in this codebase. - **Future:** the mocked "Request Access" form on the Phase 1 UI (`admin-seller-management-page.component.ts`) implies a real email would eventually be sent on submission — today it's a `setTimeout`-mocked success dialog with zero delivery. - **Migration strategy:** none — nothing exists to migrate. - **Backward compatibility:** not applicable. - **Risk:** **Low** (net-new, not migration). - **Effort:** Small to Medium, whenever built (transactional email for one form submission is a contained scope). - **Endpoint classification:** **New endpoint**, Future, not designed. ### Audit Logs - **Current:** Documented in `BACKEND.md` §5.14 as a proposed convention (structured audit-log entries with actor/action/target/timestamp) — no backend implementation confirmed to exist; this is itself already Planned/Future in the base backend doc, independent of Seller Management. - **Future:** any seller-specific action (seller created, activated, suspended, branding changed) should flow through the same audit-log convention once it exists, with an added seller-scope dimension on the log entry — not a parallel logging system. - **Migration strategy:** none specific to Seller Management beyond ensuring, whenever audit logging is built, that its schema includes an optional seller-scope field from day one rather than retrofitting it later. - **Backward compatibility:** not applicable — nothing exists yet to preserve. - **Risk:** **Low.** - **Effort:** None additional if sequenced correctly (add the field when audit logging is first built); Medium if audit logging ships first without it and needs a schema migration later. - **Endpoint classification:** **New endpoint** (audit log query surface, if one is ever exposed to admins) — Future, not designed. ## Database changes No schema exists yet for any of this (§8 — no backend implemented at all). Recommendations, not decisions: - A `sellers` table (id, marketplace_id, name, slug, status, branding JSON/columns, timestamps) — new table, no impact on existing schema. - A nullable `seller_id` foreign key on `products`/`items` and `orders` (or order-items, pending the Unified/Split decision) — nullable by design, defaulting existing rows to `NULL` (marketplace-owned). This is the one schema change that touches existing tables; every other addition is a new table. - A nullable `seller_id`/`owner_id` on the media-assets table, if Media ownership scoping is pursued. - No column should ever be added as `NOT NULL` without a default — that would force a backfill migration on existing data, which this plan's constraint explicitly rules out. ## Permission changes - New `sellers` and `seller_users` (or equivalent) authorization checks — entirely new policy, not a modification of existing `AdminRole` checks (which, per the Backoffice audit, don't enforce anything yet regardless). - Existing admin/customer authorization paths: **no change** required for marketplaces with `modules.sellerManagement.enabled = false`. - Recommendation: implement seller-scoped authorization as an additional policy layer evaluated only when a request resolves to a seller scope (§11.5) — never as a modification of the existing marketplace-level policy evaluation path. ## Caching - Bootstrap responses are already cached client-side per tenant (§1.7); a seller-scoped bootstrap response should be cached under a cache key that includes the resolved seller identity (e.g. keyed by full resolved Host, which it already effectively is), not the marketplace alone — otherwise a seller subdomain risks serving a cached marketplace-level response or vice versa. - No existing cache invalidation logic needs to change for marketplaces without sellers. ## Indexes - `sellers(marketplace_id)` — every seller lookup is scoped by marketplace. - `products(seller_id)` / `orders(seller_id)` (or order-items equivalent) — nullable-column indexes; most databases index `NULL` efficiently, but this should be verified against the chosen database engine before assuming query performance is unaffected for the common (`NULL`) case. - No index changes required on any table for marketplaces that never populate `seller_id`. ## Security - Seller-scoped data access is a new cross-tenant-adjacent boundary (a seller must never see another seller's data within the same marketplace) — recommend treating this with the same rigor as tenant isolation (§5.10/§4 §10), not as a lesser internal boundary. - The existing frozen payment flow (ADR-010) must not be reopened for the Unified Order path — only the Split Orders path (if chosen) touches payment security surface at all, and that touch should get the same security review rigor as the original payment implementation. ## Performance - Analytics is already the heaviest computation path in the app (full client-side aggregation over the entire corpus, §3.20) — building the real aggregation endpoint this plan's Analytics section calls for should happen with seller-partitioning in mind from the start, to avoid a second heavy migration shortly after the first. - No performance regression is anticipated for marketplaces without sellers — every proposed change is either a new table/endpoint (zero cost when unused) or a nullable-field filter (negligible cost when the filter is never applied). ## API Versioning Already documented as an open, undecided item in `BACKEND.md` §2.10 (no path/header versioning scheme exists today). Recommendation specific to this migration: whichever versioning decision is made generally should land **before** any Seller Management endpoint ships, so new endpoints (Seller CRUD, Activation, Invitations, Branding, Analytics, Dashboard) are versioned consistently with the rest of the API from their first day, rather than being retrofitted later as the one inconsistent set. ## Migration order Strict dependency order, not a preference: 1. **Bootstrap** (already prepared, lowest risk) — backend starts populating `modules`/`seller` fields for a resolved seller scope. 2. **Products** — `seller_id` column + filter, the foundation every downstream domain depends on. 3. **Categories** — confirm no change needed (likely true, low effort to verify). 4. **Media** — independent, can happen in parallel with Products. 5. **Reviews** — depends on Products (join through product ownership). 6. **Orders** — the Unified-vs-Split-Orders decision must be made here, informed by Products already existing. This is the hard gate — nothing past this point should start before it's resolved. 7. **Payments** — only touched if Split Orders is chosen; otherwise untouched, in parallel with everything above. 8. **Transactions** — depends on Orders. 9. **Analytics** — depends on Orders, Products, Reviews all being scoped; also depends on the pre-existing (non-seller-specific) real aggregation endpoint being built first. 10. **Authentication/Authorization** — the `SellerPermissionRole` design and its relationship to `AdminRole` should be settled in parallel with steps 2-6, not deferred to the end, since Seller Activation and any real seller-facing endpoint depend on it. 11. **Seller CRUD/Activation/Invitations/Branding** — depends on Authentication/Authorization being settled. 12. **Seller Dashboard/Analytics endpoints** — last, depends on Analytics' real aggregation endpoint existing. 13. **Notifications/Emails/Audit Logs** — can start any time after step 11 (Seller Invitations existing gives them something to notify about); not blocking anything else. 14. **CMS, Builder, Settings** — no migration anticipated; revisit only if product strategy changes. ## Recommended implementation phases - **Phase A — Foundation** (steps 1-4, 10 above): bootstrap population, Products/Categories/Media scoping, and the permission-role design done in parallel. Nothing customer-visible yet. Lowest risk, unblocks everything else. - **Phase B — The hard decision** (step 6, Orders): Unified-vs-Split-Orders locked before any real seller can transact. This phase is a design decision plus its implementation, not a feature — treat it as a gate, not a sprint item. - **Phase C — Dependent domains** (steps 5, 7, 8): Reviews, Payments (if Split chosen), Transactions — mechanical once Phase B lands. - **Phase D — Seller-facing surface** (step 11): Seller CRUD, Activation, Invitations, Branding — the first point where a seller account can actually exist and do something. - **Phase E — Analytics & reporting** (steps 9, 12): the largest single remaining lift, deliberately last since it depends on everything above. - **Phase F — Operational polish** (step 13): Notifications, Emails, Audit Logs — improves the experience of Phase D/E but blocks nothing. At every phase boundary: re-verify the non-negotiable constraint. A marketplace with `modules.sellerManagement.enabled = false` must show zero behavioral difference before and after each phase ships. If a phase can't satisfy that, it isn't ready to ship — regardless of how much of it is "done."