InsuranceBot / 70-docs /80-audit /premium-dependency-map.md
Claude Code
fix(pricing+scoring): rebuild premium engine to 0 logical violations + classification, faithfulness, links, writing
f266901
|
Raw
History Blame Contribute Delete
17.3 kB

Premium Dependency Map

Field Value
Document type Dependency / change-cascade map (data + code)
Subject data file 40-data/premiums/illustrative_premiums.json
Generated (this doc) 2026-05-18
Companion premium-source-map.md (per-sample provenance)
Source of truth for chain backend/premium_calculator.py, backend/brain_tools.py, backend/main.py, backend/scorecard.py

0. Purpose

This document is the change-cascade contract for the premium pipeline. It exists so a future edit to the curated JSON, a calculator helper, an API contract, or the reviews/scorecard parity surface does not silently break a downstream consumer.

Every node below names: what it consumes, what depends on it, and an explicit "if you change X you must re-verify Y" rule. Function and constant names are taken verbatim from the source files (read 2026-05-18); they are not paraphrased.

1. The pricing chain (end to end)

40-data/premiums/illustrative_premiums.json
   β”‚  (base_premiums{}, scaling_factors{})
   β–Ό
premium_calculator._load_data()                     [reads + json.loads the file; {} on any error]
   β–Ό
premium_calculator._canonical_sample_key()          [resolve recommended/marketplace id β†’ base_premiums key]
   β”‚   β”œβ”€ _SAMPLE_DOCTYPE_SUFFIXES   (__brochure/__cis/__wordings/__prospectus/__policy)
   β”‚   └─ _KNOWN_BAD_SAMPLE_KEYS     (currently EMPTY frozenset β€” quarantine mechanism, retained)
   β–Ό
premium_calculator._plausible_samples()             [type-aware β‚Ή/lakh sanity guard, via _per_lakh_band()]
   β–Ό
premium_calculator._interpolate_from_samples()      [nearest sample in (age, log SI) space]
   β–Ό
premium_calculator.estimate()                       [#38 FULL ratio-normalization + OUTPUT plausibility ceiling]
   β”‚      └─ NO-sample path β†’ _attribute_base_factor()  (product-TYPE model; no JSON I/O)
   β–Ό
premium_calculator.bulk_estimate()                  [calls estimate() per policy on the curated path;
   β”‚                                                  flat β‚Ή500/lakh Γ— type-factor on the no-sample path]
   β–Ό
premium_calculator.estimate_premium_band()          [prices the 26-policy _DEFAULT_BAND_POLICY_IDS basket;
   β”‚                                                  p25–p75 interquartile via resolve_profile_sum_insured()]
   β–Ό
backend/main.py  POST /api/premium/estimate         (PremiumEstimateRequest β†’ PremiumEstimateResponse)
                 POST /api/premium/bulk             (PremiumBulkRequest β†’ PremiumBulkResponse)
                 GET  /api/profile/predicted-premium-band  (β†’ PredictedPremiumBandResponse)
   β–Ό
frontend/src/lib/api.ts  postPremiumEstimate() / PremiumEstimateResponse
   β–Ό
frontend/src/components/PolicyPremiumWidget.tsx     (point + Β±15% band + methodology line + SI disclosure)
        └─ embedded in PolicyCompareModal.tsx; header chip in app/page.tsx (premiumBand state)

Parallel, independent chain (claim-experience parity, not premium-priced):

40-data/reviews/<slug>.json
   β–Ό
brain_tools._insurer_reviews(slug)        [cached read; None if missing]
   β–Ό
scorecard.score_claim_experience(p, insurer_reviews=…)   [IRDAI CSR + complaints/10k β†’ sub-score]
   β–Ό
scorecard.build_scorecard(data, insurer_reviews=…, profile=…)
   β”œβ”€ recommendation path:  brain_tools._scorecard_signal()  β†’ cited-card grade
   └─ marketplace path:     main.py /api/policies/all         β†’ marketplace grade
   β–Ό  PARITY INVARIANT (tests/test_scorecard_parity.py): cited-card GRADE LETTER == marketplace GRADE LETTER

2. Node-by-node dependency contract

2.1 40-data/premiums/illustrative_premiums.json

  • Consumes: nothing β€” it is the single source of truth. Top-level keys: last_updated, methodology, sources_consulted, notes, base_premiums (100 entries), scaling_factors, link_rot_repairs.
  • Depended on by: _load_data() (the ONLY reader), and transitively everything below it. Also documented by premium-source-map.md.
  • Sample shape (post-harvest): evidenced samples carry age, sum_insured_inr, city_tier, smoker, family_size, annual_premium_inr, source_url, source_quote, source_quality, fetched_on (+ optional source_note/derivation_note/variant). 194 evidenced samples across 73 entries; 27 entries are model-only.
  • If you change X you must re-verify Y:
    • Change/add a samples[] entry β†’ re-run _plausible_samples() mentally against _per_lakh_band() for that policy type (comprehensive 250–6500/L; top-up 10–800/L; benefit plans unbounded). A sample outside the band is silently dropped, so the policy may regress to the model path.
    • Change a base_premiums key β†’ re-verify _canonical_sample_key() still resolves the marketplace/recommended id (suffix + single-hyphen forms) AND that the key appears in _DEFAULT_BAND_POLICY_IDS if it should be in the header basket.
    • Change scaling_factors (age/SI/city/floater/smoker/PED multipliers) β†’ re-verify the #38 ratio-normalization in estimate(): it divides the sample's own multipliers out and re-applies the user's, so a changed factor moves every curated-path estimate. Re-run tests/test_premium_attribute_and_normalization.py + tests/test_premium_reconciliation.py.
    • Edit/add/remove any evidenced sample β†’ regenerate premium-source-map.md Β§1 counts and Β§2/Β§3 tables in the same commit (the JSON is the single source of truth for both docs).

2.2 premium_calculator._load_data()

  • Consumes: PREMIUM_DATA path (settings.DATA_DIR / "premiums" / "illustrative_premiums.json").
  • Depended on by: estimate(), bulk_estimate() (each call re-reads β€” no module cache).
  • If you change X: moving/renaming the JSON, or breaking its JSON validity, makes _load_data() return {} silently β†’ every policy falls to FALLBACK_* constants (no exception, no log). Re-verify by calling estimate(policy_id=...) for a known-curated policy and asserting base_sample_used is not None.

2.3 premium_calculator._canonical_sample_key()

  • Consumes: the incoming policy_id, base_premiums keys, _SAMPLE_DOCTYPE_SUFFIXES, _KNOWN_BAD_SAMPLE_KEYS.
  • Depended on by: estimate() (sample lookup) and bulk_estimate() (the if _canonical_sample_key(pid, …) is not None branch decides curated-anchor vs flat-base). Both must agree or the widget and the per-policy panel diverge.
  • If you change X: add a doctype suffix or change the hyphen-normalization β†’ re-verify BOTH call sites resolve the same key (a mismatch reintroduces the β‚Ή33,700 collision / the SBI Arogya Supreme double-floater bug). _KNOWN_BAD_SAMPLE_KEYS is currently the empty frozenset (SBI Arogya Supreme was unquarantined 2026-05-18 after its bad brochure-extract was physically replaced with real SBI rate-chart figures); to re-quarantine a proven-bad entry, add its key here AND note it in the source map.

2.4 premium_calculator._per_lakh_band() / _plausible_samples()

  • Consumes: policy_id substring (type detection), each sample's sum_insured_inr + annual_premium_inr.
  • Depended on by: estimate() (input guard before interpolation) and the OUTPUT plausibility ceiling at the end of estimate() (ceiling = _hi_b * 1.5).
  • If you change X: widening/narrowing a band β†’ re-verify (a) no real evidenced sample is now dropped (would silently demote a policy to the model path) and (b) the output ceiling still trips only on genuinely broken data. The product-type substrings (top-up, hospital-cash, cancer, critical-illness, …) are matched against the lowercased policy_id; renaming a key can flip a plan between bands.

2.5 premium_calculator._attribute_base_factor() β€” the no-sample path

  • Consumes: policy_id substring only. No JSON I/O β€” deterministic on the id.
  • Depended on by: estimate() (no-sample branch), bulk_estimate() (flat-base flat_base = BULK_BASE_INR_PER_LAKH * si_lakhs * _attribute_base_factor(pid)), and therefore every one of the 27 model-only entries in the source map Β§3.
  • Returns: super-top-up/top-up 0.35Γ—, hospital-cash/fixed-benefit 0.30Γ—, cancer/critical-illness 0.55Γ—, sanjeevani 0.70Γ—, comprehensive 1.0Γ— (no regression for the dominant type).
  • If you change X: changing a factor moves every model-only policy of that type AND the flat-base widget number. Re-verify against the type-band so a model-only estimate stays inside _per_lakh_band(); re-run tests/test_premium_attribute_and_normalization.py. Provenance label: model-path estimates carry the methodology string "Indicative estimate modelled from this plan's product type … NOT a quote" β€” if you ever anchor a model-only policy to a real sample, move it out of source-map Β§3 into Β§2 and the label flips to the "Anchored to a verified public-quote sample" variant automatically (driven by sample_used is not None).

2.6 premium_calculator.estimate()

  • Consumes: _load_data(), _canonical_sample_key(), _plausible_samples(), _interpolate_from_samples(), scaling_factors, the B6/D2/KI-275 loadings (_health_loading, _existing_cover_loading, _parents_loading, _copay_discount, _family_history_loading, _copay_multiplier).
  • Depended on by: bulk_estimate() (curated path calls estimate() directly), POST /api/premium/estimate, and indirectly the header band (estimate_premium_band β†’ bulk_estimate β†’ estimate).
  • Two critical internal invariants:
    1. #38 full ratio-normalization β€” the sample's own age/SI/city/family multipliers are divided out, then the user's profile is applied exactly once by the unconditional city/floater/smoker/PED block. Sample family_size is HEADCOUNT (1=individual); estimate()'s floater key is dependents-beyond-self (max(0, headcount-1)). Breaking this re-introduces double-counted floater (the SBI β‚Ή149,800 bug).
    2. OUTPUT plausibility ceiling β€” if a sample-anchored point still exceeds _per_lakh_band(...)[1] * 1.5 per lakh after normalization+loadings, it drops sample_used/sources and falls back to the policy-blind model base.
  • If you change X: changing the loading order, the normalization, or the ceiling β†’ re-run BOTH tests/test_premium_reconciliation.py and tests/test_premium_attribute_and_normalization.py; the header-band p25–p75 contract depends on estimate() being stable.

2.7 premium_calculator.bulk_estimate() / estimate_premium_band()

  • Consumes: bulk_estimate() calls estimate() on the curated path; estimate_premium_band() calls bulk_estimate() over _DEFAULT_BAND_POLICY_IDS (26 policies) and resolve_profile_sum_insured() for the shared SI.
  • Depended on by: POST /api/premium/bulk, GET /api/profile/predicted-premium-band, the PolicyCompareModal widget, and the header "Premium range" chip.
  • SI contract (KI-278): resolve_profile_sum_insured() precedence (desired_sum_insured_inr ?? existing_cover_inr ?? β‚Ή10L, snapped to β‚Ή50k) MUST stay byte-identical to PremiumCalculatorPanel's slider seed (frontend/src/app/page.tsx ~L2417) and PolicyPremiumWidget's initialSumInsured. The chip band is the p25–p75 interquartile of the basket, NOT raw min–max.
  • If you change X:
    • Add/remove a policy in _DEFAULT_BAND_POLICY_IDS β†’ re-verify each id resolves via _canonical_sample_key() (else it silently uses the flat path) and re-check the chip band is still a sane range.
    • Change the SI precedence on either side β†’ change BOTH resolve_profile_sum_insured() and the page.tsx slider seed in the same commit, else header β‰  panel returns. Re-run tests/test_premium_reconciliation.py.

2.8 API layer (backend/main.py)

  • Consumes: estimate, bulk_estimate, estimate_premium_band, unpublished_si_disclosure, _policy_corroborated_si.
  • Depended on by: frontend/src/lib/api.ts (postPremiumEstimate, PremiumEstimateResponse type), PolicyPremiumWidget.tsx, PolicyCompareModal.tsx, app/page.tsx (premiumBand).
  • Contract surfaces that must not drift: PremiumEstimateResponse.base_sample_used (widget shows/hides its "Estimate" badge off this β€” it is e.base_sample_used is not None), methodology (rendered verbatim under the estimate), sources (the source URLs), sum_insured_disclosure (rendered verbatim only when _policy_corroborated_si(...).kind == "none"). predicted-premium-band feeds the profile dict via brain_tools.SLOT_UNION with the answered-only gate (profile.asked).
  • If you change X: renaming/removing a response field β†’ update frontend/src/lib/api.ts types + every .tsx consumer in the same commit. Changing tenure_years/deductible_inr snapping uses BULK_TENURE_MULT/BULK_DEDUCTIBLE_DISCOUNT from the calculator β€” keep them in sync.

2.9 Reviews β†’ scorecard claim-experience PARITY chain

  • Consumes: 40-data/reviews/<slug>.json β†’ brain_tools._insurer_reviews(slug) (cached; None if missing) β†’ scorecard.score_claim_experience(p, insurer_reviews=…) β†’ scorecard.build_scorecard(data, insurer_reviews=…, profile=…).
  • Two paths that MUST stay in parity:
    • Recommendation / cited-card grade: brain_tools._scorecard_signal() builds data via _merge_curated(extracted, curated) (KI-PARITY 2026-05-18 β€” curated-only made the cited grade systematically lower), resolves slug, passes _insurer_reviews(slug) into build_scorecard.
    • Marketplace grade: /api/policies/all builds build_scorecard on the full curated+reviews layer.
  • PARITY INVARIANT: the cited-card grade letter must equal the marketplace grade letter for the same policy. Locked by tests/test_scorecard_parity.py (overall scores may differ by a few points because the marketplace overlays EXTRACTED_DIR; the LETTER, which _recommendation_fit gates on, must match).
  • If you change X:
    • Edit a 40-data/reviews/<slug>.json claim_metrics value β†’ re-verify score_claim_experience() band thresholds (CSR β‰₯95 +20, β‰₯90 +12, β‰₯85 +5, β‰₯75 βˆ’6, else βˆ’20; complaints/10k ≀10 +8 … >45 βˆ’16) and re-run tests/test_scorecard_parity.py β€” a CSR/complaints change can flip the sub-score enough to move the grade letter, which must move identically on BOTH paths.
    • Change _scorecard_signal()'s data assembly (_merge_curated, _candidate_stems, slug derivation) β†’ re-run tests/test_scorecard_parity.py; divergence makes the recommendation path drop policies (empty citations β†’ CitedPolicyCards never render).
    • This chain is independent of premium pricing β€” it does NOT read illustrative_premiums.json. Document it here only because it shares the source-methodology + dependency-map treatment and the same _insurer_reviews/scorecard machinery.

3. Quick "change X β†’ re-verify Y" lookup

If you change… You must re-verify… Tests / docs to re-run
A samples[] entry / a premium figure _plausible_samples band, _interpolate_from_samples nearest pick, estimate() #38 normalization test_premium_reconciliation.py, test_premium_attribute_and_normalization.py, regenerate premium-source-map.md
A base_premiums key name _canonical_sample_key both call sites, _DEFAULT_BAND_POLICY_IDS membership, _per_lakh_band/_attribute_base_factor substring match test_premium_reconciliation.py, source map Β§2/Β§3
scaling_factors multipliers estimate() ratio-normalization output, header band both premium tests
_attribute_base_factor every model-only entry + flat-base widget, type-band sanity test_premium_attribute_and_normalization.py, source map Β§3
resolve_profile_sum_insured precedence page.tsx slider seed byte-identity, header == panel test_premium_reconciliation.py
A PremiumEstimateResponse field frontend/src/lib/api.ts types, PolicyPremiumWidget.tsx, PolicyCompareModal.tsx, page.tsx frontend typecheck/build
_DEFAULT_BAND_POLICY_IDS each id resolves via _canonical_sample_key, chip band sanity test_premium_reconciliation.py
40-data/reviews/<slug>.json claim_metrics score_claim_experience thresholds, cited-grade == marketplace-grade test_scorecard_parity.py
_scorecard_signal data assembly parity invariant (grade letter both paths) test_scorecard_parity.py
Move/rename/corrupt the JSON _load_data() returns {} SILENTLY β†’ all-fallback manual estimate() smoke on a known-curated id

4. Regeneration

Both this map and premium-source-map.md are derived from the same single source of truth (40-data/premiums/illustrative_premiums.json) plus the code chain above. After any premium-harvest or calculator-contract change, update the affected Β§2 node contract and the Β§3 lookup row in the same commit as the code/data change β€” the "if you change X you must re-verify Y" rules are only useful if they stay in lockstep with the chain.