Sentinel Signal

Sentinel Policy M3: canonical policy/version system (v1.0.676)

Source: docs/sentinel-policy-m3-policy-version-system-v1.0.676.md

Document Content

Sentinel Policy M3: canonical policy/version system (v1.0.676)

What shipped

M3 (policy identity, versioning, sectionization) per the product spec's POL-001..007 / VER-001..007 / SEC-001..005, built on top of M1's ingestion spine and M2's real CMS NCD adapter.

  • **NormalizedDocument**: one row per DocumentAsset, holding
  • canonical_content_hash (hash of normalized section text, not raw bytes — PAR-005: a whitespace-only raw diff must not imply a material change) plus extracted identity metadata.

  • **Policy/PolicyAlias/PolicyRelationship**: canonical policy identity
  • keyed by (source_id, external_policy_id_system, external_policy_id) — for CMS NCDs, document_id (POL-002's "strong identity" signal). Title and display-ID history preserved as durable aliases (POL-003/006). PolicyRelationship table exists now for schema completeness; nothing populates it yet since CMS NCD detail responses don't expose predecessor/successor links.

  • **PolicyVersion**: immutable, ordered by CMS's own document_version
  • (source-authoritative per VER-007, not Sentinel-inferred). Never overwritten after insert (VER-006) — a content mismatch under the same version number routes to review instead.

  • **PolicySection**: CMS NCD fields mapped into the canonical taxonomy
  • (item_service_description→coverage, indications_limitations/ reasons_for_denial→limitations, cross_reference→references, transmittal/revision/AMA fields→administrative) — empty fields produce no section rather than a fabricated one.

  • **PolicyIdentityReviewCase**: the POL-005 "don't silently merge or
  • overwrite" escape hatch — no review workflow yet (that's M8), just an honest, inspectable record.

  • New PolicyNormalizer protocol (adapters/protocol.py), implemented
  • only by CMSCoverageAdapter — M1's generic DirectDocumentAdapter deliberately does not implement it (no reliable identity signal for arbitrary documents, per ADP-007/AGENT-009).

  • New policyintel.py module wires normalization + identity/version
  • resolution into ingest.py's existing run_discovery(), gated on isinstance(adapter, PolicyNormalizer).

  • Minimal admin-token-gated verification endpoints: GET /v1/policies,
  • GET /v1/policies/{id} (aliases + versions + sections inline), GET /v1/policy-review-cases. Not the M9 customer-facing contract (no pagination/filtering/entitlements) — just enough to verify the exit criterion.

A real bug found via live data, not fixtures

effective_date_raw/PolicyVersion.effective_date were originally modeled as bounded VARCHAR(64)/VARCHAR(32), sized from the happy-path fixture example ("06/11/1985"). Running the resolver against a second real NCD (ncdid=9) during verification hit a real Postgres StringDataRightTruncation error: CMS's effective_date field isn't always a date — some NCDs return free-text prose ("This is a longstanding national coverage determination. The effective date of this version has not been posted."). Widened both columns to Text. This is exactly the DAT-003 "preserve original evidence text" case the spec calls out — the fix keeps the honest raw text rather than truncating or reparsing it.

Verification

  • PYTHONPATH=policy/src:policy/tests python -m pytest policy/tests -q —
  • 48 passed, including new fixture-based tests: first-run creates policy/version/sections; a second identical run is idempotent (no duplicates); a new document_version creates version #2 linked via previous_version_id; a same-version content mismatch routes to PolicyIdentityReviewCase instead of mutating the existing version; missing identity signals route to review without creating a Policy.

  • Migration verified against real local PostgreSQL (upgrade from
  • 0001_initial, table/column shapes confirmed).

  • End-to-end smoke test against the real live CMS API (not just
  • fixtures): registered a source, resolved two real NCDs (108, 9) through the actual resolve_policy_version() path against real Postgres, then re-ran against the same two documents to confirm zero duplicate policies/versions. Queried the live result through the actual FastAPI app (GET /v1/policies, /v1/policies/{id}) — correct nested aliases/versions/sections, zero false review cases.

  • Exit criterion met: a representative CMS sample (2 real NCDs, one with
  • a normal date and one with free-text prose) reconstructed into trustworthy policy timelines with lineage back to the original DocumentAsset/DocumentObservation rows.

Known limitations / deferred

  • published_date stays null for NCDs — the CMS NCD detail endpoint has
  • no explicit publication-date field, and guessing one from revision_history free text is DAT-family extraction work, out of scope for M3.

  • PolicyRelationship (predecessor/successor) has no CMS-sourced data yet.
  • No pagination on the verification endpoints — fine at current scale, not
  • a contract that needs to survive unchanged into M9.