docs(mandate): how a domain expert commissions a run — and one honesty fix the run itself exposed

`docs/bestille-en-kjoring.md` is the commissioning half of the expert-facing pair
(`ekspert-svar.md` is the judging half): the mandate file field by field, how to
run it, and — separated deliberately — what a commission does NOT do. It directs
what is evaluated, never what is approved.

Registered in _LIVE_DOCS, so it cannot silently fall behind the code.

The example output in it is COPIED FROM A REAL RUN, not composed, and running
that run is what found the defect fixed here: three approaches against the same
cost line each validated at 30000 NOK, and the settlement printed
"Validated total: 90000 NOK". Commissioned approaches are ALTERNATIVES — they
usually attack the same line — so summing them reports money the project cannot
realise. A domain expert reading that total would reasonably believe the run
found 90k.

The settlement now reports how many approaches held and which one the run
carries: a selection, not an arithmetic claim. That also removes the last money
addition from this module, which is the right place for it not to be.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULCqjLF61rehj5cZmdUoR3
This commit is contained in:
Kjell Tore Guttormsen 2026-08-05 16:31:27 +02:00
commit 30bcdd3544
5 changed files with 174 additions and 10 deletions

View file

@ -392,6 +392,11 @@ only an expert has:
## Docs
- [Bestille en kjøring](docs/bestille-en-kjoring.md) *(norsk)* — for the domain expert who
COMMISSIONS a run: naming the approaches the run must evaluate (and/or asking the system for its
own), stating what the run is for, and reading the announcement it prints before spending
anything and the settlement it prints afterwards. The commission directs what is *evaluated*,
never what is *approved*.
- [Ekspert-svar](docs/ekspert-svar.md) *(norsk)* — for the domain expert who has to deliver the
verdict: where a judgement goes, what an approval, an approval-with-correction and a rejection
actually look like, and paste-ready examples of each. Marked throughout as AI-authored and not

132
docs/bestille-en-kjoring.md Normal file
View file

@ -0,0 +1,132 @@
# Bestille en kjøring
Dette dokumentet er for deg som **bestiller** en kjøring — fagpersonen som vet hvilke tilnærminger
det er verdt å prøve på et prosjekt, og som vil vite nøyaktig hva systemet skal gjøre før det gjør
det. Søsterdokumentet [ekspert-svar.md](ekspert-svar.md) handler om det motsatte enden: å avgi dommen
etterpå.
> **Ærlighetsmerking.** Bestillingen styrer hva som **vurderes**. Den styrer aldri hva som
> **godkjennes**. Den deterministiske validatoren gjelder uendret for en tilnærming du selv har bedt
> om — ber du om noe tallene ikke bærer, blir det avvist, og avvisningen kommer tilbake til deg med
> begrunnelsen.
## Oppdragsfila
En bestilling er én JSON-fil. Du gir den til kjøringen med `--mandate`.
```json
{
"objective": "Kutt energikostnad i BYGG-KONTOR-NORD uten ombygging, med tiltak som kan gjennomføres i 2026.",
"approaches": [
{
"id": "led-retrofit",
"label": "LED-retrofit av kontorbelysning",
"description": "Drift mener armaturene er originale og har gått langt forbi oppgitt levetid."
},
{
"id": "driftsavtale",
"label": "Reforhandling av driftsavtale",
"description": "Avtalen er ikke justert siden 2019 og dekker areal vi ikke lenger disponerer."
}
],
"allow_own_proposals": true,
"success_criteria": "Minst ett tiltak som passerer validatoren og som drift kan stå inne for."
}
```
| Felt | Hva det gjør |
|---|---|
| `objective` | Hva kjøringen er til for, i klartekst. **Påkrevd** — en kjøring uten uttalt hensikt er nettopp det denne fila finnes for å hindre. |
| `approaches` | Tilnærmingene du vil ha vurdert. Hver får sin egen vurdering og sin egen linje i oppgjøret. |
| `allow_own_proposals` | `true` (standard): systemet foreslår også sitt eget, i tillegg til dine. `false`: kun dine. |
| `success_criteria` | Hva du vil regne som vellykket. Gjengis ordrett i kunngjøringen; ingenting tolker den maskinelt. |
**Om `id`:** den er nøkkelen tilnærmingen rapporteres under. To tilnærminger kan ikke dele `id`
(da ville de kollapset til én linje i oppgjøret), og `own-proposal` er reservert for systemets eget
forslag.
**Om `description`:** den mates ordrett inn til modellen. Det er her fagkunnskapen din faktisk
gjør en forskjell — *hvorfor* du tror tilnærmingen er verdt å prøve er nøyaktig det systemet ikke
kan lese seg til av kostnadstallene.
**Enten tilnærminger eller egne forslag.** En fil med tom `approaches` **og**
`allow_own_proposals: false` blir avvist: da har kjøringen ingenting å gjøre.
## Å kjøre den
```bash
uv run python -m portfolio_optimiser.run BYGG-KONTOR-NORD \
--docs-dir shared/examples/bygg-energi-mikro \
--bundle-dir shared/examples/bygg-energi-mikro \
--mandate oppdrag.json \
--scripted-replies replies.json
```
`--mandate` virker i begge moduser. I porteføljemodus (`--portfolio`) gjelder bestillingen **hvert**
prosjekt i passet, og hvert prosjekt gjør opp for seg.
**Vil du se hva kjøringen ville gjort, uten å bruke penger?** Legg til `--live-dry-run`. Da skrives
kunngjøringen, og kjøringen stopper før første modellkall.
**Fila leses strengt.** Mangler den, eller er den feilformet, blir kjøringen **nektet** (rc 1) — den
går aldri videre på en halvlest bestilling. Det er med vilje: et oppgjør som beskriver arbeid ingen
har bestilt, er verre enn ingen bestilling.
## Hva du får: kunngjøringen
Før noe koster penger, skriver kjøringen hva den skal gjøre:
```
Run mandate for BYGG-KONTOR-NORD
Objective: Kutt energikostnad i BYGG-KONTOR-NORD uten ombygging, ...
Evaluates: 2 expert-proposed approach(es) + the system's own proposals
1. led-retrofit — LED-retrofit av kontorbelysning
2. driftsavtale — Reforhandling av driftsavtale
Stops at: 3 rounds / 100000 tokens
Contacts: no external services
Success: Minst ett tiltak som passerer validatoren ...
```
Kjører du med `--dimension-config`, kommer det en `Scoped to:`-linje til. Linjer for ting kjøringen
ikke har — avgrensning, tallmål — utelates i stedet for å stå tomme: en tom linje ville lest som en
beslutning ingen har tatt.
`Contacts` er egress-erklæringen: en kjøring når aldri en ekstern tjeneste den ikke har navngitt
her. `Stops at` er takene — systemet kjører aldri en ubegrenset løkke.
## Hva du får: oppgjøret
Etter kjøringen svarer den for bestillingen, én linje per tilnærming:
```
Mandate outcome
led-retrofit VALIDATED 30000 NOK LED-retrofit av kontorbelysning
driftsavtale REJECTED claimed 240000 exceeds feasible 90000
own-proposal NOT EVALUATED budget exhausted before this approach was evaluated
Validated: 1 of 3 approaches — best 30000 NOK (the outcome this run carries)
```
De tre statusene betyr forskjellige ting, og forskjellen er poenget:
- **VALIDATED** — tallene holdt. Beløpet er det validatoren slapp gjennom.
- **REJECTED** — tilnærmingen ble vurdert, og falt på tallene. Grunnen står der; det er den du kan
gjøre noe med (ofte er den «regnet mot feil grunnlag», ikke «dårlig idé»).
- **NOT EVALUATED** — kjøringen rakk den aldri. Den er **ikke** avvist, og den er ikke glemt.
En tilnærming du bestilte forsvinner aldri i stillhet. Det er hele grunnen til at oppgjøret finnes.
**Tilnærmingene summeres ikke.** Flere av dem angriper som regel samme kostnadslinje, så de er
alternativer — ikke besparelser som legges oppå hverandre. Derfor står det *hvor mange som holdt*
og *hvilken kjøringen bærer videre*, aldri en sum. (Dette ble oppdaget ved å faktisk kjøre den:
tre tilnærminger mot samme linje ga «totalt 90 000», som ingen av dem kunne innfri.)
## Hva bestillingen ikke gjør
- **Den overstyrer ikke validatoren.** Se ærlighetsmerkingen øverst.
- **Den avgjør ikke om et tiltak skal gjennomføres.** Det gjør et menneske, etterpå — se
[ekspert-svar.md](ekspert-svar.md).
- **Den setter ikke et tallmål.** Sparemålet har ett hjem, målkonfigurasjonen (`--goals`, som gjelder
porteføljemodus). To kopier av samme tall drifter fra hverandre, så oppdraget bærer hensikten i ord
og gjengir tallet hvis det finnes.
- **Den lagrer seg ikke som kunnskap.** Bestillingen er en instruks for én kjøring. Det som lærer
systemet noe, er dommen du avgir etterpå.

View file

@ -194,10 +194,14 @@ def settle(
Empty coverage renders an EMPTY string a run without a mandate has nothing to settle, and a
header over zero rows would imply a commission that never existed.
The ``Validated total`` is a DISPLAY figure, summed in float NOK exactly like
``PortfolioResult.sum_claimed_saving_nok``, and it decides nothing. The goal verdict is
**passed in**, never computed here: ``ledger.to_ore`` is the framework's one NOK->øre
conversion and the goal comparison already runs on quantised integers, but this module cannot
**Commissioned approaches are ALTERNATIVES and are never summed.** Several of them usually
attack the same cost line, so a total would report money the project cannot realise measured
on a real run, three approaches against one line each validated at 30000 NOK and an earlier
version of this function claimed a 90000 total. What is honest is how many held and which one
the run carries: a selection, not an arithmetic claim.
The goal verdict is **passed in**, never computed here: ``ledger.to_ore`` is the framework's one
NOK->øre conversion and the goal comparison already runs on quantised integers, but this cannot
import it without pulling ``verdicts`` and with it ``agent_framework`` into a deliberately
framework-neutral file, while a private copy of a money conversion is precisely the ``(p)``
defect. So the caller decides and this renders. Both ``goal_nok`` and ``goal_reached`` must be
@ -216,8 +220,12 @@ def settle(
else:
lines.append(f" {row.id:<20} NOT EVALUATED {row.detail}")
total = sum(r.saving_nok or 0.0 for r in coverage if r.status == "validated")
lines.append(f" Validated total: {total:.0f} NOK")
held = [r for r in coverage if r.status == "validated"]
best = max((r.saving_nok or 0.0 for r in held), default=None)
tally = f" Validated: {len(held)} of {len(coverage)} approaches"
if best is not None:
tally += f" — best {best:.0f} NOK (the outcome this run carries)"
lines.append(tally)
if goal_nok is not None and goal_reached is not None:
lines.append(
f" Target: >= {goal_nok:.0f} NOK — "

View file

@ -50,6 +50,7 @@ _LIVE_DOCS = (
"README.md",
"docs/extending.md",
"docs/ekspert-svar.md",
"docs/bestille-en-kjoring.md",
"docs/knowledge-base-recipe.md",
)

View file

@ -260,11 +260,29 @@ def test_settle_carries_the_rejection_reason() -> None:
assert "claimed 200000 exceeds feasible 90000" in settle(_ROWS)
def test_settle_totals_only_the_validated_savings() -> None:
"""The total is what passed the validator — never the claimed sum of everything attempted."""
def test_settle_never_sums_alternative_approaches() -> None:
"""Commissioned approaches are ALTERNATIVES, not additive savings — several of them usually
attack the same cost line. Summing them would report a figure the project cannot realise.
MEASURED on a real run: three approaches against one cost line each validated at 30000 and the
settlement claimed a 90000 total. What is honest is how many passed and which one the run
carries a selection, not an arithmetic claim.
"""
rows = (
ApproachOutcome(id="a", label="A", status="validated", saving_nok=30_000.0),
ApproachOutcome(id="b", label="B", status="validated", saving_nok=20_000.0),
)
text = settle(rows)
assert "50000" not in text # the sum is never formed
assert "30000" in text # the best one is named
assert "2 of 2" in text
def test_settle_counts_the_rejected_out_of_the_validated_tally() -> None:
"""A rejected approach counts toward how many were commissioned, never toward how many held."""
text = settle(_ROWS)
assert "30000" in text
assert "200000 NOK" not in text # the rejected claim is never folded into a total
assert "1 of 3" in text
assert "200000 NOK" not in text # the rejected claim is never presented as a saving
def test_settle_states_whether_the_target_was_reached() -> None: