feat: reconciled simulator-first alteration slice (sessions 0007+0008) #7

Merged
benstull merged 9 commits from feature/reconciled-simulator-alteration-slice into main 2026-06-08 06:00:32 +00:00
2 changed files with 117 additions and 48 deletions
Showing only changes of commit 12a8177793 - Show all commits
+67 -36
View File
@@ -1,22 +1,18 @@
"""The alteration engine: a knob vector -> a layered RenderPlan (design §4, §5).
Replaces the 2026-06-04 nearest-match *selection* with a *transformation* of a
neutral base clip. Given the four experience knobs, it produces three layers
that compose per §4.2:
Reconciled slice (2026-06-07): the Right axis is a DISCRETE selection of a
pre-baked, flow-stabilized restyle variant (not a continuous blend), the Left
axis carries its knob LEVEL so a runtime annotation track can pick which labels
show, and a frozen `Calibration` parameterizes the knob->strength curves so they
can be tuned by eye in the simulator and baked into DEFAULT_CALIBRATION.
- Substrate transforms (alter pixels, blend with each other):
* Restyle -- the Right axis: a pre-baked generative v2v dreamlike restyle.
* ColorGrade -- the mood axis (Dark/Light): a deterministic color grade.
- Overlay (composited on top of the substrate):
* AnalyticalOverlay -- the Left axis: HUD/labels/measurement.
Left and Right are NOT opposites; they live on different layers and stack
(§4.2). Dark and Light are the two poles of one mood grade whose center is the
identity (§5).
Calibration note: the knob->strength curves below are the single source of
truth for how a 0..4 position maps to a transform strength. See the session
0006 transcript Deferred decisions for the §3-vs-§4.2/§5 reconciliation.
Layers compose per §4.2:
- Substrate: ColorGrade (Dark/Light mood, center = identity §5) + a pre-baked
Right restyle variant.
- Overlay: AnalyticalOverlay (Left), composited on top at runtime.
Left and Right stack (different layers); Dark/Light are the two poles of one
mood grade. See docs/superpowers/specs/2026-06-07-reconciled-simulator-
alteration-slice-design.md.
"""
from __future__ import annotations
@@ -28,6 +24,27 @@ from hef.selection import Coordinate
KNOB_MAX = 4 # knob full-scale (0..4)
def _clamp(x: float, lo: float, hi: float) -> float:
return max(lo, min(hi, x))
@dataclass(frozen=True)
class Calibration:
"""Tunable knob->strength curves (settled by eye in the sim, then baked).
- mood_gain: scales the signed Dark/Light tone (result clamped to [-1, 1]).
- overlay_gain: scales the Left overlay intensity (clamped to [0, 1]).
- right_variant_map: knob value (0..4) -> pre-baked Right variant index.
"""
mood_gain: float = 1.0
overlay_gain: float = 1.0
right_variant_map: tuple = (0, 1, 2, 3, 4)
DEFAULT_CALIBRATION = Calibration()
@dataclass(frozen=True)
class ColorGrade:
"""Mood-axis grade (§5). `tone` is signed: >0 warm yellow->white (light),
@@ -42,18 +59,20 @@ class ColorGrade:
@dataclass(frozen=True)
class AnalyticalOverlay:
"""Left axis (§4.1): analytical HUD/annotation/labels, composited on top.
`intensity` 0..1 (0 = no overlay)."""
"""Left axis (§4.1): analytical HUD/annotation, composited on top at runtime.
`level` is the Left knob (0..4); a runtime annotation track uses it to pick
which labels appear. `intensity` 0..1 is the overlay opacity/strength."""
level: int
intensity: float
@dataclass(frozen=True)
class Restyle:
"""Right axis (§4.1): pre-baked generative v2v dreamlike substrate.
`blend` 0..1 (0 = raw substrate, no restyle)."""
"""Right axis (§4.1): selects a pre-baked, flow-stabilized restyle variant.
`variant` is a discrete index (0 = raw base, no restyle)."""
blend: float
variant: int
@dataclass(frozen=True)
@@ -69,30 +88,42 @@ class RenderPlan:
"""True when the plan leaves the neutral base un-altered."""
return (
self.grade.is_identity
and self.overlay.intensity == 0.0
and self.restyle.blend == 0.0
and self.overlay.level == 0
and self.restyle.variant == 0
)
def _overlay_intensity(left: int) -> float:
"""Left knob -> analytical-overlay intensity (0..1)."""
return left / KNOB_MAX
def _overlay_intensity(left: int, cal: Calibration) -> float:
return _clamp(cal.overlay_gain * left / KNOB_MAX, 0.0, 1.0)
def _restyle_blend(right: int) -> float:
"""Right knob -> v2v restyle blend (0..1)."""
return right / KNOB_MAX
def _right_variant(right: int, cal: Calibration) -> int:
return cal.right_variant_map[right]
def _mood_tone(dark: int, light: int) -> float:
"""(dark, light) -> signed mood grade in [-1, 1]; equal -> 0 identity (§5)."""
return (light - dark) / KNOB_MAX
def _mood_tone(dark: int, light: int, cal: Calibration) -> float:
return _clamp(cal.mood_gain * (light - dark) / KNOB_MAX, -1.0, 1.0)
def plan_alteration(coord: Coordinate) -> RenderPlan:
def plan_alteration(
coord: Coordinate, calibration: Calibration = DEFAULT_CALIBRATION
) -> RenderPlan:
"""Map a knob vector to its layered RenderPlan (design §4)."""
return RenderPlan(
grade=ColorGrade(tone=_mood_tone(coord.dark, coord.light)),
overlay=AnalyticalOverlay(intensity=_overlay_intensity(coord.left)),
restyle=Restyle(blend=_restyle_blend(coord.right)),
grade=ColorGrade(tone=_mood_tone(coord.dark, coord.light, calibration)),
overlay=AnalyticalOverlay(
level=coord.left,
intensity=_overlay_intensity(coord.left, calibration),
),
restyle=Restyle(variant=_right_variant(coord.right, calibration)),
)
def render_plan_to_dict(plan: RenderPlan) -> dict:
"""JSON-serializable form for the simulator API."""
return {
"grade": {"tone": plan.grade.tone},
"overlay": {"level": plan.overlay.level, "intensity": plan.overlay.intensity},
"restyle": {"variant": plan.restyle.variant},
"is_identity": plan.is_identity,
}
+50 -12
View File
@@ -2,11 +2,14 @@ import pytest
from hef.selection import Coordinate
from player.alteration import (
DEFAULT_CALIBRATION,
AnalyticalOverlay,
Calibration,
ColorGrade,
RenderPlan,
Restyle,
plan_alteration,
render_plan_to_dict,
)
@@ -17,30 +20,32 @@ def _coord(left=0, right=0, dark=0, light=0):
def test_all_zero_knobs_is_the_unaltered_base():
plan = plan_alteration(_coord())
assert plan.is_identity
assert plan.overlay.level == 0
assert plan.overlay.intensity == 0.0
assert plan.restyle.blend == 0.0
assert plan.restyle.variant == 0
assert plan.grade.tone == 0.0
assert plan.grade.is_identity
def test_left_drives_the_analytical_overlay_only():
plan = plan_alteration(_coord(left=4))
assert plan.overlay.level == 4
assert plan.overlay.intensity == 1.0
assert plan.restyle.blend == 0.0 # Left does not touch the substrate
assert plan.restyle.variant == 0 # Left does not touch the substrate
assert plan.grade.tone == 0.0
def test_right_drives_the_restyle_substrate_only():
def test_right_selects_a_discrete_restyle_variant_only():
plan = plan_alteration(_coord(right=2))
assert plan.restyle.blend == 0.5
assert plan.overlay.intensity == 0.0 # Right does not add overlay
assert plan.restyle.variant == 2
assert plan.overlay.level == 0 # Right does not add overlay
def test_left_and_right_stack_not_cancel():
# design §4.2: whole-brain corner = dreamlike substrate WITH labels on top
plan = plan_alteration(_coord(left=4, right=4))
assert plan.overlay.intensity == 1.0
assert plan.restyle.blend == 1.0
assert plan.overlay.level == 4
assert plan.restyle.variant == 4
def test_light_pole_grades_warm_positive_tone():
@@ -50,8 +55,7 @@ def test_light_pole_grades_warm_positive_tone():
def test_dark_pole_grades_cool_negative_tone():
plan = plan_alteration(_coord(dark=4))
assert plan.grade.tone == -1.0
assert plan_alteration(_coord(dark=4)).grade.tone == -1.0
def test_equal_dark_and_light_is_identity_grade():
@@ -66,14 +70,48 @@ def test_dark_minus_light_sets_intermediate_tone():
def test_whole_brain_dark_corner_stacks_grade_substrate_and_overlay():
# design §4.2 "Dark + analytical": cold measurement over a melancholy scene
plan = plan_alteration(_coord(left=4, right=2, dark=4, light=0))
assert plan.overlay.intensity == 1.0
assert plan.restyle.blend == 0.5
assert plan.overlay.level == 4
assert plan.restyle.variant == 2
assert plan.grade.tone == -1.0
assert not plan.is_identity
def test_default_calibration_is_behavior_preserving():
# DEFAULT_CALIBRATION must reproduce the original three helpers exactly.
for left in range(5):
assert plan_alteration(_coord(left=left)).overlay.intensity == pytest.approx(left / 4)
for right in range(5):
assert plan_alteration(_coord(right=right)).restyle.variant == right
for dark in range(5):
for light in range(5):
expected = (light - dark) / 4
assert plan_alteration(_coord(dark=dark, light=light)).grade.tone == pytest.approx(expected)
def test_custom_calibration_scales_mood_and_overlay():
cal = Calibration(mood_gain=0.5, overlay_gain=0.5, right_variant_map=(0, 0, 1, 1, 2))
assert plan_alteration(_coord(light=4), cal).grade.tone == pytest.approx(0.5)
assert plan_alteration(_coord(left=4), cal).overlay.intensity == pytest.approx(0.5)
assert plan_alteration(_coord(right=3), cal).restyle.variant == 1
def test_calibration_gain_is_clamped_to_unit_range():
cal = Calibration(mood_gain=10.0, overlay_gain=10.0)
assert plan_alteration(_coord(light=4), cal).grade.tone == 1.0 # clamped, not 10
assert plan_alteration(_coord(left=4), cal).overlay.intensity == 1.0
def test_render_plan_to_dict_round_trips_the_numbers():
d = render_plan_to_dict(plan_alteration(_coord(left=4, right=2, dark=4, light=0)))
assert d == {
"grade": {"tone": -1.0},
"overlay": {"level": 4, "intensity": 1.0},
"restyle": {"variant": 2},
"is_identity": False,
}
def test_render_plan_is_frozen():
plan = plan_alteration(_coord())
with pytest.raises(Exception):