v1.0.672 — Sentinel Policy M2: CMS NCD vertical slice (acquisition)
Context
M1 shipped M1's ingestion spine live with one generic DirectDocumentAdapter and no real source registered. M2's job, per the product spec's own milestone plan: prove the spine works against a real authoritative source — CMS National Coverage Determinations (NCDs) — end to end, idempotently. Acquisition only: no policy identity/versioning (M3), no diffing (M4), no extraction (M5), no customer-facing anything (M9+, still gated behind the M0 legal review that hasn't happened).
Before writing any code, pulled the real, official CMS Coverage API OpenAPI spec directly (https://api.coverage.cms.gov/docs/v1/coverage-api.json, discovered via the Swagger UI's index.js config after the HTML shell alone wasn't enough — 97 endpoints total) and hit three live endpoints to confirm actual response shapes before designing anything against them.
What shipped
**CMSCoverageAdapter** (policy/src/sentinel_policy/adapters/cms_coverage.py) — a real, second implementation of M1's unchanged SourceAdapter Protocol:
- First-ever run (
since is None):GET /v1/reports/national-coverage-ncd/— confirmed live, 345 - Every run after (
sinceset):GET /v1/reports/whats-new/national/?timeframe=N&document_type=NCD - Per-document fetch:
GET /v1/data/ncd/?ncdid=X&ncdver=Y, via M1's unmodifiedsecure_fetch()
NCDs, single unpaginated response.
— incremental, timeframe computed from since and clamped to the API's own documented 120-day maximum.
— no new fetch/retry/SSRF logic needed anywhere.
Adapter registry (policy/src/sentinel_policy/adapters/registry.py, new): build_adapter() moved out of direct_document.py now that a second adapter exists; dispatches on adapter_type. No behavior change for the existing direct_document path.
Deliberate non-goal, not an oversight: no AMA/ADA/AHA license-agreement Bearer-token flow is implemented, even though the real API confirms some NCD content requires one. Accepting a licensing agreement programmatically is exactly the kind of decision the product spec's M0 legal-review gate exists for. Any license-gated fetch 401/403s, which M1's existing fetcher already classifies as a permanent error — the source lands in REVIEW_REQUIRED with zero new code, not a crash.
A real bug found and fixed during testing
_clamp_timeframe_days() assumed its since argument was always timezone-aware. SQLite (used for local dev/tests) doesn't round-trip timezone info even through a DateTime(timezone=True) column — a value written as UTC-aware can come back naive on read. Surfaced immediately by the idempotency test's second run_discovery() call, which passes the real source.last_success_at from the DB (not a hand-constructed test datetime). Fixed by treating a naive value as UTC, matching this codebase's existing convention (ingest.py's _utcnow()).
Verified against the real, live CMS API — not just fixtures
Built the real Docker image, ran it against a real local Postgres, registered the actual CMS source via the deployed API shape, and:
- Triggered a full backfill: 345/345 real NCD documents discovered and archived successfully
- Triggered a second run: correctly took the incremental what's-new path (~3s, 8 records — real
in ~2m15s, zero errors.
CMS "recently updated" NCDs as of today), and **the database still shows exactly 345 unique document_assets** — every one of those 8 was already-archived content, correctly deduped by content hash — while document_observations grew from 345 to 353, proving observation history is preserved per run even when the underlying asset isn't re-created. This is the actual M2 exit criterion ("idempotent... can run repeatedly without duplicates"), proven against production data from the real external source, not recorded fixtures.
Verification
PYTHONPATH=policy/src:policy/tests python -m pytest policy/tests -q— 35 passed (was 30; +5 new:python -m pytest tests/ -q— 334 passed.verify/tests— 824 passed. Neither touched by this- Real fixtures (not invented) captured from the three live endpoints during planning, stored under
full-list parsing against the real captured 345-record fixture, incremental timeframe clamping, idempotent run_discovery() against fixtures, and the license-required-403 → REVIEW_REQUIRED safety test).
milestone (only files under policy/ changed).
policy/tests/fixtures/cms_coverage/.
Not done / explicitly out of scope
License-token acquisition (see above). LCD/Article/MAC adapters. Policy identity/version modeling, sectionization (M3). Deterministic diffing (M4). LLM extraction (M5). Evidence/confidence scoring (M6). Any customer-facing surface (M9+). No changes to M1's fetcher, storage, ingest orchestration, or DB schema — this milestone's whole point was proving that machinery is genuinely reusable for a real source, not a reason to modify it.