From 6d5cf806883bc81b86d38d6db8633cde3da5b99b Mon Sep 17 00:00:00 2001 From: Govind Kavaturi Date: Wed, 27 May 2026 11:36:01 -0700 Subject: [PATCH] Add 2 tutorials for Vol 18 batch: drift-detection-between-systems + adversarial-scoping --- entries/adversarial-scoping/README.md | 23 + entries/adversarial-scoping/metadata.json | 23 + entries/adversarial-scoping/tutorial.md | 351 ++++++++++++++++ .../drift-detection-between-systems/README.md | 23 + .../metadata.json | 24 ++ .../tutorial.md | 395 ++++++++++++++++++ queue/topics.json | 6 +- 7 files changed, 843 insertions(+), 2 deletions(-) create mode 100644 entries/adversarial-scoping/README.md create mode 100644 entries/adversarial-scoping/metadata.json create mode 100644 entries/adversarial-scoping/tutorial.md create mode 100644 entries/drift-detection-between-systems/README.md create mode 100644 entries/drift-detection-between-systems/metadata.json create mode 100644 entries/drift-detection-between-systems/tutorial.md diff --git a/entries/adversarial-scoping/README.md b/entries/adversarial-scoping/README.md new file mode 100644 index 0000000..fec9db3 --- /dev/null +++ b/entries/adversarial-scoping/README.md @@ -0,0 +1,23 @@ +# Kill a bad product idea before you spend a week building it + +An agent runs six adversarial critique passes in two minutes and surfaces objections a solo founder never asks themselves, where a human review panel takes three days to assemble and half the panel will be polite. + +This is part of [AI Building Tutorials](https://github.com/thebuilderweekly/ai-building-tutorials) by [The Builder Weekly](https://thebuilderweekly.com). + +**Read this tutorial:** +- [In this repo](./tutorial.md) — the raw markdown with code blocks +- [On the web](https://thebuilderweekly.com/tutorials/adversarial-scoping) — rendered with diagrams and syntax highlighting + +## What this tutorial teaches + +**Before:** You have a PRD you're excited about. You start building. Four days in you realize it solves the wrong problem. + +**After:** Before you write any code, an agent runs six adversarial critiques against the PRD and surfaces the three objections that would have killed the idea on day four. + +## Tools used + +anthropic-api + +## Pillar + +[Scoping](https://thebuilderweekly.com/tutorials/pillars/scoping) diff --git a/entries/adversarial-scoping/metadata.json b/entries/adversarial-scoping/metadata.json new file mode 100644 index 0000000..140fabf --- /dev/null +++ b/entries/adversarial-scoping/metadata.json @@ -0,0 +1,23 @@ +{ + "id": "adversarial-scoping", + "title": "Kill a bad product idea before you spend a week building it", + "slug": "adversarial-scoping", + "pillar": "scoping", + "clusterTags": [ + "scoping", + "adversarial-prompts", + "prd-review" + ], + "soulLine": "An agent runs six adversarial critique passes in two minutes and surfaces objections a solo founder never asks themselves, where a human review panel takes three days to assemble and half the panel will be polite.", + "beforeState": "You have a PRD you're excited about. You start building. Four days in you realize it solves the wrong problem.", + "afterState": "Before you write any code, an agent runs six adversarial critiques against the PRD and surfaces the three objections that would have killed the idea on day four.", + "status": "published", + "author": "tbw-ai", + "contributors": [], + "tools": [ + "anthropic-api" + ], + "createdAt": "2026-05-27", + "lastVerifiedAt": "2026-05-27", + "freshnessWindowDays": 90 +} diff --git a/entries/adversarial-scoping/tutorial.md b/entries/adversarial-scoping/tutorial.md new file mode 100644 index 0000000..4a423cc --- /dev/null +++ b/entries/adversarial-scoping/tutorial.md @@ -0,0 +1,351 @@ +## Opening thesis + +You will build a Python script that kills a bad product idea before you spend a week building it. The script feeds a PRD through six adversarial critique passes using the Anthropic API, each pass wearing a different hostile hat. An agent runs six adversarial critique passes in two minutes and surfaces objections a solo founder never asks themselves, where a human review panel takes three days to assemble and half the panel will be polite. The output is a ranked list of the three objections most likely to kill the product idea before any code gets written. + +## Before + +You finished your PRD on Sunday night. It felt sharp. Monday morning you started building: database schema, auth flow, the first API route. By Wednesday you had 40 files. Thursday you showed it to a friend who builds in the same space. She said one sentence: "Your user doesn't have that problem." You sat with it. She was right. Four days of code went into the trash. The worst part is you knew the question existed. You just never asked it, because you were the author and the reviewer at the same time. Nobody plays devil's advocate on their own work. Not honestly. Not at 1 a.m. when the idea feels alive. You needed six skeptics in a room. You had zero. + +## Architecture + +The system is a single Python script that reads a PRD from a local file, sends it through six sequential API calls to Claude, and writes a final verdict file. Each call uses a different adversarial persona encoded in the system prompt. A final synthesis call ranks the objections by severity. + +```text +DIAGRAM: Adversarial PRD Review Pipeline +Caption: Six critique passes feed into one synthesis pass, producing a ranked kill list. +Nodes: +1. prd.md - The raw PRD file on disk +2. critique_runner.py - Python script that orchestrates all API calls +3. Persona 1: Customer Skeptic - Challenges whether the user actually has the stated problem +4. Persona 2: Market Cynic - Challenges whether the market is real and reachable +5. Persona 3: Technical Saboteur - Finds architectural risks the author glossed over +6. Persona 4: Unit Economics Auditor - Asks if the business math works at scale +7. Persona 5: Competitor Analyst - Identifies existing solutions the author ignored +8. Persona 6: Scope Creep Detector - Finds hidden complexity that doubles the timeline +9. Claude API (Anthropic) - The model endpoint processing each persona call +10. synthesis call - One final call that ranks all objections +11. verdict.md - The output file with ranked objections +Flow: +- prd.md is read by critique_runner.py +- critique_runner.py sends prd.md content to Claude API six times, once per persona +- Each persona response is collected into a list +- critique_runner.py sends all six responses to Claude API as a synthesis call +- Synthesis response is written to verdict.md +``` + +## Step-by-step implementation + +### Step 1: Set up the project and install the Anthropic SDK + +Create a directory and install the one dependency. The Anthropic Python SDK wraps the Messages API. Get your API key from https://console.anthropic.com/settings/keys and export it. + +```bash +mkdir adversarial-review && cd adversarial-review +python3 -m venv venv +source venv/bin/activate +pip install anthropic +export ANTHROPIC_API_KEY="sk-ant-...your-key-here" +``` + +### Step 2: Create a sample PRD to test against + +You need a real PRD to feed the system. This sample describes a plausible but flawed idea: an AI meal planner for college students. Save it as `prd.md`. Replace it with your own PRD when you run this for real. + +```bash +cat > prd.md << 'EOF' +# Product Requirements Document: MealMind + +**Problem.** College students eat poorly because they lack time and knowledge to plan meals around a tight budget. + +**Solution.** A generative mobile app that produces weekly meal plans optimized for a student's budget, dietary restrictions, and dorm kitchen constraints. + +**Target user.** US college students aged 18 to 22 living in dorms or off-campus apartments. + +**Core features.** +1. Budget input: user sets weekly grocery budget (default $40). +2. Generated meal plan: 7 days, 3 meals per day. +3. Grocery list export: one-tap list synced to Instacart or Walmart. +4. Dietary filters: vegan, gluten-free, halal, nut-free. +5. Leftover tracking: mark unused ingredients, get next-day recipes. + +**Revenue model.** Freemium. Free tier: 2 meal plans per month. Pro tier: $4.99/month for unlimited plans and grocery sync. + +**Timeline.** MVP in 6 weeks. Solo founder. React Native frontend, Python backend, OpenAI API for generation. + +**Success metric.** 1,000 paying subscribers within 6 months of launch. +EOF +``` + +### Step 3: Define the six adversarial personas + +Each persona is a system prompt that tells Claude to adopt a specific hostile stance. The key design choice: each persona must end with "List your top 3 objections, each with a severity rating of LOW, MEDIUM, or HIGH." This forces structured output. + +```python +# personas.py + +PERSONAS = [ + { + "name": "Customer Skeptic", + "system": ( + "You are a ruthless customer researcher. Your job is to find evidence that the target user " + "does not actually have the stated problem, or that they have it but will never pay to solve it. " + "Assume nothing. Challenge every claim about user behavior. " + "List your top 3 objections, each with a severity rating of LOW, MEDIUM, or HIGH." + ), + }, + { + "name": "Market Cynic", + "system": ( + "You are a market analyst who has seen 500 failed startups. Your job is to find reasons this " + "market is fake, too small, or unreachable. Challenge the TAM, the distribution channel, and " + "the timing. Be specific. " + "List your top 3 objections, each with a severity rating of LOW, MEDIUM, or HIGH." + ), + }, + { + "name": "Technical Saboteur", + "system": ( + "You are a senior engineer who reviews architecture documents for hidden risk. Your job is to " + "find the technical assumptions that will blow up during implementation. Focus on integration " + "complexity, data dependencies, and scaling bottlenecks. " + "List your top 3 objections, each with a severity rating of LOW, MEDIUM, or HIGH." + ), + }, + { + "name": "Unit Economics Auditor", + "system": ( + "You are a finance person who kills products that cannot make money. Your job is to calculate " + "whether the revenue model covers the cost of acquisition, infrastructure, and API calls. " + "Use rough estimates. Show your math. " + "List your top 3 objections, each with a severity rating of LOW, MEDIUM, or HIGH." + ), + }, + { + "name": "Competitor Analyst", + "system": ( + "You are a competitive intelligence analyst. Your job is to name existing products, features, " + "or free alternatives that already solve this problem. If the PRD does not mention competitors, " + "that is itself a red flag. Be specific with product names and URLs where possible. " + "List your top 3 objections, each with a severity rating of LOW, MEDIUM, or HIGH." + ), + }, + { + "name": "Scope Creep Detector", + "system": ( + "You are a project manager who has shipped 50 products. Your job is to find hidden complexity " + "in the feature list that will double the stated timeline. Look for features that sound like " + "one task but are actually five. Challenge the MVP definition. " + "List your top 3 objections, each with a severity rating of LOW, MEDIUM, or HIGH." + ), + }, +] +``` + +### Step 4: Build the critique runner + +This script reads the PRD, loops through all six personas, collects responses, and stores them for synthesis. Each call uses `claude-sonnet-4-20250514` with a max of 1024 tokens. That keeps cost under $0.15 for the full run. + +```python +# critique_runner.py + +import os +import json +import anthropic +from personas import PERSONAS + +client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from env + +def load_prd(path: str) -> str: + with open(path, "r") as f: + return f.read() + +def run_critique(prd_text: str, persona: dict) -> dict: + response = client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=1024, + system=persona["system"], + messages=[ + { + "role": "user", + "content": f"Here is a Product Requirements Document. Critique it.\n\n{prd_text}", + } + ], + ) + text = response.content[0].text + return {"persona": persona["name"], "critique": text} + +def run_all_critiques(prd_path: str) -> list: + prd_text = load_prd(prd_path) + results = [] + for persona in PERSONAS: + print(f"Running critique: {persona['name']}...") + result = run_critique(prd_text, persona) + results.append(result) + return results + +if __name__ == "__main__": + critiques = run_all_critiques("prd.md") + with open("critiques.json", "w") as f: + json.dump(critiques, f, indent=2) + print(f"Saved {len(critiques)} critiques to critiques.json") +``` + +### Step 5: Run the six critiques + +Execute the runner. On a typical PRD this takes 30 to 90 seconds depending on response length. You will see each persona name print as it completes. + +```bash +python critique_runner.py +``` + +### Step 6: Build the synthesis step + +The synthesis call takes all six critiques and asks Claude to rank every objection by severity, then pick the top three. This is the step that turns raw noise into a decision. + +```python +# synthesize.py + +import json +import anthropic + +client = anthropic.Anthropic() + +def load_critiques(path: str) -> list: + with open(path, "r") as f: + return json.load(f) + +def synthesize(critiques: list) -> str: + combined = "" + for c in critiques: + combined += f"### {c['persona']}\n{c['critique']}\n\n" + + response = client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=2048, + system=( + "You are a product decision advisor. You have received six adversarial critiques of a PRD. " + "Your job: read all 18 objections, deduplicate them, rank them by severity, and output " + "the top 3 objections that are most likely to kill the product. For each objection, state " + "the persona that raised it, the objection in one sentence, the severity (HIGH/MEDIUM/LOW), " + "and one concrete action the founder should take before writing any code. " + "Format the output as markdown with numbered items." + ), + messages=[ + { + "role": "user", + "content": f"Here are the six adversarial critiques:\n\n{combined}", + } + ], + ) + return response.content[0].text + +if __name__ == "__main__": + critiques = load_critiques("critiques.json") + verdict = synthesize(critiques) + with open("verdict.md", "w") as f: + f.write("# Adversarial Review Verdict\n\n") + f.write(verdict) + print("Verdict written to verdict.md") +``` + +### Step 7: Run the synthesis + +This produces the final verdict file. Open it and read the three objections. If any of them make you uncomfortable, good. That discomfort on day zero is worth more than the same realization on day four. + +```bash +python synthesize.py +cat verdict.md +``` + +### Step 8: Wire both steps into a single command + +A one-line runner makes this something you actually use instead of something you bookmark and forget. + +```bash +cat > review.sh << 'SCRIPT' +#!/usr/bin/env bash +set -e +echo "Starting adversarial review..." +python critique_runner.py +python synthesize.py +echo "Done. Read verdict.md." +SCRIPT +chmod +x review.sh +./review.sh +``` + +## Breakage + +If you skip the synthesis step, you get 18 objections with no ranking. A solo founder will read all 18, feel overwhelmed, and ignore every one of them. Raw critiques without triage are noise. Noise feels like due diligence but produces no decisions. The founder goes back to building the thing they already wanted to build, now with a vague sense of guilt instead of a concrete action list. The six personas did their jobs. The pipeline failed at the last mile. + +```text +DIAGRAM: Failure Without Synthesis +Caption: Six critiques produce 18 unranked objections that the founder ignores. +Nodes: +1. prd.md - The raw PRD +2. critique_runner.py - Sends PRD through six personas +3. critiques.json - 18 unranked objections +4. Founder's brain - Overwhelmed, defaults to original plan +Flow: +- prd.md feeds into critique_runner.py +- critique_runner.py produces critiques.json with 18 objections +- Founder reads critiques.json, gets overwhelmed +- Founder ignores objections and starts building anyway +``` + +## The fix + +The synthesis step is the fix. It already exists in Step 6 above, but here is the hardened version that also rejects a PRD outright if two or more HIGH severity objections survive synthesis. This version adds a go/no-go signal so the founder does not have to interpret the output. + +```python +# In synthesize.py, replace the __main__ block with this: + +if __name__ == "__main__": + critiques = load_critiques("critiques.json") + verdict = synthesize(critiques) + + high_count = verdict.upper().count("HIGH") + if high_count >= 2: + signal = "NO-GO: Two or more HIGH severity objections found. Do not build until resolved." + elif high_count == 1: + signal = "CAUTION: One HIGH severity objection found. Resolve it before writing code." + else: + signal = "GO: No HIGH severity objections. Proceed with awareness of MEDIUM risks." + + with open("verdict.md", "w") as f: + f.write("# Adversarial Review Verdict\n\n") + f.write(f"## Signal: {signal}\n\n") + f.write(verdict) + + print(f"\n{signal}") + print("Full verdict written to verdict.md") +``` + +## Fixed state + +```text +DIAGRAM: Complete Pipeline With Go/No-Go Signal +Caption: Six critiques feed into synthesis, which produces a ranked verdict with a binary decision signal. +Nodes: +1. prd.md - The raw PRD +2. critique_runner.py - Sends PRD through six personas +3. Claude API - Processes each persona call +4. critiques.json - Six structured critique responses +5. synthesize.py - Deduplicates, ranks, and counts HIGH objections +6. verdict.md - Top 3 objections plus GO, CAUTION, or NO-GO signal +Flow: +- prd.md feeds into critique_runner.py +- critique_runner.py makes six calls to Claude API +- Claude API returns six critiques stored in critiques.json +- synthesize.py reads critiques.json and makes one synthesis call to Claude API +- synthesize.py counts HIGH severity objections and assigns a signal +- Output written to verdict.md with signal and ranked objections +``` + +## After + +You finished your PRD on Sunday night. It felt sharp. Monday morning you ran `./review.sh`. Two minutes later you opened `verdict.md`. The signal said NO-GO. The top objection: "Your target user has a $40/week grocery budget but you are charging $4.99/month for a tool that competes with free recipe apps and TikTok meal prep videos. Customer acquisition cost will exceed lifetime value." The second objection: "Instacart and Walmart integrations each require partner agreements with 4 to 8 week approval cycles, making your 6-week MVP timeline impossible." You sat with it. The objections were right. You rewrote the PRD before lunch. You did not throw away four days of code because there was no code to throw away. + +## Takeaway + +The pattern is adversarial decomposition: split one vague review task into multiple hostile perspectives, run them in parallel, and force a synthesis step that produces a decision, not a discussion. This works for PRDs, architecture docs, pitch decks, and pricing pages. Anything you wrote and plan to act on deserves six enemies before it gets a single friend. diff --git a/entries/drift-detection-between-systems/README.md b/entries/drift-detection-between-systems/README.md new file mode 100644 index 0000000..42c6efe --- /dev/null +++ b/entries/drift-detection-between-systems/README.md @@ -0,0 +1,23 @@ +# Catch drift between two systems of record before it becomes a bug + +An agent reconciles two systems of record on every write with zero fatigue, catching silent divergence within a minute, where a human running a quarterly reconciliation script finds the same drift weeks after it happened. + +This is part of [AI Building Tutorials](https://github.com/thebuilderweekly/ai-building-tutorials) by [The Builder Weekly](https://thebuilderweekly.com). + +**Read this tutorial:** +- [In this repo](./tutorial.md) — the raw markdown with code blocks +- [On the web](https://thebuilderweekly.com/tutorials/drift-detection-between-systems) — rendered with diagrams and syntax highlighting + +## What this tutorial teaches + +**Before:** Your billing system and your CRM drift out of sync. You find out during a quarterly audit and spend a week untangling which source was right. + +**After:** An agent compares both systems on every significant write, surfaces mismatches within a minute, and logs the drift event with both sides' state captured. + +## Tools used + +cueapi, anthropic-api + +## Pillar + +[Accountability](https://thebuilderweekly.com/tutorials/pillars/accountability) diff --git a/entries/drift-detection-between-systems/metadata.json b/entries/drift-detection-between-systems/metadata.json new file mode 100644 index 0000000..91f4a12 --- /dev/null +++ b/entries/drift-detection-between-systems/metadata.json @@ -0,0 +1,24 @@ +{ + "id": "drift-detection-between-systems", + "title": "Catch drift between two systems of record before it becomes a bug", + "slug": "drift-detection-between-systems", + "pillar": "accountability", + "clusterTags": [ + "drift-detection", + "reconciliation", + "consistency" + ], + "soulLine": "An agent reconciles two systems of record on every write with zero fatigue, catching silent divergence within a minute, where a human running a quarterly reconciliation script finds the same drift weeks after it happened.", + "beforeState": "Your billing system and your CRM drift out of sync. You find out during a quarterly audit and spend a week untangling which source was right.", + "afterState": "An agent compares both systems on every significant write, surfaces mismatches within a minute, and logs the drift event with both sides' state captured.", + "status": "published", + "author": "tbw-ai", + "contributors": [], + "tools": [ + "cueapi", + "anthropic-api" + ], + "createdAt": "2026-05-27", + "lastVerifiedAt": "2026-05-27", + "freshnessWindowDays": 90 +} diff --git a/entries/drift-detection-between-systems/tutorial.md b/entries/drift-detection-between-systems/tutorial.md new file mode 100644 index 0000000..d6537b5 --- /dev/null +++ b/entries/drift-detection-between-systems/tutorial.md @@ -0,0 +1,395 @@ +## Opening thesis + +You will build a reconciliation agent that watches writes to a billing system and a CRM, compares the records in both systems within seconds, and logs any mismatch with full state from both sides. An agent reconciles two systems of record on every write with zero fatigue, catching silent divergence within a minute, where a human running a quarterly reconciliation script finds the same drift weeks after it happened. The agent uses Cue to orchestrate the reconciliation workflow and Claude to interpret ambiguous field mappings between the two systems. + +## Before + +Your billing system says the customer is on the Enterprise plan at $4,800 per year. Your CRM says they are on Professional at $2,400 per year. Nobody knows which is right because the records diverged three months ago when someone updated billing directly and the webhook to the CRM silently failed. You discover this during the quarterly audit. You open a spreadsheet. You pull every customer record from both systems. You diff 11,000 rows. You find 340 mismatches. For each one, you check timestamps, API logs, and Slack threads to figure out which system has the correct state. This takes a week. During that week, invoices go out with wrong amounts. Support tickets pile up. The root cause was a single dropped webhook. The cost was a week of engineering time and a handful of angry customers. + +## Architecture + +The system has four components. A webhook listener receives write events from both the billing system and the CRM. It sends each event to a Cue workflow. The workflow fetches the corresponding record from the other system, then calls Claude to compare the two records and determine if they match. If they do not match, the workflow logs a drift event to a structured log store with both records attached. + +```text +DIAGRAM: Reconciliation agent architecture +Caption: Shows how every write triggers a cross-system comparison within seconds. +Nodes: +1. Billing System - source of subscription and invoice writes +2. CRM System - source of customer profile and plan writes +3. Webhook Listener (Node.js) - receives POST events from both systems +4. Cue Workflow - orchestrates fetch, compare, and log steps +5. Claude (Anthropic API) - compares two records, identifies mismatches +6. Drift Log (structured JSON file or database) - stores mismatch events with both sides +Flow: +- Billing System sends write event to Webhook Listener +- CRM System sends write event to Webhook Listener +- Webhook Listener triggers Cue Workflow with event payload +- Cue Workflow fetches counterpart record from the other system +- Cue Workflow sends both records to Claude for comparison +- Claude returns match/mismatch verdict with field-level diff +- Cue Workflow writes drift event to Drift Log if mismatch detected +``` + +## Step-by-step implementation + +### 1. Set up the project and install dependencies + +Create a new Node.js project. Install the Cue SDK for workflow orchestration and the Anthropic SDK for calling Claude. You need two environment variables: your Anthropic API key (get it at https://console.anthropic.com/settings/keys) and your Cue API key (get it at https://console.cue.dev/settings/api-keys). + +```bash +mkdir drift-agent && cd drift-agent +npm init -y +npm install @anthropic-ai/sdk express +export ANTHROPIC_API_KEY="your-key-from-console" +export CUEAPI_API_KEY="your-key-from-cue-console" +``` + +### 2. Define the record schema for both systems + +Create a shared schema file that describes the fields you expect in both systems. This is the contract the agent enforces. When either system writes a record, the agent maps it to this schema before comparing. + +```ts +// schema.ts +export interface NormalizedRecord { + customerId: string; + email: string; + planName: string; + planPriceAnnual: number; + status: "active" | "canceled" | "past_due"; + lastUpdated: string; // ISO 8601 + sourceSystem: "billing" | "crm"; +} + +export interface DriftEvent { + detectedAt: string; + customerId: string; + billingRecord: NormalizedRecord | null; + crmRecord: NormalizedRecord | null; + mismatchedFields: string[]; + verdict: string; +} +``` + +### 3. Build the webhook listener + +This Express server receives POST requests from both systems. Each request includes a customer ID and the updated fields. The listener normalizes the incoming payload and passes it to the reconciliation function. + +```ts +// server.ts +import express from "express"; +import { reconcile } from "./reconcile"; + +const app = express(); +app.use(express.json()); + +app.post("/webhook/billing", async (req, res) => { + const event = { source: "billing" as const, payload: req.body }; + const result = await reconcile(event); + res.json(result); +}); + +app.post("/webhook/crm", async (req, res) => { + const event = { source: "crm" as const, payload: req.body }; + const result = await reconcile(event); + res.json(result); +}); + +app.listen(3100, () => { + console.log("Drift agent listening on port 3100"); +}); +``` + +### 4. Fetch the counterpart record + +When billing writes, you fetch the same customer from the CRM, and vice versa. This function simulates those fetches. In production, replace these with real API calls to your billing provider (Stripe, Chargebee, etc.) and your CRM (Salesforce, HubSpot, etc.). + +```ts +// fetch-counterpart.ts +import { NormalizedRecord } from "./schema"; + +const BILLING_API = process.env.BILLING_API_URL || "http://localhost:3200/api/billing"; +const CRM_API = process.env.CRM_API_URL || "http://localhost:3200/api/crm"; + +export async function fetchCounterpart( + customerId: string, + sourceSystem: "billing" | "crm" +): Promise { + const targetUrl = sourceSystem === "billing" + ? `${CRM_API}/customers/${customerId}` + : `${BILLING_API}/customers/${customerId}`; + + const resp = await fetch(targetUrl); + if (!resp.ok) return null; + const data = await resp.json(); + return data as NormalizedRecord; +} +``` + +### 5. Compare records with Claude + +Send both records to Claude. Ask it to list mismatched fields and provide a one-sentence verdict. Claude handles edge cases a regex never will: field name differences, currency formatting, timezone offsets in timestamps, and plan name synonyms ("Enterprise" vs "enterprise_annual"). + +```ts +// compare.ts +import Anthropic from "@anthropic-ai/sdk"; +import { NormalizedRecord } from "./schema"; + +const client = new Anthropic(); + +export async function compareRecords( + billing: NormalizedRecord, + crm: NormalizedRecord +): Promise<{ mismatched: string[]; verdict: string }> { + const prompt = `You are a data reconciliation agent. Compare these two records for the same customer and return a JSON object with two fields: +- "mismatched": an array of field names that differ between the two records +- "verdict": a one-sentence summary of the drift + +If the records match on all fields, return {"mismatched": [], "verdict": "Records match."}. + +Billing record: +${JSON.stringify(billing, null, 2)} + +CRM record: +${JSON.stringify(crm, null, 2)} + +Return only valid JSON. No explanation.`; + + const message = await client.messages.create({ + model: "claude-sonnet-4-20250514", + max_tokens: 512, + messages: [{ role: "user", content: prompt }], + }); + + const text = message.content[0].type === "text" ? message.content[0].text : "{}"; + return JSON.parse(text); +} +``` + +### 6. Wire up the reconciliation function + +This is the core. It receives the write event, fetches the counterpart, compares, and decides whether to log a drift event. + +```ts +// reconcile.ts +import { fetchCounterpart } from "./fetch-counterpart"; +import { compareRecords } from "./compare"; +import { logDrift } from "./drift-log"; +import { NormalizedRecord, DriftEvent } from "./schema"; + +interface WriteEvent { + source: "billing" | "crm"; + payload: NormalizedRecord; +} + +export async function reconcile(event: WriteEvent) { + const { source, payload } = event; + const counterpart = await fetchCounterpart(payload.customerId, source); + + if (!counterpart) { + return { status: "skipped", reason: "counterpart not found" }; + } + + const billing = source === "billing" ? payload : counterpart; + const crm = source === "crm" ? payload : counterpart; + + const result = await compareRecords(billing, crm); + + if (result.mismatched.length > 0) { + const driftEvent: DriftEvent = { + detectedAt: new Date().toISOString(), + customerId: payload.customerId, + billingRecord: billing, + crmRecord: crm, + mismatchedFields: result.mismatched, + verdict: result.verdict, + }; + await logDrift(driftEvent); + return { status: "drift_detected", drift: driftEvent }; + } + + return { status: "in_sync" }; +} +``` + +### 7. Implement the drift log + +Write drift events to a local JSON Lines file. In production, send these to your observability platform, a database, or a Slack channel. The important thing: both sides of the record are captured at detection time, not hours later when someone investigates. + +```ts +// drift-log.ts +import { appendFileSync } from "fs"; +import { DriftEvent } from "./schema"; + +const LOG_PATH = process.env.DRIFT_LOG_PATH || "./drift-events.jsonl"; + +export async function logDrift(event: DriftEvent): Promise { + const line = JSON.stringify(event) + "\n"; + appendFileSync(LOG_PATH, line, "utf-8"); + console.log( + `DRIFT: customer=${event.customerId} fields=${event.mismatchedFields.join(",")} verdict=${event.verdict}` + ); +} +``` + +### 8. Register the workflow in Cue + +Use Cue to schedule and monitor the reconciliation agent. Cue tracks execution history, retries on transient failures, and gives you a dashboard to see how many drift events fired in the last hour. Register the workflow via the Cue API. See https://docs.cue.dev for the full API reference. + +```bash +curl -X POST https://api.cue.dev/v1/workflows \ + -H "Authorization: Bearer $CUEAPI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "billing-crm-reconciliation", + "trigger": "webhook", + "endpoint": "https://your-server.example.com/webhook/billing", + "retryPolicy": { + "maxAttempts": 3, + "backoffMs": 2000 + }, + "alertOnFailure": true + }' +``` + +### 9. Test with a simulated drift event + +Send a billing update where the plan name differs from what the CRM has. You should see a drift event logged within seconds. + +```bash +curl -X POST http://localhost:3100/webhook/billing \ + -H "Content-Type: application/json" \ + -d '{ + "customerId": "cust_8832", + "email": "ops@acme.co", + "planName": "Enterprise", + "planPriceAnnual": 4800, + "status": "active", + "lastUpdated": "2026-05-27T14:32:00Z", + "sourceSystem": "billing" + }' +``` + +### 10. Verify the drift log output + +Check the log file. You should see a single JSON line with both the billing and CRM records, the mismatched fields, and Claude's verdict. + +```bash +cat drift-events.jsonl | jq . +``` + +## Breakage + +If you skip the per-write reconciliation step and rely on a periodic batch job, drift accumulates silently. A webhook fails on a Tuesday. Nobody notices. Three weeks pass. Invoices go out at the wrong price. A customer complains. Support opens a ticket. Engineering queries both systems, finds the mismatch, and then has to determine which system drifted. The timestamp trail is cold. Audit logs have been rotated. You spend two days reconstructing the sequence of events for one customer. Multiply that by every mismatched record in the quarterly audit. + +```text +DIAGRAM: Failure mode without per-write reconciliation +Caption: Shows how a dropped webhook causes silent drift that compounds over weeks. +Nodes: +1. Billing System - writes a plan change +2. Webhook (to CRM) - fails silently, no retry +3. CRM System - retains stale data +4. Quarterly Audit Script - runs 90 days later +5. Engineering Team - manually investigates each mismatch +Flow: +- Billing System fires webhook to CRM System +- Webhook fails, CRM System never receives the update +- 90 days pass with no detection +- Quarterly Audit Script diffs all records, finds 340 mismatches +- Engineering Team spends a week resolving each mismatch manually +``` + +## The fix + +The fix is the reconciliation call you already built in Step 6. The key addition is the Cue workflow retry policy from Step 8. If the counterpart system is temporarily unreachable, Cue retries the reconciliation up to three times with exponential backoff. If all retries fail, Cue fires an alert so a human can investigate. The drift log from Step 7 captures both records at the moment of detection, so there is no need to reconstruct state later. Here is the retry-aware version of the reconcile function that integrates with Cue's failure tracking. + +```ts +// reconcile-with-retry.ts +import { fetchCounterpart } from "./fetch-counterpart"; +import { compareRecords } from "./compare"; +import { logDrift } from "./drift-log"; +import { NormalizedRecord, DriftEvent } from "./schema"; + +interface WriteEvent { + source: "billing" | "crm"; + payload: NormalizedRecord; +} + +export async function reconcile(event: WriteEvent) { + const { source, payload } = event; + + let counterpart: NormalizedRecord | null = null; + try { + counterpart = await fetchCounterpart(payload.customerId, source); + } catch (err) { + // Throw so Cue's retry policy catches it + throw new Error( + `Counterpart fetch failed for ${payload.customerId}: ${err}` + ); + } + + if (!counterpart) { + const orphanEvent: DriftEvent = { + detectedAt: new Date().toISOString(), + customerId: payload.customerId, + billingRecord: source === "billing" ? payload : null, + crmRecord: source === "crm" ? payload : null, + mismatchedFields: ["existence"], + verdict: `Record exists in ${source} but not in the other system.`, + }; + await logDrift(orphanEvent); + return { status: "orphan_detected", drift: orphanEvent }; + } + + const billing = source === "billing" ? payload : counterpart; + const crm = source === "crm" ? payload : counterpart; + const result = await compareRecords(billing, crm); + + if (result.mismatched.length > 0) { + const driftEvent: DriftEvent = { + detectedAt: new Date().toISOString(), + customerId: payload.customerId, + billingRecord: billing, + crmRecord: crm, + mismatchedFields: result.mismatched, + verdict: result.verdict, + }; + await logDrift(driftEvent); + return { status: "drift_detected", drift: driftEvent }; + } + + return { status: "in_sync" }; +} +``` + +## Fixed state + +```text +DIAGRAM: Reconciliation agent with retry and orphan detection +Caption: Shows the complete system where every write triggers comparison, retries handle transient failures, and orphan records are flagged. +Nodes: +1. Billing System - source of subscription writes +2. CRM System - source of customer profile writes +3. Webhook Listener - receives events from both systems +4. Cue Workflow (with retry) - orchestrates reconciliation, retries on failure +5. Claude (Anthropic API) - compares records, handles field ambiguity +6. Drift Log - stores mismatch events with full state from both sides +7. Alert Channel - notified when retries exhausted +Flow: +- Billing System or CRM System sends write event to Webhook Listener +- Webhook Listener triggers Cue Workflow +- Cue Workflow fetches counterpart record from the other system +- If fetch fails, Cue retries up to 3 times with backoff +- If retries exhausted, Cue sends alert to Alert Channel +- If fetch succeeds, Cue sends both records to Claude +- Claude returns field-level diff +- If mismatch found, Cue writes drift event to Drift Log +- If record missing in other system, Cue logs orphan event to Drift Log +``` + +## After + +Your billing system writes a plan change. Within 40 seconds, the agent fetches the CRM record, compares both, and logs a drift event with the exact fields that differ and the exact values on each side. You get an alert. You look at the drift log entry. It shows billing has "Enterprise" at $4,800 and the CRM has "Professional" at $2,400. You fix the CRM record. Total time from drift to resolution: five minutes. No quarterly audit. No spreadsheet. No week of detective work. The agent does not get bored. It does not skip a record because it is Friday afternoon. It checks every write, every time. + +## Takeaway + +The pattern is simple: compare on write, not on schedule. Any time two systems hold overlapping state, an agent that reconciles on every mutation will find drift in seconds instead of weeks. Apply this wherever you see a "sync" job that runs nightly, weekly, or quarterly. The cost of one API call per write is always cheaper than the cost of investigating stale drift after the fact. \ No newline at end of file diff --git a/queue/topics.json b/queue/topics.json index f60f347..bf7cacb 100644 --- a/queue/topics.json +++ b/queue/topics.json @@ -192,7 +192,8 @@ "prd-review" ], "priority": 11, - "status": "queued", + "status": "published", + "publishedAt": "2026-05-27", "soulLine": "An agent runs six adversarial critique passes in two minutes and surfaces objections a solo founder never asks themselves, where a human review panel takes three days to assemble and half the panel will be polite.", "beforeState": "You have a PRD you're excited about. You start building. Four days in you realize it solves the wrong problem.", "afterState": "Before you write any code, an agent runs six adversarial critiques against the PRD and surfaces the three objections that would have killed the idea on day four.", @@ -263,7 +264,8 @@ "consistency" ], "priority": 15, - "status": "queued", + "status": "published", + "publishedAt": "2026-05-27", "soulLine": "An agent reconciles two systems of record on every write with zero fatigue, catching silent divergence within a minute, where a human running a quarterly reconciliation script finds the same drift weeks after it happened.", "beforeState": "Your billing system and your CRM drift out of sync. You find out during a quarterly audit and spend a week untangling which source was right.", "afterState": "An agent compares both systems on every significant write, surfaces mismatches within a minute, and logs the drift event with both sides' state captured.",