Backward chaining silently fails any rule condition that compares two facts, while forward chaining handles the same condition correctly.
Evidence
Through vrules_core::prove (which delegates to rust_rule_engine::backward::{BackwardEngine, GRLQueryParser, GRLQueryExecutor}):
| knowledge base |
result |
when Order.Amount <= Account.Balance |
not provable, 0 proof steps |
when Order.Amount <= 100.0 |
provable, 1 proof step |
Facts {"Order.Amount": 50, "Account.Balance": 100, "Account.AgeDays": 45} in both cases.
Forward chaining, same shape, is correct in all four quadrants (Order.amount > Order.limit):
| amount |
limit |
engine |
truth |
| 100 |
50 |
true |
true |
| 50 |
100 |
false |
false |
| 1 |
999 |
false |
false |
| 999 |
1 |
true |
true |
Cause
src/backward/rule_executor.rs delegates to engine::condition_evaluator::ConditionEvaluator, whose evaluate_condition compares the resolved left field against condition.value verbatim:
Ok(condition.operator.evaluate(&value, &condition.value))
The right-hand operand is never resolved against facts, so Account.Balance is compared as the literal string "Account.Balance". The forward engine has its own path in src/engine/engine.rs including resolve_value_facts(val, facts), which is exactly the resolution the backward path lacks.
This is an asymmetry in upstream rust-rule-engine, not something vrules removed: condition_evaluator.rs and the whole backward/ tree are byte-identical to rust-rule-engine.pre-purge-backup-20260802-130744.
Blast radius
ConditionEvaluator is referenced only from src/backward/ (rule_executor.rs, search.rs), so a fix is contained to backward chaining.
Impact
The Proof browser example ships a knowledge base whose first rule uses Order.Amount <= Account.Balance, so it renders NOT PROVABLE with an empty proof tree, and has been doing so on the published site.
Coverage gap
crates/vrules-core/tests/conformance_backward.rs (43 tests) contains no fact-to-fact comparison at all, which is why this was never caught.
Suggested fix
Resolve condition.value against facts before comparing, mirroring resolve_value_facts, and add backward conformance coverage for fact-to-fact in all four comparison quadrants.
Backward chaining silently fails any rule condition that compares two facts, while forward chaining handles the same condition correctly.
Evidence
Through
vrules_core::prove(which delegates torust_rule_engine::backward::{BackwardEngine, GRLQueryParser, GRLQueryExecutor}):when Order.Amount <= Account.Balancewhen Order.Amount <= 100.0Facts
{"Order.Amount": 50, "Account.Balance": 100, "Account.AgeDays": 45}in both cases.Forward chaining, same shape, is correct in all four quadrants (
Order.amount > Order.limit):Cause
src/backward/rule_executor.rsdelegates toengine::condition_evaluator::ConditionEvaluator, whoseevaluate_conditioncompares the resolved left field againstcondition.valueverbatim:The right-hand operand is never resolved against facts, so
Account.Balanceis compared as the literal string"Account.Balance". The forward engine has its own path insrc/engine/engine.rsincludingresolve_value_facts(val, facts), which is exactly the resolution the backward path lacks.This is an asymmetry in upstream rust-rule-engine, not something vrules removed:
condition_evaluator.rsand the wholebackward/tree are byte-identical torust-rule-engine.pre-purge-backup-20260802-130744.Blast radius
ConditionEvaluatoris referenced only fromsrc/backward/(rule_executor.rs,search.rs), so a fix is contained to backward chaining.Impact
The Proof browser example ships a knowledge base whose first rule uses
Order.Amount <= Account.Balance, so it renders NOT PROVABLE with an empty proof tree, and has been doing so on the published site.Coverage gap
crates/vrules-core/tests/conformance_backward.rs(43 tests) contains no fact-to-fact comparison at all, which is why this was never caught.Suggested fix
Resolve
condition.valueagainst facts before comparing, mirroringresolve_value_facts, and add backward conformance coverage for fact-to-fact in all four comparison quadrants.