How to evaluate a rule and read its proof#
This guide shows you how to build a rule with build_application_rule, run it against a graph, and read the proof behind each result row, for when you need both the answer and a human-readable account of why it holds.
Prerequisites#
- A
FactGraphwith the facts your rule body needs. Field atoms such asUser(u).status == "active"match field claims written byfg.fields.set. See How to write and read facts. - Be aware of how existence atoms compile. An existence atom such as
User(u)lowers to theUser:existspredicate, and a field atom likeUser(u).status == "active"lowers to both theUser:existspredicate and the field predicate. So any rule body that mentions an entity variable carries an implicitUser:existsrequirement.fg.entities.create(...)emits only the entity’s identity claims; it does not emit a:existsclaim (the:existsco-emission is transitional and not performed by the current write path, see the guard note infactgraph.sdk.store). A rule body that relies on existence therefore will not match entities created through the SDK write path until a:existsclaim is present for them. - The rule-authoring imports come from
factgraph.sdk.dsl. The schema base classes (Entity,Identity,Field) come fromfactgraph.sdk.
Steps#
Define a schema and create the entities the rule will match.
createemits the identity claims for the entity;fg.fields.setadds field claims.from factgraph.sdk import Entity, FactGraph, Field, Identity class User(Entity): user_id: str = Identity() status: str = Field() fg = FactGraph.create(schema_classes=[User]) alice = fg.entities.create(User, user_id="alice") fg.fields.set(alice, "status", "active")Build the rule with
build_application_rule. Bind logic variables with thevars(...)context manager (it requires explicit names), then write the body as a list of ergonomic atoms overEntity(var). Declare aportsmapping from public port names to the logic variables that appear in the body. Passrepr=to give the rule a narrative template whose%portplaceholders are filled from the row bindings.from factgraph.sdk.dsl import build_application_rule, vars with vars("u") as (u,): rule = build_application_rule( id="active_user", when=[User(u).status == "active"], ports={"user": u}, repr="active user %user", )The body atoms are ergonomic forms over
Entity(var):User(u)is an existence atom,User(u).status == "active"adds the field equality (and an existence atom),User(u).user_id == "u-2"matches on identity,User(u).status == varbinds a field to a logic variable, andLivesIn(li).user == User(u)is a cross-entity reference. The body must be AND-only:Case, nested OR-branch lists, rawPred(...),RuleRef, and bare attribute refs are rejected withDSLToApplicationRuleError. Every named port variable must appear in the body, and an anonymousEntity(...)variable cannot be used as a port. See the rule-authoring reference for all atom forms and constraints.Evaluate the rule with
fg.eval.evaluate. Pass the rule as the body and as the closedhead=. Theengine=keyword is optional and resolves to"native"when omitted; pass"souffle","problog", or"pyreason"to use another engine (see Choosing a reasoning engine).result = fg.eval.evaluate(rule, head=rule)evaluatereturns anEvaluateResult. Index it (result[0]), iterate it, or useresult.count(),result.first(), andresult.exists().To verify:
result.count()reflects how many entities satisfy every atom in the body, including the implicitUser:existsatom. A row’s bindings are wrapped values, so the bound entity reference isresult[0].bindings["user"]["value"]. Because the SDK write path does not emit a:existsclaim oncreate, this body, which carries an existence atom, will reportresult.count()of0for entities created this way until aUser:existsclaim exists for them.Read the proof for a row by calling
row.explain(), which returns anExplanation. Checkexplanation.status("passed","failed","unsupported", or"invalid_request"); for a failed row readexplanation.failure_class. Render the proof with the.reprproperty (a tuple of lines, the structured proof tree) or with.narrate()(a tuple of lines, the prose account that uses the rule and atomreprtemplates). Both returnNonewhen the status is"unsupported"or"invalid_request".row = result[0] explanation = row.explain() assert explanation.status == "passed" print("\n".join(explanation.repr)) # structured proof tree print("\n".join(explanation.narrate())) # prose accountTo verify: with a
repr=template on the rule, the narrated lines render the bound entity, for exampleactive user alicerather than the raw encoded reference.To inspect individual atoms instead of the rendered text, walk the evidence:
explanation.evidence.pathsholds proof paths, eachpath.rulesholds head and body rules, and eachrule.atomsholds atoms with arepr_textand averdict. See the evaluation reference for theEvidenceGraphmodel.
Variations#
Replay a single closed row with
fg.eval.explain. Take a passing row, close it withrow.close()to get a closed-headRule, then callfg.eval.explain(rule, head=closed). This returns anExplanationdirectly. Thehead=must be closed (all ports bound); an open head raisesRuleExprError.closed = result[0].close() explanation = fg.eval.explain(rule, head=closed)Inspect a closed head that does not hold. When the closed head selects no matching facts,
fg.eval.explain(...)still returns anExplanationwithstatus == "failed"andfailure_class == "closed_head_false", carrying failed evidence you can render the same way.
See also: Evaluation and explanation, Rule authoring, Engines and semantics, About reasoning, evaluation, and evidence.