"""An act surfaces a SET of rules, and rank decides how loudly (#3851). WHY THIS EXISTS `RULEHINT_LIMIT` was 1. That was right while retrieval merely SUPPLEMENTED a 33-rule resident set — one salient rule beside everything already loaded. It stops being right the moment milestone 394 removes residency, because then this arm is the whole delivery, and `git push origin dev` is governed by rules 1, 2, 9 and 140 at once, each of which alone permits the mistake the others catch. Two instruments, and they answer different questions: - `_rule_band` decides HOW MANY. A fixed k fills its slots whether or not anything deserves them; a band keeps only what scored close to the top, so one clearly-relevant rule still shows one. - `compact` decides HOW LOUD. Measured at #3851: a full line is ~143 tokens once the trigger is rendered, so five of them cost ~646 before every Bash call. Top-full-plus-references costs ~198. WHAT THIS PINS Structure, never wording — the lines are prose and will be rewritten: 1. The band keeps the top hit and everything within `_RULEHINT_BAND`, and drops what falls outside. Falsified below from both sides: a hit just inside survives, a hit just outside does not. 2. Rank decides volume — the first line carries the trigger, later lines do not, and every line names its rule's id so any of them can be pulled. 3. The band reads SCORES ONLY. A top hit the session has already seen still anchors the band, and its score still sets the cutoff. This is the axis independence the renderer already keeps between `kind` and `seen`, and the regression it prevents is subtle: letting the ledger reorder the band would make "you were told this" change what counts as relevant. The band width itself is deliberately NOT pinned. It is a tuning value with a comment recording the measurement behind it, and a test asserting 0.05 would fail on every future retune while proving nothing about behaviour — so the cases below express their scores as offsets from the constant. """ import pytest from scribe.services.plugin_context import ( _RULEHINT_BAND, _rule_band, _rule_hint_line, ) from tests.helpers import fake_rule _TRIGGER = "about to run git push with an earlier CI run still unread" def _hit(score: float, rule_id: int): return (score, fake_rule(id=rule_id, title=f"rule {rule_id}", when_to_apply=_TRIGGER)) def test_an_empty_result_stays_empty(): """No hits is not a crash and not a phantom line.""" assert _rule_band([]) == [] def test_the_band_keeps_a_hit_just_inside_it(): """The whole point: a close second rule reaches the agent.""" top = 0.75 hits = [_hit(top, 1), _hit(top - _RULEHINT_BAND + 0.01, 2)] assert [r.id for _s, r in _rule_band(hits)] == [1, 2] def test_the_band_drops_a_hit_just_outside_it(): """And the band must actually BIND, or it is a fixed k wearing a hat.""" top = 0.75 hits = [_hit(top, 1), _hit(top - _RULEHINT_BAND - 0.01, 2)] assert [r.id for _s, r in _rule_band(hits)] == [1] def test_one_clearly_better_rule_still_surfaces_alone(): """The behaviour the old limit of 1 got right, which must not regress. A moment with a single relevant rule shows one line, because the corpus said so — not because a constant capped it. """ hits = [_hit(0.80, 1), _hit(0.55, 2), _hit(0.54, 3)] assert [r.id for _s, r in _rule_band(hits)] == [1] def test_a_flat_cluster_surfaces_together(): """Measured shape of this corpus: adjacent rules sit ~0.02 apart. Four rules governing one act is the `git push` case the step exists for, and at the measured spacing they must arrive together rather than the ranker picking one of four near-ties. """ hits = [_hit(0.757, 2), _hit(0.735, 7), _hit(0.726, 1), _hit(0.711, 9)] assert [r.id for _s, r in _rule_band(hits)] == [2, 7, 1, 9] def test_the_band_is_computed_from_scores_not_from_the_ledger(): """A seen top hit still anchors the band (#3750 x #3851). `_rule_band` never learns what the session has seen — dedup happens after it, in the arms. Pinned here because the tempting "reorder so a fresh rule leads" would silently change the cutoff, and the failure is invisible: the arm would still emit lines, just the wrong set. """ hits = [_hit(0.80, 1), _hit(0.78, 2), _hit(0.60, 3)] kept = _rule_band(hits) # Independent of any `already` set, because it is not consulted. assert [r.id for _s, r in kept] == [1, 2] @pytest.mark.parametrize("seen", [False, True]) def test_the_leading_line_carries_the_trigger(seen): """Rank 0 gets the full rendering, on either tail.""" line = _rule_hint_line( fake_rule(id=4, title="dev is home", when_to_apply=_TRIGGER), where="to this Bash call", seen=seen, compact=False, ) assert _TRIGGER in line assert "get_rule(4)" in line @pytest.mark.parametrize("seen", [False, True]) def test_a_later_line_cites_its_rule_without_quoting_the_trigger(seen): """Rank > 0 is a reference: identity and pointer, no trigger. The trigger is the expensive half — 300-400 characters after #3855 — and the leading line has already demonstrated the shape. Both assertions matter: dropping the trigger is the saving, and keeping `get_rule(id)` is what makes the saving safe, because a cited rule the reader cannot pull is just noise. """ line = _rule_hint_line( fake_rule(id=4, title="dev is home", when_to_apply=_TRIGGER), where="to this Bash call", seen=seen, compact=True, ) assert _TRIGGER not in line assert "dev is home" in line assert "get_rule(4)" in line def test_shortening_a_line_does_not_decide_what_it_says_about_holding(): """A reference still tells a repeat from a first surfacing (#3750 x #3851). This is the regression the first cut of #3851 actually shipped: the compact branch dropped the tail along with the trigger, so a rule the session had already been told read exactly like one it had not. #3750's whole argument is that the two are different claims — a repeat is rendered precisely because the session may no longer HOLD what it was told — and the tail is the entire difference a reader can act on. `compact` and `seen` are independent axes. How much room a line gets is a fact about its rank; whether the session holds it is a fact about the ledger; and neither may be allowed to answer the other's question. """ rule = fake_rule(id=4, title="dev is home", when_to_apply=_TRIGGER) seen = _rule_hint_line(rule, where="here", seen=True, compact=True) fresh = _rule_hint_line(rule, where="here", seen=False, compact=True) assert seen != fresh assert "no longer hold it" in seen assert "not in this session's loaded set" in fresh def test_a_compact_line_is_materially_shorter_than_a_full_one(): """The cost claim, asserted rather than left in a comment. Not a token count — that would pin the tokenizer. Half the characters is the property that makes widening the arm affordable, and it is what fails if a later edit puts the trigger back into the compact branch. Measured against a REALISTIC trigger, because that is where the saving lives: the rules this arm carries run 300-400 characters of trigger after #3855, and a toy one-line trigger would make this pass on a compact branch that had stopped saving anything. """ long_trigger = ( "Opening or merging a `dev`->`main` pull request, running " "`git push origin main`, `git tag`, or minting a release, image tag " "or other public artifact. Also whenever CI has just gone green and " "the next step feels like shipping it, and whenever an earlier merge " "this session reads like standing permission for the next one." ) rule = fake_rule(id=4, title="dev is home", when_to_apply=long_trigger) full = _rule_hint_line(rule, where="here", seen=False, compact=False) compact = _rule_hint_line(rule, where="here", seen=False, compact=True) assert len(compact) * 2 < len(full) def test_a_preference_keeps_its_noun_when_compact(): """Force survives the shortening (#3849). `kind` and `compact` are independent axes. A preference rendered as a reference must still not read as a rule — the noun is the whole of the visual difference, so losing it in the compact branch would make every cited preference bind. """ line = _rule_hint_line( fake_rule(id=5, title="pace debugging", kind="preference", when_to_apply=_TRIGGER), where="here", seen=False, compact=True, ) assert "preference" in line.lower() assert "standing rule" not in line.lower()