#!/usr/bin/env python3 """Mirror today's data + design work into kb/. Reads: - 40-data/policy_facts/*.json -> kb/policies/.md (yaml frontmatter + per-field MD) - backend.scorecard METHODOLOGY_BLUEPRINT / WEIGHTS / SCORED_FIELDS -> kb/methodology/scorecard.json - frontend/src/lib/i18n.ts GLOSSARY (hand-mirrored) -> kb/methodology/glossary.json - 70-docs/discovery-script.md -> kb/methodology/discovery-script.md - 70-docs/scorecard-knowledge-graph.md -> kb/methodology/knowledge-graph.md - 70-docs/tie-breaker-rubric.md -> kb/methodology/tie-breakers.md Rewrites: - kb/INDEX.md (top-level index with policies table + methodology links) Appends to: - kb/AUDIT_TRAIL.md ("Batch — 2026-05-14" section) Run from project root: .venv/bin/python3 tools/build_kb_mirror.py """ from __future__ import annotations import json import sys from pathlib import Path from datetime import date ROOT = Path(__file__).resolve().parent.parent KB = ROOT / "kb" POLICIES_OUT = KB / "policies" METHOD_OUT = KB / "methodology" DATA_IN = ROOT / "40-data" / "policy_facts" DOCS = ROOT / "docs" POLICIES_OUT.mkdir(parents=True, exist_ok=True) METHOD_OUT.mkdir(parents=True, exist_ok=True) # --------------------------------------------------------------------------- # 1. Insurer slug -> human-readable name # --------------------------------------------------------------------------- INSURER_NAMES = { "aditya-birla": "Aditya Birla Health Insurance", "bajaj-allianz": "Bajaj Allianz General Insurance", "care-health": "Care Health Insurance", "hdfc-ergo": "HDFC ERGO General Insurance", "icici-lombard": "ICICI Lombard General Insurance", "manipalcigna": "ManipalCigna Health Insurance", "new-india": "The New India Assurance Co.", "niva-bupa": "Niva Bupa Health Insurance", "star-health": "Star Health and Allied Insurance", "tata-aig": "Tata AIG General Insurance", } # --------------------------------------------------------------------------- # 2. Field ordering: identity -> eligibility -> waiting -> coverage -> # cost-share -> claims -> bonuses. Any unclassified fields fall through # into "Other fields" (alphabetical). # --------------------------------------------------------------------------- FIELD_GROUPS = [ ("Identity", [ "uin_code", "policy_type", ]), ("Eligibility", [ "min_entry_age", "max_entry_age", "max_renewal_age", "sum_insured_options", ]), ("Waiting periods", [ "initial_waiting_period_days", "pre_existing_disease_waiting_months", "specific_disease_waiting_months", "maternity_waiting_months", ]), ("Coverage scope", [ "pre_hospitalization_days", "post_hospitalization_days", "day_care_treatments_count", "ayush_coverage", "maternity_coverage", "newborn_coverage", "organ_donor_expenses", "ambulance_cover", "domiciliary_treatment", "preventive_health_checkup", "critical_illness_cover", "worldwide_emergency_cover", "restoration_benefit", "room_rent_capping", ]), ("Cost-share", [ "copayment_pct", "deductible_amount", "sub_limits", ]), ("Claims & service", [ "network_hospital_count", "cashless_treatment_supported", "claim_settlement_ratio", "tat_cashless_authorization_hours", "claim_process_summary", ]), ("Bonuses & loyalty", [ "no_claim_bonus_pct", "wellness_program", ]), ] FIELD_TITLES = { "uin_code": "UIN code", "policy_type": "Policy type", "min_entry_age": "Minimum entry age", "max_entry_age": "Maximum entry age", "max_renewal_age": "Maximum renewal age", "sum_insured_options": "Sum insured options", "initial_waiting_period_days": "Initial waiting period (days)", "pre_existing_disease_waiting_months": "Pre-existing disease waiting (months)", "specific_disease_waiting_months": "Specific disease waiting (months)", "maternity_waiting_months": "Maternity waiting (months)", "pre_hospitalization_days": "Pre-hospitalization (days)", "post_hospitalization_days": "Post-hospitalization (days)", "day_care_treatments_count": "Day-care treatments covered", "ayush_coverage": "AYUSH coverage", "maternity_coverage": "Maternity coverage", "newborn_coverage": "Newborn coverage", "organ_donor_expenses": "Organ donor expenses", "ambulance_cover": "Ambulance cover", "domiciliary_treatment": "Domiciliary treatment", "preventive_health_checkup": "Preventive health checkup", "critical_illness_cover": "Critical illness cover", "worldwide_emergency_cover": "Worldwide emergency cover", "restoration_benefit": "Restoration benefit", "room_rent_capping": "Room rent capping", "copayment_pct": "Co-payment (%)", "deductible_amount": "Deductible", "sub_limits": "Sub-limits", "network_hospital_count": "Network hospital count", "cashless_treatment_supported": "Cashless treatment supported", "claim_settlement_ratio": "Claim settlement ratio", "tat_cashless_authorization_hours": "Cashless TAT (hours)", "claim_process_summary": "Claim process summary", "no_claim_bonus_pct": "No-claim bonus (%)", "wellness_program": "Wellness program", } def fmt_value(field: dict) -> str: """Render a {value, unit?, ...} triple into a human-readable scalar.""" if not isinstance(field, dict): return "_n/a_" v = field.get("value") unit = field.get("unit") if v is None or v == "": return "_not specified_" if isinstance(v, bool): return "Yes" if v else "No" if isinstance(v, list): return ", ".join(str(x) for x in v) if unit: return f"{v} {unit}" return str(v) def render_field(field_key: str, field: dict) -> str: title = FIELD_TITLES.get(field_key, field_key.replace("_", " ").title()) if not isinstance(field, dict): return f"### {title}\n\n_no data_\n" value_md = fmt_value(field) quote = (field.get("source_quote") or "").strip() pdf = field.get("source_pdf_path") or field.get("source_url") or "" quote_block = f"> {quote}" if quote else "> _(no verbatim quote on record)_" src = f"`{pdf}`" if pdf else "_(no source path on record)_" return ( f"### {title}\n\n" f"**Value:** {value_md}\n\n" f"**Source quote:**\n\n{quote_block}\n\n" f"**Source:** {src}\n" ) def render_policy_md(p: dict, source_json_path: Path) -> str: pid = p.get("policy_id") or source_json_path.stem insurer_slug = p.get("insurer_slug") or pid.split("__", 1)[0] insurer_name = INSURER_NAMES.get(insurer_slug, insurer_slug) policy_name = p.get("policy_name") or pid uin = "" if isinstance(p.get("uin_code"), dict): uin = p["uin_code"].get("value") or "" meta = p.get("_meta", {}) if isinstance(p.get("_meta"), dict) else {} primary_pdf = meta.get("primary_source_pdf") or "" completeness = meta.get("completeness_pct") curated_at = meta.get("curated_at") or "" notes = meta.get("notes") or "" lines: list[str] = [] # YAML frontmatter lines.append("---") lines.append(f"policy_id: {pid}") lines.append(f"insurer_slug: {insurer_slug}") lines.append(f"insurer_name: {insurer_name}") lines.append(f"policy_name: {json.dumps(policy_name, ensure_ascii=False)}") if uin: lines.append(f"uin_code: {uin}") if primary_pdf: lines.append(f"source_pdf_path: {primary_pdf}") if completeness is not None: lines.append(f"completeness_pct: {completeness}") if curated_at: lines.append(f"curated_at: {curated_at}") lines.append("---") lines.append("") lines.append(f"# {policy_name}") lines.append("") # Header block header = ( f"**Insurer:** {insurer_name} (`{insurer_slug}`) \n" f"**Policy ID:** `{pid}`" ) if uin: header += f" \n**UIN:** `{uin}`" if completeness is not None: header += f" \n**Curation completeness:** {completeness}%" if primary_pdf: header += f" \n**Primary source PDF:** `{primary_pdf}`" if curated_at: header += f" \n**Curated at:** {curated_at}" lines.append(header) if notes: lines.append("") lines.append(f"> _Curation note: {notes}_") lines.append("") # Render grouped sections (skip empty groups) rendered_keys: set[str] = set() for grp_name, grp_fields in FIELD_GROUPS: section_blocks: list[str] = [] for fkey in grp_fields: if fkey in p: section_blocks.append(render_field(fkey, p[fkey])) rendered_keys.add(fkey) if not section_blocks: continue lines.append(f"## {grp_name}") lines.append("") lines.append("\n".join(section_blocks)) # Anything else that's in the JSON but not in a group remaining = [ k for k in p.keys() if k not in rendered_keys and not k.startswith("_") and k not in ("policy_id", "policy_name", "insurer_slug") ] if remaining: lines.append("## Other fields") lines.append("") for fkey in sorted(remaining): lines.append(render_field(fkey, p[fkey])) lines.append("") lines.append("---") lines.append("") lines.append( f"_Mirrored from `40-data/policy_facts/{source_json_path.name}`. " "Provenance — every field's verbatim quote and source PDF path is " "preserved exactly as curated. Do not hand-edit; regenerate via " "`tools/build_kb_mirror.py`._" ) return "\n".join(lines) + "\n" # --------------------------------------------------------------------------- # 3. GLOSSARY (mirror of frontend/src/lib/i18n.ts) — 13 terms × {en, hi} # --------------------------------------------------------------------------- GLOSSARY = { "PED": { "en": { "title": "Pre-Existing Disease (PED)", "body": "A health condition you already have when you buy the policy — diabetes, BP, thyroid, anything chronic. Most policies don't cover it for the first 24-48 months. Be honest about yours: hiding it gets your claim denied later.", }, "hi": { "title": "Pre-Existing Disease (पहले से चली आ रही बीमारी)", "body": "जो बीमारी आपको policy खरीदते समय पहले से है — diabetes, BP, थायरॉइड etc. ज़्यादातर policies शुरू के 24-48 महीनों में cover नहीं करतीं। ईमानदारी से बताइए, छिपाने से claim बाद में reject हो जाता है।", }, }, "AYUSH": { "en": { "title": "AYUSH coverage", "body": "Whether the policy pays for Ayurveda, Yoga, Unani, Siddha, and Homeopathy treatments at recognised hospitals. If you use these traditional systems, this matters; if you only use allopathic care, less so.", }, "hi": { "title": "AYUSH कवर", "body": "क्या policy आयुर्वेद, योग, यूनानी, सिद्ध, और होम्योपैथी treatments को cover करती है। अगर आप इन पारंपरिक चिकित्सा का उपयोग करते हैं, यह ज़रूरी है।", }, }, "NCB": { "en": { "title": "No-Claim Bonus (NCB)", "body": "Reward for not claiming in a year — your sum insured goes up (typically 25-50%) without raising your premium. Bigger NCB compounds over years if you stay claim-free.", }, "hi": { "title": "No-Claim Bonus (NCB)", "body": "बिना claim किए साल पूरा करने का इनाम — sum insured बढ़ जाता है (आम तौर पर 25-50%) बिना premium बढ़ाए।", }, }, "SI": { "en": { "title": "Sum Insured (SI)", "body": "The maximum amount the insurer pays in a policy year. For a single hospitalisation in a metro, ₹10L is the floor; ₹20L+ is safer if you have parents or family to cover.", }, "hi": { "title": "Sum Insured (बीमित राशि)", "body": "एक policy साल में बीमाकर्ता अधिकतम कितना देगा। Metro में एक hospitalisation के लिए ₹10L न्यूनतम; ₹20L+ माता-पिता या परिवार के लिए सुरक्षित।", }, }, "CSR": { "en": { "title": "Claim Settlement Ratio (CSR)", "body": "Of every 100 claims the insurer received, how many they paid. IRDAI publishes this annually. <90% = caution; 95%+ = excellent. Single most predictive metric of 'will my claim get paid'.", }, "hi": { "title": "Claim Settlement Ratio", "body": "100 claims में से बीमाकर्ता कितने pay करता है। IRDAI सालाना publish करता है। <90% = सावधान; 95%+ = बढ़िया।", }, }, "Cashless": { "en": { "title": "Cashless treatment", "body": "You don't pay the hospital — the insurer pays them directly via a pre-authorisation. Only works at network hospitals. Without it, you pay upfront and file for reimbursement later.", }, "hi": { "title": "Cashless इलाज", "body": "आप hospital को सीधे payment नहीं करते — बीमाकर्ता pre-authorisation से payment करता है। सिर्फ network hospitals पर काम करता है।", }, }, "TAT": { "en": { "title": "Cashless TAT (Turnaround Time)", "body": "How fast the insurer approves your cashless pre-auth at the hospital desk. ≤2 hours = gold standard; ≥24h = your family pays cash first and waits for reimbursement.", }, "hi": { "title": "Cashless TAT", "body": "बीमाकर्ता hospital में cashless approval कितनी जल्दी देता है। ≤2 घंटे = बढ़िया; ≥24 घंटे = परिवार को पहले cash देना पड़ेगा।", }, }, "UIN": { "en": { "title": "Unique Identification Number (UIN)", "body": "IRDAI-assigned ID for each policy product — proves it's a regulator-approved plan. You can search a UIN on irdai.gov.in to verify the policy exists and see its filed terms.", }, "hi": { "title": "UIN (Unique ID)", "body": "IRDAI द्वारा हर policy को दिया गया ID — यह साबित करता है कि policy regulator से approved है।", }, }, "CoPay": { "en": { "title": "Co-payment", "body": "The % of every claim YOU pay out of pocket. 20% co-pay on a ₹5L hospital bill = you pay ₹1L; insurer pays ₹4L. Lower premium upfront, but bigger surprise at claim time.", }, "hi": { "title": "Co-payment", "body": "हर claim का जो % आप अपनी जेब से देते हैं। ₹5L hospital bill पर 20% co-pay = आप ₹1L दें, बीमाकर्ता ₹4L।", }, }, "Deductible": { "en": { "title": "Deductible", "body": "Fixed rupee amount you pay BEFORE the insurer starts paying. ₹50k deductible = first ₹50k of every claim is on you. Reduces premium significantly but adds out-of-pocket risk.", }, "hi": { "title": "Deductible", "body": "वो fixed amount जो आप बीमाकर्ता के payment शुरू करने से पहले देते हैं।", }, }, "Floater": { "en": { "title": "Family Floater", "body": "One sum insured shared by everyone in the family. ₹15L floater for 4 people = anyone (or everyone) can use up to ₹15L combined. Cheaper than individual policies if claims are rare.", }, "hi": { "title": "Family Floater", "body": "एक sum insured पूरे परिवार के लिए share होती है। 4 लोगों के लिए ₹15L floater = कोई भी ₹15L तक use कर सकता है।", }, }, "SubLimit": { "en": { "title": "Sub-limit", "body": "A cap WITHIN your sum insured for a specific treatment — e.g., room rent capped at 1% of SI, or maternity capped at ₹50k. Watch for these — they're the #1 reason actual reimbursement < bill.", }, "hi": { "title": "Sub-limit", "body": "Sum insured के अंदर कुछ खास treatments पर एक सीमा — जैसे room rent SI का 1%, या maternity ₹50k तक। यह सबसे बड़ी वजह है कि real payment bill से कम होता है।", }, }, "RoomRent": { "en": { "title": "Room rent capping", "body": "Some policies pay only up to a % of SI per day of hospital room — e.g., 1% of ₹5L = ₹5k/day. Choose a more expensive room and ALL your other charges get scaled down proportionally. Look for 'No room rent limit'.", }, "hi": { "title": "Room rent capping", "body": "कई policies hospital room के लिए सिर्फ SI का % देती हैं — जैसे 1% का ₹5L = ₹5k/दिन। महंगा कमरा लें तो सभी अन्य charges भी scale down हो जाते हैं।", }, }, } # --------------------------------------------------------------------------- # 4. Drive # --------------------------------------------------------------------------- def main() -> int: sys.path.insert(0, str(ROOT)) from backend.scorecard import ( # type: ignore METHODOLOGY_BLUEPRINT, WEIGHTS, SCORED_FIELDS, ) new_files = 0 updated_files = 0 # 4a. methodology/scorecard.json method_path = METHOD_OUT / "scorecard.json" existed_method = method_path.exists() method_path.write_text( json.dumps( { "weights": WEIGHTS, "scored_fields": SCORED_FIELDS, "methodology": METHODOLOGY_BLUEPRINT, }, indent=2, ensure_ascii=False, ) + "\n", encoding="utf-8", ) if existed_method: updated_files += 1 else: new_files += 1 # 4b. methodology/glossary.json gloss_path = METHOD_OUT / "glossary.json" existed_gloss = gloss_path.exists() gloss_path.write_text( json.dumps(GLOSSARY, indent=2, ensure_ascii=False) + "\n", encoding="utf-8", ) if existed_gloss: updated_files += 1 else: new_files += 1 # 4c. verbatim copies of three 70-docs/*.md files copy_map = { "discovery-script.md": "discovery-script.md", "scorecard-knowledge-graph.md": "knowledge-graph.md", "tie-breaker-rubric.md": "tie-breakers.md", } for src_name, dest_name in copy_map.items(): src = DOCS / src_name dest = METHOD_OUT / dest_name if src.exists(): existed = dest.exists() dest.write_text(src.read_text(encoding="utf-8"), encoding="utf-8") if existed: updated_files += 1 else: new_files += 1 # 4d. one MD per 40-data/policy_facts/*.json index_rows: list[tuple[str, str, str, str, str]] = [] # insurer, name, uin, completeness, kb path written = 0 skipped = 0 written_pids: set[str] = set() for j in sorted(DATA_IN.glob("*.json")): if j.name.startswith("_"): continue try: data = json.loads(j.read_text(encoding="utf-8")) except Exception as e: print(f"SKIP {j.name}: {e}", file=sys.stderr) skipped += 1 continue pid = data.get("policy_id") or j.stem md = render_policy_md(data, j) out = POLICIES_OUT / f"{pid}.md" existed = out.exists() out.write_text(md, encoding="utf-8") if existed: updated_files += 1 else: new_files += 1 written += 1 written_pids.add(pid) insurer_slug = data.get("insurer_slug") or pid.split("__", 1)[0] insurer_name = INSURER_NAMES.get(insurer_slug, insurer_slug) policy_name = data.get("policy_name") or pid uin = "" if isinstance(data.get("uin_code"), dict): uin = data["uin_code"].get("value") or "" meta = data.get("_meta") or {} completeness = meta.get("completeness_pct") completeness_str = f"{completeness}%" if completeness is not None else "—" rel = f"policies/{pid}.md" index_rows.append((insurer_name, policy_name, uin or "—", completeness_str, rel)) # 4d-clean. remove stale MD files (no longer backed by 40-data/policy_facts/) stale_removed = 0 for f in POLICIES_OUT.glob("*.md"): if f.stem not in written_pids: f.unlink() stale_removed += 1 if stale_removed: print(f" (removed {stale_removed} stale MD files no longer in 40-data/policy_facts/)", file=sys.stderr) # 4e. kb/INDEX.md today = date.today().isoformat() idx: list[str] = [] idx.append("# Knowledge Base — Insurance Sales Bot") idx.append("") idx.append(f"_Last synced: {today}._") idx.append("") idx.append( "Canonical knowledge base for the Insurance Sales Bot. Every user-facing " "answer, scorecard, and comparison surface must trace back to a file in " "this directory." ) idx.append("") idx.append(f"## Policies ({len(index_rows)})") idx.append("") idx.append("| Insurer | Policy | UIN | Completeness | KB path |") idx.append("| --- | --- | --- | --- | --- |") for insurer, name, uin, comp, rel in sorted(index_rows): idx.append(f"| {insurer} | {name} | `{uin}` | {comp} | [`{rel}`]({rel}) |") idx.append("") idx.append("## Methodology") idx.append("") idx.append("| File | What it contains |") idx.append("| --- | --- |") idx.append( "| [`methodology/scorecard.json`](methodology/scorecard.json) | " "Authoritative methodology contract: 6 sub-scores, weights, scored-field " "list, consumer rationale, anchors. Exported from `backend/scorecard.py`. |" ) idx.append( "| [`methodology/glossary.json`](methodology/glossary.json) | " "User-facing jargon explanation contract — 13 terms × {en, hi} × " "{title, body}. Mirror of `frontend/src/lib/i18n.ts` GLOSSARY. |" ) idx.append( "| [`methodology/discovery-script.md`](methodology/discovery-script.md) | " "Profile Builder discovery script — verbatim copy of `70-docs/discovery-script.md`. |" ) idx.append( "| [`methodology/knowledge-graph.md`](methodology/knowledge-graph.md) | " "Profile-field ↔ sub-score weight-shift map — verbatim copy of " "`70-docs/scorecard-knowledge-graph.md`. |" ) idx.append( "| [`methodology/tie-breakers.md`](methodology/tie-breakers.md) | " "Recommendation tie-breaker rubric — verbatim copy of `70-docs/tie-breaker-rubric.md`. |" ) idx.append( "| [`methodology/INDEX.md`](methodology/INDEX.md) | " "Pointer index to all design / decision docs. |" ) idx.append("") idx.append("## Data lineage") idx.append("") idx.append( "- [`AUDIT_TRAIL.md`](AUDIT_TRAIL.md) — end-to-end pipeline lineage + " "per-batch curation log." ) idx.append("") idx.append("## Layout") idx.append("") idx.append("```") idx.append("kb/") idx.append("├── INDEX.md (this file)") idx.append("├── AUDIT_TRAIL.md (data lineage + curation history)") idx.append(f"├── policies/.md ({len(index_rows)} files — one per curated policy)") idx.append("├── methodology/") idx.append("│ ├── scorecard.json (6 sub-scores + weights + anchors)") idx.append("│ ├── glossary.json (13 terms × en/hi)") idx.append("│ ├── discovery-script.md") idx.append("│ ├── knowledge-graph.md") idx.append("│ ├── tie-breakers.md") idx.append("│ └── INDEX.md") idx.append("├── research/") idx.append("├── calculations/") idx.append("├── reviews/") idx.append("├── premiums/") idx.append("├── security/") idx.append("└── eval/") idx.append("```") idx.append("") idx.append("## Provenance convention") idx.append("") idx.append( "Every `policies/.md` file is generated from " "`40-data/policy_facts/.json` and preserves the verbatim source quote and " "source PDF path for every field. JSON is the machine source; markdown is " "the human-readable mirror. Regenerate the entire kb/ tree by running " "`.venv/bin/python3 tools/build_kb_mirror.py`." ) idx.append("") index_path = KB / "INDEX.md" existed_idx = index_path.exists() index_path.write_text("\n".join(idx), encoding="utf-8") if existed_idx: updated_files += 1 else: new_files += 1 # 4f. AUDIT_TRAIL.md — append today's batch block audit_path = KB / "AUDIT_TRAIL.md" existing = audit_path.read_text(encoding="utf-8") if audit_path.exists() else "" batch_marker = "## Batch — 2026-05-14" appended = False if batch_marker not in existing: ap: list[str] = [] ap.append("") ap.append(batch_marker) ap.append("") ap.append( "Three back-to-back curation passes brought the `40-data/policy_facts/` " f"directory to **{len(index_rows)} policies** with verbatim-quote " "provenance. Mirrored into `kb/policies/` today." ) ap.append("") ap.append( "- **Batch 1 — human-research curation (22 policies).** Manual + " "agent-assisted verbatim extraction from local PDFs in `rag/corpus/` " "for the 22 highest-priority wordings. Schema: " "`{value, unit?, source_pdf_path, source_quote}` per field with a " "`_meta` block (`curated_at`, `primary_source_pdf`, `completeness_pct`, " "`notes`). Average completeness ≈83.5%. Recorded in " "[`40-data/policy_facts/_curation_report.md`](../40-data/policy_facts/_curation_report.md)." ) ap.append( "- **Batch 2 — regex + pdfplumber pass (43 policies).** Automated " "pattern extraction across the remaining retail health policy PDFs. " "Each field carries the same provenance triple; numeric values were " "validated against the verbatim quote before being written." ) ap.append( "- **Batch 3 — group / specialty policies (37 policies).** " "`tools/curate_remaining.py` extended coverage to group, top-up, " "critical-illness, personal-accident, and specialty riders. Marked " "with `policy_type` (e.g. `hospital_cash`) where the wording diverged " "from indemnity templates." ) ap.append("") ap.append( "**Verification.** `tools/info_source_map.py` produced " "[`eval/info_source_map.json`](../eval/info_source_map.json) and " "[`40-data/information_source_map.md`](../40-data/information_source_map.md) " "with verdict counts: **✅ 798 / ⚠️ 321 / ❌ 0 / ⏳ 1385.** No ❌ " "(broken-link) verdicts remain; the ⏳ tail tracks deferred " "verifications. The ✅:⚠️ ratio is the canonical KPI for " "source-grounding quality on this dataset." ) ap.append("") ap.append("**UI / runtime changes shipped today:**") ap.append("") ap.append( "- **Profile Builder tab** — guided 8-question discovery flow " "(`70-docs/discovery-script.md`). Profile-completeness gate (≥0.6) " "controls whether the personalised scorecard renders." ) ap.append( "- **Score gate on policy cards** — recommendations suppress the " "per-buyer letter grade until completeness ≥ 0.6 (universal IRDAI " "metrics like CSR and complaints/10K still render, since they're " "insurer-level)." ) ap.append( "- **EN ↔ हिं i18n** — full bilingual UI with the 13-term jargon " "glossary at `frontend/src/lib/i18n.ts` (mirrored to " "`kb/methodology/glossary.json`)." ) ap.append( "- **Scorecard methodology expander** — every grade opens a " "transparency panel sourced from `METHODOLOGY_BLUEPRINT` (mirrored " "to `kb/methodology/scorecard.json`)." ) ap.append( "- **Source-quote popovers** — hovering a fact on a policy card " "surfaces the verbatim PDF quote that backed it." ) ap.append( "- **Cerebras Qwen-3-235B wired as primary judge** — replaces the " "previous Groq Llama-3.1 grader for the eval pipeline; legacy " "provider retained as fallback." ) audit_path.write_text( existing.rstrip() + "\n" + "\n".join(ap) + "\n", encoding="utf-8" ) appended = True updated_files += 1 print( f"Synced {len(index_rows)} policies + methodology to kb/. " f"New files: {new_files}. Updated: {updated_files}." ) if skipped: print(f" (skipped {skipped} unparseable JSON files)", file=sys.stderr) return 0 if __name__ == "__main__": sys.exit(main())