feat(validator): anchor the deterministic gate to the project's real cost baseline (S4.0)

Every stage of validate_proposal reasoned only about numbers the proposal itself
supplied, so an internally-consistent hallucination cleared the whole gate (F3).
A new stage 0 reconciles each affected_item against the project's CostBaseline
before the CBC solve: an unknown cost code is rejected, and a real code carrying
a quantity/unit_cost outside the configured tolerance (5% default, relative to
the baseline value) is rejected. Validation, never repair.

The baseline argument is OPTIONAL (None = pre-S4.0 behaviour), but both run
paths set it: the road path projects project.cost_items, the bundle path loads
cost-baseline.json when the bundle ships one. Bundles written before the
amendment stay un-anchored, so the commons-owned goldens run byte-identically;
a baseline that exists but is malformed still raises on both loaders.

F8: the method-specific cap now comes from the METHOD_CAPS registry (measure
type -> fraction, injectable) instead of an energy_efficiency string comparison.

The baseline format and tolerance semantics were decided locally — the commons
amendment (D-A pt. 2) never arrived, exactly as in S3.2. D7 mirroring stays open.

Three portfolio fixtures quoted cost codes belonging to OTHER projects; the new
gate caught them. They now quote each project's own lines, and the two copied
REPLIES tables import the single source instead of drifting from it.

Load-bearing measured (tests/test_s40_cost_baseline_loadbearing.py), six
mutations all red: detach the reconciliation stage; detach the magnitude
tolerance; detach the road wiring; detach the bundle wiring; ignore the injected
cap registry; make the optional loader tolerant of malformed content. Control:
with the road wiring detached the repaired portfolio fixtures still pass, so
they are not masking the seam. 597 -> 612 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JdwK7bQ4BZkWH4t8MRDKb4
This commit is contained in:
Kjell Tore Guttormsen 2026-08-03 17:19:31 +02:00
commit 126807aee7
16 changed files with 645 additions and 69 deletions

View file

@ -0,0 +1,20 @@
---
type: project
title: "Kontorbygg B — energibaseline med erklærte kostlinjer (repo-lokal fixture)"
description: "Syntetisk energibaseline for S4.0-fixturen. Ett kontorbygg, én kandidat-kostpost (ENERGI-TOTAL-EL), og en eksplisitt cost-baseline.json den deterministiske validatoren avstemmer mot."
timestamp: 2026-08-03
---
# Kontorbygg B (repo-lokal test-fixture)
Et lite syntetisk kontorbygg med én dokumentert energibaseline. Til forskjell fra mikro-A erklærer
denne bundelen kostlinjene sine maskinlesbart i `cost-baseline.json`, slik at et forslag som siterer
en oppdiktet kostkode — eller en ekte kode med oppdiktet størrelse — blir avvist av validatorens
avstemmings-stage før løseren i det hele tatt kjøres.
## Energibaseline
- Årlig elektrisk energikostnad (`ENERGI-TOTAL-EL`): 180 000 NOK/år (syntetisk), ført som
`quantity: 180000` × `unit_cost: 1.0` i `cost-baseline.json`.
- Kandidat-tiltak: LED-retrofit av kontorbelysning.
- Modellert besparelse: ~18 000 NOK/år (innenfor validatorens feasibelt-område).

View file

@ -0,0 +1,10 @@
{
"_note": "SYNTHETIC repo-lokal kostbaseline (S4.0-fixture) — ikke ekte data. Prosjektets FAKTISKE kostlinjer: den deterministiske validatoren avstemmer et forslags affected_items mot disse (okf.load_cost_baseline -> validator._reconcile_against_baseline). Formatet er bestemt LOKALT (commons-amendmentet D-A pkt. 2 kom aldri); D7-speiling er ÅPEN.",
"project_id": "BYGG-ENERGI-BASELINE-MIKRO",
"items": {
"ENERGI-TOTAL-EL": {
"quantity": 180000,
"unit_cost": 1.0
}
}
}

View file

@ -0,0 +1,26 @@
---
type: index
okf_version: 0.1
title: "Bygg-energi baseline-mikro — repo-lokal fixture (S4.0 kostbaseline-forankring)"
description: "Minimal repo-lokal OKF-bundle som SHIPPER en cost-baseline.json, så bundle-stiens validator-forankring (S4.0, F3) kan testes ende-til-ende. Speiler bygg-energi-mikro-A, men med kostbaselinen lagt til."
tags: [fixture, S4.0, kostbaseline]
timestamp: 2026-08-03
---
# Bygg-energi baseline-mikro (repo-lokal test-fixture)
En **repo-lokal mini OKF-bundle** under pakkens `data/` (ALDRI `shared/` — subtree er PULL-ONLY).
Eneste formål: være den ene bundelen som **erklærer sin kostbaseline**, slik at
`run_project(bundle_dir=...)` faktisk forankrer den deterministiske gaten mot prosjektets virkelige
kostlinjer. De eldre bundlene bærer bevisst INGEN `cost-baseline.json` — de er kontrollen som viser
at forankringen er opt-in per bundle (pre-amendment-bundler kjører uendret).
## Innhold (progressiv disclosure)
- [bygg-kontor-baseline.md](bygg-kontor-baseline.md) — `type: project` — bygget og energibaselinen
som lese-kontekst (og `bundle_citations`-kilde).
`validator-input.json` er IR-projeksjonen (kandidatens kost-IR), og `cost-baseline.json` er
prosjektets FAKTISKE kostlinjer (`code -> {quantity, unit_cost}`) som `okf.load_cost_baseline` leser
og validatoren avstemmer `affected_items` mot. De to er bevisst adskilt: IR-projeksjonen er hva
noen *foreslår*, baselinen er hva prosjektet *er*.

View file

@ -0,0 +1,13 @@
{
"_note": "SYNTHETIC repo-lokal IR-projeksjon (S4.0-fixture) — ikke ekte data. Kandidatens kost-IR; project_id matcher det bundle-backede prosjektets id (run._project_from_bundle fail-faster ved mismatch). Kostlinjene her SPEILER cost-baseline.json med vilje: et forslag om denne kandidaten skal avstemme rent.",
"project_id": "BYGG-ENERGI-BASELINE-MIKRO",
"measure": "LED-retrofit av kontorbelysning",
"affected_items": [
{
"code": "ENERGI-TOTAL-EL",
"quantity": 180000,
"unit_cost": 1.0
}
],
"claimed_saving_nok": 18000
}