# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE. """Arbeidskopi: kopier modellen til en mappe brukeren eier og kan endre. Bruk: python3 modell/kjor.py arbeidskopi python3 modell/arbeidskopi.py Kopierer modell/, eksempler/, fasit/, tests/ og docs/ (det som finnes), og pyproject.toml og LICENSE når de finnes, og skriver LES-MEG.md i mappa. Nekter hvis mappa finnes og ikke er tom. Kopierer ikke .venv, __pycache__, mellomlagre eller resultater/. Bare standardbiblioteket. """ from __future__ import annotations import json import os import shlex import shutil import subprocess import sys from pathlib import Path MODELLMAPPE = Path(__file__).resolve().parent ROT = MODELLMAPPE.parent MAPPER = ("modell", "eksempler", "fasit", "tests", "docs") FILER = ("pyproject.toml", "LICENSE") HOPP_OVER = (".venv", "venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", "resultater", "rettsstat-modell", ".DS_Store", "*.pyc", "*.pyo") NODVENDIG = ("kjor.py", "scenario.py", "kontroll.py", "arbeidskopi.py", "requirements.txt", "kjerne/sim.py", "kjerne/diagnose.py", "kjerne/forlop.py", "kjerne/KILDE.json", "utvidelser/__init__.py") DOKUMENTER = ( ("scenarioformat.md", "scenariofila: alle felt, sveip og utdata"), ("utvidelser.md", "hvordan du legger til en mekanisme som utvidelse, med mal"), ("modellkart.md", "fra ligning i notatet til funksjon og parameter i koden"), ("inndata.md", "inndatafila til diagnose og forlop"), ("forbehold.md", "hva modellen kan og ikke kan brukes til"), ) def paa_windows(*, windows: bool | None = None) -> bool: """Sann på Windows (eller når testene ber om det).""" return os.name == "nt" if windows is None else windows def python_navn(*, windows: bool | None = None) -> str: """Tolken heter python på Windows og python3 ellers.""" return "python" if paa_windows(windows=windows) else "python3" def sitert(sti: Path | str, *, windows: bool | None = None) -> str: """En sti slik den må skrives på kommandolinja: cmd/PowerShell på Windows, POSIX-skall ellers.""" return subprocess.list2cmdline([str(sti)]) if paa_windows(windows=windows) else shlex.quote(str(sti)) BRUK = f"Bruk: {python_navn()} modell/kjor.py arbeidskopi " class KopiFeil(Exception): """Feil i bruken eller i kildemappa; gir exit-kode 2.""" def sjekk_kilde() -> None: """Stopp hvis modell/ ikke er komplett.""" mangler = [n for n in NODVENDIG if not (MODELLMAPPE / n).is_file()] if mangler: melding = (f"Modellen i {MODELLMAPPE} er ikke komplett, så arbeidskopien ville ikke virket. Mangler: " + ", ".join(f"modell/{n}" for n in mangler) + ".") raise KopiFeil(melding) def sjekk_mal(mal: Path) -> None: """Mappa må være ny eller tom, og ikke ligge inne i det som kopieres.""" if mal.exists() and not mal.is_dir(): melding = f"{mal} finnes og er en fil, ikke en mappe." raise KopiFeil(melding) if mal.is_dir() and any(mal.iterdir()): melding = (f"Mappa {mal} finnes og er ikke tom. Velg en ny mappe; arbeidskopi skriver aldri over noe " "som finnes.") raise KopiFeil(melding) rot = ROT.resolve() if mal == rot or rot in mal.parents: melding = (f"{mal} ligger inne i {rot}, modellen som skal kopieres. Velg en mappe utenfor, for eksempel " "i arbeidsmappa di.") raise KopiFeil(melding) def kildeversjon() -> str: """Commit i notatets repo som kjernen er kopiert fra (fra KILDE.json).""" try: data = json.loads((MODELLMAPPE / "kjerne" / "KILDE.json").read_text(encoding="utf-8")) except (OSError, ValueError): return "ukjent" if not isinstance(data, dict): return "ukjent" commit = str(data.get("commit", "ukjent")) repo = str(data.get("repo", data.get("url", ""))) return f"{repo} @ {commit[:7]}" if repo else commit[:7] def les_meg(mal: Path, docs: list[str], *, windows: bool | None = None) -> str: """Innholdet i LES-MEG.md.""" py = python_navn(windows=windows) kjor = f"{py} {sitert(mal / 'modell' / 'kjor.py', windows=windows)}" eks = mal / "eksempler" def q(sti: Path) -> str: return sitert(sti, windows=windows) linjer = [ "# Arbeidskopi av modellen bak «Den dynamiske rettsstaten»", "", (f"Denne mappa er din egen kopi av modellen i pluginen dynamisk-rettsstat. Kjernen i `modell/kjerne/` er " f"en uendret kopi av notatets kode ({kildeversjon()}); sjekksummene står i `modell/kjerne/KILDE.json`."), "", "## Hva du kan endre", "", "Alt i mappa kan endres, også kjernen. Tre veier, fra minst til mest inngripende:", "", "1. Andre forutsetninger: skriv en scenariofil (se `docs/scenarioformat.md`). Ingen kode endres.", ("2. En ny mekanisme ved siden av kjernen: legg den i `modell/utvidelser/` (se `docs/utvidelser.md`). " "Regelen er den samme som i notatet: utvidelsen må ha et grensetilfelle som gjenskaper kjernen eksakt."), ("3. Endre kjernen selv (`modell/kjerne/sim.py`). Kontrollen under sier da hvilke av notatets resultater " "som flytter seg, og hvor mye."), "", "## Slik kjører du kopien", "", "Kommandoene bruker koden i denne mappa:", "", "```", f"{kjor} scenario {q(eks / 'scenarioer' / 'avviklingstid_koordinert.json')}", f"{kjor} diagnose {q(eks / 'eksempel_domstol.json')}", f"{kjor} forlop {q(eks / 'eksempel_domstol.json')}", "```", "", "Fra pluginen kan du peke på kopien med `--modell`:", f"`{py} /modell/kjor.py --modell " + q(mal) + " scenario `. Ferdighetene i " "pluginen bruker kopien selv når mappa du jobber i, har en `rettsstat-modell/`.", "", ("Scenario, forløp og kontroll trenger numpy og scipy. Mangler de, sier kommandoen fra; " f"`{kjor} miljo --opprett` installerer dem i en egen mappe."), "", "## Slik kontrollerer du en endring", "", "```", f"{kjor} kontroll # alt, om lag 1-2 minutter", f"{kjor} kontroll --rask # uten den fulle kjøringen av sim.py", "```", "", "Kontrollen sammenligner med notatets resultater i `fasit/` og ender med ett av tre nivåer:", "", "- «bit for bit som notatet»: endringen flytter ingen av notatets resultater.", ("- «innenfor toleranse»: tallene avviker med høyst én enhet i siste viste siffer, som med andre versjoner " "av numpy og scipy."), ("- «endret: ...»: noe har flyttet seg. I en arbeidskopi er det en opplysning, ikke en feil. Detaljene " "sier hvilke datafiler og utskrifter som er endret, og største avvik per kolonne."), "", "Kontrollen kjører også `grensetilfelle()` i hver utvidelse; hvert avvik skal være eksakt 0.", f"Testene kjøres med `{py} -m pytest` i mappa (krever pytest; de trege med `-m treg`).", "", "## Dokumentasjon", "", ] if docs: linjer.extend(f"- `docs/{navn}`: {hva}" for navn, hva in DOKUMENTER if navn in docs) linjer.extend(f"- `docs/{navn}`" for navn in docs if navn not in {n for n, _ in DOKUMENTER}) else: linjer.append("- (docs/ fantes ikke da kopien ble laget)") linjer += [ "", "## Sende en forbedring tilbake", "", ("Se `BIDRA.md` i pluginen. Kort: lag en patch av endringene (for eksempel med `git diff` hvis du har lagt " "mappa i git) og send den på e-post med en forklaring og utskriften fra kontrollen."), "", "Alt i pluginen er under MIT-lisensen (se `LICENSE`).", "", ] return "\n".join(linjer) def kopier(mal: Path) -> int: """Lag arbeidskopien. Returnerer antall filer.""" sjekk_kilde() sjekk_mal(mal) mal.mkdir(parents=True, exist_ok=True) ignorer = shutil.ignore_patterns(*HOPP_OVER) for navn in MAPPER: kilde = ROT / navn if kilde.is_dir(): shutil.copytree(kilde, mal / navn, ignore=ignorer) for navn in FILER: if (ROT / navn).is_file(): shutil.copy2(ROT / navn, mal / navn) docs = sorted(p.name for p in (mal / "docs").glob("*.md")) if (mal / "docs").is_dir() else [] (mal / "LES-MEG.md").write_text(les_meg(mal, docs), encoding="utf-8") return sum(1 for p in mal.rglob("*") if p.is_file()) def main(argv: list[str]) -> int: """Kommandoen arbeidskopi. Exit-kode 0 ok, 2 feil i bruken.""" if argv and argv[0] in {"-h", "--hjelp", "--help"}: sys.stdout.write(BRUK + "\n") return 0 if len(argv) != 1 or not argv[0].strip(): sys.stderr.write(BRUK + "\n") return 2 mal = Path(argv[0]).expanduser().resolve() try: antall = kopier(mal) except KopiFeil as e: sys.stderr.write(f"{e}\n") return 2 except OSError as e: sys.stderr.write(f"Kunne ikke lage arbeidskopien i {mal}: {e}\n") return 2 kjor = f"{python_navn()} {sitert(mal / 'modell' / 'kjor.py')}" sys.stdout.write(f"Arbeidskopien er laget i {mal} ({antall} filer).\n" f"Les {mal / 'LES-MEG.md'} først. Kjør den med:\n" f" {kjor} scenario \n" f"og kontroller endringer med:\n" f" {kjor} kontroll\n") return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))