TWiT-Ads v2 — Specification & Staged Build Plan
Sources: Legacy System Analysis, Rewrite Considerations, Interview Findings (Lisa + Debi, 2026-07-06). Open items in Follow-up Questions. This is the master spec; each build stage gets its own detailed implementation plan in ~/Projects/twit-ads/Plans/ written just-in-time before execution.
Mission-critical caveat: this system is the revenue workflow of the company. Every stage ships small, is verified before the next begins, and the legacy system keeps running in parallel until Lisa and Debi both sign off on cutover.
1. Product summary
A web-based insertion-order (IO) system for TWiT's ad sales, replacing the 2014 WebForms app. It manages: the sellable catalog → proposals → signed orders → episode ad placement (fair rotation) → ad copy → daily rundown → host fees, with inventory that is mathematically impossible to oversell. Seven staff users in two teams (Sales, Continuity) plus read-only rundown consumers. Numbers/delivery reporting, invoicing (QBO), and client reporting documents remain outside the system in v1.
Design tenets (from the interviews):
- No errors. Enter once, propagate everywhere. Errors cost money (makegoods).
- Proposals hold nothing; signatures hold everything. Inventory is only consumed by finalize.
- Everything sellable lives in the system — episode ads, billboard weeks, newsletters, social posts, banners, one custom line.
- The math is automated — softness/value-add deducts, discounts, net-to-TWiT. "Nobody can do math."
- Familiar screens, decluttered. Same conceptual pages (orders list, order editor, inventory, upcoming schedule, copy, rundown, host fees); everything dead removed; the word "broadcast" gone.
- Fast. Legacy is "just very slow." Server-rendered pages on Postgres with 7 users should feel instant.
2. Users & roles
| Role | People | Can |
|---|---|---|
admin |
Leo | Everything, user management, config |
sales |
Lisa, Ty | Build/edit/finalize orders, view everything, exports |
continuity |
Debi, Sebastian, Veva | Everything sales can, plus copy, rotation, rundowns, host fees, episode moves |
ratecard (flag) |
Lisa, Debi | Edit master rate card & catalog prices (flag on top of role) |
| rundown link | Editors, Micah | Read-only rundown web page via long-random-token URL, no login |
Notes: role-gating is deliberately loose within staff (any proposal builder can override prices per line — the human review step before send-out is the control). User provisioning must handle an arbitrary number of salespeople (more coming). Auth: local username/password, built as the possibly-permanent mechanism — Leo is not certain he wants Google SSO (decide after testing); keep the auth layer pluggable so SSO is an add, not a rework. All authored records carry created_by/updated_by.
3. Domain model
Names in code should follow these; plural snake_case tables, singular Go types.
- show — title, abbrev, active,
max_ads(slots per episode), recurrence rule (weekday(s) + cadence), notes (hosts line for order form). Retired shows stay for history. - episode — show, publish date (date, America/Los_Angeles), record date + time window (the rundown is organized by record day — SN records Tuesdays 1:30–3:30 for a later pub date), TD assignment (technical director shown on the rundown, e.g. "John A."), status (
scheduled|moved|cancelled), original_date when moved. Generated from show recurrence 24 months ahead (next year gets sold mid-year; Debi asked for ≥18, we do 24); safe move/cancel with audit (no temporary-edit-then-revert dance). - advertiser — name (editable), aliases[] (brand names, e.g. Abine→DeleteMe; schedule views display the brand), optional competitor tags (free-form grouping used for warn-only conflict hints — separation remains human judgment), optional contacts (light CRM: name/email/notes — never required), active flag. Deletable only if no finalized orders (deleting cascades its dead proposals after confirm). Manual proposal delete only — no auto-expiry.
- agency — name, commission % (default 15, per Follow-up Questions L3), contacts optional. Direct business = no agency ("TWiT").
- rate_card_entry — versioned by
effective_date, per show: downloads/episode (= impressions), CPM, cost/episode, ad units/episode. Latest ≤ order-creation date wins and the chosen rate is copied onto the order line at booking (legacy trap: latest-always-wins retroactively). Catalog products get rate entries too. - product — the finite catalog (per Lisa 2026-07-06):
midroll(per show, priced on impressions — the episode ad),billboard_week(opening billboard, per week),newsletter(per issue),banner(per impression),social_post(per post),custom(free-text label + flat amount). A new rate card with the final catalog arrives before production — so the product/rate schema must make adding or re-pricing unit types a data change, not a code change. - order — the IO. Fields: number, title (proposal label, e.g. "Option A"), advertiser, agency?, salesperson, status (
pending|finalized|void), quarters+year tags (Q1–4/half/annual — display metadata; real scheduling is the line dates), terms (enum: net_15/30/45/60/75/90, due_upon_receipt), terms text (auto-generated from template + advertiser name, editable), notes,finalized_at/by, presentation toggle (lumped|below_the_line), created/updated audit. No interim tables — a pending order is just a row; concurrent editing safe via optimistic locking (version column). - order_line — order, product, show?, per-unit rate + downloads (copied from rate card, overridable), quantity/dates,
is_value_add(softness flag → contributes $0 and 0 guaranteed impressions), computed line value.episode_adlines carry the selected episode dates (order_line_dates), each date one unit.billboard_week/banner_weeklines carry week-start dates.
- placement — episode × position(1..max_ads) × order_line × ad_copy?. Created at finalize (round-robin next-free position), reorderable by continuity (fair rotation). Unsold slots simply have no placement row (show blank).
- ad_copy — advertiser, per-advertiser copy number (the rundown says "Bitwarden 48", "DeleteMe 8" — continuity's shorthand for which copy version airs), google_doc_id (the Doc stays the authoring surface), per-show promo fields: code / URL / other-text (the legacy
promo_codesconcept), reader constraints (free text; the "DO NOTs" — e.g. Cohesity: "do not mention competitors, specific hacks, criminal groups, or nation-state actors"; empty = "standard"), status, effective window and/or assignment rule (evergreen|date_window|per_episode). Substitution tokens{SHOW_TITLE} {PROMO_CODE} {PROMO_URL} {PROMO_OTHER}resolved at render. - copy_assignment — resolution of which copy runs on which placement: default rule (advertiser+show+date window) with per-placement override. Aired placements keep their historical copy reference forever.
- host / host_fee_rate / episode_hosts — hosts, per-show per-host fee, who-appeared checkmarks per episode, monthly report.
- Rate card source of truth: the real 2026 card (dated 6/16/26) is archived at
~/Projects/twit-ads/reference/ratecard-2026-06-16.pdfand was entered into the system 2026-07-06 (13 shows + 5 products; banner split into Leaderboard $5 / Minibar $8 per 1k impressions). Policy facts from its notes for B-phase: cancellation 30 days; episodic separation only; quarterly minimums $10k agency / $20k direct; one social promo per quarter included with any buy; sponsor-page/RSS presence included; no tracking pixels, UTM tags OK; billboard week $17,450 "includes a 15% discount"; monthly reporting completed end of following month. - terms_template — seeded from the existing language (Lisa 2026-07-06): the due-upon-receipt variant verbatim, plus ONE net template parameterized by day count ({{days}} = 30/45/60/75/90);
{{advertiser}}substitution; "broadcast" stripped. Editable in admin — new language is expected before production, so templates are data, not code. - audit_log — actor, entity, action, before/after JSON for every money-bearing or state-changing mutation (order finalize/void, line changes, rate card, placements, episode moves).
- user / session — local creds (possibly permanent), pluggable SSO; role + ratecard flag.
Historic data (scope change, Leo 2026-07-06): the original "keep nothing" decision is reversed — "I think we will want historic data, so be prepared to import the historic data under the new schema." Consequences baked in now: imported entities carry a nullable legacy_id (+ source table) provenance column; imported historic orders/placements are flagged imported and read-only; the schema export in AIGEN_sql_format_helper/ defines the mapping, but the actual import (stage E0) needs a fresh data dump from the live SQL Server (via Paul/Azure) at import time.
Money rule (Leo, 2026-07-06): TWiT never uses cents — every dollar amount is a whole dollar. Storage stays integer cents (multiples of 100) so intermediate arithmetic is exact, but inputs round to the nearest dollar on parse, every computed amount (line values, deducts, discounts, commissions, net-to-TWiT) rounds to the nearest whole dollar at each step, and displays show whole dollars with thousands commas, right-justified. Impressions are integers with commas. Dates are civil dates in America/Los_Angeles, always displayed MM/DD/YYYY (Leo, 2026-07-06) (episodes have no meaningful time-of-day for sales purposes). No magic sentinels — nullable columns and enums instead of 7/15/1991 and show_id=32.
Order math (the contract)
gross subtotal = Σ all lines at stated value (value-add lines included at full value)
value-add deduct = Σ value-add lines (dollars AND impressions)
revised total = gross subtotal − value-add deduct ← "layered in for softness"
order discount = % or flat $, applied to revised total (money only, never impressions)
final total = revised total − discount
guaranteed impressions = Σ paid-line impressions only
agency commission = final total × agency.commission% (if agency)
net to TWiT = final total − commission
Every quantity in this contract is rounded to the nearest whole dollar at each step (house rule — no cents, ever; percentages like the discount and commission round after applying). Presentation lumped: value-add lines print inline zeroed ($0 / 0 impressions), no deduct block. Presentation below_the_line: lines print at full value with the deduct block beneath. Same stored data, two renderings. This math is pure-function Go with exhaustive table-driven tests — the single most-verified code in the system.
Availability & the sea of red
- An episode's slots =
show.max_ads; sold = count of placements. Availability is computed live; date pickers render white (open) / red (sold out for this proposal's context) / grey (episode full — unclickable). - Pending orders never consume inventory. Finalize is transactional: re-validate every line date against current placements inside one transaction; claim slots or fail with a precise conflict list. Overselling must be impossible by construction (DB unique constraint on episode+position).
- Any pending order containing a date that has since sold out is flagged (red) on the orders list and in the editor, with finalize blocked until amended.
4. Workflows (acceptance narratives)
Each stage's acceptance tests are drawn from these.
W1 — Lisa builds three proposals. New order → advertiser (create inline; later edits its name freely) → agency ARM, salesperson Lisa, title "Recommended" → add Intelligent Machines → date picker shows availability → picks 4 dates → rates auto-fill from card, she bumps CPM down on one line → adds a billboard week + newsletter → marks two IM episodes value-add → 5% order discount → terms net-45 (boilerplate auto-fills with advertiser name) → duplicates the order twice, edits into "Minimum" and "Option B" → prints PDF of each → sends via DocuSign herself. Inventory unchanged. Same dates appear in a competitor's proposal freely.
W2 — Signature race. Client B signs first; Debi finalizes B. Lisa's proposal for client A shares two dates → A's row turns red in the list; opening it shows exactly which lines conflict; finalize refuses until she swaps dates; then finalizes. QBO handoff stays human (Jennifer gets the PDF + emails as today).
W3 — Debi intake from an agency. Builds the inbound IO in-system to check the math against the rate card; system totals expose the $1,000 discrepancy; Slack back-and-forth; stays pending until Lisa nods; finalize.
W4 — Fair rotation. On finalize, placements auto-fill round-robin. Debi opens Upcoming → show view, moves advertisers up/down across the year so nobody eats all last positions. Order close + monthly audit both take minutes.
W5 — Copy lifecycle. New copy Doc from client → continuity creates ad_copy with Doc ID + per-show codes/URLs → assignment rule (evergreen or date-window) → placements resolve copy automatically → morning attention list shows any placement in next N days without copy, or copy missing URL/code. Mid-campaign copy change = new ad_copy + updated assignment going forward; aired episodes keep history.
W6 — Rundown (shape confirmed by Debi's sample, reference/rundown-sample-2026-07-07.png). Keyed to the record day: "Daily Rundown Recorded on Tuesday, 7/7" lists each show recording that day as a block — record time window, show + episode number, TD — with one row per placement in position order: advertiser + copy number ("Bitwarden 48"), promo URL. Billboard reminders print as bold instruction lines above each affected show ("NEEDS RECORDED BLACK HAT BILLBOARD. Placement is immediately after the cold open and before the TWiT.tv network open."). Below the tables: links to each copy Doc ("Security Now — Cohesity 3"). Editors open a stable tokenized URL; Debi hits "copy as text" and pastes to Slack; data change → page reflects it immediately. The system also generates the per-show TD note text she schedules in Slack today (reference/rundown-slack-note-*.png): "@TD —
W7 — Host fees. On the 1st, Sebastian opens last month, checkmarks who hosted each episode (pre-checked from show defaults), runs the report, exports, sends to Lisa + Jennifer.
W8 — Reports. Lisa: inventory % sold for Q3 (traditional calendar) → drill into TWiT to see who's on which episodes. Year-end: sales by advertiser with gross/commission/net-to-TWiT, exported to Sheets/XLSX, reconciled against QBO. Advertiser page: collapsible history, total spend, active years.
5. Screens (familiar-but-clean inventory)
- Orders — list, blue pending / green finalized, red conflict badges, filter by advertiser/status/year, advertiser rollups.
- Order editor — header, lines, date pickers, catalog add, softness/discount blocks, terms, notes; Print view; XLSX export; Duplicate.
- Inventory — % sold per show, arbitrary date range, drill-down to episode/slot occupancy.
- Upcoming schedule — per show, placements per episode, fair-rotation reorder, blank = unsold.
- Ad copy — copy records + per-show promo grid + assignments.
- Rundown — internal view + tokenized read-only page + copy-as-text.
- Attention (new) — morning list: conflicts, missing copy, missing URLs, unassigned upcoming slots.
- Host fees — setup + monthly report.
- Admin — shows/episodes, rate card, catalog, terms templates, users, audit log.
UI conventions: server-rendered, no SPA build step, htmx for in-page interactions (date picker, reorder, live totals), print stylesheets for the order PDF. Layout echoes the legacy screens (labels modernized: "Insertion Order", "New Order"). Every grid exports XLSX/CSV. Multiple tabs/windows always safe.
6. Architecture & stack
- One Go service + PostgreSQL. Business logic in Go (not procs), pure-function core for money/availability with the HTTP/UI layer thin on top.
- Go 1.24+, stdlib
net/http+html/template, htmx (vendored static file),pgxfor Postgres,goosemigrations (embedded),excelizefor XLSX. Minimal deps per house rules; no ORM. - Config: YAML file + env for secrets, fail-all validation at boot. Structured logging (slog). Single binary; static assets embedded.
- Testbed (Phase A–D): docker-compose on Framework — app + postgres, bound to tailnet only. Nightly
pg_dumpto NAS from day one (this is revenue data even in testing). - Production (Phase E) — direction set by Russell, 2026-07-06: AWS EC2 VM + RDS Postgres, fronted by Cloudflare Access (zero-trust proxy). Russell's reasoning: AWS is where the rest of TWiT's critical infrastructure is managed (Azure stays the editors' plane — good management-plane separation). Russell will partner with the coding model directly when Phase E arrives. What this means for the app (all cheap because we're 12-factor):
- Same container image as the testbed;
DATABASE_URLpoints at RDS instead of the compose postgres. RDS gives managed backups/PITR — we keep our ownpg_dump-to-NAS as a second copy anyway. - TLS + authn at the edge via Cloudflare Access. This likely settles the SSO question without in-app OAuth: the app can trust the
Cf-Access-Authenticated-User-EmailJWT header and map it to its user/role records — build the auth layer so "trusted-header identity" is one more pluggable backend beside local login (testbed keeps local login). - The tokenized rundown URL needs a Cloudflare Access bypass rule for its path (editors/hosts have no accounts) — token stays long-random; revisit if Access policies can cover editors instead.
- Design guardrail: nothing in the app may depend on AWS specifics (no SDK calls, no S3 assumptions) — it must still run as plain docker-compose on Framework forever, since that's the dev/test environment.
- Same container image as the testbed;
- Google Drive API (service account) only for reading copy Docs at rundown render (v1 can even just link out; inline preview is a nice-to-have). No Apps Script, no personal accounts, no client-side API keys.
- MCP server over the internal API (Kenobi answering "how full is MacBreak in Q3?") — cheap once the API exists; scheduled for Phase D/E, not core.
Explicitly out of v1 (phase-2 backlog): DocuSign API, Uniblab delivery import (unblocked — Uniblab has a complete API, per Leo 2026-07-06), Spotify impressions (no known automated path; the true-up/gross-up stays manual), client reporting document automation, invoicing/QBO integration, internal commission tracking, CRM depth, AI copy drafting, auto fair-rotation suggestions.
7. Engineering conventions (binding for every stage)
- TDD, red/green (superpowers:test-driven-development) — money math, availability, and finalize get exhaustive table-driven tests before implementation.
- One change at a time — build, test, confirm, next. No batching "for efficiency."
- Every stage ends: all tests green,
docker compose updemo on Framework, stage checklist in the plan file ticked, Leo verifies before the next stage starts (UI stages: Lisa or Debi eyeball it too). - Migrations are forward-only and numbered; schema changes never edit old migrations.
- No secrets in source; testbed secrets via env file, production via the Bitwarden-materialized pattern.
- Audit log entries for every mutation of orders, placements, rate cards, episodes.
- Git repo local + NAS remote (never public GitHub).
8. Staged build plan
Each stage = one to two focused Opus 4.8 sessions with its own just-in-time plan file (Plans/<stage>.md) written from this spec (superpowers:writing-plans), executed with TDD, and verified before proceeding. Stages are ordered so the system is demoable end-to-end as early as possible.
Phase A — Foundation
- A1. Scaffold. Repo/git init, Go module
twitads, config loader (YAML+env, fail-all), docker-compose (app+postgres), goose migrations wiring,/healthz, justfile (test,run,migrate), nightly pg_dump timer. ✅ Verify: fresh clone →just testgreen → compose up → healthz on tailnet. - A2. Auth, users, roles. Local login, bcrypt, sessions (DB-backed), role middleware, ratecard flag, user admin page, audit-log foundation. ✅ Verify: login as each role; forbidden pages 403; audit rows written.
- A3. Shows & episodes. CRUD, recurrence rules, 12-month episode generation, safe move/cancel with audit, admin UI. ✅ Verify: generate 2026–27 episodes for real show lineup; move one episode; no orphaned data.
- A4. Advertisers & agencies. CRUD, editable names, aliases, delete rules, commission %, optional contacts. ✅ Verify: rename an advertiser, see it everywhere; delete blocked with finalized history.
- A5. Rate card & catalog. Versioned entries per show + catalog products, effective dates, ratecard-flag gating, history view. ✅ Verify: enter the real 2026 rate card (Lisa/Debi supply); mid-year revision leaves old orders untouched.
Phase B — Orders (the heart)
- B1. Order math core (no UI). Pure Go: lines, value-add, discount, commission, net-to-TWiT, guaranteed impressions, both presentations; availability calculator. The most-tested code in the repo. ✅ Verify: table-driven tests covering every interview scenario incl. the billboard-week example ($17,450 w/ layered softness).
- B2. Orders list + CRUD. Pending/finalized/void, colors, filters, duplicate-order, optimistic locking (concurrent-edit test!), advertiser rollups. ✅ Verify: two browsers editing different orders simultaneously — zero cross-contamination (the legacy bug).
- B3. Order editor: episode lines. Add show → date picker with white/red/grey availability, multi-date select, per-line rate/download overrides, live totals. ✅ Verify: W1 front half; oversold dates unclickable.
- B4. Order editor: catalog, softness, terms. Billboard weeks, newsletter, social, custom line; value-add marking; discount; terms enum → boilerplate w/ advertiser substitution; notes; presentation toggle. ✅ Verify: W1 complete; totals match B1 goldens on-screen.
- B5. Finalize & sea of red. Transactional finalize, round-robin placement, DB-level oversell impossibility, conflict flagging across pending orders, void/makegood pattern ($0 order), post-finalize lock + role-gated edit. ✅ Verify: W2 race under concurrent load (scripted); constraint holds under fire.
- B6. Order outputs. Print view (print CSS → PDF), XLSX export, orders-list export. ✅ Verify: Lisa prints a real proposal and pronounces it DocuSign-ready.
🚦 Checkpoint 1 (after B6): Lisa builds real proposals in the testbed alongside the legacy system for a week. Feedback folded in before Phase C.
Phase C — Continuity operations
- C1. Upcoming schedule + fair rotation. Per-show placement grid (displays brand aliases), up/down + drag reorder, blank unsold slots, year-view audit. Stretch: a "suggest fair arrangement" button (Debi: useful) — suggestion only, never auto-applies, manual control is sacrosanct. ✅ Verify: Debi rearranges a real month in minutes; W4.
- C2. Ad copy. Copy records, Doc IDs, per-show promo grid, assignment rules (evergreen/window/per-episode), placement resolution, historical copy immutability. ✅ Verify: W5 with the Shopify copy Debi sent.
- C3. Rundown. Record-day view (per W6): show blocks with record window + episode # + TD, placement rows with copy numbers + promo URLs, billboard instruction lines, copy-Doc links; tokenized read-only URL; copy-as-text block; per-show TD-note text with reader constraints. Sample to match:
reference/rundown-sample-2026-07-07.png+ the two Slack-note screenshots (row colors in the sample are pure emphasis, no semantics — Leo 2026-07-06; style similarly, model nothing). ✅ Verify: Debi generates a real day and it says exactly what her hand-built one would. - C4. Attention dashboard. Conflicts, missing copy/URLs/codes, unassigned upcoming slots, competitor-tag warnings (warn-only — separation stays human judgment), (later) URL liveness checks. ✅ Verify: seeded gaps all surface; zero false alarms for a week.
- C5. Host fees. Hosts, rates, episode checkmarks, monthly report + export. ✅ Verify: Sebastian reproduces last month's real report to the penny.
🚦 Checkpoint 2 (after C5): full continuity workflow runs in parallel for one real order end-to-end.
Phase D — Reporting & hardening
- D1. Inventory report. % sold per show, arbitrary ranges, traditional calendar, drill-down. ✅ Verify: matches legacy inventory page for the same data.
- D2. Sales reports. By advertiser/year, gross/commission/net-to-TWiT, advertiser history rollups, XLSX + Sheets-friendly CSV. ✅ Verify: Lisa reconciles a quarter against QBO.
- D3. Hardening. Audit-log UI, backup/restore drill (actually restore!), performance pass, security review (/security-review), multi-window torture test. ✅ Verify: restore from last night's dump on a clean machine.
Phase E — Production
- E0. Legacy historic-data import. Fresh dump from the live SQL Server (Paul/Azure access); ETL mapping legacy tables (SHOWS, EPISODES, Advertisers, bo_header2/bo_lines2/bo_line_dates2, EP_ADS, Advertisements + Promo_Codes, Rates, HF schema) into the new schema with
legacy_idprovenance; historic orders flaggedimported, read-only; the ~⅔ dead tables ignored. Also seeds production reference data (shows, advertisers, current rate card — Lisa/Debi prune). ✅ Verify: row-count reconciliation + Lisa spot-checks known advertisers' history (e.g., 99designs total spend) against the legacy UI. - E1. Parallel run, formally. Both systems get every real order, weekly diff checklist, bug-fix loop. The new rate card + final catalog land here too. Exit criteria: N consecutive weeks with zero material discrepancies and both Lisa & Debi preferring v2.
- E2. Cloud deployment — with Russell. AWS EC2 + RDS + Cloudflare Access per §6 (Russell has offered to work with Opus/Fable directly here). Auth via CF Access trusted headers (or local login if Leo prefers), rundown-path bypass rule, RDS + NAS backups, monitoring/alerts (Telegram), runbook. Legacy system read-only → decommissioned on Lisa's word, not before.
Phase F (backlog, post-cutover)
DocuSign API · Uniblab delivery integration — top phase-2 candidate (Patrick, 2026-07-06: the API can report which advertisers ran on which episodes, and their log-processing analytics could feed delivery data straight into the system; he has "long wanted end-to-end data in a rewritten system." This would replace Debi/Veva's Friday manual Uniblab→spreadsheet copying: sold-vs-delivered per order line, in-system) · client reporting doc automation · invoice generation/QBO · internal commissions · MCP server · AI copy drafting · CRM-lite · Google SSO (if wanted) · scheduled Slack posting of rundown + TD notes (v1 generates the text; Debi pastes) · order checklist engine (Debi's new/existing-advertiser checklists — ~/Projects/twit-ads/reference/checklists.md — as in-app per-order task lists with attention-dashboard nudges) · Elroy integration (push URL/codes/sponsor data — kills today's triple entry; Elroy is the TWiTcast backend and Leo already has API access, so this is unblocked whenever wanted).
External systems map (from Debi's checklists + Leo, 2026-07-06): TwitAds is canon. Elroy = TWiTcast backend (sponsor pages, URL-code queue; API access in hand — phase-2 push target). AFB = not a saleable unit; ignore (checklist references are legacy noise). Lower thirds, banner graphics, and similar are generated independently (Kevin/Anthony; Patrick runs banner serving/reporting) — the system sells the banner line item but never touches graphics or serving. TNM-signed orders forbid direct client contact; host onboarding calls have their own ritual. v1's job in this ecosystem is exactly what the checklists show: finalize (task 1), fair rotation (task 10), promo URL/codes (task 11), copy into the system (tasks 13–14).
9. Risks & mitigations
| Risk | Mitigation |
|---|---|
| Oversell/double-book (the unforgivable) | DB unique constraint + transactional finalize + concurrency tests in B5; property-based test hammering finalize |
| Money math wrong | Pure-function core, golden tests from real orders (B1), Lisa reconciliation (D2) |
| Copy/rundown regression breaks shows | C2–C3 verified against real days; parallel run; legacy stays available |
| Scope creep from wish lists | Phase F backlog is the pressure valve; v1 scope frozen at this spec, changes go through Leo |
| Testbed data loss | pg_dump to NAS nightly from A1; restore drill in D3 |
| Opus session drift across stages | Per-stage plan files + repo CLAUDE.md carry conventions; every stage starts by reading both |