# ShipFlow weekly lifecycle digest (Part 3, option B)

**Live:** https://shipflow-crm-digest.pages.dev/digest/ · **Submission page:** https://shipflow-crm-digest.pages.dev/

Every Monday the CRM lead needs one note: what changed, what is broken, and who does what this week. This tool writes that note from the week's funnel and email data. **Code computes every number, Claude writes the note, and code checks every number in the note before anyone sees it.**

## What it does

1. **Load.** The assignment workbook (8 weekly cohorts) is built in. You can upload a new week as `.xlsx` with the same two sheets (`Trial_Cohorts`, `Campaign_Performance`) and columns; `data/sample_week9_synthetic.xlsx` is a made-up week 9 for trying it (labeled as synthetic inside the file). Uploads are parsed in the browser; raw rows never leave it.
2. **Compute (no AI).** `engine.js` computes, for the chosen week against a trailing 4-week baseline:
   - the four Monday KPIs from the deck: trial-to-paid by cohort, 48-hour activation (proxy for the planned 72-hour KPI), incremental lift (marked "needs the holdout": not measurable in this data), and list health (unsubscribes and spam per 1,000, plus billing-to-CRM reconciliation pass/fail);
   - every rate by source, platform and campaign;
   - rule alerts: email attribution missing for a week, spam above Gmail's 0.10% guideline (critical at 0.30%), unsubscribe rates at 1% or more or doubling, source volume shifts of 30% or more, trial-to-paid drops, platform activation drops, launch or promo blasts sent to trials, and the standing "onboarding sends by date, not by state" check. Recurring problems carry a "weeks in a row" count.
3. **Write (Claude).** Only the computed facts (ids, labels, formatted values, alerts) are sent to a Cloudflare Pages Function, which calls `claude-opus-5-5` with structured JSON output. The model prioritizes and writes: headline, what's broken, what changed, 3–5 actions with one owner each, and what the data cannot tell. The prompt and the schema are in `public/digest/prompt.js` and shown on the page.
4. **Check (code).** `guard.js` extracts every number from the model's text and requires it to exist in the facts (and every cited fact id to exist). It runs in the server function and again in the page. If it fails, the AI text is discarded and the rule-based digest is shown with the reason.

## Guardrails

| Risk | Guardrail |
|---|---|
| Invented or miscalculated numbers | The model never computes; the number guard rejects any number not in the facts (server and page). |
| Leaking the API key | The key is a Cloudflare encrypted secret, read only by the server function. |
| Cost or abuse | Results are cached by a hash of the facts (same week, no new call); a daily cap on live calls (`DAILY_CAP`, default 40); a spend limit on the API workspace. |
| API down, cap reached, refusal | Server-side refusal fallback to another model; otherwise the page shows a recorded run (labeled, re-checked) or the rule-based digest. |
| Bad input | Missing sheets or columns stop the run; impossible funnels (import > connect, clicks > opens, paid split not adding up) are flagged as data issues. |
| Data problems read as performance | The prompt and the rules report a tracking gap as a data issue for the Data owner, never as a performance drop. |

## Files

```
public/index.html            submission page (deck, tool, recording, notes)
public/digest/index.html     the tool
public/digest/engine.js      deterministic metrics, baseline, rules, rule-based digest
public/digest/guard.js       number guard
public/digest/prompt.js      system prompt, output schema, model id (shared by page and server)
public/digest/app.js         page logic: week picker, upload, live call, fallbacks, copy as text
public/digest/data/          assignment data as JSON + synthetic sample week 9
public/digest/recorded/      one recorded live run per assignment week (fallback only)
functions/api/digest.js      server: validation, cache, daily cap, Claude call, guard
functions/api/health.js      server: key configured yes/no, live calls today
tests/engine.test.mjs        checks the engine against the numbers verified for the deck
tools/build_data.py          builds data/ from the assignment workbook
tools/record.mjs             records one live run per week for the fallback
```

## Run it

```bash
npm install
npm test                          # engine + guard checks
npx wrangler pages dev            # local: http://localhost:8788/digest/ (rule-based digest without a key)
npx wrangler pages deploy         # deploy; set ANTHROPIC_API_KEY as a secret in the Cloudflare project
```

## Time saved, scale, limits

Pulling the numbers from the ESP and the product data, comparing them with past weeks and writing the Monday note by hand takes a good part of a CRM manager's morning; this produces a checked draft in about 30 seconds, so the time goes into acting on it. In a real team it would read the warehouse or Braze Currents export on a Monday schedule, post to Slack and email, and keep its thresholds in a config the CRM team owns, so a new check (holdout lift, dunning) is a new rule, not a new prompt. Its main limitation is that it only sees what the rules look for: a problem no rule covers is missed, and the number check stops invented figures but not a wrong emphasis, so a person still reads it before it goes out.

ShipFlow is fictional; all numbers come from the assignment workbook. Built with Claude Code.
