Day 51 of 55 · Tuesday · Learning

Exit codes are a language, not a verdictConcept

An exit code is a one-byte message the callee defines, not a universal pass/fail. Two of this week's data-loss bugs came from reading that message wrong.

Catch-up progress
51/55
Why this matters to you

rm_composite.py exits 2 to say 'no annotations on this document'. rm_pull.py treated every nonzero exit as fatal — and then marked the failed pull as seen, so real returns were silently dropped until both bugs were fixed on 8/18.

Unix processes end with a number from 0 to 255. Convention says 0 is success — everything else means whatever the program's author decided. grep exits 1 for 'no lines matched', which is information, not failure. Your own rm_composite uses 2 for 'no ink found', which is a perfectly normal outcome for an unmarked page.

The bug pattern is a vocabulary mismatch: the callee speaks one dictionary ('2 = clean, nothing to extract') and the caller reads another ('nonzero = broken'). Neither side is wrong alone; the system is wrong together. The only fix is making the contract explicit — document what each code means where the caller can see it, and branch on specific codes, never on 'nonzero'.

The second 8/18 bug is why this matters more than tidiness: on failure, rm_pull still advanced its seen-state, converting a transient error into permanent data loss. State updates belong AFTER the success branch — a rule that generalizes to every pipeline you own.

Worked example

Interrogate exit codes by hand, then find the specific-code branch in the fixed rm_pull.

grep -q zzz-not-there /etc/hosts; echo "grep: $?"
ls /no/such/dir 2>/dev/null; echo "ls: $?"
grep -n "returncode" /Users/tom/Claude/PDB/scripts/rm_pull.py | head
▶ Do it now
  1. Run rm_composite against one processed return in FOR TOM/PROJECTS/REMARKABLE/returns/_processed/ and `echo $?` — you should see 0 (it had ink).
  2. Read the returncode handling in rm_pull.py and say out loud what 0, 2, and 'anything else' each do now.
  3. Confirm the state rule: find the line that writes rm-pull-state.json and verify it's only reached on the success and clean-no-ink paths, never after a failure.

Gotchas

Go deeper: Exit status conventions (GNU docs)
One-card takeaway

Branch on the specific code the author documented, and never advance state on the failure path.