diff --git a/src/scribe/services/shape_ledger.py b/src/scribe/services/shape_ledger.py index fc657d8..8556e68 100644 --- a/src/scribe/services/shape_ledger.py +++ b/src/scribe/services/shape_ledger.py @@ -1570,12 +1570,22 @@ def _semantic_priority(row) -> tuple: # not evidence (see _SEMANTIC_LIMIT) — but the proposals themselves did not # change, so no re-examination is owed; a stale miss basis is inert. # v5: the semantic arm's floor dropped from 0.8 to the write-path floor (#4208), -# so rows it examined and found nothing for must be read once more. +# and the signature floor from 0.8 to 0.75 (#4306), so rows examined and +# found nothing for must be read once more. _PROPOSER_VERSION = 5 # Signature resemblance floor, name blanked (difflib ratio) — and a length # floor, because `def NAME():` resembles `def NAME(x):` at 0.95 while saying # nothing; a family shape has parameters to resemble. -_SIGNATURE_FLOOR = 0.8 +# +# A proposal is read before it is confirmed, so this admits the band where +# true instances live rather than the band where nothing is wrong (#4306). +# Measured 2026-09-22 over 459 judged instance rows against every same-family +# canon: true instances score 1.0 or ~0.795 (a model class whose mixin list +# differs from the canon's), and 0.8 cut the whole second band. At 0.75 all +# of them clear; the price is `async def NAME(user_id: int, …) -> dict` +# look-alikes (35 wrong-canon pairings vs 14 at 0.8), which read as wrong at +# a glance. Below 0.75 the next true row is at 0.6 behind 150+ wrong ones. +_SIGNATURE_FLOOR = 0.75 _SIGNATURE_MIN_LEN = 30 # Textual containment needs enough substance to mean anything. _TEXT_FLOOR = 40 diff --git a/tests/test_shape_ledger.py b/tests/test_shape_ledger.py index 70fc648..61a3ef3 100644 --- a/tests/test_shape_ledger.py +++ b/tests/test_shape_ledger.py @@ -180,6 +180,22 @@ def test_signature_similarity_blanks_the_names(): assert sim("def helper(x):", "helper", "def make_app():", "make_app") == 0.0 +def test_a_model_with_a_different_mixin_list_is_proposed_against_the_model_canon(): + """The band 0.8 cut (#4306). Measured over judged instances: a model class + whose mixins differ from the canon's scores ~0.795 against it — a true + instance, and the proposal is read before it is confirmed, so the floor + must admit it.""" + from scribe.services.shape_ledger import Canon, _norm_text, match_canon + + model = Canon(44, "sym", "Gadget", (("src/app/models/gadget.py", "Gadget"),), + "class Gadget(Base, TimestampMixin, SoftDeleteMixin):", + _norm_text(""), 0, "python") + hit = match_canon("sym", "src/app/models/widget.py", "Widget", + "class Widget(Base, TimestampMixin):", " pass", [model]) + assert hit is not None and hit[:2] == (44, "signature") + assert 0.75 <= hit[2] < 0.8 + + def test_text_containment_is_whitespace_insensitive_with_a_floor(): from scribe.services.shape_ledger import text_contains