Skip to content

Commit 286bdd2

Browse files
committed
refactor(operator-trend): collapse churn builders onto a parametrized base (T3-2 phase 6b)
Second structural collapse, same pattern as the persistence base (#82): the rebuild and rererestore churn builders are one algorithm differing only in the per-event freshness/reset keys, reason prose, and output keys. Extract _churn_for_target_base + a _ChurnTierSpec; both public builders become thin wrappers passing their spec. Public signatures unchanged -> apply-chain wiring, the rerererestore delegation wrapper, and all callers untouched. Net -40 lines. Verified: differential test (real builders vs base+spec over the net corpus, byte-identical both tiers) + composer golden byte-identical to main + full suite (2539 passed). ruff + mypy clean.
1 parent ab2076e commit 286bdd2

1 file changed

Lines changed: 118 additions & 158 deletions

File tree

src/operator_trend_closure_forecast_reset_controls.py

Lines changed: 118 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -3218,119 +3218,178 @@ def closure_forecast_reset_reentry_rebuild_persistence_for_target(
32183218
)
32193219

32203220

3221-
def closure_forecast_reset_reentry_rebuild_churn_for_target(
3221+
class _ChurnTierSpec(NamedTuple):
3222+
"""Tier-specific literals for the reset-reentry churn builder.
3223+
3224+
The rebuild / rererestore churn families share one algorithm and differ only in
3225+
the per-event freshness/reset keys they read, their reason prose, and their
3226+
output keys.
3227+
"""
3228+
3229+
freshness_key: str
3230+
reset_key: str
3231+
reasons: dict[str, str]
3232+
score_key: str
3233+
status_out_key: str
3234+
reason_key: str
3235+
path_key: str
3236+
3237+
3238+
def _churn_for_target_base(
32223239
target: dict[str, Any],
32233240
closure_forecast_events: list[dict[str, Any]],
32243241
transition_history_meta: dict[str, Any],
32253242
*,
3243+
spec: _ChurnTierSpec,
32263244
ordered_reset_reentry_events_for_target: Callable[
32273245
[dict[str, Any], list[dict[str, Any]]], list[dict[str, Any]]
32283246
],
3229-
closure_forecast_reset_reentry_rebuild_side_from_event: Callable[[dict[str, Any]], str],
3247+
side_from_event: Callable[[dict[str, Any]], str],
32303248
class_direction_flip_count: Callable[[list[str]], int],
3231-
target_specific_normalization_noise: Callable[[dict[str, Any], dict[str, Any]], bool],
3249+
target_specific_normalization_noise: Callable[
3250+
[dict[str, Any], dict[str, Any]], bool
3251+
],
32323252
clamp_round: Callable[..., float],
3233-
closure_forecast_reset_reentry_rebuild_path_label: Callable[[dict[str, Any]], str],
3234-
class_reset_reentry_rebuild_persistence_window_runs: int,
3253+
path_label: Callable[[dict[str, Any]], str],
3254+
window_runs: int,
32353255
) -> dict[str, Any]:
32363256
matching_events = ordered_reset_reentry_events_for_target(
32373257
target,
32383258
closure_forecast_events,
3239-
)[:class_reset_reentry_rebuild_persistence_window_runs]
3259+
)[:window_runs]
32403260
relevant_events = [
3241-
event
3242-
for event in matching_events
3243-
if closure_forecast_reset_reentry_rebuild_side_from_event(event) != "none"
3244-
]
3245-
side_path = [
3246-
closure_forecast_reset_reentry_rebuild_side_from_event(event) for event in relevant_events
3261+
event for event in matching_events if side_from_event(event) != "none"
32473262
]
3263+
side_path = [side_from_event(event) for event in relevant_events]
32483264
current_side = side_path[0] if side_path else "none"
32493265
local_noise = target_specific_normalization_noise(target, transition_history_meta)
32503266
if current_side == "none":
32513267
churn_score = 0.0
32523268
churn_status = "none"
3253-
churn_reason = ""
32543269
else:
32553270
flip_count = class_direction_flip_count(
32563271
[
3257-
"supporting-confirmation" if side == "confirmation" else "supporting-clearance"
3272+
"supporting-confirmation"
3273+
if side == "confirmation"
3274+
else "supporting-clearance"
32583275
for side in side_path
32593276
]
32603277
)
32613278
churn_score = float(flip_count) * 0.20
3262-
stability_status = target.get("closure_forecast_stability_status", "watch")
3263-
momentum_status = target.get("closure_forecast_momentum_status", "insufficient-data")
3279+
stability_status = str(target.get("closure_forecast_stability_status", "watch"))
3280+
momentum_status = str(
3281+
target.get("closure_forecast_momentum_status", "insufficient-data")
3282+
)
32643283
if stability_status == "oscillating":
32653284
churn_score += 0.15
32663285
if momentum_status == "reversing":
32673286
churn_score += 0.10
32683287
if momentum_status == "unstable":
32693288
churn_score += 0.10
32703289
freshness_path = [
3271-
event.get(
3272-
"closure_forecast_reset_reentry_freshness_status",
3273-
"insufficient-data",
3274-
)
3290+
str(event.get(spec.freshness_key, "insufficient-data"))
32753291
for event in relevant_events
32763292
]
32773293
if any(
3278-
previous == "fresh" and current in {"mixed-age", "stale", "insufficient-data"}
3294+
previous == "fresh"
3295+
and current in {"mixed-age", "stale", "insufficient-data"}
32793296
for previous, current in zip(freshness_path, freshness_path[1:])
32803297
):
32813298
churn_score += 0.10
32823299
if any(
3283-
event.get("closure_forecast_reset_reentry_reset_status", "none") != "none"
3284-
for event in relevant_events
3300+
event.get(spec.reset_key, "none") != "none" for event in relevant_events
32853301
):
32863302
churn_score += 0.10
32873303
if (
32883304
len(relevant_events) >= 2
32893305
and side_path[0] == side_path[1]
3290-
and relevant_events[0].get(
3291-
"closure_forecast_reset_reentry_freshness_status",
3292-
"insufficient-data",
3293-
)
3306+
and relevant_events[0].get(spec.freshness_key, "insufficient-data")
32943307
== "fresh"
3295-
and relevant_events[1].get(
3296-
"closure_forecast_reset_reentry_freshness_status",
3297-
"insufficient-data",
3298-
)
3308+
and relevant_events[1].get(spec.freshness_key, "insufficient-data")
32993309
== "fresh"
33003310
):
33013311
churn_score -= 0.10
33023312
churn_score = clamp_round(churn_score, lower=0.0, upper=0.95)
33033313
if local_noise and current_side == "confirmation":
33043314
churn_status = "blocked"
3305-
churn_reason = (
3306-
"Local target instability is preventing positive confirmation-side rebuild persistence."
3307-
)
33083315
elif churn_score >= 0.45 or flip_count >= 2:
33093316
churn_status = "churn"
3310-
churn_reason = (
3311-
"Rebuilt reset re-entry is flipping enough that restored posture should be softened quickly."
3312-
)
33133317
elif churn_score >= 0.20:
33143318
churn_status = "watch"
3315-
churn_reason = (
3316-
"Rebuilt reset re-entry is wobbling and may lose its restored strength soon."
3317-
)
33183319
else:
33193320
churn_status = "none"
3320-
churn_reason = ""
33213321

3322+
churn_reason = spec.reasons.get(churn_status, "")
33223323
return {
3323-
"closure_forecast_reset_reentry_rebuild_churn_score": churn_score,
3324-
"closure_forecast_reset_reentry_rebuild_churn_status": churn_status,
3325-
"closure_forecast_reset_reentry_rebuild_churn_reason": churn_reason,
3326-
"recent_reset_reentry_rebuild_churn_path": " -> ".join(
3327-
closure_forecast_reset_reentry_rebuild_path_label(event)
3328-
for event in matching_events
3329-
if event
3324+
spec.score_key: churn_score,
3325+
spec.status_out_key: churn_status,
3326+
spec.reason_key: churn_reason,
3327+
spec.path_key: " -> ".join(
3328+
path_label(event) for event in matching_events if event
33303329
),
33313330
}
33323331

33333332

3333+
_REBUILD_CHURN_SPEC = _ChurnTierSpec(
3334+
freshness_key="closure_forecast_reset_reentry_freshness_status",
3335+
reset_key="closure_forecast_reset_reentry_reset_status",
3336+
reasons={
3337+
"blocked": "Local target instability is preventing positive confirmation-side rebuild persistence.",
3338+
"churn": "Rebuilt reset re-entry is flipping enough that restored posture should be softened quickly.",
3339+
"watch": "Rebuilt reset re-entry is wobbling and may lose its restored strength soon.",
3340+
},
3341+
score_key="closure_forecast_reset_reentry_rebuild_churn_score",
3342+
status_out_key="closure_forecast_reset_reentry_rebuild_churn_status",
3343+
reason_key="closure_forecast_reset_reentry_rebuild_churn_reason",
3344+
path_key="recent_reset_reentry_rebuild_churn_path",
3345+
)
3346+
3347+
3348+
_RERERESTORE_CHURN_SPEC = _ChurnTierSpec(
3349+
freshness_key="closure_forecast_reset_reentry_rebuild_reentry_restore_rerestore_freshness_status",
3350+
reset_key="closure_forecast_reset_reentry_rebuild_reentry_restore_rerestore_reset_status",
3351+
reasons={
3352+
"blocked": "Local target instability is preventing positive confirmation-side re-re-restored hold.",
3353+
"churn": "Re-re-restored rebuilt re-entry is flipping enough that stronger posture should be softened quickly.",
3354+
"watch": "Re-re-restored rebuilt re-entry is wobbling and may lose its stronger posture soon.",
3355+
},
3356+
score_key="closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_churn_score",
3357+
status_out_key="closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_churn_status",
3358+
reason_key="closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_churn_reason",
3359+
path_key="recent_reset_reentry_rebuild_reentry_restore_rererestore_churn_path",
3360+
)
3361+
3362+
3363+
def closure_forecast_reset_reentry_rebuild_churn_for_target(
3364+
target: dict[str, Any],
3365+
closure_forecast_events: list[dict[str, Any]],
3366+
transition_history_meta: dict[str, Any],
3367+
*,
3368+
ordered_reset_reentry_events_for_target: Callable[
3369+
[dict[str, Any], list[dict[str, Any]]], list[dict[str, Any]]
3370+
],
3371+
closure_forecast_reset_reentry_rebuild_side_from_event: Callable[[dict[str, Any]], str],
3372+
class_direction_flip_count: Callable[[list[str]], int],
3373+
target_specific_normalization_noise: Callable[[dict[str, Any], dict[str, Any]], bool],
3374+
clamp_round: Callable[..., float],
3375+
closure_forecast_reset_reentry_rebuild_path_label: Callable[[dict[str, Any]], str],
3376+
class_reset_reentry_rebuild_persistence_window_runs: int,
3377+
) -> dict[str, Any]:
3378+
return _churn_for_target_base(
3379+
target,
3380+
closure_forecast_events,
3381+
transition_history_meta,
3382+
spec=_REBUILD_CHURN_SPEC,
3383+
ordered_reset_reentry_events_for_target=ordered_reset_reentry_events_for_target,
3384+
side_from_event=closure_forecast_reset_reentry_rebuild_side_from_event,
3385+
class_direction_flip_count=class_direction_flip_count,
3386+
target_specific_normalization_noise=target_specific_normalization_noise,
3387+
clamp_round=clamp_round,
3388+
path_label=closure_forecast_reset_reentry_rebuild_path_label,
3389+
window_runs=class_reset_reentry_rebuild_persistence_window_runs,
3390+
)
3391+
3392+
33343393
def apply_reset_reentry_rebuild_persistence_and_churn_control(
33353394
target: dict[str, Any],
33363395
*,
@@ -5798,118 +5857,19 @@ def closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_churn_for
57985857
clamp_round: Callable[..., float],
57995858
class_reset_reentry_rebuild_reentry_restore_rererestore_window_runs: int,
58005859
) -> dict[str, Any]:
5801-
matching_events = ordered_reset_reentry_events_for_target(
5860+
return _churn_for_target_base(
58025861
target,
58035862
closure_forecast_events,
5804-
)[:class_reset_reentry_rebuild_reentry_restore_rererestore_window_runs]
5805-
relevant_events = [
5806-
event
5807-
for event in matching_events
5808-
if closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_side_from_event(
5809-
event
5810-
)
5811-
!= "none"
5812-
]
5813-
side_path = [
5814-
closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_side_from_event(
5815-
event
5816-
)
5817-
for event in relevant_events
5818-
]
5819-
current_side = side_path[0] if side_path else "none"
5820-
local_noise = target_specific_normalization_noise(target, transition_history_meta)
5821-
if current_side == "none":
5822-
churn_score = 0.0
5823-
churn_status = "none"
5824-
churn_reason = ""
5825-
else:
5826-
flip_count = class_direction_flip_count(
5827-
[
5828-
"supporting-confirmation" if side == "confirmation" else "supporting-clearance"
5829-
for side in side_path
5830-
]
5831-
)
5832-
churn_score = float(flip_count) * 0.20
5833-
stability_status = str(target.get("closure_forecast_stability_status", "watch"))
5834-
momentum_status = str(
5835-
target.get("closure_forecast_momentum_status", "insufficient-data")
5836-
)
5837-
if stability_status == "oscillating":
5838-
churn_score += 0.15
5839-
if momentum_status == "reversing":
5840-
churn_score += 0.10
5841-
if momentum_status == "unstable":
5842-
churn_score += 0.10
5843-
freshness_path = [
5844-
str(
5845-
event.get(
5846-
"closure_forecast_reset_reentry_rebuild_reentry_restore_rerestore_freshness_status",
5847-
"insufficient-data",
5848-
)
5849-
)
5850-
for event in relevant_events
5851-
]
5852-
if any(
5853-
previous == "fresh" and current in {"mixed-age", "stale", "insufficient-data"}
5854-
for previous, current in zip(freshness_path, freshness_path[1:])
5855-
):
5856-
churn_score += 0.10
5857-
if any(
5858-
event.get(
5859-
"closure_forecast_reset_reentry_rebuild_reentry_restore_rerestore_reset_status",
5860-
"none",
5861-
)
5862-
!= "none"
5863-
for event in relevant_events
5864-
):
5865-
churn_score += 0.10
5866-
if (
5867-
len(relevant_events) >= 2
5868-
and side_path[0] == side_path[1]
5869-
and relevant_events[0].get(
5870-
"closure_forecast_reset_reentry_rebuild_reentry_restore_rerestore_freshness_status",
5871-
"insufficient-data",
5872-
)
5873-
== "fresh"
5874-
and relevant_events[1].get(
5875-
"closure_forecast_reset_reentry_rebuild_reentry_restore_rerestore_freshness_status",
5876-
"insufficient-data",
5877-
)
5878-
== "fresh"
5879-
):
5880-
churn_score -= 0.10
5881-
churn_score = clamp_round(churn_score, lower=0.0, upper=0.95)
5882-
if local_noise and current_side == "confirmation":
5883-
churn_status = "blocked"
5884-
churn_reason = (
5885-
"Local target instability is preventing positive confirmation-side re-re-restored hold."
5886-
)
5887-
elif churn_score >= 0.45 or flip_count >= 2:
5888-
churn_status = "churn"
5889-
churn_reason = (
5890-
"Re-re-restored rebuilt re-entry is flipping enough that stronger posture should be softened quickly."
5891-
)
5892-
elif churn_score >= 0.20:
5893-
churn_status = "watch"
5894-
churn_reason = (
5895-
"Re-re-restored rebuilt re-entry is wobbling and may lose its stronger posture soon."
5896-
)
5897-
else:
5898-
churn_status = "none"
5899-
churn_reason = ""
5900-
5901-
return {
5902-
"closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_churn_score": churn_score,
5903-
"closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_churn_status": churn_status,
5904-
"closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_churn_reason": churn_reason,
5905-
"recent_reset_reentry_rebuild_reentry_restore_rererestore_churn_path": " -> ".join(
5906-
closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_path_label(
5907-
event
5908-
)
5909-
for event in matching_events
5910-
if event
5911-
),
5912-
}
5863+
transition_history_meta,
5864+
spec=_RERERESTORE_CHURN_SPEC,
5865+
ordered_reset_reentry_events_for_target=ordered_reset_reentry_events_for_target,
5866+
side_from_event=closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_side_from_event,
5867+
class_direction_flip_count=class_direction_flip_count,
5868+
target_specific_normalization_noise=target_specific_normalization_noise,
5869+
clamp_round=clamp_round,
5870+
path_label=closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_path_label,
5871+
window_runs=class_reset_reentry_rebuild_reentry_restore_rererestore_window_runs,
5872+
)
59135873

59145874

59155875
def apply_reset_reentry_rebuild_reentry_restore_rererestore_persistence_and_churn_control(

0 commit comments

Comments
 (0)