feat: ingest State Hub session tokens without booking them

Store mixed measured/estimated/superseded aggregates as session-token
evidence. Map kinds onto the evidence-basis vocabulary, keep unknown
months unknown, and refuse to post the snapshot as booked spend or
resource-control usage.
This commit is contained in:
tegwick 2026-08-15 19:21:41 +02:00
parent 22b21be552
commit d343888a41
10 changed files with 578 additions and 1 deletions

View file

@ -0,0 +1,87 @@
"""Session-token evidence ingested from State Hub aggregates."""
from __future__ import annotations
from datetime import date, datetime
from decimal import Decimal
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
MEASUREMENT_KINDS = ("measured", "allocated", "estimated", "superseded")
EVIDENCE_BASIS = {
"measured": "measured",
"allocated": "derived",
"estimated": "estimated",
"superseded": "excluded",
}
class SessionTokenSlice(BaseModel):
model_config = ConfigDict(extra="forbid")
tokens_in: int = Field(ge=0)
tokens_out: int = Field(ge=0)
event_count: int = Field(ge=0)
confidence: Decimal | None = None
method: str | None = None
@field_validator("confidence")
@classmethod
def validate_confidence(cls, value: Decimal | None) -> Decimal | None:
if value is None:
return None
if not value.is_finite() or value < 0 or value > 1:
raise ValueError("confidence must be between 0 and 1")
return value
class SessionTokenAssociation(BaseModel):
model_config = ConfigDict(extra="forbid")
scope: Literal["repo", "workplan", "model"]
scope_id: str = Field(min_length=1, max_length=256)
label: str | None = None
measurement_kind: Literal["measured", "allocated", "estimated", "superseded"]
tokens_in: int = Field(ge=0)
tokens_out: int = Field(ge=0)
event_count: int = Field(ge=0, default=1)
class SessionTokenEvidence(BaseModel):
"""State Hub session-token snapshot. Not booked spend and not usage_observation."""
model_config = ConfigDict(extra="forbid")
schema_version: Literal["0.1"] = "0.1"
record_type: Literal["session_token_evidence"] = "session_token_evidence"
record_id: str = Field(min_length=1, max_length=256)
revision_of: str | None = None
source_system: Literal["state-hub"] = "state-hub"
period_start: date
period_end: date
captured_at: datetime
source_evidence: list[str] = Field(min_length=1)
totals: dict[str, SessionTokenSlice] = Field(default_factory=dict)
associations: list[SessionTokenAssociation] = Field(default_factory=list)
@field_validator("totals")
@classmethod
def validate_totals(cls, value: dict[str, SessionTokenSlice]) -> dict[str, SessionTokenSlice]:
unknown = set(value) - set(MEASUREMENT_KINDS)
if unknown:
raise ValueError(f"unknown measurement_kind: {', '.join(sorted(unknown))}")
return value
@model_validator(mode="after")
def validate_period_and_methods(self) -> "SessionTokenEvidence":
if self.period_end < self.period_start:
raise ValueError("period_end cannot precede period_start")
allocated = self.totals.get("allocated")
if (
allocated is not None
and allocated.event_count > 0
and not (allocated.method and allocated.method.strip())
):
raise ValueError("allocated totals with events require method")
return self