The Ontology Does the Reasoning
How Runtime Subtypes and Shadow Discovery Transform Care-Management Eligibility
The Problem
Healthcare care-management programs — Remote Physiologic Monitoring (RPM), Chronic Care Management (CCM), and Advanced Primary Care Management (APCM) — have eligibility rules that depend on a patient's diagnoses, their insurance payer, and the payer's program support. A patient with Type 2 diabetes and Medicare coverage may qualify for RPM. A patient with two distinct chronic conditions and a payer that supports CCM may qualify for CCM. An APCM-eligible patient's specific HCPCS code depends on how many chronic conditions they have and whether they are a Qualified Medicare Beneficiary.
In a traditional system, you would handle these rules one of three ways:
Write if-else chains that check diagnosis codes against eligibility lists, check payer support flags, and return a yes/no answer. This works, but it is brittle. Adding a new program means rewriting code. The eligibility logic is separated from the data model it operates on.
Send the patient's diagnoses and insurance to a language model and ask, "Is this patient eligible for RPM?" This is flexible but non-deterministic. The same patient may get different answers on different runs. The reasoning is a black box.
Externalize eligibility into a dedicated rules system. This separates rules from code, but it also separates rules from data. You need serialization layers, mapping layers, and synchronization strategies.
Each approach has a fundamental weakness: the eligibility logic is not part of the data model. It lives somewhere else, and that separation creates friction, opacity, or both.
This article describes a different approach, one where the ontology is not just storing facts but is doing the reasoning itself. The eligibility logic lives in the type system. Tools are typed against runtime subtypes. And shadows discover hidden patterns from outcomes — all in the same typed graph.
That is the whole chain. The rest of this article walks through each step.
Layer 1: The Medical Hierarchy Clinicians Already Use
Everything starts with the medical hierarchy that clinicians already use. ICD-10 codes are organized into branches by body system and disease category:
ICD10Code
├── E00-E89 Endocrine diseases
│ └── E11 Type 2 diabetes mellitus
│ ├── E11.9 Type 2 diabetes without complications
│ ├── E11.65 Type 2 diabetes with hyperglycemia
│ └── E11.22 Type 2 diabetes with chronic kidney disease
│
├── I00-I99 Circulatory diseases
│ ├── I10 Essential hypertension
│ └── I50 Heart failure
│ ├── I50.9 Heart failure, unspecified
│ ├── I50.22 Chronic systolic heart failure
│ └── I50.32 Chronic diastolic heart failure
│
└── J00-J99 Respiratory diseases
└── J44 COPD
└── J44.9 COPD, unspecified
These branches are unrelated in the native ICD-10 tree. E11 (diabetes) and I50 (heart failure) share no parent below the root. But from a care-management perspective, they share something important: they are all chronic conditions. The native taxonomy does not express that. We need to add it.
Layer 2: Grouping Codes by What They Mean for Care Management
Instead of creating a separate list of "chronic condition codes," we add an operational type directly to selected ICD-10 branches:
E11.AddType(ChronicCondition)
I10.AddType(ChronicCondition)
I50.AddType(ChronicCondition)
J44.AddType(ChronicCondition)
This creates a parallel taxonomy — a projected view — that is inferred, not copied:
Same eight codes, regrouped by a new type instead of copied into a new list:
ChronicCondition
├── E11 Type 2 diabetes mellitus
│ ├── E11.9 Type 2 diabetes without complications
│ ├── E11.65 Type 2 diabetes with hyperglycemia
│ └── E11.22 Type 2 diabetes with chronic kidney disease
│
├── I10 Essential hypertension
│
├── I50 Heart failure
│ ├── I50.9 Heart failure, unspecified
│ ├── I50.22 Chronic systolic heart failure
│ └── I50.32 Chronic diastolic heart failure
│
└── J44 COPD
└── J44.9 COPD, unspecified
The key insight: we do not manually mark every leaf code. We add the operational type once to the branch, and inheritance does the rest.
Path: E11.65 → parent E11
Known: E11 has type ChronicCondition
Answer: Yes. E11.65 is treated as ChronicCondition because its ancestor E11 has that type.
The runtime walks from E11.65 to its parent E11, finds the ChronicCondition type edge, and answers yes — a graph traversal, not a lookup table. A traditional system would need to maintain a list of every individual ICD-10 code that counts as chronic. With the projected taxonomy, a new code under E11 inherits ChronicCondition automatically.
Layer 3: Turning a Patient Into a Category
Now we move from the ICD-10 taxonomy to the patient. A patient has diagnoses, and those diagnoses link back to ICD-10 codes. The question is: how many distinct chronic conditions does this patient have?
That question is different from asking how many diagnoses the patient has. Consider:
Patient: Mary
Insurance: Medicare
Diagnoses:
├── E11.65 Type 2 diabetes mellitus with hyperglycemia
└── I10 Essential hypertension
Mary has two diagnoses. She also has two distinct chronic-condition branches:
E11.65 → E11 → ChronicCondition
I10 → ChronicCondition
But if Mary had E11.65 and E11.9, she would have two diagnoses but only one distinct chronic-condition branch:
E11.65 → E11
E11.9 → E11
Distinct chronic branches: E11 only
Count: 1
The count we care about is distinct chronic-condition branches, not raw diagnosis count.
In ProtoScript, we express this as a runtime subtype:
[SubType]
prototype PatientWithOneChronicCondition : Patient
{
function IsCategorized(Patient patient) : bool
{
return CountDistinctChronicConditionBranches(patient.Diagnoses) >= 1;
}
}
The [SubType] annotation tells the runtime this is a dynamic category, evaluated fresh each time rather than assigned once like a static class. As the ProtoScript documentation describes it, Subtypes “dynamically reclassify Prototypes at runtime based on context-sensitive conditions, unlike static class hierarchies.” When a patient object is evaluated, the runtime calls IsCategorized. If it returns true, the runtime adds an isa edge from the patient to PatientWithOneChronicCondition, and the patient's runtime type literally changes.
Now we can build eligibility subtypes that compose the chronic-condition subtypes with payer-support checks:
[SubType]
prototype RPMEligiblePatient : Patient
{
function IsCategorized(Patient patient) : bool
{
return patient -> PatientWithOneChronicCondition
&& patient.Insurance.Payer -> SupportsRPM;
}
}
[SubType]
prototype CCMEligiblePatient : Patient
{
function IsCategorized(Patient patient) : bool
{
return patient -> PatientWithTwoChronicConditions
&& patient.Insurance.Payer -> SupportsCCM;
}
}
[SubType]
prototype APCMEligiblePatient : Patient
{
function IsCategorized(Patient patient) : bool
{
return patient.Insurance.Payer -> SupportsAPCM;
}
}
Notice the composition: patient -> PatientWithOneChronicCondition checks whether the patient has already been categorized as that subtype. The -> operator traverses the isa edges.
The payer side works the same way:
Medicare.AddType(SupportsRPM)
Medicare.AddType(SupportsCCM)
Medicare.AddType(SupportsAPCM)
Before ontology evaluation, Mary is just a Patient. After the runtime evaluates the subtypes:
Mary
├── Patient
├── RPMEligiblePatient
├── CCMEligiblePatient
└── APCMLevel2PatientThe point is not that Mary has a field called
eligible = true. The point is that Mary's runtime object gains new types. She is, in the type system's view, an RPMEligiblePatient. That means any tool that accepts RPMEligiblePatient can now receive Mary.Layer 4: Typed Tool Signatures
Before subtype projection, Mary is just a Patient. The tools available to a generic Patient are limited:
Available tools for Patient
├── View patient
├── Review diagnoses
└── Classify eligibility
After subtype projection, Mary's expanded type set unlocks new tools:
Mary : RPMEligiblePatient
└── Enroll patient in RPM
Mary : CCMEligiblePatient
└── Create CCM monthly case
Mary : APCMLevel2Patient
└── Create APCM Level 2 candidate
In ProtoScript, a tool is a Prototype with an Execute function:
prototype ToEnrollPatientInRPM : ProtoScriptAction
{
function Execute(RPMEligiblePatient patient) : RPMEnrollment
{
return CreateRPMEnrollment(patient);
}
}
The RPM tool does not re-check ICD-10 codes. It does not re-check payer support. It does not ask the LLM whether the patient is eligible. It accepts RPMEligiblePatient as its input type. The ontology and subtype layer have already decided whether the patient object qualifies.
RPMEligiblePatient, the tool simply isn't available to her, with no argument, prompt engineering, or hallucinated eligibility able to change that.Layer 5: APCM Code Selection via Subtype Hierarchy
APCM is slightly different from RPM and CCM because even a patient with zero chronic conditions may map to Level 1. The ontology should therefore separate broad APCM support from APCM level selection. CMS defines three HCPCS codes:
- G0556 — APCM Level 1: one or fewer qualifying chronic conditions
- G0557 — APCM Level 2: two or more qualifying chronic conditions
- G0558 — APCM Level 3: Qualified Medicare Beneficiary with two or more qualifying chronic conditions
Mary is already an APCMEligiblePatient — her payer and context support APCM evaluation. The more specific subtypes determine which HCPCS code she maps to:
APCMEligiblePatient
├── APCMLevel1Patient → G0556
├── APCMLevel2Patient → G0557
└── APCMLevel3Patient → G0558
The tool accepts the broad type and then routes by the most specific subtype. For Mary, who is an APCMLevel2Patient, that means G0557:
function Execute(APCMEligiblePatient patient) : APCMCandidate
{
APCMCandidate candidate = new APCMCandidate();
candidate.Patient = patient;
if (patient -> APCMLevel3Patient)
{
candidate.Code = "G0558";
return candidate;
}
if (patient -> APCMLevel2Patient)
{
candidate.Code = "G0557";
return candidate;
}
if (patient -> APCMLevel1Patient)
{
candidate.Code = "G0556";
return candidate;
}
throw "APCM eligible patient does not match a known APCM level.";
}
In one sentence: the ontology creates the APCM subtype; the tool uses the subtype to select the HCPCS code. The tool does not contain eligibility logic. It contains code-selection logic. The eligibility was already decided by the type system.
Layer 6: Shadow Discovery from Outcomes
So far, the subtypes have been defined by hand — we wrote the IsCategorized functions based on known program rules. But what about patterns we do not know yet? What about payer-specific rules that are not published but are visible in acceptance and rejection outcomes?
This is where Shadows come in. The ProtoScript documentation describes Shadows as “a cornerstone of ProtoScript's graph-based ontology, serving as the primary mechanism for learning and categorization.” A Shadow is a Prototype generated by Least General Generalization (LGG) — the runtime compares two or more Prototypes and produces a new Prototype that captures their most specific common structure, dropping the differences.
Accepted Cases
Start with three accepted UnitedHealthcare RPM examples:
Accepted Patient 1
├── Payer: UnitedHealthcare
├── Program: RPM
├── Diagnosis: I50.22 Chronic systolic heart failure
├── RPM device: blood pressure cuff
├── Reading days: 22
├── Consent: yes
└── Outcome: accepted
Accepted Patient 2
├── Payer: UnitedHealthcare
├── Program: RPM
├── Diagnosis: I50.9 Heart failure, unspecified
├── RPM device: scale
├── Reading days: 18
├── Consent: yes
└── Outcome: accepted
Accepted Patient 3
├── Payer: UnitedHealthcare
├── Program: RPM
├── Diagnosis: I50.32 Chronic diastolic heart failure
├── RPM device: pulse oximeter
├── Reading days: 25
├── Consent: yes
└── Outcome: accepted
When the runtime applies LGG to these three cases, it produces a Shadow:
Accepted UnitedHealthcare RPM Shadow
├── Payer: UnitedHealthcare
├── Program: RPM
├── Diagnosis branch: I50 Heart failure
├── Consent: yes
├── Reading evidence: sufficient
└── Outcome: accepted
Rejected Cases
Now compare against rejected cases that look superficially similar:
Rejected Patient 1
├── Payer: UnitedHealthcare
├── Program: RPM
├── Diagnosis: E11.9 Type 2 diabetes
├── RPM device: glucometer
├── Reading days: 24
├── Consent: yes
└── Outcome: rejected
Rejected Patient 2
├── Payer: UnitedHealthcare
├── Program: RPM
├── Diagnosis: I10 Essential hypertension
├── RPM device: blood pressure cuff
├── Reading days: 21
├── Consent: yes
└── Outcome: rejected
Rejected Patient 3
├── Payer: UnitedHealthcare
├── Program: RPM
├── Diagnosis: J44.9 COPD
├── RPM device: pulse oximeter
├── Reading days: 26
├── Consent: yes
└── Outcome: rejected
The rejected Shadow:
Rejected UnitedHealthcare RPM Shadow
├── Payer: UnitedHealthcare
├── Program: RPM
├── Diagnosis branch: no shared specific ICD-10 branch below ChronicCondition
├── Consent: yes
├── Reading evidence: sufficient
└── Outcome: rejected
The Contrast
Rejected shadow: UnitedHealthcare + RPM + non-I50 chronic branches → rejected
All six cases have the same payer, the same program, consent, and sufficient readings. The only structural difference is the diagnosis branch. This contrast proposes a candidate discovered rule: for UnitedHealthcare RPM, accepted examples appear to share the I50 heart-failure branch. Other chronic-condition branches were rejected despite otherwise sufficient evidence.
This should remain “discovered, pending human verification,” not an automatic production policy rule. The Shadow mechanism proposes patterns; a human decides whether to promote them to enforced subtypes.
Related Research
The principle of compiling ontological specifications into executable tool interfaces that enforce semantic constraints at the point of mutation, rather than validating after the fact, was described by Zhou et al. in “Ontology-to-tools compilation for executable semantic constraint enforcement in LLM agents” (arXiv:2602.03439, February 2026). Their work proposes that classes, properties, and restrictions in an ontology should become constrained tool affordances, forcing the LLM to act through typed interfaces where semantic violations are caught during generation and mutation. Buffaly applies the same safety principle from inside the runtime: the ontology is the runtime type system itself, and tool access is gated by native subtype relationships. Buffaly then extends this with shadow discovery, using LGG-based pattern learning from outcomes to surface payer-specific candidate rules for human verification.
The separation of LLM reasoning from state mutation, where the model proposes and the runtime adjudicates, was independently formalized by Sterling in “Provably Auditable and Safe LLM Agents from Human-Authored Ontologies” (arXiv:2606.04903, June 2026). Sterling's Agentic Redux architecture uses typed lambda calculus and an append-only audit ledger to provide formal correctness guarantees for auditable agent execution in appropriate domains, including healthcare billing compliance. Buffaly converges on the same core boundary: the LLM can pass a handle to a patient object, but the runtime type system decides whether that patient satisfies the tool's input type before any state-changing work proceeds. The mechanism is different — Buffaly uses native ProtoScript subtypes and isa edges rather than a central meta-agent with typed lambda calculus — and the shadow layer adds a learning path for discovering eligibility patterns from acceptance and rejection outcomes.
The Full Simple Story
The ontology is not just storing facts. It is doing the reasoning. The type system gates the tools. The shadows discover the patterns. And it all happens in the same typed graph — no external rules engine, no LLM judgment calls, no black-box model, no sync problem. The ontology is the reasoning engine.
That determinism and traceability aren't just implementation details. They're what regulated healthcare AI needs: every eligibility decision traced through the graph, gated by types instead of prompts, and improved only with a human in the loop.
The neuro-symbolic framing behind this architecture is backed by research: Alexander Rombach, Chantale Lauer, and Nijat Mehdiyev, “Neuro-Symbolic Agents for Regulated Process Automation: Challenges and Research Agenda” (arXiv:2606.13405). ProtoScript documentation is available at github.com/buffaly-ai/protoscript.