v0.43.1 — Precondition parity and a guard that was not guarding
A follow-up to v0.43.0, closing the three issues its own review raised. No new features; no change to anything a correctly-formed contract does.
Fixed
attribute_change validates operator and arity, like the sibling it mirrors (#72)
It is public, accepts an arbitrary MetricDefinition, and its own comments state it cannot assume the object came through validate_decompositions at load — but it re-validated only the convention. reconcile_decomposition validates operator and arity up front for exactly that reason, before running a single query.
Malformed decompositions therefore surfaced as interpreter errors from inside the arithmetic rather than the ValueError the module commits to:
| Input | Before | After |
|---|---|---|
ratio, 1 operand |
IndexError: list index out of range |
ValueError: operator 'ratio' requires exactly 2 operands, got 1 |
ratio / difference, 3 operands |
too many values to unpack (expected 2, got 3) |
ValueError: ... requires exactly 2 operands, got 3 |
product, 0 operands |
ZeroDivisionError: division by zero |
ValueError: ... requires at least 2 operands, got 0 |
The IndexError came from the zero-denominator guard indexing operands[1], so the new block sits before it. Unreachable for any contract-loaded metric — the loader rejects all four shapes — but the asymmetry between two kernels with the same stated precondition posture is the kind that gets copied into a third.
Internal
The pinned round-trip digest now covers metric serialization (#73)
_PINNED_ROUNDTRIP_DIGEST is the repo's broadest guard against canonical-bytes drift — one changed byte anywhere fails it, with no test author needing to have anticipated the field. It was computed over a semantic source with no metrics: key at all, so _dump_metric was never called.
That is worse than a missing test, because the assertion passed either way. Every optional field added to _dump_metric since the pin was written could have been made unconditional — moving the digest of every real contract and invalidating every published ARD attestation — with the test still green. Verified by mutation: removing the if m.decompositions: guard now fails the pin, and before this change that mutation was unreachable.
Convention-default helpers moved to semantic/base.py (#74)
OssieSource was importing two private functions across from yaml_source.py, while every other shared semantic helper lives in base.py. Both operate purely on MetricDefinition / Decomposition, and both had to import VALID_CONVENTIONS and _CROSS_TERM_OPERATORS back out of base.py — constants that had no other consumer in yaml_source.py. After the move that file needs neither.
Upgrading
Nothing to do. No public API changed, and a contract that loads today produces the same canonical bytes and the same digest it did on 0.43.0.