State bindings and relational contracts

Requires mateprobe==0.1.0a4. These APIs are not available in a2.

These optional helpers cover small, explicit relationships between structured output and state. They do not parse prose, evaluate expressions, or introduce a model validation dependency.

check_fields(output, state, bindings) accepts flat mappings of JSON scalar values. A binding maps an output name to a state key. The result is the normal Report object used by the package. Missing fields produce an undetermined check, and equality is type-sensitive: True, 1, and 1.0 are different values. Unbound fields are not checked; the application decides which fields are required. The mapping must represent the actual business relationship: refund eligibility, approval, and completed execution are different facts and must not be equated.

from mateprobe import check_fields

report = check_fields(
    output={"reservation_confirmed": True},  # Structured output from the LLM.
    state={"booking.confirmed": False},  # Authoritative API result.
    bindings={"reservation_confirmed": "booking.confirmed"},
)
assert not report.accepted
print(report.to_dict())

The caller obtains the authoritative state, validates the output schema, and decides whether to block, retry, or show a fallback. A timeout is not a confirmed failure: omit an unknown fact rather than inventing False. Missing facts are undetermined and blocked by the returned strict policy. An explicit None is a known JSON null, not an automatic unknown marker. The helper never reads message prose and cannot detect prose/field contradictions.

Applications using Pydantic can adapt an explicit flat projection, for example:

bindings = {"reservation_confirmed": "booking.confirmed"}
report = check_fields(
    output_model.model_dump(mode="json", include=set(bindings)),
    trusted_state,
    bindings,
)

Here output_model is the application's validated model and trusted_state is its backend projection. Nested objects need explicit flattening. There is no native Pydantic integration or new dependency; schema validation and business validation remain distinct. Use integer minor currency units when possible; this helper accepts JSON scalars, not Decimal objects or implicit coercion.

Relational rules use FieldRef(state_ref, key) to select fields from a Context. CompareFields supports exact typed equality (eq) and numeric ordering (le). Ordering accepts finite integers and floats, excluding bool; unsupported types return an error check. AllowedTransition checks an exact, typed (before, after) pair against its finite allow-list. Missing branch or field references are undetermined.

All behavior is version 1 and deterministic. Configure relationships as data and use the regular engine, for example:

from mateprobe.engine import evaluate
from mateprobe.model import Context, Document, Surface
from mateprobe.relations import AllowedTransition, FieldRef

document = Document((Surface("out", ""),))
context = Context({"before": {"status": "draft"}, "after": {"status": "ready"}})
rule = AllowedTransition(
    "status-transition",
    FieldRef("before", "status"),
    FieldRef("after", "status"),
    (("draft", "ready"),),
)
report = evaluate(document, context, (rule,))

For a proposed amount and a trusted limit:

from mateprobe import CompareFields

context = Context({"action": {"cents": 5000}, "trusted": {"limit_cents": 6000}})
rule = CompareFields(
    "within-limit", FieldRef("action", "cents"), FieldRef("trusted", "limit_cents"), "le"
)
assert evaluate(document, context, (rule,)).accepted

The caller assigns state references and supplies trusted snapshots. These are Python APIs; the alpha JSON CLI allowlist is unchanged and does not decode the new relational rules. An empty transition allow-list denies all known transitions. Checking a proposed action is not proof of execution and does not replace the backend's authorization, concurrency, or transactional checks.