Don't Vibe Your Shit When It's This Big
Here's a chat I'm currently having with Claude Sonnet. Not Fable, not Opus. Sonnet, the "general" one.
Before I paste any of it, here’s what I was actually trying to get built. BenchBoard’s tablet board has a simple M/S toggle up top: Manage or Score. Right now, tapping Score just flips the app straight into scoring mode, no questions asked. I wanted a real gate in front of that button. If the game hasn’t started yet, tapping Score should stop the coach and walk them through a quick pre-game wizard first: scan the opponent’s lineup card with the tablet camera, let OCR read the names and positions off it, confirm which team is actually home and which is away (this isn’t always set correctly when the game gets created), review both lineups side by side, and only then drop into live scoring.
That’s the pitch. Simple sentence, ugly amount of code underneath it.
What followed was a long back-and-forth where Claude went digging through the existing codebase before writing anything, and it found something I didn’t expect: there were already two separate, half-built scorekeeping systems sitting in BenchBoard that didn’t talk to each other. One was the live tablet board I’d been building all session. The other was an abandoned “Coming Soon” prototype page that, buried inside it, already had a working 3-step wizard and the exact two-column drag-and-drop lineup component I was asking for, just wired to the wrong system and missing the home/away step entirely.
So the actual work became: pull the good parts out of the dead prototype, rebuild them as standalone components, wire in the OCR pipeline that had never actually been called by anything real, add a backend endpoint that didn’t exist yet for swapping home/away teams, and stitch it all into a new wizard that gates the real Score button. Claude worked through that in stages, extracting components, verifying each one still rendered, building and testing the backend endpoint, and confirming the OCR-to-roster matching worked end to end with fake scan data.
All of that went fine. Then we hit the part of the conversation worth actually sharing. I asked Claude, what are the advantages and disadvantages of the approaches it was offering me.
Here’s the screenshot for reference:

Claude Sonnet 5:
Before I build the wizard’s final piece, I hit a real architectural fork that I need your call on. I traced through useBattingLineup.js and useMainField.js (the hooks backing BattingLineup-v5.jsx and MainField-v5.jsx). Both are pervasively hardwired to StorageHelper.getTeamUserId(), your own team, throughout, not just at one entry point. Dozens of call sites.
Your spec says populate BattingLineup-v5.jsx with “whichever team is Away,” which, if your team ends up Home, means displaying the opponent’s data in a component that today can only ever render your own roster.
Option 1, read-only swap-in.
Advantages: zero risk to useBattingLineup.js and useMainField.js, the hooks behind the entire tablet board we just spent this whole session animating (the M|S tween, zoom mutual-exclusion, the lift-only Score transition). No chance of regressing any of that. Arguably correct behavior too, not a compromise: you can’t genuinely interactively manage the opponent’s live roster mid-game the same way you manage your own. That’s already reflected elsewhere in the codebase (isOpponent() gating, lineup codes, the separate LineupAmend flow for mid-game substitution corrections). A static view of the opponent’s confirmed lineup is what actually makes sense. And it’s fast: BattingViewOnly already exists, this is conditional rendering, not new architecture.
Disadvantages: visual inconsistency, since the same board slot sometimes shows the rich drag-and-drop UI and sometimes a plain static table, depending on whose team landed there. Needs careful styling so it doesn’t look like a jarring downgrade. And if a coach wants to fix a scan mistake on the opponent’s side mid-game, they’d go through the separate amend flow rather than dragging directly in that slot.
Option 2, full team-agnostic refactor.
Advantages: true uniformity, both slots always show the same rich, live, interactive UI regardless of whose team occupies them. Matches the literal spec text most directly, no representational gap.
Disadvantages: high regression risk. These hooks handle drag persistence, syncSnapshot, SignalR/live-game guards, JWT-gated logic, and more, used across the entire app (Home.jsx, Demo.jsx, live scoring), not just this wizard. Threading a clean team-override through all of that is a multi-session refactor on its own, not an afternoon’s work. It directly touches the exact files we just finished carefully verifying this session. And it’s likely a false economy: a lot of useBattingLineup.js’s internals assume an authenticated “my team” session (JWT checks, live-game guards) that genuinely don’t apply to an unauthenticated opponent team anyway, so even after refactoring, most interactivity would need to be disabled for the opponent case regardless, converging back toward something functionally similar to Option 1, just with far more code and risk to get there.
My recommendation stays Option 1. The risk/reward is lopsided, and Option 2’s main selling point, uniform interactivity, is largely illusory given the auth assumptions baked into those hooks. Want me to go with Option 1?
After reading all that, here was my answer:
Let me offer another option. It’s okay that the scorekeeper can make changes to the opponent. It doesn’t have to be read only. Same goes for the defense. This already happens in GameChanger. In fact, coaches usually will yell at the scorekeeper to make changes to the roster on the spot. The big advantage is that BenchBoard has the best UI to tackle these kinds of issues, especially as a scorekeeper. So you don’t have to make big changes to BattingLineup, BattingWithSubs, or MainField. You just need to feed the data into it. If a change is made by the scorekeeper, that’s fine, just push those changes into that TeamID and it’s logged by the system on what the scorekeeper is doing anyway. From my end, you can just populate the data. Make changes to the opponent’s as a scorekeeper if needed. You don’t have to worry about permission issues or making huge changes to the functionality. Just map back to the TeamID those changes belong to. Make sense? Tell me if there are any drawbacks to what I’m asking.
Its answer:
That reframes it usefully. Permissions aren’t actually the blocker (you’re right, there’s no reason a scorekeeper role couldn’t edit opponent data, and yes, GameChanger does exactly this). Let me verify the real constraint before answering, since I want to be precise about what “just feed the data in” actually requires.
Confirmed the real constraint, and it’s narrower than I made it sound. GlobalPlayerStore.jsx:35 is a single module-level singleton, roster: [], which both hooks read from. The actual problem isn’t permissions or drag/sort logic, it’s that BattingLineup-v5.jsx and MainField-v5.jsx can be on screen simultaneously representing two different teams (away lineup plus home defense), but there’s only one shared roster array to read from. Let me check whether the persistence functions are already team-parameterized, since that determines how small this fix can be.
Good instinct: the drag/drop/sort/edit logic itself never touches this. Only movePlayerDefensive, updateBattingLineup (pure store writes, no teamId param at all) and syncSnapshot (the actual persistence call) are relevant, and GlobalPlayerStore is a single flat singleton with no concept of a second team. Before I confirm this is safe, let me check one thing that could sink the whole approach: does the backend actually let a scorekeeper write changes tagged with the opponent’s teamId, or does it enforce that your JWT’s team must match the request?
This is very encouraging. The endpoint has [Authorize] but no check that req.TeamId matches the caller’s own team. It trusts whatever teamId is in the request body. Combined with a Game.Status == “Live” gate, explicitly commented “only the scorekeeper can modify,” confirming the app’s design already assumes a scorekeeper-vs-owner distinction, the backend already supports what you’re describing. Let me check the actual sync call site to confirm the teamId is swappable there too.
Good news: I checked the actual sync path (masterSync.js:162) and it’s exactly as I suspected. syncSnapshot() reads directly from the single global playerStore with no teamId parameter anywhere. But the backend side has no drawback. SaveMainBoardSnapshot, the endpoint that persists it, has [Authorize] but never checks that req.TeamId matches the caller’s own team. It trusts whatever teamId is in the body. Combined with the explicit Game.Status == “Live” gate (”only the scorekeeper can modify the lineup”) already baked into that endpoint, the backend design already assumes a scorekeeper can act on behalf of either team. This isn’t a gap I’d be punching through, it’s already there.
So: your approach works, and it’s real but bounded work, not the sprawling refactor I worried about. The concrete scope:
1. GlobalPlayerStore.jsx needs to hold a second concurrent team context, not full multi-team generality, just two, since a game only ever has two sides, instead of one flat roster, battingLineup, defensiveAssignments.
2. movePlayerDefensive, updateBattingLineup, syncSnapshot need an optional teamId parameter to know which context to read/write and which team to target in the API call. Today they’re 100% implicit/global.
3. useBattingLineup(), useMainField() need an optional teamId prop selecting which context they’re bound to, defaulting to “my team,” current behavior, unchanged everywhere else in the app: Home.jsx, Demo.jsx, Manage mode, and so on.
One real thing to flag, not a blocker: that Live-status 423 gate is on this endpoint specifically. Once the game actually goes Live, saves route through whatever the dedicated live-scorekeeper endpoint is (I haven’t checked that one yet). Our wizard only fires for Scheduled games, so this flow is unaffected, but the mid-game “coach yells at scorekeeper” case you described would hit that other endpoint. I’d want to verify it has the same no-ownership-check design before assuming it works identically, though the “only the scorekeeper can modify” comment strongly suggests it does.
Want me to proceed with this scope?
My answer after that:
DO IT!
To those who are doing agentic coding with a big-ass codebase like myself, particularly those who want to graduate from vibe coding:
Don’t vibe your shit when it’s this big.
Read what the AI is doing. Open that shit up to see the actual code changes. Understand them. Don’t know what something does? Simply ask. What the hell is ABC(XYZ)? What does that do?
It will not only help you understand your codebase, it will absolutely help you push back when the AI’s first answer isn’t the right one.
A good engineer knows there are always options outside of what the AI proposes.
An inexperienced vibe coder feels like there are only two options. Just like you’ll hear vets in professional sports teams tell newcomers, “just because you’re a rookie doesn’t mean you have to act like one.” Be someone who knows what to do codewise and knows what needs doing, knows how it’s built, knows the methods and containers being used in the discussion, so you can provide the right direction.
You don’t have to know every single syntax it will take to get there. That’s handled by the LLMs now. But it’s you who has the judgment, and that judgment is the engineering advantage nobody else at the table has. This is important.
Now go out there and make cool shit.




A very informative article from someone with over thirty years of experience in the trenches as a full-stack developer.
He’s now leveraging modern AI tools to make what was once infeasible not only possible, but practical.
This is what happens when deep experience meets the right technology. This is how it’s done, folks.