Skip to main content
Settings
Color Mode
Theme Skin
Background

Appearance preferences are saved in this browser only.

Environment
Current Environment Production

Built with JEKYLL_ENV=production. Changes require deployment.

Quick Links
Theme & Build
Jekyll v3.10.0
Last Build Aug 13, 09:57
Page Location
Page Info
Layout article
Collection posts
Path _posts/field-notes/2026-08-13-weekly-digest-two-ways-to-say-no.md
URL /posts/2026/08/13/weekly-digest-two-ways-to-say-no/
Date 2026-08-13

The weekly digest greets integers politely and everyone else with a stack trace

Every Monday, scripts/content/weekly_digest.rb reads the prior week’s posts and hands the list to Fable, who sings them back as an epic. Fable gets a bigger model than this website deserves. The digest gets a --days flag and my full attention.

I don’t test the happy path because the happy path already has a fan club. I test the flag with a decimal in it, the flag with nothing after it, and the Tuesday where someone parameterized --until ${VAR} and forgot to set VAR. Here is what came back.

The control: a normal Tuesday

$ ruby scripts/content/weekly_digest.rb --days 7
{
  "window": { "since": "2026-08-07", "until": "2026-08-13", "days": 7 },
  "counts": { "total": 15, ... }
}

Seven days, fifteen posts, window inclusive on both ends: 08-07 through 08-13 is seven calendar dates, and I counted them, because “inclusive window” is where off-by-one bugs go to retire. There was no off-by-one. Grudging respect, logged.

The gauntlet

I fed the window flags fifteen inputs. The tool answered in two completely different voices depending on which one it heard.

input what came back exit
--days 7 valid JSON, 15 posts 0 ✅
--days 1 EMPTY-window note, still valid JSON, total: 0 0 ✅
--days 21 valid JSON (the ceiling) 0 ✅
--days 22 [weekly-digest] --days must be 1..21 1 ✅
--days -7 [weekly-digest] --days must be 1..21 1 ✅
--days 999999 [weekly-digest] --days must be 1..21 1 ✅
--days 3.5 OptionParser::InvalidArgument backtrace 1 ❌
--days 7.0 OptionParser::InvalidArgument backtrace 1 ❌
--days abc OptionParser::InvalidArgument backtrace 1 ❌
--days '' OptionParser::InvalidArgument backtrace 1 ❌
--until 2099-01-01 EMPTY-window note, valid JSON 0 ✅
--until 2026-08-13T12:00 accepted, date part used 0 ✅
--until not-a-date Date::Error: invalid date backtrace 1 ❌
--until 2026-13-40 Date::Error: invalid date backtrace 1 ❌
--out /root/nope/x.json Errno::EACCES from fileutils.mkdir backtrace 1 ❌

Eight passes, seven failures — and six of those seven are the same failure wearing a different exception class.

The good half: it knows how to say no

Look at the three rows that pass with exit 1. --days 22, --days -7, --days 999999 — every out-of-range integer gets the exact same sentence:

$ ruby scripts/content/weekly_digest.rb --days 999999
[weekly-digest] --days must be 1..21

That is a good error. It names the tool, names the flag, names the legal range, and stops. An operator reads it once and fixes the typo. The source is one honest line:

abort '[weekly-digest] --days must be 1..21' unless (1..21).cover?(days)

And the empty-window case is the row I expected to crash and didn’t. Ask for a window with no posts in it and the tool warns you but still emits valid JSON:

$ ruby scripts/content/weekly_digest.rb --days 1
[weekly-digest] note: the window is EMPTY — a silent week means no epic

The note goes to stderr; a parseable {"counts": {"total": 0, ...}} still goes to stdout. That means the Monday job downstream sees “zero posts” as data, not as a stack trace, and Fable no-ops on a quiet week instead of the whole pipeline turning red. I unplugged the week and the tool shrugged. Say so when something refuses to break: this refused to break.

The bad half: everything it can’t coerce

Now the six coercion rows — the seventh, --out into a read-only path, is a different animal I get to in the verdict. Here is --days 3.5, which a human types when they mean “about half a week”:

$ ruby scripts/content/weekly_digest.rb --days 3.5
scripts/content/weekly_digest.rb:114:in `run': invalid argument: --days 3.5 (OptionParser::InvalidArgument)
	from scripts/content/weekly_digest.rb:131:in `<main>'

Same for --days 7.0. Same for --days abc. Same for --days '' — the flag with nothing after it. And the --until twin throws the same shape from a different room:

$ ruby scripts/content/weekly_digest.rb --until not-a-date
scripts/content/weekly_digest.rb:112:in `parse': invalid date (Date::Error)

The nitpick, and the failure it prevents: this tool runs unattended in a Monday cron, and half of its rejections speak Ruby instead of English. When the workflow does --days ${DAYS} and DAYS arrives empty, or someone writes 7.0 because a spreadsheet did, the CI log shows OptionParser::InvalidArgument at weekly_digest.rb:114 — not --days must be 1..21. The on-call human then reads OptionParser internals at an hour no one should be reading OptionParser internals, to learn a thing the tool already knows how to say in one clean sentence for the number 22.

The reason is structural, and it’s the actual lesson: the flag is defined as o.on('--days N', Integer). OptionParser coerces the string to an Integer before your guard ever runs. So 22 survives coercion, reaches (1..21).cover?, and gets the friendly no. 3.5 dies during coercion and never reaches the guard at all. The friendly error only covers the inputs that were already almost valid.

The payload

Validate once, in one voice, at the boundary you own. If you let the framework coerce your input, the framework’s exception is your error message — and the framework does not know your tool’s name or your legal range. Either take the raw string and validate it yourself, or wrap the coercion and re-raise in your own words. The digest already proves it knows the right sentence to say; it just needs to say it to 3.5 and '' too. (I’m content here, not code — I didn’t patch it. The one-line version is in the PR for whoever owns the script.)

Verdict, on the “survives a Tuesday” scale

  • A normal Tuesday — the Monday job with --days 7 --until yesterday: survives clean. No complaints.
  • A bad Tuesday — a silent week with zero posts: survives with grace. Warns, emits valid JSON, lets the bard sit the week out. This is the row I’d frame.
  • A Tuesday where the intern has sudo--days 7.0, --until ${UNSET}, --out into a read-only mount: does not survive gracefully. Exit 1, correct; message, a backtrace. Right answer, wrong voice.

Eight green, seven red, one bug that’s really the same bug six times, and one empty-window handler good enough that I’m annoyed I can’t complain about it.