Files
qtalker---/backend/app/models/entity.py
Indiana 36c4a9e6e4 feat: ritual + judgment + favor + cross-over (Workstream B)
Implements Workstream B of the character-depth-ghost-log spec:
ritual_start/ritual_step/judgment WS handlers, the pure judgment.py
logic module, and the User.favor / Entity.at_peace columns + migration.

- app/judgment.py: pure ritual success roll (base 65%, floored at 30%,
  driven by an entity's power+deceptiveness difficulty), the "stuck
  spirit" cross_over rule (alignment >= 0.5 and volatility > 0.6, ~20%
  of entities), judgment correctness/favor-delta/essence-delta/
  consequence resolution for all four verdicts, favor clamping, the
  favor-to-trait-roll bias applied at mint time, and tell-line
  generation (opaque behavioral flavor text, never a raw stat).
- app/ws.py: wires ritual_start/ritual_step/judgment frames, emits
  ritual_complete/tell/judgment_result/item_drop per the spec's
  Contract; traits are added to serialize_entity for internal
  server-side use but stripped from the outbound `entity` frame via a
  new _public_entity helper so hidden ground truth never reaches the
  client outside ritual_complete; _summon excludes at-peace entities
  from signature re-contact and mints a fresh (salted-signature) entity
  instead; new entities' traits are nudged by the discovering user's
  favor before being persisted.
- models/user.py, models/entity.py, main.py: User.favor and
  Entity.at_peace columns plus their idempotent ADD COLUMN IF NOT
  EXISTS migration lines in lifespan, alongside the existing ones.
- tests/test_judgment.py, tests/test_ws_ritual_judgment.py: 56 new
  tests covering the ritual/judgment correctness matrix, favor
  clamping/bias, essence crediting, at_peace persistence + re-contact,
  and the entity-frame trait leak guard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 21:27:30 +00:00

42 lines
1.9 KiB
Python

import uuid
from datetime import datetime, timezone
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from app.db import Base
RARITY_TIERS = ("common", "uncommon", "rare", "mythic")
class Entity(Base):
"""A spirit in the Codex: minted from a session's anomaly signature,
or matched again on a later contact."""
__tablename__ = "entities"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(String(64), unique=True, index=True)
epithet: Mapped[str] = mapped_column(String(128), default="")
persona: Mapped[str] = mapped_column(Text, default="")
rarity_tier: Mapped[str] = mapped_column(String(16), default="common", index=True)
signature: Mapped[str] = mapped_column(String(64), unique=True, index=True)
voice_profile: Mapped[dict] = mapped_column(JSONB, default=dict)
visual_profile: Mapped[dict] = mapped_column(JSONB, default=dict)
traits: Mapped[dict] = mapped_column(JSONB, default=dict)
sample_quotes: Mapped[list] = mapped_column(JSONB, default=list)
contact_count: Mapped[int] = mapped_column(Integer, default=0)
# Workstream B (character-depth-ghost-log spec): set true when a seeker
# correctly helps a genuinely benevolent, "stuck" spirit cross over. The
# row is never deleted (memorialized in the Codex permanently) but
# `signature_from_anomalies` re-contact in app.ws._summon skips it and
# mints a fresh entity instead.
at_peace: Mapped[bool] = mapped_column(Boolean, default=False)
discovered_by: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("users.id"), nullable=True
)
discovered_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)