diff --git a/src/scribe/services/shape_ledger.py b/src/scribe/services/shape_ledger.py index f0ccdcf..bbe89c7 100644 --- a/src/scribe/services/shape_ledger.py +++ b/src/scribe/services/shape_ledger.py @@ -710,6 +710,14 @@ _DERIVE_MIN_NAME = 3 # Semantic checks per repo per refresh — an embedding each (local fastembed), # bounded so a 4,000-row ledger is worked through over refreshes, not in one. _SEMANTIC_CAP = 150 +# The semantic basis's own floor — stricter than the write-path arm's, because +# a code BODY against a prose-forward snippet document scores 0.68–0.75 for +# "both are about migrations"; first live run paired every alembic +# upgrade()/downgrade() with an unrelated canon at exactly that band. +_SEMANTIC_FLOOR = 0.8 +# Bump when a basis's rule changes: rows remember the (body, ruleset) they +# were examined under, so a tightened rule re-examines everything once. +_PROPOSER_VERSION = 2 # 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. @@ -823,13 +831,16 @@ async def canon_catalog(user_id: int) -> list[Canon]: symbol = (fields.get("symbol") or "").strip() kind = snippet_kind(symbol, fields.get("language") or "") code = fields.get("code") or "" - defs = extract_definitions(code) - signature = "" - if defs: - own = next( - (d for d in defs if _norm_symbol(d.name) == _norm_symbol(symbol)), defs[0] - ) - signature = own.signature + # The signature basis matches against the canon's OWN definition + # line — never against an example in its code. A snippet whose code + # opens with a call-site example (confirmed()'s does) would otherwise + # make every `async function x(): Promise` resemble it. + own = next( + (d for d in extract_definitions(code) + if symbol and _norm_symbol(d.name) == _norm_symbol(symbol)), + None, + ) + signature = own.signature if own else "" out.append(Canon( int(note.id), kind, symbol, tuple( @@ -867,7 +878,8 @@ async def _semantic_canon( return None query = concept_query(body) or body hits = await semantic_search_notes( - user_id, query, limit=3, threshold=WRITEPATH_DEFAULT_THRESHOLD, + user_id, query, limit=3, + threshold=max(WRITEPATH_DEFAULT_THRESHOLD, _SEMANTIC_FLOOR), note_type="snippet", scope="browse", ) for score, note in hits: @@ -914,7 +926,8 @@ async def propose_for_repo( if d is None: continue signature, body_sha, body = d[3], d[4], d[5] - if row.proposed_at is not None and row.proposed_sha == body_sha: + examined_as = f"{body_sha}@{_PROPOSER_VERSION}" + if row.proposed_at is not None and row.proposed_sha == examined_as: continue examined += 1 group = row.proposal_group # derive grouping is reassigned below @@ -925,7 +938,7 @@ async def propose_for_repo( _clear_proposal(row) row.proposal_group = group row.proposed_at = now - row.proposed_sha = body_sha + row.proposed_sha = examined_as if hit: row.proposed_snippet_id, row.proposal_basis, row.proposal_score = hit row.proposal_group = None