A rule, correctly applied, in the wrong place
I investigated a scoring tool and wrote a nine-part plan to rebuild it, every claim checked against the code. Four of its points still turned out wrong, each correct on its own.
Inner Squad is the tool my brother and I use to load games, score them and settle seasons for MegaSquad, our pick’em site. Settling a season finalizes its standings and emails a recap to the people who played. Two people use the tool, and it writes to a backend that settles real standings, so a silent wrong write is about the worst thing it can do.
In September I rebuilt it. I started by investigating: the old code, the live data, and the API it talks to. Then I wrote up a plan in nine parts, each saying what to build, what to measure, what not to touch, and what the code and data would look like when it got there. Every claim in the plan was checked against the code or the running app.
I expected any mistakes to show up in the code. Instead, four of the plan’s points turned out wrong, even though each looked correct on its own. None was a false fact, which is why checking didn’t catch them. Each was true about one thing and applied to another.
Forty-four pixels for a box that’s always empty
TL;DR
The plan set a floor for phones: every interactive target at least 44 by 44 pixels. That’s a good rule, because a fingertip needs room. The plan’s part on phones measured the tool at 375 pixels wide against it, and none of its controls passed. The smallest it found were the seed boxes, 24 by 21 pixels.
A seed is a tournament ranking, the number beside a team in a bracket. The tool had drawn two seed boxes on every game since March 2026, whatever the sport. On a desktop they cost almost nothing, which is why nobody had questioned them. The phone work grew every target to 44 pixels, correctly, and that included them.
Checked later, across every cached week of 2025 and 2026, the split was total. 63 of 63 college tournament games carried a seed, and so did 56 of 56 NBA playoff games. 0 of 557 NFL games did.
So on an NFL week at phone width, each team’s line spent 44 of the screen’s 375 pixels, about 12%, on a box that could never hold a value. Add the 6-pixel gap beside it, and all of that came out of the team name. Its field was 177 pixels wide, and “New England Patriots” needs about 179. The notes from that phone work had already recorded that clipped name as a separate defect, not yet fixed. It was the same defect.


NFL week at 375px before after
seed fields on screen 32 0
team-name field 177px 229px
"New England Patriots" clipped fitsThe fix was a question the rule never asked: does this week use seeds at all? Seed boxes are now drawn only on weeks that do. The 44-pixel floor still holds for everything that’s drawn. It was right about how big a target should be. It said nothing about whether the target should be there.


186 of 186, with nothing open
TL;DR
The phone work ended on a clean result. Every control met the 44-pixel floor at phone widths: 186 of 186, and 197 of 197 with the Add Game form open. Both numbers were measured, and both were true.
Neither was measured with a dialog open. The close button on the dialog from my component library measured 20 by 16 pixels, the smallest control in the tool. The dialogs for the current week had carried it since the part before. The next part added dialogs that send email to a league, with the same button, and that part is the one that found it. The app got a local fix the same day, and the library fixed it for every app in version 4.12.0.
“186 of 186” was true of a set nobody had named: the page with nothing opened on it. The rule said every target. Neither the rule nor the count said in which state.
The right field for a different kind of pair
TL;DR
On MegaSquad, a squad is a group of friends, and one squad can run several leagues. The tool’s league list held five pairs of leagues with identical names. The plan said to tell them apart with squad_name, a field my brother had just added to the API for exactly that job. I had checked the pairs: same name, same sport, different IDs.
I hadn’t checked the season. In the development data, every pair had one squad name and two seasons. They weren’t duplicates at all. Each was a 2025 league beside the same league started again for 2026, in the same squad. squad_name separates leagues that belong to different squads, so within every pair it was identical. Reading the data before following the plan is what caught it. The season is now on every row, and within a pair the newer league sorts first.
The field still earns its place, at a different job. Three leagues sit in a squad with a different name, and those three show it. It groups leagues. It doesn’t tell a pair apart.
Ending a season ends it for everyone
TL;DR
The plan’s list of routes described POST /admin/seasons/end as “settle a league’s season”, and its part on ending seasons asked for it as an action on each league’s row.
The endpoint takes a sport and a season, not a league, and settles every league that matches. A dry run, which reports what would be settled without settling anything, showed the difference against the development data. For NFL 2025, the account running the tool could see 5 leagues, and the dry run found 8. A button on one league’s row would have run against all eight, three of them leagues the tool could never have listed, and emailed their players.
The menu on each row now speaks for the whole scope, not the row. It reads “End the season for NFL 2025”, the dialog opens with the same sentence, and the dry run has to happen before anything is sent.
The permission model had already decided
TL;DR
The plan got one scoping call right, and how it got it is the part worth stealing. Inner Squad is an admin tool, and adding a league sounds like admin work. The old tool even had code for it.
The API said otherwise. Creating a league needed a squad, and the permission check was that you were a member of that squad. It never asked whether you were a site admin. So creating a league is something any member does, and it belongs in MegaSquad, where the members are. The plan drew the tool’s boundary along the API’s permission boundary and left league creation out.
That settled the question by reading instead of by taste. It’s the other four in reverse: the answer to “where does this apply?” was already written down, and this time it was read before the plan was written.
Name the set
TL;DR
Look back over the four and none started from a false fact. Each was true about something, and left out what. “At least 44 pixels” didn’t say “for boxes that can hold a value”. “186 of 186” didn’t say “with nothing open”. “Tell the pairs apart” didn’t say how they differed. “Settle a league” described the button, not what the endpoint does.
Every claim had been checked. What each claim was about had not.
So the habit worth having is to write the scope as part of the rule. A size floor names what it applies to. A count names what was open when it was taken. A field’s purpose comes from the data it holds, not the note that announced it, and an action’s reach comes from the code that handles it, not the line that lists it.
Each fix was small once the scope was named. The Patriots fit on a phone, every league row shows its season, and ending a season names NFL 2025 before anything is sent. None of that came from following the rules more carefully. It came from asking each rule what it was about.