diff --git a/docs/superpowers/specs/2026-07-23-possession-presentation-design.md b/docs/superpowers/specs/2026-07-23-possession-presentation-design.md new file mode 100644 index 0000000..f9fa835 --- /dev/null +++ b/docs/superpowers/specs/2026-07-23-possession-presentation-design.md @@ -0,0 +1,82 @@ +# Possession Presentation Layer — design + +First sub-project of a larger "make contact feel real" arc (candle rituals, +quantum RNG, tuning controls, account progression, session escalation — each +gets its own later spec). This one: make a successful Direct Contact reply +feel like a spirit is *fighting through static to possess the channel*, +rather than a chat bubble appearing. Cosmetic only — the stored transcript +and reply text are unaffected; only the live presentation (text stream + +voice) carries the effect. + +## Contract + +A single number, **stability** (`0.05`–`0.98`, higher = cleaner possession), +computed once per reply on the backend and shared by both halves of this +build: + +```python +# backend/app/possession.py +def compute_stability(rarity: str, magnitude: float, rng: Callable[[], float] = random.random) -> float +``` + +- Base by rarity: `common=0.35, uncommon=0.5, rare=0.65, mythic=0.8` (rarer + spirits hold the channel more steadily). +- `+ min(0.15, magnitude / 100)` — the anomaly magnitude that most recently + triggered contact; a stronger signal, a cleaner line. +- `+ rng()` jitter scaled to `±0.1`. +- Clamped to `[0.05, 0.98]`. +- `rng` is injectable specifically so the later quantum-RNG sub-project can + swap in real entropy without touching call sites. + +`_handle_question` in `backend/app/ws.py` computes this once per question +(using `state.entity.rarity_tier` and the magnitude of the most recent +anomaly in `state.anomalies`, defaulting to 50 if none yet) and adds it to +the existing `reply_start` frame: + +```json +{"type": "reply_start", "stability": 0.62} +``` + +## Backend half (audio degradation) + +`synthesize_spirit_voice(text, voice, voice_profile, instability=0.0)` in +`backend/app/tts/piper.py` gains an `instability` param (`0.0`–`1.0`, +i.e. `1 - stability`) that scales up the `noise_level` and `bitcrush_bits` +already passed to `apply_effects` — do not touch `effects.py` itself, only +the params `synthesize_spirit_voice` feeds it. Only the `"reply"` kind +utterance in `_speak` (the Direct Contact answer) passes a non-zero +`instability`; greetings/fragments/ambient whispers are unaffected by this +spec. + +## Frontend half (text glitch renderer) + +`frontend/src/lib/possession.ts` — pure function(s) that take the +true-so-far streamed text, the stability score, and a tick/frame counter, +and return a display string with corruption bursts and brief stutters that +self-correct. Lower stability = more frequent/longer corruption; stability +above ~0.85 is nearly clean (a subtle flicker only). Must be deterministic +given its inputs (seed any internal randomness from the tick counter, not +`Math.random()`) so it's unit-testable. + +Wire into `frontend/src/state/seance.tsx`: extend `replyStreaming` with a +`stability: number` field, populated from `reply_start`'s `stability` +(default `1.0` if absent). Wire into `frontend/src/components/Transcript.tsx`: +render the streaming line through the glitch function instead of raw text, +driven by a ticking interval while `streaming.active`. + +## Testing + +- Backend: unit tests for `compute_stability` (rarity ordering, magnitude + bonus, clamping, injected rng determinism) and for the + `instability`→params scaling in `synthesize_spirit_voice`. One WS + integration test asserting `reply_start` carries a `stability` field in + range. +- Frontend: unit tests for the glitch renderer's pure functions at known + low/high stability values (corruption rate, self-correction, determinism + for a fixed tick sequence). + +## Explicitly out of scope here + +New WS frame types for interleaved glitch tokens, structural UI takeover, +candle rituals, quantum RNG source, tuning dial, account progression, +session escalation — all separate later specs per the arc this belongs to.