Skip to content

A rule can conclude a computed value, not only a constant #488

Description

@WaylandYang

Today a rule's conclusion is a constant. attribute_rules.conclude_value is a JSONB literal, and Conclusion::Attribute { predicate, value } copies it straight into derived_facts.object_value. So grade = A can be written; margin = revenue − cost cannot. A condition's operand is a constant too — you can ask revenue > 100, not revenue − cost > 0.

There are real cases for computing. This issue is about letting an operand — on either side of a rule — be an expression over the subject's own attributes.

The shape: a picked tree, not a typed string

A class/attribute picker plus an operator picker, storing a small structured tree. Not a formula text box.

A text box is not wrong because it would break the display — a string parsed at save time into the same tree keeps every property we care about (the rule still renders as a sentence, still draws as one edge, two rules still diff). It is wrong for two other reasons:

  • Errors move from unfillable to undiscovered. A picker can only produce a well-formed expression against predicates that exist. A box lets you save revenue − cost on a class that has no cost, and the symptom is a conclusion that quietly does not appear at the next materialisation.
  • A grammar grows on request. Once there is a box, if, rounding, coalesce and sum(related.x) are each one line of asking. The first few are merely work. Aggregation actually breaks the model: the premises of a sum are a set whose membership changes, and every derived fact here points at the readings that made it true and retires when they change (the same reason 0026 refused an arbitrary and/or tree).

The concession worth writing down: a picker is right for one operator and wrong for four. (revenue − cost) / revenue as nested dropdowns is worse UI than typing it. If the shapes people actually want turn out to be that deep, a box that parses into the same tree is the answer, and nothing above forbids it.

Storage: a JSONB node — {"attr": <predicate_id>} | {"const": <n>} | {"op": "add"|"sub"|"mul"|"div", "l": <node>, "r": <node>} — with a depth cap. The same node type serves a condition operand and a conclusion value.

What it changes

  • Premises and interval: nothing new. An expression reads N attribute facts; all N are premises and the conclusion holds on their intersection, which is what validity() already computes for a conjunction of conditions. Retirement works unchanged.
  • Row count is the real cost. A constant conclusion collapses many readings into one row. A computed one produces a different value per combination of readings, each on its own interval — two revenue readings and three cost readings are six conclusions. MAX_COMBOS (64) today bounds premise expansion; with computed values it bounds output, and needs measuring against a real base before this ships.
  • Units and datatype have to be checked, and today nothing checks them. relation_types.unit and .datatype exist. revenue (USD) − cost (EUR) must be refused when it is written, not silently summed. The result's type has to match the concluded predicate's.
  • Absence and division by zero produce no conclusion, consistent with 0026: a missing reading is not a zero, it is nobody having written it down. Division by zero is reported the way capped is — "something was not concluded here" must never look like "the criterion was not met".

Not in scope

Aggregation over related entities (sum, count, avg across a to-many relation). See above: its premises are a set that can change size, so nothing can retire the conclusion correctly. If it is ever wanted it needs its own decision record answering what its premise is and when it retires.

Open

  • Does the picker reach another entity's attribute? A single-valued traversal (well → field → depth across a functional relation) has a definite premise — the relation fact plus the attribute fact — and a definite interval, so it fits. A to-many traversal is aggregation and does not. If the class half of "class / attribute picker" means only "narrow the attribute list by class", this question does not arise.
  • Conditions too, or only the conclusion? The machinery is the same; doing only one leaves an obvious asymmetry.
  • Follows 0021 and A rule can only say “and” #476/A rule cannot read what another rule concluded #477. Wants its own decision record before code.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions