d7a56474fc
Ports the pr-lens agent skill: represent a diff or subsystem as one graph.json document and render it as animated SVG diagrams via the MIT npx CLI (@coldtea/pr-lens-cli), with optional opt-in publishing to a shareable canvas link. Why: PR review and architecture explanation keep producing hand-drawn Mermaid; this gives validated, animated, drill-down diagrams with a deterministic document format. Upstream created Aug 20, 1.1k stars in 3 weeks, GitHub App + Action + CLI + skill. - optional-skills/software-development/pr-lens/: SKILL.md (145 lines), references/ (config, graph document format, valid example) vendored near-verbatim, LICENSE.txt (MIT, Coldtea AI) - gh --attach caveat handled: installed gh 2.97 lacks the flag; skill documents honest fallbacks (gist, canvas link, local path) - Live smoke: validate + render of the vendored example graph passed (4 SVGs + manifest produced) - docs: own catalog row + generated page + sidebar entry only
686 lines
17 KiB
JSON
686 lines
17 KiB
JSON
{
|
||
"schemaVersion": "0.1.1",
|
||
"kind": "graph",
|
||
"generatedAt": "2026-08-19T18:24:00.000Z",
|
||
"title": "Batch broadcast sending through Postmark",
|
||
"summary": "Broadcast delivery moves from one Postmark request per recipient to batched requests of 500, with suppression filtering pulled in front of the send and the payload builder extracted into a shared library.",
|
||
"lenses": [
|
||
"architecture",
|
||
"data-flow"
|
||
],
|
||
"provenance": {
|
||
"repo": {
|
||
"owner": "ohansemmanuel",
|
||
"name": "bestregards",
|
||
"host": "github.com"
|
||
},
|
||
"base": {
|
||
"sha": "3f5c1ab9d24e7f08c6b1a5d3e9074c2b8a6f1d40",
|
||
"ref": "main"
|
||
},
|
||
"head": {
|
||
"sha": "b71e0d4c8a92f5361de7c0b4a8f2593d6c1e8a77",
|
||
"ref": "batch-broadcast-send"
|
||
},
|
||
"pullRequest": {
|
||
"number": 128,
|
||
"title": "Send broadcasts in batches of 500",
|
||
"url": "https://github.com/ohansemmanuel/bestregards/pull/128"
|
||
},
|
||
"generator": {
|
||
"name": "pr-lens-examples",
|
||
"version": "0.1.0"
|
||
}
|
||
},
|
||
"lanes": [
|
||
{
|
||
"id": "web",
|
||
"label": "Next.js",
|
||
"subtitle": "Vercel",
|
||
"order": 0
|
||
},
|
||
{
|
||
"id": "functions",
|
||
"label": "Cloud Functions",
|
||
"subtitle": "Firebase",
|
||
"order": 1
|
||
},
|
||
{
|
||
"id": "external",
|
||
"label": "External",
|
||
"subtitle": "Postmark",
|
||
"order": 2
|
||
}
|
||
],
|
||
"nodes": [
|
||
{
|
||
"id": "broadcast-composer",
|
||
"label": "Broadcast composer",
|
||
"kind": "ui",
|
||
"delta": "unchanged",
|
||
"lane": "web",
|
||
"subtitle": "app/broadcasts/new",
|
||
"summary": "Where an author writes a broadcast and hits send. Untouched by this change.",
|
||
"files": [
|
||
{
|
||
"path": "app/broadcasts/new/page.tsx"
|
||
}
|
||
],
|
||
"badges": []
|
||
},
|
||
{
|
||
"id": "queue-route",
|
||
"label": "POST /api/broadcasts/queue",
|
||
"kind": "route",
|
||
"delta": "modified",
|
||
"lane": "web",
|
||
"summary": "Writes the queue document. Now stamps the recipient count and batch size the sender will use instead of leaving batching to the worker.",
|
||
"files": [
|
||
{
|
||
"path": "app/api/broadcasts/queue/route.ts",
|
||
"startLine": 24,
|
||
"endLine": 96
|
||
}
|
||
],
|
||
"badges": [
|
||
"+38 / -12"
|
||
]
|
||
},
|
||
{
|
||
"id": "broadcast-queue",
|
||
"label": "broadcastQueue",
|
||
"kind": "datastore",
|
||
"delta": "modified",
|
||
"lane": "functions",
|
||
"subtitle": "Firestore collection",
|
||
"summary": "Queue documents gained batchSize and suppressedCount fields, and results are now written back per batch rather than per recipient.",
|
||
"files": [
|
||
{
|
||
"path": "functions/src/broadcast/schema.ts",
|
||
"startLine": 12,
|
||
"endLine": 48
|
||
}
|
||
],
|
||
"badges": []
|
||
},
|
||
{
|
||
"id": "send-broadcast-bulk",
|
||
"label": "sendBroadcastBulk",
|
||
"kind": "function",
|
||
"delta": "added",
|
||
"lane": "functions",
|
||
"subtitle": "onWrite trigger",
|
||
"summary": "New trigger handler. Fetches suppressions once, builds batched payloads, and posts them to Postmark in chunks of 500.",
|
||
"files": [
|
||
{
|
||
"path": "functions/src/broadcast/sendBroadcastBulk.ts",
|
||
"startLine": 1,
|
||
"endLine": 142
|
||
}
|
||
],
|
||
"badges": [
|
||
"new"
|
||
]
|
||
},
|
||
{
|
||
"id": "build-bulk-payload",
|
||
"label": "buildBulkPayload",
|
||
"kind": "function",
|
||
"delta": "added",
|
||
"lane": "functions",
|
||
"summary": "Turns a broadcast and its recipient slice into a Postmark batch request body.",
|
||
"files": [
|
||
{
|
||
"path": "packages/broadcast-lib/src/buildBulkPayload.ts",
|
||
"startLine": 1,
|
||
"endLine": 74
|
||
}
|
||
],
|
||
"badges": []
|
||
},
|
||
{
|
||
"id": "get-suppressed-emails",
|
||
"label": "getSuppressedEmails",
|
||
"kind": "function",
|
||
"delta": "added",
|
||
"lane": "functions",
|
||
"summary": "Pulls the Postmark suppression dump once per broadcast so suppressed addresses are filtered before any batch is sent.",
|
||
"files": [
|
||
{
|
||
"path": "packages/broadcast-lib/src/getSuppressedEmails.ts",
|
||
"startLine": 1,
|
||
"endLine": 58
|
||
}
|
||
],
|
||
"badges": []
|
||
},
|
||
{
|
||
"id": "broadcast-lib",
|
||
"label": "broadcast-lib",
|
||
"kind": "package",
|
||
"delta": "added",
|
||
"lane": "functions",
|
||
"subtitle": "packages/broadcast-lib",
|
||
"summary": "New shared package so the queue route and the sender agree on payload shape and batch size.",
|
||
"files": [
|
||
{
|
||
"path": "packages/broadcast-lib/src/index.ts"
|
||
}
|
||
],
|
||
"badges": [
|
||
"new package"
|
||
]
|
||
},
|
||
{
|
||
"id": "process-broadcast",
|
||
"label": "processBroadcast",
|
||
"kind": "function",
|
||
"delta": "removed",
|
||
"lane": "functions",
|
||
"subtitle": "onWrite trigger",
|
||
"summary": "The per-recipient loop this change replaces.",
|
||
"files": [
|
||
{
|
||
"path": "functions/src/broadcast/processBroadcast.ts",
|
||
"startLine": 1,
|
||
"endLine": 118,
|
||
"revision": "base"
|
||
}
|
||
],
|
||
"badges": []
|
||
},
|
||
{
|
||
"id": "send-single-email",
|
||
"label": "sendSingleEmail",
|
||
"kind": "function",
|
||
"delta": "removed",
|
||
"lane": "functions",
|
||
"summary": "One Postmark request per recipient. Gone with the loop that called it.",
|
||
"files": [
|
||
{
|
||
"path": "functions/src/broadcast/sendSingleEmail.ts",
|
||
"startLine": 1,
|
||
"endLine": 46,
|
||
"revision": "base"
|
||
}
|
||
],
|
||
"badges": []
|
||
},
|
||
{
|
||
"id": "postmark",
|
||
"label": "Postmark",
|
||
"kind": "external",
|
||
"delta": "modified",
|
||
"lane": "external",
|
||
"subtitle": "Email API",
|
||
"summary": "Same provider, different endpoints: the batch endpoint and the suppression dump replace repeated single sends.",
|
||
"files": [],
|
||
"badges": []
|
||
}
|
||
],
|
||
"edges": [
|
||
{
|
||
"id": "composer-to-queue",
|
||
"from": "broadcast-composer",
|
||
"to": "queue-route",
|
||
"kind": "http",
|
||
"delta": "unchanged",
|
||
"label": "send broadcast",
|
||
"emphasis": "normal",
|
||
"animated": false,
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "queue-to-firestore",
|
||
"from": "queue-route",
|
||
"to": "broadcast-queue",
|
||
"kind": "data",
|
||
"delta": "modified",
|
||
"label": "enqueue job",
|
||
"emphasis": "normal",
|
||
"animated": false,
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "queue-to-lib",
|
||
"from": "queue-route",
|
||
"to": "broadcast-lib",
|
||
"kind": "dependency",
|
||
"delta": "added",
|
||
"label": "batch size",
|
||
"emphasis": "normal",
|
||
"animated": false,
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "firestore-to-bulk",
|
||
"from": "broadcast-queue",
|
||
"to": "send-broadcast-bulk",
|
||
"kind": "event",
|
||
"delta": "added",
|
||
"label": "onWrite",
|
||
"emphasis": "normal",
|
||
"animated": false,
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "firestore-to-process",
|
||
"from": "broadcast-queue",
|
||
"to": "process-broadcast",
|
||
"kind": "event",
|
||
"delta": "removed",
|
||
"label": "onWrite",
|
||
"emphasis": "normal",
|
||
"animated": false,
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "process-to-single",
|
||
"from": "process-broadcast",
|
||
"to": "send-single-email",
|
||
"kind": "call",
|
||
"delta": "removed",
|
||
"label": "per recipient",
|
||
"emphasis": "normal",
|
||
"animated": false,
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "single-to-postmark",
|
||
"from": "send-single-email",
|
||
"to": "postmark",
|
||
"kind": "http",
|
||
"delta": "removed",
|
||
"label": "POST /email · 1 msg/call",
|
||
"emphasis": "normal",
|
||
"animated": false,
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "bulk-to-payload",
|
||
"from": "send-broadcast-bulk",
|
||
"to": "build-bulk-payload",
|
||
"kind": "call",
|
||
"delta": "added",
|
||
"emphasis": "normal",
|
||
"animated": false,
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "bulk-to-suppressions",
|
||
"from": "send-broadcast-bulk",
|
||
"to": "get-suppressed-emails",
|
||
"kind": "call",
|
||
"delta": "added",
|
||
"emphasis": "normal",
|
||
"animated": false,
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "bulk-to-lib",
|
||
"from": "send-broadcast-bulk",
|
||
"to": "broadcast-lib",
|
||
"kind": "dependency",
|
||
"delta": "added",
|
||
"emphasis": "normal",
|
||
"animated": false,
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "suppressions-to-postmark",
|
||
"from": "get-suppressed-emails",
|
||
"to": "postmark",
|
||
"kind": "http",
|
||
"delta": "added",
|
||
"label": "GET suppression dump",
|
||
"emphasis": "normal",
|
||
"animated": true,
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "bulk-to-postmark",
|
||
"from": "send-broadcast-bulk",
|
||
"to": "postmark",
|
||
"kind": "http",
|
||
"delta": "added",
|
||
"label": "500 msgs/call",
|
||
"emphasis": "hero",
|
||
"animated": true,
|
||
"summary": "The change in one edge: a broadcast to 10,000 recipients drops from 10,000 requests to 20.",
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "bulk-to-firestore",
|
||
"from": "send-broadcast-bulk",
|
||
"to": "broadcast-queue",
|
||
"kind": "data",
|
||
"delta": "added",
|
||
"label": "write results",
|
||
"emphasis": "normal",
|
||
"animated": false,
|
||
"files": []
|
||
}
|
||
],
|
||
"flows": [
|
||
{
|
||
"id": "send-pipeline",
|
||
"title": "Sending a broadcast",
|
||
"summary": "The path a queued broadcast takes now, from enqueue to per-message results.",
|
||
"delta": "modified",
|
||
"participants": [
|
||
{
|
||
"node": "queue-route",
|
||
"label": "queue route"
|
||
},
|
||
{
|
||
"node": "broadcast-queue",
|
||
"label": "Firestore"
|
||
},
|
||
{
|
||
"node": "send-broadcast-bulk",
|
||
"label": "sendBroadcastBulk"
|
||
},
|
||
{
|
||
"node": "postmark",
|
||
"label": "Postmark"
|
||
}
|
||
],
|
||
"messages": [
|
||
{
|
||
"id": "enqueue",
|
||
"from": "queue-route",
|
||
"to": "broadcast-queue",
|
||
"label": "enqueue broadcast job",
|
||
"kind": "async",
|
||
"delta": "modified",
|
||
"animated": true,
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "trigger",
|
||
"from": "broadcast-queue",
|
||
"to": "send-broadcast-bulk",
|
||
"label": "onWrite trigger",
|
||
"kind": "async",
|
||
"delta": "added",
|
||
"animated": true,
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "suppressions-request",
|
||
"from": "send-broadcast-bulk",
|
||
"to": "postmark",
|
||
"label": "GET suppression dump",
|
||
"kind": "sync",
|
||
"delta": "added",
|
||
"animated": true,
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "suppressions-response",
|
||
"from": "postmark",
|
||
"to": "send-broadcast-bulk",
|
||
"label": "suppressed addresses",
|
||
"kind": "return",
|
||
"delta": "added",
|
||
"animated": true,
|
||
"note": "Fetched once per broadcast, not once per recipient.",
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "batch-post",
|
||
"from": "send-broadcast-bulk",
|
||
"to": "postmark",
|
||
"label": "POST /email/batch · 500 msgs",
|
||
"kind": "sync",
|
||
"delta": "added",
|
||
"animated": true,
|
||
"repeat": 4,
|
||
"note": "One request per 500 recipients; four for this 2,000-recipient broadcast.",
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "batch-results",
|
||
"from": "postmark",
|
||
"to": "send-broadcast-bulk",
|
||
"label": "per-message results",
|
||
"kind": "return",
|
||
"delta": "added",
|
||
"animated": true,
|
||
"files": []
|
||
},
|
||
{
|
||
"id": "write-results",
|
||
"from": "send-broadcast-bulk",
|
||
"to": "broadcast-queue",
|
||
"label": "write results",
|
||
"kind": "async",
|
||
"delta": "added",
|
||
"animated": true,
|
||
"files": []
|
||
}
|
||
]
|
||
}
|
||
],
|
||
"stats": {
|
||
"filesChanged": 14,
|
||
"additions": 486,
|
||
"deletions": 212,
|
||
"chips": [
|
||
{
|
||
"label": "Postmark calls",
|
||
"value": "500× fewer",
|
||
"tone": "hero"
|
||
},
|
||
{
|
||
"label": "New",
|
||
"value": "4 units",
|
||
"tone": "added"
|
||
},
|
||
{
|
||
"label": "Retired",
|
||
"value": "2 units",
|
||
"tone": "removed"
|
||
}
|
||
]
|
||
},
|
||
"views": [
|
||
{
|
||
"id": "overview",
|
||
"title": "Architecture — blast radius",
|
||
"lens": "architecture",
|
||
"summary": "Everything this change touches, across all three lanes.",
|
||
"scope": {
|
||
"kind": "all"
|
||
},
|
||
"defaultOpen": true,
|
||
"children": [
|
||
{
|
||
"id": "new-batch-path",
|
||
"title": "The new batch path",
|
||
"lens": "architecture",
|
||
"summary": "What replaced the per-recipient loop.",
|
||
"scope": {
|
||
"kind": "selection",
|
||
"lanes": [],
|
||
"nodes": [
|
||
"send-broadcast-bulk",
|
||
"build-bulk-payload",
|
||
"get-suppressed-emails",
|
||
"broadcast-lib",
|
||
"postmark"
|
||
],
|
||
"edges": [
|
||
"bulk-to-payload",
|
||
"bulk-to-suppressions",
|
||
"bulk-to-lib",
|
||
"suppressions-to-postmark",
|
||
"bulk-to-postmark",
|
||
"bulk-to-firestore"
|
||
],
|
||
"flows": []
|
||
},
|
||
"defaultOpen": false,
|
||
"children": []
|
||
},
|
||
{
|
||
"id": "retired-path",
|
||
"title": "What was retired",
|
||
"lens": "architecture",
|
||
"summary": "The single-send path, kept visible so a reviewer can confirm nothing else called it.",
|
||
"scope": {
|
||
"kind": "selection",
|
||
"lanes": [],
|
||
"nodes": [
|
||
"process-broadcast",
|
||
"send-single-email"
|
||
],
|
||
"edges": [
|
||
"firestore-to-process",
|
||
"process-to-single",
|
||
"single-to-postmark"
|
||
],
|
||
"flows": []
|
||
},
|
||
"defaultOpen": false,
|
||
"children": []
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "send-pipeline-view",
|
||
"title": "Data flow — sending a broadcast",
|
||
"lens": "data-flow",
|
||
"scope": {
|
||
"kind": "selection",
|
||
"lanes": [],
|
||
"nodes": [],
|
||
"edges": [],
|
||
"flows": [
|
||
"send-pipeline"
|
||
]
|
||
},
|
||
"defaultOpen": false,
|
||
"children": []
|
||
}
|
||
],
|
||
"walkthrough": {
|
||
"steps": [
|
||
{
|
||
"id": "batches-of-500",
|
||
"heading": "sendBroadcastBulk and buildBulkPayload added",
|
||
"body": "Nothing loops over recipients any more. The sender works on a whole batch at a time.",
|
||
"stage": {
|
||
"kind": "view",
|
||
"view": "overview"
|
||
},
|
||
"focus": {
|
||
"kind": "selection",
|
||
"lanes": [],
|
||
"nodes": [
|
||
"send-broadcast-bulk",
|
||
"build-bulk-payload",
|
||
"postmark"
|
||
],
|
||
"edges": [],
|
||
"messages": []
|
||
}
|
||
},
|
||
{
|
||
"id": "suppression-first",
|
||
"heading": "getSuppressedEmails added before the send",
|
||
"body": "It pulls the blocked addresses once, before any batch is built.",
|
||
"stage": {
|
||
"kind": "view",
|
||
"view": "new-batch-path"
|
||
},
|
||
"focus": {
|
||
"kind": "selection",
|
||
"lanes": [],
|
||
"nodes": [
|
||
"get-suppressed-emails",
|
||
"postmark"
|
||
],
|
||
"edges": [],
|
||
"messages": []
|
||
}
|
||
},
|
||
{
|
||
"id": "old-path-goes-dark",
|
||
"heading": "processBroadcast and sendSingleEmail removed",
|
||
"body": "sendBroadcastBulk does their job for whole batches.",
|
||
"stage": {
|
||
"kind": "view",
|
||
"view": "overview"
|
||
},
|
||
"focus": {
|
||
"kind": "selection",
|
||
"lanes": [],
|
||
"nodes": [
|
||
"process-broadcast",
|
||
"send-single-email"
|
||
],
|
||
"edges": [],
|
||
"messages": []
|
||
}
|
||
},
|
||
{
|
||
"id": "sequence-start-to-finish",
|
||
"heading": "The send sequence gained 6 new steps",
|
||
"body": "The queue write is the only step that was there before, and it now stamps the batch size.",
|
||
"stage": {
|
||
"kind": "flow",
|
||
"flow": "send-pipeline"
|
||
},
|
||
"focus": {
|
||
"kind": "all"
|
||
}
|
||
},
|
||
{
|
||
"id": "four-batch-calls",
|
||
"heading": "Postmark now gets 500 emails per call",
|
||
"body": "One call per batch, and Postmark answers with a result for each message.",
|
||
"stage": {
|
||
"kind": "flow",
|
||
"flow": "send-pipeline"
|
||
},
|
||
"focus": {
|
||
"kind": "selection",
|
||
"lanes": [],
|
||
"nodes": [],
|
||
"edges": [],
|
||
"messages": [
|
||
"batch-post",
|
||
"batch-results"
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "blast-radius",
|
||
"heading": "4 parts added, 2 removed, across 3 lanes",
|
||
"body": "A 2,000-person broadcast used to make 2,000 calls to Postmark. It now makes 4.",
|
||
"stage": {
|
||
"kind": "view",
|
||
"view": "overview"
|
||
},
|
||
"focus": {
|
||
"kind": "all"
|
||
}
|
||
}
|
||
]
|
||
},
|
||
"layout": {
|
||
"direction": "right",
|
||
"laneOrder": [
|
||
"web",
|
||
"functions",
|
||
"external"
|
||
],
|
||
"rank": {
|
||
"queue-route": 0,
|
||
"send-broadcast-bulk": 1,
|
||
"postmark": 2
|
||
}
|
||
}
|
||
}
|