From 1a9927b1e6ae7fae8d449c55bd3cb4019d12b7e1 Mon Sep 17 00:00:00 2001 From: Matt Spinola Date: Sun, 2 Aug 2026 23:13:03 -0400 Subject: [PATCH] fix(deps): raise the crucible floor to 0.6.0, and guard the floor itself --arl0-years builds Thresholds(monitor_arl0_years=...), added in crucible 0.6.0, but it merged while the pin still read crucible>=0.5.0. 0.5.0's Thresholds has no such field, so a fresh `pip install crucible-stack` could resolve a crucible that raises TypeError the first time anyone passes the flag. It resolves, it installs, and it fails at the call site. Nothing caught it, and nothing could have. A floor is only ever exercised by the version you do NOT have, and every checkout in this workspace already ran 0.6.0. The existing compat tests run against whatever is installed, which is normally the newest thing, so they confirm the API exists without saying anything about the oldest version the pin still admits. So this adds a test that reads the declared floor out of pyproject.toml and compares it against a constant naming the newest crucible feature the package touches. Verified it actually fires: reverting the pin to 0.5.0 fails it. A guard that cannot fail is the thing this whole class of bug is made of. Also adds a compat check for Thresholds.monitor_arl0_years in the existing style, so the failure names its own cause rather than surfacing as a TypeError from a dataclass constructor, and corrects a stale comment that still warned the 0.5.0 constraint was aspirational. 407 tests pass, ruff clean. Co-Authored-By: Claude Opus 5 --- pyproject.toml | 17 +++++++++------ tests/test_crucible_compat.py | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 682fef1..1072b4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,12 +36,17 @@ dependencies = [ # not evidence that an API is present, which is the whole reason that test exists. # # RAISED TO 0.5.0 for crucible.validation.monitor (EdgeBaseline / edge_monitor), - # which orchestrate.decay and EdgeDecayTrigger are built on. Unlike the 0.3.0 note - # above, this constraint is NOT yet true: at the time of writing the monitor sits in - # crucible's [Unreleased] and the newest published crucible is 0.4.0. This must not - # merge before crucible 0.5.0 is on PyPI, or `pip install crucible-stack` resolves - # to a crucible without the monitor and `import crucible_stack.orchestrate` raises. - "crucible>=0.5.0", + # which orchestrate.decay and EdgeDecayTrigger are built on. That note used to warn + # the constraint was not yet true; crucible 0.5.0 published 2026-08-02, so it is. + # + # RAISED TO 0.6.0 for Thresholds.monitor_arl0_years, which --arl0-years constructs. + # This one was briefly WRONG rather than aspirational: the flag merged while the + # floor still said 0.5.0, whose Thresholds has no such field, so a fresh + # `pip install crucible-stack` could resolve a crucible that raises TypeError the + # first time anyone passed it. Nothing caught it, because every local checkout + # already had 0.6.0 installed and a floor is only exercised by the version you do + # NOT have. Raise the floor in the same change as the API, never afterwards. + "crucible>=0.6.0", ] [project.optional-dependencies] diff --git a/tests/test_crucible_compat.py b/tests/test_crucible_compat.py index 870a77f..4aa4ef6 100644 --- a/tests/test_crucible_compat.py +++ b/tests/test_crucible_compat.py @@ -72,3 +72,42 @@ def test_edge_monitor_still_refuses_to_rebuild_its_own_baseline(): "trades", "baseline", "design", "thresholds"}, ( "edge_monitor's signature changed. The decay trigger depends on there being no " "parameter from which a baseline could be rebuilt; re-check before relaxing this.") + + +def test_the_false_alarm_budget_can_be_set_in_years(): + """`--arl0-years` builds `Thresholds(monitor_arl0_years=...)`, added in crucible + 0.6.0. On 0.5.0 that field does not exist and the dataclass raises TypeError the + first time anyone passes the flag, which is a long way from the version constraint + that allowed it.""" + from crucible.validation import Thresholds + try: + t = Thresholds(monitor_arl0_years=10.0) + except TypeError as exc: # pragma: no cover + pytest.fail( + f"the installed crucible predates the calendar-time budget ({exc}). " + "--arl0-years requires Thresholds.monitor_arl0_years; install crucible " + ">= 0.6.0.") + assert t.monitor_arl0_years == 10.0 + # and the trades fallback is still there for a baseline with no known firing rate + assert getattr(t, "monitor_arl0_trades", None) is not None + + +def test_the_floor_is_not_lower_than_the_apis_this_package_uses(): + """The floor is a claim about the OLDEST crucible that works, and the tests above + only ever run against whatever is installed, which is normally the newest. So check + the declared floor itself, and fail when someone adds an API without raising it. + + Keep `MIN` in step with the `crucible>=` pin in pyproject.toml and with the newest + crucible feature this package touches. + """ + import pathlib + import re + + MIN = "0.6.0" # bump with the pin when a newer API is adopted + text = (pathlib.Path(__file__).resolve().parents[1] / "pyproject.toml").read_text() + m = re.search(r'"crucible>=([0-9.]+)"', text) + assert m, "no crucible floor found in pyproject.toml" + assert m.group(1) == MIN, ( + f"pyproject pins crucible>={m.group(1)} but this package uses APIs added in " + f"{MIN}. A floor satisfied by a crucible that cannot run the code is worse than " + "no floor: it resolves, installs, and fails at the call site.")