Lossless Changelog
fleet

A photo of a sign-in sheet, turned into a group chat

A roster photo looks legible and is wrong often enough to poison the whole list. Crop into full-res bands; never trust the overview.

Michael StatonMusic Editoraugmented with Claude Code on Claude Opus 5 (1M context)

Why Care?

You leave a conference, a cohort weekend, a retreat. You have a phone photo of a printed sign-in sheet with thirty-three names and mobile numbers on it, and you want a group chat before everyone's momentum fades. The gap between those two states is about forty minutes of miserable typing, and every minute of it is an opportunity to get a digit wrong.

Getting a digit wrong is not a rounding error. It means a stranger is in a private group chat, and nobody finds out until something awkward is said.

That is the real problem this skill solves, and it is why the centre of it is not the vCard generation — that part is mechanical. It's the transcription discipline, because OCR from a photo is confidently wrong about 0/8, 1/7, 3/8, and 5/6, and confidence is exactly what makes it dangerous.

What's New?

create-contact-list-from-image-or-spreadsheet/ — takes a roster in whatever form it arrived (phone photo, screenshot, PDF, CSV, spreadsheet) and produces a verified contact CSV, importable vCards, and a paste-ready list of E.164 numbers.

image/PDF/sheet  →  transcribe  →  roster.tsv  →  build-contacts.py  →  deliverables
                    (bands)        (source of      (mechanical)
                                    truth)

roster.tsv is deliberately the durable intermediate: everything downstream regenerates from it, so a transcription fix is a one-line edit plus a re-run rather than a hand-patch across six files.

The band-crop discipline

The headline rule — never transcribe digits off a downscaled overview. Read the overview once, at ~1400px, to learn column layout and row count only. Then crop the table into overlapping full-resolution horizontal bands, upscale, sharpen, and read each band in order.

The overlap isn't padding; it's a free consistency check. The last row of band N reappears as the first row of band N+1, so a misread at a boundary reveals itself.

It also encodes the two image gotchas that break this silently: HEIC isn't directly readable, and EXIF orientation lies — a 5712×4284 "landscape" file is often a portrait photo, so auto-orient before computing any crop box or every offset is wrong.

Flag, don't fix

Transcribe verbatim. Hollly with three L's stays Hollly. A blank phone cell stays blank. A date in the wrong year stays wrong. The script surfaces rows with no number, duplicate numbers, non-standard digit counts, and odd spellings — and fixes none of them.

Two people sharing a number is sometimes real (a couple; a participant who is also staff) and sometimes a printing artifact. The user knows which; the agent doesn't. Silent "helpful" corrections are how a typo becomes permanent and untraceable.

Three findings worth having even if you never run this

  • E.164 always. +1XXXXXXXXXX is what makes WhatsApp, Signal, and iOS reliably match a saved contact to a messaging account. (805) 455-1379 in a TEL field works sometimes, which is worse than never.

  • One vCard format covers both platforms. vCard 3.0 is read natively by iOS, Android/Google Contacts, macOS, and Outlook. Asked for "two formats if necessary", the correct answer is that it isn't — say so rather than shipping a 4.0 twin that adds no reach.

  • The name prefix is not cosmetic. WhatsApp's group picker searches contact names only — not company, not notes, not labels. Prefixing every card LV236 means typing LV236 once surfaces the whole cohort instead of hunting thirty-three names one at a time. Ship the -cleannames variant alongside and let the user choose tidiness over speed.

numbers.txt is a consumable, not a record

The best observation in the skill, and the one most likely to save a support conversation. The user pastes numbers into a To: field and deletes each as it lands, so the file empties as they make progress. A blank numbers.txt is not corruption — it's a finished job, and the leftover commas are the tally.

Two hard consequences follow: never regenerate a file the user already has without asking, and never diagnose a changed artifact as a defect when a human has had their hands in it. "This file is empty" and "this file is done" look identical from the outside.

Notes

Privacy is structural, not advisory. A roster is dozens of people's personal mobile numbers and they did not consent to being in a git repo. Deliverables are written to ~/Downloads/<group>/ — outside any working tree, never context-v/, never a scratch dir inside a project — the generated README says not to commit them, and the worked example in the skill is deliberately abbreviated with no real numbers reproduced.

build-contacts.py is stdlib-only — argparse, csv, io, pathlib, re, sys. No phonenumbers, no vobject. Consistent with the house rule that a hundred lines of obvious code beats a dependency, and it means the script runs anywhere python3 does.

On WhatsApp specifically: there is no supported bulk-add, and the Chrome extensions claiming otherwise want full access to a WhatsApp Web session and carry ban risk — the skill says do not recommend them. The vCard import is the bulk step. The fallback that actually scales is the invite link SMS'd to the whole list at once, which sidesteps every privacy setting; expect 60–80% in the first hour rather than everyone instantly.

One gotcha recorded here is environmental and bit us again this session: ls is aliased to eza, so ls -la <dir> | wc -l fails on flag parsing. Use find instead.