The #1 board item — three grading bugs the phone audit surfaced, all in the
live Node grade path (engine1 + analyzeViaEngine1), fixed at the source.
1. PROJECTION=0 NOW REFUSES. projectionFor returned l5_avg even when it was 0
(finite, so the `== null` gate passed it) — 9/25 live grades graded on a
zero projection, producing a degenerate edge and a hollow grade. Now a
non-positive reference is not a projection: projectionFor skips it and falls
through to the next POSITIVE reference (l5 -> l20 -> per_90 -> xg); when none
is positive it returns null and the read REFUSES (insufficient_data). The
gate also gained an explicit `> 0` guard so the invariant is structural — a
grade can never be emitted with a non-positive projection. Fewer graded
props, honest.
2. EDGE_PCT. The formula was already (model - line) / line signed by direction
— Kev's intended semantics. The broken {20,60,100,140} cluster was the
proj=0 degeneracy ((line - 0)/line = 100%); with #1 those refuse, so the
fabricated 100s vanish and real edges flow. The main-line edge now reuses
the VALIDATED projection (edgePctFor accepts an optional ref) so edge and
the persisted projection can never diverge. Frontend |edge|>40 guard stays
as a safety net.
3. LETTER == THRESHOLD_TABLE(CONFIDENCE). engine1's hand-rolled
GRADE_TO_CONFIDENCE drifted a full sub-tier low (B -> 0.55, which the
canonical grade_thresholds.json calls B-) — the "B at 45%" the audit caught.
Now confidence is DERIVED from each grade's band MIDPOINT in
grade_thresholds.json (one source of truth, shared with the Python engine),
so applying the threshold table to any grade's displayed confidence resolves
back to the same letter. Proven for all 11 grades.
Regression locks: tests/unit/mlbGradeDegradation.test.js (14 tests). Backend
suite 269/3253 green, web build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>