"""A structured calculation result. Every engine function returns one of these instead of a bare number. It carries: * ``amount`` — the headline figure (Decimal, rounded); * ``label`` — what the figure is; * ``breakdown`` — the step-by-step math, so the model can *explain* without doing arithmetic; * ``source`` / ``effective_year`` — provenance, so the UI can say "per the 2024 SAT tariff" and never present a number without its basis. This is the contract between the deterministic engine and the LLM: the model reads ``breakdown`` and ``source`` and turns them into prose. It never recomputes. """ from __future__ import annotations from dataclasses import dataclass, field from decimal import Decimal from typing import List, Optional, Tuple @dataclass class CalcResult: amount: Decimal label: str breakdown: List[Tuple[str, Decimal]] = field(default_factory=list) source: Optional[str] = None effective_year: Optional[int] = None notes: List[str] = field(default_factory=list) def explain(self) -> str: """A plain-text rendering of the math — handy for logs, tests and the UI.""" lines = [f"{self.label}: {self.amount}"] for desc, val in self.breakdown: lines.append(f" · {desc}: {val}") if self.source: year = f" ({self.effective_year})" if self.effective_year else "" lines.append(f" source: {self.source}{year}") for note in self.notes: lines.append(f" note: {note}") return "\n".join(lines)