Run: 018 · Date: 2026-05-06 · Role: persistence layer validation
Overall: migration landed correctly. All 5 new tables, 1 RPC, and 3 views are live in production. The outreach_campaigns rename is clean — zero stale .from('campaigns') references in the codebase. TS types in circuit-breaker.ts, tickets.ts, and handoff.ts all align with production schema. One gap found: Bear's "Days in Stage" feature has no stage_entered_at column on the deals table.
1. Table existence verification (all PASS)
Table / ObjectStatusNotes outreach_campaignsPASSAccessible, no errors. Old campaigns table (21 letter rows) preserved. campaign_targetsPASSFK to outreach_campaigns(id) confirmed in migration SQL. agent_ticketsPASSInsert/delete test succeeded with all columns from tickets.ts. spend_logPASSAll 4 new columns present: model, tool, source_type, pricing_model. feature_metricsPASSAppend-only event log, accessible. increment_campaign_spend RPCPASSReturns empty set for non-existent UUID (correct guard behavior). Targets outreach_campaigns. v_campaign_spendPASSView exists. v_company_spendPASSView exists. v_daily_spendPASSView exists.
2. TS-to-DB column alignment
circuit-breaker.ts → spend_log: PASS. The recordSpend() function inserts 14 columns: cost_usd, api_name, event, purpose, model, tool, source_type, pricing_model, company_id, contact_id, campaign_id, deal_id, feature_key, actor. All 14 exist in the spend_log DDL. The checkBudget() function reads daily_budget_usd, spent_usd, total_budget_usd from outreach_campaigns — all present in the DDL.
tickets.ts → agent_tickets: PASS. The AgentTicket type has 18 fields. The createTicket() insert uses 10 columns: source, severity, tier, title, body, context, feature_key, campaign_id, deal_id, source_log_id, status. All present. The escalateTicket() and resolveTicket() update tier, status, claimed_by, claimed_at, resolved_at, resolution — all present. Insert/delete round-trip confirmed in production.
handoff.ts → multiple tables: PASS.
Reads meeting_notes: meeting_id, promoted_to_deal_id, entity_id, classification — all confirmed present. Reads deals: id, slug — present. Inserts to deals: slug, name, side, stage, vertical, ev_estimate_usd, fee_pct, win_prob_pct, sourced_by, owner, flex_data — all present in deals column list. Updates meeting_notes: promoted_to_deal_id — present. Updates campaign_targets: deal_id, outreach_status — present. Inserts to deal_activity_log: deal_id, kind, headline, body, actor — all confirmed present (columns: id, deal_id, contact_id, kind, headline, body, ref_table, ref_id, actor, at).
3. Orphan references (campaigns rename) CLEAN. Zero hits for .from('campaigns') in src/, apps/, or engine/. The old campaigns table (letter-campaign data) remains accessible and untouched — 1+ rows confirmed. No collision.
4. RPC validation PASS. increment_campaign_spend targets outreach_campaigns per migration line 139: UPDATE outreach_campaigns SET spent_usd = spent_usd + p_amount. RPC call with a non-existent UUID returned empty set (budget guard working). The RPC correctly uses RETURNS SETOF outreach_campaigns.
5. FK integrity
FKStatus campaign_targets.campaign_id → outreach_campaigns(id)DDL line 74: REFERENCES outreach_campaigns(id) spend_log.campaign_id → outreach_campaigns(id)DDL line 164: REFERENCES outreach_campaigns(id) agent_tickets.campaign_id → outreach_campaigns(id)DDL line 121: REFERENCES outreach_campaigns(id)
Note: these are DDL-confirmed. Supabase JS client does not expose FK metadata directly, but the constraint enforcement was implicitly validated by the successful insert tests (nullable FK columns accepted null values, which is correct for optional foreign keys).
6. Intern data contracts
Charlie — Quick Classify (meetings.classify): PASS. The meeting_notes table has a classification column (confirmed via production query — returned null for an unclassified row). Charlie can PATCH this column. Feature def exists at meetings.classify, tier intern, status live.
Bear — Days in Stage (deals.view-pipeline): GAP. The deals table has no stage_entered_at column. Query for that column returned: column deals.stage_entered_at does not exist. Bear can compute days-in-stage only from updated_at (unreliable — any edit resets it) or by scanning deal_activity_log for the most recent kind='stage_change' entry. Neither is a clean contract.
Recommendation: Add stage_entered_at timestamptz DEFAULT now() to deals. Update the stage-change API route to set stage_entered_at = now() on every stage transition. Bear's "Days in Stage" badge then becomes now() - stage_entered_at.
FK constraints were verified from the DDL file, not from live information_schema queries. If the migration was applied with IF NOT EXISTS and a prior version of the table existed without the FK, the constraint might be missing even though the DDL declares it. A SELECT * FROM information_schema.table_constraints query would be definitive but was not run. I did not verify RLS policies on the new tables. The migration does not create any RLS policies, which means all 5 new tables are accessible to any authenticated Supabase client. This is consistent with the service-role-key usage pattern in the codebase, but if any client-side code ever uses the anon key against these tables, rows would be exposed.
Same pattern as runs 009 and 010: the TS types are designed correctly, the migration DDL is correct, but the "last mile" operational column (here: stage_entered_at for Bear's feature) is missing because it was not in the original table spec — it only becomes apparent when an intern feature needs it. The migration ships the infrastructure tables but does not retroactively add columns to existing tables that the intern features will read. This is a recurring gap between "what the migration builds" and "what the feature defs assume exists."
85%. High confidence on table existence, column alignment, and reference cleanup — these were verified with live production queries and insert round-trips. Moderate confidence on FK enforcement (DDL-based, not information_schema-based). The 15% uncertainty is: (a) RLS policies not audited, (b) FK constraints verified from DDL not from live metadata, (c) did not test increment_campaign_spend with a real campaign row to verify the budget guard math end-to-end.
Generated from 018__data-architect.md — do not edit this HTML directly.