feat(sim): emotions are a right-brain output (Right knob alone)

The affect channel previously gated on BOTH knobs (strength = min(left, right)),
so the analytical Left brain co-controlled the emotion words. Per operator
direction, emotions are now driven by the Right (dreamlike) knob ALONE — Left no
longer gates them. This sharpens the split: Left = measurement (detections +
measurements), Right = the dream AND the feeling it evokes.

- player/alteration.py: `_affect_strength(right) = right` (was min(left,right));
  `_affect_intensity` keyed off Right. AffectOverlay + is_identity docs updated
  (is_identity still holds: right>0 ⇒ dream.strength>0).
- simulator/static/app.js: renderAffect comments (gating is Right via `strength`);
  the client logic already keyed min_level/tier off the passed strength + right.
- Affect design spec: revision note + every min(left,right) → right.
- Tests rewritten: affect is Right-driven (full Right + Left 0 surfaces affect;
  Right 0 + Left 4 surfaces none).

271 passed, 2 skipped. Live API verified: left=0/right=4 → strength 4; left=4/
right=0 → strength 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-06-26 04:29:21 -07:00
parent 5bb762ae53
commit c3aa419c53
4 changed files with 71 additions and 56 deletions
+17 -14
View File
@@ -88,11 +88,12 @@ class AnalyticalOverlay:
@dataclass(frozen=True)
class AffectOverlay:
"""Affect channel (Left x Right): emotion-words surfaced only when BOTH the
analytical (Left) and dreamlike (Right) knobs are up. `strength` is
`min(left, right)` (0..4) — a runtime affect track picks which feeling words
appear by it; `intensity` 0..1 is their opacity. The dream leaking into the
machine's reading (affect-channel design, session 0013)."""
"""Affect channel (RIGHT brain): emotion-words surfaced by the Right (dreamlike)
knob alone — the analytical Left knob does NOT gate them. `strength` is the Right
knob (0..4) — a runtime affect track picks which feeling words appear by it;
`intensity` 0..1 is their opacity. Feeling is the right brain's output, surfaced
as the machine's felt reading (affect-channel design, session 0013; Right-only
since session 0018)."""
strength: int
intensity: float
@@ -122,8 +123,9 @@ class RenderPlan:
@property
def is_identity(self) -> bool:
"""True when the plan leaves the neutral base un-altered. Affect needs no
clause: `min(left,right) > 0` implies `left > 0`, so `overlay.level > 0`
already makes a plan with active affect non-identity."""
clause: `affect.strength == right`, and `right > 0` already makes
`dream.strength > 0`, so an active-affect plan is non-identity via the
dream check below."""
return (
self.grade.is_identity
and self.overlay.level == 0
@@ -135,13 +137,14 @@ def _overlay_intensity(left: int, cal: Calibration) -> float:
return _clamp(cal.overlay_gain * left / KNOB_MAX, 0.0, 1.0)
def _affect_strength(left: int, right: int) -> int:
"""Affect surfaces only when BOTH knobs are up — gated by the smaller one."""
return min(left, right)
def _affect_strength(right: int) -> int:
"""Affect is a RIGHT-brain output: the emotion channel is driven by the Right
(dreamlike) knob alone — the analytical Left knob no longer gates it."""
return right
def _affect_intensity(left: int, right: int, cal: Calibration) -> float:
return _clamp(cal.overlay_gain * _affect_strength(left, right) / KNOB_MAX, 0.0, 1.0)
def _affect_intensity(right: int, cal: Calibration) -> float:
return _clamp(cal.overlay_gain * _affect_strength(right) / KNOB_MAX, 0.0, 1.0)
def _dream_intensity(right: int, cal: Calibration) -> float:
@@ -163,8 +166,8 @@ def plan_alteration(
intensity=_overlay_intensity(coord.left, calibration),
),
affect=AffectOverlay(
strength=_affect_strength(coord.left, coord.right),
intensity=_affect_intensity(coord.left, coord.right, calibration),
strength=_affect_strength(coord.right),
intensity=_affect_intensity(coord.right, calibration),
),
dream=Dream(
strength=coord.right,