rohitsar567 Claude Opus 4.7 (1M context) commited on
Commit
208c839
Β·
1 Parent(s): a5d9bfd

fix(#60+#62): bulk-scorecard curated fallback + stop internal scoring jargon leaking

Browse files

#60 REGRESSION: /api/scorecard/bulk hard-required rag/extracted/<pid>.json
and emitted the N/A 'No extraction available' / 'Data not indexed'
sentinel for curated-only catalogued products (star-health__star-
comprehensive, UIN SHAHLIP26044V092526) β€” the broken-card defect the
user re-reported. Mirrored the curated fallback the marketplace +
single /api/scorecard endpoint already use (policies_all Pass-2). Star
Comprehensive now grades A 76 instead of N/A.

#62: scorecard.py hardcoded the user-facing signal as
'lifelong renewable (IRDAI norm β€” not scored)' β€” internal scoring
scaffolding leaking into the card UI. Now 'Lifelong renewability
guaranteed'. Test updated to assert clean copy + no jargon leak.

Full pytest gate green (rc=0).

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

backend/main.py CHANGED
@@ -3850,7 +3850,43 @@ async def scorecard_bulk(req: BulkScorecardRequest):
3850
 
3851
  for pid in req.policy_ids:
3852
  extracted_path = settings.EXTRACTED_DIR / f"{pid}.json"
3853
- if not extracted_path.exists():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3854
  out[pid] = BulkScorecardEntry(
3855
  policy_id=pid,
3856
  policy_name=pid,
@@ -3864,19 +3900,6 @@ async def scorecard_bulk(req: BulkScorecardRequest):
3864
  signals={},
3865
  )
3866
  continue
3867
- try:
3868
- policy = _json.loads(extracted_path.read_text())
3869
- except Exception as e:
3870
- out[pid] = BulkScorecardEntry(
3871
- policy_id=pid, policy_name=pid, insurer_slug="?",
3872
- overall_grade="N/A", overall_score=0, sub_scores={},
3873
- profile_rationale=[f"Data unreadable: {e}"],
3874
- data_completeness_pct=0.0,
3875
- one_liner="Extraction file is corrupted.",
3876
- signals={},
3877
- )
3878
- continue
3879
- policy = _merge_curated(policy, _curated.get(policy.get("policy_id", pid)) or _curated.get(pid))
3880
 
3881
  slug = policy.get("insurer_slug") or "?"
3882
  if slug not in insurer_cache:
 
3850
 
3851
  for pid in req.policy_ids:
3852
  extracted_path = settings.EXTRACTED_DIR / f"{pid}.json"
3853
+ policy = None
3854
+ if extracted_path.exists():
3855
+ try:
3856
+ policy = _json.loads(extracted_path.read_text())
3857
+ except Exception as e:
3858
+ out[pid] = BulkScorecardEntry(
3859
+ policy_id=pid, policy_name=pid, insurer_slug="?",
3860
+ overall_grade="N/A", overall_score=0, sub_scores={},
3861
+ profile_rationale=[f"Data unreadable: {e}"],
3862
+ data_completeness_pct=0.0,
3863
+ one_liner="Extraction file is corrupted.",
3864
+ signals={},
3865
+ )
3866
+ continue
3867
+ policy = _merge_curated(
3868
+ policy, _curated.get(policy.get("policy_id", pid)) or _curated.get(pid)
3869
+ )
3870
+ else:
3871
+ # ROOT-CAUSE FIX #60 (2026-05-18): curated-only catalogued
3872
+ # products (e.g. star-health__star-comprehensive, UIN
3873
+ # SHAHLIP26044V092526) have NO rag/extracted/<pid>.json β€” only
3874
+ # doctype-suffixed extractions. The marketplace + the single
3875
+ # /api/scorecard endpoint already resolve these from the curated
3876
+ # layer (policies_all Pass-2 / lines ~3617). The BULK endpoint
3877
+ # did not, so it emitted the N/A "No extraction available" /
3878
+ # "Data not indexed" sentinel β€” the broken-card defect the user
3879
+ # saw for Star Comprehensive. Mirror that curated fallback here;
3880
+ # the curated dict also carries doctype-suffixed alias keys.
3881
+ policy = (
3882
+ _curated.get(pid)
3883
+ or _curated.get(f"{pid}__wordings")
3884
+ or _curated.get(f"{pid}__cis")
3885
+ or _curated.get(f"{pid}__brochure")
3886
+ or _curated.get(f"{pid}__prospectus")
3887
+ )
3888
+ if not policy:
3889
+ # Genuinely absent from BOTH layers β‡’ not a catalogued product.
3890
  out[pid] = BulkScorecardEntry(
3891
  policy_id=pid,
3892
  policy_name=pid,
 
3900
  signals={},
3901
  )
3902
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
3903
 
3904
  slug = policy.get("insurer_slug") or "?"
3905
  if slug not in insurer_cache:
backend/scorecard.py CHANGED
@@ -378,7 +378,7 @@ def score_renewal_protection(p: dict) -> SubScore:
378
  maximum *entry* age β€” how late a first-time buyer can take the policy β€”
379
  so that is the sole driver of this sub-score.
380
  """
381
- signals: list[str] = ["lifelong renewable (IRDAI norm β€” not scored)"]
382
  s = 50 # true-neutral base (was 60 β€” recalibrated for real spread)
383
 
384
  maxe = _int(p, "max_entry_age")
 
378
  maximum *entry* age β€” how late a first-time buyer can take the policy β€”
379
  so that is the sole driver of this sub-score.
380
  """
381
+ signals: list[str] = ["Lifelong renewability guaranteed"]
382
  s = 50 # true-neutral base (was 60 β€” recalibrated for real spread)
383
 
384
  maxe = _int(p, "max_entry_age")
tests/test_scorecard_lifelong.py CHANGED
@@ -50,7 +50,13 @@ def test_entry_age_is_the_sole_driver():
50
  def test_lifelong_shown_as_informational_signal_not_scored():
51
  sc = score_renewal_protection({})
52
  assert sc.name == "Renewal Protection"
53
- assert any("lifelong" in s.lower() and "not scored" in s.lower() for s in sc.signals)
 
 
 
 
 
 
54
 
55
 
56
  def test_completeness_ignores_max_renewal_age():
 
50
  def test_lifelong_shown_as_informational_signal_not_scored():
51
  sc = score_renewal_protection({})
52
  assert sc.name == "Renewal Protection"
53
+ # The signal must surface lifelong renewability as an informational
54
+ # bullet β€” but in CLEAN user-facing copy. The internal "(IRDAI norm β€”
55
+ # not scored)" scaffolding must NOT leak to the UI (#62). That it is
56
+ # genuinely not scored is enforced by the score-invariance tests above.
57
+ assert any("lifelong renewab" in s.lower() for s in sc.signals)
58
+ assert not any("not scored" in s.lower() or "irdai norm" in s.lower()
59
+ for s in sc.signals), "internal scoring jargon leaked into UI signal"
60
 
61
 
62
  def test_completeness_ignores_max_renewal_age():