SPEC SHEET // BLK-2026-REV.4: UK Patent Pending (Filed 28 Oct 2026, s.62(1) UK Patents Act 1977) · Source-Available Dual Licence
EDITION:

Cornerstone Engineering Monograph · UK Patent Pending

Recursive Delta-Hydration: Multi-Turn Progressive Validation and Leaf-Locking on Nested Pydantic v2 Models

How Blockstate synthesises recursive PartialSlotModel schemas with a singleton UNSET sentinel, validates leaf paths individually (BLK-VAL-002), locks validated leaves (BLK-SEC-002), and halts immediately on DisqualifyingBreachError (BLK-VAL-003).


1. The Multi-Turn Structured Output Problem

Single-turn structured output libraries work well when a user provides every required field in one prompt. In real enterprise workflows, however, a customer filing an insurance claim, applying for a mortgage, or completing clinical triage supplies data across three to six conversational turns.

Consider a nested Pydantic v2 model for a motor insurance claim:

from datetime import date
from pydantic import BaseModel, Field, field_validator
from blockstate import BreachError

class IncidentLocation(BaseModel):
    street: str = Field(description="Street name where the incident occurred")
    city: str = Field(description="Town or city")
    postcode: str = Field(pattern=r"^[A-Z]{1,2}\d[A-Z\d]?\s*\d[A-Z]{2}$")

class MotorClaimSlots(BaseModel):
    policy_number: str = Field(pattern=r"^POL-\d{6}$")
    vehicle_usage: str = Field(description="personal | commuting | commercial_haulage")
    incident_date: date = Field(description="Date of incident (YYYY-MM-DD)")
    estimated_damage_gbp: float = Field(gt=0)
    location: IncidentLocation

    @field_validator("incident_date", mode="after")
    @classmethod
    def validate_not_in_future(cls, v: date) -> date:
        if v > date.today():
            raise ValueError("Incident date cannot be in the future.")
        return v

    @field_validator("vehicle_usage", mode="after")
    @classmethod
    def check_commercial_eligibility(cls, v: str) -> str:
        if v.lower() == "commercial_haulage":
            raise BreachError("Commercial haulage policies require the Specialist Fleet Desk.")
        return v

If a customer says on Turn 1:

"Hi, my policy is POL-884120, personal use, and someone bumped my car in Manchester (M1 1AG), date 2099-01-01."

standard MotorClaimSlots.model_validate(...) fails in three ways:

  1. location.street and estimated_damage_gbp are missing, causing the entire model instantiation to fail.
  2. Because incident_date (2099-01-01) fails validate_not_in_future, standard batch validation discards the four valid fields (policy_number, vehicle_usage, location.city, location.postcode) that were supplied in the same sentence.
  3. Creating manual Optional[...] mirror classes for every nested model loses distinction between "the user has not provided this field yet" and "the user explicitly provided None".

2. Recursive PartialSlotModel Synthesis with the Singleton UNSET Sentinel (Claim 7)

When you bind a Pydantic v2 model to a stage via @Stage(collect=MotorClaimSlots), Blockstate (src/blockstate/core/slots.py) inspects the model hierarchy at compile/load time:

  1. Nesting Depth Verification (BLK-CFG-002): The compiler verifies that the nested BaseModel tree does not exceed max_nesting_depth (default depth=8), preventing deeply nested denial-of-service schemas.
  2. Recursive UNSET Relaxation: Blockstate dynamically synthesises a recursive PartialSlotModel where every leaf field type T is relaxed to T | UnsetType, defaulting to the singleton UNSET sentinel rather than None. Nested child models (IncidentLocation) are recursively replaced with their partial counterparts (PartialIncidentLocation).
flowchart TD
    subgraph TurnInput["Turn 1 Candidate Extraction"]
        RAW["Candidate Dict from Pass 1:<br/>policy_number='POL-884120', vehicle_usage='personal',<br/>incident_date='2099-01-01', location.city='Manchester',<br/>location.postcode='<VAULT:POSTCODE_01>'"]
    end

    subgraph HydrationEngine["Recursive Delta-Hydration Engine (slots.py)"]
        RAW --> FLAT["Flatten to Dot-Separated Leaf Paths"]
        FLAT --> BREACH{"Check DisqualifyingBreachError<br/>(BLK-VAL-003)"}
        BREACH -->|"Raised (e.g. commercial_haulage)"| EXIT["Immediate Stage Exit<br/>Route to @Stage(on_breach=...)"]
        BREACH -->|"No Breach"| LEAF["Individual Per-Leaf Validation<br/>(BLK-VAL-002)"]
        LEAF -->|"Valid Leaf"| LOCK["Commit to Hydrated State &<br/>Add to locked_slots (BLK-SEC-002)"]
        LEAF -->|"Invalid Leaf (incident_date)"| REJECT["Record Leaf Error for Re-Elicitation<br/>Preserve Valid Sibling Leaves!"]
        LOCK & REJECT --> DELTA["Compute Delta = Required \ Hydrated<br/>{incident_date, location.street, estimated_damage_gbp}"]
    end

3. Individual Flattened Leaf-Path Validation (BLK-VAL-002) & Leaf Locking (BLK-SEC-002)

Rather than validating the candidate payload as an all-or-nothing monolith, Blockstate flattens the candidate dictionary into dot-separated leaf paths (policy_number, vehicle_usage, incident_date, location.city, location.postcode) and validates each leaf path individually (BLK-VAL-002):

$$\Delta = \{\text{incident\_date}, \text{location.street}, \text{estimated\_damage\_gbp}\}$$

Pass 2 is instructed to explain the date validation error and prompt the customer strictly for those three remaining fields.

4. Immediate Halt on DisqualifyingBreachError (BLK-VAL-003)

In regulated workflows, certain inputs immediately disqualify the customer from the current workflow (for example, declaring vehicle_usage = "commercial_haulage" on a consumer motor policy). Continuing to ask the customer for their street name and repair quote after they have already stated a disqualifying condition wastes their time and creates compliance risk.

When any leaf validator raises DisqualifyingBreachError (aliased as BreachError, BLK-VAL-003):

  1. The hydration loop halts immediately without evaluating remaining leaves.
  2. The engine bypasses normal $\Delta$ re-prompting and transitions deterministically to the stage specified in @Stage(on_breach="commercial_fleet_desk").
  3. Because on_breach is declared statically on @Stage, blockstate check and blockstate verify include the intake_claim -> commercial_fleet_desk edge in the compile-time Control Flow Graph.

5. Property-Based Fuzzing of $\Delta$-Hydration

In our test suite, Hypothesis (max_examples=500) generates arbitrary multi-level Pydantic v2 model trees and random multi-turn sequences of partial, valid, invalid, and breach inputs, verifying that:

Statutory Notice: Blockstate is UK Patent Pending ("Process execution control system", UKIPO priority filing 28 October 2026, UK Patents Act 1977 s.62(1)) and distributed under a Source-Available Dual Licence (LICENSE & PATENTS.md).


Interactive Verification Bench: Test the Lengauer-Tarjan dominator verifier, MappingProxyType tool gatekeeper, and TaskStack digression call stack live in the Blockstate Signal Box Simulator. Distributed under the Blockstate Source-Available Dual Licence (Free £0 for local dev, open-source CI/CD verification via blockstate verify, and 90-day internal enterprise staging evaluation; paid commercial licence for live production).