Jerlshin's picture
defect fix career
f0894e2
Raw
History Blame Contribute Delete
10.5 kB
from __future__ import annotations
import math
from collections.abc import Mapping
from datetime import date
from types import MappingProxyType
from typing import Final, final
from pydantic import BaseModel, ConfigDict, Field, field_validator
from redstack.domain.enums import EvidenceKind
from redstack.domain.ids import UnitScore
from redstack.domain.provenance import EvidenceRef
from redstack.domain.source import RawCandidate
from redstack.features.parsing import resolve_path
_VO = ConfigDict(
frozen=True, extra="forbid", str_strip_whitespace=True, validate_default=True
)
FeatureId = str
CellEmission = tuple[tuple[FeatureId, "FeatureCell"], ...]
# --------------------------------------------------------------------------- #
# Pure numeric helpers (shared normalization vocabulary). #
# --------------------------------------------------------------------------- #
def clamp_unit(value: float) -> float:
"""Clamp a finite float into ``[0, 1]``; a non-finite input is a bug → raise."""
if not math.isfinite(value):
raise ValueError(f"clamp_unit received a non-finite value: {value!r}")
if value <= 0.0:
return 0.0
if value >= 1.0:
return 1.0
return float(value)
def unit(value: float) -> UnitScore:
"""Construct a ``UnitScore`` from a float, clamping into ``[0, 1]``."""
return UnitScore(clamp_unit(value))
def bounded_log_scale(count: float, *, saturation: float) -> float:
"""Map a non-negative count onto ``[0, 1]`` with diminishing returns.
``log1p(count) / log1p(saturation)`` — a count equal to ``saturation`` maps
to ~1.0; growth past it is clamped. Negative counts (sentinels) clamp to 0.
"""
if saturation <= 0.0:
raise ValueError("saturation must be positive")
safe = count if count > 0.0 else 0.0
return clamp_unit(math.log1p(safe) / math.log1p(saturation))
def inverse_bounded(value: float, *, scale: float) -> float:
"""Map a non-negative magnitude onto ``(0, 1]`` decreasing in ``value``.
``scale / (scale + value)`` — ``value == 0`` → 1.0, ``value == scale`` → 0.5.
Used for "smaller is better" quantities such as response time in hours.
"""
if scale <= 0.0:
raise ValueError("scale must be positive")
safe = value if value > 0.0 else 0.0
return clamp_unit(scale / (scale + safe))
def recency_unit(days_elapsed: float, *, half_life_days: float) -> float:
"""Exponential recency in ``[0, 1]``: 1.0 today, 0.5 at one half-life.
A negative ``days_elapsed`` (a future date relative to ``as_of``) is treated
as 0 days (fully recent) here; the *impossibility* of a future date is the
honeypot layer's job, not the normalizer's.
"""
if half_life_days <= 0.0:
raise ValueError("half_life_days must be positive")
safe = days_elapsed if days_elapsed > 0.0 else 0.0
return clamp_unit(math.pow(0.5, safe / half_life_days))
def days_between(later: date, earlier: date) -> int:
"""Signed day delta ``later - earlier`` (negative if ``later`` precedes)."""
return (later - earlier).days
def mean_of(values: tuple[float, ...]) -> float:
"""Arithmetic mean of a non-empty tuple; empty → 0.0 (neutral)."""
if not values:
return 0.0
return math.fsum(values) / len(values)
def make_evidence(
kind: EvidenceKind,
path: str,
value: str | int | float | bool,
*,
raw: RawCandidate | None = None,
) -> EvidenceRef:
"""Mint an ``EvidenceRef``; ``date`` callers pass ``.isoformat()`` strings.
When ``raw`` is given, ``path`` is verified to resolve inside it before
the ref is minted -- a dangling path (wrong index, renamed field) raises
``ProvenanceError`` immediately rather than shipping a citation nothing
backs. ``value`` is kept as the caller supplied it (it may be a derived
label, not the literal scalar at ``path``); only existence is checked.
Callers citing a literal ``RawCandidate`` field must pass ``raw``.
``EvidenceKind.DERIVED`` evidence (and citations of fields on an
already-validated domain profile, where no raw record exists to dangle
against) may omit it.
"""
if raw is not None:
resolve_path(raw, path)
return EvidenceRef(kind=kind, path=path, value=value)
# --------------------------------------------------------------------------- #
# Feature cell. #
# --------------------------------------------------------------------------- #
@final
class FeatureCell(BaseModel):
"""One feature's ``(value, confidence, evidence)`` output.
``value`` carries no range constraint here beyond finiteness — the per-index
bounds in ``FeatureLayout`` are checked when the cell folds into the CQV.
``evidence`` is non-empty by construction: a feature with no evidence cannot
be cited by Reasoning, so emitting one would be a silent hallucination risk.
"""
model_config = _VO
value: float = Field(allow_inf_nan=False)
confidence: UnitScore = Field(ge=0.0, le=1.0, allow_inf_nan=False)
evidence: tuple[EvidenceRef, ...] = Field(min_length=1)
@field_validator("confidence", mode="after")
@classmethod
def _confidence_unit(cls, value: float) -> UnitScore:
return UnitScore(value)
def cell(
value: float, confidence: float, evidence: tuple[EvidenceRef, ...]
) -> FeatureCell:
"""Build a ``FeatureCell``, clamping ``confidence`` into ``[0, 1]``."""
return FeatureCell(value=value, confidence=unit(confidence), evidence=evidence)
def group_of(feature_id: str) -> str:
"""Group prefix (text before the first dot) of a feature id."""
return feature_id.split(".", 1)[0]
# --------------------------------------------------------------------------- #
# Read-only feature view (Part 9 — the sole engine read surface). #
# --------------------------------------------------------------------------- #
@final
class FeatureView(BaseModel):
"""Typed, read-only accessor over one candidate's cells + group confidence.
Engines never touch raw arrays; they resolve features by id through this
view. Construction is via ``from_cells`` (which derives group confidence as
the mean of each group's member-cell confidences). All three accessors are
pure and deterministic.
"""
model_config = _VO
cells: Mapping[FeatureId, FeatureCell]
group_confidences: Mapping[str, float]
importances: Mapping[FeatureId, float]
@field_validator("cells", mode="after")
@classmethod
def _freeze_cells(
cls, value: Mapping[FeatureId, FeatureCell]
) -> Mapping[FeatureId, FeatureCell]:
return MappingProxyType(dict(value))
@field_validator("group_confidences", mode="after")
@classmethod
def _freeze_group_conf(cls, value: Mapping[str, float]) -> Mapping[str, float]:
for group, conf in value.items():
if not (0.0 <= conf <= 1.0):
raise ValueError(f"group_confidence for {group!r} not in [0, 1]")
return MappingProxyType(dict(value))
@field_validator("importances", mode="after")
@classmethod
def _freeze_importance(
cls, value: Mapping[FeatureId, float]
) -> Mapping[FeatureId, float]:
for feature_id, weight in value.items():
if not math.isfinite(weight):
raise ValueError(f"importance for {feature_id!r} is not finite")
return MappingProxyType(dict(value))
# -- the Part 9 contract --------------------------------------------- #
def get(self, feature_id: FeatureId) -> FeatureCell:
"""Resolve a feature's cell. Unknown id → ``KeyError`` (programming error)."""
return self.cells[feature_id]
def group_confidence(self, group: str) -> UnitScore:
"""Group-granular confidence. Unknown group → ``KeyError``."""
return UnitScore(self.group_confidences[group])
def importance(self, feature_id: FeatureId) -> float:
"""Learned importance; a feature with no learned weight returns ``0.0``."""
return self.importances.get(feature_id, 0.0)
# -- convenience (still read-only) ----------------------------------- #
def has(self, feature_id: FeatureId) -> bool:
"""Whether a cell was emitted for ``feature_id``."""
return feature_id in self.cells
def value_of(self, feature_id: FeatureId, default: float = 0.0) -> float:
"""The cell value, or ``default`` if the feature was not emitted."""
found = self.cells.get(feature_id)
return found.value if found is not None else default
@classmethod
def from_cells(
cls,
cells: Mapping[FeatureId, FeatureCell],
*,
importances: Mapping[FeatureId, float] | None = None,
) -> FeatureView:
"""Assemble a view, deriving group confidence as the per-group mean.
Engines build the full ``{feature_id: FeatureCell}`` map from every
extractor, then hand it here; group confidence is the deterministic mean
of member-cell confidences (Part 7: confidence is stored at group
granularity).
"""
grouped: dict[str, list[float]] = {}
for feature_id, feature_cell in cells.items():
grouped.setdefault(group_of(feature_id), []).append(
float(feature_cell.confidence)
)
group_conf = {
group: math.fsum(confs) / len(confs) for group, confs in grouped.items()
}
return cls(
cells=dict(cells),
group_confidences=group_conf,
importances=dict(importances) if importances is not None else {},
)
# Saturation / scale constants shared by the extractors (documented once here).
ENDORSEMENT_SATURATION: Final[float] = 50.0
DURATION_SATURATION_MONTHS: Final[float] = 36.0
ACTIVITY_HALF_LIFE_DAYS: Final[float] = 90.0
STALE_HALF_LIFE_DAYS: Final[float] = 180.0
RESPONSE_TIME_SCALE_HOURS: Final[float] = 24.0
__all__ = (
"ACTIVITY_HALF_LIFE_DAYS",
"CellEmission",
"DURATION_SATURATION_MONTHS",
"ENDORSEMENT_SATURATION",
"FeatureCell",
"FeatureId",
"FeatureView",
"RESPONSE_TIME_SCALE_HOURS",
"STALE_HALF_LIFE_DAYS",
"bounded_log_scale",
"cell",
"clamp_unit",
"days_between",
"group_of",
"inverse_bounded",
"make_evidence",
"mean_of",
"recency_unit",
"unit",
)