fix: findings from the Cowork test, released as v0.1.1

The operator tested v0.1.0 in Cowork on 2026-11-11. README now says what
was measured there and what was not.

- miljo: no longer claims that kontroll compares "with tolerance, not bit
  for bit" when numpy or scipy differ from the note's versions. kontroll
  always compares byte for byte first, and numpy 2.5.3 gave bit for bit.
- kontroll --alt: show every differing line. The cut-off line now names
  the option instead of only counting the lines left out.
- skill simuler: the question before a run must be answerable with yes or
  no. In the test it became a choice between variants and the run stalled.
- skill forbedre: offer the working copy as a zip when the user cannot
  open the folder it was made in, as in Cowork.

263 tests pass; full kontroll: bit for bit with the note.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-11-11 20:24:58 +01:00
commit 13f6c28dc0
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
11 changed files with 126 additions and 29 deletions

View file

@ -1,7 +1,7 @@
{
"name": "dynamisk-rettsstat",
"displayName": "Den dynamiske rettsstaten",
"version": "0.1.0",
"version": "0.1.1",
"description": "Plugin for Claude som kjører modellen i Den dynamiske rettsstaten på egne tall: finn leddet som binder, følg tiltaket over tid, simuler egne scenarioer og endre modellen.",
"author": {
"name": "Kjell Tore Guttormsen"

View file

@ -2,6 +2,24 @@
All notable changes to this project will be documented in this file.
## 0.1.1 — 2026-11-11
Etter forfatterens prøve av 0.1.0 i Cowork.
- README: hva som er målt i Cowork, og hva som ikke er det. Zip-fila fra Forgejo lastes opp som den er, de fire
ferdighetene virker, diagnosen og forløpet gir notatets tall, og kontrollen gir «bit for bit som notatet» også med
Python 3.13.18, numpy 2.5.3 og scipy 1.18.1 på Linux. Ikke målt der: en egen simulering med figur, installasjon av
numpy og scipy, og om en arbeidskopi blir liggende til neste økt.
- Rettet: `miljo` sa at kontrollen «sammenligner med toleranse, ikke bit for bit» når numpy eller scipy ikke er
notatets versjoner. Det stemte ikke: kontrollen sammenligner alltid byte for byte først, og i Cowork ga numpy 2.5.3
bit for bit. Meldingen sier nå at tallene kan avvike i siste siffer, og at kontrollen sier hva de gjør.
- Nytt: `kontroll --alt` viser alle linjene som er ulike. Før ble alt etter de 40 første per sammenligning bare
talt («… linjer til»), uten noen måte å få dem fram på. Linja sier nå hvordan.
- Ferdigheten `simuler`: spørsmålet før kjøringen skal kunne besvares med ja eller nei. I prøven ble det et valg
mellom varianter, og kjøringen ble stående.
- Ferdigheten `forbedre`: ligger arbeidskopien et sted brukeren ikke åpner selv, som i Cowork, sier Claude hvor, og
tilbyr den som zip-fil sammen med kontrollutskriften.
## 0.1.0 — 2026-11-11
Første versjon.

View file

@ -43,7 +43,10 @@ justice chain (mostly non-programmers) and researchers. The note and its code li
- Skill frontmatter has only `name` and `description` (other keys make claude.ai reject an uploaded skill).
- `${CLAUDE_PLUGIN_ROOT}` and `${CLAUDE_PLUGIN_DATA}` are substituted in skill text, but they are not environment
variables in Bash. Skills write them literally into the command; `kjor.py` treats an empty `--data` or one containing
`${` as not given and falls back to `~/.dynamisk-rettsstat`.
`${` as not given and falls back to `~/.dynamisk-rettsstat`. Both were substituted in Cowork too (operator's test
2026-11-11: the plugin under `/root/.claude/plugins/synced/<id>/dynamisk-rettsstat`, data under
`/root/.claude/plugins/data/dynamisk-rettsstat-synced`). Cowork ran the commands in a Linux machine where numpy and
scipy were already installed in the system Python, so the venv path is untested there.
- Skills never invent numbers, show the numbers as understood before computing, divide yearly figures by 12 and say
so, relay script numbers verbatim, carry the caveats, and never install anything without asking. A
`rettsstat-modell/` in the user's folder is used only after asking, and only after `kontroll --rask` on it has been
@ -64,7 +67,8 @@ justice chain (mostly non-programmers) and researchers. The note and its code li
- Reproduction check against the note: `.venv/bin/python modell/kjor.py kontroll` (full run; about 1.5 minutes on the
development machine, measured 2026-11-11) or `.venv/bin/python modell/kjor.py kontroll --rask` (skips the full run of
`sim.py`). Six parts: core and `fasit/` against the note's checksums, `diagnose`, `forlop`, full run, extensions'
limit cases, scenario runner against `fasit/data`.
limit cases, scenario runner against `fasit/data`. `--alt` shows every differing line instead of the first 40 per
comparison.
- Try the skills' commands: `python3 modell/kjor.py diagnose eksempler/eksempel_domstol.json`,
`python3 modell/kjor.py forlop eksempler/eksempel_domstol.json`,
`python3 modell/kjor.py scenario eksempler/scenarioer/sjokkstorrelse_kjede.json --ut <scratch folder>` (`forlop` and

View file

@ -18,16 +18,18 @@ claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-mar
claude plugin install dynamisk-rettsstat@ktg-plugin-marketplace
```
I Claude Cowork: last ned repositoryet som zip fra Forgejo-siden (`https://git.fromaitochitta.com/open/dynamisk-rettsstat`)
og last det opp som plugin.
I Claude Cowork: last ned zip-fila for nyeste utgivelse fra
[utgivelsessiden](https://git.fromaitochitta.com/open/dynamisk-rettsstat/releases) og last den opp som plugin. Cowork
tar zip-fila slik den er.
Det som trengs:
- **Python 3.** Flaskehalsdiagnosen trenger ikke noe mer.
- **numpy og scipy** for forløpet over tid, simuleringene og kontrollen. Mangler de, spør Claude før noe installeres,
og installerer dem da fra PyPI i en egen mappe for pluginen (krever internett). Ingenting installeres uten at du
sier ja.
sier ja. I Cowork fantes begge fra før da pluginen ble prøvd, så ingenting ble installert.
Status: prøvd i Claude Code. Ikke prøvd i Cowork ennå.
Status: prøvd i Claude Code og i Cowork. Hva som er målt i Cowork, og hva som ikke er det, står under «Prøvd i
Cowork» lenger ned.
## Hva du kan gjøre med den
@ -125,9 +127,10 @@ Notatet, simuleringskoden og PDF-ene ligger i
låst med sha256 i `modell/kjerne/KILDE.json`. En test feiler hvis en av dem endres.
- Kontrollen måler kjernen og fasiten mot notatets egne sjekksummer, kjører hele notatets simulering og
sammenligner med notatets 22 datafiler og utskriftene, og kjører notatets scenariofiler gjennom scenariokjøreren.
Målt 2026-11-11 med Python 3.14.0, numpy 2.5.4 og scipy 1.18.1: alle 22 datafilene er like byte for byte, og 14 av
14 scenariofiler gjenskaper notatets datafiler byte for byte. Med andre versjoner av numpy og scipy sier kontrollen
om tallene er like innenfor én enhet i siste siffer.
Målt 2026-11-11 med Python 3.14.0, numpy 2.5.4 og scipy 1.18.1 (macOS): alle 22 datafilene er like byte for byte,
og 14 av 14 scenariofiler gjenskaper notatets datafiler byte for byte. Samme dag i Cowork med Python 3.13.18,
numpy 2.5.3 og scipy 1.18.1 (Linux): samme resultat. Med andre versjoner av numpy og scipy sier kontrollen om
tallene er like bit for bit eller innenfor én enhet i siste siffer.
- Scenariokjøreren kaller kjernen uendret. Scenariofilene i `eksempler/scenarioer/notatet/` gjenskaper notatets
datafiler, og testene sammenligner dem med fasiten.
@ -142,8 +145,10 @@ python3 -m venv .venv
.venv/bin/python -m pytest
```
Kontrollen skal ende med `Nivå: bit for bit som notatet.` når Python, numpy og scipy er versjonene over.
`pip install --group dev` trenger pip 25.1 eller nyere.
Kontrollen skal ende med `Nivå: bit for bit som notatet.` når Python, numpy og scipy er versjonene over. Med andre
versjoner kan den ende med `Nivå: innenfor toleranse`; det er ikke sett i noen av målingene. `kontroll --alt` viser
alle linjene som er ulike, ikke bare de 40 første per sammenligning. `pip install --group dev` trenger pip 25.1
eller nyere.
## Forbedre modellen selv
@ -164,6 +169,31 @@ skal kunne endre den:
Forskere som bruker git, kan også klone dette repositoryet og starte Claude Code med `claude --plugin-dir .` i mappa.
Slik sender du en forbedring tilbake: [BIDRA.md](BIDRA.md).
## Prøvd i Cowork
Forfatteren prøvde v0.1.0 i Cowork i Claude-skrivebordsappen 2026-11-11. Dette ble målt:
- **Installasjon.** Zip-fila for v0.1.0 fra Forgejo ble lastet opp uten advarsler, og de fire ferdighetene var på
plass.
- **Diagnosen** ga tallene som står i dette dokumentet, på eksempeltallene (1,28 og 19 %), på de norske tallene
(1,09 / 1,04 / 0,88 / 1,09) og på egne tall oppgitt per år. Claude delte årstallene på 12, viste tallene slik de
var forstått, og spurte før regningen.
- **Forløpet** ga 7,2 måneder, 19 % nå og 12 % om 12 måneder, også i en ny økt.
- **Kontrollen** endte med `Nivå: bit for bit som notatet.`: 22 av 22 datafiler og 14 av 14 scenariofiler.
- **Miljøet.** Cowork kjørte kommandoene i en Linux-maskin (Ubuntu 24.04) der Python 3.13.18, numpy 2.5.3 og scipy
1.18.1 fantes fra før. Pluginen installerte derfor ingenting.
- **Arbeidskopi.** En kopi med frafallet fra domstolskøen satt til 0,02 ga `Nivå: endret`, med 11 av 22 datafiler
flyttet. Kopien ble laget i Linux-maskinen, og Claude ga den tilbake som en zip-fil sammen med kontrollutskriften.
Ikke målt i Cowork:
- En egen simulering fra «hva om …» til tabell og figur. Prøven stoppet ved spørsmålet Claude stilte før kjøringen.
Scenariokjøreren selv ble kjørt av kontrollen.
- Installasjon av numpy og scipy fra pluginen, siden de fantes fra før.
- Om en arbeidskopi blir liggende til neste økt.
Windows er ikke prøvd, verken i Cowork eller i Claude Code.
## Status og begrensninger
- Modellen er ikke kalibrert på norske data. Parameterne er stiliserte, og simuleringene illustrerer mekanismer. De
@ -176,6 +206,8 @@ Slik sender du en forbedring tilbake: [BIDRA.md](BIDRA.md).
det ikke. En liten del av det kjedescenarioene viser, er denne driften, og sammendraget viser hvor langt den har kommet fram
til sjokket.
- Kostnader og gevinster er ikke regnet i kroner.
- Svarene er tekst og tabeller, med tilbud om en figur etter en simulering. Pluginen bruker ikke det Cowork ellers
kan av visuelle svar. Det er en opplagt videreutvikling for den som vil bygge videre på den.
- Diagnosen regner på tallene slik de er oppgitt, og kapasitetsbehovet er et grovt anslag.
Alle forbeholdene, for modellen, diagnosen, forløpet og de norske tallene: [docs/forbehold.md](docs/forbehold.md).

View file

@ -62,7 +62,7 @@ Kommandoer:
forlop <fil> ["ledd"] stabilitetssjekk og forløp måned for måned (numpy og scipy)
scenario <fil> [--ut <mappe>] kjør en scenariofil (numpy og scipy)
notatet [--ut <mappe>] kjør notatets simuleringer uendret, om lag 1-2 min (numpy og scipy)
kontroll [--rask] kontroller at modellen regner som notatet
kontroll [--rask] [--alt] kontroller at modellen regner som notatet (--alt: vis alle ulike linjer)
miljo [--opprett] vis Python, numpy og scipy; --opprett installerer dem i en egen mappe
arbeidskopi <mappe> lag en kopi av modellen som du kan endre
@ -568,8 +568,8 @@ def rapport_miljo(valg: Valg) -> None:
if samme_som_notatet(regner, laast):
print("Versjonene av numpy og scipy er de samme som notatets, så kontroll kan gi bit for bit.")
else:
print("Versjonene av numpy og scipy er ikke de samme som notatets: kontroll sammenligner da "
"med toleranse, ikke bit for bit.")
print("Versjonene av numpy og scipy er ikke de samme som notatets. Tallene kan da avvike i siste "
"siffer; kontroll sier om de er like bit for bit eller innenfor toleranse.")
def kjor_prosess(kommando: list[str]) -> int:
@ -608,8 +608,8 @@ def opprett_miljo(valg: Valg) -> int:
print()
rapport_miljo(valg)
if toleranse:
print("\nNumpy og scipy er ikke notatets låste versjoner. Resultatene kontrolleres da med toleranse, "
"ikke bit for bit.")
print("\nNotatets låste versjoner av numpy og scipy lot seg ikke installere, så nyere versjoner er "
"installert. Kjør kontroll for å se om tallene er like bit for bit eller innenfor toleranse.")
return OK

View file

@ -2,8 +2,11 @@
"""Gjenskapingskontroll: sjekk at modellen i denne mappa regner som notatet.
Bruk:
python3 kjor.py kontroll [--rask]
python3 kontroll.py [--rask] (med en Python som har numpy og scipy)
python3 kjor.py kontroll [--rask] [--alt]
python3 kontroll.py [--rask] [--alt] (med en Python som har numpy og scipy)
--alt viser alle linjene som er ulike. Uten den vises de 40 første per sammenligning, og
resten telles.
Kontrollene, hver med én linje dom og detaljer bare ved avvik:
1. Kjernen og fasit: sha256 for filene i kjerne/ og fasit/ mot notatets verdier, som står som
@ -118,7 +121,8 @@ SCENARIO_TREGE = ("mpc11",)
LIK, TOLERANSE, ENDRET, IKKE_KJORT = "lik", "toleranse", "endret", "ikke kjørt"
RANG = {LIK: 0, TOLERANSE: 1, IKKE_KJORT: 2, ENDRET: 3}
MAKS_LINJER = 40 # linjer med forskjeller som vises per sammenligning
MAKS_LINJER = 40 # linjer med forskjeller som vises per sammenligning (alle med --alt)
VISNING = {"alt": False} # settes av --alt i main()
SLINGRING = 1e-9 # relativ slingring når avviket sammenlignes med én enhet i siste siffer
UTVIDELSER = "Utvidelser" # merkelappen for kontroll 5
@ -220,8 +224,9 @@ def vis_tall(x: float) -> str:
def forskjeller(fasit: list[str], naa: list[str]) -> list[str]:
"""Linjene som er ulike, som en kort diff (notatet / nå)."""
linjer = list(difflib.unified_diff(fasit, naa, "notatet", "nå", lineterm="", n=0))
if len(linjer) > MAKS_LINJER:
linjer = [*linjer[:MAKS_LINJER], f"... ({len(linjer) - MAKS_LINJER} linjer til)"]
if len(linjer) > MAKS_LINJER and not VISNING["alt"]:
linjer = [*linjer[:MAKS_LINJER],
f"... ({len(linjer) - MAKS_LINJER} linjer til; «kontroll --alt» viser alle)"]
return linjer
@ -594,10 +599,11 @@ def niva(dommer: list[Dom], *, rask: bool, arbeidskopi: bool) -> list[str]:
def main(argv: list[str]) -> int:
"""Kjør kontrollene og skriv dommen. Returnerer exit-koden."""
if argv not in ([], ["--rask"]):
print("Bruk: kjor.py kontroll [--rask]", file=sys.stderr)
if len(set(argv)) != len(argv) or not set(argv) <= {"--rask", "--alt"}:
print("Bruk: kjor.py kontroll [--rask] [--alt]", file=sys.stderr)
return 2
rask = argv == ["--rask"]
rask = "--rask" in argv
VISNING["alt"] = "--alt" in argv
arbeidskopi = (ROT / ARBEIDSKOPI_MERKE).is_file()
regnemiljo = har_numpy_scipy()
print(f"Kontroll av modellen i {MODELL}" + (" (arbeidskopi)" if arbeidskopi else "") + "\n", flush=True)

View file

@ -62,6 +62,8 @@ python3 "${CLAUDE_PLUGIN_ROOT}/modell/kjor.py" --data "${CLAUDE_PLUGIN_DATA}" ar
`rettsstat-modell/`, og sier hvilket nivå kontrollen gir den.
- `modell/kontroll.py` i kopien har notatets sjekksummer som faste verdier; det er dem kontrollen måler mot. Endre
den ikke: da sier kontrollen ikke lenger noe om notatet.
- Ligger kopien et sted brukeren ikke åpner selv (i Cowork kjører kommandoene i øktas egen Linux-maskin, og kopien
havner der), si hvor den ligger, og tilby å gi den som en zip-fil sammen med hele utskriften fra kontrollen.
**Forskere som bruker git** kan i stedet klone pluginens repository (`https://git.fromaitochitta.com/open/dynamisk-rettsstat.git`),
endre der og starte Claude Code med pluginen fra klonen: `claude --plugin-dir .` i mappa. Da bruker ferdighetene
@ -131,6 +133,8 @@ python3 "${CLAUDE_PLUGIN_ROOT}/modell/kjor.py" --data "${CLAUDE_PLUGIN_DATA}" --
- Den fulle kontrollen kjører hele notatets simulering og tar et par minutter. Mens du prøver deg fram, kan du legge
til `--rask` (hopper over den fulle kjøringen av `sim.py`); kjør den fulle før du sier hva endringen gjør.
- Utskriften viser de 40 første linjene som er ulike i hver sammenligning, og teller resten («… linjer til»). Vil
brukeren se alt som har flyttet seg, eller skal utskriften sendes med en forbedring, legg til `--alt`.
- Kontrollen måler mot notatets egne sjekksummer, som står fast i `modell/kontroll.py`, ikke mot filene i kopien.
Den har seks deler, nummerert slik i utskriften:
1. kjernen (`sim.py`, `diagnose.py`, `forlop.py`) og fasiten (27 filer) mot notatets sjekksummer;

View file

@ -83,7 +83,7 @@ Exit-koder:
python3 "${CLAUDE_PLUGIN_ROOT}/modell/kjor.py" --data "${CLAUDE_PLUGIN_DATA}" miljo --opprett
```
Gjengi det kommandoen sier (blant annet om resultatene da kontrolleres bit for bit eller med toleranse), og kjør
Gjengi det kommandoen sier (blant annet om versjonene av numpy og scipy er notatets), og kjør
forløpet på nytt. Feiler installasjonen, gjengi meldingen og si at diagnosen fortsatt kan brukes uten tilleggene.
- **2**: skriptet har stoppet med en melding. Les den:
- *Feil i inndata*, *Fant ikke fila*, *ikke gyldig JSON* eller *Fant ikke leddet*: gjengi meldingen med vanlige ord,

View file

@ -91,7 +91,9 @@ stopper kjøringen uansett valg når politiets effektivitet, tilliten, innsatsen
Si det rett ut: i den modellen er det en endring i kjernen, og det er ferdigheten `forbedre`. Den koblede modellen
uten beslutningsregler (`simulate_chain`) kan ofte svare på det samme spørsmålet; tilby den.
Spør: «Skal jeg kjøre dette?» Kjør ikke før brukeren har sagt ja.
Spør: «Skal jeg kjøre dette?» Kjør ikke før brukeren har sagt ja. Spørsmålet står alene til slutt og skal kunne
besvares med ja eller nei. Legg ikke et valg mellom varianter inn i det («de tre, eller alle fire?»): da er «ja» ikke
lenger et svar, og kjøringen blir stående. Forslag til flere kjøringer kommer etter svaret, i trinn 7.
## 3. Skriv scenariofila
@ -136,7 +138,7 @@ Exit-koder:
python3 "${CLAUDE_PLUGIN_ROOT}/modell/kjor.py" --data "${CLAUDE_PLUGIN_DATA}" miljo --opprett
```
Gjengi det kommandoen sier (blant annet om resultatene da kontrolleres bit for bit eller med toleranse), og kjør på
Gjengi det kommandoen sier (blant annet om versjonene av numpy og scipy er notatets), og kjør på
nytt. Feiler installasjonen, gjengi meldingen og si at diagnosen fortsatt kan brukes uten tilleggene.
- **2**: skriptet har stoppet med en melding, og ingenting er skrevet. Les den:
- *Feil i scenariofila … startlikevekt: startpunktet er ikke en likevekt …*: en grunnparameter er endret. Gjengi

View file

@ -345,7 +345,12 @@ def test_opprett_faller_tilbake_og_sier_toleranse(
monkeypatch.setattr(kjor_modul, "rapport_miljo", lambda _valg: None)
assert kjor_modul.main(["--data", str(tmp_path), "miljo", "--opprett"]) == 0
assert [k[-2:] for k in kommandoer] == [["-r", str(MODELL / "requirements.txt")], ["numpy>=2.0", "scipy"]]
assert "kontrolleres da med toleranse, ikke bit for bit" in capsys.readouterr().out
ut = capsys.readouterr().out
assert "lot seg ikke installere, så nyere versjoner er installert" in ut
assert "bit for bit eller innenfor toleranse" in ut
# Measured in Cowork 2026-11-11: numpy 2.5.3 (not the pinned 2.5.4) still gave bit for bit,
# so the message must not promise a tolerance comparison.
assert "ikke bit for bit" not in ut
def test_opprett_som_feiler_gir_kode_3(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:

View file

@ -45,6 +45,20 @@ def test_tekst_endret(fasit: str, naa: str) -> None:
assert resultat.detaljer
def test_mange_forskjeller_kuttes_og_alt_viser_resten(monkeypatch: pytest.MonkeyPatch) -> None:
"""Cowork-prøven 2026-11-11: 274 linjer ble kuttet uten at brukeren kunne få dem fram."""
fasit = [f"linje {i}" for i in range(60)]
naa = [f"rad {i}" for i in range(60)]
kort = kontroll.forskjeller(fasit, naa)
assert len(kort) == kontroll.MAKS_LINJER + 1
assert kort[-1].endswith("linjer til; «kontroll --alt» viser alle)")
monkeypatch.setattr(kontroll, "VISNING", {"alt": True})
alle = kontroll.forskjeller(fasit, naa)
assert "-linje 59" in alle
assert "+rad 59" in alle
assert not any("linjer til" in linje for linje in alle)
DAT = "t K Q\n0.00000 1.00000 0.29100\n0.50000 1.10000 0.30000\n"
@ -183,6 +197,18 @@ def test_manglende_fasit(lett_kopi: Path, hjem: Path) -> None:
assert "norge_2025: fant ikke fasit/diagnose_norge_2025.txt" in prosess.stdout
def test_kontroll_avviser_ukjent_valg(hjem: Path) -> None:
prosess = kjor("kontroll", "--alle", hjem=hjem)
assert prosess.returncode == 2
assert "Bruk: kjor.py kontroll [--rask] [--alt]" in prosess.stderr
def test_kontroll_med_alt_gir_samme_dom(lett_kopi: Path, hjem: Path) -> None:
prosess = kjor("--modell", str(lett_kopi), "kontroll", "--rask", "--alt", hjem=hjem)
assert prosess.returncode == 0, prosess.stdout + prosess.stderr
assert "Nivå (uten full kjøring, --rask): bit for bit som notatet." in prosess.stdout
def test_diagnose_krever_eksakt_likhet(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Funn 10: én enhet i siste siffer i diagnosen er endret, ikke innenfor toleranse."""
fasit = tmp_path / "fasit"