sourcelock 0.1.0__tar.gz → 0.2.0__tar.gz

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.
Files changed (158) hide show
  1. {sourcelock-0.1.0 → sourcelock-0.2.0}/ADAPTER_GUIDE.md +11 -2
  2. {sourcelock-0.1.0 → sourcelock-0.2.0}/PKG-INFO +66 -9
  3. {sourcelock-0.1.0 → sourcelock-0.2.0}/README.md +65 -8
  4. {sourcelock-0.1.0 → sourcelock-0.2.0}/RELEASING.md +10 -0
  5. {sourcelock-0.1.0 → sourcelock-0.2.0}/action/action.yml +2 -0
  6. {sourcelock-0.1.0 → sourcelock-0.2.0}/action/run_doctor.py +4 -0
  7. {sourcelock-0.1.0 → sourcelock-0.2.0}/docs/ci.md +7 -1
  8. {sourcelock-0.1.0 → sourcelock-0.2.0}/examples/sourcelock-example-adapter/README.md +4 -0
  9. {sourcelock-0.1.0 → sourcelock-0.2.0}/examples/sourcelock-example-adapter/sourcelock_example_adapter.py +3 -3
  10. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/__init__.py +1 -1
  11. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/adapters/_demo.py +2 -2
  12. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/adapters/codes.py +127 -53
  13. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/adapters/coverage.py +93 -10
  14. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/adapters/hcc.py +117 -8
  15. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/adapters/leie.py +21 -6
  16. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/adapters/provider.py +15 -9
  17. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/cli.py +183 -35
  18. sourcelock-0.2.0/hc_source/data/codes/hcpcs_2023q1.csv.gz +0 -0
  19. sourcelock-0.2.0/hc_source/data/codes/hcpcs_2023q2.csv.gz +0 -0
  20. sourcelock-0.2.0/hc_source/data/codes/hcpcs_2023q3.csv.gz +0 -0
  21. sourcelock-0.2.0/hc_source/data/codes/hcpcs_2023q4.csv.gz +0 -0
  22. sourcelock-0.2.0/hc_source/data/codes/hcpcs_2024q1.csv.gz +0 -0
  23. sourcelock-0.2.0/hc_source/data/codes/hcpcs_2024q2.csv.gz +0 -0
  24. sourcelock-0.2.0/hc_source/data/codes/hcpcs_2024q3.csv.gz +0 -0
  25. sourcelock-0.2.0/hc_source/data/codes/hcpcs_2024q4.csv.gz +0 -0
  26. sourcelock-0.2.0/hc_source/data/codes/hcpcs_2025q1.csv.gz +0 -0
  27. sourcelock-0.2.0/hc_source/data/codes/hcpcs_2025q2.csv.gz +0 -0
  28. sourcelock-0.2.0/hc_source/data/codes/hcpcs_2025q3.csv.gz +0 -0
  29. sourcelock-0.2.0/hc_source/data/codes/hcpcs_2025q4.csv.gz +0 -0
  30. sourcelock-0.2.0/hc_source/data/codes/hcpcs_2026q1.csv.gz +0 -0
  31. sourcelock-0.2.0/hc_source/data/codes/hcpcs_2026q2.csv.gz +0 -0
  32. sourcelock-0.2.0/hc_source/data/codes/icd10cm_fy2024.csv.gz +0 -0
  33. sourcelock-0.2.0/hc_source/data/codes/icd10cm_fy2025.csv.gz +0 -0
  34. sourcelock-0.2.0/hc_source/data/codes/manifest.json +327 -0
  35. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/data/codes/regenerate.py +161 -9
  36. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/interfaces.py +40 -2
  37. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/mcp_server.py +46 -16
  38. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/receipts.py +5 -1
  39. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/schemas.py +21 -0
  40. {sourcelock-0.1.0 → sourcelock-0.2.0}/pyproject.toml +1 -1
  41. sourcelock-0.2.0/qa/FIX-WAVE-10-BRIEF.md +26 -0
  42. sourcelock-0.2.0/qa/FIX-WAVE-7-BRIEF.md +45 -0
  43. sourcelock-0.2.0/qa/FIX-WAVE-8-BRIEF.md +45 -0
  44. sourcelock-0.2.0/qa/FIX-WAVE-9-BRIEF.md +21 -0
  45. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/adapters/test_codes.py +181 -9
  46. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/adapters/test_coverage.py +120 -3
  47. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/adapters/test_hcc.py +71 -0
  48. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/adapters/test_leie.py +13 -6
  49. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/adapters/test_provider.py +8 -6
  50. sourcelock-0.2.0/tests/test_answer_contract.py +239 -0
  51. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_cli.py +78 -1
  52. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_codes_cpt_claim.py +11 -11
  53. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_demo_adapter.py +1 -1
  54. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_plugin_discovery.py +1 -1
  55. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_schemas.py +29 -0
  56. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_shortcuts.py +29 -1
  57. sourcelock-0.1.0/hc_source/data/codes/manifest.json +0 -75
  58. {sourcelock-0.1.0 → sourcelock-0.2.0}/.github/workflows/ci.yml +0 -0
  59. {sourcelock-0.1.0 → sourcelock-0.2.0}/.github/workflows/release.yml +0 -0
  60. {sourcelock-0.1.0 → sourcelock-0.2.0}/.gitignore +0 -0
  61. {sourcelock-0.1.0 → sourcelock-0.2.0}/LICENSE +0 -0
  62. {sourcelock-0.1.0 → sourcelock-0.2.0}/MORNING-REPORT.md +0 -0
  63. {sourcelock-0.1.0 → sourcelock-0.2.0}/action/dry-run.sh +0 -0
  64. {sourcelock-0.1.0 → sourcelock-0.2.0}/examples/sourcelock-example-adapter/pyproject.toml +0 -0
  65. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/adapters/__init__.py +0 -0
  66. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/adapters/_demo_fixture.json +0 -0
  67. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/adapters/_leie_sample.csv +0 -0
  68. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/cache.py +0 -0
  69. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/cli_manifest.py +0 -0
  70. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/data/codes/hcpcs_2026q3.csv.gz +0 -0
  71. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/data/codes/icd10cm_fy2026.csv.gz +0 -0
  72. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/data/codes/icd10cm_fy2027.csv.gz +0 -0
  73. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/data/hcc/hcc_data.json.zlib +0 -0
  74. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/doctor.py +0 -0
  75. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/guard.py +0 -0
  76. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/http.py +0 -0
  77. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/lockfile.py +0 -0
  78. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/manifest.py +0 -0
  79. {sourcelock-0.1.0 → sourcelock-0.2.0}/hc_source/npi.py +0 -0
  80. {sourcelock-0.1.0 → sourcelock-0.2.0}/qa/FIX-WAVE-2-BRIEF.md +0 -0
  81. {sourcelock-0.1.0 → sourcelock-0.2.0}/qa/breaker-round1.md +0 -0
  82. {sourcelock-0.1.0 → sourcelock-0.2.0}/scripts/check_wheel.py +0 -0
  83. {sourcelock-0.1.0 → sourcelock-0.2.0}/scripts/wheel_check.sh +0 -0
  84. {sourcelock-0.1.0 → sourcelock-0.2.0}/source-lock.json +0 -0
  85. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/adapters/README.md +0 -0
  86. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/conftest.py +0 -0
  87. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/README.md +0 -0
  88. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/codes/cdc_root_listing.html +0 -0
  89. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/codes/cms_landing_trimmed.html +0 -0
  90. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/coverage/README.md +0 -0
  91. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/coverage/articles_report_trimmed.json +0 -0
  92. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/coverage/contract_types.json +0 -0
  93. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/coverage/error_400_invalid_ncdid.json +0 -0
  94. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/coverage/error_401_lcd_no_token.json +0 -0
  95. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/coverage/final_lcds_report_trimmed.json +0 -0
  96. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/coverage/lcd_related_documents_33818.json +0 -0
  97. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/coverage/lcd_related_documents_head.csv +0 -0
  98. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/coverage/local_data_schedule.json +0 -0
  99. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/coverage/ncd_detail_11.json +0 -0
  100. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/coverage/ncd_report_trimmed.json +0 -0
  101. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/coverage/ncd_zip_range_headers.json +0 -0
  102. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/coverage/openapi_spec_trimmed.json +0 -0
  103. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/coverage/states.json +0 -0
  104. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/coverage/whats_new_local_trimmed.json +0 -0
  105. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/coverage/whats_new_national.json +0 -0
  106. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/hcc/VERIFIED_ADDENDUM.md +0 -0
  107. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/hcc/derive_report.txt +0 -0
  108. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/hcc/head_C2419P1M.csv +0 -0
  109. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/hcc/head_C2824T2N.csv +0 -0
  110. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/hcc/head_F2425P1M.txt +0 -0
  111. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/hcc/head_F2826T1N.txt +0 -0
  112. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/hcc/mirror_py2026.bin +0 -0
  113. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/hcc/mirror_sw2025.bin +0 -0
  114. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/hcc/mirror_sw2026.bin +0 -0
  115. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/hcc/mirror_sw2026_republished.bin +0 -0
  116. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/hcc/regenerate_vendored_data.py +0 -0
  117. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/leie/provenance.json +0 -0
  118. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/leie/updated_drift.csv +0 -0
  119. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/leie/updated_ok.csv +0 -0
  120. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/leie/updated_schema_changed.csv +0 -0
  121. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/leie/updated_unquoted.csv +0 -0
  122. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/leie/waivers_page.html +0 -0
  123. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/provider/PROVENANCE.md +0 -0
  124. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/provider/lookup_npi_missing.json +0 -0
  125. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/provider/lookup_npi_ok.json +0 -0
  126. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/provider/npi_api_error_noversion.json +0 -0
  127. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/provider/npi_files.html +0 -0
  128. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/provider/npi_files_v1_only.html +0 -0
  129. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/provider/pecos_dataset_resources.json +0 -0
  130. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/provider/pecos_filter_miss.json +0 -0
  131. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/provider/pecos_filter_ok.json +0 -0
  132. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/provider/pecos_size1.json +0 -0
  133. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/fixtures/provider/pecos_stats.json +0 -0
  134. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_action.py +0 -0
  135. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_adapter_contract.py +0 -0
  136. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_cache.py +0 -0
  137. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_canary_severity.py +0 -0
  138. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_cli_failure_modes.py +0 -0
  139. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_doctor.py +0 -0
  140. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_doctor_classification.py +0 -0
  141. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_doctor_honesty.py +0 -0
  142. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_guard.py +0 -0
  143. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_guard_bypass.py +0 -0
  144. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_guard_must_pass.py +0 -0
  145. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_http.py +0 -0
  146. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_http_limits.py +0 -0
  147. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_lock_init_safety.py +0 -0
  148. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_manifest.py +0 -0
  149. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_mcp_server.py +0 -0
  150. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_no_echo_residue.py +0 -0
  151. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_no_input_echo.py +0 -0
  152. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_npi.py +0 -0
  153. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_offline_doctor.py +0 -0
  154. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_packaging.py +0 -0
  155. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_receipt_provenance.py +0 -0
  156. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_release_workflow.py +0 -0
  157. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_stale_canaries.py +0 -0
  158. {sourcelock-0.1.0 → sourcelock-0.2.0}/tests/test_strict_discovery.py +0 -0
@@ -161,7 +161,14 @@ Rules:
161
161
  * Handlers are synchronous and receive the **validated** model.
162
162
  * Never call `handler` directly. `ToolSpec.invoke()` is where the zero-PHI guard
163
163
  runs; the CLI and the MCP server both go through it.
164
- * A miss is not an error. Return `data=None` with a warning on the receipt.
164
+ * A miss is an answer, not an error. Return structured non-null data such as
165
+ `{"found": false}` or `{"matches": [], "match_count": 0}` with an
166
+ `answered` receipt and a warning. The framework rejects an answered
167
+ `ToolResult` whose data is null.
168
+ * A deliberate fail-closed refusal is different: return `data=None` and build
169
+ the receipt with `status=ReceiptStatus.REFUSED` plus a stable uppercase
170
+ `refusal_code`. CLI route calls exit 3 and MCP marks the structured result as
171
+ an error. The framework rejects a refused result whose data is non-null.
165
172
  Raise only when the source itself failed.
166
173
 
167
174
  ### Receipts
@@ -395,7 +402,9 @@ to duplicate it. Write tests for the things only you know:
395
402
 
396
403
  * a successful call, asserting on `data` **and** on the receipt's
397
404
  `source_version` and `effective_from`;
398
- * a miss, asserting `data is None` and a warning rather than an exception;
405
+ * a miss, asserting structured negative data and a warning rather than an exception;
406
+ * a deliberate refusal, asserting `data is None`, `receipt.status == "refused"`,
407
+ and the exact stable `receipt.refusal_code`;
399
408
  * fallback: authority down, mirror up, `receipt.fallback_used is True`;
400
409
  * both sources down: `SourceUnreachable`;
401
410
  * each canary's happy path, and its drift path from a mutated fixture;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcelock
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Zero-PHI source-assurance CLI for healthcare's public data inputs
5
5
  Author: SourceLock
6
6
  License: MIT License
@@ -53,14 +53,31 @@ underneath you.
53
53
  ## Install
54
54
 
55
55
  ```
56
- git clone https://github.com/writtenonwater99/sourcelock
57
- cd sourcelock
58
- pipx install .
56
+ pipx install sourcelock
59
57
  ```
60
58
 
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.
59
+ Python 3.11 or newer. `pip install sourcelock` works too, inside a virtualenv.
60
+ Either way you get one executable: `hc-source`.
61
+
62
+ ## Vendored date-of-service coverage
63
+
64
+ - **ICD-10-CM: 2023-10-01 through 2027-03-31.** Vendored releases are
65
+ `fy2024`, `fy2024-april`, `fy2025`, `fy2025-april`, `fy2026`,
66
+ `fy2026-april`, and `fy2027`. FY2027 is supported only through March 31,
67
+ 2027 because the April 2027 publication status is not yet settled.
68
+ - **HCPCS Level II: 2023-01-01 through 2026-09-30.** Every quarterly public-use
69
+ file from `2023q1` through `2026q3` is vendored.
70
+
71
+ CMS's [ICD-10 publication record](https://www.cms.gov/medicare/coding-billing/icd-10-codes)
72
+ defines a distinct FY2025 base window (October 1, 2024 through March 31, 2025)
73
+ and April-update window (April 1 through September 30, 2025). The CDC archive
74
+ retains both upstream ZIPs. Their code-description members are byte-identical,
75
+ so the manifest keeps two release/provenance entries while deduplicating the
76
+ derived table; FY2024's published April update is handled the same way.
77
+
78
+ Dates outside the windows above fail closed at exit `3` with
79
+ `RELEASE_NOT_VENDORED` (or `AMBIGUOUS_WINDOW` when an April update is not yet
80
+ settled). SourceLock never substitutes a different release's snapshot.
64
81
 
65
82
  ## See it work, offline, right now
66
83
 
@@ -157,6 +174,44 @@ hc-source mcp # serve it all to an age
157
174
  `call` reaches every route, including ones provided by adapters you installed;
158
175
  the named commands are shorthand for the four questions people arrive with.
159
176
 
177
+ ## Machine contract for tool calls
178
+
179
+ This is a breaking CLI contract change for the next release. `hc-source call`
180
+ and the four route shortcuts use these exit codes:
181
+
182
+ | exit | meaning | trigger |
183
+ | --- | --- | --- |
184
+ | `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. |
185
+ | `1` | invalid input | The tool name, parameter syntax, PHI guard, or typed parameter validation rejected the call. |
186
+ | `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. |
187
+ | `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`). |
188
+
189
+ With `--json`, every one of those paths emits the same top-level shape:
190
+
191
+ ```json
192
+ {
193
+ "data": {},
194
+ "receipt": {"status": "answered", "refusal_code": null},
195
+ "error": null
196
+ }
197
+ ```
198
+
199
+ Exactly one outcome channel is populated: answers carry non-null `data` and an
200
+ `answered` receipt; refusals carry `data: null` and a `refused` receipt;
201
+ validation/source/adapter failures carry `data: null`, `receipt: null`, and a
202
+ structured `error` (`INVALID_INPUT`, `SOURCE_UNREACHABLE`, or
203
+ `ADAPTER_FAILURE`). MCP returns that same envelope as `structuredContent` and
204
+ marks failures and deliberate refusals as MCP errors.
205
+
206
+ `hcc.score` has no diagnosis/date-of-service parameter, so it uses one explicit
207
+ validity anchor for every payment year: the vendored ICD-10-CM `fy2026-april`
208
+ release, effective 2026-04-01 through 2026-09-30. The answer payload and receipt
209
+ name that release and its hash. A mixed list still scores from its valid codes;
210
+ a valid-but-unmapped code keeps the existing named-unmapped behavior. If every
211
+ code is invalid, the route refuses with `NO_VALID_DIAGNOSES`. Use
212
+ `--allow-demographic-only` (or MCP/call parameter
213
+ `allow_demographic_only=true`) only to opt into a demographic-only partial RAF.
214
+
160
215
  ## Pin your sources and fail the build when they move
161
216
 
162
217
  ```
@@ -172,7 +227,7 @@ run ends with:
172
227
  23 ok, 0 drift, 0 unreachable, 0 schema_changed, 0 unpinned, 0 stale, 0 error
173
228
  ```
174
229
 
175
- Exit codes are the contract: `0` everything matched, `1` drift or schema change,
230
+ Doctor has a separate, unchanged 0–4 verdict contract: `0` everything matched, `1` drift or schema change,
176
231
  `2` a source was unreachable or an adapter broke, `3` a canary was observed but
177
232
  nothing pinned it, `4` everything matched but something reported that what it
178
233
  read is out of date. `3` and `4` exist because a missing `source-lock.json` used
@@ -216,7 +271,9 @@ a stack trace — the moment upstream moves.
216
271
  `hc-source mcp` serves every discovered tool over MCP stdio: one MCP tool per
217
272
  route, the same typed parameters, the same receipts, the same PHI refusals as
218
273
  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.
274
+ that sees a short `tools/list` reads it as the whole product. Every MCP result,
275
+ including validation failures, source failures, and deliberate refusals, carries
276
+ the machine envelope documented above in `structuredContent`.
220
277
 
221
278
  Responses are cached on disk and revalidated conditionally — an entry is stored
222
279
  only if the response carried an `ETag` or a `Last-Modified`, and a hit is a
@@ -13,14 +13,31 @@ underneath you.
13
13
  ## Install
14
14
 
15
15
  ```
16
- git clone https://github.com/writtenonwater99/sourcelock
17
- cd sourcelock
18
- pipx install .
16
+ pipx install sourcelock
19
17
  ```
20
18
 
21
- Python 3.11 or newer. There is no PyPI release yet, so `pip install sourcelock`
22
- does not work and this file does not pretend it does — see
23
- [RELEASING.md](RELEASING.md) for what publishing takes.
19
+ Python 3.11 or newer. `pip install sourcelock` works too, inside a virtualenv.
20
+ Either way you get one executable: `hc-source`.
21
+
22
+ ## Vendored date-of-service coverage
23
+
24
+ - **ICD-10-CM: 2023-10-01 through 2027-03-31.** Vendored releases are
25
+ `fy2024`, `fy2024-april`, `fy2025`, `fy2025-april`, `fy2026`,
26
+ `fy2026-april`, and `fy2027`. FY2027 is supported only through March 31,
27
+ 2027 because the April 2027 publication status is not yet settled.
28
+ - **HCPCS Level II: 2023-01-01 through 2026-09-30.** Every quarterly public-use
29
+ file from `2023q1` through `2026q3` is vendored.
30
+
31
+ CMS's [ICD-10 publication record](https://www.cms.gov/medicare/coding-billing/icd-10-codes)
32
+ defines a distinct FY2025 base window (October 1, 2024 through March 31, 2025)
33
+ and April-update window (April 1 through September 30, 2025). The CDC archive
34
+ retains both upstream ZIPs. Their code-description members are byte-identical,
35
+ so the manifest keeps two release/provenance entries while deduplicating the
36
+ derived table; FY2024's published April update is handled the same way.
37
+
38
+ Dates outside the windows above fail closed at exit `3` with
39
+ `RELEASE_NOT_VENDORED` (or `AMBIGUOUS_WINDOW` when an April update is not yet
40
+ settled). SourceLock never substitutes a different release's snapshot.
24
41
 
25
42
  ## See it work, offline, right now
26
43
 
@@ -117,6 +134,44 @@ hc-source mcp # serve it all to an age
117
134
  `call` reaches every route, including ones provided by adapters you installed;
118
135
  the named commands are shorthand for the four questions people arrive with.
119
136
 
137
+ ## Machine contract for tool calls
138
+
139
+ This is a breaking CLI contract change for the next release. `hc-source call`
140
+ and the four route shortcuts use these exit codes:
141
+
142
+ | exit | meaning | trigger |
143
+ | --- | --- | --- |
144
+ | `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. |
145
+ | `1` | invalid input | The tool name, parameter syntax, PHI guard, or typed parameter validation rejected the call. |
146
+ | `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. |
147
+ | `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`). |
148
+
149
+ With `--json`, every one of those paths emits the same top-level shape:
150
+
151
+ ```json
152
+ {
153
+ "data": {},
154
+ "receipt": {"status": "answered", "refusal_code": null},
155
+ "error": null
156
+ }
157
+ ```
158
+
159
+ Exactly one outcome channel is populated: answers carry non-null `data` and an
160
+ `answered` receipt; refusals carry `data: null` and a `refused` receipt;
161
+ validation/source/adapter failures carry `data: null`, `receipt: null`, and a
162
+ structured `error` (`INVALID_INPUT`, `SOURCE_UNREACHABLE`, or
163
+ `ADAPTER_FAILURE`). MCP returns that same envelope as `structuredContent` and
164
+ marks failures and deliberate refusals as MCP errors.
165
+
166
+ `hcc.score` has no diagnosis/date-of-service parameter, so it uses one explicit
167
+ validity anchor for every payment year: the vendored ICD-10-CM `fy2026-april`
168
+ release, effective 2026-04-01 through 2026-09-30. The answer payload and receipt
169
+ name that release and its hash. A mixed list still scores from its valid codes;
170
+ a valid-but-unmapped code keeps the existing named-unmapped behavior. If every
171
+ code is invalid, the route refuses with `NO_VALID_DIAGNOSES`. Use
172
+ `--allow-demographic-only` (or MCP/call parameter
173
+ `allow_demographic_only=true`) only to opt into a demographic-only partial RAF.
174
+
120
175
  ## Pin your sources and fail the build when they move
121
176
 
122
177
  ```
@@ -132,7 +187,7 @@ run ends with:
132
187
  23 ok, 0 drift, 0 unreachable, 0 schema_changed, 0 unpinned, 0 stale, 0 error
133
188
  ```
134
189
 
135
- Exit codes are the contract: `0` everything matched, `1` drift or schema change,
190
+ Doctor has a separate, unchanged 0–4 verdict contract: `0` everything matched, `1` drift or schema change,
136
191
  `2` a source was unreachable or an adapter broke, `3` a canary was observed but
137
192
  nothing pinned it, `4` everything matched but something reported that what it
138
193
  read is out of date. `3` and `4` exist because a missing `source-lock.json` used
@@ -176,7 +231,9 @@ a stack trace — the moment upstream moves.
176
231
  `hc-source mcp` serves every discovered tool over MCP stdio: one MCP tool per
177
232
  route, the same typed parameters, the same receipts, the same PHI refusals as
178
233
  the CLI. It refuses to start if any adapter failed to import, because an agent
179
- that sees a short `tools/list` reads it as the whole product.
234
+ that sees a short `tools/list` reads it as the whole product. Every MCP result,
235
+ including validation failures, source failures, and deliberate refusals, carries
236
+ the machine envelope documented above in `structuredContent`.
180
237
 
181
238
  Responses are cached on disk and revalidated conditionally — an entry is stored
182
239
  only if the response carried an `ETag` or a `Last-Modified`, and a hit is a
@@ -99,6 +99,16 @@ The third command must print an answer and a receipt without touching the
99
99
  network. If it does not, the wheel is missing `hc_source/data/` and the release
100
100
  should be yanked rather than patched over.
101
101
 
102
+ ## Next-version breaking contract note
103
+
104
+ The next release after `0.1.1` must call out the tool-call contract break:
105
+ deliberate fail-closed outcomes move from exit 0 to exit 3 and carry
106
+ `receipt.status="refused"` plus `receipt.refusal_code`; negative answers carry
107
+ structured non-null data; and `--json`/MCP use one `{data, receipt, error}`
108
+ envelope for answers, refusals, invalid input, and source failures. Doctor's
109
+ existing 0–4 verdict meanings do not change. Do not publish the next version
110
+ without putting that migration note in its release notes.
111
+
102
112
  For the GitHub Release artifacts instead of PyPI:
103
113
 
104
114
  ```bash
@@ -9,6 +9,8 @@
9
9
  # The job fails (with error annotations carrying each canary's remediation
10
10
  # verbatim) whenever doctor reports drift, a schema change, or an unreachable
11
11
  # source. See docs/ci.md for the full workflow example.
12
+ # This action consumes doctor's 0-4 verdict contract only. Tool-call exit 3 is
13
+ # a deliberate refusal; doctor exit 3 remains observed-but-unpinned.
12
14
  name: "SourceLock Doctor"
13
15
  description: >-
14
16
  Fail the build when a pinned public healthcare data source drifts, changes
@@ -22,6 +22,10 @@ Behaviour contract:
22
22
  * refuses to report green on a report that contradicts itself -- see
23
23
  :func:`reconcile_exit_code`.
24
24
 
25
+ These are doctor-specific meanings. Route calls have a separate 0-3 contract
26
+ where exit 3 is a deliberate refusal; this script never invokes a route call and
27
+ must not reinterpret doctor's exit 3 as one.
28
+
25
29
  Local dry-run (no GitHub, no install step)::
26
30
 
27
31
  python action/run_doctor.py --from-json report.json
@@ -7,6 +7,12 @@ source. Every failing canary becomes a GitHub error annotation carrying that
7
7
  canary's remediation text verbatim, so the person reading the red build sees
8
8
  the fix, not a stack trace.
9
9
 
10
+ The action runs **doctor only**, so its exit `3` remains doctor's
11
+ "observed but unpinned" verdict. It does not consume route-call exit codes;
12
+ `hc-source call` and the route shortcuts now use exit `3` for a deliberate
13
+ fail-closed refusal. The two contracts are intentionally namespaced by command
14
+ and are both tabulated in the README.
15
+
10
16
  ## Scaffold it
11
17
 
12
18
  ```
@@ -117,7 +123,7 @@ action version you pin is exactly the scanner version you run.
117
123
 
118
124
  ## What failure looks like
119
125
 
120
- Exit codes are doctor's contract: `0` everything matches the lockfile, `1`
126
+ Exit codes here are doctor's unchanged contract: `0` everything matches the lockfile, `1`
121
127
  drift or schema change, `2` unreachable source, adapter load failure, or an
122
128
  internal adapter error, `3` a canary was observed but nothing pinned it, `4`
123
129
  every canary matched but at least one reported that what it read is out of
@@ -50,6 +50,10 @@ The same things it enforces on itself. There is no privileged path.
50
50
  * **Receipts.** `build_receipt` requires at least one domain non-claim and takes
51
51
  provenance from the fetch, so you cannot stamp a retrieval time you did not
52
52
  perform.
53
+ * **Answer nullability.** An ordinary miss is structured data such as
54
+ `{"found": false}`. Only a deliberate fail-closed refusal may use
55
+ `data=None`, and its receipt must say `status="refused"` with a stable
56
+ `refusal_code`.
53
57
 
54
58
  ## What a broken plugin looks like
55
59
 
@@ -45,7 +45,7 @@ from hc_source.schemas import SourceContract
45
45
  # name, and every tool you declare must be prefixed with this id.
46
46
  SOURCE_ID = "example_formulary"
47
47
 
48
- TRANSFORM_VERSION = "example-formulary-transform/1"
48
+ TRANSFORM_VERSION = "example-formulary-transform/2"
49
49
 
50
50
  RELEASE_ID = "2026-07"
51
51
  _RELEASE_FROM = date(2026, 7, 1)
@@ -136,7 +136,7 @@ def _lookup(params: LookupParams) -> ToolResult:
136
136
  )
137
137
 
138
138
  return ToolResult(
139
- data=None if entry is None else {"code": params.code, **entry},
139
+ data={"found": False} if entry is None else {"code": params.code, **entry},
140
140
  receipt=build_receipt(
141
141
  contract=CONTRACT,
142
142
  route=f"{SOURCE_ID}.lookup",
@@ -200,4 +200,4 @@ class ExampleFormularyAdapter(SourceAdapter):
200
200
 
201
201
 
202
202
  #: The module-level name discovery looks for. Everything above is yours.
203
- ADAPTER = ExampleFormularyAdapter()
203
+ ADAPTER = ExampleFormularyAdapter()
@@ -1,5 +1,5 @@
1
1
  """SourceLock: deterministic, provenance-tracked access to public healthcare data sources."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -28,7 +28,7 @@ from ..receipts import build_receipt
28
28
  from ..schemas import SourceContract
29
29
 
30
30
  SOURCE_ID = "demo"
31
- TRANSFORM_VERSION = "1"
31
+ TRANSFORM_VERSION = "2"
32
32
 
33
33
  #: Environment override, used by the test suite to simulate upstream drift.
34
34
  FIXTURE_ENV_VAR = "HC_SOURCE_DEMO_FIXTURE"
@@ -145,7 +145,7 @@ def _lookup_code(params: LookupCodeParams) -> ToolResult:
145
145
  )
146
146
 
147
147
  return ToolResult(
148
- data=match,
148
+ data=match if match is not None else {"found": False},
149
149
  receipt=build_receipt(
150
150
  contract=CONTRACT,
151
151
  route="demo.lookup_code",