MAINT-13: a full pack that stops at 98-99 % is not a failed charge
checks / unit (pull_request) Successful in 34m21s
infra-pin / fresh (pull_request) Successful in 1s

A charge is complete at SoC >= target, or at >= target - 3 once the SoC has
made no new high for 60 min of allowed charging, or when the charging time
runs out at >= target - 3. The 36 h timeout counts only the minutes the price
gate let the pack charge (plus a 7 day wall-clock backstop). A cycle that
fails (stuck drain, charge below target - 3) backs off 7 days, tells the
operator once (EN + NL), and no cycle starts on that pack in that time unless
the operator forces one. State survives a restart; old state files load.

No option added. DECISIONS/MAINT-13-full-pack-is-complete.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-10-08 09:36:06 +02:00
co-authored by Claude Opus 5.5
parent b527e99f13
commit 28eff568b5
9 changed files with 776 additions and 16 deletions
+10 -3
View File
@@ -622,6 +622,8 @@ def flat_store(**over):
"phase_started": "2026-09-03T22:00:00+00:00",
"last_completed": "2026-08-07T12:00:00+00:00",
"last_start_attempt": "2026-09-03T10:00:00+00:00",
# MAINT-13: a failed cycle on record, so that key's move is graded too.
"last_failed": "2026-09-01T10:00:00+00:00",
}, **over))
@@ -929,10 +931,11 @@ print(f" ok={_rep8.ok} lost={len(_rep8.lost)} kept={len(_rep8.kept)} "
f"misdirected={len(_rep8.misdirected)}; marker "
f"{_m8.data.get(MAINT_SCHEMA_KEY, '<<absent>>')!r}")
check("PRECONDITION: this fixture reaches the marker through the `lost` guard "
"and ONLY that one - two keys in `kept`, two in `lost`, `misdirected` "
"and ONLY that one - two keys in `kept`, three in `lost` (the three the "
"fixture records; MAINT-13 added `last_failed`), `misdirected` "
"empty. If it ever stops having a non-empty `kept` the mutation below "
"goes invisible again, so the shape is asserted rather than assumed",
_rep8.ran and len(_rep8.kept) == 2 and len(_rep8.lost) == 2
_rep8.ran and len(_rep8.kept) == 2 and len(_rep8.lost) == 3
and not _rep8.misdirected and not _rep8.ok)
check("⚠️ THE MARKER IS NOT STAMPED ON A MIGRATION THAT LOST A MONTH OF "
"HISTORY, and the two keys that DID land do not buy it one. `rep.ok` is "
@@ -1010,10 +1013,14 @@ check("...and nothing about a deferral, a slot or another pack appears in any "
# pressure.
_solo2 = Maintenance(cfg, MemStore())
_solo2.force_start(t0)
# MAINT-13: a drain that times out is a failed cycle, so one plain line follows
# the abort line (the abort line itself is unchanged).
check("...and so is the abort, which is the string an operator reads under "
"pressure",
_solo2.tick(t0 + timedelta(hours=13), 60.0).events
== ["maintenance: ABORT (drain stuck at 60 % after 780 min)"])
== ["maintenance: ABORT (drain stuck at 60 % after 780 min)",
"maintenance: the cycle did not finish. It waits 7 days before it "
"tries again."])