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], ...]
|
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
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user