Spaces:
Sleeping
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 bypremium-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(+ optionalsource_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_premiumskey β re-verify_canonical_sample_key()still resolves the marketplace/recommended id (suffix + single-hyphen forms) AND that the key appears in_DEFAULT_BAND_POLICY_IDSif it should be in the header basket. - Change
scaling_factors(age/SI/city/floater/smoker/PED multipliers) β re-verify the #38 ratio-normalization inestimate(): it divides the sample's own multipliers out and re-applies the user's, so a changed factor moves every curated-path estimate. Re-runtests/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).
- Change/add a
2.2 premium_calculator._load_data()
- Consumes:
PREMIUM_DATApath (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 toFALLBACK_*constants (no exception, no log). Re-verify by callingestimate(policy_id=...)for a known-curated policy and assertingbase_sample_used is not None.
2.3 premium_calculator._canonical_sample_key()
- Consumes: the incoming
policy_id,base_premiumskeys,_SAMPLE_DOCTYPE_SUFFIXES,_KNOWN_BAD_SAMPLE_KEYS. - Depended on by:
estimate()(sample lookup) andbulk_estimate()(theif _canonical_sample_key(pid, β¦) is not Nonebranch 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_KEYSis 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_idsubstring (type detection), each sample'ssum_insured_inr+annual_premium_inr. - Depended on by:
estimate()(input guard before interpolation) and the OUTPUT plausibility ceiling at the end ofestimate()(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 lowercasedpolicy_id; renaming a key can flip a plan between bands.
2.5 premium_calculator._attribute_base_factor() β the no-sample path
- Consumes:
policy_idsubstring only. No JSON I/O β deterministic on the id. - Depended on by:
estimate()(no-sample branch),bulk_estimate()(flat-baseflat_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Γ,
sanjeevani0.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-runtests/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 bysample_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 callsestimate()directly),POST /api/premium/estimate, and indirectly the header band (estimate_premium_band β bulk_estimate β estimate). - Two critical internal invariants:
- #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_sizeis 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). - OUTPUT plausibility ceiling β if a sample-anchored point still exceeds
_per_lakh_band(...)[1] * 1.5per lakh after normalization+loadings, it dropssample_used/sourcesand falls back to the policy-blind model base.
- #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
- If you change X: changing the loading order, the normalization, or the ceiling β re-run BOTH
tests/test_premium_reconciliation.pyandtests/test_premium_attribute_and_normalization.py; the header-band p25βp75 contract depends onestimate()being stable.
2.7 premium_calculator.bulk_estimate() / estimate_premium_band()
- Consumes:
bulk_estimate()callsestimate()on the curated path;estimate_premium_band()callsbulk_estimate()over_DEFAULT_BAND_POLICY_IDS(26 policies) andresolve_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 toPremiumCalculatorPanel's slider seed (frontend/src/app/page.tsx~L2417) andPolicyPremiumWidget'sinitialSumInsured. 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-runtests/test_premium_reconciliation.py.
- Add/remove a policy in
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,PremiumEstimateResponsetype),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 ise.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-bandfeeds the profile dict viabrain_tools.SLOT_UNIONwith the answered-only gate (profile.asked). - If you change X: renaming/removing a response field β update
frontend/src/lib/api.tstypes + every.tsxconsumer in the same commit. Changingtenure_years/deductible_inrsnapping usesBULK_TENURE_MULT/BULK_DEDUCTIBLE_DISCOUNTfrom 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;Noneif 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()buildsdatavia_merge_curated(extracted, curated)(KI-PARITY 2026-05-18 β curated-only made the cited grade systematically lower), resolvesslug, passes_insurer_reviews(slug)intobuild_scorecard. - Marketplace grade:
/api/policies/allbuildsbuild_scorecardon the full curated+reviews layer.
- Recommendation / cited-card grade:
- 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 overlaysEXTRACTED_DIR; the LETTER, which_recommendation_fitgates on, must match). - If you change X:
- Edit a
40-data/reviews/<slug>.jsonclaim_metricsvalue β re-verifyscore_claim_experience()band thresholds (CSR β₯95 +20, β₯90 +12, β₯85 +5, β₯75 β6, else β20; complaints/10k β€10 +8 β¦ >45 β16) and re-runtests/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-runtests/test_scorecard_parity.py; divergence makes the recommendation path drop policies (emptycitationsβ 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.
- Edit a
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.