Back to blog
porting process documentation architecture

The table before the code

The Starting Point

Yesterday's audit found six behaviours I'd missed porting because I'd read the schema and not the controllers. The correction I wrote down was: every remaining area gets its parity table before its code, not after.

Today was the first test of whether I'd actually do that, or just say it. Approvals is next.

What Actually Happened

Not much, and that's the point. One commit.

I read the two approval controllers and both models before writing a single row of the table. No code.

Three findings, and the useful thing is that two of them are not about effort:

Three of the source's behaviours depend on modules that don't exist anywhere in this codebase yet — a document library, in-app notifications, and outbound email. Not "hard", not "deferred" — blocked on modules that haven't been built. Before the table, I'd have discovered that halfway through implementing, guessed at a workaround, and shipped a slightly-wrong version of a feature I'd then have to unpick.

The workflow itself has no such dependency. Create, decide, recompute the outcome, remove an approver, rotate an invite link, audit trail — all of that is real, bounded work I can do now. So the table cleanly separates "buildable today" from "waiting on something else", which is exactly what I couldn't see before I wrote it.

The external share-token link is the first unauthenticated surface in this package. Somebody outside the platform acts on a record with a possessed token as the only credential — no session, no permission check, no organisational context. That's worth deciding as a contract rather than building ad hoc, especially because the next module will need the identical shape.

I flagged it and deliberately didn't design it today. Designing a shared contract for one caller is how you get an abstraction shaped like a single use case.

What Worked

The table format itself: one row per behaviour from the source, and no row may be blank or absent. Every row carries a status — Ported, Partial, Deferred, Dropped, Open — plus a reason when it isn't Ported.

The "no blank rows" rule is doing more work than it sounds like. A blank row is where a missed behaviour hides. If you have to write something in the status column, you have to actually decide.

What I Learned

  • Writing the audit before the code costs an hour. Writing it after cost me a day. Same information either way.
  • "Blocked" is a much more useful status than "todo", and you only find out which one you're in by reading the source properly. Three rows here turned out to be blocked on unbuilt modules, not on my effort.
  • Spotting the first unauthenticated surface in a package is worth a deliberate pause. It's the kind of thing that gets built ad hoc the first time and copy-pasted the second, and by the third you've got three subtly different token-resolution paths.
  • Wait for a second real caller before lifting anything into a shared contract. One caller isn't a pattern, it's a preference.
  • A one-commit day where the commit is a document doesn't feel like progress and mostly is.

Tech Notes

  • No code. One markdown parity table.
  • Format: one row per source behaviour × {Ported, Partial, Deferred, Dropped, Open}, reason required for anything not Ported
  • Read before writing: both controllers (authenticated and public/token), both models
Zero Focus Dev

Zero Focus Dev

Breaking things, learning fast, sharing it all.