MAINT-04 review: pin the default divergence with a check, not a comment
validate.py's comment said "if config.yaml's value moves, move this and main.py's together" and nothing enforced it - a comment where a check belonged, in a PR that elsewhere proves it knows the difference. test_validate.py now reads main.py and maintenance.py directly and asserts the divergence EXACTLY AS IT STANDS: config.yaml/validate.py at 2000, main.py's two fallbacks and MaintConfig's dataclass default at 2500. It records the mismatch rather than demanding it be closed, so it passes today and goes red if either side moves - including when somebody closes it in main.py, at which point the check and validate.py's block are deleted together. Nothing in a file another ticket owns is touched. The consequence is asserted through the real code as well as the literals: with the key absent the validator accepts, MaintConfig builds 2500 W against a 2000 W ceiling, and the arbiter resolves it to 0 W as a contradiction - the bug rule 6 exists to catch, passing rule 6. Production-unreachable (the Supervisor always supplies every key) but fully reachable from sim/inject.py and the tests, which is where anyone debugging maintenance works. Also: a CHANGELOG section for the upgrade path, covering both the case where the Supervisor re-derives the default and the case where it persisted the old one, since which applies was not verifiable without probing the live install. And maintenance.py's timeout comment said 13.5 kWh where everything else says 13.8 (300 Ah x 51.2 V, DoD 90); SAFETY-18 does not touch that file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015VXjA3LWccN1rJsk3okyPC
This commit is contained in:
co-authored by
Claude Opus 5
parent
878a6ce452
commit
ffb8ec6373
@@ -253,6 +253,74 @@ check("...and the shipped charge rate is at or under the shipped ceiling",
|
||||
check("...and the shipped discharge rate too, so it warns about nothing",
|
||||
_shipped["maintenance_discharge_w"] <= _shipped["max_w"])
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print("the KNOWN cross-file default divergence, pinned rather than commented")
|
||||
# ⚠️ WHAT THIS GUARDS, AND WHY IT ASSERTS THE BUG RATHER THAN THE FIX.
|
||||
# config.yaml and validate.py now default maintenance_charge_w to 2000, while
|
||||
# main.py's two `opts.get(..., 2500)` fallbacks and MaintConfig's dataclass
|
||||
# default still say 2500. So an options dict with the key ABSENT is ACCEPTED by
|
||||
# rule 6 at 2000 and then built by main.py at 2500 - which is a contradiction:
|
||||
#
|
||||
# validate_startup_config -> [] (accepted)
|
||||
# MaintConfig().charge_w -> 2500.0
|
||||
# arbiter -> 0.0 W, contradiction=True
|
||||
#
|
||||
# That is the bug rule 6 exists to catch, passing rule 6. It is unreachable on a
|
||||
# Supervisor install (every key is always supplied from config.yaml) but fully
|
||||
# reachable from sim/inject.py and from tests - which is exactly where anyone
|
||||
# debugging maintenance works.
|
||||
#
|
||||
# The fix is ONE SOURCE OF TRUTH for option defaults. That is a bigger change
|
||||
# than this ticket and it lands in main.py, which another ticket is live in - so
|
||||
# the divergence is RECORDED here instead of closed. The check therefore passes
|
||||
# on the mismatch and FAILS when either side moves, in EITHER direction:
|
||||
#
|
||||
# - config.yaml moves -> FAIL: re-decide what the fallback should be
|
||||
# - main.py is fixed to match -> FAIL: the divergence is gone, DELETE this
|
||||
# check and the ⚠️ block in validate.py
|
||||
#
|
||||
# A comment cannot do that, and "a comment where a check belonged" is this
|
||||
# repo's most repeated defect.
|
||||
_APP = os.path.join(os.path.dirname(os.path.abspath(__file__)), "app")
|
||||
|
||||
def _read(*parts):
|
||||
with open(os.path.join(*parts), encoding="utf-8") as fh:
|
||||
return fh.read()
|
||||
|
||||
# Every literal fallback for this key in main.py, whatever the call shape.
|
||||
_main_falls = [float(m) for m in re.findall(
|
||||
r'\.get\(\s*"maintenance_charge_w"\s*,\s*(-?\d+(?:\.\d+)?)\s*\)',
|
||||
_read(_APP, "main.py"))]
|
||||
# ...and MaintConfig's own dataclass default, which is the one that bites when
|
||||
# main.py is bypassed entirely (a Maintenance built straight from defaults).
|
||||
_dc_falls = [float(m) for m in re.findall(
|
||||
r"^\s*charge_w:\s*float\s*=\s*(-?\d+(?:\.\d+)?)\s*$",
|
||||
_read(_APP, "maintenance.py"), re.M)]
|
||||
|
||||
check(f"main.py's fallbacks are still found and still 2500 {_main_falls}",
|
||||
len(_main_falls) == 2 and set(_main_falls) == {2500.0})
|
||||
check(f"MaintConfig's default is still found and still 2500 {_dc_falls}",
|
||||
len(_dc_falls) == 1 and set(_dc_falls) == {2500.0})
|
||||
check(f"...and config.yaml still ships 2000, so the divergence is UNCHANGED "
|
||||
f"(shipped {_shipped['maintenance_charge_w']:.0f}, code 2500)",
|
||||
_shipped["maintenance_charge_w"] == 2000.0)
|
||||
|
||||
# ⚠️ And the consequence itself, through the REAL code rather than through the
|
||||
# numbers above - so that if someone "fixes" this by changing a literal without
|
||||
# understanding it, the thing that goes red is the behaviour, not the transcript.
|
||||
from app.maintenance import MaintConfig # noqa: E402
|
||||
|
||||
_absent = {k: v for k, v in base().items() if k != "maintenance_charge_w"}
|
||||
_built = MaintConfig().charge_w
|
||||
check("with the key ABSENT the validator accepts...",
|
||||
validate_startup_config(_absent, -5000, 5000) == [])
|
||||
check(f"...while the code would build {_built:.0f} W against a "
|
||||
f"{_absent['max_w']:.0f} W ceiling...", _built > _absent["max_w"])
|
||||
_r = _charge(_absent["max_w"], _built)
|
||||
check("...which the arbiter resolves to 0 W as a contradiction - the hole, "
|
||||
"asserted so it cannot widen unnoticed",
|
||||
_r.target_w == 0.0 and _r.contradiction)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print("max_w must be a positive magnitude")
|
||||
# Schema-blocked on a real install, but validate_startup_config() is also
|
||||
|
||||
Reference in New Issue
Block a user