Skip to main content

Zero-PHI source-assurance CLI for healthcare's public data inputs

Project description

SourceLock

Every answer comes with a receipt, and your build fails when the source moves.

SourceLock reads the public data healthcare software runs on — CMS coverage policy, NPPES and PECOS, ICD-10-CM and HCPCS releases, CMS-HCC risk models, the OIG LEIE — and returns, with every answer, the release it came from, the SHA-256 of the raw bytes behind it, when those bytes were retrieved, and what the answer does not prove. hc-source lock init pins those sources in a source-lock.json; hc-source doctor verifies that pin and fails CI the day one of them changes underneath you.

Install

pipx install sourcelock

Python 3.11 or newer. pip install sourcelock works too, inside a virtualenv. Either way you get one executable: hc-source.

Vendored date-of-service coverage

  • ICD-10-CM: 2023-10-01 through 2027-03-31. Vendored releases are fy2024, fy2024-april, fy2025, fy2025-april, fy2026, fy2026-april, and fy2027. FY2027 is supported only through March 31, 2027 because the April 2027 publication status is not yet settled.
  • HCPCS Level II: 2023-01-01 through 2026-09-30. Every quarterly public-use file from 2023q1 through 2026q3 is vendored.

CMS's ICD-10 publication record defines a distinct FY2025 base window (October 1, 2024 through March 31, 2025) and April-update window (April 1 through September 30, 2025). The CDC archive retains both upstream ZIPs. Their code-description members are byte-identical, so the manifest keeps two release/provenance entries while deduplicating the derived table; FY2024's published April update is handled the same way.

Dates outside the windows above fail closed at exit 3 with RELEASE_NOT_VENDORED (or AMBIGUOUS_WINDOW when an April update is not yet settled). SourceLock never substitutes a different release's snapshot.

See it work, offline, right now

hc-source valid-on E11.9 2026-07-01

No network, no credentials, no lockfile. The release tables are vendored, so that command answers in under a second on a plane:

{
  "code": "E11.9",
  "code_nodot": "E119",
  "code_system": "ICD-10-CM",
  "date": "2026-07-01",
  "valid": true,
  "description": "Type 2 diabetes mellitus without complications",
  "release": "fy2026-april",
  "release_effective_from": "2026-04-01",
  "release_effective_to": "2026-09-30"
}

followed by the receipt — source, route, release, effective window, retrieval time, upstream status, the hash of the bytes the answer was derived from, the transform version, and whether a mirror answered — and then the part most tools leave out. Four non-claims, printed in full; the first line of each:

does not prove:
  - PHI_NOT_EXPECTED: this tool accepts only public typed parameters.
  - DOES_NOT_INCLUDE_CPT_DESCRIPTORS: no HCPCS Level I (CPT) record and no ADA
  - DOES_NOT_PROVE_COVERAGE: a code being valid for a date of service says
  - DOES_NOT_PROVE_CURRENCY: answers come from the release train vendored at

The real output does not wrap and does not trail off: the CPT one runs to a paragraph naming all seventeen CMS descriptions that quote a CPT number, because that is what an AMA licence question actually needs. These four are what codes.valid_on claims it cannot tell you — every one of them, not a representative sample, and nothing this product does not actually print.

What it is for

If you are building AI or automation over revenue-cycle data, you have run into some version of this:

  • A model cited a code set and nobody can say which release. Every answer here carries source_version, effective_from/effective_to, and raw_sha256. That is an audit trail, not a log line.
  • A pipeline broke because CMS renamed something. NPPES retired its V1 bulk filenames on 2026-03-03; jobs that still generated them got silent 404s. hc-source lock init pins 23 cheap checks across six sources and hc-source doctor fails the build the day one moves — with the fix, not a stack trace.
  • An agent will confidently answer from a stale cache. Receipts distinguish when bytes were retrieved from when upstream last confirmed them, a canary that read out-of-date data is not allowed to report ok, and a source that could not be read is never reported as unchanged.
  • Your reference data is not all public. Sources are plugins. Your licensed AMA CPT tables can be a first-class adapter without forking anything.
  • PHI must not end up in a tool call. Parameters are public and typed, no receipt or log carries a payload, and a guard refuses input shaped like a patient record. It is a structural best-effort refusal, not a HIPAA control: hc_source/guard.py states exactly what it detects and, just as plainly, which evasion classes it does not close.

The six sources

source what it answers
codes ICD-10-CM / HCPCS Level II validity on a date of service, release trains
hcc CMS-HCC V24/V28 mapping, hierarchies, community continuing-enrollee scoring
provider NPI Registry lookups, NPPES bulk-file cadence, PECOS enrollment snapshot
coverage CMS coverage policy: NCDs, LCDs and articles, MCD weekly snapshot identity
leie OIG LEIE exclusion checks by NPI, candidate search, monthly refresh status
demo packaged reference adapter used in tests and examples

codes and hcc answer offline from vendored release data. The rest read live CMS and OIG endpoints.

Everyday commands

hc-source valid-on E11.9 2026-07-01                     # offline
hc-source hcc score --dx E11.9,I50.9 --model v28 --year 2026 --age 72 --sex F
hc-source lookup-npi 1003000126                         # NPPES
hc-source check-npi 1003000126                          # OIG LEIE screen
hc-source tools                                         # every route, with its parameters
hc-source call coverage.lookup_ncd --param section=30.3 # the general form
hc-source mcp                                           # serve it all to an agent over MCP

call reaches every route, including ones provided by adapters you installed; the named commands are shorthand for the four questions people arrive with.

Machine contract for tool calls

This is a breaking CLI contract change for the next release. hc-source call and the four route shortcuts use these exit codes:

exit meaning trigger
0 answer A positive or negative answer was produced. Negative answers are structured (found: false, an empty result list, or LEIE screen_result: "clear"); data is never null.
1 invalid input The tool name, parameter syntax, PHI guard, or typed parameter validation rejected the call.
2 source or adapter failure The authority could not be read, or the adapter failed unexpectedly or returned an invalid response before it could produce evidenced data.
3 deliberate refusal The route failed closed because the requested release/window/model year cannot be answered from the vendored evidence, or because every HCC diagnosis failed the validity gate without an explicit bypass. data is null; receipt.status is "refused"; receipt.refusal_code is stable (for example AMBIGUOUS_WINDOW, RELEASE_NOT_VENDORED, REFUSED_MODEL_YEAR, or NO_VALID_DIAGNOSES).

With --json, every one of those paths emits the same top-level shape:

{
  "data": {},
  "receipt": {"status": "answered", "refusal_code": null},
  "error": null
}

Exactly one outcome channel is populated: answers carry non-null data and an answered receipt; refusals carry data: null and a refused receipt; validation/source/adapter failures carry data: null, receipt: null, and a structured error (INVALID_INPUT, SOURCE_UNREACHABLE, or ADAPTER_FAILURE). MCP returns that same envelope as structuredContent and marks failures and deliberate refusals as MCP errors.

hcc.score has no diagnosis/date-of-service parameter, so it uses one explicit validity anchor for every payment year: the vendored ICD-10-CM fy2026-april release, effective 2026-04-01 through 2026-09-30. The answer payload and receipt name that release and its hash. A mixed list still scores from its valid codes; a valid-but-unmapped code keeps the existing named-unmapped behavior. If every code is invalid, the route refuses with NO_VALID_DIAGNOSES. Use --allow-demographic-only (or MCP/call parameter allow_demographic_only=true) only to opt into a demographic-only partial RAF.

Pin your sources and fail the build when they move

hc-source lock init      # observe all six sources, write source-lock.json
hc-source doctor         # re-check upstream against the lockfile
hc-source init --ci      # scaffold the GitHub Actions workflow

Commit source-lock.json and review its diffs like any other lockfile. A green run ends with:

23 ok, 0 drift, 0 unreachable, 0 schema_changed, 0 unpinned, 0 stale, 0 error

Doctor has a separate, unchanged 0–4 verdict contract: 0 everything matched, 1 drift or schema change, 2 a source was unreachable or an adapter broke, 3 a canary was observed but nothing pinned it, 4 everything matched but something reported that what it read is out of date. 3 and 4 exist because a missing source-lock.json used to report 23 ok, 0 drift at exit 0 — a build that verified nothing, reporting green. Full CI setup, per-canary severity, and the on-unreachable / on-stale policies: docs/ci.md.

Watch it catch drift, in 90 seconds

You do not have to wait for CMS to change something. NPPES really did retire its V1 bulk-file names on 2026-03-03. The repo carries a recording of what the listing page would look like if that class of change happened again:

hc-source lock init
HC_SOURCE_PROVIDER_NPPES_FILES_URL="file://$PWD/tests/fixtures/provider/npi_files_v1_only.html" \
  hc-source doctor --source provider

Doctor exits 1, marks provider.nppes_v2_files as drift, and prints:

provider.nppes_v2_files [drift]: NPPES retired the V1 bulk files on 2026-03-03; only
*_V2.zip names are published now (V2 extends the First Name and Legal Business Name
field lengths). The listing this canary just read carries a V1-style name with no _V2
suffix, which means the page has regressed, a stale mirror is being served, or
something upstream renamed the grammar again. Fix: open
https://download.cms.gov/nppes/NPI_Files.html, read the 'Important Information' block
(where CMS announced the V1 retirement), confirm the current version suffix, make sure
nothing in your pipeline still generates V1 filenames
(NPPES_Data_Dissemination_<Month>_<YYYY>.zip silently 404s), then re-pin with
`hc-source lock init`.

Run it again without the override and it goes back to green. That is the whole product: pin what your build depends on, and get told — with an instruction, not a stack trace — the moment upstream moves.

For agents

hc-source mcp serves every discovered tool over MCP stdio: one MCP tool per route, the same typed parameters, the same receipts, the same PHI refusals as the CLI. It refuses to start if any adapter failed to import, because an agent that sees a short tools/list reads it as the whole product. Every MCP result, including validation failures, source failures, and deliberate refusals, carries the machine envelope documented above in structuredContent.

Responses are cached on disk and revalidated conditionally — an entry is stored only if the response carried an ETag or a Last-Modified, and a hit is a request that came back 304, so cached bytes are ones the source confirmed a moment ago. A cache hit still reports when its bytes were retrieved, with a cache_hit flag, rather than claiming a fresh read.

How much it helps depends entirely on whether upstream sends validators, and that varies a lot. Measured here against live sources, median of five runs each:

command cold warm
hc-source call leie.check_npi --param npi=… (15.5 MB OIG file) 2.78s 0.90s
hc-source doctor (23 canaries) ~6s ~6s (no reliable difference)

The LEIE file is served with a Last-Modified, so the warm run revalidates it in one round trip instead of re-downloading 15.5 MB. doctor gets no measurable benefit: its canaries are cheap by design, so the round trip already dominates what a cache could save. Every path under api.coverage.cms.gov/v1/ and the NPPES NPI Registry API sends neither an ETag nor a Last-Modified, so they are re-fetched every time by design — storing a body with no way to re-confirm it is exactly the shortcut this product exists not to take. hc-source cache info lists the endpoints in that state rather than leaving you to wonder why the entry count is low.

hc-source cache info|clear; HC_SOURCE_CACHE_DIR moves or disables it.

Adding your own source

Three homes, one contract: ship it in the package, publish it as a distribution advertising the sourcelock.adapters entry point, or drop a file in ./.sourcelock/adapters/ and switch that directory on with HC_SOURCE_LOCAL_ADAPTERS=1 (off by default: loading a file from it means executing it, so it is never on where a pull request could add one). A third-party adapter gets the same validation, the same PHI guard, and the same receipts — and cannot claim a built-in source id.

ADAPTER_GUIDE.md is the contract; examples/sourcelock-example-adapter/ is a working skeleton with four marked places to change.

More

  • ADAPTER_GUIDE.md — writing a source adapter
  • docs/ci.md — the GitHub Action, severity, and CI policy
  • RELEASING.md — publishing to PyPI
  • hc_source/guard.py — what the PHI guard detects, and what it does not

License

MIT. See LICENSE. Source data carries its own terms; each adapter records them in its SourceContract.license_notes.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sourcelock-0.2.0.tar.gz (7.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sourcelock-0.2.0-py3-none-any.whl (7.1 MB view details)

Uploaded Python 3

File details

Details for the file sourcelock-0.2.0.tar.gz.

File metadata

  • Download URL: sourcelock-0.2.0.tar.gz
  • Upload date:
  • Size: 7.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for sourcelock-0.2.0.tar.gz
Algorithm Hash digest
SHA256 ecb6a43371f1126969289bc9da4871137fde8d439744ff687428ccd27ee07691
MD5 3bd5201128289d74b433cc8b44ab9482
BLAKE2b-256 8ee675e566bcc33bb16ad7da53ca8e6da4108ebd884fcd4e59c650aa29f501a2

See more details on using hashes here.

Provenance

The following attestation bundles were made for sourcelock-0.2.0.tar.gz:

Publisher: release.yml on writtenonwater99/sourcelock

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sourcelock-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: sourcelock-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 7.1 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for sourcelock-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 719daf128595a135a64705001ead665379181c42d06a8a4a70451a1ff00cd45c
MD5 b8083471ba24147efe890d3991814dfe
BLAKE2b-256 35064cad42dd1ad61b82034c70d0d5a45cd9539c4cb88f80efa25f8779d5f6aa

See more details on using hashes here.

Provenance

The following attestation bundles were made for sourcelock-0.2.0-py3-none-any.whl:

Publisher: release.yml on writtenonwater99/sourcelock

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page