TEST-16: pin the two one-sided arbitrage rungs, and un-mask them from each other
checks / unit (pull_request) Successful in 3m51s
checks / unit (pull_request) Successful in 3m51s
SAFETY-26's AC4 gave the same tolerance mutation to every price and quantity
boundary in arbitrage.py and static_tariff.py. Everything in arbitrage.py went
red - `price < 0`, `soc <= floor_pct`, `soc >= ceiling_pct` are each red under
+/-5. Two survived, both in static_tariff.py, and this card closes them.
D `spread == 0.0` (:912) weakened to `spread <= 0.001` SURVIVED at +2 B.
F `len(firm) < 2` (:901) weakened to `len(firm) < 1` SURVIVED at +0 B - the
delta MUTATION.md names as the risky one.
THE DEFECT IS NOT EITHER ROW, IT IS THE INTERACTION. D and F MASK EACH OTHER. A
single firm hour also has a spread of 0.0, so with F weakened the flat rung
refuses the stimulus anyway; with D weakened the small-but-real spread is still
refused, only for a lie. Each rung's failure is absorbed by the other, so BOTH
can be weakened together and a suite grading `kind == "refused"` reports
nothing. That is structurally invisible to single-mutation testing, which is how
this pair came through four rounds of review on TARIFF-03 and FCST-03 - and it
is the arrangement in which the next edit to either rung opens a real hole with
nothing to report it.
D is not cosmetic either. With the tolerance in place a series priced 0.20000 /
0.20050 - a REAL 0.0005 EUR/kWh spread - flips ALLOWED to REFUSED at
min_spread_eur_kwh=0.0, a value arbitrage()'s own docstring documents ("pass 0.0
to mean any positive spread, explicitly") and its validator at :861 accepts on
purpose. At a shipped threshold the outcome does not move, but the operator
reads the false sentence "the tariff is flat - every one of the 2 firm hours
costs 0.2000 EUR/kWh" about a tariff that is not flat.
WHAT WAS ADDED. test_static_tariff.py section 9, and the shape of it is the
argument:
* THE REASON IS GRADED, NOT THE VERDICT. The masked case refuses CORRECTLY
while reporting the wrong sentence, so a check asserting only "refused"
passes on a broken tree. A `rung9()` helper names which refusal answered,
from the sentence the operator would read; an unrecognised refusal reports
itself as UNATTRIBUTED rather than quietly matching nothing.
* EACH RUNG IS GIVEN A STIMULUS THE OTHER CANNOT ABSORB, or the pinning is
circular. The flat rung gets a THREE-firm-hour flat series, which no
`len(firm) < N` a person would write can reach. The firm-pair rung gets a
ONE-FIRM-HOUR series, which the flat rung CAN reach - so its attribution is
the whole check, and that is why it is written as a rung name rather than a
verdict.
* AC1 is a sweep of small-but-real spreads (0.0005 down to 0.000001) all
ALLOWED at min_spread_eur_kwh=0.0, with the exactly flat series on the other
side still refused BY THE FLAT RUNG - without that mirror the sweep would
also pass for a rung deleted outright. 1e-6 is the floor because the shipped
line rounds the spread to 6 decimals; that quantisation is graded in the
open as its own row rather than left unstated.
* AC3 is a five-row table graded on rung attribution, which denies the masking
from both sides at once.
* The firm-pair count is also pinned from ABOVE (`< 3`) and BELOW (`< 0`),
which the criteria do not ask for but which is what stops this section being
one-sided in exactly the way it was written to fix.
EVIDENCE. Eight rounds through TEST-15's tools/mutation_run.sh, used UNCHANGED:
recursive __pycache__ purge before each run, python3 -B, the tree re-copied FROM
DISK between rounds, the mutated line grepped out of the file before the run,
byte delta printed, test_static_tariff.py and test_arbitrage.py graded each
time. Baseline green.
baseline, shipped +0 B PASS
D1 `spread == 0.0` -> `<= 0.001` +2 B RED, 3 checks
D2 `spread == 0.0` -> `<= 0.000001` +5 B RED, AC1 alone
D3 `spread == 0.0` -> `< 0.0` (rung gone) +0 B RED (section 2 as well)
F1 `len(firm) < 2` -> `< 1` +0 B RED, 3 checks
F2 `len(firm) < 2` -> `< 3` +0 B RED, 6 checks
F3 `len(firm) < 2` -> `< 0` (rung gone) +0 B RED (ValueError)
D1+F1 BOTH AT ONCE - THE TICKET +2 B RED, 5 checks
Four of the seven are +0 B, the delta MUTATION.md names as the one stale
bytecode can hide. All are REDS, the direction that defect cannot fabricate -
but they were run under the purge anyway, because "it went red so the cache
cannot have lied" is a conclusion and not a procedure.
NO SHIPPED BEHAVIOUR CHANGED. app/ is untouched; both rungs were correct all
along, and the card is explicit that a rung found actually wrong is a stop-and-
report rather than an edit. Also updated: MUTATION.md gains a short section on
masking as a SECOND way a survivor lies, with the two rules that fall out of it
(grade the reason, and mutate suspected pairs together); and test_arbitrage.py's
SAFETY-26 AC4 note is marked closed rather than left reading as an open finding.
UNSURE ABOUT. The 5e-7 row asserts the shipped `round(spread, 6)` quantisation.
That is a real property of the line and worth stating, but it does pin a
rounding the ticket never named - if a reviewer would rather that stayed
unpinned, it is one check to drop and nothing else depends on it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e8fb596b99
commit
20f9f4969d
@@ -10,6 +10,11 @@ it:
|
||||
AC2 Fully-fixed pricing DISABLES ARBITRAGE and leaves self-consumption only.
|
||||
AC3 The Belgian scenario suite carries a static-profile regression case.
|
||||
|
||||
Section 9 is TEST-16 and is not one of those criteria. It pins the two
|
||||
`arbitrage()` rungs SAFETY-26's AC4 sweep found pinned from ONE SIDE ONLY -
|
||||
`spread == 0.0` and `len(firm) < 2` - and, more to the point, pins them so
|
||||
they stop MASKING EACH OTHER. Read its header before editing either rung.
|
||||
|
||||
THREE RULES THIS FILE HOLDS ITSELF TO, because the reviewer will:
|
||||
|
||||
* EVERY VERDICT IS DERIVED. No `check("the split is right", True)`. Where a
|
||||
@@ -62,7 +67,7 @@ from pathlib import Path
|
||||
|
||||
from app.price import (
|
||||
Absence, PricePoint, PriceProduct, PriceRead, arbitrage_allowed, brussels,
|
||||
plan_is_provisional,
|
||||
plan_is_provisional, required_spread, uncertainty_of,
|
||||
)
|
||||
from app.static_tariff import (
|
||||
MEAN_WINDOW_DAYS, ProfileInvalid, StaticProfile, TariffPlan,
|
||||
@@ -860,6 +865,255 @@ check("...and main.py grew no `o.get(\"static_profile\", <a default>)` fallback
|
||||
'get("static_profile"' not in _MAIN and "static_status(" in _MAIN)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print()
|
||||
print("9. TEST-16 - the two arbitrage rungs, pinned from BOTH sides and "
|
||||
"un-masked from each other")
|
||||
# SAFETY-26's AC4 gave the same tolerance mutation to every price and quantity
|
||||
# boundary in arbitrage.py and static_tariff.py. Everything in arbitrage.py went
|
||||
# red. The two survivors are the two rungs graded below, and section 2 above is
|
||||
# why they survived: it probes the flat rung with an EXACTLY FLAT series and
|
||||
# never probes the firm-pair rung at all.
|
||||
#
|
||||
# D `spread == 0.0` weakened to `spread <= 0.001` SURVIVED (+2 B). An
|
||||
# exactly flat series refuses under both spellings, so a tolerance is
|
||||
# invisible to a flat probe. It is NOT cosmetic: a real 0.0005 spread
|
||||
# flips ALLOWED -> REFUSED at min_spread_eur_kwh=0.0, which arbitrage()'s
|
||||
# own docstring documents as supported ("pass 0.0 to mean any positive
|
||||
# spread, explicitly") and which its validator at :861 accepts on purpose.
|
||||
#
|
||||
# F `len(firm) < 2` weakened to `< 1` SURVIVED, at +0 BYTES - the delta
|
||||
# MUTATION.md names as the risky one.
|
||||
#
|
||||
# ⚠️ AND THE FINDING IS THE INTERACTION, NOT EITHER ROW. D AND F MASK EACH
|
||||
# OTHER. A single firm hour has a spread of 0.0, so with F weakened the FLAT
|
||||
# rung catches the stimulus and the verdict is still "refused"; with D weakened
|
||||
# the tiny-spread stimulus is still refused, just for a lie. Each rung's failure
|
||||
# is absorbed by the other, so BOTH can be weakened together and a suite that
|
||||
# grades only `kind` reports nothing. That is invisible to single-mutation
|
||||
# testing, which is how this pair survived four rounds of review on TARIFF-03
|
||||
# and FCST-03.
|
||||
#
|
||||
# TWO THINGS FOLLOW, and they shape every check below.
|
||||
#
|
||||
# 1. THE REASON IS GRADED, NOT JUST THE REFUSAL. The masking case REFUSES
|
||||
# CORRECTLY WHILE REPORTING THE WRONG SENTENCE - an operator reading "the
|
||||
# tariff is flat" about a tariff that is not flat, or reading it about a
|
||||
# series whose real problem is that §8.3 has no firm pair to trade on. A
|
||||
# check that asserts only "refused" passes on a broken tree. So each
|
||||
# stimulus below is graded on WHICH RUNG ANSWERED IT.
|
||||
# 2. EACH RUNG NEEDS A STIMULUS THE OTHER CANNOT ABSORB, or the pinning is
|
||||
# circular. The flat rung gets a series with THREE firm hours, where the
|
||||
# firm-pair rung cannot fire under any weakening down to `< 0`. The
|
||||
# firm-pair rung gets a ONE-FIRM series, which the flat rung CAN answer -
|
||||
# so its attribution is the whole check, and that is why row 2 of the table
|
||||
# is written as a rung name rather than as a verdict.
|
||||
#
|
||||
# ⚠️ SHIPPED BEHAVIOUR IS UNCHANGED BY THIS SECTION. Both rungs are correct
|
||||
# today; nothing in app/ was touched. This is the missing measurement, not a fix.
|
||||
#
|
||||
# THE LEDGER. Eight rounds through `tools/mutation_run.sh` (TEST-15): recursive
|
||||
# `__pycache__` purge before each, `python3 -B`, the tree re-copied FROM DISK
|
||||
# between rounds, the mutated line grepped out of the file before the run, and
|
||||
# the byte delta printed. `test_static_tariff.py` and `test_arbitrage.py` graded
|
||||
# each time. Both suites green on the unmutated tree.
|
||||
#
|
||||
# round delta test_static_tariff.py
|
||||
# --------------------------------------------- ------ ---------------------
|
||||
# baseline, shipped +0 B PASS
|
||||
# D1 `spread == 0.0` -> `<= 0.001` +2 B RED, 3 checks
|
||||
# D2 `spread == 0.0` -> `<= 0.000001` +5 B RED, AC1 alone
|
||||
# D3 `spread == 0.0` -> `< 0.0` (rung gone) +0 B RED (section 2 too)
|
||||
# F1 `len(firm) < 2` -> `< 1` +0 B RED, 3 checks
|
||||
# F2 `len(firm) < 2` -> `< 3` +0 B RED, 6 checks
|
||||
# F3 `len(firm) < 2` -> `< 0` (rung gone) +0 B RED (and ValueError)
|
||||
# D1+F1 BOTH AT ONCE - THE TICKET +2 B RED, 5 checks
|
||||
#
|
||||
# Read the rows in pairs. D1 and F1 are SAFETY-26's two survivors and each is
|
||||
# now red on its own; D2 is the tolerance bisected to the rung's own resolution,
|
||||
# so "pinned at exactly zero" is measured rather than asserted at one epsilon;
|
||||
# F2 and F3 pin the firm-pair count from ABOVE and BELOW, which the ticket's
|
||||
# criteria do not ask for but which is what stops this section being one-sided
|
||||
# in the way it was written to fix. THE LAST ROW IS THE TICKET: with BOTH rungs
|
||||
# weakened together - the arrangement that reported nothing before this section
|
||||
# existed - five checks go red, including AC1 and AC2, because neither of them
|
||||
# is spelled in terms of the other rung's behaviour.
|
||||
#
|
||||
# ⚠️ FOUR OF THOSE SEVEN MUTATIONS ARE +0 BYTES, which MUTATION.md names as the
|
||||
# delta stale bytecode can hide. They are all REDS, the direction the bytecode
|
||||
# defect cannot fabricate - but they were run under the purge anyway, because
|
||||
# "it went red so the cache cannot have lied" is a conclusion, not a procedure.
|
||||
|
||||
_A9 = dict(peak_guard_active=False, min_spread_eur_kwh=0.0,
|
||||
uncertainty=frozenset())
|
||||
|
||||
|
||||
def plan9(prices, provisional=None, product=PriceProduct.DAY_AHEAD):
|
||||
"""A bare hourly series, built here rather than through a profile.
|
||||
|
||||
A `static_profile` cannot express a 0.0005 spread or a part-provisional day,
|
||||
and going through one would grade the profile generator as well. These are
|
||||
the smallest series that put exactly one rung in question.
|
||||
"""
|
||||
flags = provisional if provisional is not None else [False] * len(prices)
|
||||
pts = [PricePoint(start=brussels(THU + timedelta(hours=i)),
|
||||
eur_per_kwh=p, provisional=v)
|
||||
for i, (p, v) in enumerate(zip(prices, flags))]
|
||||
return TariffPlan(PriceRead.series(pts, product, "generic_dayahead", THU),
|
||||
None, "provider")
|
||||
|
||||
|
||||
def rung9(verdict):
|
||||
"""WHICH refusal answered, named from the reason the operator would read.
|
||||
|
||||
Deliberately not a code inspection: the sentence in the log IS the artefact
|
||||
that degrades when these two rungs mask each other, so it is the sentence
|
||||
that gets graded. An unrecognised refusal is reported as such rather than
|
||||
quietly matching nothing - "no rung fired" and "a rung I did not list fired"
|
||||
must not grade the same.
|
||||
"""
|
||||
if verdict.kind == "allowed":
|
||||
return "allowed"
|
||||
r = verdict.reason
|
||||
hit = [n for n, needle in (("flat", "the tariff is flat"),
|
||||
("firm-pair", "carry a published price"),
|
||||
("threshold", "needs"),
|
||||
("peak", "peak guard"),
|
||||
("no-series", "No series exists"))
|
||||
if needle in r]
|
||||
return hit[0] if len(hit) == 1 else f"UNATTRIBUTED<{r[:60]}>"
|
||||
|
||||
|
||||
# --- the sweep D survived: a small but REAL spread, at min_spread 0.0 --------
|
||||
# ⚠️ NOT AN EXACTLY FLAT SERIES, which is the whole point - a flat probe cannot
|
||||
# see a tolerance. The threshold is 0.0 so the `spread < need` rung below cannot
|
||||
# fire either (required_spread(0.0, anything) is 0.0, asserted first), leaving
|
||||
# the flat rung as the only thing that can refuse these.
|
||||
check(f"the widened threshold at min_spread_eur_kwh=0.0 is still 0.0, so the "
|
||||
f"probes below are graded by the FLAT rung and not by the threshold rung "
|
||||
f"({required_spread(0.0, uncertainty_of(PriceProduct.DAY_AHEAD)):.6f})",
|
||||
near(required_spread(0.0, uncertainty_of(PriceProduct.DAY_AHEAD)), 0.0))
|
||||
|
||||
# 1e-6 is the floor of the question, not of the check: app/static_tariff.py
|
||||
# rounds the spread to 6 decimals before comparing, so a 5e-7 difference IS
|
||||
# zero to this rung by construction. Graded as its own row below rather than
|
||||
# left as an unstated edge.
|
||||
TINY9 = (0.0005, 0.0001, 0.00001, 0.000001)
|
||||
_tiny9 = [(d, plan9([0.20000, round(0.20000 + d, 6)])) for d in TINY9]
|
||||
check(f"each tiny-spread probe really carries the spread it is named for, and "
|
||||
f"two FIRM hours - so the firm-pair rung cannot be what answers them "
|
||||
f"{[(f'{d:.6f}', f'{p.spread_eur_kwh():.6f}') for d, p in _tiny9]}",
|
||||
all(near(p.spread_eur_kwh(), d, 1e-12)
|
||||
and len([q for q in p.read.points if not q.provisional]) == 2
|
||||
for d, p in _tiny9))
|
||||
_tinyv9 = [(d, rung9(p.arbitrage(**_A9))) for d, p in _tiny9]
|
||||
check(f"TEST-16 AC1: a spread that is SMALL BUT REAL is ALLOWED at "
|
||||
f"min_spread_eur_kwh=0.0 - down to 0.000001 EUR/kWh, so the flat rung is "
|
||||
f"`spread == 0.0` and not `spread <= epsilon` for any epsilon "
|
||||
f"{_tinyv9}",
|
||||
all(r == "allowed" for _d, r in _tinyv9))
|
||||
check("...and the exactly flat series on the OTHER side of that boundary is "
|
||||
"still refused, and refused BY THE FLAT RUNG - without this the sweep "
|
||||
"above would also pass for a rung that had been deleted outright",
|
||||
rung9(plan9([0.20000, 0.20000]).arbitrage(**_A9)) == "flat")
|
||||
_q9 = plan9([0.20000, 0.2000005])
|
||||
check(f"...and a difference below the rung's own 6-decimal rounding is flat BY "
|
||||
f"CONSTRUCTION and says so: the raw difference is "
|
||||
f"{_q9.spread_eur_kwh():.7f} and non-zero, `round(..., 6)` makes it "
|
||||
f"exactly 0.0, and the rung agrees. That is the QUANTISATION the shipped "
|
||||
f"line chooses in the open, not a tolerance smuggled into the comparison "
|
||||
f"- and it is why the sweep above stops at 0.000001 rather than pretending "
|
||||
f"to bisect further",
|
||||
not near(_q9.spread_eur_kwh(), 0.0, 1e-9)
|
||||
and near(round(_q9.spread_eur_kwh(), 6), 0.0, 1e-12)
|
||||
and rung9(_q9.arbitrage(**_A9)) == "flat")
|
||||
|
||||
# --- and what D's survival actually costs: the sentence, not the verdict -----
|
||||
_lie9 = plan9([0.20000, 0.20050]).arbitrage(**_A9)
|
||||
check(f"TEST-16 AC4: the 0.0005 verdict carries the real spread rather than a "
|
||||
f"flatness claim about a tariff that is not flat - the false sentence is "
|
||||
f"what a loosened rung ships to the operator while the outcome at a "
|
||||
f"shipped threshold looks unchanged ({_lie9.kind})",
|
||||
_lie9.kind == "allowed" and near(_lie9.spread_eur_kwh, 0.0005)
|
||||
and near(_lie9.cheapest.eur_per_kwh, 0.20000)
|
||||
and near(_lie9.dearest.eur_per_kwh, 0.20050))
|
||||
|
||||
# --- the rung F survived: ON ITS OWN, not through the flat rung --------------
|
||||
# ⚠️ THIS ROW IS THE ANTI-MASKING ONE. A one-firm series has a spread of 0.0,
|
||||
# so with `< 2` weakened to `< 1` the FLAT rung refuses it and `kind` never
|
||||
# moves. The only thing that moves is the sentence: §8.3's honest "only 1 of 3
|
||||
# hours carry a published price" degrades to "the tariff is flat" about a series
|
||||
# whose firm and provisional prices differ by 0.20 EUR/kWh. Grading the rung
|
||||
# name is what makes `< 1` red without borrowing anything from the flat rung.
|
||||
_one9 = plan9([0.20, 0.40, 0.10], [False, True, True])
|
||||
check(f"the one-firm probe is well formed: exactly ONE firm hour out of three, "
|
||||
f"and its full series is anything but flat (spread "
|
||||
f"{max(p.eur_per_kwh for p in _one9.read.points) - min(p.eur_per_kwh for p in _one9.read.points):.2f} "
|
||||
f"EUR/kWh) - so 'flat' would be a false sentence about it",
|
||||
len([p for p in _one9.read.points if not p.provisional]) == 1
|
||||
and len(_one9.read.points) == 3)
|
||||
check("TEST-16 AC2: one firm hour is refused BY §8.3's FIRM-PAIR RUNG, named "
|
||||
"in the reason - not by the flat rung that would also have refused it. "
|
||||
"This is what takes `len(firm) < 2` -> `< 1` red on its own",
|
||||
rung9(_one9.arbitrage(**_A9)) == "firm-pair")
|
||||
check("...and the sentence is §8.3's in full, so the operator is told the "
|
||||
"series is half-published rather than told it is fixed-price",
|
||||
"§8.3" in _one9.arbitrage(**_A9).reason
|
||||
and "only 1 of 3" in _one9.arbitrage(**_A9).reason
|
||||
and "flat" not in _one9.arbitrage(**_A9).reason)
|
||||
# The rung pinned from ABOVE as well. Without this, `len(firm) < 3` - the same
|
||||
# +0 B edit in the other direction - would pass everything above.
|
||||
_two9 = plan9([0.20, 0.40, 0.10], [False, False, True])
|
||||
check("...and TWO firm hours are ALLOWED on the identical series shape, so the "
|
||||
"rung is pinned from above too and `< 3` is red as well as `< 1`",
|
||||
rung9(_two9.arbitrage(**_A9)) == "allowed"
|
||||
and near(_two9.arbitrage(**_A9).spread_eur_kwh, 0.20))
|
||||
# ...and from below, where the failure is not a wrong sentence but a crash:
|
||||
# `max()` over an empty `firm` raises ValueError, so `< 0` does not degrade the
|
||||
# reason, it takes the verdict away entirely.
|
||||
_zero9 = plan9([0.20, 0.40, 0.10], [True, True, True])
|
||||
check("...and a FULLY provisional series is refused by the same rung and does "
|
||||
"not raise - `max()` over an empty firm set is a ValueError, so this row "
|
||||
"is what makes `len(firm) < 0` red",
|
||||
rung9(derived(lambda: _zero9.arbitrage(**_A9))) == "firm-pair")
|
||||
|
||||
# --- AC3: the two rungs do not mask each other ------------------------------
|
||||
# ⚠️ THE POINT OF THE TICKET, AND IT IS A PROPERTY OF THE TABLE, NOT OF A ROW.
|
||||
# Masking is possible exactly when every stimulus that one rung answers could
|
||||
# also be answered by the other with an acceptable-looking result. The table
|
||||
# below denies that on both sides:
|
||||
#
|
||||
# * the flat rung is asked a question the firm-pair rung CANNOT reach - three
|
||||
# firm hours, where `len(firm) < N` refuses for no N a person would write;
|
||||
# * the firm-pair rung is asked a question the flat rung CAN reach - one firm
|
||||
# hour - and the answer is graded on WHICH rung gave it, so the flat rung
|
||||
# absorbing it is a failure rather than a pass;
|
||||
# * and the tiny-spread row is allowed, which neither rung may refuse.
|
||||
#
|
||||
# Verified by running BOTH mutations at once (`spread <= 0.001` AND
|
||||
# `len(firm) < 1`, purged harness, +2 B and +0 B): three rows move. Under
|
||||
# single-mutation testing this arrangement reports nothing at all.
|
||||
_TABLE9 = (
|
||||
("three firm hours, all 0.2000", plan9([0.2, 0.2, 0.2]), "flat"),
|
||||
("two firm hours, 0.0005 apart", plan9([0.20000, 0.20050]), "allowed"),
|
||||
("one firm hour of three", _one9, "firm-pair"),
|
||||
("two firm hours of three", _two9, "allowed"),
|
||||
("no firm hours at all", _zero9, "firm-pair"),
|
||||
)
|
||||
_got9 = [(name, want, rung9(derived(lambda p=pl: p.arbitrage(**_A9))))
|
||||
for name, pl, want in _TABLE9]
|
||||
check(f"TEST-16 AC3: each stimulus is answered by the rung that OWNS it, and "
|
||||
f"by no other - the flat rung is asked a question the firm-pair rung "
|
||||
f"cannot reach, and the firm-pair rung is asked one the flat rung could "
|
||||
f"reach but must not be allowed to. Weakening EITHER rung moves a row; "
|
||||
f"weakening BOTH TOGETHER still moves three "
|
||||
f"{[(n, w, g) for n, w, g in _got9]}",
|
||||
all(want == got for _n, want, got in _got9))
|
||||
check("...and the table really does exercise both rungs and the allowing path, "
|
||||
"so 'every row matched' is not a statement about one rung repeated",
|
||||
{want for _n, _p, want in _TABLE9} == {"flat", "firm-pair", "allowed"})
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print()
|
||||
if fails:
|
||||
|
||||
Reference in New Issue
Block a user