feat: model core, launcher, reproduction check and the first two skills

The core in modell/kjerne/ is an unmodified copy of sim.py, diagnose.py and
forlop.py from den-dynamiske-rettsstaten @2badc25, pinned by sha256.
kjor.py is the single entry point; kontroll.py reproduces the note's 22 data
files and printed results. Skills: diagnose and forlop (Norwegian).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-11-11 13:53:32 +01:00
commit 464ad38b49
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
51 changed files with 11207 additions and 0 deletions

170
tests/test_kontroll.py Normal file
View file

@ -0,0 +1,170 @@
# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
"""kontroll.py: sammenligningene, dommene og nivået."""
from __future__ import annotations
import sys
from typing import TYPE_CHECKING
import kontroll
import pytest
from conftest import kjor
if TYPE_CHECKING:
from pathlib import Path
# ---------------- sammenligningene ----------------
@pytest.mark.parametrize(("tall", "forventet"), [("19", 1.0), ("1,28", 0.01), ("7.21", 0.01),
("0.00000", 1e-5), ("1.11e-15", 1e-17), ("-3.0E+02", 10.0)])
def test_enhet_i_siste_siffer(tall: str, forventet: float) -> None:
assert kontroll.enhet(tall) == pytest.approx(forventet)
def test_tekst_lik() -> None:
assert kontroll.sammenlign_tekst("a 1,23\n", "a 1,23\n").status == kontroll.LIK
def test_tekst_innenfor_toleranse() -> None:
resultat = kontroll.sammenlign_tekst("utnyttelse 1,23 og 7.21\n", "utnyttelse 1,24 og 7.20\n")
assert resultat.status == kontroll.TOLERANSE
assert resultat.avvik == pytest.approx(0.01)
assert "-utnyttelse 1,23 og 7.21" in resultat.detaljer
assert "+utnyttelse 1,24 og 7.20" in resultat.detaljer
@pytest.mark.parametrize(("fasit", "naa"), [("a 1,23\n", "a 1,25\n"), ("a 1,23\n", "b 1,23\n"),
("a 1,23\n", "a 1,23\nb\n"), ("19 %\n", "21 %\n")])
def test_tekst_endret(fasit: str, naa: str) -> None:
resultat = kontroll.sammenlign_tekst(fasit, naa)
assert resultat.status == kontroll.ENDRET
assert resultat.detaljer
DAT = "t K Q\n0.00000 1.00000 0.29100\n0.50000 1.10000 0.30000\n"
def test_dat_lik() -> None:
assert kontroll.sammenlign_dat(DAT, DAT).status == kontroll.LIK
def test_dat_innenfor_toleranse() -> None:
resultat = kontroll.sammenlign_dat(DAT, DAT.replace("1.10000", "1.10001"))
assert resultat.status == kontroll.TOLERANSE
assert resultat.detaljer == ["største absolutte avvik per kolonne: K 1,0e-05"]
def test_dat_endret_viser_avvik_per_kolonne() -> None:
resultat = kontroll.sammenlign_dat(DAT, DAT.replace("1.10000", "1.10030").replace("0.30000", "0.30200"))
assert resultat.status == kontroll.ENDRET
assert resultat.detaljer == ["største absolutte avvik per kolonne: K 3,0e-04, Q 2,0e-03"]
assert resultat.avvik == pytest.approx(0.002)
@pytest.mark.parametrize("naa", ["t K X\n0.00000 1.00000 0.29100\n0.50000 1.10000 0.30000\n",
"t K Q\n0.00000 1.00000 0.29100\n"])
def test_dat_ulik_form(naa: str) -> None:
assert kontroll.sammenlign_dat(DAT, naa).status == kontroll.ENDRET
# ---------------- kontrollen som helhet ----------------
def test_rask_kontroll_av_pluginen(hjem: Path) -> None:
prosess = kjor("kontroll", "--rask", hjem=hjem)
assert prosess.returncode == 0, prosess.stdout + prosess.stderr
assert "1. Kjernen: uendret fra notatet (3 filer," in prosess.stdout
assert "2. diagnose: lik notatet for eksempel_domstol og norge_2025." in prosess.stdout
assert "3. forlop: lik notatet for eksempel_domstol og norge_2025." in prosess.stdout
assert "4. Full kjøring: hoppet over (--rask)." in prosess.stdout
assert "Nivå (uten full kjøring, --rask): bit for bit som notatet." in prosess.stdout
def test_kontroll_uten_numpy_gjor_det_den_kan(hjem: Path) -> None:
prosess = kjor("kontroll", python=[sys.executable, "-S"], hjem=hjem)
assert prosess.returncode == 3
assert "2. diagnose: lik notatet" in prosess.stdout
assert "3. forlop: ikke kjørt, numpy og scipy mangler." in prosess.stdout
assert "4. Full kjøring: ikke kjørt, numpy og scipy mangler." in prosess.stdout
assert "Nivå (bare det som er kjørt): bit for bit som notatet." in prosess.stdout
def endre_diagnose(kopi: Path) -> None:
"""Endre målutnyttelsen i kopiens diagnose.py fra 0,95 til 0,90."""
fil = kopi / "modell" / "kjerne" / "diagnose.py"
tekst = fil.read_text(encoding="utf-8")
assert "UPS_STAR = 0.95" in tekst
fil.write_text(tekst.replace("UPS_STAR = 0.95", "UPS_STAR = 0.90"), encoding="utf-8")
def test_endret_kjerne_i_pluginen_er_en_feil(kopi: Path, hjem: Path) -> None:
endre_diagnose(kopi)
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
assert prosess.returncode == 1
assert "diagnose.py er endret" in prosess.stdout
assert "2. diagnose: endret (se under)." in prosess.stdout
assert "- Kapasitet: (0,80 x 134,3 + 273/24) / 0,95 = 125,1 saker/mnd, det vil si 19 % mer." in prosess.stdout
assert "Nivå (uten full kjøring, --rask): endret: Kjernen, diagnose, forlop." in prosess.stdout
assert "Pluginens modell skal regne som notatet, så dette er en feil." in prosess.stdout
def test_endret_kjerne_i_arbeidskopi_er_en_opplysning(kopi: Path, hjem: Path) -> None:
endre_diagnose(kopi)
(kopi / "LES-MEG.md").write_text("Arbeidskopi\n", encoding="utf-8")
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
assert prosess.returncode == 1
assert "(arbeidskopi)" in prosess.stdout
assert "det er en opplysning, ikke en feil" in prosess.stdout
assert "+ Kapasitet: (0,80 x 134,3 + 273/24) / 0,90" in prosess.stdout
def test_manglende_fasit(kopi: Path, hjem: Path) -> None:
(kopi / "fasit" / "diagnose_norge_2025.txt").unlink()
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
assert prosess.returncode == 1
assert "norge_2025: fant ikke" in prosess.stdout
def lag_utvidelser(kopi: Path, filer: dict[str, str]) -> None:
"""Legg utvidelser i kopiens modell/utvidelser/ (og fjern dem som lå der)."""
mappe = kopi / "modell" / "utvidelser"
mappe.mkdir(exist_ok=True)
for gammel in mappe.glob("*.py"):
gammel.unlink()
for navn, innhold in filer.items():
(mappe / f"{navn}.py").write_text(innhold, encoding="utf-8")
def test_utvidelse_som_gjenskaper_kjernen(kopi: Path, hjem: Path) -> None:
lag_utvidelser(kopi, {
"eksakt": "import sim\n\ndef grensetilfelle():\n return [('p3 med trappetrinn', 0.0)]\n",
"_hjelper": "raise RuntimeError('filer som begynner med _ skal ikke kjøres')\n",
})
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
assert prosess.returncode == 0, prosess.stdout + prosess.stderr
assert "5. Utvidelser: grensetilfellene gjenskaper kjernen eksakt (eksakt)." in prosess.stdout
def test_utvidelser_som_ikke_gjenskaper_kjernen(kopi: Path, hjem: Path) -> None:
lag_utvidelser(kopi, {
"avvik": "def grensetilfelle():\n return [('p3 med trappetrinn', 1e-12), ('p6', 0.0)]\n",
"tom": "def grensetilfelle():\n return []\n",
"odelagt": "import sim\nraise RuntimeError('i stykker')\n",
})
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
assert prosess.returncode == 1, prosess.stdout + prosess.stderr
assert "5. Utvidelser: grensetilfellet gjenskaper ikke kjernen eksakt (avvik, odelagt, tom; se under)." in (
prosess.stdout)
assert "avvik: p3 med trappetrinn: avvik 1,0e-12, skal være eksakt 0" in prosess.stdout
assert "tom: grensetilfelle() ga ingen tilfeller" in prosess.stdout
assert "odelagt: grensetilfelle() kunne ikke kjøres" in prosess.stdout
assert "RuntimeError: i stykker" in prosess.stdout
assert "Nivå (uten full kjøring, --rask): endret: Utvidelser." in prosess.stdout
@pytest.mark.treg
def test_full_kontroll(hjem: Path) -> None:
prosess = kjor("kontroll", hjem=hjem)
assert prosess.returncode == 0, prosess.stdout + prosess.stderr
assert "4. Full kjøring: 22 av 22 datafiler like byte for byte med notatet; utskriften lik." in prosess.stdout
assert "\nNivå: bit for bit som notatet.\n" in prosess.stdout