Your rule: a score should never show up as a blank dash. This is the full walkthrough — how a grade actually reaches the screen, exactly where the blanks come from, the fix for each one with real numbers, and the three calls I want from you before I move a single grade.
Every read (Is the price fair? · Will you love it? · Holds its value?) and the overall grade always resolve to a letter — a fact-based grade when there are no reviews yet, upgraded to real opinion once a set has been mined. The honesty moves to the small confidence line underneath ("Early read"), never to a missing number.
When the engine doesn't have enough to say something, it shows a dash — — — instead of a letter. It does this on the individual reads and, for some sets, on the big overall grade too.
You've been clear about why that's wrong, and I want to restate it precisely because it drives every decision below. A blank fails on two counts at once:
The original spec's principle #4 said "honest blank — withhold when there isn't enough." You've since reversed it: the blank isn't honesty, it's a hole. Honesty now lives in the confidence line, which can say "Early read" out loud without hiding the grade. This doc is that reversal, made concrete and testable.
To see where the blanks come from — and why one fix is nearly free and the other is real surgery — you have to see the assembly line. Here it is in plain language.
drop_scores row per set and lens.The three reads are decided at step 6 — a mapping applied when the page is drawn, on top of numbers we already saved. The overall grade is decided at step 3 — inside the engine's math. That's why fixing the reads never moves a grade, and fixing the overall does.
"Never-blank" is really two separate jobs with very different risk — and that's good news, because one is nearly free and totally safe.
| The 3 reads (price / love / hold) | The overall grade | |
|---|---|---|
| Decided at | Step 6 — the draw-time mapping (gradeToInput.ts) |
Step 3 — the engine's math (score.ts) |
| Where the blank comes from | Each read is mapped to one saved signal; blank when that signal is insufficient | An "abstain" rule hides the letter when evidence is thin |
| Does fixing it move any saved grade? | No pure re-draw from data we already saved | Yes it changes the saved letter |
| Database re-write needed? | None — changes on the next page load | Offline recompute — the same safe pattern that activated investor value |
| Risk | Low · display only · reversible in one file | Core engine · test-driven · re-grade gated on your go |
The saved grade is a printed nutrition label on a box. Fixing the reads is like re-reading the same label and filling in a blank line using numbers already printed on it — the box doesn't change. Fixing the overall is like re-running the lab test and printing a new label — so we do it carefully, test it, and only re-print the whole catalog once you say go.
All three reads get a guaranteed answer. Two keep their current signal and gain a fallback; one — "Will you love it?" — gets rewired to the right signal entirely. Here's each, with the reasoning and real numbers.
Keeps Smart Price whenever it can score. Smart Price answers "for what this costs, is the value there?" — ideally by comparing the set's part-out value to its price, and ranking that against similar sets.
Why 32% go blank: those are sets where we have no part-out value and no cohort of comparable sets to rank against. The value math has nothing real to stand on, so — correctly — it refuses to invent a number. It won't fabricate a neutral 6.0.
The fallback: fall back to "What You Get" — our content-value-for-the-money signal (non-figure prints + element variety, minus filler), which we compute on virtually every set. It's the honest sibling of "is the price fair," and it's carried at lower confidence so the line underneath stays truthful.
The fallback is emphatically not a price-per-piece or cost-per-brick number. That stays banned — in the copy and enforced in code. "What You Get" is about content richness, not dividing dollars by bricks.
This is the big one, and it's the source of the 98% blank. Today "love" is wired to a signal the engine itself describes as "a capped nudge, not a real sub-dimension." It only fires when a reviewer literally voices "worth it" — so even the X-Wing, which has been mined, still shows blank for love. It was never a real home for the question.
"Will you love it?" is really an enjoyment question. So I'd back it with an enjoyment proxy in two layers: a fact baseline that's always there, upgraded by real opinion once reviews exist.
Here's the same set, worked all the way through, so the mechanic is concrete. Recommended weights: fact baseline = What You Get 50% · Minifig lineup 25% · Build length 25%; then once mined, blend opinion over facts 60/40.
| Stage | The math | 0–10 | Letter |
|---|---|---|---|
| Fact baseline un-mined set |
What You Get 7.0 × 0.50 + Minifigs 6.0 × 0.25 + Build Length 8.0 × 0.25 = 3.5 + 1.5 + 2.0 |
7.0 | B− |
| Opinion upgrade after mining |
Opinion mean (Display 8.7, Fidelity 8.0, Build Fun 7.8) = 8.17 Blend: 8.17 × 0.60 + baseline 7.0 × 0.40 = 4.9 + 2.8 |
7.7 | B |
The un-mined set shows a real, defensible B− for "will you love it" today — no blank. The same set, once mined, quietly sharpens to a B built on actual reviewer sentiment. Mining upgrades the read instead of filling it from empty — which is exactly the seam the review-mining half of this session plugs into. Figure-less sets simply reweight the remaining two facets so the baseline still holds.
Already lands on a letter about 98% of the time, because its main input — appreciation outlook (age progress toward retirement × theme desirability) — always scores, even for a brand-new set (age 0 still produces a number, just at lower confidence). The remaining ~2% are edge cases with no part-out value at all. I'll add a small floor so those can't slip to a blank either. This one is a safety net, not a rewire.
Everything above is a change to one small mapping file (gradeToInput.ts) that runs at step 6 — when the page is drawn. It reads signals we already computed and saved. No engine re-run, no re-grade, no database write. The reads simply fill in on the next page load, and it's reversible by reverting one file.
This is the real engine move. For some sets the engine looks at the evidence, decides it's too thin, and hides the overall letter. The change: if we have facts for a set — and we always do — it always bands to a letter.
The exact rule today, in plain words: the engine withholds the overall grade when "there's no strong value anchor and fewer than two lens areas managed to score." That "no value anchor + too few areas" condition is the main thing hiding overall grades. (There are two other conditions — "literally nothing scored" and "the overall math came back empty" — but every real set has fact signals like build length and appreciation that always score, so those two effectively never fire.)
The change: retire the "no value anchor" abstention for any real set. Because fact-based signals always produce something, there's always a legitimate fact-derived letter to show. The grade goes from "we won't say" to "here's our best read from the facts, labeled as early."
This is a genuine shift in what the app claims. Sets that showed a dash will now show a letter — a real read, not a placeholder. I think it's the right call and it's the one you asked for; I want it on the record before I move a single grade. And note the ripple: the phrase "Not enough to grade yet" effectively retires for real sets — a visible copy change, not just a numeric one.
We don't lose honesty — we relocate it. The confidence line under every grade already knows how thin the evidence is and says so in plain words. That line does the work the blank used to do, without looking broken.
| Situation | The line the shopper sees | What it signals |
|---|---|---|
| Not released yet | "Early read — sharpens as reviews land" | Facts-only prediction, openly labeled |
| Just launched (≤5 reviews) | "Early read · 3 reviews" | A letter, but hold it loosely |
| Well reviewed | "Based on 28 reviews · solid" | Firm, sentiment-backed grade |
So a brand-new set with no reviews still shows a letter, clearly flagged as an early, facts-only read. A set with 30 reviews shows the same letter styling but a confident line. The shopper always gets a grade and an honest sense of how firm it is — which is strictly more informative than a dash that says nothing.
The things I'd make sure can't go wrong, stated up front.
| Edge case | How it's handled |
|---|---|
| A set with genuinely no facts (no piece count, no price) | That's not a real product page — the app already returns "not found" upstream, so there's no card to blank. Never-blank applies to real sets, which always have facts. |
| A figure-less set (no minifig lineup) | The love baseline drops the minifig facet and reweights What You Get / Build Length so it still resolves — no divide-by-missing. |
| The price fallback drifting toward price-per-piece | Impossible by construction — it reuses "What You Get" (content richness), and the existing code guard rejects any per-brick phrasing in copy. |
| Old grades saved before this change | The reads re-derive at draw time from saved signals, so old rows light up too with no migration. The overall change is applied by the offline recompute, which re-reads each saved row. |
| A grade resting on thin evidence | Shown as a letter with an honest "Early read / thin" line — the whole point. Confidence is disclosed, never used to hide. |
These are the only real judgment calls. My recommendation is first in each, with the alternative I considered and why I set it aside. Say the word and I run with all three, or redirect any of them.
Recommendation: fall back to "What You Get," at low confidence.
It's the honest cousin of "is the price fair," we have it on ~100% of sets, and it never touches price-per-piece.
Alternative considered: let price stay the one read that can still say "can't price this yet." Set aside — it violates the never-blank rule and reintroduces a dash on the read shoppers care about most.
Recommendation: baseline = What You Get 50% · Minifigs 25% · Build Length 25%; opinion blended over facts 60/40 once mined.
These are tunable constants — easy to dial after you see them on real sets. The worked example in §04 uses exactly these.
Alternative considered: have opinion fully replace the baseline when mined. Set aside — a hard swap makes a set visibly jump when its first review lands; a 60/40 blend sharpens smoothly instead of lurching.
Recommendation: no — any set we have facts for always shows a letter. "Not enough to grade yet" retires.
The confidence line carries all the honesty. The only thing that could still show no grade is a set with no facts row at all — which isn't a real set page.
Alternative considered: keep abstaining for pre-release sets with zero reviews. Set aside — those are exactly the sets a shopper is researching before buying; a facts-based "Early read" serves them far better than a dash.
This is the plan you asked to see before I touch the scoring engine. Nothing is built yet. Give me a go — or redirect any of the three calls in §08 — and I'll start with the safe reads PR, then bring you the overall-grade change and the re-grade for a final green light.