# Mango seed handover

Mango is an approval-first marketing desk for one self-hosted ecommerce store. It turns first-party store and audience signals into a reviewable email/SMS campaign, and it must only send the exact revision an operator approves. The product contract is `CONTRACT.md`; `BASELINE.md` contains the shared security, privacy, and operational clauses; `seed.json` is the catalog manifest.

`CONTRACT.md` contains 78 product clauses: HOME 1, OPER 3, STORE 7, CONT 13, PROP 9, AUD 7, CAMP 11, MSG 11, SEND 9, RESULT 6, and DATA 1. This handover records the present implementation boundary, not contract satisfaction. `DIVERGENCE.md` remains the record of genuine friction with shared seed-spec documents.

## Running product architecture

One Cloudflare Worker serves the public front door, sign-in state, operator SPA, and API. `src/core/app/worker.ts` composes the Hono application; Vite serves the React/TanStack Router client from `src/core/app/client`. Feature-owned UI, routes, services, and repositories live together under `src/core/features/{campaigns,audience,insights,store}`. Drizzle is the only D1 access path in the current spine.

The three core migrations are:

- `migrations/core/0000_source_syncs.sql`
- `migrations/core/0001_operator_and_product_spine.sql`
- `migrations/core/0002_campaign_composition_and_review_snapshots.sql`

They persist operators and sessions; source-sync summaries; provider-capability rows; basic contacts and history; proposals; campaigns, revisions, and steps; checks; results; links; and proposal/campaign review snapshots. `sample-data/initial.json` is parsed and imported through the local bootstrap service into that D1 state. It is not React fixture data.

`pnpm dev` migrates local D1, starts the loopback-only Worker, waits for the exact `/health` response, and invokes the existing token-protected local bootstrap. It then remains the foreground server. The bootstrap reuses the same persistent generated 256-bit token as `pnpm seed:local`, imports idempotently, and never creates a session unless a caller explicitly requests `testSession: true`. `pnpm dev --port N` starts, health-checks, and seeds that same port.

## Required TanStack Start migration before further implementation

**8 September 2026 — owner direction.** TanStack Start is the adopted default, but the runtime described above remains Mango's current Vite/React/TanStack Router implementation. On resuming Mango, the **FIRST** step is to bring this committed main-branch note into any parked implementation worktree and convert through the accepted recipe before further product work: local evidence `/Users/zemaj/.orchestrator/evidence/runeditrun/default-seed-on-tanstack-start-with-the-migration-recipe/MIGRATION.md`; portable author reference [TanStack Start migration recipe](https://github.com/runeditrun/seed-spec/blob/main/TANSTACK-START-MIGRATION.md).

The conversion must preserve Mango's contract, production/local session and bootstrap boundaries (including its explicitly unconfigured magic-link delivery), public unsubscribe/consent/link/webhook route status, declared Hono HTTP routes, D1 migrations and persisted review data, and future Queue/Cron/email/event handlers. Hono remains the owner of declared HTTP contract surfaces; the custom Worker entry retains events and named exports; Start takes selected React document routes and server functions only. Public React documents need SSR HTML or an explicitly allowed invariant prerender; every private server function must re-check the native session before reading data, and `/app` navigation must hydrate and stay client-side. The recipe is accepted through local workerd validation only and makes no remote-deployment claim.

## Interface lineage and clause seams

The three refined prototype surfaces were ported into the Worker app:

- **Decision Desk (`/app`)** came from the former Decision Desk direction. It represents `PROP-001` through `PROP-003`, `PROP-008` through `PROP-009`, and source-status presentation for `STORE-003`.
- **Campaign review (`/app/campaigns/:id/review`)** came from the review direction, now reading and saving the persisted composition. It represents `CAMP-002` through `CAMP-004`, `AUD-001`, `PROP-003`, and display seams for `CAMP-006` and `MSG-001` through `MSG-009`.
- **Scheduled campaign (`/app/campaigns/:id/scheduled`)** came from the scheduled direction with the contract's honest cancellation language. It represents `CAMP-001`, `CAMP-006`, `CAMP-009`, and `CAMP-010`; only cancellation persistence is currently implemented.

The rest was designed fresh in the same product language:

| Surface | Contract seams represented |
| --- | --- |
| `/` owner landing | `HOME-001` |
| `/signin` | Future `BASE-ACCESS-001`; currently an explicit unconfigured magic-link state |
| Campaign list and detail | `CAMP-001`, `RESULT-001`, and review/audience/check summaries |
| Audience and contact detail | Narrow `CONT-002` and `CONT-008` views: independent channel states and basic history |
| Insights | Narrow `RESULT-001` through `RESULT-003`, with `PROP-008`'s no-invented-forecast boundary |
| Store | Status presentation for `STORE-003` through `STORE-006` |
| Settings / More | Narrow `OPER-001` and `MSG-002` presentation; no settings or operator-management mutation yet |

The mapping identifies visible seams, not fulfilled end-to-end clauses. Never represent a UI screen or its seeded state as proof that its full contract clause works.

## Deliberate removals and honest states

The former static prototype was removed after its useful visual decisions were ported. Its code and data were not part of the running app. The removal also eliminates fabrications the product must never revive:

- cross-store recovery, revenue, confidence, or benchmark claims;
- unsupported DNSBL and reputation assertions;
- vague send windows and fictional cancel deadlines;
- simulated-data and simulated-send panels;
- baked preview artwork that diverged from editable revision content; and
- non-persistent prototype mutations.

The current app says what it cannot do. `/signin` says magic-link delivery is unconfigured. Provider capability rows say when commerce, AI, mail, or SMS adapters are missing; proposal generation, approval, and test sends are blocked rather than simulated. Store and Settings show absent source/provider capability explicitly. Insights may truthfully show that there is nothing to graph. Contract-required public prefixes for unsubscribe, consent confirmation, link redirects, and provider webhooks are public but return explicit `501` until their real handlers exist; `/auth/` is neither declared nor implemented.

The local sample is deliberately narrow: three persisted contacts produce three candidates, one `no-eligible-channel` exclusion, two eligible contacts, two email recipient-steps, and one SMS recipient-step. It demonstrates persisted review data, not a source-record/consent-evidence resolver, a frozen candidate set, real recipient-step records, approval, or dispatch.

## Local-session boundary

Production `worker.ts` has no local-bootstrap route or feature import. Local development and tests select the separate `worker.local.ts`, which receives a gitignored 256-bit token generated in `.seed/local-bootstrap-token`. Its route accepts only loopback `Host` values and that token. It seeds through the real services and repositories; the test-only request for a session creates an ordinary persisted operator session. `/api/*` and `/app/*` remain session-gated. The production-entry test proves that supplying a local-looking Host and token to the production entry returns `404`.

## Verification today

`pnpm verify` runs typecheck, a production build, and Playwright. Playwright chooses `PLAYWRIGHT_PORT` (4179 by default), starts a fresh server unless `PLAYWRIGHT_REUSE_EXISTING_SERVER=1` is explicitly set outside CI, and uses the local bootstrap/session boundary described above.

The suite currently contains 22 tests in six specs. Eight are contract-tagged: `HOME-001`, `PROP-003`, `AUD-001`, `PROP-008`, `CAMP-002`, `PROP-005`, `CAMP-004`, and `CAMP-009`. The other 70 product clauses do not yet have tagged tests. There is no shared baseline pack or actual contract-coverage reporting. The non-contract tests check health, session gates, loopback refusal, public-route boundaries, sample import, review snapshot integrity, operator UI behavior, and production bootstrap rejection.

## Backend and release debt

The next backend pass still owes real typed production configuration; magic-link authentication; concrete commerce, AI, mail, and SMS adapters; R2, Queues, Cron, Hono RPC, and the dispatch/reconciliation/outage engine. It must add public unsubscribe/one-click, consent-confirmation, link-redirect, and webhook implementations; source records; consent evidence; phones; suppressions and opt-out hashes; recipient-steps; delivery facts; attribution; audit; idempotency; source-freshness rechecks; frozen candidates; DST validation; sender/link/authentication checks; and the delivery lifecycle.

It also owes extension mounts and examples, load testing, accessibility verification, contract reporting and the shared baseline pack, CI previews, and a real preview/deploy exercise. `setup`, `deploy`, `export`, and `import` do not exist yet and must not be claimed in documentation or release material. `seed.json` limits remain estimates until load-tested, and external adapter prices remain unestimated until concrete providers and unit rates are chosen.

## Rename

- 6 September 2026: renamed from Morrow to Mango, with the repository now `runeditrun/marketing-mango`, following the naming rule — a common object (the mango: picked when ripe, approval-first sending) that carries the product's feeling and echoes Mailchimp. Contract clause IDs, table and column names, and migration files are unchanged.

## Manifest migration

- 7 September 2026: migrated `seed.json` to the shared `category`/`spine`/`replaces` schema in `seeds/base/schema/SEED.schema.json`: added `category: marketing` and `spine` (`marketing` / "campaign over an audience"), added `replaces: [{name: Mailchimp, edition: null}]`, dropped `displayName`, renamed `identity` to `description`, turned `capabilities` from a map into an ordered array of `{id, clauses, description}` with hyphenated slug ids, split each external's dotted `id` (`commerce.store.v1` etc.) into a plain `id` slug plus an `adapters` entry, renamed `whyUnavoidable`/`purpose` to `why`, turned each external's prose `dataLeavingDeployment` into an itemised `data` array, added `required: true` to every external, dropped the non-schema `kind`/`replaceable` external fields, renamed `customFields.entities` to a plain `customFields` array, and renamed `operatingCostEstimate` to `operatingCost`. `supersededBaselineClauses` (CONT-013 superseding BASE-DATA-003) was dropped from the manifest — it has no home in the schema, but the same fact already lives in `CONTRACT.md`'s CONT-013 clause ("Supersedes BASE-DATA-003 only for this opt-out dependency"), so it is not lost. Validates clean against `SEED.schema.json`.
