Part of Fourteen Automations That Run Without Me.

Contacts are the hardest personal data to automate, and the reason is not technical. It is that the cost of a wrong write is asymmetric and delayed. A merge that combines two different people looks fine for months, until you send the wrong person a message that assumes a shared history. A fuzzy match that links a name to the wrong email address poisons every downstream decision quietly. You will not get an error. You will get a slowly degrading picture of your own life.

So the first version wrote nothing.

The first slice should be the version you can leave running after you lose interest.

The first slice

One command, read-only, no network writes, safe to run a hundred times in a row: print how much of the people directory has at least one linked channel — an email address, a phone number, a profile — and list exactly where the gaps are.

That is the whole of version one. It took an afternoon. It told me my directory had roughly two and a half percent channel coverage, which was both embarrassing and precisely the thing I needed to know before building anything that depended on the data being good. Every ambitious version of this system I had sketched previously assumed coverage I did not have.

Three properties made that first slice worth shipping on its own:

If your version one cannot be described in one sentence and cannot damage anything, you have the right version one.

Phases with acceptance tests, not a roadmap

What followed was four phases, each of which ships alone, is useful alone, and carries written acceptance tests that must pass before the phase counts as done. Later phases — the ones that write to systems of record or add new data sources — each needed a separate explicit go-ahead, not a blanket approval of the design.

The acceptance tests are the interesting part, because they are written as properties rather than as features. A representative selection, in plain terms:

Read that list again as a design document, because that is what it is. Nearly every item is a prohibition, and each prohibition encodes a specific way the system could have quietly lied to me. "Missing data reports as unknown, never as overdue" is the difference between a tool that helps and a tool that manufactures guilt from an empty field.

Humans do the linking that machines cannot prove

Identity resolution is the part everyone wants to automate with a model, and it is the part I deliberately did not. Deterministic links — an exact name match, an explicit field in the directory — are applied automatically. Everything else becomes a suggestion in a review file.

I reviewed those suggestions in two sittings. Out of 118 partial matches I approved 80 and rejected 38. A rejection rate near one in three is the entire argument. Had those been applied automatically, roughly a third of them would have been wrong, and I would have had no idea which third. Coverage went from two and a half percent to thirty percent after the first deterministic pass, and past fifty percent after my review — real improvement, none of it guessed.

An approval also upgrades the record's stored method to "human confirmed", with full confidence. The database remembers not just what is linked but how it came to be linked, which means a future version can treat a human-confirmed link differently from an inferred one. Provenance in the schema is cheap at design time and impossible to retrofit honestly.

Redaction, and the mistake I had to clean up

The system writes a short memo into a repository as a record of each run. The first version of that memo contained message content. I noticed, decided it did not belong in version control, and rewrote the repository's history to purge it — then verified the purge by taking a fresh clone and searching it, rather than trusting the local result.

Two lessons. First, decide the redaction level of any artifact before the job starts writing it, and make the redacted-versus-full split a tested property, as in the acceptance list above. Second, when you clean up a leak in history, verify from a fresh clone. Your local copy is the one place where the old objects may still exist and where a search will happily reassure you.

A kill switch and a boring schedule

The whole thing runs once each morning, and it has a pause command whose only job is to make the next run exit before touching anything. The store lives outside any repository, with owner-only permissions, and it never syncs to a cloud folder.

That last detail sounds trivial and is not. A database being written by a background process, inside a folder being synchronised by a file-sync client, is a corrupted database in waiting. Keep live state out of synchronised folders. Back it up deliberately instead.

The pattern, stated generally

  1. Version one is a read-only report that is useful by itself and cannot damage anything.
  2. Every phase has written acceptance tests, mostly phrased as prohibitions, and does not count as shipped until they pass.
  3. Automatic actions are limited to what is deterministic. Anything probabilistic becomes a suggestion for a human, and the human's decision is recorded as provenance.
  4. Redaction is designed before the first write, and the split between full and redacted output is a test.
  5. A pause command exists and is proven to stop the next run before any adapter starts.

Applied to contacts this produced a system I trust with my relationships. The same five steps apply to your finances, your health records, your notes, or anything else where a silent wrong write costs more than a loud failure.

Read next: The Mechanical Check Test — the rule that decides which of these steps an agent may ever perform alone.