ARCH-02: rehearse the flat-to-roster switch - and SAFETY-44 re-confirmed by a second instrument #70

Merged
admin merged 7 commits from ARCH-02 into release/1.0 2026-09-08 15:09:28 +02:00
3 changed files with 58 additions and 4 deletions
Showing only changes of commit cb8f800180 - Show all commits
+25
View File
@@ -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."
}
+30 -4
View File
@@ -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)):