portfolio-optimiser/docs/2026-09-04-major2-proposal-review-k2.md
Kjell Tore Guttormsen 74ea1dcf02 docs(major2): steg 9 fullfoert - M38..M40 roede, K2-omkjoeringen maalt, last_ruling-paastanden rettet
M38 (attempts_remaining fra max_attempts alene) 2 roede - T3a og T5, som BEGGE
navngir M38 i sin docstring; M39 (drop --checkpoint-dir-nekten) 1; M40
(honoured=True ved revise-tid) 2. Sum: 41 kjoeringer, 40 roede, 1 groenn (M29).

K2-omkjoeringen (kriterium 8), instrumentet validert mot en kjent positiv foerst
(3 954 tegn / 1 495 tok, S7a-3s tall reprodusert eksakt): utforskningen 12
prompter / 18 355 tokens UENDRET til tokenet, debatt-promptene uendret i antall
OG stoerrelse, genererings-promptene 2 -> 4. Totalt 17 -> 19 prompter,
19 274 -> 19 776 tokens (+2,6 %). Utfallet flytter seg 200 000 -> 150 000 NOK og
dom-noekkelen be8535e2 -> f23ecff8; ekspertens ord staar ordrett i 2 av 6
proposer-prompter (0 av 4 i kontrollen).

generate.py: kommentaren paasto at `assert last is not None` ville fyrt uten
baereren. M29 maalte at den ikke KAN - D1(a) returnerer inne i loekka naar
remaining == 0, og paa siste forsoek er max_attempts - i - 1 alltid 0, saa halen
er naabar kun etter en validator-avvisning, som setter `last` ogsaa. Baereren er
uvitnet (budget_stop-presedensen), og det staar naa i kilden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 20:39:52 +02:00

274 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MAJOR-2 — en dør for menneskelig tilbakemelding på forslaget som ligger på bordet
> **Måledato:** 2026-09-05 (stien bærer ordredatoen, headeren måledatoen — S7b/S2c-formen)
> **Ordre:** `20260904T173146Z-8102814273-from-portfolio-optimiser`
> **Kode målt ved:** `ee7da3b` (steg 18: `ce4c15e` · `d4c8691` · `bbf4d3b` · `59f3fce` ·
> `c391fb5` · `fedf989` · `bfc634d` · `ee7da3b`); mutasjonene M1M40 og K2-omkjøringen er
> målt mot nøyaktig det treet (`shasum -c` grønn før og etter hver mutasjon)
> **Grønn kontroll:** 1363 / 5 under mutasjonsmålingen, 1364 / 5 etter README-armen i steg 10
---
## 0. Hva døra er
| Spørsmål | Før | Etter |
|---|---|---|
| Kan et menneske svare på forslaget mens det ligger på bordet? | nei | ja — `--proposal-review` |
| Hvor mange menneske-sømmer finnes inne i løkka? | 1 (`plan_reviewer`, som fyrer FØR enhver hypotese finnes) | 2 |
| Hva skjer med svaret? | — | `revise` kjøper ETT forsøk til under det EKSISTERENDE taket; ordene når neste prompt ORDRETT |
| Hvem avgjør utfallet? | validatoren | validatoren — dens SISTE dom (D6) |
| Mynter en `approve` en dom? | — | nei; F2 står |
| En kjøring uten reviewer | — | byte-identisk: samme prompter, samme utboks-filer, golden uendret |
Målt før arbeidet startet (`docs/2026-09-02-misjonsreview-v2.md` § 3, re-målt mot HEAD):
`grep -n "prior_" generate.py` finner nøyaktig ÉN søm — `prior_rejection`, maskinens egen
falsifisering matet tilbake inne i samme kjøring. Et menneskes dom lander i NESTE kjøring
(Steg-7-innboksen). «Improved proposals given feedback» var altså, for en person, en to-kjørings
løkke med dager imellom.
---
## 1. Løkkas exit-kontrakt — og et premiss målingen felte
**FØR** (`generate.py` ved `bcf3337`):
```python
if isinstance(result, ValidatedProposal):
return GenerationResult(outcome=result, refinements=tuple(fed_back))
last = result
assert last is not None # max_attempts >= 1, so at least one validation ran
return GenerationResult(outcome=last, refinements=tuple(fed_back))
```
`last` settes KUN av en validator-avvisning. Planen (§ Risks, «Critical» #1) sa at «validert →
revise» på hvert forsøk derfor ville nå slutten av løkka med `last is None` og dø på asserten.
**ETTER**: en eksplisitt bærer `last_ruling: ValidatedProposal | Rejection | None`, satt etter
HVER `validate_proposal`; D6 leses rett av den.
**⚠️ PLANENS PREMISS ER FALSIFISERT AV MÅLINGEN — M29 forble GRØNN (0 røde av 1363).**
Mutasjonen reverterer bæreren til `assert last is not None`, og HELE suiten står grønn. Grunnen er
strukturell og ble først synlig da mutasjonen ble kjørt: D1(a) gjør at en `revise` med
`attempts_remaining == 0` **returnerer** den validerte dommen inne i løkka, og på siste forsøk er
`remaining = max_attempts - i - 1` alltid 0. Løkka kan derfor bare falle GJENNOM til halen etter en
validator-**avvisning**, som setter `last` også. `last_ruling` og `last` er dermed identiske i det
ene punktet halen leser dem.
Konsekvens, uttalt heller enn skjult: **bæreren er UVITNET** (`budget_stop`-presedensen). Den står
fordi den gjør D6 eksplisitt og fjerner en assert som hviler på en ikke-lokal invariant — ikke fordi
en test holder den. Kommentaren i `generate.py` som påsto at asserten ville fyrt er en påstand
flaten gjør om seg selv (Fase-3-klassen) og **må rettes i neste økt**, sammen med commit-teksten for
`bbf4d3b` som bærer samme påstand (historikken skrives ikke om; rettelsen står her og i
invariantraden).
---
## 2. Mutasjonstabellen — M1M40, ÉN liste
**Grønn kontroll:** `uv run pytest -q`**1363 passed / 5 skipped**.
Node-ID-settet er et **strengt supersett** av pre-MAJOR-2-baselinen: 1319 → 1368, **0 fjernet**,
49 lagt til. `ruff check src tests`, `ruff format --check src tests` og `mypy src` rene.
Golden `tests/golden/demo-transcript.stdout` **BYTE-UENDRET**, `shasum -a 1` av INNHOLDET =
`ea8c534773acdbe41ae68f2c55724d69aaf8be4f` (ikke git-blob-id-en).
Protokoll per mutasjon: hele suiten, maks to mutasjoner per Bash-kall, restaurert fra
`scratchpad/major2/pristine/` og verifisert med `shasum -c` — aldri `git checkout`. Alle 41
kjøringer rapporterte `restore=OK`.
**Nevneren, med enhet:** M1M40 er **40 mutasjons-ID-er**, men M26 er sjekket i TO lengder
(M26a/M26b, planens funn-1-skjerping), så måleprotokollen er **41 KJØRINGER**. Av dem er **40
røde og 1 grønn** (M29). Begge tellemåtene er sanne; dokumentet teller i KJØRINGER hele veien,
og `coord-send`-rapporten gjør det samme.
| M | hva mutasjonen gjør | røde | første navngitte armer | uavhengige vitner |
|---|---|---|---|---|
| M1 | detach reviewer-kallet (returner på validert som før) | 16 | T1, T2, T3a … | — |
| M2 | ignorer beslutningen (alltid approve) | 13 | T1, T2, T3a … | — |
| M3 | drop `prior_feedback`-injeksjonen (print-and-discard) | 6 | ordrett-armen, komposisjons-armen, T1 … | — |
| M4 | tøm feedback etter ett forsøk (ikke-klebrig) | 1 | T6 alene | — |
| M5 | akkumuler avvisninger i prompten | 1 | T6 alene | — |
| M6 | vis også en AVVIST kandidat til revieweren | 4 | T4, T6, T10 … | — |
| M7 | la en revise slippe å tikke rundeboka | 1 | T3a alene | — |
| M8 | la revisjoner løkke UTENFOR `max_attempts` | 36 | A5, mandat, portefølje … | **ja** — 18 testfiler, alle eldre enn dette arbeidet |
| M9 | `honoured` konstant `True` (og ingen forfremmelse) | 6 | T2, T3a, T3b, T7 … | — |
| M10 | `approach_id` alltid `None` | 3 | T5, T8, T5-run | — |
| M11 | skriv artefaktet etter returen, ikke fra `finally` | 2 | T9, T16 | — |
| M12 | skriv artefaktet kun når lista er ikke-tom | 2 | T10, T16 | — |
| M13 | skriv artefaktet også uten reviewer | 3 | T11 | **ja**`test_a5_per_approach_artifacts`, `test_parse_failure_capture` |
| M14 | la en `approve` mynte en `Verdict` | 1 | T12 alene | — |
| M15 | les EOF som approve | 2 | T16-unit, T16-CLI | — |
| M16 | fang ikke `ProposalReviewInputError` ved navn | 1 | T16-CLI alene | — |
| M17 | enhver ikke-revise-linje er approve | 1 | T15 alene | — |
| M18 | drop `single_only`-raden | 1 | T17 alene | — |
| M19 | drop `report_forbidden`-raden | 1 | T18 alene | — |
| M20 | bibliotekssømmen wiret, argparse-flagget borte | 9 | T13, T16, T17 … | — |
| M21 | strømmene bundet ved konstruksjon | 1 | T20 alene | — |
| M22 | slett den hostede navngitte nekten | 1 | T21 alene | — |
| M23 | videresend det hostede feltet i stedet for å nekte | 4 | T21, partisjons-armen | **ja** — Fase-4e-armene i `test_explore_callsites` + `test_hosting` |
| M24 | fersk reviewer per base i dispatcheren | 1 | T22 alene | — |
| M25 | render kandidaten som repr | 2 | T14, T13 | — |
| M26a | trunker feedback til 20 tegn | 5 | T1, T3b, T8 … | — |
| M26b | trunker feedback til 40 tegn (sentinelen OVERLEVER) | 3 | T1, T8, T13 | — |
| M27 | fall tilbake til siste validerte forslag (D6-alternativ b) | 1 | T13-loop alene | — |
| M28 | drop `verdict_key` fra posten | 2 | payload-armen, run-payload-armen | — |
| **M29** | **reverter exit-kontrakten til `assert last is not None`** | **0** | **— (se § 1)** | — |
| M30 | drop `--live-dry-run`-nekten | 1 | dry-run-armen alene | — |
| M31 | drop `--proposals-from-mandate`-nekten | 1 | pfm-armen alene | — |
| M32 | notice-rendereren skriver linja uten reviewer | 2 | notice-armen, CLI-kontrollen | — |
| M33 | notice-rendereren returnerer `None` på null | 3 | notice-armen, null-armen, CLI-null-armen | — |
| M34 | feedback løftet ut av per-approach-kallet (krysser tilnærminger) | 5 | T1, T7, T5-run … | — |
| M35 | bygg posten fra en RETURNERT liste i stedet for kaller-eid sink | 1 | T9 alene | — |
| M36 | dispatcheren slutter å tråde revieweren | 1 | T22 alene | — |
| M37 | detach CLI-utskriften mens rendereren fortsatt returnerer linja | 2 | T13, CLI-null-armen | — |
| M38 | `attempts_remaining` regnet fra `max_attempts` alene | 2 | T3a, T5 | — |
| M39 | drop `--checkpoint-dir`-nekten | 1 | park-armen alene | — |
| M40 | `honoured=True` satt ved revise-tid i stedet for etter hentingen | 2 | T3b, T9 | — |
**Sum: 41 kjøringer (40 mutasjons-ID-er), 40 røde, 1 grønn (M29).**
### Tre signaturer verdt å lese
* **M13 og M23 er røde i tester som er ELDRE enn dette arbeidet.** «Skriv artefaktet iff en reviewer
ble gitt» (D4) holdes av A5-ens eksakte fire-navns-listing og av parse-fangstens kontroll; den
hostede partisjonen holdes av Fase-4es to asserts. Sømmene er altså gatet av uavhengige vitner,
ikke bare av sine egne armer.
* **M38 er rød i TO armer, og begge NAVNGIR den.** T3a («no exception, exactly two calls»
under `max_rounds=2`/`max_attempts=10`) og T5 (terminalen viser `attempts_remaining == 0` under
`max_rounds=1`/`max_attempts=3`) er de to sidene av den samme ledger-bevisste `min(...)`: den
ene måler at løkka STOPPER der boka slutter, den andre at TALLET operatøren leser sier det
samme. Det ble VERIFISERT i docstringene, ikke antatt ut fra at armene lå i samme fil.
* **M26a og M26b har ULIKE signaturer, og det var ikke gitt.** Ved 40 tegn overlever
`PROPOSAL-REVIEW-SENTINEL-9c41ae` (31 tegn) i de armene hvis feedback er kortere enn taket, så
T3b og T9 blir grønne igjen mens T1, T8 og T13 — de med lengre feedback — forblir røde. Planens
to-lengde-skjerping virker altså etter hensikten her.
---
## 3. K2-omkjøringen — kriterium 8
Samme base, samme syntetiske prisfikstur og samme instrument som S7b § 3 og S2c § 3:
`scratchpad/s7b-syretest/K2-priset-SYNTETISK` (630 konseptfiler, erklært id `k2-trinn1-20260903`
mot monteringsnavnet — S7a-3-slakken fyrer, og kjøringen sier det). K2-basen er **utracket og
finnes kun på denne maskinen**, så kriteriet er verifiserbart her og rapporteres; det asserteres
aldri av suiten.
**Instrumentet er validert mot en KJENT POSITIV FØR bruk:** rotnivå-listingen på LEVERT K2 måles
til **3 954 tegn / 1 495 o200k-tokens over 629 konsepter**, som reproduserer S7a-3s publiserte tall
eksakt. Uten den kontrollen ville et lavt ETTER-tall like gjerne betydd at sonden var i stykker
(MAJOR-3s falske null).
### 3.1 De to kjøringene
Manuset (`scratchpad/major2/scripted-replies-major2.json`) gir proposeren FIRE steg — to
tilnærminger × to forsøk, der annethvert oppgir **150 000** i stedet for **200 000**. Manus-
posisjonen er delt over kjøringen, ikke per tilnærming (målt), så kontrollen og den reviderte
kjøringen leser den samme lista fra hver sin ende.
```bash
# KONTROLL — samme flagg, samme manus, bare et annet SVAR
printf 'approve\napprove\n' | uv run portfolio-optimiser K2 \
--docs-dir scratchpad/s7b-syretest/K2-priset-SYNTETISK \
--bundle-dir scratchpad/s7b-syretest/K2-priset-SYNTETISK \
--derive-cost-baseline \
--explore "Finn kostnadsbesparelser i Stange skole-anbudet" \
--explore-config scratchpad/s7b-syretest/explore-config.json \
--scripted-replies scratchpad/major2/scripted-replies-major2.json \
--outbox-dir scratchpad/major2/outbox-control --run-id major2-control \
--proposal-review
# REVIDERT — to revise, to approve
printf 'revise Bruk 150 000, ikke 200 000\napprove\nrevise Bruk 150 000, ikke 200 000\napprove\n' | ...
```
**Kontrollen er approve-only, ikke «uten flagget».** Det isolerer SVARET: begge kjøringene har
døra, samme manus og samme budsjett, så alt som skiller dem er hva mennesket sa.
### 3.2 Utfallet flytter seg
| | KONTROLL (approve ×2) | REVIDERT (revise/approve ×2) |
|---|---|---|
| review-spørsmål | 2 over 2 kandidater | **4 over 2 kandidater** |
| `hypothesis-1` | VALIDERT 200 000 NOK | **VALIDERT 150 000 NOK** |
| `own-proposal` | VALIDERT 150 000 NOK | VALIDERT 150 000 NOK |
| kjøringens utfall | best **200 000** NOK | best **150 000** NOK |
| dom-nøkkel | `be8535e204cdc4c6` | **`f23ecff85f4188b5`** |
| `attempts remaining` vist | 2 | 2, så **1** på forsøk to |
Begge kjøringene ender rc 0 med `ValidatedProposal`. Validatorens dom er ikke overstyrt av noen —
det er en ANNEN kandidat som ble dømt, fordi mennesket kjøpte ett forsøk til.
### 3.3 Prisen: én ekstra genererings-prompt per tilnærming
| fase | prompter (kontroll) | tokens | prompter (revidert) | tokens |
|---|---|---|---|---|
| 1 — utforskning (navigatør 5, manager 6, hypotesiser 1) | 12 | 18 355 | 12 | **18 355** |
| 2a — debatt (proposer 2, checker 1) | 3 | 513 | 3 | **513** |
| 2b — generering | 2 | 406 | **4** | 908 |
| **sum** | **17** | **19 274** | **19** | **19 776** |
**Utforskningen er uendret til tokenet**, og debatt-promptene er uendret både i antall og i
størrelse (proposer 314, checker 199 i begge). Hele forskjellen er de **to ekstra
genererings-promptene** — én per tilnærming, nøyaktig dét en `revise` kjøper. **+502 tokens, +2,6 %**
for en kjøring der et menneske faktisk endret utfallet.
Klassifiseringen debatt/generering er MÅLT, ikke antatt: genererings-prompten bærer
`Project: {id} - {name}` (`generate._build_messages`), debatt-turen bærer basepekeren.
### 3.4 Ordene når fram — ORDRETT
`Bruk 150 000, ikke 200 000` står i **2 av 6** proposer-prompter i den reviderte kjøringen og i
**0 av 4** i kontrollen — altså nøyaktig i de to andre-forsøks-promptene, og ingen andre steder.
`scratchpad/major2/outbox-revise/major2-revise-proposal-reviews.json`:
```json
{"approach_id": "hypothesis-1", "attempt": 0, "decision": "revise",
"feedback": "Bruk 150 000, ikke 200 000", "honoured": true,
"p50": 319311.8986090132, "verdict_key": "be8535e204cdc4c6"}
{"approach_id": "hypothesis-1", "attempt": 1, "decision": "approve", "feedback": "",
"honoured": true, "p50": 319311.8986090132, "verdict_key": "f23ecff85f4188b5"}
```
Posten er nøklet per `(approach_id, attempt)` og bærer `verdict_key` for den kandidaten mennesket
faktisk SÅ — attempt 0 navngir 200 000-kandidaten, attempt 1 den kjøringen endte på. Kontrollens
artefakt har to rader, begge `approve` med tom feedback: **fila skrives uansett, fordi en reviewer
ble gitt** (D4).
**Ærlighets-grense på nettopp dette manuset:** den skriptede proposeren LESER ikke feedbacken — den
returnerer steg 2 uansett. Det som er målt her er derfor (a) at ordene når prompten ordrett, (b) at
et forsøk til faktisk ble kjøpt og hentet (`honoured: true`), og (c) at kjøringen bærer validatorens
dom på DET forsøket. At en levende modell ville korrigert seg etter ordene er ikke vist, og kan
ikke vises av et manus.
## 4. Ærlighets-grenser
* **`last_ruling`-bæreren er uvitnet** — se § 1. Planens «Critical risk #1» er falsifisert.
Kommentaren i `generate.py` som påsto at asserten ville fyrt er RETTET i samme commit som dette
dokumentet; commit-teksten for `bbf4d3b` bærer fortsatt den gamle påstanden, og historikken
skrives ikke om.
* **Steg 5 under-rapporterer på en revidert kjøring:** en `revise` bruker ett av de SAMME
`max_attempts`-forsøkene en validator-avvisning ville brukt, så en kjøring kan bruke opp forsøkene
på ekspert-revisjoner og aldri nå en andre validator-falsifisering. `refinements` under-rapporterer
da ved konstruksjon. Uttalt, ikke reparert.
* **`coverage`-radene navngir ikke en revise som årsak.** Hver revise tikker den DELTE rundeboka, så
en eksperts egne revisjoner kan la halen av en kommisjon stå `not_evaluated`. Detaljen forblir
«budget exhausted before this approach was evaluated» — sann, og `coverage` er et non-goal.
Terminalen viser `attempts remaining` FØR hvert svar, og review-posten er der årsaken kan leses.
* **Checker-gaten (6b) og dimensjons-gaten (6c) overstyrer ETTER at genereringen har returnert.** En
`approve` registrert ordrett i artefaktet kan derfor sitte på en kjøring hvis utfall er en
checker-kildet `Rejection`. Posten sier hva mennesket SÅ og SVARTE, aldri hva kjøringen
konkluderte; at de to er uenige er tilsiktet, og å blande dem er nøyaktig det to-falsifiserer-raden
forbyr.
* **Feedback-teksten er UBEGRENSET**, som plan-review-feedback — belastet `max_tokens`, som er
bunden. At den når en proposer som holder navigatør-verktøyene er uttalt, ikke voktet.
* **F4s traceback-asymmetri er urørt:** `PlanReviewInputError` forlater fortsatt CLI-en som en
traceback, mens `ProposalReviewInputError` fanges ved navn. Uttalt, ikke fikset her.
* **`run_portfolio` tar ingen reviewer**, og fraværet er assertert.
* **Ingen levende modell har svart på en review.** Alt her er skriptede stand-ins
(structured-output-grensens klasse).
* **K2-prisene er SYNTETISKE**, og manuset er skriptet: null modellkall, banneret sier det på hver
kjøring. Det som er vist er at maskineriet kommer HELT fram med et menneske i løkka, ikke hva et
tilbud koster. Multiplikatorene i § 3.3 gjelder dette manuset.
* **README-armen har intet M-nummer** — M-lista lukket ved M40. Den ble drevet rød to ganger
(manglende blokk, og blokk uten `--checkpoint-dir` navngitt), og kontrollen på at uttrekkeren
ikke stille finner ingenting er `--plan-review`-blokka.