rohitsar567 Claude Opus 4.7 (1M context) commited on
Commit
4a63fa6
·
1 Parent(s): 5a4b5e8

fix(scorecard): rec-path scorecard == marketplace (KI-FULLFACTS) — restores recommendation cards

Browse files

ROOT CAUSE of empty CitedPolicyCards: brain_tools._scorecard_signal fed
build_scorecard the 7-key eligibility subset (_load_policy_facts), which is
below build_scorecard's data-completeness floor → every recommended policy
returned grade "—" / overall 0 → the Bug #71 >=70 fitness gate dropped 100%
of them → recommendation citations always empty → no cards. The marketplace
(/api/policies/all) scored the SAME policies correctly (HDFC ERGO Optima
Restore = A/78) because it feeds the FULL curated layer
(main._load_curated_facts, KI-219/251 canonical precedence).

Fix: _scorecard_signal now reuses main._load_curated_facts (lazy import —
main is loaded at request time; dodges the cycle) + the same
40-data/reviews/<slug>.json insurer reviews + profile that the marketplace
passes to build_scorecard. A policy's recommendation grade is now identical
to its marketplace grade by construction. The 7-key _load_policy_facts is
unchanged — it remains the correct input for the eligibility/dedup gate,
NOT for grading. Cached (curated layer + reviews) to stay off the hot path.

tests/test_scorecard_parity.py: NEW regression guard — rec-path grade letter
MUST equal marketplace grade letter for a representative policy spread
(incl. Optima Restore = strong A/B and rec-fit-floor-clearing). The two
paths can no longer silently diverge.

Verified: full pytest gate green (rc=0, 0 failures). Local E2E
(/tmp/local-verify.py) now PASSES first time — name captured turn 0 →
fact-find → retrieve_policies → grounded recommendation → structured
citation populated (CitedPolicyCards render). Not pushed
(deploy-altogether-later). KNOWN NEXT (#24): retrieval/ranking surfaces
C/D/F policies while A/87 maternity family floaters exist unretrieved —
separate retrieval-quality issue, not scorecard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

backend/brain_tools.py CHANGED
@@ -203,19 +203,75 @@ def _load_policy_facts(policy_id: str) -> dict:
203
  return out
204
 
205
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  def _scorecard_signal(policy_id: str, profile=None) -> dict:
207
- """Best-effort {_grade, _overall_score} from backend.scorecard. The
208
- scorecard module's scoring math is being repaired concurrently — we
209
- call it READ-ONLY and degrade silently (the ranking still works off
210
- co-pay + SI headroom + cosine without it)."""
 
 
211
  try:
212
- facts = _load_policy_facts(policy_id)
213
- if not facts:
214
- # Need the full curated dict for a real grade; fall back to a
215
- # minimal dict so build_scorecard at least sees co-pay/SI.
216
- facts = {}
 
 
 
 
 
217
  from backend.scorecard import build_scorecard
218
 
 
 
 
 
219
  prof = None
220
  if profile is not None:
221
  prof = {
@@ -225,7 +281,7 @@ def _scorecard_signal(policy_id: str, profile=None) -> dict:
225
  "existing_cover_inr", "copay_pct",
226
  )
227
  }
228
- sc = build_scorecard(facts or {"policy_id": policy_id}, profile=prof)
229
  # KI-280 (2026-05-16) — the Scorecard dataclass field is
230
  # `overall_score` (scorecard.py:95), NOT `overall`. The previous
231
  # getattr(sc, "overall", ...) ALWAYS returned None, so every
 
203
  return out
204
 
205
 
206
+ _curated_all_cache: dict = {} # single-slot cache for the full curated layer
207
+ _reviews_cache: dict = {}
208
+
209
+
210
+ def _curated_facts_all() -> dict:
211
+ """The FULL curated-facts layer, EXACTLY as the marketplace uses it.
212
+
213
+ KI-FULLFACTS (2026-05-17) — root cause of empty recommendation cards:
214
+ `_scorecard_signal` used to feed build_scorecard the 7-key eligibility
215
+ subset (`_load_policy_facts`), which is BELOW build_scorecard's
216
+ data-completeness floor → every recommended policy returned grade "—"
217
+ / overall 0 → the Bug #71 ≥70 gate dropped 100% of them → no cards.
218
+ Meanwhile /api/policies/all scored the SAME policies correctly
219
+ (HDFC Ergo Optima Restore = A/78) because it feeds the FULL curated
220
+ dict from main._load_curated_facts (KI-219/251 canonical precedence).
221
+ We now reuse that EXACT function (lazy import — main is loaded at
222
+ request time; dodges the import cycle) so a policy's recommendation
223
+ grade is identical to its marketplace grade by construction. The
224
+ 7-key `_load_policy_facts` stays as-is — it is the correct input for
225
+ the eligibility/dedup gate, NOT for grading."""
226
+ if "d" not in _curated_all_cache:
227
+ from backend.main import _load_curated_facts # lazy: avoids cycle
228
+ _curated_all_cache["d"] = _load_curated_facts()
229
+ return _curated_all_cache["d"]
230
+
231
+
232
+ def _insurer_reviews(slug: str) -> Optional[dict]:
233
+ """Same reviews source the marketplace passes to build_scorecard
234
+ (40-data/reviews/<slug>.json) — drives the claim-experience sub-score.
235
+ Without it the rec-path grade would drift below the marketplace's."""
236
+ if not slug:
237
+ return None
238
+ if slug in _reviews_cache:
239
+ return _reviews_cache[slug]
240
+ ir = None
241
+ try:
242
+ rp = settings.DATA_DIR / "reviews" / f"{slug}.json"
243
+ if rp.exists():
244
+ ir = _json.loads(rp.read_text())
245
+ except Exception: # noqa: BLE001 — reviews optional
246
+ ir = None
247
+ _reviews_cache[slug] = ir
248
+ return ir
249
+
250
+
251
  def _scorecard_signal(policy_id: str, profile=None) -> dict:
252
+ """Best-effort {_grade, _overall_score} from backend.scorecard, using
253
+ the IDENTICAL inputs as the marketplace (/api/policies/all) so a
254
+ policy's recommendation grade == its marketplace grade by construction
255
+ (parity is asserted by tests/test_scorecard_parity.py). READ-ONLY;
256
+ degrades silently to {} on any failure (ranking still works off
257
+ co-pay + SI headroom + cosine)."""
258
  try:
259
+ cur = _curated_facts_all()
260
+ data = None
261
+ for stem in _candidate_stems(policy_id):
262
+ if stem in cur:
263
+ data = cur[stem]
264
+ break
265
+ if not data:
266
+ # Genuinely unknown policy → fail OPEN (no false weak signal;
267
+ # _recommendation_fit keeps chunks with no grade evidence).
268
+ return {}
269
  from backend.scorecard import build_scorecard
270
 
271
+ slug = data.get("insurer_slug") or (
272
+ policy_id.split("__", 1)[0] if "__" in policy_id else ""
273
+ )
274
+ ir = _insurer_reviews(slug)
275
  prof = None
276
  if profile is not None:
277
  prof = {
 
281
  "existing_cover_inr", "copay_pct",
282
  )
283
  }
284
+ sc = build_scorecard(data, insurer_reviews=ir, profile=prof)
285
  # KI-280 (2026-05-16) — the Scorecard dataclass field is
286
  # `overall_score` (scorecard.py:95), NOT `overall`. The previous
287
  # getattr(sc, "overall", ...) ALWAYS returned None, so every
tests/test_scorecard_parity.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Regression guard — recommendation-path scorecard == marketplace scorecard.
2
+
3
+ ROOT CAUSE THIS LOCKS DOWN (KI-FULLFACTS, 2026-05-17)
4
+ -----------------------------------------------------
5
+ `brain_tools._scorecard_signal` used to feed `build_scorecard` the 7-key
6
+ eligibility subset (`_load_policy_facts`), which is below build_scorecard's
7
+ data-completeness floor → every recommended policy came back grade "—" /
8
+ overall 0 → the Bug #71 >=70 fitness gate dropped 100% of them → the
9
+ recommendation `citations` array was always empty → CitedPolicyCards never
10
+ rendered. Meanwhile /api/policies/all scored the SAME policies correctly
11
+ (HDFC ERGO Optima Restore = A) because it feeds the FULL curated layer
12
+ (main._load_curated_facts, KI-219/251 canonical precedence).
13
+
14
+ The fix makes `_scorecard_signal` reuse that EXACT function. This test makes
15
+ the two paths *provably* unable to diverge again: for a representative
16
+ spread of policies, the recommendation-path GRADE LETTER must equal the
17
+ marketplace GRADE LETTER. (Overall scores may differ by a few points
18
+ because the marketplace overlays EXTRACTED_DIR data on top; the LETTER —
19
+ what _recommendation_fit gates on — must match.)
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import sys
24
+ import unittest
25
+ from pathlib import Path
26
+
27
+ _REPO_ROOT = Path(__file__).resolve().parent.parent
28
+ if str(_REPO_ROOT) not in sys.path:
29
+ sys.path.insert(0, str(_REPO_ROOT))
30
+
31
+
32
+ class TestScorecardParity(unittest.TestCase):
33
+ """Rec-path _scorecard_signal grade letter == marketplace grade letter."""
34
+
35
+ @classmethod
36
+ def setUpClass(cls):
37
+ from backend.main import _load_curated_facts
38
+ from backend.scorecard import build_scorecard
39
+ from backend.brain_tools import _scorecard_signal, _insurer_reviews
40
+
41
+ cls._scorecard_signal = staticmethod(_scorecard_signal)
42
+ cur = _load_curated_facts()
43
+
44
+ # Marketplace grade for a policy_id = build_scorecard on the SAME
45
+ # full curated dict + reviews the /api/policies/all path uses.
46
+ def _mkt_grade(pid: str):
47
+ data = cur.get(pid)
48
+ if not data:
49
+ return None
50
+ slug = data.get("insurer_slug") or (
51
+ pid.split("__", 1)[0] if "__" in pid else ""
52
+ )
53
+ sc = build_scorecard(
54
+ data, insurer_reviews=_insurer_reviews(slug), profile=None
55
+ )
56
+ return sc.grade
57
+
58
+ cls._mkt_grade = staticmethod(_mkt_grade)
59
+ # A representative spread across grade bands + insurers, incl. the
60
+ # user's own policy (HDFC ERGO Optima Restore — must be a strong A).
61
+ cls._sample = [
62
+ "hdfc-ergo__optima-restore",
63
+ "hdfc-ergo__my-health-sampoorna-suraksha",
64
+ "hdfc-ergo__my-optima-secure",
65
+ "oriental-insurance__happy-family-floater",
66
+ "star-health__family-health-optima",
67
+ ]
68
+
69
+ def test_rec_path_grade_matches_marketplace_grade(self):
70
+ mismatches = []
71
+ for pid in self._sample:
72
+ mkt = self._mkt_grade(pid)
73
+ if mkt is None:
74
+ continue # policy not in curated layer in this env — skip
75
+ rec = (self._scorecard_signal(pid, profile=None) or {}).get("_grade")
76
+ if rec != mkt:
77
+ mismatches.append(f"{pid}: rec={rec!r} marketplace={mkt!r}")
78
+ self.assertEqual(
79
+ mismatches, [],
80
+ "recommendation-path scorecard grade diverged from the "
81
+ "marketplace grade — the two paths must feed build_scorecard "
82
+ "the SAME curated facts (see KI-FULLFACTS):\n "
83
+ + "\n ".join(mismatches),
84
+ )
85
+
86
+ def test_optima_restore_is_a_strong_recommendable_grade(self):
87
+ # The user's own policy — a genuinely strong plan. It must NOT come
88
+ # back as the data-starved "—"/0 sentinel, and must clear the Bug
89
+ # #71 recommendation fitness floor so its card can render.
90
+ from backend.single_brain import _recommendation_fit
91
+
92
+ sig = self._scorecard_signal("hdfc-ergo__optima-restore", profile=None)
93
+ self.assertIn(sig.get("_grade"), ("A", "B"),
94
+ f"Optima Restore must be A/B, got {sig!r}")
95
+ strong, overall, grade = _recommendation_fit({
96
+ "_overall_score": sig.get("_overall_score"),
97
+ "_grade": sig.get("_grade"),
98
+ })
99
+ self.assertTrue(
100
+ strong,
101
+ f"Optima Restore must clear the rec-fit floor "
102
+ f"(grade={grade} overall={overall})",
103
+ )
104
+
105
+
106
+ if __name__ == "__main__":
107
+ unittest.main()