feat(ccip): matcher reads the incremental prototype store (#1317, m138 step 4)
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 34s
CI / integration (push) Successful in 3m43s

match_image now sources character references from character_prototype via a
per-character in-process cache (_load_prototypes) that reloads ONLY the
characters whose ccip_prototype_state.updated_at advanced — no request-path
rebuild, so the per-accept ~4s stall is gone once the store is populated. Cold
start (store empty pre-first-refresh) falls back to the legacy on-the-fly
reference build, so character suggestions work immediately post-deploy and the
background refresh populates the store within ~15 min. Match math + grounding
are unchanged; existing tests exercise the legacy fallback, and a new test
covers matching from the populated prototype store.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
2026-07-06 16:21:38 -04:00
parent a1ed53136e
commit a94f6a2789
2 changed files with 99 additions and 4 deletions
+25 -1
View File
@@ -3,7 +3,13 @@ model needed, so it runs in CI with synthetic CCIP vectors."""
import pytest
from sqlalchemy import select
from backend.app.models import ImageRecord, ImageRegion, TagKind
from backend.app.models import (
CcipPrototypeState,
CharacterPrototype,
ImageRecord,
ImageRegion,
TagKind,
)
from backend.app.models.tag import image_tag
from backend.app.services.ml.ccip import match_image
from backend.app.services.tag_service import TagService
@@ -61,6 +67,24 @@ async def test_matches_same_character_across_images(db):
assert m["grounding"]["kind"] == "figure"
@pytest.mark.asyncio
async def test_matches_from_prototype_store(db):
# Once the prototype store is populated (#1317), match_image reads it (the
# fast incremental path) instead of rebuilding references on the request —
# same match + grounding as the legacy build.
raven = await TagService(db).find_or_create("Raven", TagKind.character)
db.add(CharacterPrototype(tag_id=raven.id, ccip_embedding=_ccip(0)))
db.add(CcipPrototypeState(tag_id=raven.id, fingerprint="1:1"))
query = await _img(db, "n" * 64)
await _figure(db, query.id, _ccip(0)) # query figure matches the prototype
await db.commit()
matches = await match_image(db, query.id)
m = next(x for x in matches if x["tag_id"] == raven.id)
assert m["source"] == "ccip" and m["score"] > 0.9
assert m["grounding"]["kind"] == "figure"
@pytest.mark.asyncio
async def test_no_match_for_different_character(db):
raven = await TagService(db).find_or_create("Raven", TagKind.character)