feat(s36): costsim CLI + no-network + no-hardcoded-price guards
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0145ZKPLMVeqM47z2jxxokym
This commit is contained in:
parent
37625435c4
commit
76d9f793c6
3 changed files with 127 additions and 0 deletions
|
|
@ -19,6 +19,7 @@ kr is formatted only at the display edge.
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -243,3 +244,66 @@ 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"
|
||||
|
||||
|
||||
def _format_table_text(table: dict[str, Any]) -> str:
|
||||
"""Human-readable estimate table for the CLI (honest labels + per-model guidance + kost-mot-verdi
|
||||
+ adoption-path footer)."""
|
||||
lines = [
|
||||
f"Kostnadsestimat (øvre grense, ikke prediksjon) — profil: {table['profile']}, "
|
||||
f"prosjekter: {table['n_projects']}, max_tokens/run: {table['max_tokens']}",
|
||||
f"Prisdata: {table['pricing_source']} ({table['pricing_date']})",
|
||||
"",
|
||||
f"{'rolle':<10} {'modell':<30} {'effort':<10} {'estimat':>16} type",
|
||||
]
|
||||
for row in table["rows"]:
|
||||
est = row["estimat_kr"] if row["estimat_kr"] is not None else "—"
|
||||
lines.append(
|
||||
f"{row['role']:<10} {row['model']:<30} {row['effort']:<10} {est:>16} "
|
||||
f"{row['estimat_type']}"
|
||||
)
|
||||
guidance = row["kvalitets_veiledning"]
|
||||
if guidance is not None:
|
||||
lines.append(f" veiledning (kilde: {guidance['source']}): {guidance['note']}")
|
||||
kmv = table["kost_mot_verdi"]
|
||||
lines += [
|
||||
"",
|
||||
f"kost-mot-verdi: kjøringen kostet ~{kmv['kjoring_kostet_kr']} (modellert øvre grense); "
|
||||
"kvalitetssikret modellert besparelse fylles av S5.4 verdirapport",
|
||||
"",
|
||||
"Adopsjonssti: start liten (én dimensjon, ett prosjekt, lavt tak) → eskaler med tilliten.",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
"""CLI entry: ``python -m portfolio_optimiser.costsim`` — offline what-if estimate table over the
|
||||
model-map (models × effort levels). No network; fail-fast (rc 1) on missing/invalid config."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="portfolio_optimiser.costsim",
|
||||
description="Offline kostnadssimulering før kjøring (S3.6) — estimerer kjøringskost per "
|
||||
"modell × effortnivå fra skjema-validert prisdata. Ingen modellkall, ingen nettverk.",
|
||||
)
|
||||
parser.add_argument("--profile", default="local", help="model-map profil (local/azure)")
|
||||
parser.add_argument("--projects", type=int, default=4, help="antall prosjekter i porteføljen")
|
||||
parser.add_argument(
|
||||
"--efforts", default="low,standard,high", help="komma-liste av effortnivåer"
|
||||
)
|
||||
parser.add_argument("--pricing", default=None, help="sti til prisdata-konfig (default: pakket)")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
efforts = [e.strip() for e in args.efforts.split(",") if e.strip()]
|
||||
try:
|
||||
pricing = load_pricing(args.pricing)
|
||||
table = build_estimate_table(args.profile, efforts, pricing, n_projects=args.projects)
|
||||
except (FileNotFoundError, ValueError) as exc:
|
||||
print(f"costsim: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
print(_format_table_text(table))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover - console entry
|
||||
raise SystemExit(main())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue