[object Object]

← back to Norma Platform

Record campaign browser proof and explicit provider integration gaps

4de7056e3a0ed47697ad0e7691962d5dcf51569c · 2026-09-09 13:11:16 -0700 · Steve Abrams

Files touched

Diff

commit 4de7056e3a0ed47697ad0e7691962d5dcf51569c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 9 13:11:16 2026 -0700

    Record campaign browser proof and explicit provider integration gaps
---
 components/campaigns/CampaignWorkspace.tsx         |   2 +-
 verification/e2e-proof.json                        | 179 ++++++++++++-
 verification/platform/CTA-REPORT.md                |  42 +++
 verification/platform/e2e-proof.json               | 172 ++++++++++++
 verification/platform/openclaw-workspace.json      | 297 +++++++++++++++++++++
 verification/platform/regression-gate.txt          | 154 +++++++++++
 .../platform/screenshots/openclaw-riverside.jpg    | Bin 0 -> 36676 bytes
 7 files changed, 831 insertions(+), 15 deletions(-)

diff --git a/components/campaigns/CampaignWorkspace.tsx b/components/campaigns/CampaignWorkspace.tsx
index 8e1f33b..549dd87 100644
--- a/components/campaigns/CampaignWorkspace.tsx
+++ b/components/campaigns/CampaignWorkspace.tsx
@@ -110,7 +110,7 @@ export default function CampaignWorkspace() {
       if(editor.type==='task'){path=editor.task?'/tasks/'+editor.task.id:'/tasks';method=editor.task?'PATCH':'POST';}
       const res=await (await api(path,{method,body:JSON.stringify(data)})).json();setEditor(null);
       if(editor.type==='campaign'&&!editor.campaign)selectCampaign(res.campaign.id);else await reload();
-    }}/>} 
+    }}/>}
   </div>;
   function renderTasks(){return <><div className={styles.sectionhead}><div><h2>The next good step.</h2><p className={styles.muted}>Organizer tasks and message drafts. “Ready” never sends a message.</p></div><button className={styles.button} disabled={!workspace?.campaigns.length} onClick={()=>setEditor({type:'task'})}><Plus size={16}/> Add follow-up</button></div>{tasks.length?<div className={styles.grid}>{tasks.map(t=><article className={styles.card} key={t.id}><div className={styles.cardhead}><span className={styles.eyebrow}>{t.channel}</span><Badge value={t.status}/></div><h3>{t.title}</h3>{t.supporter_name&&<p><strong>{t.supporter_name}</strong><br/><span className={styles.muted}>{t.supporter_email}</span></p>}{!selected&&<button className={styles.textbutton} onClick={()=>selectCampaign(t.campaign_id)}>View campaign: {t.campaign_title||'Open campaign'} ↗</button>}<p style={{whiteSpace:'pre-wrap'}}>{t.body}</p><DateChip value={t.created_at}/><div className={styles.cardfoot}><button className={styles.textbutton} onClick={()=>setEditor({type:'task',task:t})}>Edit draft</button><button className={styles.textbutton} disabled={busy} onClick={()=>void mutate('/tasks/'+t.id,{status:t.status==='draft'?'ready':t.status==='ready'?'completed':'draft'})}>{t.status==='draft'?'Mark ready':t.status==='ready'?'Mark task complete':'Reopen draft'}</button></div></article>)}</div>:<Empty title="Keep the momentum going"><p>New participation creates a review task automatically. Add your own email draft, event reminder, or volunteer checklist.</p></Empty>}</>;}
 }
diff --git a/verification/e2e-proof.json b/verification/e2e-proof.json
index 7d14396..58fad68 100644
--- a/verification/e2e-proof.json
+++ b/verification/e2e-proof.json
@@ -1,21 +1,172 @@
 {
-  "intent": "Test Norma UX with browser agents",
+  "intent": "Original unified Norma campaign workspace combining petition, pledge, event, volunteer and advocacy journeys; verified local milestone, not complete ActBlue/MoveOn parity",
+  "ticket": "TK-11327-build-unified-norma-campaign-organizing",
   "risk_tier": "R3",
-  "environment": "isolated seeded local service http://127.0.0.1:7411 plus live http://127.0.0.1:7400",
-  "timestamp": "2026-09-09T16:00:00-07:00",
-  "baseline": "isolated seeded instance started with repository test accounts; live norma-email remained healthy",
+  "timestamp": "2026-09-09T20:08:32.082166+00:00",
+  "environment": {
+    "worktree": "/Users/macstudio3/Projects/Norma-platform",
+    "preview": "http://127.0.0.1:7416/campaigns",
+    "database": "sdcc_test",
+    "gate": "http://127.0.0.1:7417",
+    "live_service": "Existing :7400 was not restarted or migrated"
+  },
+  "build": {
+    "source_commit": "841ad4c0bbfa261fe9133cf76382e93c8708cb3b",
+    "next_build_id": "b7vXAy7wsC7WvPgYVxLsg",
+    "note": "Only trailing whitespace and verification documentation changed after tested build."
+  },
+  "baseline": "Existing Norma main 2cd602c; additive migration 026 on sdcc_test only. Seeded admin/staff sessions, no production credentials copied. External send integrations sinked.",
   "commands": [
-    "NORMA_TEST_URL=http://127.0.0.1:7400 npx playwright test tests/e2e --project=chromium"
+    "env SESSION_SECRET=<known-test-secret> DATABASE_URL=postgresql://127.0.0.1:5432/sdcc_test npm run build",
+    "npx tsc --noEmit",
+    "npx eslint <changed application paths>",
+    "bash scripts/campaign-preview.sh",
+    "node tests/campaign-platform.mjs",
+    "npx playwright test --config verification/platform/browser.config.ts",
+    "env GATE_PORT=7417 npm run gate",
+    "env NORMA_TEST_URL=http://127.0.0.1:7417 node tests/campaign-disabled.mjs",
+    "node scripts/seed-campaign-demo.mjs",
+    "OpenClaw dedicated norma-platform profile: seeded test login and Riverside workspace"
   ],
   "assertions": [
-    {"journey":"public /pulse","verdict":"PASS","evidence":"Playwright test 7; no real console errors"},
-    {"journey":"public /pulse/petitions","verdict":"PASS","evidence":"Playwright test 8; no real console errors"},
-    {"journey":"public /pulse/about","verdict":"PASS","evidence":"Playwright test 9; no real console errors"},
-    {"journey":"wrong credentials remain on /login","verdict":"PASS","evidence":"Playwright test 6; visible invalid-credentials error"},
-    {"journey":"admin/staff/pulse authenticated UX on isolated :7411","verdict":"PASS","evidence":"Chromium Playwright tests 1-5 passed; all three roles landed and admin navigated core tabs"},
-    {"journey":"public and authenticated UX on WebKit :7411","verdict":"PARTIAL","evidence":"All documents returned 200 and admin login landed at /; WebKit emitted fetch/RSC errors during client navigation"}
+    {
+      "journey": "Private API rejects anonymous access and malformed/oversized bodies",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Campaign creation and read-back are persisted",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Staff org spoof and foreign campaign IDs are denied",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Five typed actions save and enforce type-specific validation",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Draft is hidden from public but available in authenticated preview",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Explicit sandbox activation makes campaign and actions available",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Petition intake is transactional and idempotent",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Pledges do not touch donation ledger; monthly intent is explicit",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Concurrent RSVP honors capacity and attendance lifecycle",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Shared supporter identity spans volunteer and advocacy actions",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Follow-up drafts save, transition, and remain unsent",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "CSV is tenant scoped and neutralizes spreadsheet formulas",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Cross-origin requests and duplicate hidden fields are rejected",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Pause immediately stops new public intake",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Chromium and WebKit UI creation, all five public forms, success, joined supporters, calendar/CSV downloads, check-in, follow-up editing, full-event alternative, mobile and staff deep link",
+      "verdict": "PASS",
+      "evidence": "platform/browser-results.json; 6 passed, 0 skipped, 0 flaky; screenshots and video artifacts"
+    },
+    {
+      "journey": "Existing regression battery: route/write sweeps, 20 security checks, 35 CRUD checks, 12 authz checks, 9 Chromium UI tests",
+      "verdict": "PASS",
+      "evidence": "platform/regression-gate.txt"
+    },
+    {
+      "journey": "Default activation and public intake off",
+      "verdict": "PASS",
+      "evidence": "platform/publication-results.json"
+    },
+    {
+      "journey": "Compilation and TypeScript",
+      "verdict": "PASS",
+      "evidence": "production build completed0; tsc completed0"
+    },
+    {
+      "journey": "Scoped application lint",
+      "verdict": "PASS",
+      "reason": "0 errors; 3 warnings (existing Sidebar unused constant and 2 React cleanup advisories)"
+    },
+    {
+      "journey": "Real money, subscription billing, message delivery and verified consent",
+      "verdict": "SKIP",
+      "reason": "Not implemented in local milestone; explicit provider/consent work tracked in follow-up tickets"
+    },
+    {
+      "journey": "Browserbase clean remote environment",
+      "verdict": "SKIP",
+      "reason": "Local-only loopback preview is not reachable from cloud; no public tunnel opened"
+    },
+    {
+      "journey": "Exhaustive testing of legacy settings, every theme and every external provider",
+      "verdict": "SKIP",
+      "reason": "Outside campaign change; no claim that every Norma control or competitor capability is verified"
+    },
+    {
+      "journey": "OpenClaw dedicated browser opens populated five-action Riverside campaign",
+      "verdict": "PASS",
+      "evidence": "platform/screenshots/openclaw-riverside.jpg; authenticated through normal test login endpoint"
+    }
   ],
-  "cleanup": "No application data writes; no code changes; test artifacts retained",
-  "verdict": "PARTIAL",
-  "blocker": "WebKit client fetch/RSC failures remain to verify on reachable HTTPS staging; Browserbase cannot reach loopback"
+  "artifacts": {
+    "api": "platform/api-results.json",
+    "browser": "platform/browser-results.json",
+    "screenshots": "platform/screenshots",
+    "videos": "platform/browser-artifacts/**/video.webm (local, gitignored)",
+    "regression": "platform/regression-gate.txt",
+    "publication": "platform/publication-results.json",
+    "demo_ids": "platform/demo.json",
+    "runbook": "../docs/platform/RUNBOOK.md"
+  },
+  "review": {
+    "engineer": "Changed duplicate409 correction accepted and verified",
+    "designer": "Select height and receipt state fixed and browser tested; global toolbar advisory retained by prior user preference",
+    "user": "Contextual followups and full/closed event state fixed and tested",
+    "skeptic": "Export acceptance corrected to all-org export; no full-parity claim",
+    "strategist": "Local milestone accepted; full-parity request remains partial"
+  },
+  "cleanup": "API-owned campaign/action/participation/supporter operational fixtures removed; browser fixtures archived and demo retained. Receipt audit evidence retained. Gate and default-check servers stopped. Preview :7416 and dedicated OpenClaw profile retained. No production mutation, sending, remote push or funds moved.",
+  "followup_tickets": [
+    "TK-11334-norma-verified-consent-and-provider-sand",
+    "TK-11335-norma-payment-provider-and-donation-reco",
+    "TK-11336-norma-shared-contact-history-and-field-o"
+  ],
+  "verdict": "PASS for tested local campaign milestone; PARTIAL for full competitor parity"
 }
diff --git a/verification/platform/CTA-REPORT.md b/verification/platform/CTA-REPORT.md
new file mode 100644
index 0000000..939c2e3
--- /dev/null
+++ b/verification/platform/CTA-REPORT.md
@@ -0,0 +1,42 @@
+# Campaign interaction verification
+
+Tested build: `b7vXAy7wsC7WvPgYVxLsg`; source commit `841ad4c`.
+
+## Verified interaction matrix
+
+| Journey | Chromium | WebKit | Evidence |
+| --- | --- | --- | --- |
+| Admin creates campaign, activates petition, submits, finds supporter and contextual follow-up | PASS | PASS | `browser-results.json`, campaign/receipt screenshots |
+| Staff narrow-screen workspace, keyboard dialog dismissal, shell deep link, 42px organization control | PASS | PASS | mobile screenshots |
+| Create and activate fundraiser, event, volunteer and advocacy forms | PASS | PASS | recorded all-forms browser journey |
+| Monthly pledge entry; no payment claim | PASS | PASS | fundraiser screenshots; API donation count unchanged |
+| RSVP and valid calendar-file download | PASS | PASS | event journey, downloaded VCALENDAR assertion |
+| Full event disabled upfront with alternatives | PASS | PASS | event journey after second RSVP |
+| Volunteer and advocacy participation | PASS | PASS | respective screenshots |
+| One supporter joins four action records; CSV download | PASS | PASS | persisted response and download assertions |
+| Attendance change, follow-up edit and ready transition, archive | PASS | PASS | recorded journey |
+| Console page errors and API 500 responses on tested campaign journeys | None | None | browser assertions |
+
+Recordings are retained locally under `browser-artifacts/**/video.webm`; screenshots are committed under `screenshots/`. Six tests passed, zero retries/flakes/skips. The broader existing Norma gate separately passed its nine Chromium browser journeys plus API/write/security/CRUD/authorization suites.
+
+## Review fixes independently verified
+
+Five separate critics reviewed through Engineer, Designer, User, Skeptic and Strategist lenses and logged their findings on TK-11327. Parent acceptance followed source inspection and updated real API/browser tests.
+
+- Engineer: changed duplicates with fresh submission keys had silently returned success. They now return 409, while identical retries retain one receipt and one follow-up.
+- Designer: WebKit select sizing and stale receipt totals. Selects now use normalized appearance; mobile engine assertions require at least 42px. Success receipts suppress stale progress and refresh authoritative action data.
+- User: repeated follow-ups lacked participant context; full/closed event forms still solicited input. Joined participant/campaign data is now visible on tasks; availability is exposed before RSVP.
+- Skeptic: plan inaccurately promised filtered export. Corrected to the implemented organization-wide export; UI and test agree.
+- Strategist: intake and drafts are a useful foundation, but payment/delivery/legacy-record integration are substantial remaining work. Explicit follow-up tickets preserve that distinction.
+
+The critics disagreed on milestone readiness, not the existence of full parity: initial FIX FIRST concerns were accepted and fixed, conditional local approval was satisfied by the parent tests, and the full-parity verdict remains REVISE. Highest-leverage next milestone: confirmed-consent follow-up through a provider sandbox to a second action.
+
+## Limits, not blanket certification
+
+- This is a campaign-journey matrix, not proof that every control in all of Norma works. Exhaustive permutations of legacy settings, themes, external connections, sort/density persistence and every field validation were not individually clicked.
+- Browserbase was skipped because the preview is loopback-only. No cloud reachability or public tunnel is claimed.
+- The existing global theme/age toolbar remains as previously requested; its unrelated Gmail label/mobile footprint is advisory UX debt.
+- OpenClaw opened a separate `norma-platform` profile for the populated demo because another task reused the shared browser tab. Its login/workspace opening supplements the recorded Chromium/WebKit journeys.
+- Actual money collection, recurring billing, verified consent, delivery, dialing and full competitor parity are not implemented or tested here.
+
+**Verdict: PASS for the named local journeys; PARTIAL for exhaustive CTA and full competitor parity.**
diff --git a/verification/platform/e2e-proof.json b/verification/platform/e2e-proof.json
new file mode 100644
index 0000000..58fad68
--- /dev/null
+++ b/verification/platform/e2e-proof.json
@@ -0,0 +1,172 @@
+{
+  "intent": "Original unified Norma campaign workspace combining petition, pledge, event, volunteer and advocacy journeys; verified local milestone, not complete ActBlue/MoveOn parity",
+  "ticket": "TK-11327-build-unified-norma-campaign-organizing",
+  "risk_tier": "R3",
+  "timestamp": "2026-09-09T20:08:32.082166+00:00",
+  "environment": {
+    "worktree": "/Users/macstudio3/Projects/Norma-platform",
+    "preview": "http://127.0.0.1:7416/campaigns",
+    "database": "sdcc_test",
+    "gate": "http://127.0.0.1:7417",
+    "live_service": "Existing :7400 was not restarted or migrated"
+  },
+  "build": {
+    "source_commit": "841ad4c0bbfa261fe9133cf76382e93c8708cb3b",
+    "next_build_id": "b7vXAy7wsC7WvPgYVxLsg",
+    "note": "Only trailing whitespace and verification documentation changed after tested build."
+  },
+  "baseline": "Existing Norma main 2cd602c; additive migration 026 on sdcc_test only. Seeded admin/staff sessions, no production credentials copied. External send integrations sinked.",
+  "commands": [
+    "env SESSION_SECRET=<known-test-secret> DATABASE_URL=postgresql://127.0.0.1:5432/sdcc_test npm run build",
+    "npx tsc --noEmit",
+    "npx eslint <changed application paths>",
+    "bash scripts/campaign-preview.sh",
+    "node tests/campaign-platform.mjs",
+    "npx playwright test --config verification/platform/browser.config.ts",
+    "env GATE_PORT=7417 npm run gate",
+    "env NORMA_TEST_URL=http://127.0.0.1:7417 node tests/campaign-disabled.mjs",
+    "node scripts/seed-campaign-demo.mjs",
+    "OpenClaw dedicated norma-platform profile: seeded test login and Riverside workspace"
+  ],
+  "assertions": [
+    {
+      "journey": "Private API rejects anonymous access and malformed/oversized bodies",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Campaign creation and read-back are persisted",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Staff org spoof and foreign campaign IDs are denied",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Five typed actions save and enforce type-specific validation",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Draft is hidden from public but available in authenticated preview",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Explicit sandbox activation makes campaign and actions available",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Petition intake is transactional and idempotent",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Pledges do not touch donation ledger; monthly intent is explicit",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Concurrent RSVP honors capacity and attendance lifecycle",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Shared supporter identity spans volunteer and advocacy actions",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Follow-up drafts save, transition, and remain unsent",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "CSV is tenant scoped and neutralizes spreadsheet formulas",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Cross-origin requests and duplicate hidden fields are rejected",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Pause immediately stops new public intake",
+      "verdict": "PASS",
+      "evidence": "platform/api-results.json"
+    },
+    {
+      "journey": "Chromium and WebKit UI creation, all five public forms, success, joined supporters, calendar/CSV downloads, check-in, follow-up editing, full-event alternative, mobile and staff deep link",
+      "verdict": "PASS",
+      "evidence": "platform/browser-results.json; 6 passed, 0 skipped, 0 flaky; screenshots and video artifacts"
+    },
+    {
+      "journey": "Existing regression battery: route/write sweeps, 20 security checks, 35 CRUD checks, 12 authz checks, 9 Chromium UI tests",
+      "verdict": "PASS",
+      "evidence": "platform/regression-gate.txt"
+    },
+    {
+      "journey": "Default activation and public intake off",
+      "verdict": "PASS",
+      "evidence": "platform/publication-results.json"
+    },
+    {
+      "journey": "Compilation and TypeScript",
+      "verdict": "PASS",
+      "evidence": "production build completed0; tsc completed0"
+    },
+    {
+      "journey": "Scoped application lint",
+      "verdict": "PASS",
+      "reason": "0 errors; 3 warnings (existing Sidebar unused constant and 2 React cleanup advisories)"
+    },
+    {
+      "journey": "Real money, subscription billing, message delivery and verified consent",
+      "verdict": "SKIP",
+      "reason": "Not implemented in local milestone; explicit provider/consent work tracked in follow-up tickets"
+    },
+    {
+      "journey": "Browserbase clean remote environment",
+      "verdict": "SKIP",
+      "reason": "Local-only loopback preview is not reachable from cloud; no public tunnel opened"
+    },
+    {
+      "journey": "Exhaustive testing of legacy settings, every theme and every external provider",
+      "verdict": "SKIP",
+      "reason": "Outside campaign change; no claim that every Norma control or competitor capability is verified"
+    },
+    {
+      "journey": "OpenClaw dedicated browser opens populated five-action Riverside campaign",
+      "verdict": "PASS",
+      "evidence": "platform/screenshots/openclaw-riverside.jpg; authenticated through normal test login endpoint"
+    }
+  ],
+  "artifacts": {
+    "api": "platform/api-results.json",
+    "browser": "platform/browser-results.json",
+    "screenshots": "platform/screenshots",
+    "videos": "platform/browser-artifacts/**/video.webm (local, gitignored)",
+    "regression": "platform/regression-gate.txt",
+    "publication": "platform/publication-results.json",
+    "demo_ids": "platform/demo.json",
+    "runbook": "../docs/platform/RUNBOOK.md"
+  },
+  "review": {
+    "engineer": "Changed duplicate409 correction accepted and verified",
+    "designer": "Select height and receipt state fixed and browser tested; global toolbar advisory retained by prior user preference",
+    "user": "Contextual followups and full/closed event state fixed and tested",
+    "skeptic": "Export acceptance corrected to all-org export; no full-parity claim",
+    "strategist": "Local milestone accepted; full-parity request remains partial"
+  },
+  "cleanup": "API-owned campaign/action/participation/supporter operational fixtures removed; browser fixtures archived and demo retained. Receipt audit evidence retained. Gate and default-check servers stopped. Preview :7416 and dedicated OpenClaw profile retained. No production mutation, sending, remote push or funds moved.",
+  "followup_tickets": [
+    "TK-11334-norma-verified-consent-and-provider-sand",
+    "TK-11335-norma-payment-provider-and-donation-reco",
+    "TK-11336-norma-shared-contact-history-and-field-o"
+  ],
+  "verdict": "PASS for tested local campaign milestone; PARTIAL for full competitor parity"
+}
diff --git a/verification/platform/openclaw-workspace.json b/verification/platform/openclaw-workspace.json
new file mode 100644
index 0000000..2df5d34
--- /dev/null
+++ b/verification/platform/openclaw-workspace.json
@@ -0,0 +1,297 @@
+{
+  "ok": true,
+  "format": "ai",
+  "targetId": "4934F80AB6AE463FC96B7C9226D1D744",
+  "url": "http://127.0.0.1:7416/campaigns",
+  "snapshot": "- button \"Previous theme\" [ref=e1]\n- button \"Next theme\" [ref=e2]\n- button \"Random theme\" [ref=e3]\n- combobox \"Pick inbox theme\" [ref=e4]\n- option \"🎨 1 · Classic Dense\" [ref=e5] [selected]\n- option \"🎨 2 · Notion Cards\" [ref=e6]\n- option \"🎨 3 · Linear Keyboard\" [ref=e7]\n- option \"🎨 4 · Apple Mail\" [ref=e8]\n- option \"🎨 5 · Front Conversation\" [ref=e9]\n- option \"🎨 6 · Hey Zero-Distraction\" [ref=e10]\n- option \"🎨 7 · Spotify Dark\" [ref=e11]\n- option \"🎨 8 · Minimal Mono\" [ref=e12]\n- option \"🎨 9 · Newsprint\" [ref=e13]\n- option \"🎨 10 · Glassmorphism\" [ref=e14]\n- option \"🎨 11 · Terminal\" [ref=e15]\n- option \"🎨 12 · Mochi Pastel\" [ref=e16]\n- option \"🎨 13 · High-Contrast A11y\" [ref=e17]\n- option \"🎨 14 · Material 3\" [ref=e18]\n- option \"🎨 15 · Synthwave\" [ref=e19]\n- option \"🎨 16 · Kanban Lanes\" [ref=e20]\n- option \"🎨 17 · Ottoman Tile\" [ref=e21]\n- option \"🎨 18 · Brutalist Concrete\" [ref=e22]\n- option \"🎨 19 · Library Catalog\" [ref=e23]\n- option \"🎨 20 · Wabi-Sabi\" [ref=e24]\n- option \"🎨 21 · Cyberpunk Rain\" [ref=e25]\n- option \"🎨 22 · Bauhaus\" [ref=e26]\n- option \"🎨 23 · Memphis 80s\" [ref=e27]\n- option \"🎨 24 · Vellum Manuscript\" [ref=e28]\n- option \"🎨 25 · Solarpunk\" [ref=e29]\n- option \"🎨 26 · Frutiger Aero\" [ref=e30]\n- option \"🎨 27 · Crossword Grid\" [ref=e31]\n- option \"🎨 28 · 8-Bit Pixel\" [ref=e32]\n- option \"🎨 29 · Watercolor\" [ref=e33]\n- option \"🎨 30 · Sakura\" [ref=e34]\n- option \"🎨 31 · Forest Walk\" [ref=e35]\n- option \"🎨 32 · Botanical Print\" [ref=e36]\n- link \"Gallery ↗\" [ref=e37]\n- button \"30 · Adult\" [ref=e38]\n- slider \"Adjust theme for viewer age\" [ref=e39]\n- link \"N Norma / Campaigns\" [ref=e40]\n- combobox \"Campaign organization\" [ref=e41]\n- option \"Choose an organization\" [ref=e42]\n- option \"AUTHZ-Tenant-B\" [ref=e43]\n- option \"Hacked Org\" [ref=e44]\n- option \"Riverside Community · Sandbox\" [ref=e45] [selected]\n- button \"Refresh workspace\" [ref=e46]\n- button \"Create campaign\" [ref=e47]\n- tab \"Campaigns\" [ref=e48] [selected]\n- tab \"Supporters\" [ref=e49]\n- tab \"Follow-ups\" [ref=e50]\n- tab \"Insights\" [ref=e51]\n- tab \"Connections\" [ref=e52]\n- searchbox \"Search campaigns\" [ref=e53]\n- combobox \"Status\" [ref=e54]\n- option \"All statuses\" [ref=e55] [selected]\n- option \"draft\" [ref=e56]\n- option \"active\" [ref=e57]\n- option \"paused\" [ref=e58]\n- option \"completed\" [ref=e59]\n- option \"archived\" [ref=e60]\n- combobox \"Sort\" [ref=e61]\n- option \"Newest\" [ref=e62] [selected]\n- option \"Oldest\" [ref=e63]\n- option \"Title A–Z\" [ref=e64]\n- option \"Most supporters\" [ref=e65]\n- slider \"Card size\" [ref=e66]\n- button \"Saturday park crew\" [ref=e67]\n- button \"Open campaign ↗\" [ref=e68]\n- button \"A reading room for everyone\" [ref=e69]\n- button \"Open campaign ↗\" [ref=e70] [nth=1]",
+  "refs": {
+    "e1": {
+      "role": "button",
+      "name": "Previous theme"
+    },
+    "e2": {
+      "role": "button",
+      "name": "Next theme"
+    },
+    "e3": {
+      "role": "button",
+      "name": "Random theme"
+    },
+    "e4": {
+      "role": "combobox",
+      "name": "Pick inbox theme"
+    },
+    "e5": {
+      "role": "option",
+      "name": "🎨 1 · Classic Dense"
+    },
+    "e6": {
+      "role": "option",
+      "name": "🎨 2 · Notion Cards"
+    },
+    "e7": {
+      "role": "option",
+      "name": "🎨 3 · Linear Keyboard"
+    },
+    "e8": {
+      "role": "option",
+      "name": "🎨 4 · Apple Mail"
+    },
+    "e9": {
+      "role": "option",
+      "name": "🎨 5 · Front Conversation"
+    },
+    "e10": {
+      "role": "option",
+      "name": "🎨 6 · Hey Zero-Distraction"
+    },
+    "e11": {
+      "role": "option",
+      "name": "🎨 7 · Spotify Dark"
+    },
+    "e12": {
+      "role": "option",
+      "name": "🎨 8 · Minimal Mono"
+    },
+    "e13": {
+      "role": "option",
+      "name": "🎨 9 · Newsprint"
+    },
+    "e14": {
+      "role": "option",
+      "name": "🎨 10 · Glassmorphism"
+    },
+    "e15": {
+      "role": "option",
+      "name": "🎨 11 · Terminal"
+    },
+    "e16": {
+      "role": "option",
+      "name": "🎨 12 · Mochi Pastel"
+    },
+    "e17": {
+      "role": "option",
+      "name": "🎨 13 · High-Contrast A11y"
+    },
+    "e18": {
+      "role": "option",
+      "name": "🎨 14 · Material 3"
+    },
+    "e19": {
+      "role": "option",
+      "name": "🎨 15 · Synthwave"
+    },
+    "e20": {
+      "role": "option",
+      "name": "🎨 16 · Kanban Lanes"
+    },
+    "e21": {
+      "role": "option",
+      "name": "🎨 17 · Ottoman Tile"
+    },
+    "e22": {
+      "role": "option",
+      "name": "🎨 18 · Brutalist Concrete"
+    },
+    "e23": {
+      "role": "option",
+      "name": "🎨 19 · Library Catalog"
+    },
+    "e24": {
+      "role": "option",
+      "name": "🎨 20 · Wabi-Sabi"
+    },
+    "e25": {
+      "role": "option",
+      "name": "🎨 21 · Cyberpunk Rain"
+    },
+    "e26": {
+      "role": "option",
+      "name": "🎨 22 · Bauhaus"
+    },
+    "e27": {
+      "role": "option",
+      "name": "🎨 23 · Memphis 80s"
+    },
+    "e28": {
+      "role": "option",
+      "name": "🎨 24 · Vellum Manuscript"
+    },
+    "e29": {
+      "role": "option",
+      "name": "🎨 25 · Solarpunk"
+    },
+    "e30": {
+      "role": "option",
+      "name": "🎨 26 · Frutiger Aero"
+    },
+    "e31": {
+      "role": "option",
+      "name": "🎨 27 · Crossword Grid"
+    },
+    "e32": {
+      "role": "option",
+      "name": "🎨 28 · 8-Bit Pixel"
+    },
+    "e33": {
+      "role": "option",
+      "name": "🎨 29 · Watercolor"
+    },
+    "e34": {
+      "role": "option",
+      "name": "🎨 30 · Sakura"
+    },
+    "e35": {
+      "role": "option",
+      "name": "🎨 31 · Forest Walk"
+    },
+    "e36": {
+      "role": "option",
+      "name": "🎨 32 · Botanical Print"
+    },
+    "e37": {
+      "role": "link",
+      "name": "Gallery ↗"
+    },
+    "e38": {
+      "role": "button",
+      "name": "30 · Adult"
+    },
+    "e39": {
+      "role": "slider",
+      "name": "Adjust theme for viewer age"
+    },
+    "e40": {
+      "role": "link",
+      "name": "N Norma / Campaigns"
+    },
+    "e41": {
+      "role": "combobox",
+      "name": "Campaign organization"
+    },
+    "e42": {
+      "role": "option",
+      "name": "Choose an organization"
+    },
+    "e43": {
+      "role": "option",
+      "name": "AUTHZ-Tenant-B"
+    },
+    "e44": {
+      "role": "option",
+      "name": "Hacked Org"
+    },
+    "e45": {
+      "role": "option",
+      "name": "Riverside Community · Sandbox"
+    },
+    "e46": {
+      "role": "button",
+      "name": "Refresh workspace"
+    },
+    "e47": {
+      "role": "button",
+      "name": "Create campaign"
+    },
+    "e48": {
+      "role": "tab",
+      "name": "Campaigns"
+    },
+    "e49": {
+      "role": "tab",
+      "name": "Supporters"
+    },
+    "e50": {
+      "role": "tab",
+      "name": "Follow-ups"
+    },
+    "e51": {
+      "role": "tab",
+      "name": "Insights"
+    },
+    "e52": {
+      "role": "tab",
+      "name": "Connections"
+    },
+    "e53": {
+      "role": "searchbox",
+      "name": "Search campaigns"
+    },
+    "e54": {
+      "role": "combobox",
+      "name": "Status"
+    },
+    "e55": {
+      "role": "option",
+      "name": "All statuses"
+    },
+    "e56": {
+      "role": "option",
+      "name": "draft"
+    },
+    "e57": {
+      "role": "option",
+      "name": "active"
+    },
+    "e58": {
+      "role": "option",
+      "name": "paused"
+    },
+    "e59": {
+      "role": "option",
+      "name": "completed"
+    },
+    "e60": {
+      "role": "option",
+      "name": "archived"
+    },
+    "e61": {
+      "role": "combobox",
+      "name": "Sort"
+    },
+    "e62": {
+      "role": "option",
+      "name": "Newest"
+    },
+    "e63": {
+      "role": "option",
+      "name": "Oldest"
+    },
+    "e64": {
+      "role": "option",
+      "name": "Title A–Z"
+    },
+    "e65": {
+      "role": "option",
+      "name": "Most supporters"
+    },
+    "e66": {
+      "role": "slider",
+      "name": "Card size"
+    },
+    "e67": {
+      "role": "button",
+      "name": "Saturday park crew"
+    },
+    "e68": {
+      "role": "button",
+      "name": "Open campaign ↗",
+      "nth": 0
+    },
+    "e69": {
+      "role": "button",
+      "name": "A reading room for everyone"
+    },
+    "e70": {
+      "role": "button",
+      "name": "Open campaign ↗",
+      "nth": 1
+    }
+  },
+  "stats": {
+    "lines": 70,
+    "chars": 2712,
+    "refs": 70,
+    "interactive": 70
+  }
+}
diff --git a/verification/platform/regression-gate.txt b/verification/platform/regression-gate.txt
new file mode 100644
index 0000000..27e90aa
--- /dev/null
+++ b/verification/platform/regression-gate.txt
@@ -0,0 +1,154 @@
+
+> norma@0.1.4 gate
+> bash scripts/predeploy-gate.sh
+
+[gate] starting isolated instance on :7417 (sdcc_test, sends sinked)…
+[gate] instance ready (Ready marker + /login 200×2) — running durable battery…
+──────── api-smoke ────────
+
+=== Norma API smoke — http://127.0.0.1:7417 ===
+routes: 273 total · 197 GET-able · 76 mutate-only (skipped in GET sweep)
+
+swept 197 GET route(s) × {anon, admin}
+
+VISIBILITY CENSUS — 4 GET route(s) answer 200 unauthenticated (audit, not a fail):
+    · /api/auth/session
+    · /api/pulse/petitions
+    · /api/pulse/stats
+    · /api/pulse/topics
+
+PASS — 0 crashes + 0 swallowed query-errors across 197 GET routes.
+
+✔ api-smoke green  (197 swept, 4 public, 0 crashes, 0 swallowed)
+
+[gate] api-smoke: PASS
+──────── security-regression ────────
+
+=== Norma security regression — http://127.0.0.1:7417 ===
+
+  PASS admin login → 200 + role admin + cookie
+  PASS staff login → 200 + role staff
+  PASS pulse login → 200 + role pulse
+  PASS wrong password → 401
+  PASS non-JSON body → 400 (not 500)
+  PASS login page shows no OAuth buttons
+  PASS social /api/social/posts/pending → 403 for pulse
+  PASS social /api/social/analytics/dashboard → 403 for pulse
+  PASS social /api/social/bulk-schedule → 403 for pulse
+  PASS social pending → 200 for admin
+  PASS impersonate DELETE with bogus imp-by → 400 (no escalation)
+  PASS registry → 200 for admin
+  PASS registry → 403 for pulse
+  PASS /api/users → 403 for staff
+  PASS /api/users → 200 for admin
+  PASS petition-sign flood → 429 (rate limited)
+  PASS petition CREATE flood → 429 (rate limited)
+  PASS /api/petitions unauth → not 200 (auth required)
+  PASS brute-forced username → 429 (locked)
+  PASS DIFFERENT username still logs in (no tenant-wide lockout)
+
+=== 20 passed, 0 failed ===
+[gate] security-regression: PASS
+──────── write-smoke ────────
+
+=== Norma write-path smoke — http://127.0.0.1:7417 ===
+219 mutating (route,verb) pair(s) across POST/PUT/PATCH/DELETE
+
+admin org scope: 9e0fca77-3168-4772-bc67-6fbee6901be4
+
+probed 219 unauth · fired 155 as admin (64 external/AI/send routes probed unauth-only)
+
+PASS — every mutation gates unauth (4xx) + no crash on empty body.
+
+✔ write-smoke green  (219 pairs, 0 crashes, 0 unauth-writes)
+
+[gate] write-smoke: PASS
+──────── crud-lifecycle ────────
+
+=== Norma CRUD lifecycle — http://127.0.0.1:7417 ===
+
+organizations id: 0ab4542f-8bfc-4a4d-991b-69024620a278  ·  nonprofit_accounts id: 2b99a35f-c43d-4376-9f57-dd78423226fb
+
+clients
+  PASS clients: create → 2xx + id
+  PASS clients: read-back → 200
+  PASS clients: created first_name persisted
+  PASS clients: update → 2xx
+  PASS clients: update city persisted
+  PASS clients: delete → 2xx
+  PASS clients: gone → 404
+
+donations
+  PASS donations: create → 2xx + id
+  PASS donations: read-back → 200
+  PASS donations: created amount persisted
+  PASS donations: update → 2xx
+  PASS donations: update notes persisted
+  PASS donations: delete → 2xx
+  PASS donations: gone → 404
+
+grants
+  PASS grants: create → 2xx + id
+  PASS grants: read-back → 200
+  PASS grants: created title persisted
+  PASS grants: update → 2xx
+  PASS grants: update notes persisted
+  PASS grants: delete → 2xx
+  PASS grants: gone → 404
+
+library
+  PASS library: create → 2xx + id
+  PASS library: read-back → 200
+  PASS library: created title persisted
+  PASS library: update → 2xx
+  PASS library: update title persisted
+  PASS library: delete → 2xx
+  PASS library: gone → 404
+
+journalists
+  PASS journalists: create → 2xx + id
+  PASS journalists: read-back → 200
+  PASS journalists: created name persisted
+  PASS journalists: update → 2xx
+  PASS journalists: update beat persisted
+  PASS journalists: delete → 2xx
+  PASS journalists: gone → 404
+
+=== 35 passed, 0 failed ===
+
+[gate] crud-lifecycle: PASS
+──────── authz-matrix ────────
+
+=== Norma authz-matrix + tenant-isolation — http://127.0.0.1:7417 ===
+
+tenant A (teststaff): 2b99a35f-c43d-4376-9f57-dd78423226fb
+tenant B (foreign):   485fd4e5-192e-4ff7-bcb0-d3a9a60669c0
+
+ROLE MATRIX — 180 GET + 201 mutation handlers × roles
+  under-restrictions (security): 0 · over-restrictions: 0
+
+TENANT ISOLATION — staff(A) sees A's records, never B's
+
+=== 12 passed, 0 failed ===
+✔ authz + isolation green
+[gate] authz-matrix: PASS
+──────── e2e-playwright ────────
+
+Running 9 tests using 1 worker
+
+(node:45504) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
+(Use `node --trace-warnings ...` to show where the warning was created)
+  ✓  1 [chromium] › tests/e2e/admin.spec.ts:18:5 › admin loads the shell and navigates core tabs with no console errors (1.8s)
+  ✓  2 [chromium] › tests/e2e/auth.spec.ts:35:7 › admin logs in, leaves /login, no console errors (691ms)
+  ✓  3 [chromium] › tests/e2e/auth.spec.ts:35:7 › staff logs in, leaves /login, no console errors (775ms)
+  ✓  4 [chromium] › tests/e2e/auth.spec.ts:35:7 › pulse logs in, leaves /login, no console errors (597ms)
+  ✓  5 [chromium] › tests/e2e/auth.spec.ts:46:5 › pulse tier lands on the pulse surface (510ms)
+  ✓  6 [chromium] › tests/e2e/auth.spec.ts:52:5 › wrong credentials stay on /login and surface an error (2.0s)
+  ✓  7 [chromium] › tests/e2e/pulse.spec.ts:17:7 › public /pulse renders without a login wall or console errors (416ms)
+  ✓  8 [chromium] › tests/e2e/pulse.spec.ts:17:7 › public /pulse/petitions renders without a login wall or console errors (387ms)
+  ✓  9 [chromium] › tests/e2e/pulse.spec.ts:17:7 › public /pulse/about renders without a login wall or console errors (391ms)
+
+  9 passed (8.3s)
+[gate] e2e-playwright: PASS
+════════════════════════════════════════
+[gate] ✅ all suites green — safe to deploy.
diff --git a/verification/platform/screenshots/openclaw-riverside.jpg b/verification/platform/screenshots/openclaw-riverside.jpg
new file mode 100644
index 0000000..1bf3a1f
Binary files /dev/null and b/verification/platform/screenshots/openclaw-riverside.jpg differ

← c2e108f Build unified campaign workspace with five action journeys a  ·  back to Norma Platform  ·  Capture populated campaign preview at desktop width 1e82ed2 →