i-am-jacob:~$

~ / writing / fish-bite-scoring

The Catch Log Is the Reviewer

product engineering · scoring

Fish Bite looks like a fishing app. Underneath, it's an argument about how scoring systems lie — and the two times the model got caught lying are the most transferable engineering lessons to come out of this shop. This is the story of those lies, in order of embarrassment.

First lie: the tyranny of the average

Version one scored fishing spots with a weighted mean: nine factors, each rated zero to ten, blended into a final number. Mathematically respectable. Practically broken. Every spot scored a polite 7-to-9 because nine decent factors will always outvote one terrible factor — even when the terrible factor is the one that decides whether fish exist.

A brutal midday lull, a post-frontal high-pressure ceiling — these aren't deductions. They're vetoes. Fish don't care that the moon phase is excellent if the water temperature says nobody's home. But a weighted mean has no mechanism for "this one thing overrides everything," so the app cheerfully recommended dead water with confident orange badges.

The fix wasn't better weights. It was a different shape:

// the shape that worked

Dominant factors became multiplicative modulators, not additive weights. A tough-regime verdict multiplies the whole score down; a prime window lifts it up. And the verdict badge and the number are computed from the same object — one function returns a state and its multiplier, the UI renders the state, the score consumes the multiplier. They physically cannot disagree anymore.

Generalize the lesson: any scoring UI where the label and the number come from different code paths will eventually contradict itself in front of a user. Compute once, consume twice.

Second lie: the model knew better than the fish

After the modulator rebuild, the time-of-day curve was textbook: crepuscular peaks, a midday trough floor near half credit. Textbook — and contradicted by the catch log, which held multiple proven midday catches on soaked bait at a familiar riprap spot. The fish had not read the textbook.

This is the moment every model-builder faces: the data disagrees with the curve. The lazy move is to dismiss outliers. The honest move is to notice that six logged midday catches is not noise — it's a pattern with bait and locations attached. The crepuscular-only assumption was wrong for how this angler actually fishes, so the midday floor got raised, recalibrated from logged reality instead of literature.

Then came the part that matters more than the calibration: both fixes got pinned by regressions named after their own failures. One test fails if a tough badge ever appears next to a score of nines again. Another fails if midday ever gets undersold for soaked-bait patterns again. The bugs are now load-bearing — the old wrong behavior can't quietly return, because CI remembers.

Lie prevention: sources that admit weakness

The engine fuses live feeds — weather, solunar timing, water gauges. Feeds die; it happened within the first month. A scoring system that hides missing data produces confident nonsense. Fish Bite degrades loudly instead: when a source drops out, the affected factors renormalize over what remains and the UI reflects the reduced confidence rather than pretending nothing happened. When a lake has no gauge, the system preserves the absence rather than borrowing a river's numbers from downstream — a nearby gauge that measures different water is worse than no gauge.

Same discipline on the knowledge side: species lists and behaviors get verified against state wildlife resources before entering the seed data, not invented. An early hand-written species list confidently included a species the state agency says doesn't live in these waters — which is exactly the kind of error that survives forever unless something external is allowed to say "no."

model overruled
v3+scoring architecture
0silent degradations
100%fixes regression-pinned

The transferable version

  • Averages hide vetoes. If one input can invalidate the whole answer, it needs multiplicative power, not a weight.
  • Label and number must share a source. Two renderings of one truth drift; one computation consumed twice never does.
  • Ground truth outranks theory. When the log and the curve disagree, the log wins — then find the assumption that made the curve wrong.
  • Name regressions after embarrassments. A test called tough_badge_never_next_to_nines documents the failure better than any comment.
  • Preserve missingness. "Unknown" is honest; "borrowed from a neighbor" is a lie with extra steps.

// field note

The best property of this system isn't accuracy — it's accountability. Every score change traces to a logged catch or a documented source failure. The catch log is the reviewer, the reviewer keeps receipts, and the model has learned humility accordingly.