portfolio-optimiser/docs/ekspert-svar.md
Kjell Tore Guttormsen 56f4f6d084 feat(hitl): ekspertdommen kan ikke oppstaa av stillhet (F2, ORDRE 20260825T214801Z)
run_project KREVDE verdict_input og kjorte capture_verdict ubetinget; CLI-en
defaultet det til {"approved", "reviewed by expert"} og hosting listet det som
PAAKREVD. Netto: hver flaggloes kjoering myntet en ekspertgodkjenning ingen ga,
den gikk inn i den delte storen, og run_portfolio bar den inn i neste prosjekts
hypotese-prompt som en prior expert verdict -- paa flaten som ble overlevert
14.08. Non-goal 3, brutt i en soem.

RunResult.verdict er naa Verdict | None, og None er hva stillhet produserer:
ingenting myntes, ingenting lagres, ingenting varsles. Prinsippet sto allerede i
repoet -- RunFailure sin docstring: aa fylle et felt med en dummy legger
FABRIKKERT proveniens inn i aggregatet.

Traceability koster ingenting: RunResult.verdict_key (property, derivert fra
kandidaten) er verdicts.verdict_key sitt alt dokumenterte formaal -- identisk
med verdict.id naar en dom BLE gitt, og fortsatt meningsfull naar ingen ble det.
Det er den outboxen og den hostede responsen stempler.

Halv dom NEKTES paa begge doerer (FeedbackContract er eneste sted formen
valideres; CLI-en nekter ved navn FOER enhver mode-dispatch). Validering, aldri
reparasjon. De to mode-partisjonene fikk --decision/--rationale inn: kommentarene
sa ordrett at en aerlig nekt var uimplementerbar fordi de non-None
argparse-defaultene gjorde en eksplisitt verdi uskillbar fra defaulten -- med
defaultene borte er den implementerbar.

Hosting er WIDENING, ikke bryting: verdict_input flyttet fra _REQUIRED_FIELDS
til _OPTIONAL_FIELDS. Ingen ekstern kaller brekker.

AERLIGHETS-GRENSE: referanse-fixturens SYNTETISKE verdict_input-rader staar
uroert -- de er merket SYNTETISK paa fire steder og er reviewens F5 (maaling av
misjonspaastanden), ikke F2. Project.verdict_input er naa valgfri.

Load-bearing MAALT (tests/test_ungiven_verdict_loadbearing.py, 15 armer), aatte
mutasjoner alle roede mot HELE suiten + gronn kontroll 1080/5 og golden
demo-transcript.stdout BYTE-UENDRET (ea8c534773acdbe41ae68f2c55724d69aaf8be4f).
En mutasjon falsifiserte testen foerst (vakuoes-gate-klassen, ellevte gang):
--report-armen brukte et bart --report, som nekter rc 1 uansett fordi --ledger
mangler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 01:22:07 +02:00

11 KiB

Limbare ekspert-svar

Dette dokumentet er for deg som skal avgi dommen i loopen — fageksperten som får et validert forslag i fanget og skal si ja, ja-med-forbehold eller nei. Det gir deg ferdige svar du kan lime rett inn, og forklarer hvor de skal limes.

Ærlighetsmerking. Svarteksten under er AI-forfattet og er ikke verifisert fagdom. Den er laget for å vise formen et ekspertsvar har og for å gjøre gjennomkjøringen mulig uten en levende ekspert. Tallene og resonnementene er forankret i kildene som ligger i kunnskapsbasen (shared/examples/bygg-energi-mikro/kilder-realiseringsgap.md), men ingen fagperson har godkjent dem. Skal en dom telle som fagdom, må en fagperson skrive den selv.

De to formene, og når du bruker hvilken

Systemet tar imot en ekspertdom på to steder, og de gjør forskjellige ting:

Form A — begrunnelses-streng Form B — dom-fil i innboksen
Hvor --decision / --rationale på kommandolinjen en JSON-fil i mappa --verdict-dir peker på
Når du er til stede mens kjøringen skjer du dømmer etterpå, kanskje dager senere
Rekkevidde denne ene kjøringen alle senere kjøringer som leser innboksen
Hva du skriver bare begrunnelsen (én streng) hele dommen som JSON-dokument

Form B er den som lærer systemet noe. Form A er en synkron snarvei.

Ingen av dem er default, og det er poenget. Utelater du begge flaggene, blir det ikke registrert noen dom i det hele tatt — kjøringen sier det høyt (no expert verdict given; verdict key=…), og ingenting går inn i læringsstoren. Fram til 1.1.0 defaultet --decision til approved, slik at hver flaggløs kjøring produserte en ekspertgodkjenning ingen ekspert hadde gitt, og bar den videre inn i neste prosjekts hypotese. Nøkkelen linja skriver ut er den du fører inn i id-feltet i Form B, og det er slik en dom avgitt dager senere finner tilbake til kandidaten den gjelder. Halv dom nektes: --decision og --rationale må gis sammen. Den manglende halvdelen er ekspertens å skrive, aldri systemets å fylle inn.

decision er binær på kjørestien: approved eller rejected. Det finnes ikke et tredje «godkjent med justering» her — en realiseringskorreksjon er en godkjenning der forbeholdet står i begrunnelsen. Det er med vilje: forbeholdet er fagkunnskap som skal leses, ikke en enum-verdi som kan telles.

Form A — begrunnelses-streng på kommandolinjen

uv run python -m portfolio_optimiser.run BYGG-KONTOR-NORD \
  --docs-dir shared/examples/bygg-energi-mikro \
  --bundle-dir shared/examples/bygg-energi-mikro \
  --scripted-replies replies.json \
  --decision approved \
  --rationale "Godkjent med realiseringskorreksjon. Besparelsen er teknisk korrekt utledet, men i drift realiseres erfaringsvis ~79 % av en timeplan-stipulert LED-besparelse i kontorbygg; forventet faktisk besparelse ca 23 700 NOK/aar."

Form B — dom-fil i innboksen

Slik finner du feltene du må fylle ut

Kjør med --outbox-dir (som krever --run-id), så legger kjøringen igjen artefaktene sine:

uv run python -m portfolio_optimiser.run BYGG-KONTOR-NORD \
  --docs-dir shared/examples/bygg-energi-mikro \
  --bundle-dir shared/examples/bygg-energi-mikro \
  --scripted-replies replies.json \
  --outbox-dir utboks --run-id run-001

uv run python -m portfolio_optimiser.hitl pending --outbox-dir utboks --verdict-dir innboks

pending skriver én linje per forslag som fortsatt venter på en dom:

run-001 c91cb2fe1aa139a9 validated

Midterste kolonne er id-en dommen din må bære. Den er en innholds-hash av forslagets egenskaper, ikke et løpenummer — den er nøkkelen en senere kjøring bruker for å finne igjen dommen din, så den må gjengis ordrett. Resten av feltene ligger i utboks/run-001-proposal.json: measuremeasure_type, hver affected_items[].codeaffected_codes, og claimed_saving_nok uendret.

Har du bestilt flere tilnærminger?

Da får hver vurderte tilnærming sin egen artefakt — ellers kunne du bare dømt den ene kjøringen valgte, og de andre du bestilte ville ikke lært systemet noe. Filene heter utboks/run-001-<tilnærming>-proposal.json, og pending lister dem hver for seg med tilnærmingen i klammer:

run-001 c91cb2fe1aa139a9 validated [behovsstyrt-lys]
run-001 4f0a1d77b2e5c318 rejected [aggregat-bytte]
run-001 9b31e0c4a7d6f025 validated [own-proposal]

own-proposal er systemets eget forslag, ikke et av dine. Hver linje dømmes for seg, med sin egen id — akkurat som over. To tilnærminger som endte på nøyaktig samme forslag deler id (den er en innholds-hash), og da gjør én dom opp for begge; det er ikke en feil, det er at de var samme forslag.

Merk hva artefaktene ikke sier: en tilnærming som står som rejected, er avvist av den deterministiske validatoren — altså på tallene. Det er ikke en fagdom over ideen. Det er nettopp derfor den fortsatt havner i pending og venter på deg.

Utboks og innboks skal være to forskjellige mapper. De har motsatt eierskap: systemet skriver utboksen, du skriver innboksen. (Målt: å peke dem på samme mappe ødelegger ingenting i dag — utboksens filer heter run-001-*.json og mangler dom-feltene, så innboks-lasteren hopper over dem. Men den dagen noe skriver en dom-formet fil i utboksen, blir rå agent-output lest inn som om en ekspert hadde godkjent den. Hold dem adskilt.)

Fila legges i innboksen som <id>.json, altså innboks/c91cb2fe1aa139a9.json.


Svar 1 — godkjent med realiseringskorreksjon

Det vanligste ekte svaret på et energitiltak: regnestykket stemmer, men drift leverer mindre enn modellen lovte. Dette er dommen som bærer mest læring, fordi den forteller systemet noe det ikke kunne lest ut av tallene selv.

{
  "id": "c91cb2fe1aa139a9",
  "decision": "approved",
  "rationale": "Godkjent med realiseringskorreksjon. Den modellerte besparelsen er teknisk korrekt utledet fra parameterne, og validatoren bekrefter at den ligger innenfor feasibelt omraade. Men i drift realiseres erfaringsvis ~79 % av en timeplan-stipulert LED-besparelse i kontorbygg (realiseringsgrad=0.79), fordi driftstimene er overestimerte og in-service rate er under 1. Forventet faktisk besparelse er ca 23 700 NOK/aar. Tiltaket gjennomfoeres, men gevinsten foeres opp med det korrigerte tallet.",
  "proposal_features": {
    "affected_codes": ["ENERGI-TOTAL-EL"],
    "measure_type": "LED-retrofit av kontorbelysning",
    "claimed_saving_nok": 30000.0,
    "description": ""
  }
}

Svar 2 — godkjent uten forbehold

Når du faktisk mener tallet står seg som det er. Kort er riktig her; en lang begrunnelse for et enkelt ja gir systemet støy å lære av.

{
  "id": "c91cb2fe1aa139a9",
  "decision": "approved",
  "rationale": "Godkjent. Effekt foer/etter og antall armaturer er verifisert mot anleggets egen dokumentasjon, og driftstimene er metret, ikke stipulert. Det vanlige realiseringsavviket gjelder derfor ikke her, og besparelsen foeres opp uendret.",
  "proposal_features": {
    "affected_codes": ["ENERGI-TOTAL-EL"],
    "measure_type": "LED-retrofit av kontorbelysning",
    "claimed_saving_nok": 30000.0,
    "description": ""
  }
}

Svar 3 — avvist: tiltaket er ikke gjennomførbart her

Den viktigste dommen å ha et eksempel på, fordi den er den eneste som stopper noe. Validatoren har allerede sagt at tallene holder — du sier at virkeligheten rundt tallene ikke gjør det. Det er en dom bare et menneske kan avgi.

{
  "id": "c91cb2fe1aa139a9",
  "decision": "rejected",
  "rationale": "Avvist. Regnestykket er internt konsistent, men forutsetningen om 3 000 driftstimer holder ikke for dette bygget: belysningen staar paa naermest kontinuerlig i fellesarealene og er allerede dagslysstyrt i kontorsonene, saa faktisk brenntid ligger langt fra timeplanen tiltaket er regnet paa. I tillegg ble armaturene skiftet i 2023 og er ikke avskrevet. Et nytt bytte naa gir en gevinst som ikke forsvarer kostnaden, uavhengig av at den modellerte besparelsen er feasibel.",
  "proposal_features": {
    "affected_codes": ["ENERGI-TOTAL-EL"],
    "measure_type": "LED-retrofit av kontorbelysning",
    "claimed_saving_nok": 30000.0,
    "description": ""
  }
}

Svar 4 — avvist: feil kostnadsgrunnlag

Bruk denne når tiltaket i seg selv er fornuftig, men det er regnet mot feil tall. Her sier du noe om grunnlaget, ikke om tiltaket — og det er en annen lærdom for systemet.

{
  "id": "c91cb2fe1aa139a9",
  "decision": "rejected",
  "rationale": "Avvist paa grunnlaget, ikke paa tiltaket. Besparelsen er regnet mot byggets totale elforbruk, men belysning utgjoer bare ~18 % av det forbruket. Den paastaatte besparelsen tilsvarer over halvparten av hele belysningsposten, noe som ikke er oppnaaelig med et rent armaturbytte. Send tiltaket inn paa nytt regnet mot belysningsposten alene, saa er det sannsynligvis godkjennbart i redusert form.",
  "proposal_features": {
    "affected_codes": ["ENERGI-TOTAL-EL"],
    "measure_type": "LED-retrofit av kontorbelysning",
    "claimed_saving_nok": 30000.0,
    "description": ""
  }
}

Hva systemet gjør med dommen din

Legg fila i innboksen og kjør på nytt med --verdict-dir:

uv run python -m portfolio_optimiser.run BYGG-KONTOR-NORD \
  --docs-dir shared/examples/bygg-energi-mikro \
  --bundle-dir shared/examples/bygg-energi-mikro \
  --scripted-replies replies.json \
  --verdict-dir innboks

Dommen slås sammen inn i minnet før hypotesen lages, så neste forslag ser hva du sa forrige gang. hitl pending er tom etterpå — forslaget er dømt.

Dommen din blir liggende i råt-laget: den er lest, men den er ikke skrevet inn i kunnskapsbasen. Det siste skrittet er bevisst atskilt og krever en godkjenning (verdicts.promote_verdict), slik at bare kunnskap et menneske har stått inne for havner i wikien. En avvist dom kan ikke promoteres i det hele tatt — porten er lukket den veien, så rå agent-output aldri kan skrive seg selv inn som lærdom.

Om du skriver dine egne

  • id gjengis ordrett fra hitl pending. Endrer du den, blir dommen din hengende på et forslag som ikke finnes.
  • decision er approved eller rejected. Ingen tredje verdi på denne stien.
  • proposal_features skal beskrive forslaget du dømte, ikke det du mener det burde vært. Feltene kopieres fra run-001-proposal.json.
  • Begrunnelsen er det eneste som bærer fagkunnskap videre. Skriv hvorfor, ikke hva — «avvist, ikke feasibelt» lærer systemet ingenting det ikke allerede visste.
  • En ufullstendig eller halvskrevet fil hoppes stille over, ikke feiler. Sjekk med hitl pending at forslaget faktisk forsvant fra lista.