hal2Bayes: explain the estimate, and let a rule be an AND - #44
Merged
Conversation
Tuning meant reading a probability and guessing what produced it. The snapshot carried the raw material — activeRules, terms and fsm — but not in a form anyone could use: rules appeared as internal ids, contributions were in log-odds while the whole editor speaks share-of-the-way in percent, the three fields had to be cross-read against each other, and only contributing rules appeared at all. The question while tuning is usually why a rule is *not* firing, and nothing in the snapshot answered it. The three fields are replaced by one `rules` array listing every configured rule, every time, with a status saying which case it is in: contributing, fading (with age and half-life), waiting (with step and deadline), armed, condition-false, no-value, or injected. Each entry carries what its source reads right now, so a false condition can be seen rather than inferred. Contributions are reported as shares of the way from the prior to the on-threshold — the unit the rule bars and the summary already use, so what you tuned is what you read back. Log-odds are additive, so the shares are too: the contributing ones sum to the snapshot's own share, which is pinned by a test because a report that does not add up is a report that lies. Labels are derived from the steps and phrased as the editor phrases them, so a rule reads the same in both places. The phrasing lives in a UMD module for the editor to adopt later; name resolution stays in the node, which is the only layer that can reach RED.nodes. The report is built in the same pass that computes the estimate, so a rule cannot be described as contributing unless its weight really went into the log-odds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"While it is 09:00–10:00 and the terrace is above 100 lux" could not be expressed. Anything with more than one step went to the sequence machinery, which is driven by edges — and a condition is not an event, so the first step never completed and the rule sat armed forever. The editor warned about it, but the only way out was to rewrite the rule as something that meant something else. A rule whose steps are all conditions is now continuous: one weight that applies while every condition holds at the same time. The estimator's isContinuous test grows from "a lone level check" to "every step is a level check", and the label module and the editor follow it, so such a rule reads "While A and B" in all three places. condition-false now carries failedStep. With one condition the status was the whole story; with several, which one broke it is the question. A rule that opens with a condition but waits for an event later is still unreachable — nothing completes that first step — and now says so as never-fires rather than sitting at armed looking merely idle. The editor's warning is narrowed to that case and names both ways out. Verified against the live rule that prompted this: it contributes at 35.2 % inside the window with the lux above the threshold, and reports which of the two conditions failed in each of the other three combinations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In an AND every condition is judged at the same instant, so there is no previous step to be "soon" after — "now or soon" and "now" are the same thing there, and the estimator already treats them identically. Later steps in such a rule now offer only "now", which also removes the state that prompted this: a step reading "now or soon" with no window field, because the window had been hidden as inapplicable while the qualifier stayed on offer. The first step decides the kind of rule and the rest follow from it. A step that is already a condition is narrowed silently-but-announced, since the behaviour is identical either way. An event after a leading condition is left alone: that is a broken rule rather than an AND, and coercing it would quietly turn "then the door opens" into "and the door is open" — the warning on the rule is what should speak there, not a rewrite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… one Choosing "> 100" left the value on the default string type, where the comparison only worked through JavaScript's coercion rules rather than because the field said what it was. It now switches to num for <, <=, > and >= — but only where nothing is lost by it: the field must still be on its default string type and hold either nothing or something that already reads as a number. Text typed on purpose, or a flow/msg reference, is a decision and stays untouched. Every type remains on offer, so this is a default and not a lock. hal2Event and hal2Gate already restrict those operators to num outright; this is the softer form, which is what the node's freer type list calls for. The decision lives in resources/hal.js so it is testable without an editor, and is pinned there — including that it does not read "Infinity" or "10 lux" as numbers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follows hal2Event and hal2Gate instead of inventing a third behaviour: a comparison that can only mean a number offers only num, regex offers only re, and everything else keeps the node's full list. That closed a hole rather than just tidying one. hal2Bayes has a regex operator but never forced the matching type, and COMPARE.regex calls b.test — so a rule saved as regex-against-a-string threw a TypeError out of evaluate() and took the whole evaluation with it, every other rule included. conditionMatches now guards the comparison as well as the conversion: a condition that cannot be evaluated has not matched, and its neighbours still run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The README claimed a snapshot "on every evaluation", which reads as every tick. It follows sensor reports and result flips; the tick produces one only when Snapshot every tick is enabled, which is where to look if you want to watch evidence decay between reports. Also worth stating, since the wording invited the opposite conclusion: decay does not depend on that setting. Contributions are computed from timestamps at evaluation, so the value is right whenever the node looks — only the reporting is on demand. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Tuning a Bayes node meant reading a probability and guessing what produced it. The snapshot on
output 2 already carried the raw material —
activeRules,termsandfsm— but not in a formanyone could use: rules appeared as internal ids, contributions were in log-odds while the whole
editor speaks share of the way to on in percent, the three fields had to be cross-read against
each other, and only contributing rules appeared at all. The question while tuning is usually
why a rule is not firing, and nothing in the snapshot answered it.
One
rulesarrayThe three fields are replaced by one array listing every configured rule, every time, with a
status saying which case it is in.
{ "p": 0.86, "logOdds": 1.86, "share": 108.4, "binary": true, "held": false, "rules": [ { "label": "While Hall Rörelse · Motion is true", "status": "contributing", "share": 73.8, "logOdds": 2.303, "value": true }, { "label": "When Ytterdörr · Contact is true on a full cycle and Hall Rörelse · Motion is true", "status": "waiting", "step": 2, "steps": 2, "deadline": 47 }, { "label": "While Kontor Sensor · Temperature > 25", "status": "condition-false", "value": 23.4 } ] }contributing,fading(with age and half-life),waiting(with step and deadline),armed,condition-false(withfailedStep),no-value,injected,never-fires. Each entry carrieswhat its source reads right now, so a false condition can be seen rather than inferred.
Contributions are reported as shares of the way from the prior to the on-threshold — the unit
the rule bars and the summary already use, so what you tuned is what you read back. Log-odds are
additive, so the shares are too: the contributing ones sum to the snapshot's own
share, pinned bya test, because a report that does not add up is a report that lies.
The report is built in the same pass that computes the estimate, so a rule cannot be described as
contributing unless its weight really went into the log-odds.
Labels are derived from the steps and phrased as the editor phrases them. The phrasing lives in a
UMD module (
resources/bayes-label.js) so the editor can adopt it later; name resolution stays inthe node, the only layer that can reach
RED.nodes.A rule of only conditions is an AND
Found by using the report: a live rule sat at
armedforever, and asking why exposed a real gap."While it is 09:00–10:00 and the terrace is above 100 lux" could not be expressed. Anything
with more than one step went to the sequence machinery, which is driven by edges — and a condition
is not an event, so the first step never completed. The editor warned about it, but the only way
out was to rewrite the rule as something that meant something else.
A rule whose steps are all conditions is now continuous: one weight that applies while every
condition holds at the same time.
isContinuousgrows from "a lone level check" to "every step isa level check", and the label module and the editor follow, so such a rule reads "While A and B" in
all three places. A rule that opens with a condition but waits for an event later is still
unreachable and now says so as
never-firesrather than sitting atarmedlooking merely idle.The editor stops offering what such a step cannot use: in an AND there is no previous step to be
"soon" after, so later steps offer only "now". A step that is already a condition is narrowed with
a notice, since the behaviour is identical either way; an event after a leading condition is left
alone — that is a broken rule rather than an AND, and coercing it would quietly turn "then the door
opens" into "and the door is open".
A crash on the way out
hal2Bayes has a
regexoperator but never forced the matching value type, andCOMPARE.regexcalls
b.test— so a rule saved as regex-against-a-string threw aTypeErrorout ofevaluate()and took the whole evaluation with it, every other rule included. Reachable from the editor until
now.
The value types are narrowed to what each operator can use, following what hal2Event and hal2Gate
already did rather than inventing a third behaviour. And
conditionMatchesnow guards thecomparison as well as the conversion: a condition that cannot be evaluated has not matched, and its
neighbours still run.
Testing
249 passing. The new ones cover every status, that every configured rule appears exactly once in
every state, that the shares sum to the total, the AND semantics at each edge,
never-fires, andthe regex crash.
Verified against the running instance: labels rendered from the live configuration, the rule that
prompted this simulated through all four combinations of its two conditions, and
waitingwith acounting-down deadline plus
fadingdecaying per tick observed on a real node.