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:
parent
eb889a7f0e
commit
37625435c4
3 changed files with 150 additions and 0 deletions
|
|
@ -20,6 +20,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
@ -150,3 +151,95 @@ def estimate_portfolio_ore(
|
||||||
if per_run is None:
|
if per_run is None:
|
||||||
return None
|
return None
|
||||||
return n_projects * per_run
|
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"
|
||||||
|
|
|
||||||
|
|
@ -76,3 +76,17 @@ def test_unknown_effort_raises() -> None:
|
||||||
pricing = costsim.PricingContract(**_VALID_PRICING)
|
pricing = costsim.PricingContract(**_VALID_PRICING)
|
||||||
with pytest.raises(ValueError, match="unknown effort"):
|
with pytest.raises(ValueError, match="unknown effort"):
|
||||||
costsim.estimate_run_ore("m-cheap", "turbo", pricing)
|
costsim.estimate_run_ore("m-cheap", "turbo", pricing)
|
||||||
|
|
||||||
|
|
||||||
|
def test_estimate_table_structure() -> None:
|
||||||
|
pricing = costsim.PricingContract(**_VALID_PRICING)
|
||||||
|
mm = {"prof": {"proposer": "m-cheap", "checker": "m-dear"}}
|
||||||
|
table = costsim.build_estimate_table(
|
||||||
|
"prof", ["low", "high"], pricing, n_projects=2, model_map=mm
|
||||||
|
)
|
||||||
|
assert len(table["rows"]) == 4 # 2 roles × 2 efforts
|
||||||
|
for row in table["rows"]:
|
||||||
|
assert row["estimat_type"] == "øvre grense"
|
||||||
|
assert isinstance(row["estimat_kr"], str) # edge-formatted string, never a float
|
||||||
|
assert table["kost_mot_verdi"]["kjoring_kostet_ore"] > 0
|
||||||
|
assert table["pricing_source"] == "unit-test prices"
|
||||||
|
|
|
||||||
|
|
@ -125,3 +125,46 @@ def test_estimate_scales_with_model_and_effort() -> None:
|
||||||
assert dear_high != high, "model must scale the estimate"
|
assert dear_high != high, "model must scale the estimate"
|
||||||
# control: same model + same effort → identical (not incidental)
|
# control: same model + same effort → identical (not incidental)
|
||||||
assert costsim.estimate_run_ore("m-cheap", "high", pricing) == high
|
assert costsim.estimate_run_ore("m-cheap", "high", pricing) == high
|
||||||
|
|
||||||
|
|
||||||
|
def test_azure_placeholder_rows_unpriced() -> None:
|
||||||
|
"""The defined azure path: ``--profile azure`` ships REPLACE-WITH-* placeholders → rows are
|
||||||
|
unpriced (``estimat_ore`` None, placeholder label), NEVER a crash. Uses the bundled config."""
|
||||||
|
pricing = costsim.load_pricing()
|
||||||
|
table = costsim.build_estimate_table("azure", ["standard"], pricing, n_projects=4)
|
||||||
|
assert table["rows"], "azure profile must still produce rows"
|
||||||
|
for row in table["rows"]:
|
||||||
|
assert row["estimat_ore"] is None
|
||||||
|
assert "placeholder" in row["estimat_type"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_table_carries_kost_mot_verdi_field() -> None:
|
||||||
|
"""SC5: the table carries a top-level ``kost_mot_verdi`` field ready for the S5.4 value report.
|
||||||
|
Detach point: remove the field from ``build_estimate_table`` → RED."""
|
||||||
|
pricing = costsim.PricingContract(**_TWO_MODEL_PRICING)
|
||||||
|
table = costsim.build_estimate_table(
|
||||||
|
"p", ["standard"], pricing, n_projects=1, model_map={"p": {"proposer": "m-cheap"}}
|
||||||
|
)
|
||||||
|
assert "kost_mot_verdi" in table
|
||||||
|
assert "kjoring_kostet_ore" in table["kost_mot_verdi"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_row_carries_sourced_quality_guidance() -> None:
|
||||||
|
"""Brief Goal 4: a priced row surfaces ``kvalitets_veiledning`` with a source (guidance with
|
||||||
|
kilde, never a bare number). Detach point: drop ``kvalitets_veiledning`` from the row → RED."""
|
||||||
|
pricing = costsim.PricingContract(**_PRICING) # m-known carries guidance
|
||||||
|
table = costsim.build_estimate_table(
|
||||||
|
"p", ["standard"], pricing, n_projects=1, model_map={"p": {"proposer": "m-known"}}
|
||||||
|
)
|
||||||
|
row = table["rows"][0]
|
||||||
|
assert row["kvalitets_veiledning"] is not None
|
||||||
|
assert row["kvalitets_veiledning"]["source"].strip()
|
||||||
|
|
||||||
|
|
||||||
|
def test_table_is_byte_deterministic() -> None:
|
||||||
|
"""Serialized table is byte-stable across identical inputs (sort_keys/indent/LF)."""
|
||||||
|
pricing = costsim.PricingContract(**_TWO_MODEL_PRICING)
|
||||||
|
mm = {"p": {"proposer": "m-cheap", "checker": "m-dear"}}
|
||||||
|
t1 = costsim.build_estimate_table("p", ["low", "high"], pricing, n_projects=3, model_map=mm)
|
||||||
|
t2 = costsim.build_estimate_table("p", ["low", "high"], pricing, n_projects=3, model_map=mm)
|
||||||
|
assert costsim.dump_estimate_table(t1).encode() == costsim.dump_estimate_table(t2).encode()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue