feat: confirm-loop tools + descriptor reads for nameless villains

Phase 3. She can now log and resolve identity at the table:
- add_read gains a `descriptor` param — a read on an unnamed player resolves to an
  existing descriptor villain (confident match) or opens a new one, so reads on
  "neck tattoo guy" accumulate and reuse across the night.
- name_villain(descriptor, name): attach a real name once caught (history carries).
- link_villains(a, b, same): merge on confirmed same-person, or mark distinct so
  she stops asking. Refuses to act on a vague reference — never merges on a guess.
- Cash card PLAYERS guidance: log nameless villains by distinctive descriptor,
  cite the SCOUTING DESK note, ask before assuming a callback, confirm before merge.

4 tests. Full suite 150 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 22:52:20 +00:00
parent 9cd962625d
commit f2944ed402
4 changed files with 184 additions and 7 deletions
+14 -2
View File
@@ -1393,14 +1393,26 @@ def scan_merge_candidates(sim_threshold: float = _SIM_HIGH) -> int:
def add_read(note: str, seat: str | None = None, name: str | None = None,
session_id: int | None = None, **player_fields) -> int:
"""Log a live read. If `name` is given, upsert the player and link the read."""
descriptor: str | None = None, session_id: int | None = None,
**player_fields) -> int:
"""Log a live read. `name` upserts a named player; `descriptor` (a nameless
villain's physical description) resolves to an existing descriptor villain when
confident, else opens a new one — so reads on unnamed players still accumulate."""
sid = _resolve(session_id)
venue = player_fields.get("venue")
pid = None
if name:
pid = upsert_player(name, **{k: v for k, v in player_fields.items()
if k in ("venue", "description", "tendencies",
"adjustment", "category")})
elif descriptor:
res = resolve_villain(descriptor, venue=venue, session_id=sid)
if res["band"] in ("name", "high") and res["match_id"]:
pid = res["match_id"]
add_descriptor(pid, descriptor) # sharpen the key with this phrasing
else:
pid = create_descriptor_villain(descriptor, venue=venue,
category=player_fields.get("category"))
conn = _c()
with conn:
cur = conn.execute(