d5c80f6153
Phases 5 & 6, completing the scouting desk. Phase 5 — the nightly consolidation (dream cycle, coherence block) now runs poker.scan_merge_candidates(), filing likely same-person merges to the review queue off the hot path. Fail-safe: a scan error never sinks the cycle. Phase 6 — "you've hit this leak before". Scar/confidence notes are embedded on write; recall_similar_rituals() finds past ones close to the current spot, excluding tonight. The scouting desk adds a pattern pass that surfaces them — but ONLY on genuine strategy/tilt talk (a length + cue gate), so routine logging like "stack 350" never pays for an embed. Respects the per-turn latency concern. 7 tests (deterministic embed stub to keep threshold assertions stable). Full suite 153 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
71 lines
2.6 KiB
Python
71 lines
2.6 KiB
Python
"""Pattern desk: embedded scar recall + strategy gating in the scouting desk."""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import importlib
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
|
|
def _idx(w: str) -> int:
|
|
# Stable across processes (unlike hash()), so threshold tests aren't flaky.
|
|
return int.from_bytes(hashlib.md5(w.encode()).digest()[:4], "little") % 256
|
|
|
|
|
|
def _fake_embed(texts):
|
|
out = []
|
|
for t in texts:
|
|
v = np.zeros(256, dtype=np.float32)
|
|
for w in t.lower().split():
|
|
v[_idx(w)] += 1.0
|
|
out.append((v if v.any() else np.full(256, 1e-6, dtype=np.float32)).tolist())
|
|
return out
|
|
|
|
|
|
@pytest.fixture
|
|
def mods(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("LYRA_DB_PATH", str(tmp_path / "test.db"))
|
|
from lyra import llm
|
|
monkeypatch.setattr(llm, "embed", _fake_embed)
|
|
import lyra.memory as memory
|
|
importlib.reload(memory)
|
|
import lyra.poker as poker
|
|
importlib.reload(poker)
|
|
import lyra.scouting as scouting
|
|
importlib.reload(scouting)
|
|
return poker, scouting
|
|
|
|
|
|
def test_scar_recall_finds_similar_past_leak(mods):
|
|
poker, _ = mods
|
|
old = poker.start_session(stakes="1/3", buy_in=300)
|
|
poker.log_ritual("scar", "overvalued top pair and stacked off on a wet board",
|
|
classification="punt", session_id=old)
|
|
poker.end_session(200, session_id=old)
|
|
hits = poker.recall_similar_rituals("stacked off top pair wet board again")
|
|
assert hits and hits[0]["classification"] == "punt"
|
|
|
|
|
|
def test_recall_excludes_current_session(mods):
|
|
poker, _ = mods
|
|
sid = poker.start_session(stakes="1/3", buy_in=300)
|
|
poker.log_ritual("scar", "punted river bluff into the nut flush", session_id=sid)
|
|
assert poker.recall_similar_rituals("river bluff nut flush punt", exclude_session=sid) == []
|
|
|
|
|
|
def test_pattern_pass_only_fires_on_strategic_talk(mods):
|
|
poker, scouting = mods
|
|
old = poker.start_session(stakes="1/3", buy_in=300)
|
|
poker.log_ritual("scar", "punting river bluffs into missed draws again",
|
|
classification="punt", session_id=old)
|
|
poker.end_session(200, session_id=old)
|
|
poker.start_session(stakes="1/3", buy_in=300, venue="Meadows")
|
|
# A routine, non-strategic line pays no embed and surfaces nothing.
|
|
assert scouting.scout("stack is 350 now", venue="Meadows") is None
|
|
# A real strategy question in the same shape recalls the leak. (The test stub
|
|
# embeds by shared tokens; real embeddings match on meaning/paraphrase.)
|
|
note = scouting.scout(
|
|
"why do i keep punting river bluffs into missed draws", venue="Meadows")
|
|
assert note and "punting river bluffs" in note
|