The consumer picks the group's function, not the group - #46
Merged
Conversation
One configured function had to serve every reader. Building the MCP tools showed why that is the wrong shape: get_groups lets an assistant ask for any function over the same members, and the moment it could, the configured one stopped looking like the group's value and started looking like an arbitrary default. Any true asks "is a lamp on?", all true asks "did the command to turn them all on work?" — the same members, two questions, one of them unreachable from a flow. Two things in the code decided how far this could go. The engine emits on every member update rather than only on a change of value, and hal2Event compares state against laststate in the payload rather than against anything the engine holds. So a consumer can take the wake-up and compute its own value with its own function: the engine never needs a value per function. It says something moved, and each node decides what that means to it. Value, Gate and Bayes read by pull and needed a parameter. Event needed to keep its own previous value, persisted, to do its own change detection — the one node that gains state, and the one where the payoff is clearest: two Event nodes can now watch the same group for different things. The group's Value field is gone. The default is derived from the HAType, which is what it always should have been — a fallback rather than a decision. Boolean groups default to any true because it is honest standing still: false means everything is off and nothing else, where all true would report 38 of 39 lamps as false. A HAType with no natural summary gets no default at all rather than a meaningless "latest", and the reading nodes must then each pick. The editor's function list is derived from the HAType rather than from live values, because a temperature item will not start reporting booleans and the editor cannot see runtime state anyway. A saved function that no longer fits is kept in the list with a warning — the rule the group HAType select already follows, so a node can never be trapped by a group whose members changed under it. At runtime it degrades to undefined, which every consumer already treats as no value. Also: halGetGroups' 'value' filter now asks whether a group has state-capable members rather than whether someone configured a function, since nobody does that any more. Verified live: all ten groups now report a derived value, the seven light groups included, with live correctly below members where devices are offline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A picker of fourteen entries each reading "average — the mean of all values" is a wall of text you scan past. The label is now the short name alone, and what it means appears once, under the picker, for the one actually chosen — using the info line each of the four nodes already had. FUNCTIONS keeps the two apart as `t` and `d`, with a test that the label never swallows its description again. The line also covers the case that had no wording before: with no function chosen it says which one the group falls back to and what that one means, or that this HAType has no default at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Bayes selector snapped back to its saved value on every pick: its change handler ran the same function that refills the options, and refilling sets the select to the value it was passed. Filling now happens only when the group changes; the function select's own change updates the tip and nothing else. The other three nodes never had the snap-back, but they did refill from the originally saved value, so changing group threw away a function just chosen. All four now refill from the current selection when there is one. "(group default: average)" did not fit the field. It is "(group default)" now, and an unfit saved function reads "average (unfit)" — the tip line under the picker says which default applies, what it means, and why a stray no longer fits, so none of that has to survive inside a dropdown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both pairs already sat in an identically sized wrapper; only the division inside differed — thing/item share it 5:3 and group/function shared it evenly, so a group step's columns did not line up with a thing step's above it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The only entry that still overflowed the narrower field. The tip line under the picker already names which function the default is and what it means, so the option itself only has to say that it is one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It was lost with the Value row. Restored, and widened to answer the question the tab could not: what this group can actually do. Members split into how many can be read and how many can be commanded — separate sets, either of which can be empty, which is the one thing about groups that surprises people and which the counts now make plain at a glance (Lampor Maja: 4 readable, 2 commandable). It also names what the group reports by itself, and follows the HAType select, so changing the type shows immediately what the group would then say. Deliberately per-group only: that every reader can choose its own function is a rule about groups rather than about this one, and repeating it on every row is noise once read. The tab intro carries it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The summary line was wired to the HAType select above the point where its own handler is assigned. syncSummary is a var, so the call hit undefined and threw out of the editableList's addItem — which does not fail one row, it fails the list, so the Groups tab came up empty. Wired below the assignment instead. A function declaration would also have hoisted past it, but the surrounding rows are all var-assigned closures and one odd one out is worse than moving two lines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two gaps, both from the same root: only the default function had a record, so everything else had to answer with undefined or with something derived. Deriving last_change is not possible. We hold each member's current last_change, not its history, and for min, max and any-true-when-true a member can change without moving the value at all. On the nine-sensor indoor group right now, the minimum has been steady since 06:50 while the newest member change is 07:16 — a derived answer would be 26 minutes wrong, and Gate's last_change_gte would act on it silently. So the engine now keeps a record per function the group's HAType can serve — seven or eight, not fourteen, and bounded either way. last_change and last_update are real for every reader, including the one-off ones from MCP, and no registration API was needed to get there. The live handler shows it working: min last changed at 09:27:42 and latest at 09:28:34, which is the distinction the whole thing exists for. The cost is a context file of 30 kB instead of 1. Alongside it, two different questions that had been conflated. `source` names the member the value came from, for latest, min and max where one member owns it; a mean belongs to nobody and carries none rather than an arbitrary one. `last_changed_by` names the member that last moved the value. They coincide only for latest — for min, the member that reported most recently is rarely the one holding the minimum, and reporting that as the source would read as "Maja is the coldest room" when it is the laundry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It reported the group's default — anyTrue whatever the node was set to — because the engine's emission describes the default and the node was overriding only the value, not the block that describes it. Every field in that block belongs to a function, not just the name: live and the timestamps come from the default's record, and source names the member that owns the default's value. The block is rebuilt from this node's own read instead, and source and last_changed_by are dropped rather than inherited when the chosen function has none — allTrue is owned by no member, so carrying the default's source would have pointed at one anyway. The payload was right throughout, which is why this looked cosmetic: the node computed and compared its own value correctly and only described it wrongly. 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.
A group was configured with one aggregation function, and that single choice had to serve every
reader. Building the MCP tools showed why that is the wrong shape:
get_groupslets an assistantask for any function over the same members, and the moment it could, the configured one stopped
looking like the group's value and started looking like an arbitrary default. Any true asks "is a
lamp on?", all true asks "did the command to turn them all on work?" — the same members, two
questions, and only one of them reachable from a flow.
The engine never needed a value per function
Two things in the code decided how far this could go:
hal2Eventcomparesstateagainstlaststatein the payload, not against anything theengine holds.
So a consumer can take the wake-up and compute its own value with its own function. The emission is
a wake-up first and a value second. Value, Gate and Bayes read by pull and needed a parameter;
Event needed to keep its own previous value, persisted, to do its own change detection — the one
node that gains state, and the one where the payoff shows: two Event nodes can now watch the same
group for different things.
The Value field is gone
A group's default is derived from its HAType — a temperature group averages, a light or switch
group answers any true. There is nothing to configure, and a HAType with no natural summary
(Other, the modes, colours) gets no default rather than a meaningless latest.
Boolean groups default to any true because it is honest standing still:
falsemeans everythingis off and nothing else, where all true would report 38 of 39 lamps as
false— which inget_all_statesreads as "all dark".Saved
aggregatevalues are ignored with no migration: hal2 is published at 2.4.2, so none of thegroup-value model has ever shipped.
Every function is tracked, not just the default
last_changecannot be derived. We hold each member's currentlast_change, not its history,and for
min,maxand any-true-when-true a member can change without moving the value at all. Onthe live nine-sensor indoor group, the minimum had been steady since 06:50 while the newest member
change was 07:16 — a derived answer would have been 26 minutes wrong, and
Gate'slast_change_gtewould have acted on it silently.So the engine keeps a record per function the group's HAType can serve — seven or eight, bounded
either way. Timestamps are real for every reader, including the one-off ones from MCP, and no
registration API was needed. The live handler shows the distinction the whole thing exists for:
minlast changed at 09:27:42,latestat 09:28:34. The cost is a context file of 30 kB ratherthan 1 kB.
Whose value it is, and who moved it
Two questions that had been conflated, and coincide only for
latest:source— the member the value came from, forlatest,minandmaxwhere one memberowns it. "The coldest room is the laundry" becomes one read. A mean belongs to nobody and carries
no
sourcerather than an arbitrary one.last_changed_by— the member that last moved the value. Formin, the member that reportedmost recently is rarely the one holding the minimum; reporting that as the source would have read
as "Maja is the coldest room" when it is the laundry.
Editor
A function selector beside each group picker in all four nodes, filtered by HAType rather than by
live values — the editor cannot see runtime state, and a temperature item will not start reporting
booleans. A saved function that no longer fits is kept in the list with a warning, the rule the
group HAType select already follows, so a node can never be trapped by a group whose members
changed under it. At runtime it degrades to
undefined, which every consumer already treats as novalue.
The Groups tab loses the Value row and gains a summary line saying what MCP says: how many members
can be read, how many can be commanded — separate sets, either of which can be empty, which is the
one thing about groups that surprises people.
Testing
302 passing. The new ones cover
defaultFunction(including that it answers null where nodefault is warranted),
functionsForHaType,valueSource(including that it names nobody where nomember owns the value), and the provenance fields.
Verified live throughout: all ten groups report a derived value, per-function records carry
genuinely different
last_changevalues,minsources to the laundry sensor, and the payoff test— two Event nodes on one group, one on any true and one on all true — behaves as intended.