fix(ledger): the proposer prefers a same-project canon on a tie — family canon elsewhere is the fallback, not the first hit
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 35s
CI & Build / TypeScript typecheck (push) Successful in 37s
CI & Build / Python tests (push) Successful in 1m17s
CI & Build / Build & push image (push) Successful in 40s

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 21:36:10 -04:00
co-authored by Claude Fable 5
parent ba0030e51d
commit 386b27e422
2 changed files with 37 additions and 15 deletions
+20 -13
View File
@@ -685,6 +685,7 @@ class Canon(NamedTuple):
locations: tuple[tuple[str, str], ...] locations: tuple[tuple[str, str], ...]
signature: str signature: str
code_norm: str code_norm: str
project_id: int = 0
def _norm_text(text: str) -> str: def _norm_text(text: str) -> str:
@@ -718,17 +719,20 @@ def text_contains(body: str, code: str) -> bool:
def match_canon( def match_canon(
kind: str, path: str, symbol: str, signature: str, body: str, kind: str, path: str, symbol: str, signature: str, body: str,
canons: Iterable[Canon], canons: Iterable[Canon], *, project_id: int = 0,
) -> tuple[int, str, float] | None: ) -> tuple[int, str, float] | None:
"""The strongest (snippet_id, basis, score) a shape earns against the """The strongest (snippet_id, basis, score) a shape earns against the
canon catalog, by the non-semantic bases — or None. Strongest by canon catalog, by the non-semantic bases — or None. Strongest by
basis order, then by score within the basis.""" basis order, then by score within the basis; on a tie, a canon recorded
best: dict[str, tuple[float, int]] = {} in the shape's own project beats family canon from another (the same
helper recorded in two projects is the shape's own first)."""
best: dict[str, tuple[float, bool, int]] = {}
def offer(basis: str, score: float, sid: int) -> None: def offer(basis: str, score: float, canon: Canon) -> None:
cur = best.get(basis) cur = best.get(basis)
if cur is None or score > cur[0]: same = bool(project_id) and canon.project_id == project_id
best[basis] = (score, sid) if cur is None or (score, same) > (cur[0], cur[1]):
best[basis] = (score, same, canon.snippet_id)
norm_sym = _norm_symbol(symbol) norm_sym = _norm_symbol(symbol)
for c in canons: for c in canons:
@@ -736,19 +740,19 @@ def match_canon(
continue continue
if c.symbol and _norm_symbol(c.symbol) == norm_sym: if c.symbol and _norm_symbol(c.symbol) == norm_sym:
if not any(location_covers(lp, ls, path, symbol) for lp, ls in c.locations): if not any(location_covers(lp, ls, path, symbol) for lp, ls in c.locations):
offer("symbol", 1.0, c.snippet_id) offer("symbol", 1.0, c)
continue # its own location is canonical territory, not a proposal continue # its own location is canonical territory, not a proposal
if c.symbol and references_symbol(body, c.symbol, kind): if c.symbol and references_symbol(body, c.symbol, kind):
offer("reference", 0.9, c.snippet_id) offer("reference", 0.9, c)
if c.code_norm and text_contains(body, c.code_norm): if c.code_norm and text_contains(body, c.code_norm):
offer("text", 0.95, c.snippet_id) offer("text", 0.95, c)
if kind == "sym" and c.signature: if kind == "sym" and c.signature:
ratio = signature_similarity(signature, symbol, c.signature, c.symbol) ratio = signature_similarity(signature, symbol, c.signature, c.symbol)
if ratio >= _SIGNATURE_FLOOR: if ratio >= _SIGNATURE_FLOOR:
offer("signature", round(ratio, 3), c.snippet_id) offer("signature", round(ratio, 3), c)
for basis in _BASIS_ORDER: for basis in _BASIS_ORDER:
if basis in best: if basis in best:
score, sid = best[basis] score, _same, sid = best[basis]
return (sid, basis, score) return (sid, basis, score)
return None return None
@@ -790,7 +794,7 @@ async def canon_catalog(user_id: int) -> list[Canon]:
((loc.get("path") or ""), (loc.get("symbol") or "")) ((loc.get("path") or ""), (loc.get("symbol") or ""))
for loc in fields.get("locations") or [] for loc in fields.get("locations") or []
), ),
signature, _norm_text(code), signature, _norm_text(code), int(note.project_id or 0),
)) ))
return out return out
@@ -872,7 +876,10 @@ async def propose_for_repo(
continue continue
examined += 1 examined += 1
group = row.proposal_group # derive grouping is reassigned below group = row.proposal_group # derive grouping is reassigned below
hit = match_canon(row.kind, row.path, row.symbol, signature, body, canons) hit = match_canon(
row.kind, row.path, row.symbol, signature, body, canons,
project_id=project_id,
)
_clear_proposal(row) _clear_proposal(row)
row.proposal_group = group row.proposal_group = group
row.proposed_at = now row.proposed_at = now
+17 -2
View File
@@ -190,9 +190,24 @@ def test_text_containment_is_whitespace_insensitive_with_a_floor():
assert not text_contains("x = 1", "x = 1") # below the substance floor assert not text_contains("x = 1", "x = 1") # below the substance floor
def _canon(sid, kind="sym", symbol="", locations=(), signature="", code=""): def _canon(sid, kind="sym", symbol="", locations=(), signature="", code="", project_id=0):
from scribe.services.shape_ledger import Canon, _norm_text from scribe.services.shape_ledger import Canon, _norm_text
return Canon(sid, kind, symbol, tuple(locations), signature, _norm_text(code)) return Canon(sid, kind, symbol, tuple(locations), signature, _norm_text(code), project_id)
def test_match_canon_prefers_the_shapes_own_project_on_a_tie():
"""The same helper recorded in two projects: the shape's own project's
record is its canon; family canon elsewhere is the fallback."""
from scribe.services.shape_ledger import match_canon
family = _canon(3, "sym", "slugify", [("lib/text.py", "slugify")], project_id=1)
own = _canon(4, "sym", "slugify", [("src/util/text.py", "slugify")], project_id=2)
assert match_canon("sym", "src/other.py", "slugify", "def slugify(t):", "",
[family, own], project_id=2) == (4, "symbol", 1.0)
assert match_canon("sym", "src/other.py", "slugify", "def slugify(t):", "",
[family, own], project_id=1) == (3, "symbol", 1.0)
# No project given → first-best stands; nothing breaks.
assert match_canon("sym", "src/other.py", "slugify", "def slugify(t):", "",
[family, own])[1] == "symbol"
def test_match_canon_orders_bases_strongest_first_and_respects_kind(): def test_match_canon_orders_bases_strongest_first_and_respects_kind():