Skip to content

California SBCs: a hand-built manifest, Terms-gated crawling, and manual import - #39

Merged
Badri-Rajendran merged 5 commits into
mainfrom
feature_ca_sbc
Sep 26, 2026
Merged

Badri-Rajendran merged 5 commits into
mainfrom
feature_ca_sbc

Conversation

@Badri-Rajendran

Copy link
Copy Markdown
Owner

Problem

Every California plan loaded from CMS's state-based exchange PUF (ADR 0024) had no SBC link, because the PUF's SBC column is empty. So every California coverage question ended with "no Summary of Benefits and Coverage link is listed for this plan". The SBC pipeline had three gaps:

  • It accepted only the 30 HealthCare.gov states.
  • It read only CMS-provided links.
  • It had no way to take a PDF a person downloaded.

Decisions (ADR 0026, amending 0013, 0019 and 0025)

  • Links come from a hand-built manifest. Carriers publish their SBCs in very different ways, and their pages print no HIOS IDs, so a person finds and checks each link.

  • Website Terms govern, as for Covered California. A carrier whose Terms ban robots or scrapers is manual-only, even where robots.txt allows us.

    Treatment Carriers
    Crawled IEHP, Molina, Sharp, Balance by CCHP
    Manual-only Kaiser, Blue Shield, Western Health Advantage, Anthem, L.A. Care, Valley
    Disabled Health Net (SBCs only behind a POST search form)
  • A new registry status, mandated_disclosure. An SBC is published because federal law requires it, and that is not a licence. The status is allowed only for an sbc_host, and always with commercial_use = "review", because nearly every carrier's Terms limit use to personal or non-commercial purposes. A commercial launch needs a legal review of these entries.

  • Base-plan SBC only. Answers say so.

Implementation (5 commits)

  1. Registry. There are 11 California sbc_host entries, each with verbatim Terms evidence, its robots outcome, its access and how to remove it. sbc_host_for matches the longest prefix. Every scope URL must now be an https://host/… prefix.
  2. Manifest, gating and import.
    • src/ingestion/sbc/manifests/ca-2026.csv links 114 of 190 plans to 58 documents.
    • make apply-sbc-manifest validates, then applies. It is authoritative: a plan with no row, or on a disabled host, has no link. make ingest-ca-plans also runs it after each load.
    • make check-sbc-manifest lists what a person should look at.
    • make ingest-sbc STATES=CA is gated by the registry: crawl is fetched, manual is never requested, and unregistered or disabled links are skipped.
    • make import-sbc reads a PDF a person downloaded, with no request made. It is recorded as acquisition = 'manual' (migration 3b1d6e2a9c47) and never re-requested.
  3. Fix, older than this work. ingest-sbc counted a recorded failure both as "already current" and as "skipped". It is now counted once.
  4. Server-written notice. When a California plan's SBC is quoted, the answer opens with a notice that the SBC shows the plan's standard version, and that people with cost-sharing reductions or American Indian and Alaska Native cost sharing pay less. A first version asked the model to include a note, and the model dropped it in a live question.
  5. Docs. docs/findings/ca-sbc.md, the runbook sections, the README and CHANGELOG.

Validation

  • make check: 643 tests pass with 94.1% coverage. alembic check is clean, and bandit and pip-audit report nothing.
    • New tests: test_sbc_manifest, test_sbc_import (a mock fails the test on any network request), California gating in test_sbc_ingest and test_sbc_refresh, and additions to test_registry, test_sbc_report, test_ca_puf_load, test_tools, test_plan_coverage, test_ingestion_plans and test_sbc_fetch.
    • A test checks the committed manifest against the committed registry.
  • Real data:
    • apply-sbc-manifest: 114 plans linked to 58 documents.
    • ingest-sbc STATES=CA: 19 of 19 crawlable documents read ok, and 39 are awaiting manual import.
    • check-sbc-manifest: no name mismatches.
    • sbc-report STATES=CA VERIFY=1: no missing or changed files.
  • eval_sbc_ranking --year 2026: 1,303 of 1,320 (98.7%; the floor is 97%). IEHP, Molina and Sharp each scored 20/20.
  • Live question: urgent care for Sharp Silver 70 Performance HMO and Kaiser Silver 70 HMO. Sharp was answered from its SBC and cited. Kaiser, not yet imported, got a link to its PDF. The answer opened with the notice.
  • Every intermediate commit passes the test suite on its own.

Still to do before merge: the manual carriers' 39 SBCs are downloaded in a browser and imported. Any manifest row that fails, since Anthem's links are inferred and L.A. Care's and Valley's come from search results, is fixed in a further commit, and the findings counts are updated.

Security, database and API impact

  • Security:
    • Manifest links must pass fetch.unsafe_reason (HTTPS, public host) and be covered by a registry entry.
    • Imported files are checked for a PDF header and the size cap, and are written only to a hash-derived path under data/sbc/.
    • SQL is parameterized only.
    • The repository holds public URLs and plan IDs only, and no documents: data/ stays gitignored and no PDF is tracked. No file is ever deleted (ADR 0016).
    • No secrets or PII are added to logs or prompts. The notice is written by the server, never by the model.
  • Database: migration 3b1d6e2a9c47 adds sbc_documents.acquisition (String(8), NOT NULL, default 'crawl', checked). It is additive and reversible.
  • API: no endpoint changes. plan_coverage results no longer carry a note; the notice is prepended server-side instead.
  • Scripts that contacted carriers before their Terms were read are recorded in docs/findings/ca-sbc.md, and none of those files is used.

Known follow-ups

  • Apply the Terms rule to the 30-state crawl. For example, it reads Anthem's other-state SBCs, and Anthem's Terms ban robots.
  • Per-variant SBCs.
  • Find Balance by CCHP's 2026 SBCs (only 2025 ones are published) and Valley's 3 missing ones.

CMS's California file has no SBC links, so sub-project 2 links each plan by
hand and reads its SBC only as the carrier allows. This commit records that
decision and the carriers.

ADR 0026 and the design spec. The rule that shapes everything: a carrier's
website Terms govern, as they already did for Covered California. Kaiser,
Blue Shield, Western Health Advantage and Anthem ban robots or scrapers in
their Terms although robots.txt allows us, and L.A. Care appears to; they are
manual-only, with Valley, whose host blocks bots. IEHP, Molina, Sharp and
Balance by CCHP are crawled. Health Net is recorded, disabled.

The registry gains mandated_disclosure: an SBC is published because federal
law requires it, not under a licence, so the status is allowed only for an
sbc_host and only with commercial use under review. sbc_host_for finds a
link's entry by the longest prefix. Every scope URL must now be
https://host/..., so a prefix can never also match a look-alike host.

ADR 0026 amends 0013 (Terms can refuse us; a person's browser download is not
the crawler working around a refusal), 0019 (manual documents are never
re-requested) and 0025 (the new status).
…t by hand

Every California plan had no SBC link, because CMS's file has none.

A committed manifest (src/ingestion/sbc/manifests/ca-2026.csv) now gives 114
of the 190 plans a link, to 58 documents. Each row was found and checked by
hand; the evidence is in docs/findings/ca-sbc.md. load_manifest checks the
file on its own: the plan ID, the year, a safe HTTPS link, and a California
registry entry covering it. apply_manifest is authoritative: a plan with no
row, or whose carrier's entry is disabled, loses its link, which is how a
carrier is removed. make ingest-ca-plans applies it after every load, and
make check-sbc-manifest lists what a person should look at after an ingest.

make ingest-sbc STATES=CA works, gated by the registry: a crawl carrier is
fetched with every existing check, a manual one is never requested, and an
unregistered or disabled link is skipped. The 30 API states are read as
before. First run: the 19 IEHP, Molina and Sharp SBCs all read ok, with no
name mismatches; 39 documents await a manual import.

make import-sbc reads a PDF a person downloaded in a browser. The link must
be a plan's and its carrier enabled, and the file a PDF under the size cap.
The file is copied into the link's cache path, archiving any different one,
and parsed with no request. It is recorded as acquisition = 'manual'
(migration 3b1d6e2a9c47), kept through re-parses, never refreshed, and
reported for re-import if its file goes missing. sbc-report counts read
documents by acquisition.
make ingest-sbc's first line gave "N already current" as every document it
was not reading, which included the recorded failures that the next line
reported as "M recorded failures skipped". A blocked document was counted
twice, and a run over a host that refuses us read as mostly healthy.

It now counts a recorded failure once, as skipped. This predates the
California work; the code review of sub-project 2 found it.
California's links are chosen by hand, one document per base plan: the
standard (-01) version's. People who qualify for cost-sharing reductions or
American Indian and Alaska Native cost sharing pay less than that document
shows, and nothing in it says so.

When plan_coverage quotes a filed-rate state's plan, the answer now opens
with a server-written notice saying so. plan_coverage returns each plan's
state to decide it. It uses the prior-year notice's mechanism, for the same
reason: a first version put a note in the tool result and asked the model to
include it, and in a live question about Sharp's and Molina's Silver 70 plans
the model left it out. API-state plans are unchanged, since nothing has been
checked about which variant their links point at.
… commands

docs/findings/ca-sbc.md records the per-carrier research behind ADR 0026:
- where each carrier publishes;
- its Terms on automated access, quoted from the pages;
- its robots.txt;
- how each manifest row was checked;
- which requests scripts made before the Terms were read;
- the first run: 19 of 190 plans read, 95 awaiting a manual import, 76 with
  no link, and the ranking eval at 98.7%.

The California runbook gains the yearly SBC routine: re-check each carrier's
robots.txt and Terms before any request, write the manifest, apply it, crawl,
download the manual carriers' SBCs in a browser, import them, and check. The
SBC runbook says how imported documents behave in the monthly one. The
README documents apply-sbc-manifest, check-sbc-manifest and import-sbc, and
the registry's new status.
Copilot AI lite review requested due to automatic review settings September 26, 2026 20:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Badri-Rajendran
Badri-Rajendran merged commit 917adbf into main Sep 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants