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:
203
prod/clean_arch/violet/live_factors.py
Normal file
203
prod/clean_arch/violet/live_factors.py
Normal 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()
|
||||
Reference in New Issue
Block a user