The lead ledger was one file on one PC, and a missing file read as zero leads. The file was .runs/blog-leads/leads.jsonl, and if it wasn’t there, read_leads returned []. The module’s docstring said its sources were verified on 2026-09-30. Ten days later a dependency audit flagged the ledger as row D6, and we moved it off that PC.
What the ledger holds
Each row is {slug, kind, at}, with no personal data in it. The site stamps post_slug on every intake form. The form provider delivers submissions to email and exposes no API, so rows are recorded with blog-funnel --record-lead SLUG --kind review|discovery|contact. The metrics report reads the file to count leads per post.
The old read_leads opened with if not ledger.exists(): return []. The existing test for missing sources asserts the text “no leads recorded” for that case. A missing ledger and a ledger with no leads came back as the same [].
Moving it to a Worker
The ledger now lives in a Cloudflare Worker, dxdev-leads, backed by a SQLite Durable Object on our own Cloudflare account. The commit touched 3 files, with 106 insertions and 6 deletions, 49 of those insertions in tests.
In funnel.py, the ledger constant changed:
LEADS_LEDGER= lead_metrics.RemoteLedger()
RemoteLedger in lead_metrics.py is a thin client over urllib.request. read_rows() is a GET and append(row) is a POST. Each request carries an Authorization: Bearer header, and urlopen is called with timeout=20. The token comes from secrets.get("DXDEV_LEADS_TOKEN") unless one is passed in. A runbook for the Worker is referenced in the module docstring.
read_leads and record_lead now take either kind of ledger. Given a RemoteLedger, they go to the Worker. Given a plain Path, they behave as before, which keeps the local jsonl ledger usable for tests and offline runs. read_leads filters the remote rows to ones that have a slug and a kind in LEAD_KINDS. record_lead returns only slug, kind and at from what the Worker saved.
Failures raise
The class docstring states the rule: “Failures raise: a lead read that cannot reach the ledger must never look like ‘no leads’ (the callers already surface exceptions as an error, not a zero).”
I kept the local path as it was, so the [] on a missing file still exists there. The remote path has no such branch. The docstring also says the PC is only a client of the ledger now.
A missing leads.jsonl still returns [], and a RemoteLedger call that fails raises instead.
AI Skills
Use this lesson with the AI assistant you already use
The lead ledger was a single local file, and the old read_leads returned an empty list whenever that file was missing. When a dependency audit flagged the ledger as row D6, it was moved to a Cloudflare Worker, and the new client was written so that unreachable-ledger failures raise.
Paste the prompt, share only the context needed to answer it, and treat the result as a draft for your review. Do not include confidential information or let an AI assistant make changes without your approval.
Optional: for a visual report and saved memory, run /dxdev first.
Don’t have it? Get it at dxdev.com/skills/dxdev. The prompt works without it.
dxdev LESSON · paste into your AI coding agent
LESSON: A Failed Read of a Data Source Must Raise Instead of Returning the Same Empty Value as No Data
SOURCE: dxdev.com/blog/lead-ledger-off-the-desktop
WHAT HAPPENED: The ledger of per-post lead rows lived in one jsonl file on one PC, and read_leads began with a check that returned [] if the file did not exist. That meant a missing ledger and a ledger with zero leads looked identical to the metrics report, and an existing test even asserted the 'no leads recorded' text for that case. The ledger was moved to a Cloudflare Worker backed by a SQLite Durable Object, with a RemoteLedger client that does a GET to read and a POST to append, using a bearer token and a 20 second timeout. The remote path has no branch that swallows errors, so an unreachable ledger raises, and callers already show exceptions as an error rather than a zero. The local Path behavior was kept for tests and offline runs, so the old [] on a missing file still exists there.
THE RULE: When code reads a source of truth, a failure to reach it must surface as an error and never collapse into the same value that means 'nothing there'. Keep the empty result for a source that was reached and genuinely holds no data.
CHECK MY CODE, then report PASS or FAIL with file:line for each:
1. Search every reader of a data source for an early return of an empty list, zero or default on a missing file, network error or timeout, and confirm each such return is deliberate and distinguishable from a real empty result.
2. Write a test that makes the source unreachable (missing file, refused connection, timeout) and assert that the reader raises, rather than asserting an empty or zero-valued report.
3. Confirm that every caller of the reader either lets the exception propagate or reports it as an error state, so a dashboard or metrics report can never show a zero count for a source it failed to read.
THEN PRINT: a table (check, PASS/FAIL, evidence, fix) + a verdict (applies / partially / OUT_OF_SCOPE / no) + the single most important next action.