InsuranceBot / tools /build_kb_mirror.py
rohitsar567's picture
refactor: KI-050 — complete data/ → 40-data/ rename across all Python refs
52c6351
Raw
History Blame Contribute Delete
31.2 kB
#!/usr/bin/env python3
"""Mirror today's data + design work into kb/.
Reads:
- 40-data/policy_facts/*.json -> kb/policies/<id>.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/<policy_id>.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/<id>.md` file is generated from "
"`40-data/policy_facts/<id>.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())