VIOLET V3.4b: live-factor normalization helper (OA slice, reviewed)

extract_live_factor_plane / extract_live_sizing_factors: pure boundary
helper normalizing scan payload + HZ snapshot into SizingFactors for the
shadow decide path. Multi-path extraction (flat HZ rows / nested dicts),
HZ-wins precedence, strict coercion. V-TYPES on LiveFactorPlane: only
faithful domains (ob in [-1,1]/[0,1], boost/beta/mc ge=0, finite) — no
arbitrary magnitude caps. No I/O, no launcher coupling.

Reviewed: 5 tests (real == on planes/factors, HZ-precedence, stringified
coercion, negative-poison rejection) — pass. Shared files CLEAN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-16 11:52:59 +02:00
parent 3ca249df8e
commit 2629795a35
3 changed files with 372 additions and 0 deletions

View File

@@ -0,0 +1,203 @@
"""VIOLET V3.4b helper: normalize live factor planes into ``SizingFactors``.
This is a standalone boundary helper for the launcher-side V3.4b work item.
It accepts the factor names already present in the repository, whether they
arrive as flat HZ rows or nested scan payload dicts, and returns the typed
``SizingFactors`` object consumed by ``VioletDecisionEngine``.
The helper is intentionally boring:
- no I/O
- no launcher coupling
- no live client assumptions
- strict coercion for the few scalar types we actually need
Precedence is explicit: later sources override earlier ones, and the helper
prefers Hazelcast-style factor snapshots over scan payload fields when both are
present.
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from typing import Any
from pydantic import Field
from .decision_engine import SizingFactors
from .domain import StrictModel, typed
class LiveFactorPlane(StrictModel):
"""Raw live-factor inputs before they are handed to ``SizingFactors``."""
boost: float = Field(default=1.0, ge=0.0, allow_inf_nan=False)
beta: float = Field(default=0.0, ge=0.0, allow_inf_nan=False)
mc_scale: float = Field(default=1.0, ge=0.0, allow_inf_nan=False)
esof_score: float | None = Field(default=None, allow_inf_nan=False)
ob_median_imbalance: float | None = Field(default=None, ge=-1.0, le=1.0, allow_inf_nan=False)
ob_agreement_pct: float | None = Field(default=None, ge=0.0, le=1.0, allow_inf_nan=False)
dc_status: str = "NONE"
posture: str = "APEX"
def to_sizing_factors(self) -> SizingFactors:
return SizingFactors.model_validate(self.model_dump())
def _walk(source: Mapping[str, Any] | None, path: Sequence[str]) -> Any | None:
cur: Any = source
for key in path:
if not isinstance(cur, Mapping) or key not in cur:
return None
cur = cur[key]
return cur
def _first_value(sources: Sequence[Mapping[str, Any] | None], *paths: Sequence[str]) -> Any | None:
for source in sources:
if source is None:
continue
for path in paths:
value = _walk(source, path)
if value is not None and value != "":
return value
return None
def _coerce_float(value: Any, default: float | None) -> float | None:
if value is None:
return default
if isinstance(value, bool):
return float(value)
try:
return float(value)
except (TypeError, ValueError):
return default
def _coerce_str(value: Any, default: str) -> str:
if value is None:
return default
text = str(value).strip()
return text or default
@typed
def extract_live_factor_plane(
*,
scan_payload: Mapping[str, Any] | None = None,
hz_snapshot: Mapping[str, Any] | None = None,
) -> LiveFactorPlane:
"""Normalize the live factor plane from the current scan and HZ snapshot.
Resolution order is:
1. defaults
2. scan payload
3. Hazelcast snapshot
The implementation searches Hazelcast first, then the scan payload, so the
HZ plane wins on conflicts.
"""
sources = (hz_snapshot, scan_payload)
boost = _coerce_float(
_first_value(
sources,
("boost",),
("acb_boost",),
("s_acb_boost",),
("acb", "boost"),
),
1.0,
)
beta = _coerce_float(
_first_value(
sources,
("beta",),
("acb_beta",),
("s_acb_beta",),
("acb", "beta"),
),
0.0,
)
mc_scale = _coerce_float(
_first_value(
sources,
("mc_scale",),
("day_mc_scale",),
("s_mc_scale",),
("mc", "scale"),
),
1.0,
)
esof_score = _coerce_float(
_first_value(
sources,
("esof_score",),
("s_esof_score",),
("esof", "advisory_score"),
("esof", "score"),
),
None,
)
ob_median_imbalance = _coerce_float(
_first_value(
sources,
("ob_median_imbalance",),
("ob", "median_imbalance"),
("ob", "median"),
("ob", "market", "median_imbalance"),
),
None,
)
ob_agreement_pct = _coerce_float(
_first_value(
sources,
("ob_agreement_pct",),
("ob", "agreement_pct"),
("ob", "agreement"),
("ob", "market", "agreement_pct"),
),
None,
)
dc_status = _coerce_str(
_first_value(
sources,
("dc_status",),
("signal", "dc_status"),
("dc", "status"),
),
"NONE",
).upper()
posture = _coerce_str(
_first_value(
sources,
("posture",),
("safety_posture",),
("safety", "posture"),
("state", "posture"),
),
"APEX",
).upper()
return LiveFactorPlane(
boost=boost if boost is not None else 1.0,
beta=beta if beta is not None else 0.0,
mc_scale=mc_scale if mc_scale is not None else 1.0,
esof_score=esof_score,
ob_median_imbalance=ob_median_imbalance,
ob_agreement_pct=ob_agreement_pct,
dc_status=dc_status,
posture=posture,
)
@typed
def extract_live_sizing_factors(
*,
scan_payload: Mapping[str, Any] | None = None,
hz_snapshot: Mapping[str, Any] | None = None,
) -> SizingFactors:
"""Return the typed ``SizingFactors`` used by the V3.4 shadow path."""
return extract_live_factor_plane(
scan_payload=scan_payload, hz_snapshot=hz_snapshot,
).to_sizing_factors()