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:
parent
b9d795307a
commit
30bcdd3544
5 changed files with 174 additions and 10 deletions
|
|
@ -392,6 +392,11 @@ only an expert has:
|
||||||
|
|
||||||
## Docs
|
## 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
|
- [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
|
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
|
actually look like, and paste-ready examples of each. Marked throughout as AI-authored and not
|
||||||
|
|
|
||||||
132
docs/bestille-en-kjoring.md
Normal file
132
docs/bestille-en-kjoring.md
Normal 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å.
|
||||||
|
|
@ -194,10 +194,14 @@ def settle(
|
||||||
Empty coverage renders an EMPTY string — a run without a mandate has nothing to settle, and a
|
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.
|
header over zero rows would imply a commission that never existed.
|
||||||
|
|
||||||
The ``Validated total`` is a DISPLAY figure, summed in float NOK exactly like
|
**Commissioned approaches are ALTERNATIVES and are never summed.** Several of them usually
|
||||||
``PortfolioResult.sum_claimed_saving_nok``, and it decides nothing. The goal verdict is
|
attack the same cost line, so a total would report money the project cannot realise — measured
|
||||||
**passed in**, never computed here: ``ledger.to_ore`` is the framework's one NOK->øre
|
on a real run, three approaches against one line each validated at 30000 NOK and an earlier
|
||||||
conversion and the goal comparison already runs on quantised integers, but this module cannot
|
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
|
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)``
|
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
|
defect. So the caller decides and this renders. Both ``goal_nok`` and ``goal_reached`` must be
|
||||||
|
|
@ -216,8 +220,12 @@ def settle(
|
||||||
else:
|
else:
|
||||||
lines.append(f" {row.id:<20} NOT EVALUATED {row.detail}")
|
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")
|
held = [r for r in coverage if r.status == "validated"]
|
||||||
lines.append(f" Validated total: {total:.0f} NOK")
|
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:
|
if goal_nok is not None and goal_reached is not None:
|
||||||
lines.append(
|
lines.append(
|
||||||
f" Target: >= {goal_nok:.0f} NOK — "
|
f" Target: >= {goal_nok:.0f} NOK — "
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ _LIVE_DOCS = (
|
||||||
"README.md",
|
"README.md",
|
||||||
"docs/extending.md",
|
"docs/extending.md",
|
||||||
"docs/ekspert-svar.md",
|
"docs/ekspert-svar.md",
|
||||||
|
"docs/bestille-en-kjoring.md",
|
||||||
"docs/knowledge-base-recipe.md",
|
"docs/knowledge-base-recipe.md",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -260,11 +260,29 @@ def test_settle_carries_the_rejection_reason() -> None:
|
||||||
assert "claimed 200000 exceeds feasible 90000" in settle(_ROWS)
|
assert "claimed 200000 exceeds feasible 90000" in settle(_ROWS)
|
||||||
|
|
||||||
|
|
||||||
def test_settle_totals_only_the_validated_savings() -> None:
|
def test_settle_never_sums_alternative_approaches() -> None:
|
||||||
"""The total is what passed the validator — never the claimed sum of everything attempted."""
|
"""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)
|
text = settle(_ROWS)
|
||||||
assert "30000" in text
|
assert "1 of 3" in text
|
||||||
assert "200000 NOK" not in text # the rejected claim is never folded into a total
|
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:
|
def test_settle_states_whether_the_target_was_reached() -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue