Skip to content

role_facts refuses a scale with a numerator or denominator <= 0; a value at a scaled bound is within it, tested - #28

Merged
neu-rah merged 2 commits into
mainfrom
scale-positive-exact-bound
Oct 8, 2026
Merged

neu-rah merged 2 commits into
mainfrom
scale-positive-exact-bound

Conversation

@neu-rah

@neu-rah neu-rah commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Two follow-ups to #27, one commit each.

1. A scale with a numerator or denominator ≤ 0 is refused, for analog and scaled alike

role_facts (python/onemachine/machine.py:338) now refuses scale 0/1023, -5/1023, 5/-1023 and the like, naming the role: role vin (analog): scale -5/1023 is not positive. #27 kept the check for scaled only, so an analog role with such a scale was accepted by both consumers; the check now sits right after the scale and unit lines and covers both kinds. A denominator of 0 was already refused by the description itself.

test/role/facts_check.py:86-92: numerator 0, negative numerator, negative denominator on an analog; negative numerator and denominator on a scaled; denominator 0.

2. A value exactly at the presented bound of a scaled role is within it

A value must be compared with a scaled role's presented bound as the decimal the client wrote, not as the double's binary value (Fraction(v)): with a bound of 1.1 (max 11, scale 1/10) the double 1.1 is a hair above 11/10, so a set of exactly the maximum would be refused as "above the maximum 1.1". The server in main compares the decimal as written, but nothing in the tests would catch a regression, and the conversion was written out in two places.

  • Facts.written(value) (python/onemachine/machine.py:265): a number as the client wrote it, exactly (the shortest decimal that round-trips the double, as a Fraction). to_raw (:269) already did this inline and now calls it; examples/mcp/server.py:127 compares through it.
  • test/role/facts_check.py:101-117, for scaled bounds 1.1, 0.4 and 0.9 (the double is above the decimal) and 0.3 (a control: below): presented(high) is exact; the double is above/below the bound in binary; written(x) equals the bound, so the value is within it; the next double above is above it; and through examples/mcp's own Role.refuse: duty_set x is accepted with the maximum raw value, duty_set of the next double is refused naming the maximum.

The cases fail on a patched copy (ran)

  • Facts.written changed to Fraction(float(value)) (sed): FAIL to_raw: nearest integer of the decimal as written … [(12.35, 123, 124), (0.15, 1, 2)], FAIL bound 1.1: written(1.1) is the bound …, FAIL examples/mcp, bound 1.1: duty_set 1.1 is accepted, raw 11 (None, 'value: 1.1 is above the maximum 1.1'), and the same for 0.4, 0.9, plus the 0.3 control's written case: 9 FAIL.
  • Only the server's line changed back to Fraction(v): the three consumer cases fail (duty_set 1.1, 0.4, 0.9 refused as "above the maximum"), the shared cases pass.
  • The positivity check deleted (sed): the 0/10 case of examples/mcp: the examples/sila machine served to AI assistants; the kind facts move into python/onemachine #27 and the five new cases fail (6 FAIL).

Verification (ran)

  • facts_check.py: 76 checks (46 before), all pass.
  • The eight feature definitions the SiLA gateway generates for examples/sila and the MCP server's tools/list and instructions are byte-identical to the ones of main.
  • On a Nano (ATmega328P, examples/sila firmware): the MCP server through an independent client (the MCP Inspector, a raw JSON-RPC client, the link trace): 66 checks, 67 on the host build, all held; the SiLA duty oracle (independent SiLA 2 client) passes 13 checks with the specified rounding and fails SetValue(0.05) with a round() copy of Facts.to_raw.
  • test/role/build.sh locally: 34 ok, 1 failed: role_link_avr (AVR image 6984 B against the 6950 B baseline), which fails the same on an untouched main here (local AVR toolchain); CI decides.

Behaviour that changes

An analog role whose scale has a numerator or denominator ≤ 0 is refused by both examples (the SiLA gateway and the MCP server) at start, naming the role. #27 had made both accept it.

🤖 Generated with Claude Code

neu-rah and others added 2 commits October 8, 2026 23:00
…ominator <= 0

presented = raw * num / den. A scale that is zero or negative in either part
has no meaning for an analog or a scaled value, and for a scaled one it
reverses its bounds. The check was only made for a scaled role; it is now made
for both kinds, right after the scale and unit lines, and names the role.
A denominator of 0 is still refused by the description itself. facts_check.py
has a case for each: numerator 0, negative numerator, negative denominator, on
an analog and on a scaled role.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…amples/mcp compares bounds through it

A value is compared with a presented bound as the decimal the client wrote,
not as the double's binary value: 1.1 is 11/10, but the double 1.1 is a hair
above it, so a set of exactly the maximum of a scaled value whose maximum is
1.1 (or 0.4, or 0.9) would be refused as "above the maximum 1.1".
Facts.written holds that conversion (to_raw already used it); examples/mcp
takes its bound comparison from it.

facts_check.py: for scaled bounds 1.1, 0.4, 0.9 (the double is above the bound)
and 0.3 (a control, below) it checks that written(value) is the bound, that the
next double above is above it, and, through examples/mcp's own Role.refuse,
that duty_set of exactly the bound is accepted and of the next double refused.
It fails on a copy whose written() compares the double's binary value, and, for
the consumer cases alone, on a copy of the server that does.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@neu-rah
neu-rah merged commit 9f04ebe into main Oct 8, 2026
10 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.

1 participant