feat(s36): estimate table (kost-mot-verdi + placeholder rows + sourced quality guidance)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145ZKPLMVeqM47z2jxxokym
This commit is contained in:
Kjell Tore Guttormsen 2026-07-15 10:02:00 +02:00
commit 37625435c4
3 changed files with 150 additions and 0 deletions

View file

@ -20,6 +20,7 @@ from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from pydantic import BaseModel, Field
@ -150,3 +151,95 @@ def estimate_portfolio_ore(
if per_run is None:
return None
return n_projects * per_run
def _ore_to_kr_str(ore: int) -> str:
"""Format integer øre as an edge-formatted kr STRING (never a float — preserves the no-float
invariant). E.g. 123456 øre '1 234,56 kr' (Norwegian: space thousands, comma decimals)."""
sign = "-" if ore < 0 else ""
kr, rest = divmod(abs(ore), 100)
kr_str = f"{kr:,}".replace(",", " ") # non-breaking space thousands separator
return f"{sign}{kr_str},{rest:02d} kr"
def _guidance_for(model: str, pricing: PricingContract) -> dict[str, str] | None:
"""The model's quality guidance as ``{note, source}`` (guidance with kilde), or ``None``."""
entry = pricing.models.get(model)
if entry is None or entry.quality_guidance is None:
return None
return {"note": entry.quality_guidance.note, "source": entry.quality_guidance.source}
def build_estimate_table(
profile: str,
efforts: list[str],
pricing: PricingContract,
*,
n_projects: int,
model_map: dict[str, dict[str, str]] | None = None,
max_tokens: int = _DEFAULT_MAX_TOKENS,
) -> dict[str, Any]:
"""Build the what-if estimate table for ``profile`` — one row per (role × effort), sorted for
determinism. Priced rows carry ``estimat_ore`` (int), ``estimat_kr`` (edge string),
``estimat_type: "øvre grense"`` (honesty label) and ``kvalitets_veiledning`` (sourced guidance,
brief Goal 4). Placeholder-deployment rows carry ``estimat_ore: None`` +
``estimat_type: "uspesifisert (placeholder-deployment)"`` (no crash). The top-level
``kost_mot_verdi`` field is the S5.4 value-report seam. A genuine (non-placeholder) model absent
from the price map fails fast (``ValueError`` via ``_price_ore_per_1k``)."""
roles = _load_model_map(profile, model_map)
rows: list[dict[str, Any]] = []
total_ore = 0
for role in sorted(roles):
model = roles[role]
rate = _price_ore_per_1k(model, pricing) # raises for genuine-missing; None for placeholder
guidance = _guidance_for(model, pricing)
for effort in sorted(efforts):
if rate is None:
rows.append(
{
"role": role,
"model": model,
"effort": effort,
"estimat_ore": None,
"estimat_kr": None,
"estimat_type": "uspesifisert (placeholder-deployment)",
"kvalitets_veiledning": guidance,
}
)
else:
est = estimate_portfolio_ore(
model, effort, pricing, n_projects=n_projects, max_tokens=max_tokens
)
assert est is not None # rate is not None → est is int
total_ore += est
rows.append(
{
"role": role,
"model": model,
"effort": effort,
"estimat_ore": est,
"estimat_kr": _ore_to_kr_str(est),
"estimat_type": "øvre grense",
"kvalitets_veiledning": guidance,
}
)
return {
"profile": profile,
"n_projects": n_projects,
"max_tokens": max_tokens,
"pricing_source": pricing.source,
"pricing_date": pricing.date,
"rows": rows,
"kost_mot_verdi": {
"kjoring_kostet_ore": total_ore,
"kjoring_kostet_kr": _ore_to_kr_str(total_ore),
"kvalitetssikret_modellert_besparelse_ore": None,
"note": "fylles av S5.4 verdirapport",
},
}
def dump_estimate_table(table: dict[str, Any]) -> str:
"""Byte-deterministic JSON serialization of an estimate table (mirrors ``outbox._dump``:
``sort_keys``, ``indent=2``, trailing LF)."""
return json.dumps(table, sort_keys=True, indent=2) + "\n"