CFG-04 (#45) merged first and moved the base. Two conflicts, both resolved as the union of the two sides: - app/main.py: an import-block collision. CFG-04 added validate_unit_ranges to the .validate import; SAFETY-30 added AGAINST_METER, AGAINST_OWN_POWER, SIGN_OPTION_DEFAULTS and SignWatchdog to the .watchdog import. Neither side dropped anything, so the resolution keeps both lists whole. - CHANGELOG.md: two adjacent new entries under ## Unreleased. Both kept. Both sides also independently rewrote large parts of test_peakguard.py (60 and 47 insertions against the merge base) and git merged them textually, so the full suite was run on the MERGE RESULT rather than inferred from the two parents: 19 passed, 0 failed, twice. One caveat recorded rather than hidden: test_peakguard.py failed once during the first sequential full-suite run and then passed 10/10 in isolation and in two further full runs on this same tree. It passes on both parents too, so this is the pre-existing flakiness CFG-04 already disclosed, not a merge defect - it is timing- or load-sensitive. It sits on the required CI context and can therefore block an unrelated merge at random, which wants a card of its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
@@ -178,20 +178,65 @@ def fresh_store(name: str) -> PeakStore:
|
||||
return PeakStore(str(TMP / f"{name}.db"))
|
||||
|
||||
|
||||
def read_option(key: str):
|
||||
"""The shipped default for `key`, out of config.yaml.
|
||||
# ⚠️⚠️ THESE THREE READERS WERE CHANGED BY CFG-04, AND THE OLD ONES WERE THE
|
||||
# PERMISSIVE KIND. Read this before touching them.
|
||||
#
|
||||
# WHAT THEY USED TO DO. `read_option()` walked the whole file and took the FIRST
|
||||
# line whose STRIPPED text started with `<key>:`; `_schema_bound()` did the same
|
||||
# from `schema:` onwards. The docstring justified first-match by naming the one
|
||||
# collision that existed - `peak_cap_w:` appears once under `options:` and once
|
||||
# under `schema:` - and it was right about that one.
|
||||
#
|
||||
# WHY THAT STOPPED BEING SAFE. CFG-04's `inverters:` is a LIST OF OBJECTS, and
|
||||
# its entries carry a `max_w:` of their own, indented six spaces under
|
||||
# `schema:`. `line.strip()` throws the indentation away, so `read_schema_max
|
||||
# ("max_w")` began answering with the PER-INVERTER bound instead of the site
|
||||
# one - silently, and in this file's case correctly, because the two bounds
|
||||
# happen to be the same `int(100,5000)` today. A reader that returns inverter
|
||||
# #1's number and keeps passing is the failure mode this repository is named
|
||||
# after: green, and not watching. config.yaml declares `inverters:` BEFORE
|
||||
# `max_w:` deliberately so that the trap stays baited rather than sorted out by
|
||||
# an ordering nobody would maintain.
|
||||
#
|
||||
# WHAT THEY DO NOW. A top-level option and a top-level schema key are at EXACTLY
|
||||
# two spaces; anything nested is at four or more. So the anchor is the
|
||||
# indentation, the file's two halves disambiguate `peak_cap_w` (the options half
|
||||
# for a default, the schema half for a bound) and first-match is not relied on
|
||||
# at all. And where a key really is ambiguous - the same key twice at top level
|
||||
# in the same half - these RAISE instead of picking one, because a reader that
|
||||
# guesses is how this got here.
|
||||
#
|
||||
# test_inverter.py part 12 runs all of them against a synthetic TWO-INVERTER
|
||||
# config.yaml whose nested numbers differ from its flat ones, which is the only
|
||||
# way to tell a reader that is anchored from one that is lucky.
|
||||
def _cfg_halves():
|
||||
body = (HERE / "config.yaml").read_text(encoding="utf-8")
|
||||
marker = body.index("\nschema:")
|
||||
return body[:marker], body[marker:]
|
||||
|
||||
⚠️ `peak_cap_w:` appears TWICE - once under `options:`, once under `schema:`.
|
||||
test_tariff.py's part 9 documents the trap; this takes the FIRST match, which
|
||||
is the options block, i.e. the number an install actually lands on.
|
||||
"""
|
||||
for line in (HERE / "config.yaml").read_text(encoding="utf-8").splitlines():
|
||||
if line.strip().startswith(f"{key}:"):
|
||||
try:
|
||||
return float(line.split(":", 1)[1].strip())
|
||||
except ValueError:
|
||||
continue
|
||||
return None
|
||||
|
||||
def _top_level(half: str, key: str) -> list:
|
||||
"""Every ` key: ...` line at EXACTLY two spaces. Raises if there are two."""
|
||||
hits = [ln for ln in half.splitlines()
|
||||
if ln.startswith(f" {key}:") and not ln[2:3].isspace()]
|
||||
if len(hits) > 1:
|
||||
raise AssertionError(
|
||||
f"config.yaml declares {key!r} {len(hits)} times at the top level "
|
||||
f"of one block: {hits!r}. This reader will not pick one for you - "
|
||||
"an option with two defaults is a question for whoever wrote the "
|
||||
"second, not a tie for a check to break.")
|
||||
return hits
|
||||
|
||||
|
||||
def read_option(key: str):
|
||||
"""The shipped default for `key`, out of config.yaml's `options:` block."""
|
||||
hits = _top_level(_cfg_halves()[0], key)
|
||||
if not hits:
|
||||
return None
|
||||
try:
|
||||
return float(hits[0].split(":", 1)[1].strip())
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def read_schema_max(key: str):
|
||||
@@ -210,11 +255,9 @@ def read_schema_min(key: str):
|
||||
|
||||
|
||||
def _schema_bound(key: str, which: int):
|
||||
body = (HERE / "config.yaml").read_text(encoding="utf-8")
|
||||
marker = body.index("\nschema:")
|
||||
for line in body[marker:].splitlines():
|
||||
for line in _top_level(_cfg_halves()[1], key):
|
||||
st = line.strip()
|
||||
if st.startswith(f"{key}:") and "(" in st and ")" in st:
|
||||
if "(" in st and ")" in st:
|
||||
return float(st[st.index("(") + 1:st.rindex(")")].split(",")[which])
|
||||
return None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user