Writing & build logs.
Essays on software development, web builds, and AI tooling. Plus build notes from what I'm actually working on right now.
Sender reputation first: why the Inbox Hub has no messages table
Building an inbox triage surface meant deciding what goes in the primary lookup table. The answer wasn't messages — it was senders. Five SQLite tables, a partial index on pending unknowns, and a 60-second badge later, the schema design is the product.
I built six sprints of article scheduling. For four of them, nothing fired.
Four sprints of Telegram button wiring, queue state machines, and free-text time parsing — all recording intent into `article_queue` without dispatching a single article. Sprint 5 was the first one that actually fired. Separating mechanism-to-record from mechanism-to-execute isn't a new idea; applying it deliberately at the sprint level is.
The schedule queue took four sprints to remember. One to act.
After four sprints of B-schedule, a scheduled article existed as a database row with a correct timestamp. No article had published. Sprint 5 added the sweep daemon and publisher_lock. That is when scheduling became real.
A missed slot at 09:00 should not silently vanish: building past-due grace into the article pipeline publisher
The `schedule_sweep` runs on a laptop. Laptops sleep. Without a grace policy, an article scheduled for 09:00 silently strands when the machine wakes at 09:07. Sprint 6 of B-schedule — past-due grace plus 48-hour hard-expire — turns that silent drop into a recoverable state with a reason, an audit trail, and a path back to publish.
The classifier is the product: how four branches replaced an inbox
Inbox Hub is five SQLite tables, twelve route handlers, four Shadow DOM components, and a launchd cron. Strip those away and what remains is a four-branch decision tree that runs on every message. Everything else is plumbing around that core.




