docs(s7a): syretesten maalt paa K2 - navigasjonen holder, tre krav staar igjen

MAALING, ingen produksjonskode. Instrumentet validert mot publisert fasit FOER
bruk (tunnel read_bundle 259 tok, bundle_context 12 595 tok - begge eksakt).

Tre premisser felt av maalingen i stedet for omgaatt:
- ordrens "ny profil med adjudication": 0 av 39 konsepter baerer noekkelen
  (kjent-positiv kontroll: 39 av 39 baerer ^type). Ingen verified/sources heller.
- ordrens punkt 4 ber om et konsept med adjudication: proposed - det finnes ikke.
  K5-terskelen er derfor kjoert paa det korpuset faktisk baerer, mot en patchet
  kopi som kontroll: admits_falsification flipper False -> True, saa "alle 39
  diskontert" er en maaling av korpuset og ikke av en doed funksjon.
- mandat-diffen foer/etter revise er TOM ved konstruksjon (hypotesiseren er et
  konstant manus). Maalt i stedet: kjeden i tre ledd. Operatoerens tekst naar
  manageren (6 av 8 kall), aldri deltakerne direkte (0 av 4 / 0 av 1); den naar
  hypotesiseren KUN via managerens egen instruction_or_question (1 av 1,
  diskriminerende sentinel), aldri via plan-teksten. Ledd 3 er ikke maalbar
  offline og er rapportert som det.

Maalt ellers: 39 konsepter, 0 ufulgte lenker, bundle_id mount-derived (foerste i
naturen). list_bundles 127 tok, read_bundle 2 312 tok, bundle_context 682 303 -
MAJOR-3 er det som gjoer K2 navigerbar i det hele tatt, ikke bare billigere.
tool_calls: list_bundles -> read_bundle -> read_file, i rekkefoelge (plan SS 5s
dialog-rad oppfylt). Ingen "object at" noe sted (BLOCKER-1 lukket paa begge
doerene); current_progress sier "(no progress ledger yet)" (PM-tillegg 4 lukket).
Tokenprofil 45 643 o200k over 13 prompter; de tre stoerste postene er 54 % og
deler aarsak: navigatoerens read_file rir med i hver senere prompt.

Tre krav for S7b, i den rekkefoelgen de blokkerer:
1. bundelen mangler validator-input.json - kjoeringen nekter etter utforskningen
   uansett priser. Kryss-repo mot llm-ingestion-okf.
2. MAJOR-4 nekter mot det ekte prisskjemaet (ingen tabell med alle tre roller;
   "Enhetspris" forekommer 0 ganger; eneste prisede rad er post 82 = 5 647 500).
   Kontroll: syntetisk fixture gir 3 kostlinjer, saa nekten er K2s egenskap.
3. adjudication mangler i hele korpuset. Kryss-repo, samme klasse som 1.

Levende kjoering IKKE gjort - alle fem env-variabler tomme, ingen model_map.
Kontroll: 1230 passed / 5 skipped uendret, golden ea8c534 byte-uendret, ruff ren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-03 03:38:52 +02:00
commit 73827a854a

View file

@ -0,0 +1,359 @@
# Syretest S7a — K2 → kunnskapsbase → utforskning → revise
**Dato:** 2026-09-03 · **Ordre:** `20260902T233842Z-2955810469-from-.claude` ·
**Grunnlag:** `~/.claude/docs/helhetlig-plan-okf-og-portfolio-optimiser.md` § 0, § 5, § 9.1
Dette er en **måling**. Ingen produksjonskode er endret; hele måleoppsettet ligger utenfor treet
(`scratchpad/s7a/`, utracket) og hver tabell under er produsert av en kommando som står i teksten.
**Ingen validert besparelse er forventet eller funnet** — K2 er pre-award og prisskjemaet er
uutfylt (plan § 9.1). Det som måles er navigasjonen, dialogen, revise-sløyfa, falsifiseringen mot
proveniens og tokenprofilen.
---
## 0. Instrumentet, validert mot en kjent positiv FØR bruk
Alle tokentall er `tiktoken` `o200k_base` over den fulle prompt-blobben (tekst + `function_call` +
`function_result`), kjørt med `uv run --with tiktoken``tiktoken` er fortsatt ikke en
prosjektavhengighet.
| Kjent-positiv sjekk | Publisert fasit | Målt nå |
|---|---|---|
| `read_bundle` over `tunnel-hauglia` | 259 tok (`docs/2026-09-02-read-bundle-kontekstkostnad.md`) | **259 tok** |
| `bundle_context` over `tunnel-hauglia` | 12 595 tok (samme) | **12 595 tok** |
Instrumentet reproduserer begge eksakt. Først etter det er K2-tallene under lest som tall.
En andre instrumentfeil ble fanget underveis og er verdt å skrive ned, fordi den er samme klasse
som misjonsreview v2 rapporterte: første versjon av prompt-sonden leste `repr(Content)` og fikk
`<agent_framework._types.Content object at 0x…>` for **hver** melding, altså en prompt som så tom ut
mens den bar 108 000 tegn. Sonden henter nå `text`/`result`/`arguments` ut av hvert `Content`.
---
## 1. Kunnskapsbasen slik den faktisk er levert
Bygget av `llm-ingestion-okf` 02.09 (`tools/okf_corpus_run.py`, steg 17) til `/tmp/k2-trinn1-bundle`;
kopiert til `scratchpad/s7a/k2-bundle` og målt der (`/tmp` er flyktig).
| Egenskap | Målt |
|---|---|
| Konseptfiler (`context_files`) | **39** |
| Filer totalt (`files`) | 40 (39 + `index.md`) |
| `type: verdict`-lag | 0 |
| Ufulgte kryss-lenker (`skipped`) | **0** |
| `bundle_id` / opphav | `k2-bundle` / **`mount-derived`** |
| `index.md`-body | 6 044 tegn, ingen frontmatter, ren lenkeliste |
| `log.md` (OKF § 7) | **finnes ikke** |
**Ordrens `N=43, merged 39` er delvis verifiserbart.** `merged = 39` er målt direkte (39
konseptfiler). `N = 43` er produsentens tall og kan **ikke** verifiseres fra bundelen: den bærer
ingen `log.md` og ingen manifest-oppføring som teller inn-dokumentene. Det står som produsentens
påstand, ikke som en måling gjort her.
### `adjudication`-nøkkelen: fraværende, med nevner
```
cd scratchpad/s7a/k2-bundle && grep -l '^adjudication:' *.md | wc -l # 0
grep -l '^type:' *.md | wc -l # 39 (kjent-positiv kontroll)
```
Frontmatter-folketellingen over alle 39, med sifre i nøkkelnavnet tatt med:
| Nøkkel | Antall (av 39) |
|---|---|
| `type`, `title`, `source_file`, `source_sha256`, `ingested_at`, `generated`, `derived` | 39 |
| `references` | 20 |
| **`adjudication`** | **0** |
| **`verified`** | **0** |
| **`sources`** | **0** |
**Ordrens premiss om «den nye profilen (`adjudication`)» holder ikke mot det som ble levert.**
Bundelen bærer ingen `adjudication`-nøkkel og ingen provenienss-nøkkel i det hele tatt. Det er et
faktum om bundelen, ikke om leseren — se § 4, der gaten bevises å kunne si ja.
### Navigasjonskostnad, per kall
| Kall | Tegn | o200k-tokens |
|---|---|---|
| `list_bundles` (1 base) | 352 | **127** |
| `read_bundle("k2-bundle")` | 6 244 | **2 312** (39 oppføringer) |
| `read_file` × 39, sum | — | **684 236** |
| `read_file`, median / min / maks | — | 6 709 / 541 / **121 462** |
| `bundle_context` (formen før MAJOR-3) | — | **682 303** |
Tre ting følger av tabellen.
**(a) MAJOR-3 er det som gjør K2 navigerbar i det hele tatt.** Den gamle `read_bundle`-kroppen
returnerte `bundle_context` — 682 303 tokens for denne basen, altså mer enn noe kontekstvindu
tar, og den ville ridd med i hver senere prompt. Den nye formen koster 2 312. Reduksjonen er
**99,7 %**, og forskjellen her er ikke «billigere», den er «mulig mot umulig».
**(b) Katalogtaket holder på et 39-dokuments korpus.** `list_bundles` koster 127 tokens, og
`index_truncated` kommer tilbake `true` — de 6 044 tegnene i indeksen blir til et 200-tegns
utdrag. Det bekrefter det `docs/2026-08-26-katalogkostnaden.md` forutså for maskin-importerte
baser: utdraget er nesten innholdsløst (første lenkelinje), men det er *bundet*, og hele indeksen
er ett `read_file(id, "index.md")` unna.
**(c) `read_bundle` er O(dokumenter), og på 39 dokumenter er det 2 312 tokens.**
`tests/test_read_bundle_cost_loadbearing.py` setter taket til 1 500 **tegn** mot 3-dokuments
baser. K2 ligger på 6 244 tegn. Det er ikke et brudd på gaten — gaten måler sine egne baser — men
det er den ærlige lesningen av hva taket er: en per-base-grense som skalerer med dokumentantallet,
ikke en absolutt grense.
**(d) To dokumenter sprenger § 5s tokenkriterium hvis de leses hele.** Kriteriet er «ingen
enkeltpost > 100 000 tok uten en navngitt grunn». To av 39 passerer det som `read_file`:
| Dokument | Tokens |
|---|---|
| `inbox-del-ii-bilag-3-1-milj-teknisk-rapport-med-tiltaksplan.md` | 121 462 |
| `inbox-del-ii-bilag-9-1-avtale-som-tiltransporteres-norconsult-as.md` | 119 909 |
Grunnen er navngitt her: begge er enkeltdokumenter fra korpuset som ikke er delt opp i konsepter.
De ble **ikke** lest i kjøringene under — men en navigatør som velger dem, betaler det.
---
## 2. Dialogen: én offline utforskning med verktøyspor
```bash
PYTHONIOENCODING=utf-8 uv run python -m portfolio_optimiser.run K2-STANGE \
--docs-dir scratchpad/s7a/k2-bundle --bundle-dir scratchpad/s7a/k2-bundle \
--explore "Finn kostnadsbesparelser i Stange skole-anbudet" \
--explore-config scratchpad/s7a/explore-config.json \
--scripted-replies scratchpad/s7a/scripted-replies.json \
--outbox-dir scratchpad/s7a/outbox --run-id s7a-offline
```
MAJOR-1s trinn-manus er brukt: navigatøren får `{"call": …}`-steg, manageren én tekst per stadium
(fakta, plan, tre ledgere, sluttsvar). Uten manager-manuset avslutter kjøringen etter én runde uten
å gi noen deltaker en tur — det er MAJOR-1s egen måling, og den er grunnen til at manageren er
skriptet i stadier.
`{run_id}-exploration.json`:
| Felt | Målt |
|---|---|
| `completed` | `true` |
| `stop` | `null` |
| `tool_calls` | **`list_bundles``read_bundle(k2-bundle)``read_file(k2-bundle)`**, i kallrekkefølge |
| `rounds` | 3, alle med `speaker_known: true` og en ekte deltaker som `next_speaker` |
| `approaches` (i mandatet) | 1 — `hypothesis-1 — SENTINEL-ALFA prisskjema-post 82` |
**Plan § 5s dialog-rad er oppfylt og målt:** ≥ 1 `read_bundle`/`read_file`-kall er logget i
artefaktet. Kjøringen er ikke-vakuøs i den forstand MAJOR-1 definerte: tre runder, kjent taler
hver gang, tre verktøykall, ett mandat.
### Kjøringen ender likevel rc = 1, og det er S7bs skarpeste funn
```
run refused: IR projection not found in bundle: 'validator-input.json'
```
Utforskningen fullførte, mandatet ble kunngjort — og så nektet pipelinen. Bundelen bærer ingen
`validator-input.json`, så `run_project`s bundle-arm kan ikke slå opp prosjektet. Artefaktet
overlevde fordi det skrives fra en `finally`, som er nøyaktig hva den konstruksjonen finnes for.
Dette er et **mer spesifikt** S7b-gap enn «K2 har ingen priser»: selv med priser ville kjøringen
stoppet her. Ingesterings-profilen emitterer ingen IR-projeksjon. **Kryss-repo-funn for
`llm-ingestion-okf`.**
### Levende kjøring: ikke kjørt
Ordrens escape-klausul fyrer. Målt, ikke antatt:
| Variabel / fil | Tilstand |
|---|---|
| `PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT` | tom/usatt |
| `FOUNDRY_PROJECT_ENDPOINT` | tom/usatt |
| `PORTFOLIO_MODEL_MAP` | tom/usatt |
| `OPENAI_API_KEY` / `AZURE_OPENAI_API_KEY` | tom/usatt |
| `data/model_map.json` | finnes ikke |
`az account show` svarer riktignok med en konto, men Fase 4e målte at AZURE-armen slår opp et
deployment-navn i modell-mappet **før** noen klient bygges — uten mappet er en innlogget CLI
ikke nok. **Levende kjøring ikke gjort, mangler nøkkel og modell-map.** Ingen konflikt med
STATE-ens `IKKE RØR AZURE` oppsto.
---
## 3. Revise-sløyfa
```bash
printf 'revise SENTINEL-BRAVO: …\napprove\n' | uv run python -m portfolio_optimiser.run K2-STANGE \
… --explore-config scratchpad/s7a/explore-config-review.json --plan-review \
--outbox-dir scratchpad/s7a/outbox-review --run-id s7a-review
```
| Egenskap | Målt |
|---|---|
| Plan-reviewer stilt | **2** (`#1` besvart `revise`, `#2` besvart `approve`) |
| Manageren replanla mellom dem | ja — plan `#1` 1 115 tegn, plan `#2` 1 132 tegn, ulik tekst |
| Tilbakemelding i artefaktet | **ordrett**, `SENTINEL-BRAVO: …` |
| `"object at"` i artefakt eller stdout | **0 forekomster** (BLOCKER-1 bekreftet lukket på begge dørene) |
| `current_progress` | `"(no progress ledger yet)"` — ikke ordet `None` (PM-tillegg 4 bekreftet lukket) |
### Mandat-diffen er TOM, og grunnen er instrumentet — ikke systemet
Mandatet bygges av hypotesiserens `HYPOTHESIS:`-merkede turer. Under et **konstant** manus sier
hypotesiseren det samme uansett hva revisjonen sa, så før/etter er identiske ved konstruksjon. Å
rapportere den tomme diffen som et funn ville vært å lese et instrumentresultat som et faktum om
verden. Det som *kan* måles offline er **kjeden**: hvilke prompter bærer operatørens
tilbakemelding? Med to ASCII-sentineler (norske tegn serialiseres escaped i prompten og ville gitt
falske nuller — samme felle misjonsreview v2 rapporterte):
| Ledd | Bærer | Målt |
|---|---|---|
| 1. Operatørens revise-tekst → managerens prompt | `SENTINEL-BRAVO` | **6 av 8** manager-kall |
| 1b. Samme tekst → deltakernes prompt | `SENTINEL-BRAVO` | **0 av 4** navigatør, **0 av 1** hypotesiser |
| 2. Managerens post-revise-output → hypotesiserens prompt | `SENTINEL-CHARLIE` i `instruction_or_question` | **1 av 1** |
| 2b. Samme, via plan-TEKSTEN | revidert plan-streng | **0 av 1** hypotesiser, **0 av 4** navigatør |
| 3. Endret hypotese ut av deltakeren | — | **ikke målbar offline** |
**Mekanismen, uttalt:** operatørens tilbakemelding når **manageren**, aldri deltakerne direkte.
Den når deltakerne bare gjennom det manageren selv skriver i neste rundes
`instruction_or_question` — og det er bevist diskriminerende: sentinelen manageren skrev *etter*
revisjonen står i hypotesiserens prompt, mens plan-teksten (verken den opprinnelige eller den
reviderte) gjør det ikke.
Sløyfa er altså hel, men leddet som faktisk former hypotesen er managerens **omskriving** av
tilbakemeldingen. Under et skriptet manus er den omskrivingen min, ikke en modells. **At en revise
endrer hva hypotesiseren foreslår er derfor ikke bevist her, og kan ikke bevises offline** — det
krever en levende manager. Det er S7bs andre åpne krav.
---
## 4. Falsifisering mot proveniens (K5-terskelen)
Ordren ber om et K2-konsept med `adjudication: proposed`. **Det finnes ikke: 0 av 39.** Terskelen
er derfor kjørt på det korpuset faktisk bærer, mot en påstand om den ene prisede posten:
> «Post 82 *Prosjektering (tiltransport av prosjekterende)* i Bilag 7 Prisskjema er priset til
> 5 647 500 NOK og er derfor en kandidat for kostnadskutt.»
| | K2 slik den er levert | Kjent-positiv kontroll (patchet kopi) |
|---|---|---|
| `adjudication_for` | `unknown` | `adjudicated` |
| `(state, reason, items_seen)` | `(absent, None, 0)` | `(present, None, 1)` |
| `trust_tier` | `None` | `human-reviewed` |
| `admits_falsification` | **`False`** | **`True`** |
Over alle 39, med nevner: `adjudication` `unknown` 39/39 · `evidence.state` `absent` 39/39 ·
`tier` `None` 39/39 · `admits_falsification` `False` **39/39**.
**Kontrollen er bærende.** Uten den er «alle 39 diskontert» ikke til å skille fra «funksjonen
diskonterer alltid» — repoets vakuøs-gate-klasse. Kontrollen er en kopi under
`scratchpad/s7a/k2-control/` der ett konsept har fått
`verified: { by: human:…, at: … }` + `adjudication: adjudicated`; gaten flipper `False → True`.
Den leverte bundelen er aldri skrevet til.
**Dommen, slik skillen krever den:** `undecided`, aldri `survived`. Diskonteringen sies eksplisitt
med trippelen, ordrett fra `evidence_notice`:
```
provenance absent in …/inbox-del-ii-bilag-7-prisskjema.md; items_seen=0
```
Og med skillens egen regel: en gjendrivelse som ikke navngir en gjendriver er ingen gjendrivelse —
her finnes ingen kilde som klarerer terskelen, så «vi kan ikke bekrefte» må aldri skrives om til
«påstanden er gal».
---
## 5. Tokenprofil
Ekte `usage` finnes ikke: `ScriptedChatClient` rapporterer 8 tokens per svar, så artefaktets
`tokens_spent` (88 offline, 104 med review) er **syntetisk** og sier ingenting om kostnad. Profilen
under er derfor instrument-målt over de faktiske prompt-blobbene i revise-kjøringen.
**13 prompter, 45 643 o200k-tokens.**
| Rolle | Prompter | Tokens | Andel |
|---|---|---|---|
| manager | 8 | 29 568 | 65 % |
| navigator | 4 | 9 392 | 21 % |
| hypothesiser | 1 | 6 683 | 15 % |
De tre største postene — til sammen 24 598 tokens, **54 % av hele kjøringen**:
| # | Post | Tokens |
|---|---|---|
| 1 | manager-kall 6 (ledger etter at navigatøren hadde lest prisskjemaet) | 8 572 |
| 2 | manager-kall 5 (samme, runden før) | 8 444 |
| 3 | manager-kall 7 (sluttsvar) | 7 582 |
**Det de tre har til felles er årsaken.** Alle tre bærer navigatørens `read_file`-resultat —
Bilag 7 Prisskjema, 100 695 tegn / 4 043 tokens — fordi utforskningens deltakere deler **én**
samtalehistorikk. Ett `read_file` betales altså av hver senere prompt i kjøringen, som er nøyaktig
mekanismen `docs/2026-09-02-read-bundle-kontekstkostnad.md` beskrev for `bundle_context`, ett trinn
ned på stigen. Ingen enkeltpost passerer § 5s 100 000-tak i denne kjøringen; de to dokumentene som
*ville* gjort det (§ 1d) ble ikke åpnet.
---
## 6. Hva som mangler for S7b (validert besparelse)
Tre krav, alle målt her, i den rekkefølgen de blokkerer:
**1. Kunnskapsbasen mangler en IR-projeksjon.** `run_project`s bundle-arm nekter med
`IR projection not found in bundle: 'validator-input.json'` (§ 2). Uten den stopper kjøringen etter
utforskningen uansett hva prisskjemaet inneholder. Ingesterings-profilen emitterer den ikke.
**Kryss-repo-krav mot `llm-ingestion-okf`** — ingen brief i dette repoet kan lukke det.
**2. Prisskjemaet er uutfylt, og MAJOR-4 nekter — korrekt.** Kjørt mot det ekte K2-prisskjemaet:
```
CostBaselineDerivationError: no cost table found in bundle 'scratchpad/s7a/k2-bundle':
no concept file carries a markdown table whose header names all three of
['code', 'quantity', 'unit_cost']
```
Kontroll: samme funksjon mot `tests/fixtures/k2-prisskjema-SYNTETISK` gir **3 kostlinjer**, så
avlederen virker — nekten er en egenskap ved K2, ikke ved koden.
Formen er målt presist: K2s Bilag 7 er **ett** ark (`## Prissammenstilling`), en pandoc SIMPLE table
med kolonneparet `Post … SUM`. Ordet `Enhetspris` forekommer **0 ganger** i hele filen, og
`Mengde`/`Timepris` står bare i prosa — de tilhørende cellene er tomme, fordi totalentreprenøren
skal fylle dem. Den **eneste** prisede raden i hele skjemaet er
`82 Prosjektering (tiltransport av prosjekterende) = 5 647 500`.
S7b trenger altså den syntetisk utfylte kopien plan § 9.1 anbefaler. MAJOR-4 har allerede formen:
`okf.derive_cost_baseline` (eier B) og fixturparet
`tests/fixtures/k2-prisskjema-SYNTETISK` / `…-uprisert-SYNTETISK`, med
`--derive-cost-baseline` som CLI-døra.
**3. `adjudication` mangler i hele korpuset (0/39).** K5-terskelen diskonterer derfor hvert konsept
(§ 4). En validert besparelse kan formelt sett fortsatt produseres — terskelen gjelder
falsifiseringsdommen, ikke validatoren — men den kan ikke *hvile* på noe i basen. **Kryss-repo-krav
mot `llm-ingestion-okf`**, samme klasse som punkt 1.
Ett krav er **ikke** blokkerende og bør ikke telles med: at en revise endrer hypotesen (§ 3). Det
krever en levende manager, ikke en endring i noe av dette.
---
## 7. Verifiseringslogg
| # | Påstand | Kommando → resultat |
|---|---|---|
| 1 | Instrumentet reproduserer publisert fasit | `uv run --with tiktoken python scratchpad/s7a/m1_navigation.py` → tunnel `read_bundle` 259, `bundle_context` 12 595 |
| 2 | 39 konsepter, 0 `adjudication`, kjent-positiv 39 `type` | `grep -l '^adjudication:' *.md \| wc -l` → 0; `grep -l '^type:'` → 39 |
| 3 | `N=43` ikke verifiserbart | ingen `log.md` i bundelen (`Path(base)/'log.md').exists()``False`) |
| 4 | Navigasjonstall | samme skript → § 1-tabellen |
| 5 | Verktøysporet | `outbox/s7a-offline-exploration.json``tool_calls` = 3, i rekkefølge |
| 6 | Pipelinen nekter på manglende IR | stderr → `IR projection not found in bundle` |
| 7 | Levende ikke kjørbar | alle fem env-variabler tomme; `data/model_map.json` finnes ikke |
| 8 | Revise → replan → nytt spørsmål | `outbox-review/s7a-review-exploration.json` → 2 `plan_reviews`, ulik `plan` |
| 9 | Ingen `object at` | `grep -c 'object at'` over artefakter + stdout → 0 |
| 10 | Kjeden, tre ledd | `uv run --with tiktoken python scratchpad/s7a/m3_revise_chain.py` → § 3-tabellen |
| 11 | K5 diskriminerer | `uv run python scratchpad/s7a/m4b_falsification.py``False` vs `True` |
| 12 | MAJOR-4 nekter mot ekte K2, godtar syntetisk | `uv run python scratchpad/s7a/m6_derive.py` → feilmelding vs 3 kostlinjer |
| 13 | Tokenprofil | samme skript som 10 → § 5-tabellene |
**Ikke verifisert her:** at en levende modell kaller verktøyene eller velger godt (samme grense som
structured-output-raden); at `N=43` stemmer; at leverandørens caching treffer prefiksene; K2s
Prisskjema slik en utfylt versjon faktisk ville sett ut (fixturen er syntetisk).
**Premisser som ble felt av målingen, og som er rapportert i stedet for omgått:** ordrens
«ny profil med `adjudication`» (0/39), ordrens punkt 4 (ingen `proposed`-konsept finnes), og
antakelsen om at mandat-diffen ville vise revisjonens virkning (tom ved konstruksjon).