feat: the matcher gets an identity route, so a name can link what time cannot (4392)
CI and images / lint (push) Successful in 4s
CI and images / extension-version (push) Successful in 4s
CI and images / frontend-build (push) Successful in 26s
CI and images / backend-lint-and-test (push) Successful in 34s
CI and images / integration (push) Successful in 2m52s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 6s
CI and images / build-web (push) Successful in 2m6s
CI and images / smoke-web (push) Successful in 55s
CI and images / promote (push) Skipped
CI and images / lint (push) Successful in 4s
CI and images / extension-version (push) Successful in 4s
CI and images / frontend-build (push) Successful in 26s
CI and images / backend-lint-and-test (push) Successful in 34s
CI and images / integration (push) Successful in 2m52s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 6s
CI and images / build-web (push) Successful in 2m6s
CI and images / smoke-web (push) Successful in 55s
CI and images / promote (push) Skipped
#4392 measured three compounding reasons the announcement matcher could not fire for this creator. This is the first two. **Vocabulary.** `discord` is not how these creators write once the audience already knows where the server is. Across 20,558 Patreon bodies `discord` appears in 486 and `the server` in 37 — but the distribution is the point, not the totals: for artist 8, 21 of 42 posts say `discord`, 7 say `the server`, and it is the RECENT ones that say the latter. A vocabulary list written from old posts silently stops matching the posts that still need it. **Two routes.** Circumstance and identity answer different questions. Proximity and a declaration say two things happened near each other; a working name the creator uses on these two posts and nowhere else says they are the same piece. Averaging those makes the threshold uninterpretable and costs both ways — adding identity as a fourth weight dilutes the others enough that measured teaser/drop pairs an hour apart stop proposing, while capping identity at a weight means the strongest evidence available can never carry a pair alone. So identity may OVERRIDE, never dilute; below IDENTITY_FLOOR it is recorded for the operator and moves nothing, which is the conservative direction, since a wrong link asserts two different pieces are one. The circumstantial bundle is now proximity/declared/marker at .45/.35/.20. Rebalanced rather than extended: the existing guard still holds arithmetically (no single weight reaches the threshold) and the existing pairs still score where they did — a teaser and its drop an hour apart with an invite link go from 0.815 to 0.781, both well over. Measured against the live instance, artist 8: 17 candidate pairs, 14 carried by a shared name, no false positives, and no drop claimed by two posts. The operator's own example — `Anya -- 🍈🍈` and the drop 75 minutes before it — links on the token `0-k`, which is what its two files are called on either platform. Two pairs 21h apart that proximity scores 0.10 are reached, and two more (`680lc`, `cnni18x`, four posts each) are the stated cost of the floor. Both rare-token signals are artist-scoped and loaded once per artist per sweep, not per candidate pair. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
+237
-10
@@ -22,6 +22,7 @@ from backend.app.models import (
|
||||
)
|
||||
from backend.app.services.discord_grouping import DROP_GROUPER
|
||||
from backend.app.services.post_association_service import (
|
||||
DECLARED_MENTION,
|
||||
WEIGHTS,
|
||||
PostAssociationService,
|
||||
declared_signal,
|
||||
@@ -130,30 +131,42 @@ async def _artist_with_channels(db, name: str):
|
||||
return artist, patreon, discord
|
||||
|
||||
|
||||
async def _teaser(db, artist, source, *, at, body, ext="teaser"):
|
||||
async def _images(db, artist, post, ext, names):
|
||||
"""Attach named files to a post. The NAME is the point — the working-name
|
||||
signal reads it, so a test that cares about identity supplies one."""
|
||||
for i, name in enumerate(names):
|
||||
db.add(ImageRecord(
|
||||
path=f"/images/{artist.id}/{ext}_{i}_{name}.jpg",
|
||||
sha256=f"{ext}{i}{name}".ljust(64, "0")[:64],
|
||||
size_bytes=10, mime="image/jpeg", width=10, height=10,
|
||||
origin="downloaded", primary_post_id=post.id, artist_id=artist.id,
|
||||
))
|
||||
await db.flush()
|
||||
|
||||
|
||||
async def _teaser(db, artist, source, *, at, body, ext="teaser", names=None,
|
||||
title="New piece"):
|
||||
post = Post(
|
||||
source_id=source.id, artist_id=artist.id, external_post_id=ext,
|
||||
post_date=at, post_title="New piece", description=body,
|
||||
post_date=at, post_title=title, description=body,
|
||||
)
|
||||
db.add(post)
|
||||
await db.flush()
|
||||
db.add(ImageRecord(
|
||||
path=f"/images/{source.id}-{ext}.jpg", sha256=f"{ext:0>64}"[:64],
|
||||
size_bytes=10, mime="image/jpeg", width=10, height=10,
|
||||
origin="downloaded", primary_post_id=post.id, artist_id=artist.id,
|
||||
))
|
||||
await db.flush()
|
||||
await _images(db, artist, post, ext, names or [ext])
|
||||
return post
|
||||
|
||||
|
||||
async def _drop(db, artist, source, *, at, ext="fc-drop:1"):
|
||||
async def _drop(db, artist, source, *, at, ext="fc-drop:1", body=None,
|
||||
names=()):
|
||||
post = Post(
|
||||
source_id=source.id, artist_id=artist.id, external_post_id=ext,
|
||||
post_date=at, synthesized_by=DROP_GROUPER,
|
||||
post_date=at, description=body, synthesized_by=DROP_GROUPER,
|
||||
synthesis_details={"message_count": 4, "images_since_surface": 0},
|
||||
)
|
||||
db.add(post)
|
||||
await db.flush()
|
||||
if names:
|
||||
await _images(db, artist, post, ext.replace(":", "-"), names)
|
||||
return post
|
||||
|
||||
|
||||
@@ -372,3 +385,217 @@ async def test_the_rescan_is_a_no_op_when_the_switch_is_off(db):
|
||||
settings.discord_link_enabled = False
|
||||
await db.commit()
|
||||
assert await rescan(db) == {"enabled": False, "scanned": 0, "proposed": 0}
|
||||
|
||||
|
||||
# --- the identity route ----------------------------------------------------
|
||||
#
|
||||
# Milestone 388. Everything below was calibrated against the operator's real
|
||||
# library (artist 8: 520 images, 15 same-artist pairs that share a name, no
|
||||
# false positives) rather than invented, so the numbers in these docstrings are
|
||||
# measurements.
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_shared_working_name_proposes_a_pair_time_cannot_reach(db):
|
||||
"""The reason identity is a route and not a fourth weight.
|
||||
|
||||
These two are 20 hours apart and the teaser says nothing about Discord, so
|
||||
every circumstantial signal is near zero — proximity scores 0.17 and the
|
||||
weighted total 0.075, a long way under the bar. What links them is that
|
||||
the creator exported both from one file and the name came along.
|
||||
|
||||
Measured equivalents on the live instance: `ConnFront` ↔ `ConnFront` and
|
||||
`Thicc Tomboy scene2` ↔ `Thicc_Tomboy_scene2`, the latter 23.8 hours apart
|
||||
at a proximity of 0.005.
|
||||
"""
|
||||
artist, patreon, discord = await _artist_with_channels(db, "namedartist")
|
||||
now = datetime.now(UTC)
|
||||
teaser = await _teaser(
|
||||
db, artist, patreon, at=now - timedelta(hours=20),
|
||||
body="a little preview", names=["ConnFront"],
|
||||
)
|
||||
drop = await _drop(db, artist, discord, at=now, names=["01_ConnFront"])
|
||||
await db.commit()
|
||||
|
||||
made = await PostAssociationService(db).match_post(
|
||||
teaser.id, threshold=DEFAULT_THRESHOLD, window_hours=WINDOW,
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
assert made == 1
|
||||
assoc = (await db.execute(select(PostAssociation))).scalar_one()
|
||||
assert assoc.payload_post_id == drop.id
|
||||
assert assoc.signals["identity"] == 1.0
|
||||
assert weighted_score(assoc.signals) < DEFAULT_THRESHOLD, (
|
||||
"the circumstantial bundle alone must NOT reach the bar here — if it "
|
||||
"does, this test has stopped proving what it claims to"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_proposal_says_which_name_it_matched_on(db):
|
||||
"""A review queue that cannot explain itself is one the operator learns to
|
||||
click through without reading."""
|
||||
artist, patreon, discord = await _artist_with_channels(db, "explainartist")
|
||||
now = datetime.now(UTC)
|
||||
teaser = await _teaser(
|
||||
db, artist, patreon, at=now - timedelta(hours=20),
|
||||
body="a little preview", names=["LoisLaneTB2"],
|
||||
)
|
||||
await _drop(db, artist, discord, at=now, names=["01_LoisLaneTB2"])
|
||||
await db.commit()
|
||||
|
||||
await PostAssociationService(db).match_post(
|
||||
teaser.id, threshold=DEFAULT_THRESHOLD, window_hours=WINDOW,
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
assoc = (await db.execute(select(PostAssociation))).scalar_one()
|
||||
assert assoc.signals["identity_token"] == "loislanetb2"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_name_the_creator_reuses_everywhere_links_nothing(db):
|
||||
"""THE false-positive guard for this route.
|
||||
|
||||
A character name is not an identity. Measured on artist 8: `anya` is on
|
||||
images in 8 posts, `riju` 9, `bea` 16 — ungated, every Anya post would
|
||||
match every Anya drop, and the matcher would confidently assert that a
|
||||
year of unrelated pieces are all the same piece.
|
||||
"""
|
||||
artist, patreon, discord = await _artist_with_channels(db, "habitartist")
|
||||
now = datetime.now(UTC)
|
||||
teaser = await _teaser(
|
||||
db, artist, patreon, at=now - timedelta(hours=20),
|
||||
body="a little preview", ext="t0", names=["Anya"],
|
||||
)
|
||||
await _drop(db, artist, discord, at=now, names=["01_Anya"])
|
||||
# The same name across enough of this artist's OTHER posts to make it a
|
||||
# habit. Nothing about the pair above changes; only its context does.
|
||||
for i in range(7):
|
||||
await _teaser(
|
||||
db, artist, patreon, at=now - timedelta(days=30 + i),
|
||||
body="older", ext=f"other{i}", names=["Anya"],
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
made = await PostAssociationService(db).match_post(
|
||||
teaser.id, threshold=DEFAULT_THRESHOLD, window_hours=WINDOW,
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
assert made == 0
|
||||
assert (await db.execute(select(PostAssociation))).scalars().all() == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_name_below_the_floor_is_recorded_but_carries_nothing(db):
|
||||
"""Identity may override, never dilute. Below the floor it is real
|
||||
evidence that is not strong enough to assert sameness on its own, so it is
|
||||
written down for the operator and moves no score.
|
||||
|
||||
The measured cost of the floor sitting at 0.75: artist 8's `680lc` and
|
||||
`cnni18x` pairs each span four posts and are genuine, and neither will
|
||||
propose on this signal alone.
|
||||
"""
|
||||
artist, patreon, discord = await _artist_with_channels(db, "floorartist")
|
||||
now = datetime.now(UTC)
|
||||
teaser = await _teaser(
|
||||
db, artist, patreon, at=now - timedelta(hours=20),
|
||||
body="a little preview", ext="t0", names=["cnni18x"],
|
||||
)
|
||||
await _drop(db, artist, discord, at=now, names=["01_cnni18x"])
|
||||
for i in range(2):
|
||||
await _teaser(
|
||||
db, artist, patreon, at=now - timedelta(days=30 + i),
|
||||
body="older", ext=f"other{i}", names=["cnni18x"],
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
svc = PostAssociationService(db)
|
||||
made = await svc.match_post(
|
||||
teaser.id, threshold=DEFAULT_THRESHOLD, window_hours=WINDOW,
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
assert made == 0
|
||||
corpus = await svc._corpus(artist.id)
|
||||
assert corpus.token_posts["cnni18x"] == 4, "four posts carry the name"
|
||||
|
||||
|
||||
# --- the marker, and the vocabulary ----------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_marker_the_creator_uses_once_lifts_a_pair_over_the_line(db):
|
||||
"""The operator's own tie-back: `🍈🍈` in the Patreon title and
|
||||
`@everyone 🍈 🍈` in the Discord message. No vocabulary list would predict
|
||||
it, and no filename carries it.
|
||||
|
||||
Measured here: 0.446 on proximity alone, 0.646 once the marker counts —
|
||||
the bar is 0.60. The mirror case is the guard below it.
|
||||
"""
|
||||
artist, patreon, discord = await _artist_with_channels(db, "markerartist")
|
||||
now = datetime.now(UTC)
|
||||
teaser = await _teaser(
|
||||
db, artist, patreon, at=now - timedelta(minutes=12),
|
||||
body="it's up", title="Anya -- \U0001F348\U0001F348",
|
||||
)
|
||||
await _drop(db, artist, discord, at=now, body="@everyone \U0001F348 \U0001F348")
|
||||
await db.commit()
|
||||
|
||||
made = await PostAssociationService(db).match_post(
|
||||
teaser.id, threshold=DEFAULT_THRESHOLD, window_hours=WINDOW,
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
assert made == 1
|
||||
assoc = (await db.execute(select(PostAssociation))).scalar_one()
|
||||
assert assoc.signals["marker"] == 1.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_marker_the_creator_uses_constantly_lifts_nothing(db):
|
||||
"""The same arrangement, with the marker turned into punctuation.
|
||||
|
||||
This is a real measured proposal, not a hypothetical: 💦 is in 13 of artist
|
||||
8's 300 posts and it was the DECIDING term for a pair that proximity alone
|
||||
scored 0.441. A habitual marker riding along with proximity is just
|
||||
proximity wearing a hat, which the threshold sits above 0.45 to prevent.
|
||||
"""
|
||||
artist, patreon, discord = await _artist_with_channels(db, "punctartist")
|
||||
now = datetime.now(UTC)
|
||||
teaser = await _teaser(
|
||||
db, artist, patreon, at=now - timedelta(minutes=12),
|
||||
body="it's up", title="Drizzle \U0001F4A6", ext="t0",
|
||||
)
|
||||
await _drop(db, artist, discord, at=now, body="@everyone \U0001F4A6")
|
||||
for i in range(5):
|
||||
await _teaser(
|
||||
db, artist, patreon, at=now - timedelta(days=30 + i),
|
||||
body="\U0001F4A6", title=f"older {i}", ext=f"other{i}",
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
made = await PostAssociationService(db).match_post(
|
||||
teaser.id, threshold=DEFAULT_THRESHOLD, window_hours=WINDOW,
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
assert made == 0
|
||||
|
||||
|
||||
def test_the_creators_own_phrasing_counts_as_a_declaration():
|
||||
"""`discord` is not how these creators actually write once the audience
|
||||
already knows where the server is. Measured on artist 8: 21 of 42 posts
|
||||
say `discord`, 7 say `the server`, and it is the RECENT ones that say the
|
||||
latter — so a vocabulary list written from old posts silently stops
|
||||
matching the posts that still need it."""
|
||||
assert declared_signal("<p>Full res is on the server</p>") == DECLARED_MENTION
|
||||
assert declared_signal("<p>up on our server now</p>") == DECLARED_MENTION
|
||||
assert declared_signal("<p>posted to my server</p>") == DECLARED_MENTION
|
||||
|
||||
|
||||
def test_an_unrelated_server_is_still_not_a_declaration():
|
||||
"""The widened vocabulary must not widen into ordinary prose."""
|
||||
assert declared_signal("<p>the servers were down all morning</p>") == 0.0
|
||||
|
||||
Reference in New Issue
Block a user