A rule computes what it concludes - #495
Merged
Merged
Conversation
WaylandYang
force-pushed
the
feat/a-rule-computes
branch
from
September 9, 2026 02:20
365649f to
300ef2e
Compare
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
WaylandYang
force-pushed
the
feat/a-rule-computes-cut2
branch
from
September 9, 2026 02:26
b90b24a to
f68bede
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cut 2 of #488, on top of the record (#493). Stacked on
feat/a-rule-computes.Exprinutopia-reason—Attr(predicate) | Const(f64) | Arith { op, l, r }, depth capped byMAX_EXPR_DEPTH.evalreturnsOption<f64>and returnsNonefor a reading that is not there, a value that is not a number, division by zero, and any non-finite result. Never zero: a missing reading is nobody having written it down, and filling that in with a definite answer is the one thing this ledger must not do.A third conclusion kind,
computed(migration0041), with the tree inattribute_rules.conclude_exprand the shape CHECK extended to three branches. The two existing branches are untouched, and a{"const": …}is what every rule stores today, so nothing migrates.An operand may be an expression on the comparison ops, so a threshold can be computed from another reading:
revenue > cost × 1.5. No column for it —operandis already JSONB, and the four operand shapes are distinguishable (a number, a two-element array, a string array, an object).The evaluator now builds the combination before testing the conditions. It has to: a computed threshold reads whichever readings this round picked, so a condition can no longer be decided against one fact in isolation. Slots are one per condition (unchanged — two conditions on the same attribute still pick independently) plus one per predicate an expression reads that no condition covers; an expression referring to a predicate a condition already has binds to that same slot, so
thc > thc × 0.5means one reading on both sides. Every attribute the expression touched is a premise, which is what makes a computed conclusion able to say why it holds and to retire when any of those readings changes.Dedupe is now by
(interval, value)rather than interval alone — two different computed values on one interval are two rows, not one.A malformed tree is refused where it is written.
validate_exprchecks the shape, the depth, that every attribute exists in the base and is an attribute predicate, and that the tree reads at least one attribute (an expression that reads none is a constant, and there is already a branch for that). This is deliberate and it is the lesson of #494: a tree the compiler cannot read would otherwise mean "this rule silently concludes nothing" with nothing anywhere saying why.Tests. Seven in the evaluator — the readings an expression touched become premises; each combination computes its own value; a missing reading computes nothing; division by zero computes nothing; a computed threshold works and its reading is a premise; two values on one interval are two hits; depth counts the deepest branch. Three against a real database — the round trip through
createlands 180 with both readings as premises and then followscostdown to 250 on the next run; a missing reading lands nothing; four kinds of broken expression are refused at write time.cargo fmt --check,cargo clippy --workspace --all-targets -- -D warningsand the full suite (72 test binaries) are clean.Not in this cut: the picker (an expression is reachable through the API, not through the page) and reaching an attribute across a relation, which the record decides and does not build — the evaluator reads only the literal channel today, and wiring the entity–entity channel into it is the bulk of that work.
🤖 Generated with Claude Code