feat(profiler/behave_shell): emit motor.keystroke_cadence
BEHAVE-EXTRACTOR.md Phase B Step B.1.
* SessionContext gains typing_bursts: tuple[tuple[float, ...], ...]
built by _split_typing_bursts(iats) — splits at gaps > IKI_THINK_MAX_S
(1.5s) and drops bursts of fewer than 3 IATs. Mirrors prototype's
_split_into_bursts at BEHAVE/prototype_extractors/shell/extract.py:275.
* _features/motor.py:keystroke_cadence(ctx) emits one Observation
in {steady, bursty, hunt_and_peck, machine}. Median CV across
typing bursts; mean IKI < IKI_MACHINE_MAX_S paired with CV <
CV_MACHINE_MAX → machine. Confidence 0.85/0.70/0.65/0.60 per the
prototype's calibration history.
* < MIN_INPUTS_FOR_CADENCE inputs or zero typing bursts → skip
emission. v0.1 emits only the burst-CV variant; the prototype's
NAIVE session-CV variant is parked for v0.2.
* Calibration grid widened (PHASE_A_PRIMITIVES → PHASE_AB_PRIMITIVES)
to include motor.keystroke_cadence. Grid green across all five
shards.
Tests: too-few-inputs → no emit, all-think-pauses → no burst → no
emit, uniform IATs → steady, sub-5ms → machine, mixed-pace → bursty,
extreme bimodal → hunt_and_peck.
This commit is contained in:
@@ -20,6 +20,7 @@ from decnet.profiler.behave_shell._parse import (
|
||||
hash_token,
|
||||
)
|
||||
from decnet.profiler.behave_shell._thresholds import (
|
||||
IKI_THINK_MAX_S,
|
||||
PASTE_BURST_MAX_IAT_S,
|
||||
PASTE_MIN_CHARS_PER_EVENT,
|
||||
)
|
||||
@@ -47,6 +48,9 @@ class SessionContext:
|
||||
inter_cmd_iats: tuple[float, ...] = field(default_factory=tuple)
|
||||
output_per_cmd: tuple[int, ...] = field(default_factory=tuple)
|
||||
|
||||
# Step B.1 derivations — typing bursts (IATs split at think-pauses)
|
||||
typing_bursts: tuple[tuple[float, ...], ...] = field(default_factory=tuple)
|
||||
|
||||
|
||||
def _detect_paste_bursts(
|
||||
inputs: list[AsciinemaEvent],
|
||||
@@ -102,6 +106,22 @@ def _detect_paste_bursts(
|
||||
return tuple(bursts), paste_count
|
||||
|
||||
|
||||
def _split_typing_bursts(iats: tuple[float, ...]) -> tuple[tuple[float, ...], ...]:
|
||||
"""Split a flat IAT sequence at gaps > IKI_THINK_MAX_S.
|
||||
|
||||
Drops bursts of fewer than 3 IATs — too short to compute a stable
|
||||
CV. Mirrors BEHAVE prototype's ``_split_into_bursts``.
|
||||
"""
|
||||
bursts: list[list[float]] = [[]]
|
||||
for x in iats:
|
||||
if x > IKI_THINK_MAX_S:
|
||||
if bursts[-1]:
|
||||
bursts.append([])
|
||||
else:
|
||||
bursts[-1].append(x)
|
||||
return tuple(tuple(b) for b in bursts if len(b) >= 3)
|
||||
|
||||
|
||||
def _segment_commands(inputs: list[AsciinemaEvent]) -> tuple[Command, ...]:
|
||||
"""Walk input events, splitting on ``\\r`` / ``\\n`` into commands.
|
||||
|
||||
@@ -179,6 +199,7 @@ def build_session_context(
|
||||
max(0.0, inputs[i][0] - inputs[i - 1][0]) for i in range(1, len(inputs))
|
||||
)
|
||||
paste_bursts, paste_count = _detect_paste_bursts(inputs)
|
||||
typing_bursts = _split_typing_bursts(iats)
|
||||
commands = _segment_commands(inputs)
|
||||
inter_cmd_iats = tuple(
|
||||
max(0.0, commands[i + 1].start_ts - commands[i].end_ts)
|
||||
@@ -204,4 +225,5 @@ def build_session_context(
|
||||
commands=commands,
|
||||
inter_cmd_iats=inter_cmd_iats,
|
||||
output_per_cmd=output_per_cmd,
|
||||
typing_bursts=typing_bursts,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user