Buffaly Logo
Technical Explainer

The Ontology Does the Reasoning

How Runtime Subtypes and Shadow Discovery Transform Care-Management Eligibility

Matt Furnari July 3, 2026

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:

Hardcode the rules in application logic.

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.

Ask an LLM to judge eligibility case by case.

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.

Maintain a separate rules engine.

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.

ICD-10 branch gets a ChronicCondition type Diagnosis becomes ChronicDiagnosis Patient becomes PatientWithOneChronicCondition or PatientWithTwoChronicConditions Patient may become RPMEligiblePatient, CCMEligiblePatient, or a specific APCM level subtype Tool accepts the subtype — type system gates execution Shadows discover hidden payer patterns from outcomes

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.

Example: Is E11.65 a ChronicCondition?

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)
What happens to Mary?

Before ontology evaluation, Mary is just a Patient. After the runtime evaluates the subtypes:

Mary
├── Patient
├── RPMEligiblePatient
├── CCMEligiblePatient
└── APCMLevel2Patient


The 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.

The critical distinction: The LLM doesn't decide eligibility — it passes a handle, a reference to the patient object in the graph, and the runtime type system decides whether that handle satisfies the tool's input type. If Mary isn't an 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:

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
What fell out: The exact leaf codes (I50.22, I50.9, I50.32) were different, so LGG generalized them to their common parent: I50 Heart failure. The device types were different, so they were dropped. The reading-day counts were different, so they generalized to “sufficient.” What remained was the structural pattern that all three cases share.

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

Accepted shadow: UnitedHealthcare + RPM + I50 branch → accepted

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

ICD-10 branch gets ChronicCondition type Diagnosis becomes ChronicDiagnosis Patient becomes PatientWithOneChronicCondition or PatientWithTwoChronicConditions Patient may become RPMEligiblePatient, CCMEligiblePatient, or a specific APCM level subtype Tool accepts the subtype — type system gates execution Shadows discover hidden payer patterns from outcomes

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.