"""Data models for symbol resolution results.""" from __future__ import annotations from enum import Enum from pydantic import BaseModel, Field class MatchType(str, Enum): """How a resolution candidate was matched.""" exact_ticker = "exact_ticker" exact_name = "exact_name" alias = "alias" fuzzy = "fuzzy" class MentionType(str, Enum): """Whether a mention is explicitly stated or inferred from context.""" explicit = "explicit" inferred = "inferred" class UnresolvedReason(str, Enum): """Why a mention could not be resolved.""" not_in_registry = "not_in_registry" ambiguous = "ambiguous" context_needed = "context_needed" class ResolutionCandidate(BaseModel): """A single candidate match from the symbol registry. Candidates are ranked by confidence, with match_type indicating how the match was derived. """ company_id: str = Field(description="UUID of the matched company") ticker: str = Field(description="Ticker symbol of the matched company") name: str = Field(description="Legal or display name of the matched company") confidence: float = Field(ge=0.0, le=1.0, description="Match confidence score") match_type: MatchType = Field(description="How the match was derived") class ResolutionResult(BaseModel): """Full resolution output for a single mention. Contains ranked candidates, ambiguity margin, and mention classification. """ candidates: list[ResolutionCandidate] = Field(default_factory=list) ambiguity_margin: float = Field( default=1.0, ge=0.0, le=1.0, description="Difference between top-2 candidate confidences. 1.0 = unambiguous single match, 0.0 = tied.", ) is_ambiguous: bool = Field( default=False, description="True when top candidates are too close to distinguish without context.", ) mention_type: MentionType = Field( default=MentionType.explicit, description="Whether the mention is explicit text or inferred exposure.", ) class UnresolvedMention(BaseModel): """A mention that could not be resolved to any company in the registry. Preserved as-is rather than having a ticker invented for it. """ literal_text: str = Field(description="Original text as it appeared in the document") start_char: int = Field(ge=0, description="Start character offset in source document") end_char: int = Field(gt=0, description="End character offset (exclusive)") reason: UnresolvedReason = Field(description="Why resolution failed")