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
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:
@@ -685,6 +685,7 @@ class Canon(NamedTuple):
|
||||
locations: tuple[tuple[str, str], ...]
|
||||
signature: str
|
||||
code_norm: str
|
||||
project_id: int = 0
|
||||
|
||||
|
||||
def _norm_text(text: str) -> str:
|
||||
@@ -718,17 +719,20 @@ def text_contains(body: str, code: str) -> bool:
|
||||
|
||||
def match_canon(
|
||||
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:
|
||||
"""The strongest (snippet_id, basis, score) a shape earns against the
|
||||
canon catalog, by the non-semantic bases — or None. Strongest by
|
||||
basis order, then by score within the basis."""
|
||||
best: dict[str, tuple[float, int]] = {}
|
||||
basis order, then by score within the basis; on a tie, a canon recorded
|
||||
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)
|
||||
if cur is None or score > cur[0]:
|
||||
best[basis] = (score, sid)
|
||||
same = bool(project_id) and canon.project_id == project_id
|
||||
if cur is None or (score, same) > (cur[0], cur[1]):
|
||||
best[basis] = (score, same, canon.snippet_id)
|
||||
|
||||
norm_sym = _norm_symbol(symbol)
|
||||
for c in canons:
|
||||
@@ -736,19 +740,19 @@ def match_canon(
|
||||
continue
|
||||
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):
|
||||
offer("symbol", 1.0, c.snippet_id)
|
||||
offer("symbol", 1.0, c)
|
||||
continue # its own location is canonical territory, not a proposal
|
||||
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):
|
||||
offer("text", 0.95, c.snippet_id)
|
||||
offer("text", 0.95, c)
|
||||
if kind == "sym" and c.signature:
|
||||
ratio = signature_similarity(signature, symbol, c.signature, c.symbol)
|
||||
if ratio >= _SIGNATURE_FLOOR:
|
||||
offer("signature", round(ratio, 3), c.snippet_id)
|
||||
offer("signature", round(ratio, 3), c)
|
||||
for basis in _BASIS_ORDER:
|
||||
if basis in best:
|
||||
score, sid = best[basis]
|
||||
score, _same, sid = best[basis]
|
||||
return (sid, basis, score)
|
||||
return None
|
||||
|
||||
@@ -790,7 +794,7 @@ async def canon_catalog(user_id: int) -> list[Canon]:
|
||||
((loc.get("path") or ""), (loc.get("symbol") or ""))
|
||||
for loc in fields.get("locations") or []
|
||||
),
|
||||
signature, _norm_text(code),
|
||||
signature, _norm_text(code), int(note.project_id or 0),
|
||||
))
|
||||
return out
|
||||
|
||||
@@ -872,7 +876,10 @@ async def propose_for_repo(
|
||||
continue
|
||||
examined += 1
|
||||
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)
|
||||
row.proposal_group = group
|
||||
row.proposed_at = now
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
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
|
||||
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():
|
||||
|
||||
Reference in New Issue
Block a user