sourcelock 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hc_source/__init__.py +5 -0
- hc_source/adapters/__init__.py +500 -0
- hc_source/adapters/_demo.py +258 -0
- hc_source/adapters/_demo_fixture.json +25 -0
- hc_source/adapters/_leie_sample.csv +15 -0
- hc_source/adapters/codes.py +1232 -0
- hc_source/adapters/coverage.py +1569 -0
- hc_source/adapters/hcc.py +1450 -0
- hc_source/adapters/leie.py +1310 -0
- hc_source/adapters/provider.py +1159 -0
- hc_source/cache.py +664 -0
- hc_source/cli.py +959 -0
- hc_source/cli_manifest.py +207 -0
- hc_source/data/codes/hcpcs_2026q3.csv.gz +0 -0
- hc_source/data/codes/icd10cm_fy2026.csv.gz +0 -0
- hc_source/data/codes/icd10cm_fy2027.csv.gz +0 -0
- hc_source/data/codes/manifest.json +75 -0
- hc_source/data/codes/regenerate.py +291 -0
- hc_source/data/hcc/hcc_data.json.zlib +0 -0
- hc_source/doctor.py +472 -0
- hc_source/guard.py +877 -0
- hc_source/http.py +541 -0
- hc_source/interfaces.py +395 -0
- hc_source/lockfile.py +236 -0
- hc_source/manifest.py +422 -0
- hc_source/mcp_server.py +203 -0
- hc_source/npi.py +50 -0
- hc_source/receipts.py +74 -0
- hc_source/schemas.py +339 -0
- sourcelock-0.1.0.dist-info/METADATA +272 -0
- sourcelock-0.1.0.dist-info/RECORD +34 -0
- sourcelock-0.1.0.dist-info/WHEEL +4 -0
- sourcelock-0.1.0.dist-info/entry_points.txt +2 -0
- sourcelock-0.1.0.dist-info/licenses/LICENSE +21 -0
hc_source/receipts.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Receipt construction.
|
|
2
|
+
|
|
3
|
+
Adapters build receipts through :func:`build_receipt` so that provenance is
|
|
4
|
+
filled in from the fetch itself rather than retyped, and so that every receipt
|
|
5
|
+
carries the zero-PHI non-claim without the author remembering it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import date
|
|
11
|
+
from typing import Sequence
|
|
12
|
+
|
|
13
|
+
from .guard import PHI_NON_CLAIM
|
|
14
|
+
from .http import FetchResult
|
|
15
|
+
from .schemas import Receipt, SourceContract
|
|
16
|
+
|
|
17
|
+
__all__ = ["build_receipt"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_receipt(
|
|
21
|
+
*,
|
|
22
|
+
contract: SourceContract,
|
|
23
|
+
route: str,
|
|
24
|
+
fetch: FetchResult,
|
|
25
|
+
source_version: str,
|
|
26
|
+
transform_version: str,
|
|
27
|
+
non_claims: Sequence[str],
|
|
28
|
+
effective_from: date | None = None,
|
|
29
|
+
effective_to: date | None = None,
|
|
30
|
+
warnings: Sequence[str] | None = None,
|
|
31
|
+
) -> Receipt:
|
|
32
|
+
"""Assemble a receipt from a fetch result.
|
|
33
|
+
|
|
34
|
+
``non_claims`` is required and must be non-empty: state at least one thing
|
|
35
|
+
this result does not prove, in the language of your source. The zero-PHI
|
|
36
|
+
non-claim is prepended for you.
|
|
37
|
+
|
|
38
|
+
``retrieved_at`` comes from ``fetch``, not from the clock. This function used
|
|
39
|
+
to stamp ``utcnow()`` here, which meant every answer served out of an adapter's
|
|
40
|
+
process cache -- the LEIE snapshot, the coverage bulk crosswalk, the vendored
|
|
41
|
+
code files -- carried a receipt claiming bytes had just been read from CMS.
|
|
42
|
+
"""
|
|
43
|
+
domain_claims = [c for c in non_claims if c.strip() and c != PHI_NON_CLAIM]
|
|
44
|
+
if not domain_claims:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"{route}: build_receipt requires at least one domain non-claim, e.g. "
|
|
47
|
+
"'DOES_NOT_PROVE_COVERAGE: an active NPI does not mean the payer covers this service.'"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return Receipt(
|
|
51
|
+
source_id=contract.source_id,
|
|
52
|
+
route=route,
|
|
53
|
+
upstream_status=fetch.status,
|
|
54
|
+
retrieved_at=fetch.retrieved_at,
|
|
55
|
+
source_version=source_version,
|
|
56
|
+
effective_from=effective_from,
|
|
57
|
+
effective_to=effective_to,
|
|
58
|
+
raw_sha256=fetch.sha256,
|
|
59
|
+
transform_version=transform_version,
|
|
60
|
+
fallback_used=fetch.fallback_used,
|
|
61
|
+
fallback_name=fetch.fallback_name,
|
|
62
|
+
# Both come from the fetch for the same reason ``retrieved_at`` does: a
|
|
63
|
+
# cache that renewed either of these would be attesting to a read that
|
|
64
|
+
# did not happen. Which means an adapter re-answering from its OWN
|
|
65
|
+
# in-process cache has to say so at the fetch object -- pass
|
|
66
|
+
# ``hc_source.http.as_cache_hit(meta)``, not the original result. Two
|
|
67
|
+
# adapters used to pass the original, so their receipts carried a prose
|
|
68
|
+
# warning saying "served from the in-process cache" beside
|
|
69
|
+
# ``cache_hit: false``, and the field is what a machine reads.
|
|
70
|
+
cache_hit=fetch.cache_hit,
|
|
71
|
+
revalidated_at=fetch.revalidated_at,
|
|
72
|
+
warnings=list(warnings or []),
|
|
73
|
+
non_claims=[PHI_NON_CLAIM, *domain_claims],
|
|
74
|
+
)
|
hc_source/schemas.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Core schemas for SourceLock.
|
|
2
|
+
|
|
3
|
+
Three of these types are the public contract that route adapters and CI
|
|
4
|
+
consumers depend on:
|
|
5
|
+
|
|
6
|
+
* ``Receipt`` -- evidence attached to every tool result.
|
|
7
|
+
* ``CanaryResult`` -- one canary observation, as ``hc-source doctor`` reports it.
|
|
8
|
+
* ``SourceContract``-- the promises a source adapter makes about its upstream.
|
|
9
|
+
* ``Lockfile`` -- the pinned state written to ``source-lock.json``.
|
|
10
|
+
|
|
11
|
+
Everything here is strict: unknown fields are rejected so that a typo in an
|
|
12
|
+
adapter fails loudly at construction rather than silently disappearing from a
|
|
13
|
+
receipt.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from datetime import date, datetime, timezone
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from typing import Annotated, Any
|
|
22
|
+
|
|
23
|
+
from pydantic import (
|
|
24
|
+
BaseModel,
|
|
25
|
+
ConfigDict,
|
|
26
|
+
Field,
|
|
27
|
+
field_serializer,
|
|
28
|
+
field_validator,
|
|
29
|
+
model_validator,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"CanaryResult",
|
|
34
|
+
"CanarySeverity",
|
|
35
|
+
"CanaryStatus",
|
|
36
|
+
"ExpectedCanary",
|
|
37
|
+
"Lockfile",
|
|
38
|
+
"LOCKFILE_VERSION",
|
|
39
|
+
"Receipt",
|
|
40
|
+
"SourceContract",
|
|
41
|
+
"SourceLockEntry",
|
|
42
|
+
"utcnow",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
LOCKFILE_VERSION = 1
|
|
46
|
+
|
|
47
|
+
_SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9_]*$")
|
|
48
|
+
_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
49
|
+
|
|
50
|
+
Sha256 = Annotated[str, Field(pattern=r"^[0-9a-f]{64}$")]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def utcnow() -> datetime:
|
|
54
|
+
"""Current time as a timezone-aware UTC datetime."""
|
|
55
|
+
return datetime.now(timezone.utc)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _to_utc(value: datetime) -> datetime:
|
|
59
|
+
if value.tzinfo is None:
|
|
60
|
+
return value.replace(tzinfo=timezone.utc)
|
|
61
|
+
return value.astimezone(timezone.utc)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _Strict(BaseModel):
|
|
65
|
+
model_config = ConfigDict(extra="forbid", validate_assignment=True)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Receipt(_Strict):
|
|
69
|
+
"""Evidence for one tool call.
|
|
70
|
+
|
|
71
|
+
A receipt answers: which source, which route, what the upstream said, when,
|
|
72
|
+
which version of the source and of our transform, the hash of the raw bytes
|
|
73
|
+
we based the answer on, whether we fell back, and -- explicitly -- what the
|
|
74
|
+
result does NOT prove.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
source_id: str = Field(description="Adapter source id, e.g. 'nppes'.")
|
|
78
|
+
route: str = Field(description="Fully-qualified tool/route name, e.g. 'nppes.lookup_npi'.")
|
|
79
|
+
upstream_status: int | None = Field(
|
|
80
|
+
default=None, description="HTTP status of the upstream call, or None for local sources."
|
|
81
|
+
)
|
|
82
|
+
retrieved_at: datetime = Field(description="When the upstream bytes were retrieved (UTC).")
|
|
83
|
+
source_version: str = Field(description="Upstream release/version identifier.")
|
|
84
|
+
effective_from: date | None = Field(
|
|
85
|
+
default=None, description="First date this data is in force, per the source contract."
|
|
86
|
+
)
|
|
87
|
+
effective_to: date | None = Field(
|
|
88
|
+
default=None, description="Last date this data is in force; None means open-ended."
|
|
89
|
+
)
|
|
90
|
+
raw_sha256: Sha256 = Field(description="SHA-256 of the raw upstream bytes.")
|
|
91
|
+
transform_version: str = Field(
|
|
92
|
+
description="Version of the adapter transform that produced the result."
|
|
93
|
+
)
|
|
94
|
+
fallback_used: bool = Field(default=False, description="True if the fallback source was used.")
|
|
95
|
+
fallback_name: str | None = Field(
|
|
96
|
+
default=None, description="Name of the fallback used; required when fallback_used is True."
|
|
97
|
+
)
|
|
98
|
+
cache_hit: bool = Field(
|
|
99
|
+
default=False,
|
|
100
|
+
description=(
|
|
101
|
+
"True when the bytes behind this answer came out of a cache rather "
|
|
102
|
+
"than off the wire. `retrieved_at` still reports when they were "
|
|
103
|
+
"RETRIEVED -- a cache that renewed that timestamp would be inventing "
|
|
104
|
+
"a read that did not happen."
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
revalidated_at: datetime | None = Field(
|
|
108
|
+
default=None,
|
|
109
|
+
description=(
|
|
110
|
+
"When upstream last confirmed these bytes are current: the moment of "
|
|
111
|
+
"the 304 on a cache hit, the moment of the download otherwise. Kept "
|
|
112
|
+
"apart from retrieved_at because 'you still have the right bytes' and "
|
|
113
|
+
"'these bytes are new' are different claims."
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
warnings: list[str] = Field(default_factory=list)
|
|
117
|
+
non_claims: list[str] = Field(
|
|
118
|
+
default_factory=list,
|
|
119
|
+
description="Explicit statements of what this result does NOT prove.",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@field_validator("revalidated_at")
|
|
123
|
+
@classmethod
|
|
124
|
+
def _utc_optional(cls, v: datetime | None) -> datetime | None:
|
|
125
|
+
return _to_utc(v) if v is not None else None
|
|
126
|
+
|
|
127
|
+
@field_serializer("revalidated_at")
|
|
128
|
+
def _ser_optional_dt(self, v: datetime | None) -> str | None:
|
|
129
|
+
return _to_utc(v).isoformat() if v is not None else None
|
|
130
|
+
|
|
131
|
+
@field_validator("source_id")
|
|
132
|
+
@classmethod
|
|
133
|
+
def _slug(cls, v: str) -> str:
|
|
134
|
+
if not _SLUG_RE.match(v):
|
|
135
|
+
raise ValueError("source_id must be a lowercase slug ([a-z0-9_], not starting with _)")
|
|
136
|
+
return v
|
|
137
|
+
|
|
138
|
+
@field_validator("retrieved_at")
|
|
139
|
+
@classmethod
|
|
140
|
+
def _utc(cls, v: datetime) -> datetime:
|
|
141
|
+
return _to_utc(v)
|
|
142
|
+
|
|
143
|
+
@field_serializer("retrieved_at")
|
|
144
|
+
def _ser_dt(self, v: datetime) -> str:
|
|
145
|
+
return _to_utc(v).isoformat()
|
|
146
|
+
|
|
147
|
+
@model_validator(mode="after")
|
|
148
|
+
def _fallback_consistency(self) -> Receipt:
|
|
149
|
+
if self.fallback_used and not (self.fallback_name or "").strip():
|
|
150
|
+
raise ValueError("fallback_used=True requires fallback_name")
|
|
151
|
+
if not self.fallback_used and self.fallback_name:
|
|
152
|
+
raise ValueError("fallback_name set but fallback_used is False")
|
|
153
|
+
if self.effective_from and self.effective_to and self.effective_to < self.effective_from:
|
|
154
|
+
raise ValueError("effective_to precedes effective_from")
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class CanaryStatus(str, Enum):
|
|
159
|
+
OK = "ok"
|
|
160
|
+
DRIFT = "drift"
|
|
161
|
+
UNREACHABLE = "unreachable"
|
|
162
|
+
SCHEMA_CHANGED = "schema_changed"
|
|
163
|
+
#: Observed successfully but never compared, because nothing pinned it.
|
|
164
|
+
#: Deliberately not ``ok``: a summary that says "22 ok, 0 drift" when
|
|
165
|
+
#: nothing was compared is a green build that verified nothing.
|
|
166
|
+
UNPINNED = "unpinned"
|
|
167
|
+
#: Compared and matched, but the canary itself reported that what it read is
|
|
168
|
+
#: out of date -- a weekly snapshot that has not moved in a month, a
|
|
169
|
+
#: cross-check it had to skip. An `ok` computed from data the canary says is
|
|
170
|
+
#: stale is not assurance, so it does not get to be `ok`.
|
|
171
|
+
STALE = "stale"
|
|
172
|
+
#: The canary raised something that is not a transport failure. Upstream may
|
|
173
|
+
#: be perfectly healthy; SourceLock's own code is what broke. Kept apart from
|
|
174
|
+
#: `unreachable` so "CMS is down" and "our adapter has a bug" are not the
|
|
175
|
+
#: same line in a CI log -- they have completely different fixes.
|
|
176
|
+
ERROR = "error"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class CanarySeverity(str, Enum):
|
|
180
|
+
"""Whether a finding from this canary may fail somebody's build.
|
|
181
|
+
|
|
182
|
+
Status and severity are different questions, and conflating them is how a
|
|
183
|
+
check that watches an HTML page CMS restyles quarterly ends up blocking a
|
|
184
|
+
release. `drift` is `drift` either way -- it is reported, annotated, and
|
|
185
|
+
counted. Severity only decides whether it reaches the exit code.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
BLOCKING = "blocking"
|
|
189
|
+
ADVISORY = "advisory"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class CanaryResult(_Strict):
|
|
193
|
+
"""The outcome of one canary, as reported by ``hc-source doctor``."""
|
|
194
|
+
|
|
195
|
+
canary_id: str
|
|
196
|
+
source_id: str
|
|
197
|
+
status: CanaryStatus
|
|
198
|
+
severity: CanarySeverity = Field(
|
|
199
|
+
default=CanarySeverity.BLOCKING,
|
|
200
|
+
description=(
|
|
201
|
+
"Whether this canary's finding counts toward the exit code. Advisory "
|
|
202
|
+
"findings are reported in full and excluded from the verdict."
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
attempts: int = Field(
|
|
206
|
+
default=1,
|
|
207
|
+
ge=1,
|
|
208
|
+
description=(
|
|
209
|
+
"How many times the observation was attempted. Greater than one means "
|
|
210
|
+
"a transient upstream failure was retried -- worth seeing on a canary "
|
|
211
|
+
"that reports ok, because 'ok on the third try' is a different fact "
|
|
212
|
+
"about the source than 'ok'."
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
observed: str | None = Field(default=None, description="What we saw upstream.")
|
|
216
|
+
expected: str | None = Field(default=None, description="What source-lock.json pinned.")
|
|
217
|
+
checked_at: datetime
|
|
218
|
+
remediation: str = Field(
|
|
219
|
+
default="",
|
|
220
|
+
description="Exact human-readable fix. Required for any non-ok status.",
|
|
221
|
+
)
|
|
222
|
+
warnings: list[str] = Field(default_factory=list)
|
|
223
|
+
fallback_used: bool = Field(
|
|
224
|
+
default=False,
|
|
225
|
+
description=(
|
|
226
|
+
"True when a mirror answered because the pinned authority could not be read. "
|
|
227
|
+
"An `ok` from a mirror is not the same claim as an `ok` from the authority."
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
fallback_name: str | None = Field(
|
|
231
|
+
default=None, description="Which mirror answered, when fallback_used is True."
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
@field_validator("checked_at")
|
|
235
|
+
@classmethod
|
|
236
|
+
def _utc(cls, v: datetime) -> datetime:
|
|
237
|
+
return _to_utc(v)
|
|
238
|
+
|
|
239
|
+
@field_serializer("checked_at")
|
|
240
|
+
def _ser_dt(self, v: datetime) -> str:
|
|
241
|
+
return _to_utc(v).isoformat()
|
|
242
|
+
|
|
243
|
+
@model_validator(mode="after")
|
|
244
|
+
def _remediation_required(self) -> CanaryResult:
|
|
245
|
+
if self.status is not CanaryStatus.OK and not self.remediation.strip():
|
|
246
|
+
raise ValueError(f"status={self.status.value} requires non-empty remediation text")
|
|
247
|
+
return self
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def ok(self) -> bool:
|
|
251
|
+
return self.status is CanaryStatus.OK
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class SourceContract(_Strict):
|
|
255
|
+
"""The unit of value: what a source promises and how to read its dates."""
|
|
256
|
+
|
|
257
|
+
source_id: str
|
|
258
|
+
authority_url: str = Field(description="The one canonical, citable upstream URL.")
|
|
259
|
+
fallback_url: str | None = Field(
|
|
260
|
+
default=None, description="Mirror used only when the authority is unreachable."
|
|
261
|
+
)
|
|
262
|
+
license_notes: str = Field(description="Redistribution/attribution terms in plain language.")
|
|
263
|
+
cadence: str = Field(description="How often upstream publishes, e.g. 'quarterly'.")
|
|
264
|
+
effective_date_semantics: str = Field(
|
|
265
|
+
description="What effective_from/effective_to mean for THIS source."
|
|
266
|
+
)
|
|
267
|
+
invariants: list[str] = Field(
|
|
268
|
+
default_factory=list,
|
|
269
|
+
description="Statements that must hold upstream; canaries should test these.",
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
@field_validator("source_id")
|
|
273
|
+
@classmethod
|
|
274
|
+
def _slug(cls, v: str) -> str:
|
|
275
|
+
if not _SLUG_RE.match(v):
|
|
276
|
+
raise ValueError("source_id must be a lowercase slug ([a-z0-9_], not starting with _)")
|
|
277
|
+
return v
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class ExpectedCanary(_Strict):
|
|
281
|
+
"""The pinned expectation for one canary."""
|
|
282
|
+
|
|
283
|
+
value: str
|
|
284
|
+
schema_hash: str | None = None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class SourceLockEntry(_Strict):
|
|
288
|
+
"""Pinned state for one source inside ``source-lock.json``."""
|
|
289
|
+
|
|
290
|
+
source_id: str
|
|
291
|
+
pinned_version: str
|
|
292
|
+
release_id: str | None = None
|
|
293
|
+
schema_hash: str | None = None
|
|
294
|
+
checked_at: datetime
|
|
295
|
+
expected_canaries: dict[str, ExpectedCanary] = Field(default_factory=dict)
|
|
296
|
+
|
|
297
|
+
@field_validator("checked_at")
|
|
298
|
+
@classmethod
|
|
299
|
+
def _utc(cls, v: datetime) -> datetime:
|
|
300
|
+
return _to_utc(v)
|
|
301
|
+
|
|
302
|
+
@field_serializer("checked_at")
|
|
303
|
+
def _ser_dt(self, v: datetime) -> str:
|
|
304
|
+
return _to_utc(v).isoformat()
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class Lockfile(_Strict):
|
|
308
|
+
"""``source-lock.json``: what CI compares today's upstream against."""
|
|
309
|
+
|
|
310
|
+
lockfile_version: int = LOCKFILE_VERSION
|
|
311
|
+
generated_at: datetime
|
|
312
|
+
sources: dict[str, SourceLockEntry] = Field(default_factory=dict)
|
|
313
|
+
|
|
314
|
+
@field_validator("generated_at")
|
|
315
|
+
@classmethod
|
|
316
|
+
def _utc(cls, v: datetime) -> datetime:
|
|
317
|
+
return _to_utc(v)
|
|
318
|
+
|
|
319
|
+
@field_serializer("generated_at")
|
|
320
|
+
def _ser_dt(self, v: datetime) -> str:
|
|
321
|
+
return _to_utc(v).isoformat()
|
|
322
|
+
|
|
323
|
+
@model_validator(mode="after")
|
|
324
|
+
def _keys_match(self) -> Lockfile:
|
|
325
|
+
for key, entry in self.sources.items():
|
|
326
|
+
if key != entry.source_id:
|
|
327
|
+
raise ValueError(f"lockfile key {key!r} does not match source_id {entry.source_id!r}")
|
|
328
|
+
return self
|
|
329
|
+
|
|
330
|
+
def expected_canary_ids(self) -> set[str]:
|
|
331
|
+
return {cid for entry in self.sources.values() for cid in entry.expected_canaries}
|
|
332
|
+
|
|
333
|
+
def expectation(self, source_id: str, canary_id: str) -> ExpectedCanary | None:
|
|
334
|
+
entry = self.sources.get(source_id)
|
|
335
|
+
return entry.expected_canaries.get(canary_id) if entry else None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def is_sha256(value: Any) -> bool:
|
|
339
|
+
return isinstance(value, str) and bool(_SHA256_RE.match(value))
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sourcelock
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zero-PHI source-assurance CLI for healthcare's public data inputs
|
|
5
|
+
Author: SourceLock
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 SourceLock
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Python: >=3.11
|
|
29
|
+
Requires-Dist: cryptography>=42
|
|
30
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
31
|
+
Requires-Dist: mcp==2.0.0
|
|
32
|
+
Requires-Dist: pydantic<3.0,>=2.7
|
|
33
|
+
Requires-Dist: rich>=13.0
|
|
34
|
+
Requires-Dist: typer>=0.12
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pyyaml>=6.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# SourceLock
|
|
42
|
+
|
|
43
|
+
**Every answer comes with a receipt, and your build fails when the source moves.**
|
|
44
|
+
|
|
45
|
+
SourceLock reads the public data healthcare software runs on — CMS coverage
|
|
46
|
+
policy, NPPES and PECOS, ICD-10-CM and HCPCS releases, CMS-HCC risk models, the
|
|
47
|
+
OIG LEIE — and returns, with every answer, the release it came from, the SHA-256
|
|
48
|
+
of the raw bytes behind it, when those bytes were retrieved, and what the answer
|
|
49
|
+
does not prove. `hc-source lock init` pins those sources in a `source-lock.json`;
|
|
50
|
+
`hc-source doctor` verifies that pin and fails CI the day one of them changes
|
|
51
|
+
underneath you.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
git clone https://github.com/writtenonwater99/sourcelock
|
|
57
|
+
cd sourcelock
|
|
58
|
+
pipx install .
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Python 3.11 or newer. There is no PyPI release yet, so `pip install sourcelock`
|
|
62
|
+
does not work and this file does not pretend it does — see
|
|
63
|
+
[RELEASING.md](RELEASING.md) for what publishing takes.
|
|
64
|
+
|
|
65
|
+
## See it work, offline, right now
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
hc-source valid-on E11.9 2026-07-01
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
No network, no credentials, no lockfile. The release tables are vendored, so
|
|
72
|
+
that command answers in under a second on a plane:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"code": "E11.9",
|
|
77
|
+
"code_nodot": "E119",
|
|
78
|
+
"code_system": "ICD-10-CM",
|
|
79
|
+
"date": "2026-07-01",
|
|
80
|
+
"valid": true,
|
|
81
|
+
"description": "Type 2 diabetes mellitus without complications",
|
|
82
|
+
"release": "fy2026-april",
|
|
83
|
+
"release_effective_from": "2026-04-01",
|
|
84
|
+
"release_effective_to": "2026-09-30"
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
followed by the receipt — source, route, release, effective window, retrieval
|
|
89
|
+
time, upstream status, the hash of the bytes the answer was derived from, the
|
|
90
|
+
transform version, and whether a mirror answered — and then the part most tools
|
|
91
|
+
leave out. Four non-claims, printed in full; the first line of each:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
does not prove:
|
|
95
|
+
- PHI_NOT_EXPECTED: this tool accepts only public typed parameters.
|
|
96
|
+
- DOES_NOT_INCLUDE_CPT_DESCRIPTORS: no HCPCS Level I (CPT) record and no ADA
|
|
97
|
+
- DOES_NOT_PROVE_COVERAGE: a code being valid for a date of service says
|
|
98
|
+
- DOES_NOT_PROVE_CURRENCY: answers come from the release train vendored at
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The real output does not wrap and does not trail off: the CPT one runs to a
|
|
102
|
+
paragraph naming all seventeen CMS descriptions that quote a CPT number, because
|
|
103
|
+
that is what an AMA licence question actually needs. These four are what
|
|
104
|
+
`codes.valid_on` claims it cannot tell you — every one of them, not a
|
|
105
|
+
representative sample, and nothing this product does not actually print.
|
|
106
|
+
|
|
107
|
+
## What it is for
|
|
108
|
+
|
|
109
|
+
If you are building AI or automation over revenue-cycle data, you have run into
|
|
110
|
+
some version of this:
|
|
111
|
+
|
|
112
|
+
- **A model cited a code set and nobody can say which release.** Every answer
|
|
113
|
+
here carries `source_version`, `effective_from`/`effective_to`, and
|
|
114
|
+
`raw_sha256`. That is an audit trail, not a log line.
|
|
115
|
+
- **A pipeline broke because CMS renamed something.** NPPES retired its V1 bulk
|
|
116
|
+
filenames on 2026-03-03; jobs that still generated them got silent 404s.
|
|
117
|
+
`hc-source lock init` pins 23 cheap checks across six sources and `hc-source
|
|
118
|
+
doctor` fails the build the day one moves — with the fix, not a stack trace.
|
|
119
|
+
- **An agent will confidently answer from a stale cache.** Receipts distinguish
|
|
120
|
+
when bytes were *retrieved* from when upstream last *confirmed* them, a canary
|
|
121
|
+
that read out-of-date data is not allowed to report `ok`, and a source that
|
|
122
|
+
could not be read is never reported as unchanged.
|
|
123
|
+
- **Your reference data is not all public.** Sources are plugins. Your licensed
|
|
124
|
+
AMA CPT tables can be a first-class adapter without forking anything.
|
|
125
|
+
- **PHI must not end up in a tool call.** Parameters are public and typed, no
|
|
126
|
+
receipt or log carries a payload, and a guard refuses input shaped like a
|
|
127
|
+
patient record. It is a structural best-effort refusal, not a HIPAA control:
|
|
128
|
+
`hc_source/guard.py` states exactly what it detects and, just as plainly,
|
|
129
|
+
which evasion classes it does not close.
|
|
130
|
+
|
|
131
|
+
## The six sources
|
|
132
|
+
|
|
133
|
+
| source | what it answers |
|
|
134
|
+
|------------|-----------------------------------------------------------------------------|
|
|
135
|
+
| `codes` | ICD-10-CM / HCPCS Level II validity on a date of service, release trains |
|
|
136
|
+
| `hcc` | CMS-HCC V24/V28 mapping, hierarchies, community continuing-enrollee scoring |
|
|
137
|
+
| `provider` | NPI Registry lookups, NPPES bulk-file cadence, PECOS enrollment snapshot |
|
|
138
|
+
| `coverage` | CMS coverage policy: NCDs, LCDs and articles, MCD weekly snapshot identity |
|
|
139
|
+
| `leie` | OIG LEIE exclusion checks by NPI, candidate search, monthly refresh status |
|
|
140
|
+
| `demo` | packaged reference adapter used in tests and examples |
|
|
141
|
+
|
|
142
|
+
`codes` and `hcc` answer offline from vendored release data. The rest read live
|
|
143
|
+
CMS and OIG endpoints.
|
|
144
|
+
|
|
145
|
+
## Everyday commands
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
hc-source valid-on E11.9 2026-07-01 # offline
|
|
149
|
+
hc-source hcc score --dx E11.9,I50.9 --model v28 --year 2026 --age 72 --sex F
|
|
150
|
+
hc-source lookup-npi 1003000126 # NPPES
|
|
151
|
+
hc-source check-npi 1003000126 # OIG LEIE screen
|
|
152
|
+
hc-source tools # every route, with its parameters
|
|
153
|
+
hc-source call coverage.lookup_ncd --param section=30.3 # the general form
|
|
154
|
+
hc-source mcp # serve it all to an agent over MCP
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`call` reaches every route, including ones provided by adapters you installed;
|
|
158
|
+
the named commands are shorthand for the four questions people arrive with.
|
|
159
|
+
|
|
160
|
+
## Pin your sources and fail the build when they move
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
hc-source lock init # observe all six sources, write source-lock.json
|
|
164
|
+
hc-source doctor # re-check upstream against the lockfile
|
|
165
|
+
hc-source init --ci # scaffold the GitHub Actions workflow
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Commit `source-lock.json` and review its diffs like any other lockfile. A green
|
|
169
|
+
run ends with:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
23 ok, 0 drift, 0 unreachable, 0 schema_changed, 0 unpinned, 0 stale, 0 error
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Exit codes are the contract: `0` everything matched, `1` drift or schema change,
|
|
176
|
+
`2` a source was unreachable or an adapter broke, `3` a canary was observed but
|
|
177
|
+
nothing pinned it, `4` everything matched but something reported that what it
|
|
178
|
+
read is out of date. `3` and `4` exist because a missing `source-lock.json` used
|
|
179
|
+
to report `23 ok, 0 drift` at exit 0 — a build that verified nothing, reporting
|
|
180
|
+
green. Full CI setup, per-canary severity, and the `on-unreachable` /
|
|
181
|
+
`on-stale` policies: [docs/ci.md](docs/ci.md).
|
|
182
|
+
|
|
183
|
+
## Watch it catch drift, in 90 seconds
|
|
184
|
+
|
|
185
|
+
You do not have to wait for CMS to change something. NPPES really did retire its
|
|
186
|
+
V1 bulk-file names on 2026-03-03. The repo carries a recording of what the
|
|
187
|
+
listing page would look like if that class of change happened again:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
hc-source lock init
|
|
191
|
+
HC_SOURCE_PROVIDER_NPPES_FILES_URL="file://$PWD/tests/fixtures/provider/npi_files_v1_only.html" \
|
|
192
|
+
hc-source doctor --source provider
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Doctor exits `1`, marks `provider.nppes_v2_files` as `drift`, and prints:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
provider.nppes_v2_files [drift]: NPPES retired the V1 bulk files on 2026-03-03; only
|
|
199
|
+
*_V2.zip names are published now (V2 extends the First Name and Legal Business Name
|
|
200
|
+
field lengths). The listing this canary just read carries a V1-style name with no _V2
|
|
201
|
+
suffix, which means the page has regressed, a stale mirror is being served, or
|
|
202
|
+
something upstream renamed the grammar again. Fix: open
|
|
203
|
+
https://download.cms.gov/nppes/NPI_Files.html, read the 'Important Information' block
|
|
204
|
+
(where CMS announced the V1 retirement), confirm the current version suffix, make sure
|
|
205
|
+
nothing in your pipeline still generates V1 filenames
|
|
206
|
+
(NPPES_Data_Dissemination_<Month>_<YYYY>.zip silently 404s), then re-pin with
|
|
207
|
+
`hc-source lock init`.
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Run it again without the override and it goes back to green. That is the whole
|
|
211
|
+
product: pin what your build depends on, and get told — with an instruction, not
|
|
212
|
+
a stack trace — the moment upstream moves.
|
|
213
|
+
|
|
214
|
+
## For agents
|
|
215
|
+
|
|
216
|
+
`hc-source mcp` serves every discovered tool over MCP stdio: one MCP tool per
|
|
217
|
+
route, the same typed parameters, the same receipts, the same PHI refusals as
|
|
218
|
+
the CLI. It refuses to start if any adapter failed to import, because an agent
|
|
219
|
+
that sees a short `tools/list` reads it as the whole product.
|
|
220
|
+
|
|
221
|
+
Responses are cached on disk and revalidated conditionally — an entry is stored
|
|
222
|
+
only if the response carried an `ETag` or a `Last-Modified`, and a hit is a
|
|
223
|
+
request that came back `304`, so cached bytes are ones the source confirmed a
|
|
224
|
+
moment ago. A cache hit still reports when its bytes were *retrieved*, with a
|
|
225
|
+
`cache_hit` flag, rather than claiming a fresh read.
|
|
226
|
+
|
|
227
|
+
**How much it helps depends entirely on whether upstream sends validators, and
|
|
228
|
+
that varies a lot.** Measured here against live sources, median of five runs
|
|
229
|
+
each:
|
|
230
|
+
|
|
231
|
+
| command | cold | warm |
|
|
232
|
+
| --- | --- | --- |
|
|
233
|
+
| `hc-source call leie.check_npi --param npi=…` (15.5 MB OIG file) | 2.78s | **0.90s** |
|
|
234
|
+
| `hc-source doctor` (23 canaries) | ~6s | ~6s (no reliable difference) |
|
|
235
|
+
|
|
236
|
+
The LEIE file is served with a `Last-Modified`, so the warm run revalidates it
|
|
237
|
+
in one round trip instead of re-downloading 15.5 MB. `doctor` gets no measurable
|
|
238
|
+
benefit: its canaries are cheap by design, so the round trip already dominates
|
|
239
|
+
what a cache could save. Every path under `api.coverage.cms.gov/v1/` and the
|
|
240
|
+
NPPES NPI Registry API sends **neither** an `ETag` nor a `Last-Modified`, so
|
|
241
|
+
they are re-fetched every time by design — storing a body with no way to
|
|
242
|
+
re-confirm it is exactly the shortcut this product exists not to take.
|
|
243
|
+
`hc-source cache info` lists the endpoints in that state rather than leaving you
|
|
244
|
+
to wonder why the entry count is low.
|
|
245
|
+
|
|
246
|
+
`hc-source cache info|clear`; `HC_SOURCE_CACHE_DIR` moves or disables it.
|
|
247
|
+
|
|
248
|
+
## Adding your own source
|
|
249
|
+
|
|
250
|
+
Three homes, one contract: ship it in the package, publish it as a distribution
|
|
251
|
+
advertising the `sourcelock.adapters` entry point, or drop a file in
|
|
252
|
+
`./.sourcelock/adapters/` and switch that directory on with
|
|
253
|
+
`HC_SOURCE_LOCAL_ADAPTERS=1` (off by default: loading a file from it means
|
|
254
|
+
executing it, so it is never on where a pull request could add one). A
|
|
255
|
+
third-party adapter gets the same validation, the same PHI guard, and the same
|
|
256
|
+
receipts — and cannot claim a built-in source id.
|
|
257
|
+
|
|
258
|
+
[ADAPTER_GUIDE.md](ADAPTER_GUIDE.md) is the contract;
|
|
259
|
+
[examples/sourcelock-example-adapter/](examples/sourcelock-example-adapter/) is
|
|
260
|
+
a working skeleton with four marked places to change.
|
|
261
|
+
|
|
262
|
+
## More
|
|
263
|
+
|
|
264
|
+
- [ADAPTER_GUIDE.md](ADAPTER_GUIDE.md) — writing a source adapter
|
|
265
|
+
- [docs/ci.md](docs/ci.md) — the GitHub Action, severity, and CI policy
|
|
266
|
+
- [RELEASING.md](RELEASING.md) — publishing to PyPI
|
|
267
|
+
- `hc_source/guard.py` — what the PHI guard detects, and what it does not
|
|
268
|
+
|
|
269
|
+
## License
|
|
270
|
+
|
|
271
|
+
MIT. See [LICENSE](LICENSE). Source data carries its own terms; each adapter
|
|
272
|
+
records them in its `SourceContract.license_notes`.
|