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, failed — or 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.