From 6763fceb0bbe08062603eccbdf127b97045f083f Mon Sep 17 00:00:00 2001 From: anti Date: Sun, 3 May 2026 07:49:03 -0400 Subject: [PATCH] feat(profiler/behave_shell): emit motor.paste_burst_rate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BEHAVE-EXTRACTOR.md Phase A Step 3. Same paste-event ratio as motor.input_modality but coarser-bucketed: this is the *habit* signal (does the operator reach for paste at all?), where input_modality is the dominant-channel signal. * _features/motor.py:paste_burst_rate(ctx) emits one Observation per session in {none, occasional, habitual} with confidence 0.70 / 0.70 / 0.80. * Thresholds: PASTE_RATE_OCCASIONAL_MIN=0.10, PASTE_RATE_HABITUAL_MIN=0.50. Splits YOU-sim from LW/CLAUDE-FF/CLAUDE-CL — LLM-driven sessions paste habitually, real humans rarely paste. Tests: pure-typed → none; 1-paste-in-10 → occasional; paste-majority → habitual; output-only → no observation; habitual confidence > occasional confidence. --- .../behave_shell/_features/__init__.py | 6 ++- .../profiler/behave_shell/_features/motor.py | 33 ++++++++++++ .../test_motor_paste_burst_rate.py | 53 +++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tests/profiler/behave_shell/test_motor_paste_burst_rate.py diff --git a/decnet/profiler/behave_shell/_features/__init__.py b/decnet/profiler/behave_shell/_features/__init__.py index 68ba4548..b4292136 100644 --- a/decnet/profiler/behave_shell/_features/__init__.py +++ b/decnet/profiler/behave_shell/_features/__init__.py @@ -11,10 +11,14 @@ from typing import Callable, Iterable from decnet_behave_core.spec.envelope import Observation from decnet.profiler.behave_shell._ctx import SessionContext -from decnet.profiler.behave_shell._features.motor import input_modality +from decnet.profiler.behave_shell._features.motor import ( + input_modality, + paste_burst_rate, +) FeatureFn = Callable[[SessionContext], Iterable[Observation]] FEATURES: tuple[FeatureFn, ...] = ( input_modality, + paste_burst_rate, ) diff --git a/decnet/profiler/behave_shell/_features/motor.py b/decnet/profiler/behave_shell/_features/motor.py index b4e604a5..f808c895 100644 --- a/decnet/profiler/behave_shell/_features/motor.py +++ b/decnet/profiler/behave_shell/_features/motor.py @@ -14,6 +14,8 @@ from decnet.profiler.behave_shell._features._emit import make_observation from decnet.profiler.behave_shell._thresholds import ( MODALITY_PASTED_MIN, MODALITY_TYPED_MAX, + PASTE_RATE_HABITUAL_MIN, + PASTE_RATE_OCCASIONAL_MIN, ) @@ -43,3 +45,34 @@ def input_modality(ctx: SessionContext) -> Iterator[Observation]: value=modality, confidence=confidence, ) + + +def paste_burst_rate(ctx: SessionContext) -> Iterator[Observation]: + """Emit ``motor.paste_burst_rate`` ∈ {none, occasional, habitual}. + + Same paste-event ratio as ``input_modality`` but coarser-bucketed: + this primitive is the *habit* signal (does the operator reach for + paste at all?), where input_modality is the dominant-channel + signal (is the session paste-driven overall?). Splits YOU-sim from + LW/CLAUDE-FF/CLAUDE-CL — LLM-driven sessions paste habitually, + real humans don't. + """ + n = len(ctx.input_events) + if n == 0: + return + ratio = ctx.paste_event_count / n + if ratio >= PASTE_RATE_HABITUAL_MIN: + level = "habitual" + confidence = 0.80 + elif ratio >= PASTE_RATE_OCCASIONAL_MIN: + level = "occasional" + confidence = 0.70 + else: + level = "none" + confidence = 0.70 + yield make_observation( + ctx, + primitive="motor.paste_burst_rate", + value=level, + confidence=confidence, + ) diff --git a/tests/profiler/behave_shell/test_motor_paste_burst_rate.py b/tests/profiler/behave_shell/test_motor_paste_burst_rate.py new file mode 100644 index 00000000..00684a9d --- /dev/null +++ b/tests/profiler/behave_shell/test_motor_paste_burst_rate.py @@ -0,0 +1,53 @@ +"""Step 3: ``motor.paste_burst_rate`` — none / occasional / habitual.""" +from __future__ import annotations + +from decnet.profiler.behave_shell import extract_session +from decnet.profiler.behave_shell._parse import AsciinemaEvent + + +def _of(observations: list, primitive: str): + obs = [o for o in observations if o.primitive == primitive] + assert len(obs) == 1, f"expected exactly one {primitive}, got {len(obs)}" + return obs[0] + + +def test_pure_typed_session_emits_none() -> None: + events: list[AsciinemaEvent] = [(i * 0.1, "i", c) for i, c in enumerate("ls -la\r")] + out = list(extract_session(events, sid="rate-typed")) + assert _of(out, "motor.paste_burst_rate").value == "none" + + +def test_one_paste_in_ten_emits_occasional() -> None: + # 1 paste + 9 single-char typed events → ratio 0.10 → occasional + events: list[AsciinemaEvent] = [(0.0, "i", "echo paste\r")] + events += [(0.5 + i * 0.1, "i", c) for i, c in enumerate("ls -la\rp")] + out = list(extract_session(events, sid="rate-occasional")) + assert _of(out, "motor.paste_burst_rate").value == "occasional" + + +def test_paste_majority_emits_habitual() -> None: + events: list[AsciinemaEvent] = [ + (0.0, "i", "echo a\r"), + (1.0, "i", "echo b\r"), + (2.0, "i", "echo c\r"), + (3.0, "i", "x"), + ] + out = list(extract_session(events, sid="rate-habitual")) + assert _of(out, "motor.paste_burst_rate").value == "habitual" + + +def test_zero_input_emits_nothing() -> None: + out = list(extract_session([(0.0, "o", "hi\r\n")], sid="rate-empty")) + assert [o for o in out if o.primitive == "motor.paste_burst_rate"] == [] + + +def test_confidence_higher_for_habitual_than_occasional() -> None: + pasted = [ + (0.0, "i", "echo a\r"), (1.0, "i", "echo b\r"), (2.0, "i", "echo c\r"), + ] + occasional = [(0.0, "i", "echo a\r")] + [ + (0.5 + i * 0.1, "i", c) for i, c in enumerate("ls -la\rps\r") + ] + h = _of(list(extract_session(pasted, sid="conf-h")), "motor.paste_burst_rate") + o = _of(list(extract_session(occasional, sid="conf-o")), "motor.paste_burst_rate") + assert h.confidence > o.confidence