
- Claude Code
- Codex
- Copilot
- +1
API Contract Reviewer
Catches breaking REST changes in a PR before merge: diff vs spec, severity table, migration notes.
13 results

Catches breaking REST changes in a PR before merge: diff vs spec, severity table, migration notes.
Project: fresh `npm init -y` + `npm i -D vitest typescript` (Vitest 5.1.0, TypeScript strict). Every runner output below is pasted from the actual run.
Behaviour list (simplest first): 1. returns empty for blank input · 2. returns the address trimmed and lowercased when valid · 3. returns malformed when there is no @ · 4. returns malformed when there is more than one @ · 5. returns malformed when the domain has no dot · 6. returns too_long when the address is over 254 characters
Cycle 1: returns empty for blank input
```
[TDD] cycle 1/6 step RED behaviour: returns empty for blank input
test: src/validateEmail.test.ts > validateEmail > returns empty for blank input
ran: npx vitest run src/validateEmail.test.ts -t "returns empty for blank input"
result: FAIL Error: Cannot find module './validateEmail' imported from <project>/src/validateEmail.test.ts
reason: right (the unit under test does not exist yet: missing function, not a typo in the test)
changed: src/validateEmail.test.ts (+8, new file)
next: minimum code in src/validateEmail.ts
```
```
[TDD] cycle 1/6 step GREEN behaviour: returns empty for blank input
ran: npx vitest run src/validateEmail.test.ts -t "returns empty for blank input" --reporter=verbose; npx vitest run src/validateEmail.test.ts
result: PASS Tests 1 passed (1)
changed: src/validateEmail.ts (+9, new file: result types + hard-coded `{ ok: false, error: 'empty' }`)
next: refactor
```
Refactor: no refactor (hard-coded return is forced into the general version by cycle 2).
[Cycles 2–6 follow the same shape: each RED shows a real assertion mismatch such as `Tests 1 failed | 1 skipped (2)`, each GREEN the passing count, with refactors in cycles 5 and 6.]
Done
```
[TDD] DONE cycles 6 tests added 6 suite PASS
ran: npm test (vitest run) and npx tsc -p .
result: Test Files 1 passed (1) | Tests 6 passed (6); tsc exit 0
pre-existing failures: none (no tests existed)
found, not fixed: the 254 boundary itself (exactly 254 chars accepted) has no test; it passes today, so a test would be green on first run. 'a@.com' and 'a@b.' are accepted because the spec only asks for a dot in the domain.
```
No-fake-green check before every GREEN: no `.skip`/`.todo`, no assertion weakened, no test-only branch, nothing mocked.Use it to make your coding agent build every change test-first: one failing test, the minimum code, a refactor, and a status block after each step.

Your agent interviews you about a plan until nothing is assumed, then writes the spec and tracer-bullet tickets.

Strips AI tells from a draft without inventing a new voice: quoted flags, a clean rewrite, a second-pass check.
BRAND BRIEF — Northwood Beard Oil
1. Positioning
We help men 28–45 with full beards stop smelling like drugstore musk. We can do this because a third-generation perfumer blends 14 natural notes other beard brands will not pay for.
2. Audience snapshot
- Situation: has worn the same $9 oil for years; wife or colleague has commented on the smell
- Trigger: a wedding, a new job, or the bottle running out
- Tried: "premium" brands that still lean on synthetic musk
- Fears: smelling like a teenager's locker; paying $40 for marketing
3. Promise
A scent people ask about, not one they step away from — within the first week.
4. Voice rules
Do: short declaratives ("Three notes. Zero musk.") / Don't: "elevate your grooming ritual"
...
6. Instruction for future prompts
Use this brief. Never use "elevate", "curated", "ritual". Always name a specific scent note.
Skeptic re-read: "Third-generation perfumer" is doing all the work — if it is not provable on the site, the positioning collapses to "nice smell". Holds only with proof.The brief you paste on top of every marketing prompt for a year: positioning, audience, promise, voice.
Built. Here is what exists now: - /today — the screen learners live on: today's lesson card, a 7-day streak strip, and a trial banner counting down from 5 days with one coral "Keep my streak" button that routes to /upgrade. - /login — magic-link sign-in by email, no password. - /settings — sign-out only. Data model (created before UI, as asked): users(id, email, created_at, trial_ends_at, plan) lessons(id, day_number, title, duration_min) completions(id, user_id, lesson_id, completed_at) Streak is computed from completions, not stored. Assumptions: one lesson per day; streak resets after a missed day; trial length fixed at 5 days. Left out on purpose: analytics, onboarding tour, dark mode, admin. One question before the next screen: should a learner on day 6 with no plan still see today's lesson, or only the upgrade page?
Who, the one job, the data, the feel — and an app that looks like yours on the first generation.
Node list
1. Stripe Trigger — fires on checkout.session.completed — reads nothing upstream.
2. Notion — Create database page in "Customers" — Email ← $json.customer_details.email; Plan ← $json.metadata.plan; Amount ← $json.amount_total / 100; Date ← $json.created.
3. Slack — Post to #revenue — "New paid signup: {email} on {plan} ({amount} USD)" built from the same fields.
Workflow JSON: attached below, importable; credentials referenced as "Stripe account", "Notion workspace", "Slack bot" — create those three in Credentials before activating.
Error path: Error Trigger workflow "Alerts" → Slack #ops with the failing workflow name, the item's customer email, and the error message.
Test plan: in the Stripe Trigger node, click "Listen for test event", then send a test checkout.session.completed from the Stripe dashboard → Notion shows one row with the four fields → Slack shows one message → execution list shows green.
Smallest v1: this is already the smallest version (3 nodes). Add the welcome email as node 4 next week, after you have watched this run for a few days.Five spec lines in; a node list, importable JSON, an error path and a test plan out.
VOICE OF CUSTOMER — GrowNest (30 reviews, App Store, 3 competing apps) Top 5 pains 1. "it was fine in the shop and now it's dropping leaves" (11) 2. "I can't tell if I'm overwatering or underwatering" (9) 3. "every blog says something different" (7) 4. "the reminders nag me but never tell me why" (5) 5. "I've killed three of these already" (4) — EARLY SIGNAL: one reviewer, "I'd honestly given up on plants" Top 5 dream outcomes 1. "finally know what's wrong before it's too late" (8) 2. "it just told me: less water, more light. done." (6) 3. "my monstera actually grew a new leaf" (5) ... Recurring words: dropping, yellowing, overwatering, "no idea", nag, finally, survive, confident, "too late", simple Words to avoid: ecosystem, curated, holistic, "plant parent", elevate
Turn 30+ reviews into a one-page brief of pains, outcomes and objections in the customer's exact words.
Weekly briefing — AI search and research tools, 29 Apr – 6 May 1. ChatGPT Deep Research extended to 30-minute runs with export (OpenAI release notes, 2 May) — Why: your "research faster" lesson shows a 5-minute run; the screenshots are stale. Do: re-capture by Friday. 2. Perplexity Spaces adds shared instructions for teams (Perplexity blog, 30 Apr) — Why: your team-research module assumes personal Spaces. Do: a one-paragraph update. 3. Google AI Mode expands to 12 countries incl. DE and ES (Google blog, 1 May) — Why: 40% of your traffic is DE/ES; organic click-through will move. Do: watch Search Console for two weeks. 4. Anthropic raises Claude file limits (Anthropic docs, 3 May) — Why: your "upload 400 pages" lesson can cite a bigger number. Do: nothing yet. 5. Perplexity reported raising at $18B (The Information, 5 May) — unverified, single source — affects nothing you ship. Safe to ignore: the "AI search is dead" essay — opinion, no data. Quiet item: AI Mode ad formats mentioned in a Google developer note (4 May) — if ads enter AI answers, your CAC assumptions change in Q3.
What changed in your space this week, ranked by what it means for your business — with dates and links.
Subject: the 14-week ramp Sam — on the call you said ramp "is going to kill us by Q2" if the eight new AEs land without a playbook. That line has stayed with me, which is why I am writing rather than waiting. I know a quiet week usually means priorities moved, not that the problem did. Two options: A — we start a two-week pilot with the first cohort on Thursday the 19th; B — we park it until the cohort is hired and I check back the first week of July. Reply "A" or "B" and I will handle the rest. If this is a no, a one-line "not now" is genuinely fine — it lets me stop guessing. — Dana LinkedIn version: Sam — your "ramp will kill us by Q2" line is still on my notepad. Pilot Thursday, or park it till July? A or B is enough.
Re-open a stalled deal by quoting one moment from the call and offering a one-word next step.
Edited draft AI lessons that take 15 minutes a day. Used by 12,000 professionals in 40 countries. Start your free 5-day trial. Change log - "a revolutionary AI-powered learning experience" → "AI lessons that take 15 minutes a day" — "revolutionary" is a tell; 15 minutes is a number the reader can picture - "empowering professionals worldwide to thrive" → "used by 12,000 professionals in 40 countries" — abstraction replaced with proof - "Begin your transformative journey today" → "Start your free 5-day trial" — the actual offer is more persuasive than the metaphor - cut "seamlessly", "cutting-edge", "unlock" — adjectives with no referent - "trusted by teams" → [NEEDS NUMBER] — kept, but it needs a count or a name Weakest remaining line: "40 countries" is impressive only if the reader is outside the US; consider replacing with a named company.
Tighten a draft by a set percentage, swap vague adjectives for numbers, and log each change with its reason.
/imagine a 30-something founder in a sunlit home office, editorial photography, warm golden-hour window light from behind and to the right, shallow depth of field, quietly confident expression, candid moment not posed, subject placed on the left third with clean negative space on the right for a headline, muted cream and royal-blue tones, slight film grain, no text, no logos --ar 16:9 --s 150 --c 10 --v 7 Four-grid read: image 1 and 3 have the subject centred (discard); image 2 has the cleanest right-side space and the light wraps the face without blowing out; image 4 is a near-duplicate of 2 with a busier background. Upscale 2 (Subtle). If the hand on the mug is off, Vary Region the hand only; do not reroll.
A candid, magazine-feel hero image with the subject on the left third and clean space for your headline.
Restating: submitting the sign-up form throws `column "email" does not exist`, and you expected to be redirected to /dashboard. It worked before the profile table was added.
Ranked causes
1. The sign-up handler now inserts into `profiles`, which has no `email` column — the form was pointed at the new table, not `users`. Check: open the insert in `signup.ts`; if the table is `profiles`, confirmed.
2. A migration added `profiles` without `email` while the ORM model still declares it. Check: compare `schema.sql` to the model.
3. Two databases (local vs. hosted) with different schemas. Check: run the migration list against the hosted DB.
Checking #1: `signup.ts` line 22 inserts `{ email, name }` into `profiles`. Confirmed.
Fix (smallest): insert `email` into `users` as before, and only `{ user_id, name }` into `profiles`.
- await db.from("profiles").insert({ email, name })
+ const { data: u } = await db.from("users").insert({ email }).select("id").single()
+ await db.from("profiles").insert({ user_id: u.id, name })
Verify: sign up with a fresh address; you should land on /dashboard and see one row in each table.
Regression risk: any other code that assumed profiles.email — I found none.A bug report the AI can act on: what you did, what happened, what you expected — with a confirm-first rule.