Skip to main content

Reviews Engine (Real-Estate MVP) — Expected Output Spec

✅ STATUS: APPROVED — 2026-06-24, founder (John Moelker).

The five §1 decisions are confirmed on their RECOMMENDED DEFAULTS (owner- pressed sends; no unguarded cron; operator-marked auto-stop; real-estate only; 24-month EBR window; bundled pricing). Build phase = the owner-facing engine (multi-platform targets, create-request, CSV import, suppression/unsubscribe, CASL footer, funnel dashboard, owner-pressed sends with DRY-RUN by default); the automated multi-touch cadence cron is DEFERRED to a separate, guarded follow-up. Sends to real clients remain founder/owner-gated (house rule) and dry-run by default. The Phase-2 schema is already applied — verify columns via information_schema; no new migration in this phase (Rule #18 still applies).

Sourced from the seven Reviews Engine design docs (all read-only research, 2026-06-24): REVIEWS_ENGINE_PLAN.md, REVIEWS_ENGINE_ARCH.md, REVIEWS_ENGINE_DATAMODEL.md, REVIEWS_ENGINE_VERTICALS.md, REVIEWS_ENGINE_COMPLIANCE.md, REVIEWS_ENGINE_INFRA_MAP.md, REVIEWS_ENGINE_PLATFORMS.md. Every "exists today" claim traces to the infra map's verified information_schema reads; every compliance rule traces to the compliance/platforms docs' named sources.


0. Scope — what this spec covers, and what it does NOT

This spec covers the Real-Estate MVP of the Reviews Engine: an owner-operated tool inside the WiseAI Agency dashboard (wiseaiagency.com/business/[token]) that nudges a real-estate agent's own past and recent clients — by email (SMS deferred, §1.3) — to leave a review on the platforms AI search engines cite (Google Business Profile primary; RankMyAgent → Realtor.ca secondary), with a multi-touch reminder cadence that auto-stops when the client reviews or opts out, a working CASL opt-out spine, and a review-funnel dashboard.

~70% already exists in the production local_business_* platform (Lane 7) — a single-shot, email-first, Google-only, owner-pressed "Review Request" flow with a non-gating /r/[token] private-feedback surface. This MVP is an extension of that, not a rebuild. This spec is the sibling of ai-front-desk.md and local-business-photos-branding.md (the approved voice/chatbot + media specs for the same dashboard) and does not re-specify the dashboard shell, leads inbox, module gating, brand chrome, or the local_businesses lifecycle — those are assumed and cross-referenced.

Real Estate is the FIRST vertical of a portable engine. The ARCH/DATAMODEL docs design a vertical-agnostic CORE (cadence, suppression, tracking, consent gate, dashboard) + a thin per-vertical ADAPTER (platform registry, copy, consent windows, industry rules). This spec accepts that boundary but only specifies the real-estate adapter; other verticals are explicitly out of scope (§7) and several are legally GATED (§5.7).

Covered here (NEW for real-estate MVP)Assumed / cross-referenced (already specced)
Multi-platform review targets (Google + RankMyAgent)/business/[token] dashboard shell + tabs (ai-front-desk.md, photos spec)
Owner-approved batch + multi-touch auto-stopping cadenceLeads inbox + pipeline, owner notifications mechanics
CASL EBR consent gate + client opt-out/suppression spine/r/[token] private-feedback page (non-gating — kept as-is)
Past-client import (last 24 months, EBR-logged)sendLocalBusinessMessage transport (email/SMS, dry-run, audit)
Review-funnel dashboard (sent → clicked → reviewed)reviews module entitlement gating (modules.ts)
Extended click-redirect allowlist (Google already covered)Brand chrome / host-aliasing on wiseaiagency.com

These are the open product decisions (PLAN §6, COMPLIANCE §4) that gate the build. Each carries a recommended default the spec is written against. The founder must confirm or change each before this spec is approved. Until then, the spec text below assumes the defaults; if the founder changes one, the affected sections change with it.

Owner-approved batches. A human at the business (the agent/owner) presses Send for each batch and each cadence run. NO unguarded cron may auto-send to real clients. This is the house rule that never lifts (feedback_never_cron_send_campaigns, feedback_db_writes_ok_sends_need_blessing): DB writes by the engine (drafting requests, importing, scheduling, suppression) are fine without a human; sends to real people require a human press.

Concretely (PLAN §6.2 option b): the cadence engine drafts each touch and computes its due time, but a scheduled reminder is only dispatched after the owner has approved the batch it belongs to (and any global / per-business kill switch is off). A cron may surface due reminders for owner approval and may dispatch only owner-approved, consent-checked, suppression-checked, rate-limited, auto-stopping touches — it may never originate an unapproved send. No review send ever routes through the cold-outreach machinery (/api/cron/outreach-send, leads/email_unsubscribers/campaign_members).

Operator-marked "reviewed" for MVP. The owner clicks a "they reviewed us" control (or the request is marked reviewed manually); Google Business Profile API auto-detection is deferred to a later phase. For MVP, clicked is a soft signal (logged, optionally slows cadence) and reviewed / opted_out are the hard stops. Any transition to a terminal state (reviewed, private_feedback, cancelled, failed) or an opt-out nulls next_reminder_at. The reviewed_detected_via column records provenance (manual / owner_confirmed / clicked_proxy / gbp_api) so adding GBP auto-detection later is a logic change, not a migration.

Real Estate only for v1. Veterinary is the next adapter (low legal risk — CVO no-pressure mode). Dental / medical are deferred pending legal review (RCDSO/CPSO testimonial bans + PHIPA/HIPAA — COMPLIANCE §3.2–3.3, §3.6). Funeral is GATED (bereavement delay + BAO — §5.7). A vertical whose posture is unset fails closed (BLOCKED) — never "allowed."

Bundled in the pilot; metering deferred. The Reviews Engine is included for pilot real-estate tenants (it is the Local Authority package's review value per ai-front-desk.md §8). No per-send pricing, no usage meter, no paywall in the MVP. A send-volume cap exists only as an abuse/cost guardrail (§4.2 — reuse the existing 100/business/day limit), not as a billing meter.

Last 24 months (CASL EBR), owner-approved batch. Past clients who purchased/leased (or signed a written contract) within 24 months, and inquirers within 6 months, are eligible under CASL implied consent. Every imported row must carry an ebr_date (or a stated basis) — no date, no send (burden of proof on sender). Reactivation runs as an owner-approved batch (Decision 1), and every message carries sender ID + a current mailing address + a working unsubscribe (CASL, §5.1–5.2). Contacts outside the window import as non-sendable (review/manual only).


2. The AI-Bridge / honesty anchor (applies to every behaviour below)

Per knowledge/architecture/ai-bridge-principle.md and the engine's compliance spine, three honesty rules sit above every behaviour:

  • No gating, ever. The same public review ask goes to every eligible client — no pre-screen, no happy→public / unhappy→private routing. The existing /r/[token] private-feedback surface is compliant only because it never gates who is asked for a public review (it never shows a public-review link conditioned on rating). The build must NOT add sentiment routing around it. (Google policy + FTC 16 CFR Part 465, penalties up to $51,744/violation.)
  • No incentives. The engine never offers a reward/discount/gift/entry for leaving, changing, or removing a review. No incentive field exists in the data model — do not add one.
  • No fake / no cross-copy. The engine only ever sends a client a link to write their own review; it never drafts, pre-fills, submits, or warehouses review content. It never auto-replicates review text onto Rate-My-Agent (that platform penalizes cross-platform copying). A reviewer-initiated "also share on Google" nudge to the same client is allowed (RankMyAgent facilitates this).

3. User states (which state an expected output applies to)

StateConditionEngine behaviour
Eligible client, consent validebr_date inside window, not suppressed, business activeFull cadence (§4.1–4.4)
Consent lapsedebr_date outside the EBR window at send timeNo send — request stays non-sendable; importer marks it skipped (§4.5, §5.3)
Suppressed clienton local_business_client_suppressions (email/sms/all)No send — skipped before every touch; persists across batches (§4.4)
Reviewed / opted-outterminal state reachedCadence auto-stops; next_reminder_at nulled (§4.3)
Cadence pausedcadence_paused = true (manual hold)No further touches until resumed
Batch not yet approveddrafts staged, owner has not pressed SendNo send — drafts visible to owner; nothing dispatched (Decision 1)
Paused / cancelled businesslocal_businesses.status ∈ {paused, cancelled}Engine read-only; no sends; dashboard editors read-only (consistent with local-business-platform.md)

Demo tenant (real-estate): Terry & Sheri Real Estate (local_businesses.id = 00000000-0000-4000-a000-000000000c01, slug terry-and-sheri, vertical real_estate). The reviews module must be enabled on it for the tab to render. It has 0 review requests today; MVP seeds real-estate demo content + platform targets (Google + RankMyAgent). NEVER use a real customer tenant as a write target (feedback_never_modify_customer_data).


4. Expected outputs — the engine behaviours

4.1 — Owner sets up review targets (multi-platform)

State: business has the reviews module; owner opens the Setup tab.

Should see (the owner):

  • A multi-platform targets editor that captures, for real estate: a Google Place ID (primary, priority 1) and a RankMyAgent request-review link (secondary, priority 2); optionally Rate-My-Agent and Facebook. Each target has enabled + priority.
  • The existing single google_review_url is migrated into a platform:'google' target as the seed (DATAMODEL §6 step 8) — no owner re-entry required.
  • A note that for Google the engine stores the Place ID (never expires) and builds https://search.google.com/local/writereview?placeid=<id> at send time.
  • A RankMyAgent compliance note: reviewer-initiated Google cross-post is OK; the engine never copies review text.

Should NOT see:

  • A free-text box that accepts arbitrary review hosts not on the allowlist.
  • Any "auto-copy this review to other platforms" control (cross-copy is banned).
  • Rate-My-Agent presented as a place the engine will replicate text to.

Data-model touchpoints (NEW — migrate-before-use):

  • local_business_review_targets (one row per (business_id, platform), UNIQUE (business_id, platform)); or the lighter jsonb-on-setup-profile path (founder decision PLAN §6.4; recommend the table). platform is a soft-validated text key (google, rankmyagent, rate_my_agent, facebook, …) — a loose CHECK guards typos; the authoritative per-vertical catalog lives in code.

Success criteria:

  • A real-estate tenant has at least a Google target (seeded) and can add a RankMyAgent target; both render with correct priority. Verified on the demo tenant.

4.2 — Click-redirect allowlist covers the platforms (blocking)

State: a client taps a tracked review link.

Expected behaviour:

  • Every platform link 302s through /api/local-business/review-requests/track/click to the platform's write-review URL — it does not 400.
  • Google hosts (search.google.com, g.page, maps.google.com) are already in ALLOWED_HOSTS — do not re-add. The build adds: rankmyagent.com, rate-my-agent.com (hyphenated), realtor.ca, facebook.com (+ future-vertical hosts only when those verticals onboard).
  • Naming trap (blocking): add rate-my-agent.com (hyphenated, the Canadian platform). Never add ratemyagent.com (un-hyphenated = an unrelated Australian/US company). The hyphen is load-bearing.
  • A click advances sent → clicked and logs an inbound event (existing behaviour).

Verification gate:

  • Each new platform link verified to 302 (not 400) on the real production host (wiseaiagency.com — host-aliasing applies; preview hosts skip the hostname-rewrite middleware, feedback_preview_host_hides_middleware_rewrites).

4.3 — Multi-touch cadence + auto-stop (owner-approved)

State: an eligible, consent-valid, non-suppressed client is enrolled and the owner has approved the batch (Decision 1).

Should happen (the client receives):

  • An initial review request email pointing to the priority-1 target (Google), with a tracked one-tap link.
  • If no review/opt-out occurs, a gentler reminder after a business-day delay, and a second reminder after a further delay. Default cadence: initial → +3 business days → +7 business days, max 3 touches [RECOMMENDED DEFAULT — founder to confirm; PLAN §6.5]. Per-business override via cadence_steps_days / max_touches.
  • Reminders land on business days (reuse addBusinessDays in followup-rules.ts).

Auto-stop (the hard guarantee):

  • The cadence stops immediately and next_reminder_at is nulled when the request reaches any terminal state — reviewed, private_feedback, cancelled, failedor the client opts out (opted_out_at set), or cadence_step >= max_touches, or cadence_paused = true.
  • For MVP, reviewed is set by operator/owner action [RECOMMENDED DEFAULT — Decision 2]; reviewed_detected_via records how. GBP auto-detection is a later phase and requires no migration to add.

Should NOT happen:

  • A reminder sent after the client reviewed or opted out (auto-stop must fire).
  • A send to a client whose EBR window has lapsed since enrollment — canSend() re-checks consent + suppression before EVERY touch (a client may lapse or opt out mid-cadence).
  • Any touch dispatched for a batch the owner has not approved (Decision 1).
  • A reminder routed through the cold-outreach send path.
  • INBOX_COMPOSE_DRY_RUN ignored — dry-run must short-circuit and still audit.

Data-model touchpoints (NEW — migrate-before-use, on local_business_review_requests):

  • target_id, cadence_step, max_touches, next_reminder_at, last_reminder_at, cadence_paused, reviewed_detected_via.
  • No new status value is strictly required (cadence drives off columns); a dedicated terminal opted_out status is optional and, if chosen, must widen the verified status CHECK and update statuses.ts nextReviewRequestStatuses in the same change (grep callers first).

Success criteria:

  • A demo enrollment runs initial → reminder_1 → reminder_2, and stops the instant the request is marked reviewed or opted-out — verified by observing next_reminder_at nulling and the message log showing no further touch (sample state at ≥2 timepoints; behaviour, not DOM presence).

4.4 — Client opt-out / suppression spine (CASL)

State: a client clicks unsubscribe (email) or replies STOP (SMS, when SMS is enabled in a later phase).

Should happen (the client):

  • A working, free, single-step unsubscribe link in every email (and a STOP keyword for SMS) immediately suppresses them; an idempotent confirmation page is shown.
  • The opt-out is honored within 10 business days (standard immediately; the engine honors on receipt).

Data-model touchpoints (NEW — migrate-before-use):

  • local_business_client_suppressions — business-scoped (email/sms/all), partial-unique on (business_id, lower(email)) and (business_id, phone). This is the client-side spine — deliberately separate from the cold-outreach email_unsubscribers/leads/campaign_members tables (reuse the pattern, not the tables). On local_business_review_requests: opted_out_at, unsubscribe_token (UNIQUE, issued lazily at send like feedback_token).
  • A Resend bounce/complaint on a review send should add the contact to suppressions (wire local_business_message_logs.provider_message_id into /api/webhooks/resend/events — later phase; no schema change).

Should NOT happen:

  • A suppressed client receiving any further touch (checked before every send).
  • An opt-out that does not persist across future batches.
  • The unsubscribe link pointing at a churchwiseai.com default — every link MUST use the campaign's own property host (wiseaiagency.com) and resolve on the real host (feedback_outreach_links_per_property_host).

Success criteria:

  • A simulated unsubscribe writes a suppression row, the confirmation page is idempotent, and the next cadence tick skips that client — verified end-to-end on the real host.

4.5 — Past-client import + reactivation (24-month EBR)

State: the owner uploads a past-client list (CSV at MVP: name, email, phone, transaction/inquiry date).

Should happen (the owner):

  • Each row is validated: ebr_date required (else rejected — no date, no send); ebr_basis derived (purchase_24mo from a transaction date); consent window checked (≤24 months purchase / ≤6 months inquiry) — lapsed → skipped (imported as non-sendable, not sent). Email lower-cased, phone E.164-normalized; de-duped against existing leads + suppressions.
  • A summary returns to the owner: { imported, skipped, rejected } with a skip-reason histogram ({duplicate, no_ebr_date, suppressed, lapsed}).
  • Imported, in-window contacts land as draft review requests (carrying ebr_basis + ebr_date + import_id); they are not sent until the owner approves the batch (Decision 1).

Data-model touchpoints (NEW — migrate-before-use):

  • local_business_client_imports (batch provenance + counts); local_business_review_requests.import_id, ebr_basis, ebr_date. consent_valid is computed at send time, never stored (storing it goes stale and violates burden-of-proof).

Should NOT happen:

  • A row without an ebr_date becoming sendable.
  • An out-of-window contact enqueued for sending.
  • An import auto-sending anything (Decision 1 — owner presses Send).

Success criteria:

  • A test CSV with mixed rows (valid / no-date / lapsed / duplicate) imports with correct accept/skip/reject counts; valid rows are staged as drafts and nothing is sent until approval. Verified against the demo tenant only.

4.6 — Review-funnel dashboard

State: owner opens the Review Requests tab / Monthly Report.

Should see (the owner):

  • A funnel per platform: sent → clicked → reviewed / private-feedback, with velocity, layered onto the existing tab and Monthly Report panel.
  • Honest metrics only: real sent / clicked / reviewed / private-feedback / bounced counts. Never "Opened/Delivered" theater presented as engagement; clicked is the real engagement signal. Un-provisioned/empty state reads honestly (e.g. "No requests yet"), not fabricated numbers.
  • SMS is shown as "coming soon" until the SMS sender + SMS-consent path lands (§1.3 / later phase) — email-first MVP.

Should NOT see:

  • Fabricated counts for a business that has sent nothing.
  • A "review text / star rating over time" panel built on the Places API (Places content may not be cached/stored — only Place IDs; persistent rating history needs the owned-location Business Profile API, later phase).

Success criteria:

  • After a demo run, the funnel reflects the real local_business_message_logs / request statuses for the period.

5. Compliance guardrails the build MUST enforce (not bolted on)

A build that violates any of these is not done. Sources: REVIEWS_ENGINE_COMPLIANCE.md, REVIEWS_ENGINE_PLATFORMS.md. [CORE] = one global rule for every vertical; [CFG] = per-vertical setting (unset → fail-closed/BLOCKED).

Every request carries ebr_basis + a concrete ebr_date; consent_valid is computed at send time (24mo purchase / 6mo inquiry / 24mo written contract); no date or lapsed window → no send. Burden of proof is on the sender — the date is the audit record. Importer requires it per row (§4.5).

5.2 CASL message form on EVERY send — [CORE]

Every email (and SMS, when enabled) carries: (a) sender identification (business name), (b) a current mailing address valid ≥60 days, (c) a clear/free/working unsubscribe (STOP for SMS). A send with no resolvable mailing address is blocked. Opt-outs honored within 10 business days. Which address (brokerage vs agency) is per-business [CFG] (PLAN §6.8 — founder to confirm the source); defaults to the business address.

5.3 Treat every review message as a regulated commercial message — [CORE]

Comply as if it is a CASL CEM / CAN-SPAM commercial message (full ID + address + opt-out + consent basis). No "transactional exemption" path. A CASL-compliant send is automatically CAN-SPAM-compliant on consent.

5.4 NO review gating — [CORE]

Same public ask to every eligible client; no sentiment routing; /r/[token] never gates the public ask. (Google policy + FTC 16 CFR Part 465.) The dominant funeral review tool's happy/unhappy "smart routing" is exactly what we must NOT build.

5.5 NO incentives; NO fake/self-authored/cross-copied reviews — [CORE]

No reward tied to a review (no incentive field in the model). Engine never drafts/ pre-fills/submits review content. Never auto-copy text to Rate-My-Agent [CFG, real-estate]; reviewer-initiated RankMyAgent→Google cross-post nudge OK.

5.6 Google Places API storage constraint — [CORE]

Never cache/store Places-API review text/ratings; store only Place IDs; persistent rating history (later phase) comes from the owned-location Business Profile API.

5.7 Per-vertical testimonial posture — [CFG], fail-closed

Real-estate (LIVE this MVP): solicit = allowed; republish in own ads = allowed only with client consent + RECO registrant-ID footer + CREA material-connection disclosure. MVP collects only — it does NOT republish testimonials into the business's own marketing; that path is out of scope (§7). GATED verticals (dental, medical, funeral) must NOT be enabled in production until their open legal question is cleared by counsel + a knowledge/acceptance/ spec exists. An unset posture defaults to BLOCKED (Decision 3).

5.8 Owner-gated sends; never the cold machinery — [CORE]

Sends to real clients are founder/owner-gated (Decision 1). The cadence sender's hard guards (all non-negotiable): consent-checked + suppression-checked

  • gating-safe + rate-limited (reuse 100/business/day) + auto-stop + DRY_RUN-respected
  • audited, on a dedicated authenticated path — never /api/cron/outreach-send, the cold tables, or any cold-campaign code. A founder kill switch disables the cadence globally + per-business.

6. Referenced data-model changes (NOT yet applied — migrate-before-use)

All NEW columns/tables below are PROPOSAL ONLY (DATAMODEL §6 carries the ⛔ DO NOT APPLY YET SQL). Before ANY CREATE TABLE/ALTER TABLE: grep every repo for callers of the affected tables/columns, document coverage in the migration header (Rule #18 / never-migrate-before-audit), and apply founder-gated. Migrations land in churchwiseai-web/migrations/ (AI-product feature). Re-verify every EXISTING column via information_schema.columns at build time.

ObjectStatusPurpose
local_business_review_targetsNEW tableone row per (business_id, platform); UNIQUE(business_id, platform); seed from setup_profiles.google_review_url
local_business_review_requests.target_idNEW colwhich platform the ask points at (FK → targets)
local_business_review_requests.{cadence_step,max_touches,next_reminder_at,last_reminder_at,cadence_paused}NEW colscadence machinery
local_business_review_requests.reviewed_detected_viaNEW colauto-stop provenance (manual/gbp_api/clicked_proxy/owner_confirmed)
local_business_review_requests.{ebr_basis,ebr_date,opted_out_at,unsubscribe_token,import_id}NEW colsCASL consent + opt-out + import provenance
local_business_review_settings (or fold into setup_profiles)NEW table (optional)cadence_steps_days, max_touches, casl_sender_name, casl_mailing_address, sms_from_number
local_business_client_suppressionsNEW tablebusiness-scoped opt-out spine
local_business_client_importsNEW tablepast-client import provenance + counts
local_business_message_logsEXISTING — no schema changereuse metadata jsonb for {cadence_step, platform, target_id, event} + provider_message_id
track/click ALLOWED_HOSTSEXISTING — extend (code)add RankMyAgent / rate-my-agent.com / realtor.ca / facebook.com (NOT ratemyagent.com)

Optional flagged widenings (only if the founder picks them; each touches a verified CHECK + has code callers — grep + update code in the same change): a dedicated terminal opted_out status (else drive opt-out off opted_out_at); a dedicated import value on local_business_leads.source (else map to manual).


7. OUT OF SCOPE (explicit)

The MVP does NOT include, and the build must not silently add:

  • Any vertical other than Real Estate. Vet is next; dental/medical deferred pending legal review; funeral GATED (Decision 3, §5.7).
  • Unguarded cron auto-send to real clients. Sends are owner-approved batches only (Decision 1). No new cron may originate an unapproved send.
  • GBP / Realtor.ca review auto-detection. Auto-stop is operator-marked for MVP (Decision 2); GBP API v4 detection is a later phase.
  • SMS to clients (this MVP is email-first). SMS needs a per-business sender DID for non-voice tenants + a recorded SMS-consent basis (TCPA prior-express-written-consent zone for US) + an inbound STOP webhook — all later phases. The UI shows SMS as "coming soon."
  • Republishing testimonials in the agent's own ads/site. MVP collects only; any republish path needs the RECO/CREA consent + registrant-ID + material-connection flow (§5.7) and a separate spec.
  • Pricing / metering / paywall. Bundled in the pilot (Decision 4).
  • Persistent review-text / rating-over-time warehousing. Places API content may not be stored (§5.6).
  • CRM (non-CSV) imports. CSV only at MVP; CRM connectors are later.
  • Multi-location targets (location_id) and the Resend-webhook → suppression wiring (later phase).

8. Acceptance test stubs (Stage 3 — test.fixme() skeletons, created on approval)

Evidence is behavioural verification on the deployed wiseaiagency.com host (real host, not preview) plus DB row inspection — never "build passes." Run only against the demo tenant (…0c01).

// Targets + allowlist
test.fixme('Google target is seeded from google_review_url; RankMyAgent target can be added', () => {});
test.fixme('every platform review link 302s through track/click (not 400) on wiseaiagency.com', () => {});
test.fixme('rate-my-agent.com is allowlisted; ratemyagent.com (un-hyphenated) is NOT', () => {});

// Consent + cadence + auto-stop
test.fixme('a request with no ebr_date cannot be sent', () => {});
test.fixme('cadence runs initial → +3bd → +7bd (max 3) only after owner approves the batch', () => {});
test.fixme('marking reviewed/opted-out nulls next_reminder_at — no further touch fires', () => {});
test.fixme('canSend re-checks consent + suppression before EVERY touch (lapsed mid-cadence → skipped)', () => {});

// Opt-out / suppression
test.fixme('unsubscribe writes a business-scoped suppression row; confirmation page idempotent', () => {});
test.fixme('suppressed client is skipped on the next cadence tick; persists across batches', () => {});
test.fixme('unsubscribe link uses the wiseaiagency.com host and resolves on the real host', () => {});

// Import
test.fixme('CSV import: ebr_date required (reject), lapsed/duplicate skipped, valid staged as draft (not sent)', () => {});

// Compliance / guardrails
test.fixme('every email carries sender ID + mailing address + working unsubscribe; no-address send blocked', () => {});
test.fixme('no review send routes through the cold-outreach cron/tables', () => {});
test.fixme('founder kill switch disables the cadence globally + per-business', () => {});

// Dashboard
test.fixme('review funnel shows real sent/clicked/reviewed counts; no fabricated numbers; SMS shows "coming soon"', () => {});

9. Guardrails for agents building against this spec (after approval)

  • Rule #17 is NOT satisfied yet. Do not start the customer-facing build until the founder approves this spec AND confirms the five decisions in §1.
  • Migrate-before-use. All NEW columns/tables (§6) need a founder-gated migration with caller-grep first. A column is not "used" until it exists. The DATAMODEL SQL is PROPOSAL ONLY.
  • Sends to real clients are the gate that never lifts. DB writes (drafting, importing, scheduling, suppression) by the engine are fine; a human presses Send. No cron may originate an unapproved send (feedback_never_cron_send_campaigns, feedback_db_writes_ok_sends_need_blessing).
  • Verify on the real host. wiseaiagency.com host-aliasing + any new email/API path must be in middleware SHARED_API_PREFIXES; preview hosts hide rewrites. Links must use the property's own host, never the churchwiseai.com default (feedback_outreach_links_per_property_host).
  • Evidence-or-nothing. Behavioural verification (sample state at ≥2 timepoints and assert change) + DB rows; never claim done on a green build alone.
  • Knowledge-sync (Rule #16). When the build modifies any file referenced here or in the infra map, update the doc in the same PR; run pnpm derive --check.
  • Honest metrics only (Clicks/Replies/Reviewed/real bounce — never Delivered/Opened theater), per the outbound process map.
  • If code diverges from this spec, the code is wrong — update the spec first (founder approval), then the code.

End of spec. STATUS: APPROVED (2026-06-24, founder). Five §1 decisions confirmed on recommended defaults. Sends to real clients remain owner-gated and dry-run by default. Build phase = owner-facing engine; the auto-cadence cron is deferred to a separate guarded follow-up.