feat(profiler/behave_shell): emit temporal.escalation_pattern

Bin commands into non-overlapping windows of width
max(ESCALATION_WINDOW_MIN_S, duration_s / ESCALATION_WINDOW_TARGET).
CV of per-window counts + zero-window fraction classify bursty /
sustained / erratic. v0.1; corpus re-tune deferred.
This commit is contained in:
2026-05-04 00:13:45 -04:00
parent 627fa59c15
commit d40495d71b
4 changed files with 167 additions and 0 deletions

View File

@@ -25,6 +25,7 @@ from decnet.profiler.behave_shell._features.cognitive import (
inter_command_latency_class,
)
from decnet.profiler.behave_shell._features.temporal import (
escalation_pattern,
session_duration,
)
from decnet.profiler.behave_shell._features.motor import (
@@ -63,4 +64,5 @@ FEATURES: tuple[FeatureFn, ...] = (
error_resilience_frustration_typing,
error_resilience_fallback_to_man,
session_duration,
escalation_pattern,
)

View File

@@ -6,9 +6,12 @@ observation history. The other three (``session_timing``,
and computed by the attribution engine, not the extractor.
Step E.1: ``temporal.session_duration``.
Step E.2: ``temporal.escalation_pattern``.
"""
from __future__ import annotations
import math
import statistics
from typing import Iterator
from decnet_behave_core.spec.envelope import Observation
@@ -16,6 +19,13 @@ from decnet_behave_core.spec.envelope import Observation
from decnet.profiler.behave_shell._ctx import SessionContext
from decnet.profiler.behave_shell._features._emit import make_observation
from decnet.profiler.behave_shell._thresholds import (
ESCALATION_BURSTY_CV,
ESCALATION_BURSTY_ZERO_FRAC,
ESCALATION_MIN_COMMANDS,
ESCALATION_MIN_WINDOWS,
ESCALATION_SUSTAINED_CV,
ESCALATION_WINDOW_MIN_S,
ESCALATION_WINDOW_TARGET,
SESSION_DURATION_LONG_MAX,
SESSION_DURATION_MEDIUM_MAX,
SESSION_DURATION_SHORT_MAX,
@@ -48,3 +58,58 @@ def session_duration(ctx: SessionContext) -> Iterator[Observation]:
value=value,
confidence=0.85,
)
def escalation_pattern(ctx: SessionContext) -> Iterator[Observation]:
"""Emit ``temporal.escalation_pattern`` ∈ {sustained, erratic, bursty}.
Bin commands into non-overlapping windows of width
``max(ESCALATION_WINDOW_MIN_S, duration_s / ESCALATION_WINDOW_TARGET)``.
Compute the CV of per-window command counts and the fraction of
zero-count windows.
* **bursty** — significant silence (zero_frac ≥ threshold) AND
high dispersion (CV ≥ threshold). Real spikes against a quiet
background.
* **sustained** — low dispersion (CV < threshold). Steady cadence.
* **erratic** — fall-through. Variable but no clear silence
pattern.
Skip emission when the session is too short to bin meaningfully
(no commands, or duration too small to produce any window).
"""
n_cmds = len(ctx.commands)
if n_cmds == 0 or ctx.duration_s <= 0.0:
return
width = max(ESCALATION_WINDOW_MIN_S, ctx.duration_s / ESCALATION_WINDOW_TARGET)
n_windows = max(1, math.ceil(ctx.duration_s / width))
counts = [0] * n_windows
for cmd in ctx.commands:
offset = cmd.start_ts - ctx.t_start
idx = min(n_windows - 1, max(0, int(offset / width)))
counts[idx] += 1
mean = statistics.fmean(counts)
if mean <= 0.0 or len(counts) < 2:
cv = 0.0
else:
cv = statistics.stdev(counts) / mean
zero_frac = sum(1 for c in counts if c == 0) / len(counts)
if zero_frac >= ESCALATION_BURSTY_ZERO_FRAC and cv >= ESCALATION_BURSTY_CV:
value = "bursty"
elif cv < ESCALATION_SUSTAINED_CV:
value = "sustained"
else:
value = "erratic"
if n_windows < ESCALATION_MIN_WINDOWS or n_cmds < ESCALATION_MIN_COMMANDS:
confidence = 0.40
else:
confidence = 0.60
yield make_observation(
ctx,
primitive="temporal.escalation_pattern",
value=value,
confidence=confidence,
)