Skip to content

Match Vode statuses and rule keys exactly, not by prefix - #94

Merged
cemde merged 1 commit into
liukidar:mainfrom
cemde:fix/69-vode-prefix-matching
Aug 11, 2026
Merged

Match Vode statuses and rule keys exactly, not by prefix#94
cemde merged 1 commit into
liukidar:mainfrom
cemde:fix/69-vode-prefix-matching

Conversation

@cemde

@cemde cemde commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Bug

Two regexes in _vode.py were anchored only at their start, so both matched by prefix where they meant to match the whole string.

Ruleset.filter used re.match(pattern, status). The built-in ruleset registers its forward-init rules under the pattern "init", so re.match accepted any status beginning with init.

Vode.set built its rule pattern as f"...<-\s*({key}.*)". The trailing .* means the key group matched any rule whose right-hand side merely starts with the key.

Impact

A user who names a phase initialise or init_weights silently gets the h, u <- u rule, which copies the prediction into the node value. The prediction error, and therefore the energy, is identically zero for that whole phase.

For keys the damage is doubled. A node with two incoming activations, u from the layer above and u2 from a skip connection, has u2's rule fire with u's value. And because a rule matched, the len(rules) == 0 fallback never runs, so u is never stored under its own name at all. With ruleset ("z <- u2",), set("u", 7.0) left cache["z"] == 7.0 and cache["u"] unset.

Fix

Both matches become re.fullmatch, and the key group gains an explicit optional tail for the :transformation suffix:

if re.fullmatch(_pattern, status) is None:      # was re.match
...
if _match := re.fullmatch(rule_pattern, _rule):  # was re.match
...
_rule_pattern = f"(.*(?<!\\s))\\s*<-\\s*({key}(?::.*)?)"   # was ({key}.*)

fullmatch restores the documented meaning of the patterns as regexes: .* is the way to say "any status", which was meaningless while every pattern carried an implicit one. (?::.*)? is non-capturing, so _match.group(1, 2) still yields the same targets and transformation.

Why not a trailing $

$ also matches immediately before a trailing newline, so it is strictly weaker than fullmatch. It would also leave Ruleset.filter applying whole-string matching to its status argument and prefix matching to its rule argument, with the anchoring obligation pushed into an undocumented caller convention that Vode.get does not follow.

Closes #69

Ruleset.filter matched a status with re.match, so the pattern "init" also
fired for "initialise"; Vode.set interpolated the key as "{key}.*", so
set("u", ...) fired a rule written for "u2" and then skipped the fallback
that stores "u" itself.
@cemde
cemde force-pushed the fix/69-vode-prefix-matching branch from e909bd8 to 45a1eda Compare August 9, 2026 18:23
@cemde
cemde requested a review from liukidar August 9, 2026 18:34

@liukidar liukidar left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@cemde
cemde merged commit d5ceb0f into liukidar:main Aug 11, 2026
31 of 33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vode matches statuses and rule keys by prefix instead of exactly

2 participants