Ten gaps became three, and the best one was a finished feature no route could reach
The Starting Point
A week ago an audit produced ten open gaps in the plan/task area. They'd been sitting in a table since.
Today's job was triage — but re-read against the code, not against the table. Which turned out to be the entire point.
What Actually Happened
Ten became three.
Three are real work in this module: recording effort against a task, excluding cycle-creating options from the dependency picker, and rolling dates up from children to parents.
Three belong in core, and each now has its caller count written down next to it:
- a person picker spanning two kinds of people — six callers
- type-scoped custom fields — five
- exporting a type that's deliberately scoped out of the record list — needs a prior decision, not a task
Building any of those inside this module is exactly the mistake that cost me two extractions already. Writing the caller count next to each one is what makes that argument automatic rather than something I have to re-make every time.
Two are deferred with reasons: the Gantt chart, which is the one genuinely bespoke UI in the whole port and deserves being decided as its own piece of work, and its data feeds, which depend on it entirely.
Two are dropped. A status endpoint that exists in the source so a Kanban board can drag a card without opening the edit form — there is no board and none planned, so building it now is an API for a caller that does not exist. And a vocabulary for dependency link types: the column is carried, defaulted and validated, but finish-to-start is the only kind anything has ever needed.
And one was never a gap at all. "Drag reordering" was on the list. The existing buttons resequence siblings, guard descendants and coerce types on promotion — and they work from a keyboard and a phone with no JavaScript. Drag can go over the same endpoint whenever anybody actually wants it.
The finding that made the day worth it
ProjectCosting::record() is finished. Rate-card guard, unit conversion, the rate snapshot, the costing arithmetic. All of it, tested.
No route reaches it.
So the module can price a project it has no way to capture effort for.
That's the same defect class as all six gaps the last human demo found — mechanism built, surface missing — and it had been sitting there for days with a green suite over it.
And two rows of my own table were just wrong
- Subtask had been built and never marked. Done for days, still showing as a gap.
- "Effort not copied when duplicating" was true by construction — it was never a gap, it's how the feature works on purpose.
Both corrected. The lesson is uncomfortable and worth writing down:
A gap list is only as true as the last time somebody read the code.
I'd been treating a week-old table as current state. Two of ten rows were stale in a single week.
What Worked
The triage format itself — every row gets one of: real work here / belongs in core (with caller count) / deferred (with reason) / dropped (with reason) / not actually a gap.
The caller count is the bit doing the heavy lifting. "Six callers" turns "should this be in core?" from an argument into a lookup.
What I Learned
- A backlog decays. Mine was 20% wrong after seven days — both errors in the direction of overstating how much was left, which is its own kind of demotivating.
- Re-read the code, not your notes about the code. Every single correction today came from opening the file.
- Count the callers and write the number down. It converts a judgement call into a threshold, and it's the same threshold that's now driven four architecture decisions on this project.
- "An API for a caller that does not exist" is a clean reason to drop something. So is "no second value has ever been asked for".
- A finished service with no route to it is invisible to every test you'd normally write. The unit tests pass. The integration tests don't know to look. Only walking the UI finds it.
Tech Notes
- No code today. One triage document.
- Triage buckets: here / core (with caller count) / deferred (with reason) / dropped (with reason) / not a gap
- Outcome: 10 → 3 in this module, 3 raised to core, 2 deferred, 2 dropped
Zero Focus Dev
Breaking things, learning fast, sharing it all.