feat(verdicts): key each verdict on its own candidate, not the bundle's one IR projection (S3.2)

seed_store_from_bundle keyed EVERY `type: verdict` file on bundle_candidate_features — the single
candidate the bundle's validator-input.json describes. A bundle carrying verdicts about several
candidates collapsed them onto one key, so a verdict about candidate B scored a perfect structural
match against candidate A's query and could be folded into A's hypothesis prompt. The ExpeL
substrate was single-candidate by construction.

A verdict file may now carry its own structural key in frontmatter (affected_codes / measure_type /
claimed_saving_nok); absent, keying falls back to the bundle candidate, so every pre-S3.2 seed keeps
working unchanged. promote_verdict writes the three fields, so a promoted verdict — frequently about
a different candidate than the target bundle's projection — does not impersonate that candidate.

Semantics decided HERE, not pulled: commons' seeding rule (method-spec §3 Steg 1 + bundle example)
has not arrived; we said we would build locally first. D7 mirroring stays open.

- ALL THREE fields or none. A partial declaration raises VerdictFrontmatterError rather than merging
  with the bundle candidate, which would mint a key belonging to NEITHER candidate. Validation,
  never repair (mirrors write_concept_file); the tolerant-skip rule belongs to the RAW inbox layer.
- claimed_saving_nok parses via json.loads — the SAME literal rule the IR projection went through —
  and is written back with str() of the raw value. _mint_id hashes that value, so 30000 and 30000.0
  are different keys; a normalising writer would split one candidate's signal across two ids.
- The structural key is signal-free, so it does not weaken the Step-8 no-leak property (Test C green).

Load-bearing MEASURED, five mutations all red: detach per-verdict keying · detach the fields
promote_verdict writes · make a partial/unparseable key tolerant · normalise the magnitude on write ·
remove the fallback (control — breaks the step1 suite at collection, proving the fallback bears load).

589 -> 597 tests. Full gate green (pytest, ruff, mypy).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkjvTTxrg9LTrmghebfiij
This commit is contained in:
Kjell Tore Guttormsen 2026-08-03 16:44:58 +02:00
commit 012adc0a3c
10 changed files with 496 additions and 17 deletions

View file

@ -0,0 +1,34 @@
---
type: index
okf_version: 0.1
title: "Multi-kandidat mikro — repo-lokal fixture (S3.2 per-dom-noekling)"
description: "Minimal repo-lokal OKF-bundle under data/ med dommer om TO ULIKE kandidater, mens IR-projeksjonen beskriver kandidat A alene. Fixturen gjoer forskjellen mellom per-dom-noekling og bundle-noekling maalbar."
tags: [fixture, S3.2, multi-kandidat]
timestamp: 2026-08-03
---
# Multi-kandidat mikro (repo-lokal test-fixture)
En **repo-lokal mini OKF-bundle** under pakkens `data/` (ALDRI `shared/` — subtree er PULL-ONLY).
Den finnes for ett formaal: bundelen bærer dommer om **to ulike kandidater**, mens
`validator-input.json` (ExpeL-query-noekkelen) beskriver **kandidat A alene**.
Foer S3.2 noeklet `seed_store_from_bundle` HVER dom paa bundelens ene IR-projeksjon, saa dommen om
kandidat B ble uskillelig fra dommen om kandidat A og kunne naa kandidat As hypotese-prompt. Med
per-dom-noekling leser hver dom sine EGNE strukturelle felt fra frontmatter.
Rekkefoelgen under er bevisst: **kandidat B er lenket foerst**, saa en detachet per-dom-noekling
(begge dommer faar kandidat As noekkel → uavgjort likhet, uavgjort id) rangerer B-dommen oeverst.
## Innhold (progressiv disclosure)
- [verdict-b-asfalt.md](verdict-b-asfalt.md) — `type: verdict` — dom om **kandidat B**
(asfalttykkelse, kode `05.2`). Bærer sine egne `affected_codes`/`measure_type`/
`claimed_saving_nok`. Skal ALDRI naa kandidat As hypotese-prompt.
- [verdict-a-led.md](verdict-a-led.md) — `type: verdict` — dom om **kandidat A**
(LED-retrofit, kode `ENERGI-TOTAL-EL`). Dette er dommen kandidat As query skal hente.
- [prosjekt-multi.md](prosjekt-multi.md) — `type: project` — bygget/strekningen begge kandidatene
hoerer til. Concept-filen OKF-navigasjonen leser inn som lese-kontekst.
`validator-input.json` er IR-projeksjonen den deterministiske validatoren konsumerer, og kilden for
ExpeL-query-noekkelen (`bundle_candidate_features`) — kandidat A.

View file

@ -0,0 +1,20 @@
---
type: project
title: "Kombinert bygg + veistrekning (syntetisk fixture-prosjekt)"
description: "Syntetisk prosjekt som baerer to uavhengige kandidat-tiltak — ett energitiltak i kontorfloy A og ett dekketiltak paa veistrekning B."
resource: MULTI-KANDIDAT-MIKRO
tags: [fixture, project]
timestamp: 2026-08-03
---
# Kombinert bygg + veistrekning
Syntetisk fixture-prosjekt. To uavhengige tiltak er identifisert:
- **Kandidat A — LED-retrofit kontorfloy A.** Beroerer kostkode `ENERGI-TOTAL-EL`.
Modellert besparelse 18 000 NOK/aar.
- **Kandidat B — redusert asfalttykkelse veistrekning B.** Beroerer kostkode `05.2`.
Modellert besparelse 900 000 NOK.
Kandidatene deler verken kostkode, tiltakstype eller stoerrelsesorden. Det er med vilje: en dom om
den ene sier ingenting om den andre, og skal derfor ikke hentes for den andre.

View file

@ -0,0 +1,16 @@
{
"_note": "SYNTHETIC repo-local multi-candidate mini-bundle IR-projeksjon (Fase 2a S3.2-fixture) — ikke ekte data. Bundelen bærer dommer om TO kandidater, men IR-projeksjonen (og dermed bundle_candidate_features) er kandidat A alene — nettopp asymmetrien per-verdict-noekling maa haandtere.",
"project_id": "MULTI-KANDIDAT-MIKRO",
"measure": "LED-retrofit kontorfloy A",
"affected_items": [
{
"code": "ENERGI-TOTAL-EL",
"quantity": 180000,
"unit_cost": 1.0
}
],
"claimed_saving_nok": 18000,
"assumptions": {
"ENERGI-TOTAL-EL": [0.70, 1.40]
}
}

View file

@ -0,0 +1,27 @@
---
type: verdict
title: "Ekspert-dom (froe) — LED-retrofit kontorfloy A"
description: "Froesatt realiseringskorreksjon for KANDIDAT A (energitiltak). Dette er dommen kandidat As hypotese-prompt skal hente."
resource: MULTI-KANDIDAT-MIKRO
measure_id: LED-RETROFIT-A
decision: approved
affected_codes: [ENERGI-TOTAL-EL]
measure_type: "LED-retrofit kontorfloy A"
claimed_saving_nok: 18000
realization_rate: 0.80
expected_actual_saving_nok: 14400
gap_source: hours-of-use-overestimation
provenance: "froe — AI-forfattet test-fixture for S3.2; ikke ekte data"
tags: [verdict, fixture, kandidat-A]
timestamp: 2026-08-03
---
# Ekspert-dom (froe) — kandidat A
**Beslutning:** godkjent, med realiseringskorreksjon.
Den modellerte besparelsen paa 18 000 NOK/aar er teknisk korrekt, men i drift realiseres
erfaringsvis ~80 % av en timeplan-stipulert LED-besparelse i kontorbygg.
Dette er dommen som ER relevant for LED-hypotesen — samme kostkode, samme tiltakstype, samme
stoerrelsesorden.

View file

@ -0,0 +1,27 @@
---
type: verdict
title: "Ekspert-dom (froe) — redusert asfalttykkelse veistrekning B"
description: "Froesatt dom om KANDIDAT B (dekketiltak). Baerer sine egne strukturelle felt, saa den noekles paa sin egen kandidat og ikke paa bundelens IR-projeksjon."
resource: MULTI-KANDIDAT-MIKRO
measure_id: ASFALT-B
decision: rejected
affected_codes: [05.2]
measure_type: "Redusert asfalttykkelse veistrekning B"
claimed_saving_nok: 900000
realization_rate: 0.31
expected_actual_saving_nok: 279000
gap_source: levetidsforkortelse-gir-tidligere-reasfaltering
provenance: "froe — AI-forfattet test-fixture for S3.2; ikke ekte data"
tags: [verdict, fixture, kandidat-B]
timestamp: 2026-08-03
---
# Ekspert-dom (froe) — kandidat B
**Beslutning:** avvist.
Den modellerte besparelsen paa 900 000 NOK er regnemessig korrekt, men tynnere dekke forkorter
levetiden saa mye at reasfaltering kommer tidligere. Livsloepsregnskapet blir negativt.
Denne dommen handler om **dekketiltaket**, ikke om energitiltaket. Den skal ikke hentes naar
hypotesen gjelder LED-retrofit — kostkode, tiltakstype og stoerrelsesorden er alle ulike.