SAFETY-25: record the owner's decision - option a, with alloc-01 as the revisit trigger
checks / unit (pull_request) Successful in 3m43s
checks / unit (pull_request) Successful in 3m43s
The product owner answered SAFETY-25's §1 on 2026-09-02: OPTION A. The BMS cut-out is the DELIBERATE and only backstop for demand-driven discharge on the grid-following loop, and Option B - a loop-side reserve floor - is DEFERRED rather than rejected. Option A is the shipped behaviour, so this is a recording, not a change. `app/` is touched in exactly two places and both are comments that claimed the question was still open (freeze.py's finding, peakguard.py's point 2). No setpoint, no claim, no option, no default moved. The 17-suite run is green on the same numbers as before. WHY DEFERRING IS THE SAFE DIRECTION. Option B's proof - bms_soc_floor < loop_soc_floor <= peak_soc_floor, run through the real arbiter in test_peakguard.py part 10d - has an unstated premise: the reserve predicate and discharge_permitted() read the SAME state of charge. With two packs they read different numbers. The reserve binds on the emptier pack and files hi = 0; the peak guard, reading the other pack or an average, files lo = excess > 0; lo > hi is arbiter clause 3 and the answer is 0 W - the battery parked in the billed quarter, which is the failure the memo itself marks fatal. And no config validator can catch it: it is a runtime divergence between two readings, not a config ordering, so CFG-01's shape of check passes on the exact tree that parks the battery. Option A's cost is energy; Option B's cost built too early is the one quarter of the month that sets the bill. THE REVISIT TRIGGER IS ALLOC-01, recorded in §0c. Under ARCH-01's host-side allocator the two claims stop sharing a list: the loop floor becomes a per-inverter `hi` consumed as an INTERVAL at stage A, while the peak floor stays a site `lo` at stage B. Different claim lists, different stages, structurally unable to cross - a stronger argument than the ordering rule, because it makes the crossing unreachable rather than harmless. That is the shape in which Option B becomes safe to build. WHAT HAS TO BE RE-EARNED AT N=2 is stated plainly in §0d rather than left for someone to discover. SAFETY-27 now names five floors across two packs, so "the SoC" stops being a scalar the config can bound. And part 10f - the pin that made SAFETY-25 mergeable - CANNOT EXPRESS TWO PACKS: its dynamic half is a scalar 0-100 % SoC sweep against a single-series capture, and a two-pack exposure lives in the DIFFERENCE between two readings, which that sweep visits at no resolution. Its static half, the AST scan pinning the eight claim sources, still bites at N=2; the dynamic half goes silent. Merging a two-pack reserve on 10f's current green would be merging on a check that cannot see the failure. The options analysis is kept intact on purpose - §1-§7 stand exactly as written, including §5's "(recommended)" on Option B and §6's recommendation, with a new §0 saying what was decided and §6 marked as answered. A memo that recorded the verdict and deleted the reasoning is the failure this DECISIONS/ directory exists to prevent. Part 10e's check moved with the memo: it used to pin the honest "Status: OPEN" line and now pins that the memo names the option, the date and the trigger AND still carries both options and the recommendation. The tripwire itself stays - a ninth claim source in Controller.cycle() still takes 10f red on the line it is written, and "deferred" is precisely the state in which someone builds Option B by accident. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1c59cfeaf5
commit
ab6fbe2f9e
@@ -2,6 +2,64 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
**SAFETY-25's open question is ANSWERED: the BMS cut-out is the DELIBERATE and
|
||||
only backstop for demand-driven discharge, and a loop-side reserve floor is
|
||||
DEFERRED to `ALLOC-01` rather than rejected (SAFETY-25).** Decided by the
|
||||
product owner on 2026-09-02. **Option A is the shipped behaviour, so `app/`
|
||||
changed only where a comment claimed the question was still open** - no
|
||||
setpoint, no claim, no option, no default moved, and the 17-suite run is green
|
||||
on the same numbers as before.
|
||||
|
||||
* **Why deferring is the safe direction, and it is a property of the shape
|
||||
rather than a preference.** Option B's proof - `bms_soc_floor <
|
||||
loop_soc_floor <= peak_soc_floor`, verified through the real arbiter in
|
||||
`test_peakguard.py` part 10d - has an unstated premise: the reserve predicate
|
||||
and `discharge_permitted()` read the **same** state of charge. With two packs
|
||||
they read different numbers. The reserve binds on the emptier pack and files
|
||||
`hi = 0` while the peak guard, reading the other pack or an average, files
|
||||
`lo = excess > 0`; `lo > hi` is arbiter clause 3 and the answer is **0 W, the
|
||||
battery parked in the billed quarter** - the failure the memo itself marks
|
||||
fatal. **No config-ordering validator can catch it**, because it is a runtime
|
||||
divergence between two readings and not a config ordering, so CFG-01's shape
|
||||
of check would pass on the exact tree that parks the battery. Option A's cost
|
||||
is energy; Option B's cost built too early is a parked battery in the one
|
||||
quarter of the month that sets the bill.
|
||||
* **The revisit trigger is recorded explicitly, and it is `ALLOC-01`.** Under
|
||||
`ARCH-01`'s host-side allocator the two claims stop sharing a list: the loop
|
||||
floor becomes a **per-inverter `hi` consumed as an interval at stage A**,
|
||||
while the peak floor stays a **site `lo` at stage B**. Different claim lists,
|
||||
different stages, **structurally unable to cross** - which is a stronger
|
||||
argument than the ordering rule, because it makes the crossing unreachable
|
||||
rather than harmless. That is the shape in which Option B becomes safe to
|
||||
build, and the memo says so at the point a future reader will look.
|
||||
* **What will have to be RE-EARNED at N=2 is stated plainly rather than left
|
||||
for someone to discover.** `SAFETY-27` now names **five floors across two
|
||||
packs**, so "the SoC" stops being a scalar the config can bound. And
|
||||
`test_peakguard.py` **part 10f - the pin that made SAFETY-25 mergeable** -
|
||||
**cannot express two packs**: its dynamic half is a scalar 0-100 % SoC sweep
|
||||
against a single-series capture, and a two-pack exposure lives in the
|
||||
*difference* between two readings, which that sweep visits at no resolution.
|
||||
Its static half (the AST scan pinning the eight claim sources) still bites at
|
||||
N=2; the dynamic half goes silent. Merging a two-pack reserve on 10f's
|
||||
current green would be merging on a check that cannot see the failure.
|
||||
* **The options analysis is kept intact on purpose.** A memo that recorded the
|
||||
verdict and deleted the reasoning is the failure mode this `DECISIONS/`
|
||||
directory exists to prevent, so §1-§7 stand exactly as written - including
|
||||
§5's *"(recommended)"* on Option B and §6's recommendation - with the new §0
|
||||
saying what was decided and §6 marked as answered. Part 10e's check moved
|
||||
with the memo: it used to pin the honest `Status: OPEN` line and now pins
|
||||
that the memo names the option, the date and the trigger **and still carries
|
||||
both options and the recommendation**.
|
||||
* **The tripwire stays, and "deferred" is why it stays.** Part 10 and mutation
|
||||
13 are unchanged in substance: a ninth claim source in `Controller.cycle()`
|
||||
still takes 10f red on the line it is written. Deferred, not rejected, is
|
||||
precisely the state in which someone builds Option B by accident.
|
||||
|
||||
Operator-facing surfaces follow the decision and nothing else changed on them:
|
||||
`DOCS.md`'s SAFETY-25 section now says a choice was made and points at the
|
||||
trigger, and `app/freeze.py`'s finding and `app/peakguard.py`'s point 2 say
|
||||
"decided, on purpose" where they said "open".
|
||||
|
||||
**`PeakStore.record_block()` now has its POLICY ARGUMENT graded, and the
|
||||
SAFETY-20 import check now looks at all three ways of writing an import
|
||||
(TEST-13).** Two survivors from TARIFF-01's review, both closed in
|
||||
|
||||
@@ -1,20 +1,156 @@
|
||||
# SAFETY-25 — does the grid-following loop get a reserve floor?
|
||||
|
||||
**Status: OPEN. This is a memo, not a decision.** It sets out the options, what
|
||||
each costs, what each protects, and a recommendation. **The choice belongs to the
|
||||
product owner**, because it decides whether this house's battery is allowed to
|
||||
discharge to its BMS cut-out on a sustained-load evening, and that moves real
|
||||
energy.
|
||||
**Status: DECIDED 2026-09-02 — Option A.** The product owner has answered §1:
|
||||
**the BMS cut-out stays the only backstop for demand-driven discharge on the
|
||||
grid-following loop.** **Option B — a loop-side reserve floor — is DEFERRED**,
|
||||
not rejected, with an explicit revisit trigger recorded in §0c: **`ALLOC-01`**.
|
||||
|
||||
Filed with SAFETY-25. The other half of that ticket — making `peak_soc_floor`'s
|
||||
scope honest in the config, the docs and on the status page — **shipped in the
|
||||
same PR** and did not wait on this, because it is required whichever way the
|
||||
answer goes.
|
||||
Option A **is the shipped behaviour**, so recording this decision changed no
|
||||
code. The honesty half of SAFETY-25 — `peak_soc_floor`'s scope made true in the
|
||||
config, in `DOCS.md` and on the status page — had already shipped and was
|
||||
required under every option. What was missing was the answer, and the answer is
|
||||
now here rather than in a Trello card that scrolls away.
|
||||
|
||||
**Everything from §1 down is the memo exactly as it was written before the
|
||||
decision, and is deliberately left intact.** It is the record of *why*, and a
|
||||
future reader arriving at the revisit trigger needs the options analysis, the
|
||||
costs, the measured replay and the trap in Option B's obvious shape — not a
|
||||
one-line verdict. §5 still labels Option B *"(recommended)"* and §6 still says
|
||||
"Option B is what I would build"; those were the engineering recommendation put
|
||||
to the owner, they are preserved as written, and **§0 is what was decided.**
|
||||
|
||||
Filed with SAFETY-25. Card 6a94b2b98b73123d520e69f2. The other half of that
|
||||
ticket — making `peak_soc_floor`'s scope honest in the config, the docs and on
|
||||
the status page — **shipped in SAFETY-25's own PR** and did not wait on this,
|
||||
because it was required whichever way the answer went.
|
||||
|
||||
*This directory exists so a recorded decision has somewhere to live that is not a
|
||||
comment in a source file nobody reads and not a Trello card that scrolls away.
|
||||
`SAFETY-27` asks for a second one (the naming of the floors); it belongs here
|
||||
too.*
|
||||
too, and §0d says what N=2 does to it.*
|
||||
|
||||
---
|
||||
|
||||
## 0. The decision
|
||||
|
||||
### 0a. What was decided, and the owner's reasoning
|
||||
|
||||
**Option A. The BMS remains the only backstop for demand-driven discharge on the
|
||||
grid-following loop. Option B is deferred until `ALLOC-01` exists.**
|
||||
|
||||
The reasoning is not "Option B is wrong" — §5 and §6 still stand as engineering.
|
||||
It is that **deferring is the safe direction, and building now is not.**
|
||||
|
||||
Option B's safety proof is the ordering constraint in §5:
|
||||
|
||||
bms_soc_floor < loop_soc_floor <= peak_soc_floor
|
||||
|
||||
That proof has an unstated premise: **the reserve predicate and
|
||||
`discharge_permitted()` must read the SAME state of charge.** At N=1 they do,
|
||||
which is why the proof reads as airtight, and why part 10d can run it through
|
||||
the real arbiter and watch the contradiction disappear.
|
||||
|
||||
**With two packs they read different numbers.** The reserve binds on whichever
|
||||
pack is emptier and files `hi = 0`; the peak guard, reading the other pack or an
|
||||
average of the two, is *not* floor-limited and files `lo = excess > 0`. Then
|
||||
`lo > hi`, arbiter clause 3 fires, and the result is **0 W — the battery parked,
|
||||
in the billed quarter.** That is precisely the failure §5 marks *"⚠️ fatal if
|
||||
missed"*, arriving by a route the ordering rule cannot close, because
|
||||
`loop_soc_floor <= peak_soc_floor` is still perfectly satisfied while it happens.
|
||||
|
||||
**And no config-ordering validator can catch it.** The rule §5 proposes — refuse
|
||||
the bad ordering at startup, in CFG-01's manner — grades *configuration*. This is
|
||||
a **runtime divergence between two readings**, not a config ordering. The startup
|
||||
check would pass, every time, on the exact tree that parks the battery.
|
||||
|
||||
So the shape of the risk decides the direction: **Option A's cost is energy
|
||||
(§5, "every watt below the reserve becomes billed import" — inverted: the pack
|
||||
may sit on its own cut-out). Option B's cost, built too early, is a parked
|
||||
battery in the one quarter of the month that sets the bill.** One of those is a
|
||||
tariff line; the other is the failure this memo already calls fatal. Take the
|
||||
one that cannot park the battery, and build Option B when its proof holds again.
|
||||
|
||||
### 0b. What this decision does NOT say
|
||||
|
||||
- It does **not** say a loop-side reserve is a bad idea. §5's protection
|
||||
argument — keeping the pack off its own cut-out, which is the `MAINT-01` drift
|
||||
condition — is unchanged and still wanted.
|
||||
- It does **not** close §4's fifth-key question. `SAFETY-27` still owns the
|
||||
naming of the floors, and any future `loop_soc_floor` still lands after it.
|
||||
- It does **not** weaken §6 item 3: **the tripwire in `test_peakguard.py`
|
||||
part 10 stays.** A deferred option needs its tripwire more than an open one
|
||||
does, because "deferred" is exactly the state in which someone builds it by
|
||||
accident. Part 10f still takes red on the line a ninth claim source is
|
||||
written. Do not remove it.
|
||||
|
||||
### 0c. The revisit trigger: `ALLOC-01`
|
||||
|
||||
**`ALLOC-01` is the trigger, and it is a trigger because it changes the SHAPE of
|
||||
the two claims, not because it changes anyone's mind.**
|
||||
|
||||
`ARCH-01` decided that the site grid target is **allocated host-side, above the
|
||||
driver seam**: one integrator on P1, a sum-invariant split into per-inverter
|
||||
shares, and each inverter told a function of **its own share** only. It also
|
||||
decided that **per-inverter constraints are consumed by the allocator as
|
||||
INTERVALS, never filed as site-level claims** — which is the structural fix for
|
||||
the cross-inverter `lo > hi -> 0 W` contradiction.
|
||||
|
||||
Under the allocator, the two claims that collide in §0a stop sharing a claim
|
||||
list at all:
|
||||
|
||||
| | today (N=1, no allocator) | under `ALLOC-01` |
|
||||
|---|---|---|
|
||||
| the loop reserve | a site `hi` at `P_LOOP` | a **per-inverter `hi`**, consumed at **stage A** as that unit's interval |
|
||||
| the peak floor | a site `lo` at `P_PEAK` | still a **site `lo`**, filed at **stage B** against the site target |
|
||||
|
||||
**Different claim lists, evaluated at different stages, structurally unable to
|
||||
cross.** The reserve bounds one inverter's share; the peak floor bounds the site
|
||||
sum. There is no arbiter resolution in which one is the other's `lo` and `hi`,
|
||||
so §0a's failure cannot be constructed — not "is guarded against", *cannot be
|
||||
constructed*. That is the same class of argument as §5's ordering rule but a
|
||||
stronger one: §5 makes the crossing harmless, `ALLOC-01` makes it unreachable.
|
||||
|
||||
**That is the shape in which Option B becomes safe to build.** When `ALLOC-01`
|
||||
exists, reopen this memo at §5 Option B, restate the reserve as a stage-A
|
||||
per-inverter interval, and the ordering rule becomes an implementation
|
||||
convenience rather than the load-bearing proof.
|
||||
|
||||
### 0d. What has to be RE-EARNED at N=2, stated plainly
|
||||
|
||||
Two things this decision leans on do not survive a second pack, and saying so is
|
||||
part of the decision rather than a footnote to it.
|
||||
|
||||
**1. `SAFETY-27` now names FIVE floors across TWO packs.** §4's table already
|
||||
carries four keys plus "the loop"; two of them ship the same default of 20 from
|
||||
different cards. At N=2 each of those is a floor *per pack*, and "the SoC" stops
|
||||
being a scalar the config can bound — which is the same divergence that makes
|
||||
§0a's contradiction possible. `SAFETY-27` inherits a harder question than the
|
||||
one it was filed with.
|
||||
|
||||
**2. `test_peakguard.py` part 10f — the guarantee that made SAFETY-25 mergeable —
|
||||
CANNOT EXPRESS TWO PACKS, and will not cover the two-pack case.** Say it flatly,
|
||||
because it is the load-bearing pin (§6 item 3: PR #31 shipped this section
|
||||
claiming a tripwire it did not have, and the reviewer disproved the claim by
|
||||
building Option B under a green suite):
|
||||
|
||||
- 10f's dynamic half is a **scalar 0–100 % SoC sweep** — one number swept over
|
||||
one range, in the idle and floor-limited states — against a **single-series
|
||||
capture** (§7: the fixtures carry signed net power at the meter and nothing
|
||||
else; the pack is modelled, and one pack is modelled).
|
||||
- A two-pack exposure lives in the **difference** between two SoC readings. A
|
||||
sweep over one scalar visits no point of that space, at any resolution. It
|
||||
cannot fail on the divergence because it cannot represent it.
|
||||
- 10f's **static** half — the AST scan pinning the eight claim sources in
|
||||
`Controller.cycle()` — survives, and is the half that keeps working: a ninth
|
||||
source still goes red on the line it is written, at N=2 as at N=1. It is the
|
||||
*dynamic* half, the one that proves the named sources actually reach the
|
||||
arbiter across the whole state space, that goes silent.
|
||||
|
||||
So: **10f remains a valid tripwire against building Option B today, and is not a
|
||||
guarantee about N=2.** Whoever reaches the `ALLOC-01` revisit inherits the job of
|
||||
re-earning it — a two-pack fixture with two SoC series, or a claim-shape argument
|
||||
at stage A that does not need one. Merging a two-pack reserve on 10f's current
|
||||
green is merging on a check that cannot see the failure.
|
||||
|
||||
---
|
||||
|
||||
@@ -216,6 +352,11 @@ around.
|
||||
|
||||
## 6. Recommendation
|
||||
|
||||
⚠️ **This section is the recommendation that was PUT to the owner, kept as
|
||||
written. It was answered on 2026-09-02 — see §0. The owner took Option A and
|
||||
deferred Option B to `ALLOC-01`; item 3 below (keep the part 10 tripwire)
|
||||
was taken as written and still stands.**
|
||||
|
||||
**Option A is honest and cheap; Option B is what I would build.** Concretely:
|
||||
|
||||
1. **Now (shipped with SAFETY-25):** the honesty half. `peak_soc_floor`, its
|
||||
|
||||
@@ -619,10 +619,16 @@ rail. What it means is that the *depth* your pack reaches on a heavy evening is
|
||||
set by your house, not by any number in this file, and the BMS is the only thing
|
||||
underneath it.
|
||||
|
||||
**Whether it should stay that way is an open product decision**, recorded with its
|
||||
options, costs and a recommendation in
|
||||
[`DECISIONS/SAFETY-25-loop-soc-floor.md`](DECISIONS/SAFETY-25-loop-soc-floor.md).
|
||||
Until it is answered, this section is the honest statement of what you have.
|
||||
**This is now a recorded decision, not an open question.** On **2026-09-02** the
|
||||
product owner took **Option A**: the BMS cut-out is **deliberately** the only
|
||||
backstop for demand-driven discharge, and a loop-side reserve floor is
|
||||
**deferred** rather than rejected. The options, their costs, the measured replay
|
||||
and the reasoning are in
|
||||
[`DECISIONS/SAFETY-25-loop-soc-floor.md`](DECISIONS/SAFETY-25-loop-soc-floor.md),
|
||||
together with the revisit trigger (`ALLOC-01`, the host-side share allocator)
|
||||
under which a reserve becomes safe to build. **Nothing about the behaviour above
|
||||
changed** — Option A is what has always shipped. What changed is that this
|
||||
section now describes a choice somebody made, and you can read why.
|
||||
|
||||
### The winding-loop alarm (the output clamp)
|
||||
|
||||
|
||||
@@ -23,8 +23,11 @@ THE FAILURE NEEDS NO FAULT ANYWHERE ELSE. A frozen-but-talking meter is
|
||||
"there is one SoC floor and it is maintenance's" - but neither of the new
|
||||
ones binds THIS loop, and `peak_soc_floor`'s name reads as though it does.
|
||||
SAFETY-25 replayed the exposure (test_peakguard.py part 10) and recorded
|
||||
the options in DECISIONS/SAFETY-25-loop-soc-floor.md; whether the loop gets
|
||||
a reserve of its own is an open product decision, not an oversight.
|
||||
the options in DECISIONS/SAFETY-25-loop-soc-floor.md. DECIDED 2026-09-02,
|
||||
Option A: the BMS is the sole energy backstop for demand-driven discharge
|
||||
ON PURPOSE. A loop-side reserve is DEFERRED, not rejected - its revisit
|
||||
trigger is ALLOC-01. So the sentence above is a recorded decision, not an
|
||||
oversight and no longer an open question.
|
||||
|
||||
The loop reads a constant error and integrates it. Time from freeze to the rail,
|
||||
replaying the real `control.compute()` at 15 s cycles:
|
||||
|
||||
@@ -300,7 +300,10 @@ the one place the question is answered. Four things about the shape of it:
|
||||
PEAK GUARD FLOOR-LIMITED while the arbiter grants the loop up to 2000 W of
|
||||
discharge in every one of them. test_peakguard.py part 10 is the replay;
|
||||
DECISIONS/SAFETY-25-loop-soc-floor.md carries the options and the
|
||||
recommendation, and the decision itself is the product owner's and is open.
|
||||
recommendation, and the product owner DECIDED it on 2026-09-02: Option A,
|
||||
the BMS stays the only backstop for demand-driven discharge, with a
|
||||
loop-side reserve DEFERRED to ALLOC-01. This floor's scope is therefore
|
||||
settled rather than provisional - it bounds the peak guard, on purpose.
|
||||
|
||||
3. IT IS GRADED ONLY WHERE IT IS NOT DEGENERATE. The SoC question is asked
|
||||
AFTER `wanted > 0` is established - i.e. only where a forced discharge is
|
||||
|
||||
@@ -1877,7 +1877,8 @@ print("10. SAFETY-25: the loop discharges past this floor, and how far past")
|
||||
# point. The exposure must not be able to change under a green suite; it has
|
||||
# to be closed deliberately, and closing it means answering AC 2 and
|
||||
# deleting or inverting these checks along with
|
||||
# DECISIONS/SAFETY-25-loop-soc-floor.md's open status.
|
||||
# DECISIONS/SAFETY-25-loop-soc-floor.md's status - OPEN when this was
|
||||
# written, DECIDED (Option A, deferred B) since 2026-09-02.
|
||||
#
|
||||
# ⚠️ AND IT IS 10f THAT MAKES THAT SENTENCE TRUE, NOT 10a-10c. The replay
|
||||
# below hand-builds main.py's claim list, so on its own it is blind to a
|
||||
@@ -2131,8 +2132,11 @@ check("THE ASYMMETRY, ON REAL DATA. On every cycle where the guard reports "
|
||||
and HOT_R["max_granted"] > 0.0)
|
||||
check("...which is why SAFETY-24's floor could not be widened into this one, "
|
||||
"and why the fix is not a bigger number. Recorded in "
|
||||
"DECISIONS/SAFETY-25-loop-soc-floor.md; AC 2 is the product owner's and "
|
||||
"is open", HOT_R["min_soc"] <= BMS_FLOOR + 0.05)
|
||||
"DECISIONS/SAFETY-25-loop-soc-floor.md, where AC 2 was DECIDED on "
|
||||
"2026-09-02: Option A, the BMS stays the only backstop, and the "
|
||||
"loop-side reserve is DEFERRED to ALLOC-01 - so this replay is not a "
|
||||
"pending exposure but the RECORDED behaviour, and part 10 stays as its "
|
||||
"tripwire", HOT_R["min_soc"] <= BMS_FLOOR + 0.05)
|
||||
|
||||
# --- 10d. the trap in the obvious fix, checked rather than asserted ---------
|
||||
# ⚠️ THE MEMO'S CENTRAL ENGINEERING CLAIM, RUN THROUGH THE REAL ARBITER. The
|
||||
@@ -2182,12 +2186,30 @@ check("...and DOCS.md says it in the options table AND in the section, with the
|
||||
"not a pack-wide reserve" in _docs_src
|
||||
and "IT BOUNDS THE PEAK GUARD ONLY" in _docs_src
|
||||
and "SAFETY-25" in _docs_src)
|
||||
check("...and the decision memo AC 1 asks for is COMMITTED, carries the options "
|
||||
"and a recommendation, and is honest that the decision is not taken",
|
||||
# ⚠️ THE DECISION LANDED (2026-09-02, Option A) AND THIS CHECK MOVED WITH IT.
|
||||
# It used to pin "Status: OPEN. This is a memo, not a decision." - the memo being
|
||||
# HONEST that AC 2 was unanswered. AC 2 is answered now, so the same check pins
|
||||
# the same property in its new form: the memo states WHICH option, WHEN, and its
|
||||
# REVISIT TRIGGER, and it still carries the options and the recommendation
|
||||
# UNCHANGED. A memo that recorded the verdict and deleted the reasoning would be
|
||||
# the failure this is guarding against, which is why "Recommendation" and both
|
||||
# rejected options are still asserted here rather than only the status line.
|
||||
_memo_txt = _memo.read_text(encoding="utf-8")
|
||||
check("...and the decision memo AC 1 asks for is COMMITTED, records AC 2 as "
|
||||
"DECIDED with a date, an option and a revisit trigger, and STILL carries "
|
||||
"the options analysis and the recommendation that were put to the owner",
|
||||
_memo.exists()
|
||||
and "Status: OPEN. This is a memo, not a decision." in _memo.read_text(
|
||||
encoding="utf-8")
|
||||
and "Recommendation" in _memo.read_text(encoding="utf-8"))
|
||||
and "Status: DECIDED 2026-09-02 — Option A." in _memo_txt
|
||||
and "ALLOC-01" in _memo_txt
|
||||
and "Recommendation" in _memo_txt
|
||||
and "### Option A" in _memo_txt and "### Option B" in _memo_txt)
|
||||
check("...and it says plainly that part 10f - the pin that made SAFETY-25 "
|
||||
"mergeable - is a SCALAR SoC sweep against a single-series capture and "
|
||||
"so cannot express two packs. The tripwire's own limits belong in the "
|
||||
"memo that leans on it, or the next reader merges a two-pack reserve on "
|
||||
"a green check that cannot see the failure",
|
||||
"10f" in _memo_txt and "SAFETY-27" in _memo_txt
|
||||
and "cannot express two packs" in _memo_txt.lower())
|
||||
check("...and DOCS.md sends the reader to it rather than leaving the open "
|
||||
"question implicit",
|
||||
"DECISIONS/SAFETY-25-loop-soc-floor.md" in _docs_src)
|
||||
@@ -2288,8 +2310,12 @@ check("...and so does the persistent notification, which outlives the page and "
|
||||
# The whole inventory of claim sources main.py can file, in any configuration.
|
||||
# ⚠️ ADDING TO THIS SET IS A DELIBERATE ACT. If you are here because you added a
|
||||
# claim to `Controller.cycle()` and this went red, that is the tripwire working:
|
||||
# read DECISIONS/SAFETY-25-loop-soc-floor.md, answer AC 2 in it, and update
|
||||
# part 10 and mutation 13 rather than this line alone.
|
||||
# read DECISIONS/SAFETY-25-loop-soc-floor.md and update part 10 and mutation 13
|
||||
# rather than this line alone. ⚠️ AC 2 IS DECIDED (2026-09-02, Option A) AND
|
||||
# THAT DOES NOT RETIRE THIS SET - Option B is DEFERRED, not rejected, and
|
||||
# "deferred" is exactly the state in which a ninth source gets built by
|
||||
# accident. Its revisit trigger is ALLOC-01, under which the reserve becomes a
|
||||
# PER-INVERTER interval at the allocator rather than a ninth claim here.
|
||||
CLAIM_SOURCES = {"device", "supervised", "failsafe", "safety", "peak",
|
||||
"arbitrage", "maintenance", "loop"}
|
||||
# The two appends that file a claim BUILT ELSEWHERE in the method. Named here so
|
||||
|
||||
Reference in New Issue
Block a user