|
|
|
@@ -411,70 +411,91 @@ check("...and `phase` is NOT among them, because its value 'idle' IS the "
|
|
|
|
|
"phase" not in _m9_vanished)
|
|
|
|
|
shutil.rmtree(Path(_m9).parent, ignore_errors=True)
|
|
|
|
|
|
|
|
|
|
# ⚠️ AND `M12` BOUNDS THE **MOVED-NOT-LOST** RULE, WHICH NOTHING DID - AND ITS
|
|
|
|
|
# FIRST DRAFT DID NOT EITHER, WHICH IS THE WORSE FAILURE OF THE TWO.
|
|
|
|
|
# ⭐ `M12` IS BACK, AND THE CLAIM THAT KILLED IT WAS WRONG.
|
|
|
|
|
#
|
|
|
|
|
# The rule cited M3 as its bound, and the commit message cited it too. M3 drops
|
|
|
|
|
# `sign_reason` from `LEGACY_UNIT_KEYS`; M9, the one actually meant, vanishes
|
|
|
|
|
# only FLAT keys, so neither can satisfy the `unit/` gate the rule sits behind.
|
|
|
|
|
# THE BRANCH HAD NO CONTROL AT ALL.
|
|
|
|
|
# M12 is SAFETY-44's end-to-end control for the off-roster narrowing: a mutant
|
|
|
|
|
# makes one ROSTER unit's latch key vanish while a sibling holds the same
|
|
|
|
|
# value, and it must go red when the narrowing is removed. INSTR-01's first
|
|
|
|
|
# draft DELETED it, on the claim that the provenance clause made it
|
|
|
|
|
# unrestorable and that "NO FIXTURE CAN RESTORE IT, measured both ways".
|
|
|
|
|
#
|
|
|
|
|
# ⚠️ THE FIRST FIX FOR THAT WAS INERT, AND REVIEW MEASURED IT: run M12's mutant
|
|
|
|
|
# over `state-legacy.json` and the findings diff against the UNMUTATED tree is
|
|
|
|
|
# EMPTY. It "passed" because that fixture's STAGED leg is already exit 1 on
|
|
|
|
|
# pre-existing litter, and `"migrated unit/shed/" not in out` was trivially
|
|
|
|
|
# true because a pre-DEV-02 FLAT store has no shed latch to lose - after
|
|
|
|
|
# migration only the FIRST roster entry is ever latched. A control written
|
|
|
|
|
# against a fixture that cannot hold the failure is habit 4 wearing the clothes
|
|
|
|
|
# of its own fix, and it is WORSE than the honest "no control" it replaced,
|
|
|
|
|
# because it reads as checked.
|
|
|
|
|
# ⚠️ TWO MEASUREMENTS ARE NOT AN IMPOSSIBILITY PROOF, AND THIS ONE WAS FALSE.
|
|
|
|
|
# Both arrangements I measured were SAME-STEP shaped, and the structural
|
|
|
|
|
# argument - `run()` builds both snapshots from one state file, so "already
|
|
|
|
|
# latched" and "made by this transition" are incompatible - is TRUE OF
|
|
|
|
|
# SAME-STEP AND FALSE OF STAGED. `snapshot_over()` starts the after-leg over
|
|
|
|
|
# the file the BEFORE-leg wrote, so on the staged leg a transition-made
|
|
|
|
|
# destination is available and the two questions stop being incompatible.
|
|
|
|
|
# Review found the third arrangement I had generalised past.
|
|
|
|
|
#
|
|
|
|
|
# ⭐ SO THE FIXTURE CAME FIRST. `state-both-latched.json` is a store at
|
|
|
|
|
# SCHEMA_VERSION with `unit/garage/*` AND `unit/shed/*` both latched, by ONE
|
|
|
|
|
# site event so the reasons match too - the arrangement in which the
|
|
|
|
|
# suppression is most dangerous, and the one no existing fixture could produce.
|
|
|
|
|
# The mutant then makes ONE roster unit's latch key VANISH while its sibling's
|
|
|
|
|
# survives at the same value.
|
|
|
|
|
# HOW IT WORKS. The fixture is `state-legacy.json` plus a second roster unit
|
|
|
|
|
# already latched. The mutant makes `shed` POP its own latch key and come up
|
|
|
|
|
# untripped - a truer SAFETY-02 loss than the original, which left `tripped`
|
|
|
|
|
# True in memory. On the staged leg, losing shed's latch makes
|
|
|
|
|
# `_grade_store_migration()` fall through `on_roster_latched` and RE-ARM
|
|
|
|
|
# `unit/garage/sign_tripped`, which is absent from `before.data` - so the
|
|
|
|
|
# destination is transition-made and C7 lets it through. The off-roster
|
|
|
|
|
# `inverter-a` litter is present in BOTH snapshots on this leg, so it never
|
|
|
|
|
# vanishes and never claims the destination, and C6 stays out of the way.
|
|
|
|
|
#
|
|
|
|
|
# THE DECISIVE MEASUREMENT, which is what the previous draft could not make:
|
|
|
|
|
# reverting the `vanished_unit not in after.units` narrowing - restoring exactly
|
|
|
|
|
# the rule review proved hides a real loss - turns this RED. Before, it was
|
|
|
|
|
# 28/28 green.
|
|
|
|
|
# ⚠️ AND WHAT ACTUALLY BROKE WAS THE PRECONDITION, NOT THE CONTROL. M12's
|
|
|
|
|
# `exit == EXIT_OK` baseline does not survive a legacy-shaped fixture. That
|
|
|
|
|
# precondition is the one SAFETY-44's own commit called "belt-and-braces,
|
|
|
|
|
# load-bearing only if the exit-0 requirement is ever relaxed", saying that
|
|
|
|
|
# what closed the first draft's gap is `_m12_new` BEING A SET DIFFERENCE. A
|
|
|
|
|
# stable dirty baseline is fine for a set difference. So the control was
|
|
|
|
|
# deleted on the strength of a precondition the same commit had already
|
|
|
|
|
# characterised as not load-bearing - which is a worse mistake than the
|
|
|
|
|
# original inert check, because it removed coverage rather than failing to add
|
|
|
|
|
# it.
|
|
|
|
|
_M12_OLD = 'tripped=bool(data.get(self.key("sign_tripped"), False)),'
|
|
|
|
|
_M12_NEW = ('tripped=bool(data.pop(self.key("sign_tripped"), False)'
|
|
|
|
|
+ chr(10) + ' if self.name == "shed"'
|
|
|
|
|
+ chr(10) + ' else data.get(self.key("sign_tripped"), False)),')
|
|
|
|
|
# ⚠️ THE MUTANT POPS **AND COMES UP UNTRIPPED**, which is the whole point and
|
|
|
|
|
# which the first draft got wrong: `data.pop(k, False)` RETURNS the value, so
|
|
|
|
|
# shed stayed latched in memory, `_grade_store_migration()` saw
|
|
|
|
|
# `on_roster_latched` and never re-armed garage - no transition-made
|
|
|
|
|
# destination, and the finding was reported whether or not C5 existed. The
|
|
|
|
|
# control looked restored and discriminated nothing. `and False` is the
|
|
|
|
|
# difference between "the key is gone" and "the LATCH is gone", and only the
|
|
|
|
|
# second reaches the re-arm this arrangement needs.
|
|
|
|
|
_M12_NEW = ('tripped=bool(data.pop(self.key("sign_tripped"), False) and False'
|
|
|
|
|
+ chr(10) + ' if self.name == "shed" else'
|
|
|
|
|
+ chr(10) + ' data.get(self.key("sign_tripped"), False)),')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def m12_findings(tree):
|
|
|
|
|
"""The finding SET of a staged-leg run over the legacy+shed fixture."""
|
|
|
|
|
code, out = run(Path(tree), "state-legacy-plus-shed.json", "roster.yaml",
|
|
|
|
|
"--paths", "staged")
|
|
|
|
|
return code, set(re.findall(r"⚠️ ([A-Z ]+): (\S+)", out)), out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_m12 = mutant_tree([("app/inverter.py", _M12_OLD, _M12_NEW)])
|
|
|
|
|
_m12_code, _m12_out = run(Path(_m12), "state-both-latched.json", "roster.yaml")
|
|
|
|
|
_m12_clean_code, _m12_clean_out = run(HERE, "state-both-latched.json", "roster.yaml")
|
|
|
|
|
_m12_new = (set(re.findall(r"⚠️ ([A-Z ]+): (\S+)", _m12_out))
|
|
|
|
|
- set(re.findall(r"⚠️ ([A-Z ]+): (\S+)", _m12_clean_out)))
|
|
|
|
|
print(f" both-latched store, shipped -> exit {_m12_clean_code}; "
|
|
|
|
|
f"one unit's latch key deleted -> exit {_m12_code}")
|
|
|
|
|
print(f" findings the mutation ADDS: {sorted(_m12_new)}")
|
|
|
|
|
# ⚠️ THE SECOND CONJUNCT IS IMPLIED BY THE FIRST TODAY - on a clean run the
|
|
|
|
|
# finding regex matches nothing - and the commit message that introduced it
|
|
|
|
|
# overclaimed by saying the exit code "cannot say" it. It is belt-and-braces,
|
|
|
|
|
# load-bearing only if the exit-0 requirement is ever relaxed. WHAT ACTUALLY
|
|
|
|
|
# CLOSED THE FIRST DRAFT'S GAP IS `_m12_new` BEING A SET DIFFERENCE: the
|
|
|
|
|
# mutation must ADD a finding, which is the check the narrowing's removal turns
|
|
|
|
|
# red. Said plainly here because the wrong half was credited once already.
|
|
|
|
|
check("⚠️ PRECONDITION: the fixture is clean on the shipped tree AND does not "
|
|
|
|
|
"already report the finding this mutation exists to produce - the second "
|
|
|
|
|
"is implied by the first today and kept as a belt-and-braces guard",
|
|
|
|
|
_m12_clean_code == RR.EXIT_OK
|
|
|
|
|
and not any(k == "unit/shed/sign_tripped"
|
|
|
|
|
for _, k in re.findall(r"⚠️ ([A-Z ]+): (\S+)", _m12_clean_out)))
|
|
|
|
|
check("⭐ M12: a ROSTER unit's latch key that VANISHES is a finding even though "
|
|
|
|
|
"a sibling on the same roster still holds True - the moved-not-lost rule "
|
|
|
|
|
"suppresses a RENAME (a key leaving a name this roster does not carry), "
|
|
|
|
|
"never a LOSS",
|
|
|
|
|
_m12_code == RR.EXIT_FINDING
|
|
|
|
|
and any(k == "unit/shed/sign_tripped" for _, k in _m12_new))
|
|
|
|
|
check("...and the mutation ADDS findings rather than merely coinciding with "
|
|
|
|
|
"ones the fixture already had - an empty diff here is what made the "
|
|
|
|
|
"previous draft inert",
|
|
|
|
|
bool(_m12_new))
|
|
|
|
|
_m12_clean_code, _m12_clean, _ = m12_findings(HERE)
|
|
|
|
|
_m12_code, _m12_mut, _ = m12_findings(Path(_m12))
|
|
|
|
|
_m12_new = _m12_mut - _m12_clean
|
|
|
|
|
print(f" staged leg, legacy+shed: shipped exit {_m12_clean_code}, "
|
|
|
|
|
f"one roster unit's latch popped -> exit {_m12_code}")
|
|
|
|
|
# ⚠️ THE PRINTED SET IS LOSSIER THAN IT LOOKS: the regex captures the first
|
|
|
|
|
# TOKEN of each finding's text, so two findings of one kind collapse into one
|
|
|
|
|
# tuple. Harmless here - the difference is one-directional and the pair M12
|
|
|
|
|
# asserts on is exact - but do not read this line as an inventory.
|
|
|
|
|
print(f" findings the mutation ADDS (first token only): {sorted(_m12_new)}")
|
|
|
|
|
check("⚠️ PRECONDITION: the baseline is STABLE and does not already carry the "
|
|
|
|
|
"finding this mutation exists to produce. It is NOT clean - a legacy "
|
|
|
|
|
"fixture never is - and the SET DIFFERENCE is what makes that fine, "
|
|
|
|
|
"which is the sentence the deletion should have read before acting on it",
|
|
|
|
|
not any(k == "unit/shed/sign_tripped" for _, k in _m12_clean))
|
|
|
|
|
# ⚠️ AND IT MATCHES THE **KIND** AS WELL AS THE KEY, which SAFETY-44's original
|
|
|
|
|
# did not. Review measured it: renaming the finding kind in `diff()` from
|
|
|
|
|
# "VANISHED STORE KEY" to "GONE STORE KEY" left M12 GREEN. A vanished latch
|
|
|
|
|
# quietly downgraded to a softer heading is exactly the shape part 3 requires
|
|
|
|
|
# every mutation to be graded against - "the SPECIFIC finding it should produce"
|
|
|
|
|
# - and this file was not holding itself to its own rule.
|
|
|
|
|
check("⭐ M12 RESTORED: a ROSTER unit's latch key that vanishes is a "
|
|
|
|
|
"VANISHED STORE KEY finding even though a sibling holds True - and this "
|
|
|
|
|
"is the STAGED leg, where `snapshot_over()` supplies a transition-made "
|
|
|
|
|
"destination, so the provenance clause does not foreclose the narrowing",
|
|
|
|
|
("VANISHED STORE KEY", "unit/shed/sign_tripped") in _m12_new)
|
|
|
|
|
shutil.rmtree(Path(_m12).parent, ignore_errors=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -715,60 +736,39 @@ check("...and no leg's text has swallowed the CLOSING SUMMARY, which repeats "
|
|
|
|
|
"red whenever any leg was",
|
|
|
|
|
not any("FINDING(S) - this transition is not clean" in v
|
|
|
|
|
for v in _L.values()))
|
|
|
|
|
# ⚠️ TWO FURTHER HOLES IN THIS RULE ARE **KNOWN, MEASURED BY REVIEW, AND NOT
|
|
|
|
|
# FIXED HERE** - deliberately, and this paragraph is the whole of the entry.
|
|
|
|
|
# ⚠️ THIS BLOCK USED TO SAY THOSE HOLES WERE KNOWN AND NOT FIXED, AND IT IS
|
|
|
|
|
# NOW STALE IN THE OTHER DIRECTION - INSTR-01 fixed them, and left the
|
|
|
|
|
# paragraph saying "BOTH ARE REVERTED OUT OF THIS CARD, because THIS SUITE
|
|
|
|
|
# CANNOT MEASURE EITHER ONE" describing a tree that no longer exists. Review
|
|
|
|
|
# found it. A comment that outlives its subject is the same defect as a
|
|
|
|
|
# citation that outlives its check.
|
|
|
|
|
#
|
|
|
|
|
# INJECTIVITY. `moved_to` is "any machine-read key with the same bare name
|
|
|
|
|
# and the same value", so N vanished keys can all claim the SAME destination.
|
|
|
|
|
# Review measured a rename carrying `inverter-a` -> `garage` correctly while
|
|
|
|
|
# DROPPING `inverter-b` -> `shed`: both reported as migrated to
|
|
|
|
|
# `unit/garage/sign_tripped`, zero store-key findings, `shed` unlatched.
|
|
|
|
|
# WHERE EACH ONE NOW LIVES, and every clause of `moved_to` has a row in 5e:
|
|
|
|
|
#
|
|
|
|
|
# PROVENANCE. A destination is accepted regardless of whether the transition
|
|
|
|
|
# put the value there, so a roster unit already latched in its own right is a
|
|
|
|
|
# free suppression slot. Review measured a build deleting `old-a` while
|
|
|
|
|
# renaming `old-b` -> `shed`: exit 0, NO FINDINGS, `old-a` reported as having
|
|
|
|
|
# moved into a key that already held True.
|
|
|
|
|
# INJECTIVITY (C6) R5, and removing the clause flips exactly R5
|
|
|
|
|
# PROVENANCE (C7) R3, likewise
|
|
|
|
|
# SAME VALUE (C3) R4
|
|
|
|
|
# SAME BARE KEY(C2) R7 - which had NO row until review deleted the clause
|
|
|
|
|
# and the whole 28-file suite stayed green
|
|
|
|
|
# MACHINE-READ (C1) R6
|
|
|
|
|
# OFF-ROSTER (C5) R2, and M12 end-to-end on the staged leg
|
|
|
|
|
# PREFIX GATE (C4) NO 5e ROW, deliberately - `vanished_unit is not None`
|
|
|
|
|
# is reached end-to-end by part 3's M9, and a synthetic
|
|
|
|
|
# row would duplicate that rather than add separability.
|
|
|
|
|
# Listed because C5 lives in the same `if` and omitting
|
|
|
|
|
# one guard while naming the other reads as an oversight.
|
|
|
|
|
#
|
|
|
|
|
# ATTRIBUTION. Which vanished key gets a destination is decided by
|
|
|
|
|
# `sorted()`, so on a build renaming `inverter-b` -> `garage` while DELETING
|
|
|
|
|
# `inverter-a`, the deleted key sorts first and takes it: the report names the
|
|
|
|
|
# DELETED key as migrated and the one that really moved as VANISHED. The run
|
|
|
|
|
# is still exit 1, so this is a wrong sentence rather than a hidden loss - but
|
|
|
|
|
# an operator chasing the named key looks at the wrong inverter.
|
|
|
|
|
#
|
|
|
|
|
# VALUE MATCHING. `and after.data.get(k) == value` survives the suite and is
|
|
|
|
|
# load-bearing: without it the rule degenerates to BARE-NAME matching, and
|
|
|
|
|
# review measured a deleted `unit/old-x/sign_reason` reported as having
|
|
|
|
|
# migrated into a `sign_reason` holding a COMPLETELY DIFFERENT SENTENCE.
|
|
|
|
|
# Essentially every off-roster vanished `unit/*` key would be suppressed.
|
|
|
|
|
#
|
|
|
|
|
# ⚠️ BOTH FIXES ARE ONE LINE AND BOTH WERE WRITTEN, AND BOTH ARE REVERTED OUT
|
|
|
|
|
# OF THIS CARD, because THIS SUITE CANNOT MEASURE EITHER ONE. Three attempts:
|
|
|
|
|
# a check over part 5's destinations passed with the whole injectivity
|
|
|
|
|
# mechanism deleted (its two destinations are `sign_reason` and `sign_tripped`
|
|
|
|
|
# - different bare keys, which can never contend); a dedicated fixture
|
|
|
|
|
# discriminated nothing (both snapshots read one state file, so nothing
|
|
|
|
|
# vanishes and there are no migrated lines either way); and rebuilding M12's
|
|
|
|
|
# fixture to reach the provenance case disarmed M12 for the NARROWING, so all
|
|
|
|
|
# three clause-removals came back green at once.
|
|
|
|
|
#
|
|
|
|
|
# ⚠️ THAT LAST ONE IS THE REASON THIS IS A SPLIT AND NOT A JUDGEMENT CALL: a
|
|
|
|
|
# later fix to the same function silently disarmed the control for an earlier
|
|
|
|
|
# one, and I only saw it because the removal was re-driven. Shipping two more
|
|
|
|
|
# unmeasured clauses into the instrument, on a card whose subject is checks
|
|
|
|
|
# that cannot reach what they name, would be the fourth instance in one PR.
|
|
|
|
|
# They go on their own card, with review's measurements as the starting
|
|
|
|
|
# evidence and a fixture built BEFORE the fix.
|
|
|
|
|
#
|
|
|
|
|
# ⚠️ ALL FOUR ARE CLAUSES OF **ONE COMPREHENSION**, WHICH IS THE STRUCTURAL
|
|
|
|
|
# POINT AND NOT A LIST OF CHORES. `diff()`'s `moved_to` now carries four
|
|
|
|
|
# conditions with four different reasons to exist, against one fixture set that
|
|
|
|
|
# can reach only some of them at a time - so rebuilding a fixture to reach
|
|
|
|
|
# clause N moves the others. That is exactly how (3) happened, and it is what
|
|
|
|
|
# INSTR-01 has to design against rather than patch around.
|
|
|
|
|
#
|
|
|
|
|
# What IS measured here is the narrowing, by M12 above, both ways.
|
|
|
|
|
# ⚠️ ONE HOLE IS STILL OPEN AND IS **UNMEASURED**, stated so nobody reads
|
|
|
|
|
# `sorted()` as guarded: ATTRIBUTION. Which vanished key gets a destination is
|
|
|
|
|
# decided by `sorted()` order, so on a build renaming `inverter-b` -> `garage`
|
|
|
|
|
# while DELETING `inverter-a`, the deleted key sorts first and takes it - the
|
|
|
|
|
# report names the DELETED key as migrated and the one that really moved as
|
|
|
|
|
# VANISHED. Review measured that replacing `sorted(` with `list(` leaves the
|
|
|
|
|
# suite green: every 5e case has at most one candidate destination, so no row
|
|
|
|
|
# can see ordering. The run is still exit 1, so this is a WRONG SENTENCE rather
|
|
|
|
|
# than a hidden loss - an operator chasing the named key looks at the wrong
|
|
|
|
|
# inverter. Fixing it needs the destination's provenance rather than
|
|
|
|
|
# first-come, which is more than a rename diff currently knows.
|
|
|
|
|
check("⚠️ and the key that MOVED is not reported as lost: the before-snapshot "
|
|
|
|
|
"holds `unit/inverter-a/sign_tripped = True` and the after-snapshot "
|
|
|
|
|
"holds it under `garage`, three lines apart in the same report. A "
|
|
|
|
@@ -873,6 +873,184 @@ check("the SAME state file onto a roster whose first entry IS `inverter-a` "
|
|
|
|
|
code_ok == RR.EXIT_OK and "LATCH CAME UP DISARMED" not in out_ok)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
print()
|
|
|
|
|
print("5e. INSTR-01: `diff()`'s suppression rule, ONE FIXTURE PER CLAUSE")
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# ⚠️ THIS SECTION EXISTS BECAUSE SAFETY-44 PRODUCED THREE INERT CONTROLS FOR
|
|
|
|
|
# THIS ONE RULE, EACH INERT DIFFERENTLY:
|
|
|
|
|
#
|
|
|
|
|
# 1. a check over part 5's real destinations passed with the whole mechanism
|
|
|
|
|
# deleted - its two destinations are `sign_reason` and `sign_tripped`,
|
|
|
|
|
# DIFFERENT bare keys, which can never contend;
|
|
|
|
|
# 2. a dedicated end-to-end fixture discriminated nothing - both snapshots
|
|
|
|
|
# read ONE state file, so nothing vanishes and there are no migrated lines
|
|
|
|
|
# with the clause or without it;
|
|
|
|
|
# 3. rebuilding M12's fixture to reach a new clause DISARMED M12 for an older
|
|
|
|
|
# one, and all three clause-removals went green at once.
|
|
|
|
|
#
|
|
|
|
|
# ⭐ (3) IS THE STRUCTURAL FINDING AND IT IS WHY THIS SECTION IS SHAPED THIS WAY.
|
|
|
|
|
# `moved_to` is ONE comprehension carrying FIVE conditions with five different
|
|
|
|
|
# reasons to exist, and an END-TO-END fixture cannot separate them: both
|
|
|
|
|
# snapshots come from one state file, so the arrangement that reaches clause N
|
|
|
|
|
# forecloses clause M. Rebuilding the fixture to reach one moves the others.
|
|
|
|
|
#
|
|
|
|
|
# So the rule is graded at the level it is written at - `diff()` on constructed
|
|
|
|
|
# Snapshots - with ONE CASE PER CLAUSE. Nothing here is a fiction: the inputs
|
|
|
|
|
# are the same three fields `diff()` reads (`data`, `machine_read`, `units`),
|
|
|
|
|
# and the end-to-end controls above - M12 on the STAGED leg, and part 5 -
|
|
|
|
|
# still grade the same rule through a real start. This is the separability
|
|
|
|
|
# those cannot have, not a replacement for them.
|
|
|
|
|
CLAUSE_CASES = {
|
|
|
|
|
# name: (units, machine_read, before, after)
|
|
|
|
|
"R1-rename-legit": (
|
|
|
|
|
["garage"], ["unit/garage/sign_tripped"],
|
|
|
|
|
{"unit/old/sign_tripped": True}, {"unit/garage/sign_tripped": True}),
|
|
|
|
|
"R2-narrowing": (
|
|
|
|
|
["garage", "shed"], ["unit/garage/sign_tripped", "unit/shed/sign_tripped"],
|
|
|
|
|
{"unit/shed/sign_tripped": True}, {"unit/garage/sign_tripped": True}),
|
|
|
|
|
"R3-provenance": (
|
|
|
|
|
["garage"], ["unit/garage/sign_tripped"],
|
|
|
|
|
{"unit/old/sign_tripped": True, "unit/garage/sign_tripped": True},
|
|
|
|
|
{"unit/garage/sign_tripped": True}),
|
|
|
|
|
"R4-value": (
|
|
|
|
|
["garage"], ["unit/garage/sign_reason"],
|
|
|
|
|
{"unit/old/sign_reason": "X-unique"},
|
|
|
|
|
{"unit/garage/sign_reason": "Y-different"}),
|
|
|
|
|
"R5-injectivity": (
|
|
|
|
|
["garage"], ["unit/garage/sign_tripped"],
|
|
|
|
|
{"unit/old-a/sign_tripped": True, "unit/old-b/sign_tripped": True},
|
|
|
|
|
{"unit/garage/sign_tripped": True}),
|
|
|
|
|
"R6-machine-read": (
|
|
|
|
|
["garage"], [],
|
|
|
|
|
{"unit/old/sign_tripped": True}, {"unit/garage/sign_tripped": True}),
|
|
|
|
|
# ⚠️ R7 EXISTS BECAUSE THE BARE-KEY MATCH HAD NO ROW AT ALL, and review
|
|
|
|
|
# deleted it and left the whole 28-file suite GREEN. The rule's own comment
|
|
|
|
|
# says "THE TEST IS NOT 'the name appears somewhere'. It is: the SAME bare
|
|
|
|
|
# key" - and nothing measured that sentence. It is load-bearing: a vanished
|
|
|
|
|
# SAFETY-02 `sign_tripped` was suppressed by a SAFETY-42
|
|
|
|
|
# `delivery_tripped` on a roster unit, i.e. by a DIFFERENT FAILSAFE's key.
|
|
|
|
|
# `unit/*/delivery_tripped` is machine-read, so this is reachable on any
|
|
|
|
|
# real two-latch store rather than a contrivance.
|
|
|
|
|
"R7-bare-key": (
|
|
|
|
|
["garage"], ["unit/garage/delivery_tripped"],
|
|
|
|
|
{"unit/old/sign_tripped": True},
|
|
|
|
|
{"unit/garage/delivery_tripped": True}),
|
|
|
|
|
}
|
|
|
|
|
# What the SHIPPED rule must answer. `migrated` is the suppression firing.
|
|
|
|
|
CLAUSE_EXPECT = {
|
|
|
|
|
"R1-rename-legit": ([], ["unit/old/sign_tripped"]),
|
|
|
|
|
"R2-narrowing": (["unit/shed/sign_tripped"], []),
|
|
|
|
|
"R3-provenance": (["unit/old/sign_tripped"], []),
|
|
|
|
|
"R4-value": (["unit/old/sign_reason"], []),
|
|
|
|
|
"R5-injectivity": (["unit/old-b/sign_tripped"], ["unit/old-a/sign_tripped"]),
|
|
|
|
|
"R6-machine-read": (["unit/old/sign_tripped"], []),
|
|
|
|
|
"R7-bare-key": (["unit/old/sign_tripped"], []),
|
|
|
|
|
}
|
|
|
|
|
# ⚠️ THE DRIVER **IMPORTS** THE TREE UNDER TEST AND THE TABLE STAYS HERE, so
|
|
|
|
|
# expectation and implementation sit on opposite sides of the subprocess and
|
|
|
|
|
# a mutation to the tool cannot also move the answer it is graded against.
|
|
|
|
|
# (It is WRITTEN to a tempdir - see `clause_table()`. The first draft of this
|
|
|
|
|
# sentence said "written into the tree", which is the read-only-CI bug the
|
|
|
|
|
# same commit says it fixed, stated as if it were the design.)
|
|
|
|
|
CLAUSE_DRIVER = """
|
|
|
|
|
import json, sys
|
|
|
|
|
import rehearse_roster as RR
|
|
|
|
|
spec = json.load(sys.stdin)
|
|
|
|
|
out = {}
|
|
|
|
|
for name, case in spec.items():
|
|
|
|
|
units, mread, before, after = case
|
|
|
|
|
b = object.__new__(RR.Snapshot); a = object.__new__(RR.Snapshot)
|
|
|
|
|
for snap, data in ((b, before), (a, after)):
|
|
|
|
|
snap.label = name
|
|
|
|
|
snap.data = dict(data)
|
|
|
|
|
snap.units = list(units)
|
|
|
|
|
snap.machine_read = set(mread)
|
|
|
|
|
snap.entities = {}
|
|
|
|
|
snap.latched = {u: False for u in units}
|
|
|
|
|
snap.latch_reason = {u: "" for u in units}
|
|
|
|
|
snap.flat_tripped = False
|
|
|
|
|
snap.history = {}
|
|
|
|
|
snap.read = set()
|
|
|
|
|
snap.stranded_at_default = []
|
|
|
|
|
snap.vanished_at_default = []
|
|
|
|
|
snap.vanished_but_migrated = []
|
|
|
|
|
f = RR.diff(b, a)
|
|
|
|
|
out[name] = [
|
|
|
|
|
sorted(x.text.split(" = ")[0] for x in f
|
|
|
|
|
if x.kind == "VANISHED STORE KEY"),
|
|
|
|
|
sorted(t.split(" = ")[0] for t in a.vanished_but_migrated),
|
|
|
|
|
]
|
|
|
|
|
print(json.dumps(out))
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clause_table(tree) -> dict:
|
|
|
|
|
"""Run CLAUSE_CASES through `diff()` in `tree`, as the tool would see it."""
|
|
|
|
|
# ⚠️ THE DRIVER GOES IN A TEMPDIR, NOT IN THE TREE. `checks / unit` mounts
|
|
|
|
|
# the checkout READ-ONLY, so writing beside the tool works on a dev box and
|
|
|
|
|
# is `OSError: Read-only file system` in CI - a difference that would have
|
|
|
|
|
# turned this section red on every PR and green for whoever wrote it.
|
|
|
|
|
tools = Path(tree) / "tools"
|
|
|
|
|
tmp = tempfile.mkdtemp(prefix="clause-drv-")
|
|
|
|
|
drv = Path(tmp) / "_clause_driver.py"
|
|
|
|
|
drv.write_text(CLAUSE_DRIVER, encoding="utf-8")
|
|
|
|
|
proc = subprocess.run(
|
|
|
|
|
[sys.executable, str(drv)],
|
|
|
|
|
input=json.dumps(CLAUSE_CASES), capture_output=True, text=True,
|
|
|
|
|
cwd=tmp,
|
|
|
|
|
env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1",
|
|
|
|
|
"PYTHONPATH": f"{tools}{os.pathsep}{Path(tree)}"})
|
|
|
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
|
|
|
assert proc.returncode == 0, (
|
|
|
|
|
"the clause driver did not run, so every row below would be "
|
|
|
|
|
f"fabricated:\n{proc.stdout}\n{proc.stderr}")
|
|
|
|
|
return {k: (v[0], v[1]) for k, v in json.loads(proc.stdout).items()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_ct = clause_table(HERE)
|
|
|
|
|
for _name in sorted(CLAUSE_CASES):
|
|
|
|
|
_got, _want = _ct[_name], CLAUSE_EXPECT[_name]
|
|
|
|
|
print(f" {_name:<18} vanished={_got[0]} migrated={_got[1]}")
|
|
|
|
|
check(f"{_name}: the shipped rule answers as specified", list(_got) == [list(_want[0]), list(_want[1])])
|
|
|
|
|
check("⚠️ ...and the LEGITIMATE case is still suppressed - a rule that reports "
|
|
|
|
|
"every rename is a rule an operator turns off, which is the failure "
|
|
|
|
|
"CONTROL-C exists to prevent",
|
|
|
|
|
_ct["R1-rename-legit"][1] == ["unit/old/sign_tripped"])
|
|
|
|
|
|
|
|
|
|
# ⭐ AND EACH CLAUSE REMOVAL MUST FLIP **EXACTLY ITS OWN ROW**. That second half
|
|
|
|
|
# is the acceptance criterion this card was filed with: a removal that flips
|
|
|
|
|
# nothing is an unmeasured clause, and a removal that flips someone else's row
|
|
|
|
|
# means the cases are not separable after all - which is (3) above, caught.
|
|
|
|
|
CLAUSE_MUTATIONS = [
|
|
|
|
|
("C2 same bare key", "R7-bare-key",
|
|
|
|
|
' if k.rsplit("/", 1)[-1] == bare and after.data.get(k) == value',
|
|
|
|
|
" if True and after.data.get(k) == value"),
|
|
|
|
|
("C1 machine_read", "R6-machine-read",
|
|
|
|
|
" k for k in after.machine_read",
|
|
|
|
|
" k for k in after.data"),
|
|
|
|
|
("C3 same value", "R4-value",
|
|
|
|
|
" and after.data.get(k) == value\n", "\n"),
|
|
|
|
|
("C5 off-roster", "R2-narrowing",
|
|
|
|
|
" if vanished_unit is not None and vanished_unit not in after.units \\\n"
|
|
|
|
|
" and moved_to:",
|
|
|
|
|
" if vanished_unit is not None and moved_to:"),
|
|
|
|
|
("C6 injectivity", "R5-injectivity",
|
|
|
|
|
" and k not in claimed)", ")"),
|
|
|
|
|
("C7 provenance", "R3-provenance",
|
|
|
|
|
" and before.data.get(k) != value", " and True"),
|
|
|
|
|
]
|
|
|
|
|
for _label, _owns, _old, _new in CLAUSE_MUTATIONS:
|
|
|
|
|
_t = mutant_tree([("tools/rehearse_roster.py", _old, _new)])
|
|
|
|
|
_mt = clause_table(_t)
|
|
|
|
|
_flipped = sorted(n for n in CLAUSE_CASES if _mt[n] != _ct[n])
|
|
|
|
|
print(f" {_label:<16} removed -> rows that changed: {_flipped}")
|
|
|
|
|
check(f"⭐ removing {_label} flips {_owns} - the clause is load-bearing and "
|
|
|
|
|
f"this row is what measures it",
|
|
|
|
|
_flipped == [_owns])
|
|
|
|
|
shutil.rmtree(Path(_t).parent, ignore_errors=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
print()
|
|
|
|
|
print("6. THE REVERSE DIRECTION: the retreat, and the return from it")
|
|
|
|
|