fix(family): the repeat trigger compares a record only with its own kind - a snippet with snippets, a note with notes (#5130)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m38s
CI & Build / Python tests (push) Successful in 2m16s
CI & Build / Build & push image (push) Successful in 32s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m38s
CI & Build / Python tests (push) Successful in 2m16s
CI & Build / Build & push image (push) Successful in 32s
A dated release log (a note) matched a signed-APK CI snippet at 0.83 on shared release vocabulary and was opened as a family candidate, then came back as a precedent. One chunk of a long narrative scores like the shape it mentions, so a cross-kind score does not mean "the same thing made twice". The search is filtered to the new record's kind and the loop checks it too. Code built from an idea written elsewhere still reaches the engine through the citation trigger and the shape ledger. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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