ARCH-02: rehearse the flat-to-roster switch - and SAFETY-44 re-confirmed by a second instrument #70
@@ -142,9 +142,18 @@ print("2. THE CLEAN CONTROLS: two transitions on which the shipped code is "
|
||||
# CONTROL-B a store that has already run flat under 0.7.0 (schema stamped,
|
||||
# no latch), onto a roster named `garage`/`shed`. This is the
|
||||
# MAINT-05 half, and the names are deliberately not `inverter-a`.
|
||||
# CONTROL-C a FRESH INSTALL - an empty state file - onto `garage`/`shed`.
|
||||
# ⚠️ THIS ONE FOUND A DEFECT IN THE HARNESS. A fresh store's
|
||||
# migration writes `sign_tripped = False` and `sign_reason = ""`
|
||||
# under `inverter-a` before any roster exists, and the first draft
|
||||
# reported both as STRANDED - red on every fresh install. Losing a
|
||||
# DEFAULT loses nothing; that is `MigrationReport.lost`'s own
|
||||
# distinction, and an alarm that fires on the harmless case is one
|
||||
# an operator learns to dismiss before the real one arrives.
|
||||
CONTROLS = {
|
||||
"CONTROL-A": ("state-legacy.json", "roster-inverter-a.yaml"),
|
||||
"CONTROL-B": ("state-flat-070.json", "roster.yaml"),
|
||||
"CONTROL-C": ("state-fresh.json", "roster.yaml"),
|
||||
}
|
||||
|
||||
|
||||
@@ -379,6 +388,22 @@ print("6. THE REVERSE DIRECTION: the retreat, and the return from it")
|
||||
# --------------------------------------------------------------------------- #
|
||||
code_r, out_r = run(HERE, "state-flat-070.json", "roster.yaml",
|
||||
"--reverse", "--round-trip")
|
||||
# ⚠️ AND THE SOFTENING IS NOT A HOLE. The SAME key stranded with a NON-default
|
||||
# value is still a finding - that is the whole of part 5 - so the two cases are
|
||||
# separated by the VALUE and not by the key name.
|
||||
_fresh_code, _fresh_out = run(HERE, "state-fresh.json", "roster.yaml")
|
||||
check("a fresh install DOES strand `unit/inverter-a/sign_tripped` - the key is "
|
||||
"written and nothing on the roster reads it - and the report SAYS SO",
|
||||
_fresh_code == RR.EXIT_OK
|
||||
and "unit/inverter-a/sign_tripped = False" in _fresh_out)
|
||||
check("...and does NOT call it a finding, because the value is the default; "
|
||||
"the same key at True is the finding part 5 turns on, so the two cases "
|
||||
"are separated by the VALUE and not by the key name",
|
||||
"STRANDED STORE KEY: unit/inverter-a/sign_tripped" not in _fresh_out
|
||||
and "STRANDED STORE KEY: unit/inverter-a/sign_tripped = True"
|
||||
in "\n".join(l.strip() for l in
|
||||
run(HERE, "state-legacy.json", "roster.yaml")[1].splitlines()))
|
||||
|
||||
_orphaned = set(re.findall(r"ORPHANED ENTITY: (sensor\.\S+)", out_r))
|
||||
print(f" the retreat orphans: {sorted(_orphaned)}")
|
||||
check("the RETREAT is rehearsed at all, and it reports the entities it "
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"_comment": "ARCH-02 SYNTHETIC FIXTURE - a FRESH INSTALL. No maintenance history, no latch, no schema markers: what /data/state.json looks like before the add-on has ever completed a cycle. It is a clean control, and it earned its place: the first draft of the harness reported this transition RED, because the DEV-02 migration writes both SAFETY-02 defaults under `inverter-a` before a roster exists and the tool called them stranded. Losing a default loses nothing."
|
||||
}
|
||||
@@ -371,6 +371,9 @@ class Snapshot:
|
||||
self.store_migration = controller.store_migration
|
||||
self.maint_migration = controller.maint_migration
|
||||
self.machine_read = machine_reads(controller, store)
|
||||
# Keys no machine reads whose value is the default `Store.load()` would
|
||||
# have supplied. Reported, never a finding: see `diff()`.
|
||||
self.stranded_at_default = []
|
||||
|
||||
@property
|
||||
def unit_keys(self):
|
||||
@@ -451,14 +454,27 @@ def diff(before: Snapshot, after: Snapshot) -> list:
|
||||
# ever look under. `MigrationReport.misdirected` says the same thing about
|
||||
# the keys the migration itself wrote; this says it about the WHOLE FILE,
|
||||
# including keys written by a version that is no longer installed.
|
||||
#
|
||||
# ⚠️ ONLY WHERE THE VALUE IS NOT THE DEFAULT, WHICH IS `MigrationReport`'s
|
||||
# OWN DISTINCTION AND NOT A SOFTENING OF THIS ONE. A stranded
|
||||
# `sign_tripped = False` has lost the DEFAULT - the same statement "absent"
|
||||
# already makes - so nothing is gone; a stranded `sign_tripped = True` has
|
||||
# lost A LATCH. Reporting the first would make this tool red on a FRESH
|
||||
# INSTALL, whose migration writes both defaults under `inverter-a` before
|
||||
# any roster exists, and an alarm that fires on the harmless case is an
|
||||
# alarm an operator learns to dismiss before the real one arrives - which is
|
||||
# the argument `MigrationReport.lost` already carries in store.py.
|
||||
for key in sorted(after.data):
|
||||
if not key.startswith("unit/"):
|
||||
if not key.startswith("unit/") or key in after.machine_read:
|
||||
continue
|
||||
if key in after.machine_read:
|
||||
value = after.data[key]
|
||||
bare = key.rsplit("/", 1)[-1]
|
||||
if value in (None, DEFAULTS.get(bare)):
|
||||
after.stranded_at_default.append(key)
|
||||
continue
|
||||
findings.append(Finding(
|
||||
"STRANDED STORE KEY",
|
||||
f"{key} = {after.data[key]!r} is in the store and NO machine on "
|
||||
f"{key} = {value!r} is in the store and NO machine on "
|
||||
f"the roster {after.units!r} read it during a full start. Its "
|
||||
f"value is not information."))
|
||||
|
||||
@@ -511,7 +527,8 @@ def _latch_finding(key, before, after):
|
||||
"LATCH KEY NOT UNDER A ROSTER NAME",
|
||||
f"the flat `{key}` = {was!r} did not reach {target}, which is the "
|
||||
f"key `Inverter({unit!r})` restores from"))
|
||||
elif target in after.data and target not in after.machine_read:
|
||||
elif (target in after.data and target not in after.machine_read
|
||||
and after.data[target] not in (None, DEFAULTS.get(key))):
|
||||
out.append(Finding(
|
||||
"LATCH KEY NOT READ BY ANY MACHINE",
|
||||
f"{target} = {after.data[target]!r} exists but no machine on the "
|
||||
@@ -660,6 +677,15 @@ def report_transition(name, before, after, findings) -> None:
|
||||
mark = " " if key in after.data else " (absent, defaulted) "
|
||||
print(f" {key}{mark}")
|
||||
|
||||
if after.stranded_at_default:
|
||||
print()
|
||||
print(" keys no machine reads whose value IS the default, so "
|
||||
"nothing is lost by")
|
||||
print(" stranding them - reported rather than hidden, not counted "
|
||||
"as findings:")
|
||||
for key in after.stranded_at_default:
|
||||
print(f" {key} = {after.data[key]!r}")
|
||||
|
||||
print()
|
||||
print(" -- MAINTENANCE HISTORY (MAINT-05: a second move must not drop it) -")
|
||||
for label, hist in (("before", before.history), ("after", after.history)):
|
||||
|
||||
Reference in New Issue
Block a user