Merge dev → main: family precedents say their reason once; the repeat trigger compares like with like #208
@@ -119,7 +119,15 @@ _LINEAGE = re.compile(
|
||||
_LINEAGE_WINDOW = 80
|
||||
|
||||
# Note kinds the repeat trigger compares: recorded knowledge and shapes, not
|
||||
# the to-do list.
|
||||
# the to-do list — and each only against its own kind (#5130). A repeat is the
|
||||
# same KIND of thing made twice: two shapes built, or two ideas written down.
|
||||
# Across kinds the score stops meaning that. One chunk of a long narrative
|
||||
# note that mentions a shape's vocabulary scores like the shape itself: a
|
||||
# dated release log matched a signed-APK CI snippet at 0.83, above this
|
||||
# trigger's floor, and opened the log as a candidate. The cross-kind relation
|
||||
# that IS real — code built from an idea written elsewhere — has its own
|
||||
# routes: the citation trigger when the lineage is named, and the shape ledger
|
||||
# once the idea is canon.
|
||||
_REPEAT_KINDS = ("snippet", "note")
|
||||
|
||||
MEMBER_STATES = ("declared", "detected")
|
||||
@@ -928,13 +936,15 @@ async def citation_trigger(user_id: int, note) -> str | None:
|
||||
|
||||
|
||||
async def repeat_trigger(user_id: int, note) -> str | None:
|
||||
"""A new record whose meaning repeats a record in ANOTHER project that
|
||||
shares a platform with this one opens an evaluation of the earlier record.
|
||||
Silent when the projects share no platform, below the threshold, or when
|
||||
the project has no platforms yet."""
|
||||
"""A new record whose meaning repeats a record OF THE SAME KIND in another
|
||||
project that shares a platform with this one opens an evaluation of the
|
||||
earlier record. Silent when the projects share no platform, below the
|
||||
threshold, when the project has no platforms yet, or when the only match
|
||||
is a different kind (`_REPEAT_KINDS` says why)."""
|
||||
from scribe.services.embeddings import embedding_text, semantic_search_notes
|
||||
|
||||
if not getattr(note, "project_id", None) or note.is_task:
|
||||
kind = getattr(note, "note_type", None) or "note"
|
||||
if not getattr(note, "project_id", None) or note.is_task or kind not in _REPEAT_KINDS:
|
||||
return None
|
||||
async with async_session() as session:
|
||||
mine = await _member_platform_ids(session, note.project_id)
|
||||
@@ -942,12 +952,14 @@ async def repeat_trigger(user_id: int, note) -> str | None:
|
||||
return None
|
||||
hits = await semantic_search_notes(
|
||||
user_id, embedding_text(note.title, note.body), exclude_ids={note.id},
|
||||
limit=8, threshold=REPEAT_THRESHOLD, scope="read", note_type=_REPEAT_KINDS,
|
||||
limit=8, threshold=REPEAT_THRESHOLD, scope="read", note_type=kind,
|
||||
is_task=False, demote_superseded=False,
|
||||
)
|
||||
for score, other in hits:
|
||||
if not other.project_id or other.project_id == note.project_id:
|
||||
continue
|
||||
if (other.note_type or "note") != kind:
|
||||
continue
|
||||
async with async_session() as session:
|
||||
shared = mine & await _member_platform_ids(session, other.project_id)
|
||||
if not shared or not await access.can_write_note(user_id, other.id):
|
||||
|
||||
@@ -278,6 +278,26 @@ async def test_a_repeat_with_no_shared_platform_is_silent(family):
|
||||
assert await _idea(family["note"]) is None
|
||||
|
||||
|
||||
async def test_a_repeat_across_kinds_is_silent(family):
|
||||
"""A snippet whose only match is a NOTE is not a repeat (#5130): a dated
|
||||
release log scored 0.83 against a signed-APK CI snippet on shared
|
||||
vocabulary and was opened as a candidate. The stub hands back the
|
||||
cross-kind hit directly, so this pins the trigger's own check, not the
|
||||
search filter in front of it."""
|
||||
async with async_session() as s:
|
||||
snippet = Note(user_id=family["owner"], project_id=family["b"], note_type="snippet",
|
||||
title="android-release lane", body="sign with the one release key")
|
||||
s.add(snippet)
|
||||
await s.commit()
|
||||
await s.refresh(snippet)
|
||||
original = await s.get(Note, family["note"])
|
||||
assert original.note_type == "note"
|
||||
with patch("scribe.services.embeddings.semantic_search_notes",
|
||||
AsyncMock(return_value=[(0.86, original)])):
|
||||
assert await family_svc.repeat_trigger(family["owner"], snippet) is None
|
||||
assert await _idea(family["note"]) is None
|
||||
|
||||
|
||||
async def test_a_milestone_closing_on_a_platform_asks_and_one_off_platform_does_not(family):
|
||||
async with async_session() as s:
|
||||
on_platform = Milestone(user_id=family["owner"], project_id=family["a"], title="m1")
|
||||
|
||||
Reference in New Issue
Block a user