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
|
_LINEAGE_WINDOW = 80
|
||||||
|
|
||||||
# Note kinds the repeat trigger compares: recorded knowledge and shapes, not
|
# 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")
|
_REPEAT_KINDS = ("snippet", "note")
|
||||||
|
|
||||||
MEMBER_STATES = ("declared", "detected")
|
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:
|
async def repeat_trigger(user_id: int, note) -> str | None:
|
||||||
"""A new record whose meaning repeats a record in ANOTHER project that
|
"""A new record whose meaning repeats a record OF THE SAME KIND in another
|
||||||
shares a platform with this one opens an evaluation of the earlier record.
|
project that shares a platform with this one opens an evaluation of the
|
||||||
Silent when the projects share no platform, below the threshold, or when
|
earlier record. Silent when the projects share no platform, below the
|
||||||
the project has no platforms yet."""
|
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
|
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
|
return None
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
mine = await _member_platform_ids(session, note.project_id)
|
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
|
return None
|
||||||
hits = await semantic_search_notes(
|
hits = await semantic_search_notes(
|
||||||
user_id, embedding_text(note.title, note.body), exclude_ids={note.id},
|
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,
|
is_task=False, demote_superseded=False,
|
||||||
)
|
)
|
||||||
for score, other in hits:
|
for score, other in hits:
|
||||||
if not other.project_id or other.project_id == note.project_id:
|
if not other.project_id or other.project_id == note.project_id:
|
||||||
continue
|
continue
|
||||||
|
if (other.note_type or "note") != kind:
|
||||||
|
continue
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
shared = mine & await _member_platform_ids(session, other.project_id)
|
shared = mine & await _member_platform_ids(session, other.project_id)
|
||||||
if not shared or not await access.can_write_note(user_id, other.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
|
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 def test_a_milestone_closing_on_a_platform_asks_and_one_off_platform_does_not(family):
|
||||||
async with async_session() as s:
|
async with async_session() as s:
|
||||||
on_platform = Milestone(user_id=family["owner"], project_id=family["a"], title="m1")
|
on_platform = Milestone(user_id=family["owner"], project_id=family["a"], title="m1")
|
||||||
|
|||||||
Reference in New Issue
Block a user