This commit is contained in:
Kjell Tore Guttormsen 2026-07-31 18:37:48 +02:00
commit 2d91943fab
29 changed files with 2712 additions and 21 deletions

8
shared/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
# Operational continuity state — LOCAL-ONLY, never committed.
# Rationale: commons is subtree-consumed (a committed root STATE.md would land in every
# consumer's shared/STATE.md) and is intended for open publication (STATE must never reach
# a public mirror). The guard roll-up greps STATE.md locally, so local-only is sufficient.
/STATE.md
# Local-scoped notes/overrides convention (KTG global): never mirrored.
*.local.md

View file

@ -20,6 +20,15 @@ so the only thing that differs is the agent framework itself.
bundle (OKF / LLM-wiki): one office building, one LED-retrofit measure, with a seed expert
verdict encoding the realization gap and a golden-suite of expected validator outcomes. A
small **dev fixture** for exercising the agentic loop; a realistic full-scale example comes later.
- [`examples/nav-golden-hierarchy/`](examples/nav-golden-hierarchy/) and
[`examples/nav-golden-escape/`](examples/nav-golden-escape/) — the **nav-golden** fixture class:
`bundle/` in, `expected-read-context.md` out, exercising the navigation contract (method-spec §3
Step 1) with one positive case (hierarchy, dedup, cycle termination, verdict exclusion) and one
negative case (`..` and root-relative escapes, so a gate built on them can actually go red). Each
case documents itself in its own `README.md`. **Informative listing only** — no normative file
refers to this class, and the comparison rule is deliberately not pinned (each case's README
leaves a gate free to compare byte-exact or after trailing-whitespace normalization), so these
are not ground truth in the sense method-spec §7 uses the word.
- [`skills/expert-reviewer/`](skills/expert-reviewer/) — the **expert-reviewer persona** as a
framework-neutral Agent Skill: a `SKILL.md` persona prompt (energy-advisor / M&V role + the
realization-gap methodology the validator cannot compute) and a canonical

View file

@ -0,0 +1,174 @@
# Adoption plan — `llm-ingestion-guard` at the ingest persist gate
> **Status:** plan only. This document decides *when* and *where* the write-time
> ingestion guard (`llm-ingestion-guard`, alpha — this plan pins no version; see
> §3.2 for the `allow_reserved` default, which the guard changed in v0.3.0) earns its place in the
> architecture this commons specifies. **It does NOT wire the guard in** — the
> executable wiring lands in the two reference implementations; this document is
> the framework-neutral contract they build to, same as [method-spec.md](../../method-spec.md)
> and [ingest-spec.md](../../ingest-spec.md).
>
> Source brief: `llm-ingestion-pipeline-security/docs/ADOPTION-BRIEF.md` (§7 checklist
> is the template used below). Guard repo: `git.fromaitochitta.com/open/llm-ingestion-pipeline-security`.
## 1. What this commons is (relevant framing)
`portfolio-optimiser-commons` is the **framework-neutral shared core** consumed as a
`shared/` git subtree by both reference implementations (MAF and Claude Agents SDK).
It contains **normative specs + fixtures, no executable pipeline.** Two specs matter here:
- **[ingest-spec.md](../../ingest-spec.md)** — a *deterministic, zero-model-call* step that
materializes real data sources into OKF bundles BEFORE the loop. Source types: `file`
(local CSV catalogue), `sql` (read-only SELECT), and `http` (an **OPTIONAL extension
point** — remote endpoint / MCP connector, §4).
- **[method-spec.md](../../method-spec.md)** — the 8-step agentic loop that *reads* the OKF
bundle, enriches via a proposer/checker model, gates numbers with a mandatory
deterministic validator, takes human expert verdicts, and **promotes approved verdicts
back into the bundle wiki** (the promotion gate, §6 / Step 8).
The bundle *is* an OKF second-brain a downstream run later reads as trusted context — the
exact shape the guard's brief targets.
## 2. §7 checklist, scored against this architecture
| §7 condition | Verdict for the commons-specified system |
|---|---|
| Persist LLM-enriched / externally-received content into a store a *downstream* agent reads as trusted | **Partial.** The **promotion gate** (method §6) lifts model-*proposed*, expert-*approved* verdicts into the OKF wiki; the next run's seeding fold reads them as trusted. Ingest materializes *raw source extracts***no LLM step at ingest time**. |
| **≥1 ingest path takes UNTRUSTED content** *(the decisive box)* | **Not on any path implemented today.** The two conformance-required sources — `file` and `sql` — are **first-party by origin** (the operator's own project files / database). The untrusted boundary is the **`http`/MCP source type** (ingest §4, an explicit not-yet-built extension point) and any future **received-external-OKF-bundle** merge (not yet specified). |
| An LLM step sits between the untrusted source and the store | Ingest: **no** (zero model calls, ingest §1). Loop: yes, but over *first-party* bundle context. |
| You want fail-secure (halt before persist) | **Yes, already the design ethos** — ingest is fail-fast/fail-closed throughout (§3 verdict-layer reservation, §4 manifest validation, §8 network opt-in). |
**Verdict: the decisive box is currently NO** — every live/required ingest path is
first-party. But the architecture has *designed, foreseeable* untrusted extension points
(`http`/MCP; received bundles). Per brief §7 ("note the guard as a dependency to add when —
not if — you open an external/inbox/received-bundle path"), the honest status is
**`planned`**, not `not-applicable`.
## 3. Untrusted boundaries (guard wires here) vs first-party paths (it does not)
### 3.1 Untrusted boundary A — the `http`/MCP connector (ingest §4 extension point)
When a connector fetches remote/vendor/web content, the extracted body is untrusted, and
ingest §5 persists it **verbatim inside a fenced code block** into a concept file the loop
later reads as trusted context. That verbatim persist of remote content into an agent-read
bundle is precisely the write-time boundary the guard defends.
**Minimal wiring (at ingest materialization, before writing `ingest-{id}.md`):** ingest
makes *no model call*, so only the **scan-before-persist half** of the contract applies
(brief §3 steps 1, 6, 7 — the fence-a-model-call half is N/A here):
```python
from llm_ingestion_guard import sanitize, scan_output, Disposition # primitives are exported per-piece
raw = connector.fetch(extraction) # untrusted remote body
cleaned = sanitize(raw) # strip carrier classes (zero-width/BIDI/tag/comment/data:)
decision = scan_output(cleaned) # lexicon + entropy + active-content, high-untrust
if decision.disposition is Disposition.FAIL_SECURE:
alert(gate_code=decision.reasons, run_id=run_id) # minimal payload, no content (brief §2/§3.8)
raise SystemExit # halt — never materialize this concept
# else: write the concept file exactly as ingest §5 specifies today
```
### 3.2 Untrusted boundary B — a received external OKF bundle (`okf.import_bundle`)
Distinct from the first-party `commons` subtree sync: if either implementation ever adds a
"merge a *foreign* OKF bundle" path, run the guard's OKF adapter at that persist gate.
```python
from llm_ingestion_guard.okf import import_bundle, Origin, Channel
result = import_bundle(bundle, origin=Origin.EXTERNAL, channel=Channel.AUTOMATIC)
for c in result.concepts:
if c.error: # FAIL_SECURE per concept: bad path, unsafe frontmatter, non-https resource
skip(c.path) # do not merge this concept
# then materialize the surviving concepts
```
This maps directly onto ingest-spec gates the commons *already* mandates — path
boundary-check (§4), **verdict-layer reservation (§3 — a received bundle MUST NOT inject
`type: verdict` files)**, and the index cross-link graph (§6). The adapter implements a
superset (frontmatter parse-safety, `resource` URL allowlist, dangling-link detection). Pick
`allow_reserved=True` for a whole received bundle (scan `index.md`/`log.md` bodies),
`allow_reserved=False` if the path ever materialises *individual* uploads.
### 3.3 First-party path — the promotion gate (method §6). **Do NOT wire the guard here.**
The promotion gate lifts model-proposed, expert-approved verdicts into the wiki. Its content
originates from the **first-party bundle context**, passes the **deterministic validator**,
and crosses only on **human/persona approval** (method §6 fail-closed). Per brief §7,
trusted-author approvals are **out of the guard's threat model by design**. Wiring the guard
here would trip on legitimate content and add no security the human + validator do not
already provide. *Recorded explicitly so a future session does not over-wire.*
### 3.4 Sharp edge — origin vs channel for `file`/`sql`
`file`/`sql` are first-party *channels*, but their *content origin* can be external (a
vendor's CSV, a subcontractor's report the operator ingests). Brief §7: **"trust follows the
data's origin, not the insertion channel."** A natural — but **not-yet-decided** — extension
is a per-source `origin: first-party | external` field in the ingest manifest; a source
declared external-origin would route its extracted text through the §3.1 scan-before-persist
gate. Flagged as an open design question, **not prescribed here** (avoid scope creep); it
keeps the decisive-box judgment honest.
## 4. When — roadmap placement
- **T0 (now):** first-party only (`file` + `sql`, curated bundles, first-party `commons`
subtree). Guard = **documented dependency, not wired.** Marker status `planned`.
- **Trigger A — `http`/MCP source type implemented** (in either reference impl): wire §3.1
at ingest materialization **before that path handles any real remote content**, and author
the normative gate as a new ingest-spec section (below) so both stacks wire it identically.
- **Trigger B — a received-external-OKF-bundle merge path is added:** wire §3.2
(`okf.import_bundle`) at that persist gate.
- **Trigger C (weaker) — a manifest per-source `origin: external` field is added:** route
external-origin `file`/`sql` extracts through the same §3.1 gate.
**Commons' specific role.** Because this is the normative shared core, the *contract* for the
untrusted-source gate ("untrusted extracted text MUST be sanitized + scanned before persist;
FAIL_SECURE halts materialization, never a silent commit") should be authored **here** — a
future ingest-spec section, added when Trigger A/B nears — so MAF and the Claude-SDK
implementation wire the identical guard. The executable wiring lives in each implementation
repo; the *rule* lives in commons, mirroring how method-spec/ingest-spec already work.
**Coordination with consuming implementation repos (recorded 2026-07-16).** The Claude
Agents SDK implementation (`claude-code-llm-wiki`) is running this same adoption task in
parallel and has drawn the reciprocal boundary: its `shared/` is a **pull-only subtree of
commons**, so guard wiring that touches `shared/` or the **ingest-spec** is owned by *this*
commons session, not there — its plan covers only its **repo-local modules** (`ingest.py`,
`verdicts.py`, `okf.py`). Division of labor, both directions:
- **Commons (here)** authors the normative untrusted-source *gate contract* (a future
ingest-spec section) and MUST NOT edit implementation-repo modules.
- **Each implementation repo** wires the guard in its own runtime modules and pulls the spec
via subtree; it MUST NOT edit the shared ingest-spec in place (edits land in commons first,
then `git subtree pull`, per README).
## 5. Honest limitations to carry into any future wiring (brief §8)
- **Semantic / factual poisoning is invisible** to the deterministic core — highest impact
for a wiki. A plausible-but-wrong extracted value (wrong metric, wrong cost) passes clean.
This system already has a strong mitigation: the **deterministic validator** (method §4)
and **human expert gate** anchor the *numbers* the wiki feeds — the guard is defense in
depth over the *text carrier*, not the semantic truth.
- **Dormant / broken-link injection** — the guard's `link_graph` surfaces the dangling edge;
blocking is a disposition call. Ingest's index-link contract (§6) is the natural place.
- **Text-only, extracted-text-only** — if ingest ever pulls binary/file sources, extract
text first, then scan with high-untrust provenance (brief §6).
## 6. Verification (testable criteria)
Run from the repo root:
1. Plan exists: `test -f docs/plan/2026-07-16-llm-ingestion-guard-adoption.md`
2. Marker present, status first token: `grep -Eq '^llm-ingestion-guard: (planned|integrated|not-applicable) — ' STATE.md`
3. STATE.md is LOCAL-ONLY: `git check-ignore -q STATE.md` (exit 0 = ignored, never committed)
4. Both persist gates named, promotion gate excluded:
`grep -q 'import_bundle' docs/plan/2026-07-16-llm-ingestion-guard-adoption.md` and
`grep -q 'Do NOT wire the guard here' docs/plan/2026-07-16-llm-ingestion-guard-adoption.md`
5. Scope guard — guard NOT wired yet (this repo has no runtime code):
`! grep -rq 'llm_ingestion_guard' --include='*.py' .` (no Python files exist; the only
`llm_ingestion_guard` references are the illustrative snippets in this plan)
## 7. Explicitly out of scope for this task
Implementing the guard, adding it as a dependency, editing ingest-spec.md/method-spec.md, or
touching either implementation repo. This task **plans and records only.**

View file

@ -0,0 +1,111 @@
# F1 — fritekst-kilder i ingest-spec: retningsvurdering
**Status:** vurdering, ikke vedtak. Spec-en er IKKE endret. Beslutning tas av operatøren.
**Foranledning:** `llm-ingestion-okf` v0.3.1 har ingen fritekst-connector; `file` er en streng
CSV-leser og §5-body rendres som markdown-tabell med newline-kollaps. To uavhengige repo
(`claude-playlist-corpus`, `claude-code-llm-wiki`) blokkeres. Spørsmålet stilt til commons:
løses dette i `ingest-spec.md` (dør A) eller i bibliotekets dør B (innboks, ikke spec-styrt)?
---
## 1. Retning: løses i spec-en, i dør A
Fire grunner, i synkende vekt:
1. **Arkitekturregelen krever det.** §1: *«data reaches the model ONLY via OKF bundles»*, og
bundlen materialiseres deterministisk FØR loopen. Fritekst er data. Ingenting i §1§3
skiller tabulært fra ustrukturert — utelukkelsen er et artefakt av at v1s `file`-connector
ble en CSV-leser, ikke en prinsipiell grense. Skyves den vanligste kunnskapsbase-kilden ut
til dør B, faller den samtidig utenfor provenance-stemplingen (§7), verdict-reservasjonen
(§3), golden-regimet (§11) og determinismen (§10) — spec-ens dekning uthules der den betyr
mest.
2. **Sitatkravet peker samme vei.** Method-spec §9 krever *«file + exact text span +
snippet»*. En span inn i en tabell-rendret, newline-kollapset body er i praksis ubrukelig;
en verbatim dokument-body er det naturlige span-målet. Fritekst gjennom dør A gjør
sitatkjeden bedre, ikke svakere.
3. **Determinismen er lettere, ikke vanskeligere.** Verbatim bytes har ingen typekoercion,
ingen tallformatering, ingen NULL-semantikk — hele §5s tabell-fallgruvefelt forsvinner.
Det finnes ikke noe determinisme-argument for å holde fritekst ute.
4. **Spec-en har allerede render-modusen — feilplassert.** §5 sier for `http`: *«the response
body verbatim inside a fenced code block»*. Verbatim-render finnes altså, men er bundet til
**transport** (`http`) i stedet for til **innholdsform**. Det er den faktiske defekten: §5
konflaterer kildetype og body-form. F1 er symptomet.
**Grensen mot dør B holder likevel — men den går ikke ved innholdsform.** Den går ved
*adresserbarhet + re-eksekverbarhet*: en kilde med manifest-deklarert, repeterbar `query` mot
en adresserbar kilde hører i dør A (en transkripsjonsfil under `root` kvalifiserer). Materiale
uten re-eksekverbar kilde-query — limt inn tekst, en e-post, et engangsdropp — hører i dør B.
Bruker vi innholdsform som grense i stedet, havner halve kunnskapsbasen utenfor spec-en av en
grunn som ikke er prinsipiell.
### Foreslått form (retning, ikke spec-tekst)
Ikke en ny kildetype `document` — den ville duplisere `file`s `root`- og
grensesjekk-logikk. I stedet: **skill body-form fra kildetype.** Et nytt påkrevd
extraction-felt (arbeidsnavn `render`) med `table` | `verbatim`. `file`+`table` = dagens
CSV-leser; `file`+`verbatim` = fritekst-dokumentet som mangler; `http` beskrives retroaktivt
som `verbatim` og blir dermed konsistent i stedet for et unntak. Ett felt, én rad i §12s
kryssjekk-tabell, ingen ny kildetype.
## 2. Hva §5 må si om verbatim body-render
Seks punkter. De fire første er ikke valgfrie — dagens `http`-render mangler dem alle, så
dette lukker et latent hull samtidig som det løser F1.
- **Bytes og dekoding.** Innholdet leses som bytes og dekodes som UTF-8, strengt. Dekodefeil
er en ERROR — aldri erstatningstegn, aldri lossy koercion. Samme disiplin som §5s *«any
other value type MUST fail (never silent coercion)»*.
- **Linjeskift — her er «verbatim» nødt til å vike.** §5 krever LF-only og nøyaktig én
avsluttende newline. CRLF/CR MÅ derfor normaliseres til LF, og etterfølgende blank plass ved
EOF kollapses til nøyaktig én LF. Uten denne regelen motsier «verbatim» og «LF-only»
hverandre, og golden-ekstraksjoner blir plattformavhengige. Spec-en MÅ si rett ut at
renderen dermed er lossy mot kildens eksakte bytes — og at en eventuell kildehash regnes
over **kildens** bytes, ikke de rendrede.
- **Fencing — obligatorisk, med deterministisk bredde.** Fence-lengde =
`max(3, lengste backtick-run i innholdet + 1)` (CommonMark-standard, deterministisk).
- **Fencing er sikkerhetsbærende, ikke kosmetikk.** Ufenset markdown-passthrough er IKKE v1.
Grunnen er konkret: method-spec §3 Step 1 navigerer ved å følge body-lenker
(`](target.md)`), så et ufenset ingested dokument kan injisere navigasjonskanter utenom
`index.md` og forsøke bundle-escape-stier. I tillegg rendres read-context som
`## {type}: {title}`-seksjoner — et dokument med egne `##`-overskrifter visker ut
seksjonsgrensene, og en ledende `---` kan leses som et nytt frontmatter-blokk. Fenset
innhold har ingen av delene. (Ufenset passthrough = utvidelsespunkt.)
- **`max_rows` for dokumenter.** Feltet er radorientert og meningsløst for fritekst. Enkleste
konsistente lesning: for `verbatim` teller `max_rows` LF-delimiterte linjer, og overskridelse
er ERROR (§8, aldri stille trunkering). **Åpen underbeslutning:** en 40 MB transkripsjon
passerer en linjegrense lenge før den blir håndterbar — trengs et valgfritt `max_bytes` i
tillegg? Anbefaling: ja, valgfritt, men dette er verdt et eksplisitt operatørvalg.
- **`source_query` og provenance ellers:** uendret — relativ sti, whitespace-kollapset (§5/§7).
### `ingested_at`-determinismen bevares — med én felle å lukke eksplisitt
Verbatim-render leser ingen klokke. `ingested_at` forblir et eksplisitt påkrevd argument,
stemplet ordrett; §5s regel er urørt. Fellen er en fristende «forbedring»: å stemple kildefilens
mtime eller størrelse som ekstra provenance. Det MÅ forbys — git bevarer ikke mtime, så et
golden-fixture ville gi ulikt resultat ved frisk utsjekking, og §11s byte-for-byte-krav brytes.
Verdt en eksplisitt MUST NOT.
### §11 — nye seams
- Ny golden-case, konvensjon `examples/ingest-golden-verbatim/`. Fixturet MÅ inneholde et
dokument med CRLF, en backtick-run ≥ 3, en ledende `---`, og ikke-ASCII — så escape-reglene
fryses av golden i stedet for å leve i prosa.
- Ny load-bearing rad: testen MÅ feile når fence-bredden slutter å vokse med innholdets
lengste backtick-run (dvs. når ingested innhold kan bryte ut av fencen).
## 3. Sikkerhetsobservasjon (til operatøren, ikke en spec-endring)
Dette er første gang dør A materialiserer innhold som er **untrusted av opphav** selv om
transporten er lokal og first-party: `root` eies av operatøren, men en transkripsjon eller
artikkel under `root` er tredjeparts tekst. Guard-adopsjonsplanen
([2026-07-16](2026-07-16-llm-ingestion-guard-adoption.md) §4) definerte Trigger A som «når
`http`/MCP-kildetypen bygges». En fritekst-connector treffer samme untrusted-grense uten å gå
via `http` — så Trigger A bør trolig omformuleres fra *transport* til *opphav*, og kan dermed
inntreffe tidligere enn planen antar. Flagget her, ikke besluttet.
## 4. Hva dette IKKE avgjør
- Ingen spec-tekst er skrevet. §4, §5, §11 og §12 er urørt.
- `render`-feltnavnet er et arbeidsnavn.
- `max_bytes`-spørsmålet står åpent (§2).
- Dør B (innboks) berøres ikke: den beholder materiale uten re-eksekverbar kilde-query.

View file

@ -0,0 +1,125 @@
# Q3 — flate vs. hierarkiske bundles: en reell konflikt i method-spec §3
**Status:** funn + retningsvurdering. Spec-en er IKKE endret. Beslutning tas av operatøren.
**Foranledning:** spørsmål 3 fra `llm-ingestion-okf` (postkasse `_okf-interim`, trinn C):
dør A produserer flate bundles; second-brain-siden (okr, catalog) bruker hierarkisk form med
per-nivå-indekser. Sier spec-en noe om dette, og bør den?
---
## 1. Funnet: spec-en sier noe — og den forbyr hierarki, stilltiende
Spørsmålet var stilt som om spec-en er taus. Den er ikke det.
**method-spec.md:66-69 (§3 Step 1):**
> Navigation starts at `index.md` and follows its intra-bundle markdown cross-links
> (`](target.md)`). **Targets containing a path separator are out-of-bundle and MUST be
> skipped.**
Dette er verifisert implementert i MAF-halvdelen, `portfolio-optimiser/src/portfolio_optimiser/okf.py:123-125`:
```python
for target in _LINK_RE.findall(index.body):
if "/" in target or target in seen:
continue # only same-dir bundle files; de-dup repeated links
```
Og okr-siden produserer nøyaktig det som forkastes:
- `okr/scripts/okf-index.mjs:110` emitterer underkatalog-pekere som `` `${sd}/index.md` ``.
- `okr/lib/okf-links.mjs:23` **krever** ledende `/`: `if (!target.startsWith('/')) return false;`
— «maa vaere bundle-root-relativ».
Sammensatt: **hver eneste lenke okr produserer inneholder `/`, og hver eneste av dem forkastes
av navigatoren.** okr kan ikke engang skrive flatt — `routeLevel()` (`lib/okf-vocab.mjs:71`,
konsumert i `lib/innboks-frontmatter.mjs:74`) ruter alltid til en underkatalog. Dette er ikke
et kantttilfelle; det er okrs normaltilstand.
**Det verste er at feilen er stille.** method-spec.md:72-73 krever det: *«A broken or
bundle-escaping cross-link MUST be tolerated — skipped, never raised (OKF robustness rule).»*
En hierarkisk bundle gir altså ikke en feilmelding — den gir en read-context som inneholder
rot-indeksen og ingenting annet. På okrs eget fixture `tests/fixtures/okf-realistic/` forsvinner
alle 8 konseptfiler, fordi ingen av dem ligger i roten. Symptomet operatøren ville sett er
«modellen kjente ikke innholdet», ikke «bundelen er ugyldig».
**To spec-er kolliderer på samme paragrafnummer — ved et sammentreff.** `catalog`s svar siterer
«§3 MUST» for per-nivå-indeks. Det er `catalog/docs/okf-second-brain/spec.md:45`, et *annet*
dokument enn method-spec.md §3. De er ikke to lesninger av én regel; de er to normative regimer
som tilfeldigvis møtes på «§3». Verdt å holde fra hverandre i all videre diskusjon.
## 2. Hvorfor regelen er gal — den er feil proxy for riktig egenskap
Separator-forbudet ble åpenbart skrevet som en *sikkerhets*regel: hindre at en lenke rømmer
bundelen. Men den forveksler «inneholder separator» med «rømmer bundelen». `sub/index.md`
rømmer ingenting. `../../etc/passwd` gjør det.
Og method-spec **har allerede den riktige mekanismen**, to linjer lenger ned (:73):
> path resolution MUST be boundary-checked against the bundle directory, fail-closed.
Så §3 Step 1 bærer både den presise regelen og en grov proxy — og det er proxyen som gjør
skaden. Den presise regelen alene er tilstrekkelig.
**Den korrekte formen finnes allerede som fungerende prior art hos okr**
(`lib/okf-links.mjs:20-26`): avvis null-byte, backslash, URL-scheme, `..`-segment; krev `.md`;
krev bundle-rot-relativ form. Dette er resolve-og-grensesjekk gjort riktig. Retningen er derfor
ikke å oppfinne noe, men å løfte disse semantikkene inn i method-spec §3 Step 1 og la
separator-forbudet gå ut.
## 3. Retning: la navigatoren traversere hierarki; la dør A fortsatt produsere flatt
To beslutninger som med fordel holdes fra hverandre:
**(a) Navigasjon (method-spec §3 Step 1) bør støtte hierarki.** Alternativet — flat-only for
alltid — betyr at okr/catalogs output aldri kan mate optimiser-loopen, og at de to halvdelene
ikke kan dele fixture-suite i det hele tatt (se §4). Kostnaden ved å åpne er håndterbar; se
åpne punkter under.
**(b) Dør A (ingest-spec §5) bør forbli flat i v1.** `ingest-{id}.md` uten katalogkomponent,
extraction-id-grammatikken utelukker `/`. En flat bundle er et gyldig degenerert tre, så
ingenting brekker. Hierarkisk *produksjon* er et utvidelsespunkt, ikke v1 — og å holde det
utenfor gjør at F1 (fritekst) og Q3 (form) forblir uavhengige beslutninger. Det er navigatoren
som må lese begge former, ikke materialisereren som må produsere begge.
### Hva som må avklares hvis (a) velges — ikke gratis
- **Lenkesyntaks, normativt.** To former er i omløp: okr emitterer rot-relativt `/foo/bar.md`;
catalog spec.md:76 tillater både rot-relativt og relativt. Spec-en MÅ velge. Anbefaling:
støtt begge, med `/` = **bundle-rot-relativ, IKKE filsystem-absolutt**. Den lesningen er
nøyaktig der en naiv implementasjon blir en path traversal-sårbarhet (`open("/etc/passwd")`),
så den må være normativ og dekkes av en load-bearing test.
- **Traverseringsrekkefølge.** Dagens «index first, then links in first-seen order» er definert
for én flate. For et tre må dybde-først vs. bredde-først pinnes, ellers er read-context ikke
deterministisk og golden-suiten brekker.
- **Dedup på *resolvert* sti**, ikke rå target — ellers teller `./a.md` og `a.md` som to
oppføringer, og sykler (`a/index.md``b/index.md``a/index.md`) termineres ikke.
- **Verdict-eksklusjonen må holde på alle nivåer.** method-spec §3 Step 1 utelukker
`type: verdict` fra read-context; med hierarki må det gjelde rekursivt, ikke bare i roten.
Dette er lekkasjeflaten hvis den glemmes.
- **Read-context-rendering.** `## {type}: {title}`-seksjoner har ingen nivåforestilling.
Anbefaling: behold flat seksjonsrendering også for hierarkiske bundles (nivået er
navigasjonsstruktur, ikke presentasjonsstruktur) — men si det eksplisitt.
## 4. Konsekvens for delt fixture-suite (det spørsmålet egentlig gjaldt)
Direkte svar: **Python- og Node-halvdelen kan dele fixture-suite kun hvis traverserings-
kontrakten er én kontrakt.**
- Velges flat-only, er okrs `okf-realistic` ikke en gyldig input til Python-navigatoren, og de
to halvdelene deler ikke bundle-fixtures i det hele tatt — bare ingest-fixtures.
- Velges hierarki, MÅ fixture-suiten inneholde en hierarkisk case, og begge halvdeler MÅ
produsere **identisk read-context** fra den. okr har allerede kandidaten ferdig
(`tests/fixtures/okf-realistic/`, tre nivåer dypt, per-nivå-indekser), og et flatt
motstykke (`okf-minimal/`). De bør adopteres som delte fixtures, ikke gjenskapes.
Med andre ord: (a) er det som gjør delt fixture-suite mulig i første omgang. Det er
hovedargumentet, sterkere enn eleganse.
## 5. Løse tråder som ikke er mine å lukke
- **Versjonsdrift:** `catalog/docs/okf-second-brain/spec.md:47` oppgir rotmarkøren som «currently
`0.1`», mens `okr/scripts/okf-index.mjs:27` setter `OKF_VERSION = 'kb-layout-2026-06'`.
Spec og referanse-implementasjon er ikke i sync. Meldt, ikke rørt.
- **okrs lukkede vokabular håndheves kun på skrivestien** (`lib/okf-vocab.mjs:6-8`); lesesiden
aksepterer ukjente typer. Relevant for catalogs krav (i) — se svarfilen.

View file

@ -0,0 +1,177 @@
# D2 — register-form-fiks: to-utgangs-modell som løser ÅS#5
**Status:** forslag + kontraktutkast. Ingen global konvensjon er endret. Ratifiseres av operatøren.
**Foranledning:** D2 (koordineringsregister — committet per-repo-bærer + generert pekerindeks) er
blokkert av **ÅS#5**: en committet, fjern-hentbar bærer lekker intern koordineringsmetadata
(repo-navn, statuser, arkitekturprosa) til offentlige speil. Commons er selve mønstereksemplet —
åpen publisering **+** subtree-konsumert → `STATE.md` er allerede LOCAL-ONLY av nettopp denne
grunn. Denne doken forfatter register-form-**kontrakten** HER (jf. driftsmodell: kontrakten
forfattes i commons, wiringen i catalog/impl), til ratifisering inn i koordineringskonvensjonen.
**Ratifiserte valg (operatør, 2026-07-23):** to-utgangs-modell; leveranse = denne proposal-doken
(ingen unilateral global edit).
---
## 1. Rotårsak: D2 konflaterer durabilitet med offentlighet
Roll-up-markøren har i dag én form — en linje i hvert repos `STATE.md`:
```
<topic>: <status> — <fri prosa; kan navngi repo, arkitektur, blokkere>
```
Første token etter `:` er maskinlesbar status; resten er menneskeprosa. `STATE.md` er LOCAL-ONLY
(gitignored), og «roll-up-en» er en tverrsnittsvisning satt sammen ved å **grep-e
`~/repos/*/STATE.md`**. Den assemblen er **manuell/ad-hoc** — det finnes ikke noe automatisert
assembler-script (verifisert: `session-start.sh` injiserer kun *nærmeste enkelt*-STATE, ikke noe
tverrsnitt). Det finnes altså ingen automatisert konsument å regressere.
D2 ville gjøre markøren **committet** slik at den overlever fresh clone / subtree-konsum *uten* en
co-lokalisert LOCAL-ONLY STATE. Men «committet» drar **to uavhengige egenskaper** sammen:
- **Durabilitet** — reiser markøren med repoet (clone/subtree) uten STATE?
- **Offentlighet** — tåler innholdet å bli speilet til en offentlig flate?
D2 valgte «committet» for durabilitet og fikk offentlighet på kjøpet. **Fiksen er å koble dem
fra:** en markør kan være durabel uten å være offentlig-eksponert, og offentlig-eksponert innhold
kan reduseres til det som trygt tåler speiling.
## 2. Fiksen: to utganger, aldri konflatert igjen
Registeret har **to distinkte utganger** med ulik målgruppe. De blandes aldri.
### Utgang A — rik lokal roll-up (LOCAL-ONLY)
- **Kilde:** `STATE.md`-markørlinjene (full `topic: status — prosa`), grep-et over
`~/repos/*/STATE.md`.
- **Målgruppe:** operatøren, lokalt. **Publiseres aldri.**
- **Endring: ingen.** Dette er dagens mekanisme. Den forblir komplett og LOCAL-ONLY.
### Utgang B — offentlig status-token-indeks (VALGFRI, per repo)
- **Kilde:** en per-repo **minimal committet bærer** — status-token(s) *alene*, ingen prosa, ingen
repo-intern arkitekturdetalj.
- **Målgruppe:** enhver som kloner/konsumerer repoet (inkl. offentlige speil). Trygg fordi den kun
bærer `topic: status`.
- **Valgfri per repo:** et repo i offentlig-speil-klassen (commons) KAN velge å publisere ingenting
→ det opptrer da ikke i utgang B. Det er «tverrsnitt-eksklusjon by design», ikke et tap.
Utgang B er en **projeksjon** av utgang A, bevisst strippet til den offentlig-trygge delmengden
(status-token), og **opt-in per repo**. Rik prosa forlater aldri utgang A.
## 3. Bærer-grammatikk (normativ)
### 3.1 Utgang A-kilde — STATE.md-markøren (uendret)
Som i dag; LOCAL-ONLY. Ingen endring, tatt med for kontrast.
### 3.2 Utgang B-kilde — minimal committet bærer
- **Fil:** en committet fil i repo-rot. Navn (forslag): `.rollup-carrier`.
- **Form:** én linje per tema, **status-token alene** — ingen em-dash, ingen prosa:
```
llm-ingestion-guard: planned
llm-ingestion-okf: not-applicable
```
- **Vokabular:** status-token trekkes fra D3-status-vokabularet (`planned` / `in-progress` /
`blocked` / `done` / `not-applicable` / `partial` / `deferred` / `active` …). D3 er eget spor;
denne doken *refererer* vokabularet, definerer det ikke.
- **Offentlig-trygghet er per repo, ikke absolutt:** selv `topic: status` avslører at repoet
deltar i en intern koordinering. `.rollup-carrier` brukes derfor KUN når topic+status er
ikke-sensitivt for det repoet. Er selv det sensitivt → bruk modus «fraværende» (§4.1).
## 4. Tre-modus builder-toleranse (avgjort ved register-build)
Register-builderen prober hvert repo og **tolererer fravær** — ingen global regel tvinger commit.
Hver modus svarer til en repo-klasse:
### 4.1 Fraværende bærer
Repoet committer ingenting. **Utgang B utelater det** (ingen feil); **utgang A** (lokal STATE-grep)
dekker det fullt. → Offentlig-speil-klassen (**commons**).
### 4.2 Minimal committet bærer
Repoet committer `.rollup-carrier` (`topic: status`, §3.2). Reiser med clone/subtree/offentlig
speil. → Repo der topic+status er offentlig-trygt OG durabel offentlig-side-reise ønskes.
### 4.3 Privat sidekanal
Bæreren bor på en privat-kun flate — en `*.local`-gitignored fil, coord-området
(`~/.claude/coord/`, allerede lokalt/privat), eller en privat-remote-kun sti. Builderen leser den
ved assembly av den **private/lokale** indeksen (utgang A-siden). → Privat repo (intet offentlig
speil), subtree-konsumert, som vil ha markør-reise *innen* det private økosystemet uten offentlig
eksponering (f.eks. `claude-code-llm-wiki`: «fullt privat, intet open/-speil» i dag).
## 5. Presedens når et repo har flere kilder
- **Utgang A** er autoritativt fra `STATE.md`-markøren (kanonisk state-of-play per
kontinuitets-systemet).
- **Utgang B** er autoritativt fra `.rollup-carrier`.
- **Drift-invariant:** når STATE-markørens status ≠ committet bærers status, **vinner STATE**, og
builderen SKAL varsle (den offentlige projeksjonen har drevet fra den lokale sannheten). Prosa
promoteres aldri inn i utgang B; intet repo tvinges til å committe.
## 6. Commons' egen posisjon
Commons velger **modus «fraværende»** (§4.1): ingen `.rollup-carrier`, markøren blir i LOCAL-ONLY
`STATE.md`. Begrunnelse er commons' definerende egenskap — åpen publisering + subtree-konsum — den
samme som allerede holder `STATE.md` gitignored. Commons opptrer i utgang A (lokal grep), aldri i
utgang B. **Interim-linjen i STATE («hold roll-up-markørene LOCAL-ONLY, aksepter
tverrsnitt-eksklusjon») blir dermed den permanente designen for denne klassen**, ikke en midlertidig
innrømmelse.
## 7. Plassering av grammatikken (anbefaling — avgjøres ved ratifisering)
STATE peker grammatikken til «koordineringskonvensjonen». Men coord-README-en er eksplisitt
*postkasse-transport* (Regel 1: «Postkasse, ikke state»; Regel 2: «Ingen varige beslutninger bor
her»). Roll-up-registeret er **state-roll-up, ikke transport** — å veve det inn i postkasse-reglene
grumser Regel 1.
**Anbefaling:** plasser det som en **avgrenset register-del i coord-området**, eksplisitt adskilt
fra postkasse-transporten — f.eks. `~/.claude/coord/register.md`, eller en klart merket seksjon
«Register — state-roll-up, distinkt fra postkasse-transporten». Det honorer STATEs retning
(koordineringsflaten er den globale hjemmen for tverrsnitt-mekanikk) samtidig som coord Regel 1/2
holdes rene. Alternativet — å legge det ved STATE-konvensjonen i global `CLAUDE.md` — ble vurdert:
forkastet fordi registeret er en *tverr-repo* mekanisme, ikke en enkelt-repos STATE-form.
## 8. Avhengigheter og grenser
- **D3-status-vokabular** (eget spor) leverer status-token-settet. Denne kontrakten refererer det;
definerer det ikke.
- **catalog** eier builderens *wiring* (det faktiske register-build-scriptet + adversariske akser).
Denne doken er den framework-nøytrale kontrakten — commons rører ikke catalog-moduler.
- **Ingen global konvensjon endres her.** Ratifisering (den globale register-seksjonen +
catalog-wiring) er neste steg, etter operatør-OK.
## 9. Verifisering
Testbare kriterier som beviser at fiksen er korrekt og ikke-regressiv:
- **V1 (ingen regresjon i utgang A):** `for f in ~/repos/*/STATE.md; do grep -nE '^[a-z][a-z0-9-]+:
(planned|not-applicable|blocked|in-progress|done|partial|deferred|active)' "$f"; done` gir fortsatt
begge temaene på tvers av repo (uendret mot i dag).
- **V2 (commons fraværende fra utgang B):** `git ls-files | grep -i 'rollup-carrier'` i commons →
tomt; commons' markør vises fortsatt i V1-grepet. Bekrefter tverrsnitt-eksklusjon by design.
- **V3 (sidekanal er gitignored):** `git check-ignore .rollup-carrier.local` → matcher (`*.local.md`
dekker ikke bare `.local` — bekreft mønsteret ved ratifisering; utvid `.gitignore` om nødvendig).
- **V4 (offentlig-trygghet):** en `.rollup-carrier` inneholder ingen prosa/em-dash:
`grep -n '—' .rollup-carrier` → tomt. Regex-gate builderen kan håndheve.
- **V5 (drift-varsel):** når STATE-markørens status ≠ bærers status, varsler builderen (§5). Spec-nivå-
assertion; catalog-wiring implementerer.
**Nøkkelantakelser — eksplisitt testet:**
- *«Ingen automatisert roll-up-konsument å knuse.»* Testet: `grep -rl 'repos/\*/STATE'
~/.claude/{scripts,hooks}` → kun `session-start.sh`, som injiserer nærmeste *enkelt*-STATE, ikke
tverrsnitt. **Verifisert 2026-07-23.**
- *«Commons' offentlig-lekkasje er prospektiv, ikke live.»* Testet: `git remote -v` → kun privat
Forgejo (`git.fromaitochitta.com`), intet `open/`-speil. **Verifisert 2026-07-23.** Fiksen er
altså forebyggende for klassen, ikke en akutt tetting.
## 10. Ratifiseringssti
1. Operatør-OK på denne kontrakten.
2. `coord-send` til catalog (builder-wiring) + broadcast til deltakende repo (bærer-modus-valg per repo).
3. Ratifiser inn i koordineringskonvensjonen (register-del, §7).
4. catalog wirer builderen med de tre modusene + V4/V5-gatene.

View file

@ -0,0 +1,140 @@
# D3 — status-vokabular: kanonisk token-sett for roll-up-registeret
**Status:** RATIFISERT 2026-07-24 — landet i `~/.claude/coord/register.md` (kanonisk 7-token-sett +
de to reglene inline; `active` og det åpne `…` fjernet). Denne doken er kontrakt-kilden.
**Foranledning:** D2-register-form-fiksen (`docs/plan/2026-07-23-d2-register-form-fix.md`, `c66ccc3`)
landet register-*formen* og ble ratifisert inn i `~/.claude/coord/register.md` (2026-07-23). Både
kontrakten og den globale register-fila **refererer** et status-token-vokabular men **definerer det
ikke** — begge peker eksplisitt til «D3-sporet» (register.md linje 4244, 73; D2 §3.2). Denne doken
forfatter det vokabular-**kontrakten** HER (jf. driftsmodell: kontrakten forfattes i commons,
wiringen i catalog), til ratifisering inn i register-fila.
**Ratifiserte valg (operatør, 2026-07-23):** kanonisk 7-token-sett (`active` foldet inn i
`in-progress`); to normative regler (`partial` transitorisk; `deferred``blocked`); leveranse =
denne proposal-doken (ingen unilateral global edit), speiler D2-mønsteret.
---
## 1. Gapet: vokabularet er referert, ikke definert
Roll-up-markøren har formen `topic: status — prosa` (utgang A / STATE.md) og `topic: status`
(utgang B / `.rollup-carrier`). Første token etter `:` er den maskinlesbare statusen. D2 fastslo at
token-settet trekkes fra «D3-status-vokabularet» men lot definisjonen stå åpen — register.md lister
et *kandidatsett* med et etterfølgende `…` (åpen-endet), ikke en normativ definisjon.
**Ground truth (verifisert 2026-07-23):** kun to tokens er i faktisk bruk på tvers av
`~/repos/*/STATE.md``planned` (5×) og `not-applicable` (1×). Resten av settet er prospektivt.
Dette er altså en definisjon *før* bred bruk: D3 setter den kanoniske betydningen slik at fremtidige
markører er konsistente, ikke en migrasjon av eksisterende data.
## 2. Kanonisk token-sett (normativt)
Syv tokens. `active` (i register.md-kandidatlista) droppes — det overlapper `in-progress` uten skarp
semantisk forskjell; en ekstra synonym svekker den maskinlesbare disiplinen.
| Token | Semantikk | Terminal? |
|-------|-----------|-----------|
| `planned` | Avgrenset scope, ikke startet. | Nei |
| `in-progress` | Aktivt arbeid, én eier, forventet å nå `done`. | Nei |
| `partial` | **Transitorisk.** Noen konstituerende deler ferdig, andre utestående — potensielt under ulike eiere. MÅ bære eier + neste-steg i utgang-A-prosaen (§3.1). Aldri en terminal hviletilstand. | Nei |
| `blocked` | Kan ikke fortsette — venter på en **ekstern** avhengighet. Navngir blokkereren i prosaen. | Nei |
| `deferred` | **Bevisst** utsatt — et valg om å ikke gjøre nå, ikke venting på noe. Distinkt fra `blocked` (§3.2). | Nei |
| `done` | Fullført, ingen utestående arbeid. | Ja |
| `not-applicable` | Temaet gjelder ikke dette repoet (f.eks. spec-eier uten kjørbar pipeline). | Ja |
**Terminal-klassifiseringen er informativ, ikke håndhevet:** den skiller hviletilstander (`done`,
`not-applicable`) fra tilstander som forventer videre bevegelse. Den muliggjør en fremtidig
staleness-heuristikk (en ikke-terminal markør som ikke har endret seg på lenge fortjener tilsyn),
men denne doken *innfører ingen slik gate* — det finnes ingen pålitelig tidsstempel-mekanisme i
markør-formen, og å oppfinne en her ville være spekulativt scope.
## 3. To normative regler
### 3.1 `partial` er transitorisk — krever eier + neste-steg
`partial` beskriver arbeid delt i konstituerende deler med ulik framdrift, ofte ulike eiere. Uten en
eksplisitt eier + neste-steg blir det en åpen «halvferdig»-tilstand uten vei ut. Regelen:
> En `partial`-markør hvis utgang-A-prosa mangler både en eier og et neste-steg er **malformert**.
Dette er en disiplin på **utgang A** (STATE-prosaen), ikke utgang B. Utgang B bærer kun token-et
(`topic: partial`) og er offentlig-trygg per D2 §3.2 — eier/neste-steg-prosaen forlater aldri utgang
A. Regelen kolliderer derfor ikke med D2s offentlig-trygghet (V4): den håndheves der prosaen finnes.
**Grounding:** «form→catalog, plumbing→okf, tillit→guard, spec→commons» (fra
`llm-ingestion-okf`-markøren) er nettopp den fler-eier-delingen `partial` fanger — et tema der én
eiers del kan være ferdig mens en annens er utestående.
### 3.2 `deferred``blocked` — frivillig vs. ufrivillig
- `blocked`: arbeidet *kan ikke* fortsette; en ekstern avhengighet står i veien. Ufrivillig.
- `deferred`: arbeidet *kunne* fortsette, men er bevisst nedprioritert. Frivillig.
Skillet bærer beslutnings-informasjon: `blocked` inviterer «løs blokkereren»; `deferred` sier
«dette ligger med vilje, ikke rør». Å konflatere dem skjuler hvorvidt noen venter på noe.
**Grounding (begge fra dagens økosystem):**
- **`deferred`:** commons' «broadcast (bærer-modus per repo) **bevisst utsatt** — builder tolererer
fravær → ingen andre repo blokkeres» (STATE). Ingenting venter; valgt å ligge. Tekstbok `deferred`.
- **`blocked`:** catalogs «ÅS#5-gatede STEG 0» var `blocked` på D2-ratifiseringen inntil commons
varslet via coord (som *låste opp* steget). Ekstern avhengighet, ufrivillig — tekstbok `blocked`.
## 4. Konsekvens for register.md ved ratifisering
Ratifisering erstatter register.md-kandidatlista (linje 4244) med det kanoniske settet:
- Fjern `active` (foldet inn i `in-progress`).
- Fjern det åpen-endede `…` — settet er lukket og normativt.
- Legg til de to reglene (§3.1, §3.2) eller en peker til denne doken.
Ingen eksisterende markør må endres: `planned` og `not-applicable` (de eneste i bruk) er begge i det
kanoniske settet.
## 5. Avhengigheter og grenser
- **D2** (`c66ccc3`, register.md) leverer *formen* som konsumerer dette vokabularet. D3 fyller
vokabular-referansen D2 lot stå åpen. D3 endrer **ikke** to-utgangs-modellen, bærer-grammatikken
eller builder-modusene.
- **catalog** eier builderens *wiring*. En builder KAN håndheve §3.1-gaten (regex på `partial`-linjer
i utgang-A-kilden) og bør avvise ukjente tokens utenfor det kanoniske settet — men det er
catalog-wiring, ikke denne kontrakten. Commons rører ikke catalog-moduler.
- **Ingen global konvensjon endres her.** Ratifisering (register.md-oppdateringen) er neste steg,
etter operatør-OK.
## 6. Verifisering
Testbare kriterier som beviser at kontrakten er korrekt og ikke-regressiv:
- **V1 (ingen regresjon):** de to tokens i faktisk bruk er begge i det kanoniske settet.
`grep -rhoE '^[a-z][a-z0-9-]+: (planned|not-applicable)' ~/repos/*/STATE.md` → treff; ingen
eksisterende markør faller utenfor settet.
- **V2 (`active` er ikke i bruk — trygt å droppe):**
`grep -rhE '^[a-z][a-z0-9-]+: active\b' ~/repos/*/STATE.md` → tomt. Bekrefter at å folde `active`
inn i `in-progress` ikke bryter noen levende markør. **Verifisert 2026-07-23: tomt.**
- **V3 (register.md refererer, definerer ikke — gapet er reelt):**
`grep -n 'D3' ~/.claude/coord/register.md` → linjene som utsetter til D3-sporet (i dag 4244, 73).
Etter ratifisering skal disse erstattes av den normative definisjonen.
- **V4 (`partial`-gaten er utgang-A-lokal, kolliderer ikke med D2 V4):** en `partial`-linje bærer
eier + neste-steg kun i prosa (etter em-dash); utgang B (`.rollup-carrier`) forblir prosa-fri per
D2 §3.2. Gaten `partial ⇒ prosa har eier+neste-steg` og D2s gate `.rollup-carrier har ingen prosa`
gjelder to ulike utganger og kan begge holde samtidig.
- **V5 (`deferred`/`blocked`-skillet er observerbart):** dagens `deferred`-case (commons' bevisst
utsatte broadcast) og `blocked`-case (catalogs ÅS#5-gatede STEG 0, nå løst) er distinkte i
økosystemet (§3.2 grounding) — skillet koder reell beslutnings-informasjon, ikke en synonym.
**Nøkkelantakelser — eksplisitt testet:**
- *«Vokabularet er referert men udefinert i register.md.»* Testet: `grep -n 'D3' register.md`
linje 4244 («D3 er eget spor; her *refereres* vokabularet, defineres ikke») + linje 73
(«Status-vokabularet: D3-sporet»). **Verifisert 2026-07-23.**
- *«Kun `planned` + `not-applicable` er i bruk; resten er prospektivt.»* Testet:
`grep -rhoE ... ~/repos/*/STATE.md | sort | uniq -c``planned` 5×, `not-applicable` 1×, ingen
andre. **Verifisert 2026-07-23.** D3 er en definisjon før bred bruk, ikke en migrasjon.
## 7. Ratifiseringssti
1. ~~Operatør-OK på dette vokabularet + de to reglene.~~ **GJORT** (2026-07-23).
2. ~~Ratifiser inn i `~/.claude/coord/register.md`: erstatt kandidatlista med det kanoniske settet +
reglene; fjern `active` og `…`.~~ **GJORT** (2026-07-24): ny «Status-vokabular»-seksjon (tabell +
regel 1/2) + kontrakt-kilde-peker; vokabular-bulleten og Eierskap-pekeren oppdatert.
3. catalog kan (valgfritt, deres wiring) håndheve §3.1-gaten + avvise ukjente tokens i builderen. — GJENSTÅR (catalog-spor).

View file

@ -0,0 +1,530 @@
# Amendment-underlag — hva frossen tekst sier i dag, per køpunkt
**Status:** underlag. Spec-en er IKKE endret, og dette dokumentet foreslår INGEN ny tekst.
Ny tekst er pakken operatøren ratifiserer, og den eies oppstrøms (`portfolio-optimiser`,
sammen med `portfolio-optimiser-claude`s tekstforslag). Dette dokumentet svarer bare på
spørsmålet commons kan svare på uten å foregripe ratifiseringen: **hvilke seksjoner røres,
hva står der i dag (fil + linje), og hva koster endringen mot fasit-bundelen.**
**Foranledning:** `portfolio-optimiser` klarerte 2026-07-25 (05:09Z, punkt 3) generelt
underlag som trygt arbeid som ikke invalideres av at pakken deres holdes: *«preparing GENERAL
underlag (which spec sections those five stories touch, what the frozen text says today) is
safe work that will not be invalidated»*. Dette er det arbeidet.
**Grunnlag:** commons HEAD `a67a243`. Normativ spec-tekst er uendret siden `bfa5a9b`
(ingest-spec, D1-stempelmodellen) / `9801d35` (method-spec §3 Steg 1, Q3-navigasjonen) /
`b641741` (nav-golden-fixtures) — alle 2026-07-21. Hver sitat-blokk under er hentet fra HEAD
og linjeankeret verifisert, ikke gjengitt fra hukommelse.
> **Leseanvisning for konsumenter (tilføyd 2026-07-26).** Alle linjeankere i dette dokumentet
> gjelder **commons HEAD**. Konsumentene er pinnet på eldre `shared/`-subtrær, og et
> linjenummer overlever ikke en pull-grense: `portfolio-optimiser-claude` står på `7aa53fc`,
> der `method-spec.md` er 441 linjer og §12 begynner på `:413` — mot 464 linjer og `:436` her.
> **Sitér seksjon + ordrett tekst; linjenummeret er en bekvemmelighet, aldri ankeret.** Et
> linjenummer sitert over en pull-grense er et *premiss*, ikke et faktum (metodenote fra
> `portfolio-optimiser-claude`, 2026-07-26 — de fant den på dette dokumentets egne sitater).
---
## 0. Køen, avstemt
To korreksjoner fra `portfolio-optimiser` (16:11Z) er **godtatt**, begge verifisert:
1. **Tittel-whitespace er ikke et eget punkt** — det ER amendment-innholdet i S3.5. Vår
telling dobbeltførte det. (Deres kø-linje `(a) commons-amendment tittel-whitespace → S3.5`
ble lest som «punkt ved siden av», ikke «innholdet i».)
2. **B1 er ute av pakken** — commons-eid, ingen adferds-interesse oppstrøms. Bæres separat
(§8 her).
Én korreksjon går **tilbake** oppstrøms, og den er grunnen til at underlaget er verdt å
skrive før pakken lukkes:
3. **De to oppstrøms-enumerasjonene er ikke den samme mengden.** `portfolio-optimiser`
teller fem *stories* (S2.7 / S2.22.4 / S3.2 / S3.5 / S4.0); `portfolio-optimiser-claude`
teller fem *D-A-punkter*. Bare tre par binder (S2.7 = D-A#1, S3.2 = D-A#4, S4.0 = D-A#2).
De to gjenstående D-A-punktene har **ingen story-etikett** oppstrøms:
- **D-A#3 (ledende `/`)** — trenger ingen amendment i det hele tatt. Frossen tekst i
commons HEAD sier allerede det punktet ber om (§3 under). **Årsaken er repo-avhengig**
(begge konsumenter svarte, med hver sin — begge er sanne): drift over pull-grensen hos
`portfolio-optimiser-claude`, målt konformitetsgap i `okf.py` hos `portfolio-optimiser`.
Anbefalingen er den samme uansett hvilken som gjelder hvor.
- **D-A#5 (hovedbok-projeksjoner)** — er reell og har ingen tekst å endre (§6 under), men
mangler story-etikett. Ratifiserer operatøren «pakken» slik `portfolio-optimiser`
beskriver den, faller D-A#5 utenfor.
Netto, fra commons' side: pakken er deres seks **pluss** hovedbok-kontrakten = **sju
ratifiserbare punkter**, med D-A#3 strøket som amendment. B1 utenfor, som avtalt.
Tallet er ikke poenget — membership er.
**S2.22.4 (D-B)** var det ene punktet commons ikke kunne ankre. **Substansen kom 2026-07-26**
og er ført i §7.17.2: D-B er en *scope*-beslutning, ikke spec-tekst. **To av de tre gatede
storiene viste seg UGATED** — S2.2 fordi §4s `http`-punkt normativt plasserer MCP-konnektorer i
`http`-familien (MUST på samme kontrakter), S2.4 fordi berikelsen holder seg innenfor §8s tre
felt. **Kun S2.3 (`doc`) krever amendment.** Begge frigjøringene kom av ankring i frossen tekst,
ingen av dem av en endring.
---
## 1. S2.7 / D-A#1 — nominal feasibility skal GATE, + IR-invariant `low ≤ unit_cost ≤ high`
**Seksjoner:** `method-spec.md` §3 Steg 4 (`:150-166`), §7.1 (`:335-346`), §7.2 (`:348-365`),
§12 (`:458`).
**Hva frossen tekst sier i dag.** Den nominale grensen *beregnes*, og bare `p90` *blokkerer*:
> **`method-spec.md:157-160`** (§3 Steg 4, punkt 2)
> 2. **Feasibility bound:** the maximum feasible saving is capped at a policy fraction
> (0.30) of the affected items' total cost. *(reference: computed with an LP solve whose
> closed form here is `0.30 × Σ quantity·unit_cost`; a missing solver MUST escalate, never
> silently fall back.)*
> **`method-spec.md:163-166`** (§3 Steg 4, punkt 4)
> 4. **Structural block:** a claim above the optimistic feasible bound (`p90`) yields a
> **rejection that is a distinct type from a validated proposal**
Punkt 2 sier `capped at`, men gir ingen avvisningsregel; punkt 4 er den ENESTE blokkeringen
og bruker `p90`. Påstanden «beregnes, men bare p90 blokkerer» holder altså mot teksten.
§7.2 sier det samme eksplisitt om hva assertionen betyr:
> **`method-spec.md:356-358`** (§7.2)
> … The meaningful assertion is `validates` = true (claimed ≤ `p90`); the frozen numbers are
> the regression net.
`nominal_feasible` er et frosset felt (`:352-356`, kryssjekk `:458`) — det er allerede
normativt *rapportert*, bare ikke *gatende*.
**IR-invarianten finnes ikke.** §7.1 har én construction-time-invariant, og den handler om
noe annet:
> **`method-spec.md:343-344`** (§7.1)
> - Construction-time invariant: the claimed saving MUST NOT exceed the affected items' own
> total (`Σ quantity·unit_cost`); violation is a schema error, not a validator rejection.
`assumptions` er definert som band per kostkode (`:341-342`), men ingenting binder
`unit_cost` til å ligge INNI sitt eget band. Hullet er reelt.
**Målt mot fasit-bundelen (`examples/bygg-energi-mikro/`) — begge endringene er byte-nøytrale:**
| Sjekk | Verdi i fasit | Konsekvens |
|---|---|---|
| `claimed_saving_nok` vs. `nominal_feasible` | `30000` vs. `90000.0` | claimed ≤ nominal → en nominal gate endrer ikke `validates` (fortsatt `true`) |
| `unit_cost` vs. eget band | `1.0` i `[0.70, 1.40]` | invarianten er allerede oppfylt → fixture trenger ingen endring |
Kilder: `examples/bygg-energi-mikro/golden.json` (validator-blokken),
`examples/bygg-energi-mikro/validator-input.json` (`affected_items[0]`, `assumptions`).
**Klassifisering:** ekte hull i frossen tekst (begge deler). Gratis mot goldenbytene.
**Operatør-spørsmålet:** skal nominal-grensen gi samme *type* avvisning som p90-blokket
(`:163-166` sier «distinct type … carrying the claimed and feasible figures … and no
percentiles») eller en egen? Teksten har i dag én avvisningsform, og den er definert av p90.
---
## 2. S3.2 / D-A#4 — seedet dom skal nøkles på SINE EGNE features
**Seksjoner:** `method-spec.md` §3 Steg 1, experience fold (`:105-123`).
> **`method-spec.md:108-114`**
> - The retrieval query key is the bundle's candidate features, read from the IR projection
> (§7.1) — available *before* any proposal exists.
> - Seeding: every `type: verdict` file in the bundle becomes a store entry keyed on those
> candidate features, with `decision` from frontmatter (default `approved`) …
`those candidate features` refererer tilbake til forrige kulepunkt — bundelens ENE
IR-projeksjon. Frossen tekst sier altså eksplisitt det punktet vil bort fra: hver seedet dom
arver bundelens projeksjonsnøkkel, ikke sin egen. Hullet er reelt og teksten er entydig.
**Berøringsflate videre:** nøkkelen er det rangeringen (`:115-120`) og id-mintingen
(§4.2 `:254-260`) hviler på. `affected_codes` / `measure_type` / `claimed_saving_nok` er
feltene en «egen-features»-nøkling må komme fra, og §4.2 `:250-252` sier at `description`
bevisst er utenfor både likhet og minting. En amendment her må si hvor en seed-fils egne
features LESES fra (frontmatter? egen projeksjon?) — det er den åpne enden, ikke prinsippet.
**Målt:** fasit-bundelen har én seed (`verdict-led-fro.md`) og én projeksjon, så
`golden.json` kan ikke skille gammel og ny nøkling. Endringen er byte-nøytral her, og
fasiten er derfor ikke et vern mot regresjon på dette punktet.
**Klassifisering:** ekte hull. **Operatør-spørsmålet:** kilden for en seeds egne features.
---
## 3. D-A#3 — ledende `/` : INGEN amendment nødvendig
**Dette punktet er allerede normativt i commons HEAD** (landet `9801d35`, 2026-07-21):
> **`method-spec.md:67-71`** (§3 Steg 1)
> - Navigation starts at `index.md` and follows its intra-bundle markdown cross-links
> (`](target.md)`). A target is resolved **relative to the bundle** and boundary-checked
> fail-closed (below): a leading `/` denotes the **bundle root** (NEVER a filesystem-absolute
> path), any other form is relative to the linking file's own directory — so a target MAY
> address a nested directory (`sub/index.md`, `/a/b.md`).
Og §11 har allerede rød-betingelsen som feiler hvis en implementasjon hopper over den:
> **`method-spec.md:425`** (§11, «Navigation boundary»)
> … an escaping cross-link (`..`, a filesystem-absolute path, or a bundle-root `/` read as
> filesystem-absolute) is followed, a malformed target … is raised instead of skipped, or a
> legitimate nested in-bundle link is skipped
Teksten sier også selv at den ERSTATTET den gamle skip-heuristikken (`:73-74`).
**Klassifisering:** **drift over pull-grensen** — verken spec-hull eller konformitetsavvik.
Ligger et forslag om å «endre» §3 Steg 1 her, endrer det tekst som allerede sier det forslaget
vil oppnå — og risikoen er at ratifiseringen omformulerer en fungerende regel. **Meldt
oppstrøms.**
> **Årsaks-korreksjon (2026-07-26).** Første utgave av dette avsnittet klassifiserte D-A#3 som
> «konformitetsgap i implementasjonen» og skrev at forslaget beskrev implementasjonens
> oppførsel framfor specens. **Det var feil årsak.** `portfolio-optimiser-claude` målte det mot
> eget tre og svarte at deres `shared/method-spec.md` greper 0 for «bundle root» og
> «filesystem-absolute» — fordi teksten landet i `9801d35`, og:
>
> ```
> git merge-base --is-ancestor 9801d35 7aa53fc -> false (reprodusert her)
> ```
>
> Deres split er `7aa53fc`; driften er 15 commits per 2026-07-26. De står i «ikke pullet»
> **bevisst** — avtalen er samme commons-commit i begge repo før noen bygger. D-A#3 var altså
> korrekt formulert mot den frosne teksten de har lov til å se; regelen landet oppstrøms
> etterpå. **Anbefalingen er uendret** (punktet ut av pakken, siden regelen er normativ når
> pakken lander), men den følger av drift, ikke av et avvik hos dem.
>
> Lærdommen generaliserer og er derfor løftet til leseanvisningen øverst: **samme felle venter
> på ethvert punkt målt mot en bevegelig frossen tekst mens konsumentene er pinnet.**
> **Andre korreksjon, samme dag — årsaken er REPO-AVHENGIG, og begge svar er sanne.**
> `portfolio-optimiser` svarte kort etter og klassifiserte D-A#3 som **«et KONFORMITETSGAP i
> implementasjonen vår — ikke et spec-hull»**, med en konkret årsak: *«okf.py hopper over enhver
> lenke med `/`»*. Det er den motsatte klassifiseringen av den `portfolio-optimiser-claude` ga.
>
> **Ingen av dem tar feil.** De beskriver to forskjellige repo:
>
> | Repo | Har `9801d35`? | Årsak der |
> |---|---|---|
> | `portfolio-optimiser-claude` | nei (pinnet `7aa53fc`) | **drift** — regelen finnes ikke i teksten de har |
> | `portfolio-optimiser` | nei (pinnet `7aa53fc`) | **konformitetsgap** — de har målt eget avvik i `okf.py` og har det på operatørkøen som implementasjonssak |
>
> Begge konsumenter står på samme commons-commit (`7aa53fc`, bekreftet av begge uavhengig), så
> pullen er én koordinert handling, ikke en opprydding etter usynk.
>
> **Dette avsnittets egen historie er poenget:** første utgave ga én universell årsak, korreksjonen
> ga en annen universell årsak, og begge var for brede. «Årsaken til D-A#3» er ikke én ting når
> konsumentene er pinnet og har ulik implementasjonstilstand. Anbefalingen har vært uendret hele
> veien — punktet ut av pakken — og den avhenger ikke av hvilken årsak som gjelder hvor.
---
## 4. S4.0 / D-A#2 — kostbaseline-forankring (`cost-baseline.json`)
**Seksjoner:** `method-spec.md` §7 (`:330-333`), §7.1 (`:335-346`), §12 (`:441-464`).
**Frossen tekst har ingen baseline-forankring** — `grep -c 'cost-baseline'` over begge
spec-er: **0 treff**. Det som finnes, og som en ubetinget baseline kolliderer med:
> **`method-spec.md:332-333`** (§7)
> The shared example bundle ships two JSON files that are **the only ground truth** ("fasit")
> for cross-implementation equivalence. Implementations MUST consume them unchanged.
**Målt:** `examples/bygg-energi-mikro/` inneholder nøyaktig to JSON-filer
(`validator-input.json`, `golden.json`) og seks markdown-filer. Ingen baseline. Et ubetinget
krav gjør en tredje fil til ground truth og gjør setningen over usann samtidig — pluss at
fasit-bundelen må utvides, hvilket er en fixture-endring, ikke bare en tekstendring.
`portfolio-optimiser-claude`s egen formulering («åpent om den skal være obligatorisk … et
ubetinget krav endrer golden-bytene») treffer riktig, og §7:332-333 er ankeret som gjør det
konkret.
**Presedens for hvordan en required input formuleres**, hvis den blir obligatorisk:
> **`method-spec.md:345-346`** (§7.1)
> - Loading the IR projection from a bundle is FAIL-FAST: a missing file raises (required
> input — contrast the tolerant inbox, §5).
**Klassifisering:** ekte hull, men det eneste punktet i køen som (hvis ubetinget) **endrer
fasit-bundelen** og ikke bare prosa. **Operatør-spørsmålet:** obligatorisk for alle kjøringer
(→ fixture-endring + §7-setningen må omskrives), eller opsjonell med fail-fast-semantikk kun
når den finnes (→ ren prosa-utvidelse, fasiten uendret)?
---
## 5. S3.5 / D-F — tittel-whitespace
**Seksjoner:** `ingest-spec.md` §4 (`:125`), §5 (`:149-161`), §6 (`:194-197`), §11 (`:278`),
§12 (`:301`).
> **`ingest-spec.md:125`** (§4, extraction-tabellen)
> | `title` | Human-readable title; … Single-line, and MUST NOT contain `[` or `]` … Validated
> fail-fast at manifest load — rendered verbatim thereafter (§5, §6). |
> **`ingest-spec.md:158-161`** (§5, frontmatter-kulepunktet)
> … All values MUST be single-line; `title` is emitted **verbatim** (it is `[`/`]`-free by §4,
> so verbatim rendering is safe — the invariant is met by validation, not repair); the
> materializer MUST collapse whitespace runs (including newlines) in `source_query` to single
> spaces.
Formen på hullet, presist: **collapse er scoped til `source_query` alene**, og `title` er
uttrykkelig `verbatim` med invarianten «met by validation, not repair». En tittel med interne
whitespace-runs er derfor ikke normalisert noe sted — den er bare «single-line», og hva
`single-line`-valideringen dekker (bare `\n`? `\r`? ledende/etterfølgende blanke?) står ikke.
Amendment-punktet ligger i den sømmen, og valget er **validere hardere** vs. **reparere**
teksten har i dag valgt validering for `title` og reparasjon for `source_query`.
**⚠ Anker-avvik oppstrøms (viktig for denne teksten spesielt).** `portfolio-optimiser` ankret
sitatet til `shared/ingest-spec.md:140`. Det linjenummeret er commons ved **`7aa53fc`**, ikke
HEAD:
- ved `7aa53fc`, linje 140: `` `generated`. All values MUST be single-line; the materializer MUST collapse whitespace runs ``
- ved HEAD (`bfa5a9b`): samme setning er splittet over `:158` og `:160-161`, og `title is
emitted verbatim` + «validation, not repair» er **ny tekst** fra `bfa5a9b`.
`bfa5a9b` (+39/9) skrev om nettopp dette kulepunktet OG `title`-raden i §4 (som gikk fra
bare «Single-line.» til dagens `[`/`]`-forbud + fail-fast + verbatim). Deres `shared/`-kopi
ser altså ut til å ligge ett commons-commit bak, fra FØR D1-stempelmodellen landet. Det
betyr at S3.5 kan være formulert mot tekst som ikke lenger finnes i den formen — og det er
samme risiko de selv navnga («pullen må være koordinert, samme commons-commit»). **Meldt
oppstrøms.**
**Klassifisering:** ekte, men smal søm. Ingen fasit-effekt her (våre fixtures har ingen
ingest-manifester; goldenbundlene for ingest bor i implementasjonene, jf. §11 `:259-261`).
---
## 6. D-A#5 — projeksjoner over besparelses-hovedboken (INGEN eksisterende tekst)
**Målt:** `grep -ci 'ledger'` og `grep -ci 'hovedbok'` over `method-spec.md` +
`ingest-spec.md`**0 og 0**. Ingen monetær avrundingsregel finnes heller: alle 13
forekomster av `round` (12 linjer: 9 i method-spec, 3 i ingest-spec) er andre ting
(`round-capped` debatt `:141`/`:386`, `max_rounds` `:378`/`:385`, `shortest round-trip
decimal` `ingest-spec.md:165`, `round-trip` `ingest-spec.md:85`, `around` `:323`).
Nærmeste eksisterende flater, som en ny kontrakt må forholde seg til uten å kollidere:
- **`method-spec.md:367-372`** (§7.2 `learning_surface`) — de monetære feltene som ER frosset
(`modelled_saving_nok`, `expected_actual_saving_nok`, intern konsistens påkrevd).
- **`method-spec.md:199-202`** (§3 Steg 6) — råresultater er output-laget, «plain JSON», og
bruker bevisst IKKE wiki-formatet.
- **`method-spec.md:250-252`** (§4.2) — `claimed_saving_nok` som tall i verdict-kontrakten,
og `description` bevisst utenfor likhet/minting.
- **`method-spec.md:436-464`** (§12) — kryssjekk-tabellen er *fullstendighets-håndhevet* av
spec-integritetstesten (§11 `:434`), så en ny kontrakt med nye felter MÅ inn her, ellers
feiler den testen. Dette er den mekaniske konsekvensen ingen av oppstrøms-meldingene nevner.
**Klassifisering:** ny seksjon, ikke en amendment. Dette er køens største punkt (den eneste
som utvider spec-ens virkeområde) og samtidig den uten story-etikett oppstrøms — mest utsatt
for å falle mellom de to enumerasjonene. **Meldt oppstrøms.**
**Note, videreformidlet ikke verifisert her:** `portfolio-optimiser` melder (16:11Z, pkt. 4)
at forslagets A5-regel 1 («Monetary figures MUST NOT be rounded in the projection») treffer
en persisterings-kant hos dem (NOK→øre-kvantisering) og bør skille projeksjons-aritmetikk fra
enhets-kvantisering. Commons har ikke lest deres kode og fører det som deres måling, ikke som
vårt faktum.
---
## 7. S2.22.4 / D-B — substansen er ikke ankret hos commons
Punktet er kjent bare som etikett. Vi leste det ut av `portfolio-optimiser`s STATE
(GATES-blokk) 2026-07-25 04:05Z og har aldri fått innholdet. `grep -rn 'D-B'` over dette
repoet gir **0 treff** — heller ikke i vår egen STATE, som bare bærer story-etiketten
`S2.2-2.4`. Koblingen `S2.22.4 (D-B)` finnes utelukkende i coord-arkivet
(`20260725T040535Z`, vår egen melding), ikke i noen commons-tekst.
Commons kan derfor ikke si hvilken seksjon det rører. **Ingen antakelse føres.** Dette er den
ene raden i underlaget som er blank av en grunn — og den blir stående blank til substansen
kommer. **Etterspurt oppstrøms.**
### 7.1 Substansen ER LEVERT (2026-07-26) — raden er ikke lenger blank
`portfolio-optimiser` leverte D-B-substansen på direkte forespørsel. Kilde:
`docs/plan/2026-07-10-sesjonsplan-fase2-6.md` **i deres repo, ikke commons** — gjengitt her som
mottatt, ikke verifisert av oss (vi har ikke lesetilgang til premisset, og fører det som
*rapportert*, ikke *målt*).
**D-B er en SCOPE-beslutning, ikke en spec-tekst.** Det er den avklaringen som gjør raden
meningsfull: vi lette etter hvilken seksjon den rører, og svaret er at *den selv* ikke rører
noen — den avgjør om tre andre stories gjør det. Fire delbeslutninger (`:56-62`): (1) amender
ingest-målbildet bevisst vs. nedskopér roadmap-B til mock-herding *(deres anbefaling: nedskopér
nå)*; (2) første live-kilde *(anbefaling: SQL — `read_sql` er mest herdet, read-only by
construction)*; (3) dokument-konnektor-avgrensning *(PDF via `pypdf` nå, DOCX utsatt)*;
(4) MCP-kildefamiliens nettverks-/subprosess-grant-form.
**Konsekvensen for commons — hvilke av de tre gatede storiene som faktisk krever amendment:**
| Story | Krever ingest-spec-amendment? | Hva |
|---|---|---|
| **S2.2** MCP-konnektor | **NEI — UGATED** (se §7.2) | §4s `http`-punkt (HEAD `:113-114` = pin `:101-102`) sier normativt at en MCP-konnektor **er en utvidelse av `http`-familien** med MUST på samme kontrakter. Ingen ny familie, ingen amendment. Bygges mot `http` + §8-grant |
| **S2.3** Dokument-konnektor | **JA** — ny kildefamilie | `{type: "doc"}`. PDF→OKF-konseptfil med provenance, deterministisk mot committede fixtures |
| **S2.4** Konnektor-herding | **NEI — UGATED** (bekreftet av po) | Timeout-parameter, feilkategorisering, beriket §8-logg (kilde/tidspunkt/radantall — aldri innhold). Retry KUN hvis D-B sier ja (default nei, av hensyn til determinisme). Inkrementell re-ingest: **ikke**, med mindre D-B amender §8 |
**Commons' observasjon, ikke et forslag:** §4 har i dag tre kildetyper (`file`, `sql`, `http`),
og §1 (`:27-31`) binder konformans til `file` + `sql` som påkrevde med `http` som eksplisitt
OPTIONAL utvidelsespunkt. To nye familier reiser derfor et spørsmål ingen av meldingene har
stilt: **blir `mcp` og `doc` påkrevde for konformans, eller valgfrie som `http`?** Svaret
avgjør om `:27-31` må skrives om i samme amendment. Vi fører ingen antakelse om hvilket.
**⚠️ Og frossen tekst sier allerede noe om MCP som ingen av meldingene har nevnt.** Ordrett,
`ingest-spec.md:29-31`:
> The `http` source type is an OPTIONAL extension point: implementing it (e.g. against a local
> mock, **or via an MCP-based connector**) does not require any change to this spec, and NOT
> implementing it does not break conformance.
Specen forutser altså eksplisitt at MCP kan være **transporten under `http`**, og sier at den
veien **ikke krever noen spec-endring**. Det gjør S2.2s premiss — «krever ingest-spec-amendment
i commons FØRST» — til noe som må avgjøres, ikke antas:
- Er `mcp` en **fjerde kildefamilie** (nytt `{type: "mcp"}`-skjema, amendment nødvendig), eller
en **transport for den eksisterende `http`-familien** (allerede dekket, ingen amendment)?
- Distinksjonen er ikke akademisk: den avgjør om S2.2 er blokkert på commons i det hele tatt.
Under den andre lesningen er S2.2 **ugated** og kan bygges nå, mot `http` + opt-in-flagget (§8).
### 7.2 Spørsmålet er AVGJORT av frossen tekst — det er ikke to forsvarlige svar (korr. 2026-07-26)
Avsnittet over stilte (a)/(b) som et åpent valg. **Det var feil, og `portfolio-optimiser` fant
hvorfor: det finnes et ANDRE anker vi ikke siterte, og det er normativt.**
> **`ingest-spec.md` §4, kildetypelisten, `type: "http"`-punktet** — sitert ORDRETT fordi
> linjenumrene ikke overlever pull-grensen (se rammen under):
> `type: "http"` — a remote endpoint (OPTIONAL extension point, §1). Field `base_url`: the
> endpoint base; it MUST NOT embed credentials. Optional field `credential_ref`: the NAME of
> a runtime-resolved secret reference. **An MCP-based connector is an extension of this family
> and MUST honour the same extraction, materialization, and gate contracts.**
> **⚠️ Ref-binding for sitatet over (korr. 2026-07-27, meldt av `repos`).** Avsnittet siterte
> tidligere «`:112-114`» uten ref. To feil i én: (1) numrene er HEAD-relative, og konsumentene
> står på `7aa53fc`; (2) selv mot HEAD var de av med én linje. Målt:
>
> | Ref | MCP-setningen | Hele `http`-punktet |
> |---|---|---|
> | commons HEAD | `:113-114` | `:111-114` |
> | `7aa53fc` (konsumentenes pin, verifisert identisk i BEGGE arbeidstrær) | `:101-102` | `:99-102` |
>
> **Failure-moden er grunnen til at dette er verdt en korreksjon:** en konsument som slår opp
> `:112-114` i SIN fil finner ikke ingenting — den finner felt-tabellen for `extractions`
> (`id`/`title`/`query`), troverdig og relatert spec-tekst uten et ord om MCP. Den mest
> sannsynlige konklusjonen er da at ankeret ikke finnes og at kjennelsen hviler på løs grunn.
> Et åpenbart tomt treff hadde vært tryggere. **Regel, samme klasse som `git log -S`-regelen
> under: sitér SEKSJON + ORDRETT TEKST når mottakeren står på en annen ref — eller skriv
> hvilken ref numrene gjelder.**
`:29-31` sier at MCP-veien ikke krever spec-endring. `http`-punktet sier **normativt hvor den
hører hjemme** — som en utvidelse av `http`-familien, med **MUST** på de samme kontraktene.
Frossen tekst har altså allerede valgt (b). En fjerde `{type: "mcp"}`-familie ville **motsagt**
MCP-setningen, ikke utfylt den.
**Konsekvens: S2.2 er UGATED.** Premisset «krever ingest-spec-amendment i commons FØRST» er
avkreftet av vår egen frosne tekst. S2.2 kan bygges mot `http`-familien + §8s opt-in-grant uten
å vente på noe herfra.
**Og §1-spørsmålet i 7.1 faller bort for MCP:** MCP arver `http`s status, og `http` er eksplisitt
OPTIONAL (`:27-31`). `:27-31` trenger derfor **ingen** omskriving for MCP. Spørsmålet gjenstår
kun for **S2.3 (`doc`)**, som fortsatt er en genuint ny familie. `portfolio-optimiser`s
anbefaling der: valgfri som `http` — en konform implementasjon bør ikke tvinges til en
PDF-parser. Det er deres anbefaling til ratifisering, ikke en beslutning.
**(a)-argumentet lever videre, men som implementasjonssak, ikke spec-tekst.** At
`base_url`/`credential_ref` passer dårlig på en kommando, og at env-var-med-kommando er en
subprosess-grant, er ekte friksjon — men MCP-setningen sier MUST på *kontraktene*, ikke på
*feltnavnene*, og §8 dekker «any other non-local transport». Friksjonen lever i hvordan en
kommando uttrykkes innenfor `http`-familien. Støter implementasjonen faktisk på noe MCP-setningen
forbyr, er det et målt funn og kommer tilbake hit.
**Vår egen feil, ført åpent fordi den er tredje instans samme dag:** vi skrev at `:29-31` var
«frossen siden `bfa5a9b`». Målt med `git log -S`: **begge** MCP-ankrene landet i **`7aa53fc`** —
nøyaktig commit-en konsumentene står på. `bfa5a9b` rørte bare D1-stempelmodellen. Vi tok repoets
*siste* ingest-spec-commit og antok at filen daterte derfra. Og vi fant ikke `:113` fordi vårt
`grep -n 'connection_ref' | head -3` stoppet på `:109` uten å lese omgivelsene — setningen i §1
er dessuten linjebrutt (`:30-31`), så et grep på hele frasen ville også bommet.
**Sikt grepet mot seksjonen, ikke mot ett token — og bekreft opphav med `git log -S`, aldri med
«siste commit som rørte filen».**
Merk også at S2.4s «beriket ingest-logg» ligger mot §8, som allerede sier *«Source calls are
logged (which source, when, row count)»* — en berikelse innenfor de tre feltene er neppe en
tekstendring; en berikelse *utover* dem er det. Grensen går ved «aldri innhold», som deres egen
formulering allerede respekterer.
---
## 8. B1 — nav-golden-klassen (utenfor pakken, commons-eid)
Ankret, etter å ha vært et etikett-punkt i tre uker. `llm-ingestion-okf` svarte
`portfolio-optimiser` 2026-07-25 04:19Z at «B1 bundle class» ikke finnes hos dem, og at
beskrivelsen matcher **D4 fra OKF-runden, ratifisert i trinn F 2026-07-21**: commons eier
nav-golden-bundleklassen + forventet utfall og leverer den inn; catalog eier
korpus-containeren, de adversarielle aksene og runner/gate.
**Commons-halvdelen er levert** (`b641741`, 2026-07-21):
| Case | Innhold |
|---|---|
| `examples/nav-golden-escape/` | `bundle/`, `expected-read-context.md`, `README.md`, `SHOULD-NOT-BE-READ.md` |
| `examples/nav-golden-hierarchy/` | `bundle/` (nestet `a/b/`, `c/orphan.md`), `expected-read-context.md`, `README.md` |
**Det åpne spørsmålet er ett, og det er vårt:** `grep -c 'nav-golden'` i `method-spec.md`,
`ingest-spec.md`, `CONCEPT.md`, `README.md` og `skills/expert-reviewer/SKILL.md` → **0 i alle
fem**. Klassen er levert som fixture, men ingen normativ seksjon peker på den — i motsetning til
`validator-input.json` / `golden.json` (navngitt i §7 `:330-346`) og
`examples/ingest-golden-{source type}/` (navngitt som konvensjon i `ingest-spec.md:259-261`).
§11-raden «Navigation boundary» (`:425`) beskriver rød-betingelsen, men nevner ikke fixturene
som beviser den.
**Operatør-spørsmålet:** skal nav-golden-klassen få en normativ referanse (§7 ground truth
og/eller §11-raden), eller forbli en informativ fixture? Dette er commons' eget punkt, ingen
venter på oss, og det hører IKKE inn i oppstrøms-pakken.
**Utskrevet i sin helhet:** `docs/plan/2026-07-25-b1-nav-golden-normative-status.md` — fire
opsjoner med målt kostnad. Spørsmålet viste seg ikke å være binært: specen har **tre**
referanseformer i bruk (artefaktnavn som fasit, katalogkonvensjon, informativ lenke), og
method-spec navngir aldri en repo-sti (`grep -c 'examples/' method-spec.md` → 0).
---
## 9. Sammendrag
| # | Punkt | Fil + seksjoner | Klassifisering | Fasit-effekt |
|---|---|---|---|---|
| 1 | S2.7 / D-A#1 nominal gate + IR-band | method §3.4 `:157-166`, §7.1 `:339-346`, §7.2 `:352-358` | ekte hull | ingen (målt) |
| 2 | S3.2 / D-A#4 seed-nøkkel | method §3.1 `:108-114` | ekte hull | ingen (fasiten skiller ikke) |
| 3 | D-A#3 ledende `/` | method §3.1 `:67-71`, §11 `:425` | **allerede normativt i HEAD — drift, ikke avvik** | — |
| 4 | S4.0 / D-A#2 kostbaseline | method §7 `:332-333`, §7.1 `:345-346` | ekte hull | **endrer fasit hvis ubetinget** |
| 5 | S3.5 / D-F tittel-whitespace | ingest §4 `:125`, §5 `:158-161`, §6 `:194-197`, §11 `:278` | ekte, smal søm | ingen |
| 6 | D-A#5 hovedbok-projeksjoner | ingen tekst; naboer method §7.2 `:367-372`, §12 `:436-464` | **ny seksjon** | ingen direkte; §12 må utvides |
| 7 | S2.22.4 / D-B | **ANKRET** (§7.17.2): kun **S2.3** (`doc`) krever amendment. **S2.2 + S2.4 er UGATED** — S2.2 av §4s `http`-punkt, S2.4 innenfor §8s tre felt | **scope-beslutning, ikke spec-tekst** | ingen |
| 8 | B1 / D4 nav-golden | levert `b641741`; 0 spec-referanser | commons-eget, utenfor pakken | — |
Commons' rolle er uendret: vi forbereder underlaget, operatøren ratifiserer, og frossen tekst
endres ikke uten den ratifiseringen.
---
## Verifiseringslogg
| Påstand | Sjekk | Resultat |
|---|---|---|
| Spec-tekst uendret siden 2026-07-21 | `git log -- method-spec.md ingest-spec.md` | `9801d35` / `bfa5a9b`, begge 07-21 |
| Bare p90 blokkerer i dag | lest `method-spec.md:150-166` i sin helhet | punkt 2 «capped», punkt 4 eneste blokk |
| Ingen `low ≤ unit_cost ≤ high`-invariant | lest §7.1 `:339-346` | én invariant, om claimed vs. total |
| Nominal gate er byte-nøytral | `golden.json`: claimed `30000` ≤ nominal `90000.0` | `validates` uendret `true` |
| IR-band-invarianten er alt oppfylt i fasit | `validator-input.json`: `1.0``[0.70, 1.40]` | ingen fixture-endring |
| Seeding nøkles på bundelens projeksjon | lest `:108-114` | «keyed on those candidate features» |
| Ledende `/` alt normativt | `git log -S'denotes the **bundle root**'` | `9801d35`, 2026-07-21 |
| §11 har alt rød-betingelsen | `method-spec.md:425` | «bundle-root `/` read as filesystem-absolute» |
| Ingen hovedbok-/baseline-tekst | `grep -ci ledger\|hovedbok\|cost-baseline` | 0 / 0 / 0 |
| Ingen monetær avrundingsregel | `grep -n round` begge spec-er, alle 18 treff lest | alle urelaterte |
| Fasit-bundelen har to JSON-filer | `ls examples/bygg-energi-mikro/` | `validator-input.json`, `golden.json` |
| Oppstrøms-anker `:140` er stale | `git show 7aa53fc:ingest-spec.md \| grep -n` | treff på `:140` ved `7aa53fc`, `:158/:160` ved HEAD |
| `bfa5a9b` rørte nettopp den teksten | `git show bfa5a9b -- ingest-spec.md` | +39/9; §5-kulepunkt + §4 `title`-rad omskrevet |
| Multi-manifest er extension point | `ingest-spec.md:173-174` | «Version 1 assumes ONE manifest per bundle» |
| B1 = D4, ikke okf-eid | `~/.claude/coord/portfolio-optimiser/archive/20260725T041948Z-*` | okf: «never owned here; it is D4» |
| nav-golden levert | `git log -- examples/nav-golden-*` | `b641741`, 2026-07-21 |
| nav-golden ikke normativt referert | `grep -c 'nav-golden'` i de fem normative filene | 0 i alle fem |
| D-B ikke ankret hos commons (per 07-25) | `grep -rn 'D-B'` | 0 treff utenfor STATE |
| D-B-substansen levert 07-26 | coord fra `portfolio-optimiser` | rapportert fra deres `sesjonsplan-fase2-6.md:56-62`, ikke målt av oss |
| §1 gjør `http` OPTIONAL, `file`+`sql` påkrevd | `ingest-spec.md:27-31` | ordrett sitert i §7.1 |
| …og nevner MCP som `http`-transport | `ingest-spec.md:29-31` | «or via an MCP-based connector … does not require any change to this spec» |
| **ANDRE anker: MCP hører NORMATIVT til `http`** | `ingest-spec.md` §4, `http`-punktet (HEAD `:113-114` = pin `:101-102`) | «An MCP-based connector **is an extension of this family** and **MUST** honour the same … contracts» |
| Begge MCP-ankere landet i `7aa53fc`, ikke `bfa5a9b` | `git log -S` på hver frase | `7aa53fc` for begge — altså i konsumentenes egen kopi |
| `bfa5a9b` rørte kun stempelmodellen | `git show --stat bfa5a9b` | 39 innsettinger, D1-stempel |
| §8 logger tre felter | `ingest-spec.md:230` | «which source, when, row count» |
| `connection_ref` er env-var-NAVN | `ingest-spec.md:109`, `:117` | «the NAME of a runtime-resolved …», secret resolved at run time |
| Begge konsumenter står på `7aa53fc` | coord fra begge, uavhengig | po: `git log --grep=git-subtree-split``ef1a2c5`; po-claude: egen måling |

View file

@ -0,0 +1,238 @@
# B1 / D4 — skal nav-golden-klassen ha en normativ referanse?
**Status: AVGJORT OG UTFØRT 2026-07-31 — operatøren valgte O1** (form C: informativ kataloglenke
fra `README.md`). Utført i samme økt; se **§5.1**. Underlaget under står uendret som grunnlaget
vedtaket ble tatt på.
Opprinnelig status: operatør-spørsmål. Dette dokumentet endrer **ingen** normativ tekst og foreslår
ingen. Det svarer på det commons kan svare på uten å foregripe en ratifisering: hva som
allerede er avgjort, hva tilstanden er i dag (fil + linje), hvilke referanseformer specen
faktisk har, og hva hver av dem koster å ta i bruk her.
**Grunnlag:** commons HEAD `29ad8ca`. Normativ spec-tekst uendret siden `9801d35`
(method-spec) / `bfa5a9b` (ingest-spec), fixturene siden `b641741` — alle 2026-07-21. Alle
linjeankere under er lest fra HEAD, ikke gjengitt fra hukommelse.
**Avgrensning:** dette er commons' eget punkt. Det står **utenfor** amendment-pakken
(`portfolio-optimiser` bekreftet 2026-07-25 at B1 ikke har adferds-interesse oppstrøms), og
ingen venter på oss. Det kan avgjøres når som helst, også etter pakken.
---
## 1. Det som allerede er avgjort — ikke oppe til vurdering
D4 fra OKF-runden, ratifisert i trinn F 2026-07-21, deler arbeidet i to:
- **commons eier** nav-golden-bundleklassen og det forventede utfallet, og leverer den inn.
- **catalog eier** korpus-containeren, de adversarielle aksene og runner/gate.
`llm-ingestion-okf` bekreftet 2026-07-25 04:19Z at klassen aldri var deres, og at beskrivelsen
matcher D4. Commons-halvdelen er **levert** i `b641741`:
| Case | Innhold | Rolle |
|---|---|---|
| `examples/nav-golden-hierarchy/` | `bundle/` (nestet `a/b/`, `c/orphan.md`), `expected-read-context.md`, `README.md` | positiv: ni Q3-beslutninger utøves |
| `examples/nav-golden-escape/` | `bundle/`, `expected-read-context.md`, `README.md`, `SHOULD-NOT-BE-READ.md` | negativ: porten må kunne gå rød |
Splitten, eierskapet og fixturenes innhold er altså ikke spørsmålet. Spørsmålet er hva
spec-teksten sier om dem — i dag: ingenting.
---
## 2. Tilstanden i dag, verifisert
Ingen normativ fil nevner klassen. Ikke «få treff» — null, i hver enkelt:
| Fil | `grep -c 'nav-golden'` |
|---|---|
| `method-spec.md` | 0 |
| `ingest-spec.md` | 0 |
| `CONCEPT.md` | 0 |
| `README.md` | 0 |
| `skills/expert-reviewer/SKILL.md` | 0 |
Fixturene er dermed i dag rene artefakter: de finnes, de er dokumentert i sine egne
`README.md`-filer, og ingenting utenfor katalogen deres viser til dem.
**Merk om sjekken:** `grep -rn 'nav-golden' *.md` gir i dag **3** treff, alle i `STATE.md`
(LOCAL-ONLY, gitignored, ikke-normativ). Underlagets verifiseringslogg
(`2026-07-25-amendment-underlag.md:350`) noterte «0 treff» på nettopp den kommandoen, og den
reproduserer ikke lenger. Påstanden er uendret sann — sjekken var for vid. Korrigert i samme
commit som dette dokumentet.
---
## 3. Specen har tre referanseformer, ikke én
Dette er kjernen, og grunnen til at spørsmålet ikke er binært. «Normativ referanse» er ikke
én ting i dette repoet — tre distinkte former er allerede i bruk, med forskjellig
bindingskraft:
| | Form | Hvor den er brukt | Hva den binder |
|---|---|---|---|
| **A** | **Artefaktnavn som fasit** | method-spec §7 `:332-333`: *«The shared example bundle ships two JSON files that are the only ground truth ("fasit") … Implementations MUST consume them unchanged»* | navngir `validator-input.json` / `golden.json`; konformans-krav på verdiene |
| **B** | **Katalogkonvensjon + oppføringstabell** | ingest-spec §11 `:259-267`: *«Golden extraction format: one directory per case, convention `examples/ingest-golden-{source type}/`»* + tabell over påkrevde oppføringer | navngir et **format** for en case-klasse, ikke enkeltcaser |
| **C** | **Informativ kataloglenke** | `README.md:19``examples/bygg-energi-mikro/`, beskrevet som *«a small dev fixture»* | oppdagbarhet; null konformansvekt |
nav-golden har i dag **ingen** av de tre. Det er den presise mangelen — skarpere enn «0 treff».
**En egenskap ved method-spec som begrenser opsjonsrommet:** den navngir **aldri en katalogsti
i repoet**. `grep -c 'examples/'` → 0, `grep -c 'bygg-energi-mikro'` → 0. Den navngir filnavn
(`validator-input.json`, `golden.json`, `test_okf.py`) og aldri hvor de bor. De ti
`/`-holdige tokenene i filen er lenke-syntaks *inne i* en bundle (`sub/index.md`, `/a/b.md`),
ikke repo-stier. En form B-referanse i method-spec ville være den **første** repo-stien i det
dokumentet.
---
## 4. Spørsmålet
**Skal nav-golden-klassen gis en normativ referanse, og i så fall i hvilken form?**
Underspørsmål som følger med, uansett svar: hvis klassen blir normativ, må serialiserings-
sammenligningen pinnes? (Se O2 og O3 under — det er den ene konsekvensen som ikke er
kosmetisk.)
---
## 5. Opsjonene, med målt kostnad
| | Opsjon | Berørt tekst | Frossen tekst endres? | Reverserbar |
|---|---|---|---|---|
| **O0** | Forbli informativ fixture (status quo) | ingen | nei | — |
| **O1** | Form C: lenke fra `README.md` | `README.md:19`-blokken | nei (README er ikke normativ) | trivielt |
| **O2** | Form B: katalogkonvensjon i method-spec §11 | ny avsnitt/rad ved `:425` | **ja**, tillegg | middels |
| **O3** | Form A: løft til fasit i §7 | `§7:332-333` **og** `§1:23` | **ja**, omskriving av tellende påstand | dyrt |
### O0 — forbli informativ fixture
Ingen endring. Fixturene er fortsatt levert, catalog kan fortsatt bygge runner/gate på dem,
og D4s commons-halvdel er fortsatt oppfylt (D4 krevde levering, ikke normativ referanse).
Kostnaden er at bindingen mellom §11-raden `:425` og beviset for den bare finnes i catalogs
runner og i fixturenes egne README-er — ingen spec-tekst sier at de hører sammen.
### O1 — form C, lenke fra README
Billigst mulige forbedring av oppdagbarhet. `README.md` er katalog, ikke kontrakt, så ingen
implementasjon får en ny plikt. Løser ikke bindingsproblemet i O0; gjør det bare synlig.
### O2 — form B, katalogkonvensjon i method-spec §11
Den formen som passer klassen best: en nav-golden-case *er* en formatklasse (`bundle/` inn,
`expected-read-context.md` ut), akkurat som ingest-golden er det. Naturlig plassering er ved
**Navigation boundary**-raden `:425`, som i dag beskriver rød-betingelsen og siterer
referansetesten `test_okf.py (navigation boundary)`, men ikke navngir noe korpus som beviser
den.
Tre konkrete kostnader, alle verifisert:
1. **Første repo-sti i method-spec** (se §3). Enten aksepteres det som et bevisst brudd, eller
så må konvensjonen formuleres uten sti — og da mister den det meste av verdien.
2. **Oppføringstabellen blir ikke uniform.** ingest-goldens tabell (`:262-267`) har fire
påkrevde oppføringer og ingen valgfrie. nav-golden-casene har **ulike** oppføringssett:
escape-casen trenger `SHOULD-NOT-BE-READ.md` som bevisst ligger *utenfor* `bundle/` (den
er lokkematen `../SHOULD-NOT-BE-READ.md` peker på), hierarchy-casen har ingen slik fil. En
felles konvensjon må derfor ha en valgfri rad — noe ingest-golden ikke har presedens for.
3. **Serialiseringen er ikke pinnet.** Dette er den reelle. `nav-golden-hierarchy/README.md`
sier eksplisitt: *«The spec does not pin whitespace beyond this shape; a gate MAY compare
byte-exact or after trailing-whitespace normalization.»* ingest-goldens motstykke er
pinnet: `:267` *«compared file by file, byte for byte»* og §11-raden `:280` *«any byte …
diverges»*. Gjør man nav-golden normativ i form B uten å ta stilling, arver klassen en
`MAY` der søsterklassen har en `MUST` — to konforme porter kan da være uenige om samme
fixture. Enten pinnes sammenligningen, eller så uttales `MAY`-en som bevisst.
### O3 — form A, løft til fasit i §7
Dyrest, og kostnaden er ikke størrelsen på diffen, men at to **tellende** påstander må
skrives om:
- `§7:332-333`: *«The shared example bundle ships **two** JSON files that are **the only**
ground truth»* — «two» og «the only» er begge usanne i det øyeblikket en tredje ground
truth finnes. I tillegg er setningen scoped til **én** bundle («the shared example
bundle»), mens nav-golden-casene er *andre* bundler.
- `§1:23`, konformansledd 2: *«reproduces the shared golden suite's decided outcomes (§7) on
the shared example bundle»* — samme entallsbinding, i selve konformansdefinisjonen.
O3 er altså ikke et tillegg, men en utvidelse av hva «fasit» betyr i denne specen, fra én
bundle til en mengde. Det er en reell arkitekturbeslutning, ikke en redaksjonell.
Serialiseringspoenget fra O2 punkt 3 gjelder her med full tyngde: en fasit med en `MAY` i
sammenligningsregelen er ikke en fasit.
### 5.1 Svaret, og hva som faktisk ble gjort (2026-07-31)
**Vedtatt: O1** — form C. Én oppføring i `README.md`s «Contents (growing)»-liste, i samme form
som `examples/bygg-energi-mikro/``:19`, som lenker begge casene og peker på deres egne
`README.md`-filer for detaljene.
**Frossen tekst er ikke rørt, og klassen har ikke fått bindingskraft.** Verifisert etter
endringen: `grep -c 'nav-golden'` gir fortsatt **0** i `method-spec.md`, `ingest-spec.md`,
`CONCEPT.md` og `skills/expert-reviewer/SKILL.md`. Bare `README.md` gikk fra 0 til treff, og
README er katalog, ikke kontrakt (§3, form C: «oppdagbarhet; null konformansvekt»). Ingen
implementasjon har fått en ny plikt. `git status` viste `README.md` som eneste endrede fil.
**Oppføringen sier eksplisitt at den er informativ**, og at sammenligningsregelen ikke er pinnet
— nettopp for å hindre den ene misforståelsen O1 kunne skapt: at en lenke fra rot-README gjør
klassen til fasit i den forstand method-spec §7 bruker ordet.
**Det O1 med vilje IKKE løser** (uendret fra §5s beskrivelse): bindingsproblemet i O0 står. Ingen
spec-tekst sier at §11-raden `:425` og disse fixturene hører sammen — O1 gjør mangelen synlig,
ikke borte. Å lukke den krever O2/O3 og en ratifiseringsrunde.
**Serialiseringsspørsmålet er fortsatt åpent, og er uavhengig av dette vedtaket** (§6): to
implementasjoner som sammenligner ulikt vil være uenige om nav-golden enten specen nevner klassen
eller ikke. Det kan avgjøres separat og tidligere, og O1 verken løser eller foregriper det.
---
## 6. Hva commons vil peke på (ikke en anbefaling)
Én asymmetri er verdt å ha for hånden når spørsmålet avgjøres: **O0 og O1 er reverserbare,
O2 og O3 er det ikke på samme måte.** Frossen tekst i dette repoet endres bare ved
ratifisering, og en normativ referanse som senere skal fjernes koster en ny runde. Motsatt
koster det ingenting å utsette O2/O3 — fixturene er levert, catalog kan bygge på dem uansett,
og ingen implementasjon blokkeres av at referansen mangler.
Serialiseringsspørsmålet (O2 punkt 3) er derimot **uavhengig** av om referansen kommer: to
implementasjoner som sammenligner ulikt vil være uenige om nav-golden enten specen nevner
klassen eller ikke. Det kan avgjøres separat, og tidligere.
---
## 7. Det som ikke er commons' å avgjøre
- **Runner og gate** — catalogs eierskap per D4. Om porten sammenligner byte-eksakt eller
normalisert er catalogs implementasjonsvalg *inntil* specen pinner det; da blir det vårt.
- **De adversarielle aksene** og korpus-containeren — catalog.
- Ingenting her berører amendment-pakken. Køpunkt 17 i
`2026-07-25-amendment-underlag.md` står uendret.
---
## 8. Det som ikke endres uansett utfall
Fixturene selv. `b641741` er levert og korrekt, og ingen av opsjonene over foreslår å røre
`bundle/`, `expected-read-context.md` eller case-README-ene. Spørsmålet gjelder utelukkende
om — og hvordan — spec-teksten viser til dem.
---
## Verifiseringslogg
| Påstand | Sjekk | Resultat |
|---|---|---|
| Fixturene levert 2026-07-21 | `git log --oneline -- examples/nav-golden-*` | `b641741`, eneste commit |
| Ingen normativ fil nevner klassen | `grep -c 'nav-golden'` per fil | 0 / 0 / 0 / 0 / 0 |
| Den siterte sjekken reproduserer ikke | `grep -rn 'nav-golden' *.md` | 3 treff, alle `STATE.md` |
| method-spec navngir ingen repo-sti | `grep -c 'examples/' method-spec.md` | 0 |
| method-spec navngir ikke fasit-bundelen | `grep -c 'bygg-energi-mikro' method-spec.md` | 0 |
| `/`-tokenene er lenke-syntaks, ikke stier | `grep -oE '\`[^\`]*/[^\`]*\`' method-spec.md`, alle 10 lest | `sub/index.md`, `/a/b.md`, `./a.md`, `/` |
| §7 sier «two» og «the only» | `method-spec.md:332-333` | ordrett sitert over |
| §1 binder konformans til én bundle | `method-spec.md:23` | «on the shared example bundle» |
| §11-raden navngir ingen fixture | `method-spec.md:425` | seam + rød-betingelse + `test_okf.py` |
| ingest-golden er en katalogkonvensjon | `ingest-spec.md:259-267` | «one directory per case, convention …» + 4-rads tabell |
| ingest-golden er byte-pinnet | `ingest-spec.md:267`, `:280` | «byte for byte» / «any byte … diverges» |
| nav-golden er IKKE byte-pinnet | `examples/nav-golden-hierarchy/README.md:31-33` | «a gate MAY compare byte-exact or after … normalization» |
| Escape-casen har fil utenfor `bundle/` | `ls examples/nav-golden-escape/` | `SHOULD-NOT-BE-READ.md` er søsken av `bundle/` |
| README er eneste kataloglenke | `grep -n 'examples/' README.md` | `:19`, `bygg-energi-mikro` |
| B1 = D4, ikke okf-eid | `~/.claude/coord/portfolio-optimiser/archive/20260725T041948Z-*` | okf: «never owned here; it is D4» |
| Spec-tekst uendret siden 07-21 | `git log --oneline -- method-spec.md ingest-spec.md` | `9801d35` / `bfa5a9b` |

View file

@ -0,0 +1,130 @@
# Ordering axes — three distinct axes already present in the frozen spec
**Status:** INTERPRETATION of frozen spec text. This document changes **no** normative text and
introduces **no** new requirement. Where this document and the spec disagree, **the spec wins** and
this document is the thing that is wrong. It is a reading aid, not a contract.
**Issued:** 2026-07-25. Applied by at least one downstream implementation as binding for its own
work before this record existed — which is why the record exists.
**Scope:** the disambiguation is **across** the two specs. Each axis is already stated, normatively,
in one of them; nothing here is derived, inferred or extended. What was missing was a single place
saying that these are three different things and which one governs a given piece of code.
## 1. Why this exists
Three separate implementations reached, independently, for "the ordering rule" and each picked a
different one — twice arriving at code that looked correct and sat on the wrong contract. That
failure mode is expensive precisely because it is quiet: an implementation on the wrong axis is
usually deterministic, passes its own tests, and produces plausible output. Nothing fails until
another consumer depends on the property the right axis guaranteed.
The specs are not at fault. Each axis is stated clearly where it belongs. The gap is that no single
document said "there are three, they are not interchangeable, here is how to tell which one you are
standing on."
## 2. The three axes
| | Axis | Question it answers | Normative source |
|---|---|---|---|
| **A** | Membership | *Which files does a re-materialization replace?* | `ingest-spec.md:175-177` |
| **B** | Generation / index-link order | *In what order are generated files and their index links emitted?* | `ingest-spec.md:178-181`, §6 (`:183`) |
| **C** | Navigation order | *In what order does a reader traverse a bundle?* | `method-spec.md:76-78` |
**Axis A — membership, read from disk.** The set of files a re-materialization replaces is exactly
those the bundle already carries the stamp for, *"read from disk (§3), **never a caller-supplied
file list**; a caller can therefore never name a file for deletion"* (`:175-177`). This is a safety
property, not an ordering one: it is what keeps re-materialization from becoming a caller-controlled
delete.
**Axis B — generation order, supplied by the caller.** *"The ORDER of generated files and their
index links (§6) is the manifest's extraction order — supplied by the caller, **never filesystem
enumeration order**"* (`:178-181`). This is a determinism property: output must not vary with
directory iteration order.
**Axis C — navigation order, first-seen link order.** *"Traversal is **depth-first in first-seen
link order** and fully deterministic (the root index first, then each link followed in the order it
appears, recursively)"* (`method-spec.md:76-78`). This governs a **reader** walking an existing
bundle. It says nothing about how that bundle came to be ordered.
A and B are deliberately split by the spec itself — `:175` labels the split *load-bearing* — because
fusing them would trade a safety property for a determinism property. B and C are adjacent and
easily confused: both are "order", but one describes an act of **writing** and the other an act of
**reading**.
## 3. The test — which axis am I on?
Ask what the code *does*, not what it is called:
- **Does it decide what to delete or replace?****A**. The answer comes from disk. A caller-supplied
list is a defect regardless of how convenient it is.
- **Does it emit files or index entries?****B**. The order comes from the caller's extraction
order. Any order the code chooses for itself is wrong — including a deterministic one.
- **Does it read an existing bundle and visit its files?****C**. The order comes from the links,
depth-first, first seen.
The trap is that "generator" and "reader" are roles, not file names. A module that both writes an
index and reads one is on **both** B and C, and owes each its own rule.
## 4. Worked applications
**An index generator that sorts its entries alphabetically.** It emits index entries, so it is on
**B**. Alphabetical order is deterministic, which is what makes this hard to see — but B does not ask
for *a* deterministic order, it asks for *the caller's* order. Alphabetical is therefore
deterministically **wrong**, not merely unlucky, and the fix is to thread the caller's extraction
order through, not to substitute a different self-chosen sort. Attributing such a generator to axis A
("it is about membership, so ordering is a benign detail") is the specific mis-read that lets the
sort survive review.
**A convention that says a directory index is a "directory enumeration".** Ambiguous on exactly this
axis, and worth resolving in the text rather than in the reader's head. If it means the index file
*contains* an authored enumeration of that directory's children, it is compatible with C. If it means
the reader *enumerates the directory* to navigate, it contradicts `method-spec.md:82-83`, which says
an intermediate directory is *"navigated only through the links its own files carry, **never by
directory enumeration**."* Prefer wording that cannot be read the second way.
**A re-materializer narrowing what it deletes.** On **A**. Note that A's text scopes membership by
the stamp, and the stamp's identity field is `{manifest filename stem}@{h}` (`ingest-spec.md:168-170`)
where `{h}` is a hash of the manifest's bytes. Matching on the stem rather than the full reference is
what lets an edited manifest reclaim its own prior output, since `{h}` changes on every edit.
(Separately: `ingest-spec.md:171-174` assumes **one** manifest per bundle in version 1 and names the
multi-manifest case an extension point. Code that scopes ownership per-manifest is implementing that
extension point and runs ahead of the frozen text — see the amendment queue, not this document.)
## 5. Axis B does not travel alone
An implementation adopting B inherits §6's idempotence with it, not afterwards:
- linking is **idempotent by target** — a link whose target is already present is never added twice
(`ingest-spec.md:187-190`);
- on re-materialization, index links whose target is an ingest-owned file removed in that run MUST be
removed, and **all** other links — curated and promoted — are preserved verbatim (`:191-193`).
Taking the ordering rule without the idempotence rule produces duplicate links on the second run.
The two are one package.
## 6. What this document does not say
- It does not rule on any axis outside these three. Installation root, bundle root and source root are
a **different** family of distinctions and are not addressed here.
- It does not make the multi-manifest extension point written, ratified, or safe to assume.
- It does not bind any implementation's internal design — only the observable properties the specs
already require.
## 7. Verification
Testable criteria, so this document can be checked rather than believed:
- **V1 (the anchors are real and still say what is quoted):**
`grep -n 'Membership from disk' ingest-spec.md``:175`;
`grep -n 'never filesystem enumeration order' ingest-spec.md``:179`;
`grep -n 'depth-first in first-seen' method-spec.md``:76`.
Verified 2026-07-25.
- **V2 (no normative text was changed by this document):** `git show --stat` for the commit adding
this file touches `docs/plan/` only — never `ingest-spec.md`, `method-spec.md` or `examples/`.
- **V3 (the axes are genuinely distinct, not three phrasings of one rule):** A is sourced from disk,
B from the caller, C from link order in the file. Substituting any one for another changes
observable behaviour: A→B would let a caller name files for deletion; B→C would make output order
depend on a reader's traversal; C→B would make navigation depend on how the bundle was generated.
- **V4 (§6 idempotence is not separable from B):** `grep -n 'idempotent by target' ingest-spec.md`
a hit inside §6, i.e. in the same section B points at for index links.

View file

@ -0,0 +1,732 @@
# V1 — hvilken form skal `generated` ha i ingest-spec etter OKF v0.2?
> **Status: AVGJORT 2026-07-31 — operatøren valgte O2** (`generated: { by: "process:<fast id>",
> at: <ingested_at> }`). Underlaget under står uendret som grunnlaget vedtaket ble tatt på;
> ingenting i §5§7 er skrevet om i etterkant. Se **§4.1** for hva vedtaket utløser.
>
> **Vedtaket er IKKE utført.** `ingest-spec.md` står fortsatt uendret på `bfa5a9b`/`9801d35`.
> Utførelsen er gated — se §4.1.
>
> Opprinnelig status: beslutningsunderlag for operatøren, fire opsjoner med målt kostnad, ingen
> anbefaling. Utløst av `llm-ingestion-okf` (coord, 2026-07-26) som spør fordi authorship er
> vår: deres DEFAULT-profil staterer ingest-spec §5, og «ingen lokale spec-endringer» er deres
> stående non-goal.
Beslektet: `2026-07-25-amendment-underlag.md` (køen av ratifiserbare punkter — V1 hører hjemme
der hvis den ratifiseres), `2026-07-25-b1-nav-golden-normative-status.md` (samme form).
---
## 1. Det som ikke er oppe til vurdering
- **Ærlighetsregelen selv** (`ingest-spec.md:33-34`, avledet av `method-spec.md:26`) er
*unwaivable*. Alle fire opsjoner under oppfyller den. Spørsmålet er hvilken **form**
merkingen har, aldri **om** den finnes.
- **Eierskaps-stempelets funksjon** (`:70`, `:82`): at re-materialisering bare rører egne filer,
og at en kurert skriver avviser det *komplette* stempelet. Ingen opsjon svekker den regelen.
- **`ingested_at` som eksplisitt påkrevd argument** (`:139-143`). Ingen opsjon innfører
wall-clock. Der `at` finnes, bindes den til `ingested_at`.
- **Hastegrad.** `llm-ingestion-okf` er *ikke* blokkert: deres v0.2-støtte kommer som en ny
profil ved siden av DEFAULT, additivt. Ingenting her er en brannslukking.
## 2. Premissene, verifisert
`llm-ingestion-okf`s to påstander er kontrollert mot kilden, ikke overtatt:
- **Oppstrøms:** OKF v0.2 (`GoogleCloudPlatform/knowledge-catalog`, `okf/SPEC.md`) definerer
`generated: { by, at }`, og `generated.by` er **REQUIRED within `generated`**, typet som en
aktør per §7. Aktørkonvensjonen har **tre** former: `<producer>/<version>` for agenter og
verktøy, `human:<id>` for en person, `process:<id>` for en automatisert prosess.
- **Toleransen dekker det ikke.** v0.2 sier at konsumenter «MUST NOT reject documents with
unrecognized fields» og ikke skal avvise for *manglende valgfrie* felter eller *ukjente*
nøkler. Ingen setning pålegger en leser å svelge en **kjent nøkkel med feil type**.
`llm-ingestion-okf`s lesning holder.
- **Historikken frikjenner valget.** `generated` var ikke reservert i v0.1. Ingen innførte en
defekt; v0.2 tok navnet etterpå.
**Én forbeholdsrad:** deres egen plan (`okf-v0.2-alignment.md`) slår fast at spec-en er lest på
`main` — en gren, ikke en tag — og at «an enumeration read off a moving branch is a premise, not
a fact». Vår kontroll traff samme bevegelige gren. **Ingen frossen spec-tekst bør endres før
oppstrømsversjonen er pinnet til en commit.** Det gjelder O1, O2 og O3 likt.
## 3. `generated` bærer to laster i vår spec, ikke én
Dette er funnet som former underlaget, og grunnen til at opsjonssettet ikke er tre.
Nøkkelen opptrer 7 ganger som kontraktsreferanse, fordelt på fem seksjoner. De deler seg på
**to akser** som ingen av dem navngir:
| Last | Hvor | Hva den svarer på | Hvem leser den |
|---|---|---|---|
| **Ærlighetsmarkør** | §1 `:34`, §7 `:214` | «er dette maskingenerert?» | en leser/presentatør av bundelen |
| **Eierskapspredikat** | §3 `:70`, §3 `:82`, §11 `:275` | «hvilke filer eier ingest — og hva skal en kurert skriver avvise?» | materialiseringen og dør C |
Oppstrøms `generated { by, at }` er **provenance/attribusjon** — den første lasten, ikke den
andre. v0.2 har ingen skriveeierskaps-semantikk; feltet er valgfritt, konsument-skrivbart og
fritt for enhver aksesskontrollmening.
Konsekvensen er ikke at feltet ikke *kan* bære begge, men at en opsjon som flytter
eierskapspredikatet over på et oppstrømsdefinert felt **må uttale det**: predikatet går fra en
literal-sjekk (`generated == true`) til en parse-og-match på et felt hvis grammatikk oppstrøms
eier og fritt kan revidere. Konjunksjonen med `ingest_manifest` — vår egen nøkkel, ikke reservert
oppstrøms i noen versjon — bærer fortsatt uforfalskbarhet-mot-uhell, så stempelet kollapser
ikke. Men `:82`s «permitting either field alone» må omformuleres: under v0.2 vil *kurert*
innhold legitimt kunne bære en `generated`-mapping med en helt annen `by`.
> **Empirisk bekreftet 2026-07-26, ikke lenger bare utledet.** Oppstrøms skriver `generated`
> **håndskrevet** innhold. Verifisert her:
> `generated: { by: human:jsmith@acme, at: 2024-01-15T10:00:00Z }`
> (`okf/bundles/acme_retail/metrics/gross-margin-legacy.md`, en menneskeskrevet metrikkdefinisjon).
>
> **Konsekvensen gjelder uansett hvilken opsjon som vedtas:** en implementasjon som utleder «er
> dette maskingenerert?» eller «eier vi denne fila?» fra at `generated` **finnes**, er feil under
> v0.2. Under O1/O2 må predikatet derfor være `generated.by == <vår aktør>` **og**
> `ingest_manifest` — aldri nøkkelens tilstedeværelse. Under O0 og O3 er poenget uten virkning,
> siden markøren vår da ikke deler navn med et oppstrømsfelt som brukes slik.
### 3.1 Målt i den pinnede implementasjonen — predikatet er KONJUNKSJONEN, og det er konformt
`portfolio-optimiser-claude` meldte 2026-07-26 at `generated` «er et eierskaps-predikat i den
pinnede v0.3.2», som en spenning mot oppstrøms' rene attribusjon. **Vi målte deres kilde. Funnet
er ekte, men konklusjonen er én hakk for sterk — og forskjellen er akkurat den V1 handler om.**
Lest i `llm_ingestion_okf/materialize.py` @ **0.3.2** (deres `.venv`, verifisert her):
```python
# :86-89 §3/§5 ownership: the ingest stamp is `generated: true` AND an
# `ingest_manifest` reference. ← deres egen kodekommentar
def _is_ingest_owned(path: Path) -> bool:
frontmatter = _parse_frontmatter(path)
return frontmatter.get("generated") == "true" and "ingest_manifest" in frontmatter
# :103 emitter: "generated": "true"
# :258 eneste kaller → bygger eierskapsmengden
# :260-268 kollisjonsporten → MaterializationError(code="collision_unstamped")
```
Porten leser **konjunksjonen**, ikke feltet. Det er nøyaktig det frossen tekst krever, ordrett:
> **`ingest-spec.md` §3, «No other writer may forge the stamp»** — «the *complete* ownership
> stamp — `generated: true` together with an `ingest_manifest` reference — while permitting
> either field alone … **The check is on the complete stamp, never on the individual field
> names**»
**Så det finnes ingen «feltet ER to ting»-konflikt mellom oss og oppstrøms på predikat-nivå:**
`generated` alene har aldri båret eierskap, verken i vår tekst eller i deres implementasjon.
Aksefunnet i §3 står uendret — de to lastene er reelle — men eierskapslasten hviler på
konjunksjonen, og bare ærlighetsmarkør-lasten ligger på feltet alene.
**Det meldingen FAKTISK avdekker, og som er en ny kostnadsrad:** emitter (`:103`) og port
(`:89`) er koblet gjennom strengliteralen `"true"`. Blir `generated` en mapping, feiler
konjunksjonens **første ledd** — og da faller emitter og port samtidig, i samme fil. Det er en
migrasjonskostnad **under O1 og O2 likt**, og den er ikke synlig fra spec-teksten alene:
| Opsjon | Hva `_is_ingest_owned` må bli |
|---|---|
| O0 / O3 | uendret (`== "true"` består; under O3 med ny nøkkel) |
| O1 / O2 | `generated.by == <vår aktør>` **og** `ingest_manifest` — parse, ikke literal-sammenligning |
**Versjonsforskjellen som ingen av de to meldingene har sett** (målt her, i begge trær):
| Tre | `_is_ingest_owned`-signatur | Eierskapsporten er |
|---|---|---|
| v0.3.2 (det BEGGE konsumenter kjører) | `(path)``:84` | **global** over bundelen |
| `llm-ingestion-okf` HEAD | `(path, manifest_stem, *, profile)``:132`, kalt `:393` | **per manifest** |
`portfolio-optimiser-claude` beskriver v0.3.2-oppførsel; `llm-ingestion-okf` beskriver
HEAD-oppførsel (deres Q3-punkt om at «stemmen bærer per-manifest eierskap i kollisjonsporten»).
**Begge er sanne om hvert sitt tre, og de er ulike på nøyaktig det punktet begge meldingene
handler om.** Samme akseklasse som resten av dette dokumentet: «implementasjonen» er ikke én
ting når trærne ikke er pinnet til hverandre.
## 4. Spørsmålet
> Skal `generated` i ingest-spec §5/§7 forbli den literale `true`, anta v0.2-formen
> `{ by, at }` — og i så fall med hvilken aktørstreng — eller skal vår markør flytte til en
> nøkkel oppstrøms ikke eier?
### 4.1 Svaret, og hva det utløser (2026-07-31)
**Vedtatt: O2** — `generated: { by: "process:<fast id>", at: <ingested_at> }`, predikatet utledet
av `generated.by`. Aktørstrengen er specens, ikke produsentens.
**Hva vedtaket IKKE avgjør.** O2 ble valgt uten å ta stilling til `:29` — og trenger det ikke.
Det var nettopp O1 som ikke kunne vedtas alene (§6). O2 lar `:29` stå ordrett som den er, og
fasit-bytene forblir produsent-nøytrale. `:29` er dermed ikke i køen som følge av dette vedtaket.
**⛔ Utførelsen er gated, og gaten er ikke vår.** Ingen frossen tekst endres før
`llm-ingestion-okf` er **pinnet til en commit**. Vår forrige kryssgrense-kontroll traff `main`
en bevegelig gren — og det ble korreksjonen i `ab0ea8f`. Gaten gjelder O2 som den gjaldt O1/O3.
**Køen dette legger seg i, når gaten åpner** (5 av 7 kontraktslinjer, uendret fra §5):
`:34`, `:70`, `:82`, `:214`, `:275`. `:152` og `:309` navngir bare nøkkelen og overlever.
V1 hører hjemme i `2026-07-25-amendment-underlag.md`s kø ved ratifisering.
**Varslingsplikten er utløst, ikke lenger betinget.** §9s formulering var «vedtas O2, skal
`llm-ingestion-okf` ha beskjed FØR de fryser en v0.2-fikstur som bærer `generated`». O2 ER
vedtatt, så plikten er live og forfaller ved deres neste fikstur-frys — ikke ved vår utførelse.
Varslet sendt 2026-07-31, samtidig som dette føres.
**Serialiseringsformen er BUNDET, og ikke av dette vedtaket** (tilført 2026-07-31 etter at
`portfolio-optimiser-claude` og deres måling reiste spørsmålet to ganger). Begge spurte om §4.1
binder inline flow normativt eller lar serialiseringen være implementasjonsvalg — forskjellen er
6 mot 7 sider hos dem. **Svaret er at spørsmålet allerede er avgjort av frossen tekst, og at V1
derfor ikke skal binde noe:**
- `ingest-spec.md:158` (inne i §5s frontmatter-punkt, som definerer det påkrevde ordnede
prefikset `generated` ligger i): **«All values MUST be single-line»**. `MUST`, i kraft i dag,
uendret siden `bfa5a9b`.
- Blokk-form er per definisjon flerlinjes. Den er derfor **allerede ikke-konform** — ikke som
følge av O2, men som følge av en regel som har stått hele tiden.
- **Inline flow er den eneste konforme serialiseringen.** `generated: { by: …, at: … }` er
single-line og line-oriented, og oppfyller `:150` og `:158` slik §5 allerede slår fast.
**Konsekvens: 6 er invariant, ikke betinget.** Den betingede raden (`O2, blokk-form → 7 sider`)
beskriver en form specen ikke tillater. V1 trenger ingen ny kontraktslinje for å binde formen,
og `:152`/`:309` overlever fortsatt. Samme klasse som D-B: **et spørsmål som ser åpent ut, men
er avgjort av tekst som allerede er frossen.** Målingen deres er likevel verdifull, og av en
annen grunn enn de sendte den: den viser at `test_provenance_keys_are_in_the_spec_order`
faktisk er en søm på `:158`-konformitet — den ville gått rød hvis noen emitterte blokk-form,
altså brøt `:158`. Det er en egenskap ingen hadde lagt merke til.
**Konsument-kostnaden er kjent på forhånd** (§5.1, målt @ `8a14137`): **6 sider** hos
`portfolio-optimiser-claude` — 4 byte-frosne fasit-blober + 2 verbatim likhets-assert. De 3
navn/orden-sidene rører O2 **ikke** (ordens-testen bygger nøkkelen med `ln.split(":", 1)[0]`, som
fortsatt gir `generated`). Fasit-blobene er en **fasit-endring, ikke en kodeendring** — method
spec §7 gjør goldenen til eneste fasit.
**Det som fortsatt er åpent og IKKE følger av dette vedtaket:** hvilken `<fast id>` strengen
skal bære. O2 fastslår formen (`process:<id>`, som v0.2 §7 eksplisitt tillater) og at aktøren
navngir prosessen specen definerer. Selve id-en er en redaksjonell avgjørelse som tas når
kontraktslinjene skrives, og bør avklares med `llm-ingestion-okf` i samme runde som pinnen.
### 4.2 Pinnen, id-en og siteringen — avgjort 2026-07-31 (økt 4)
`llm-ingestion-okf` svarte samme dag. Tre ting falt på plass, og ett premiss i §4.1 viste seg
for svakt formulert. Grunnlaget over står ordrett uendret; dette er status, ikke omskriving.
**Pinnen foreligger: `2504011`** — «feat(okf-v0.2): D5 — the v0.2 golden fixture, with
`okf_version` in root frontmatter», pushet til `open/llm-ingestion-okf`. Det er en PIN, sagt
eksplisitt som sådan. `6f42c10`/`ed08ac1` var siterte refs; skillet holdt.
**Deres v0.2-fikstur var aldri rammet, og det er målt, ikke antatt.** Den bar O2-formen fra
`c90171d` (07-27), vedtatt uavhengig av oss. Vår varsling traff et annet sett: de fire
DEFAULT-profil-fasitene (`ingest-golden-{file,sql,http}`, alle `:8`), som er VÅRT lag å endre.
**`<fast id>` = `process:okf-ingest`** (operatøren, 2026-07-31). Okfs eget forslag. De foreslo
først `process:llm-ingestion-okf` og argumenterte samtidig mot den — riktig, og strengere enn
de kunne se herfra:
| Kilde | Ordrett | Frossen siden |
|---|---|---|
| `ingest-spec.md:8-9` | «The prose is framework-neutral by rule: it never names a concrete agent toolkit or vendor stack, and a guard test keeps it that way» | `7aa53fc` (07-03) |
| `ingest-spec.md:7-8` | «implemented **from this spec alone** — without reverse-engineering any existing implementation» | `7aa53fc` |
| `ingest-spec.md:282` (§11-seam «Spec integrity») | «this spec goes missing, **names a concrete agent toolkit**, or stops documenting a contract field» | `7aa53fc` |
**Presisjonsforbehold, så raden ikke føres for sterkt:** `llm-ingestion-okf` er en
ingest-implementasjon, ikke strengt tatt en «agent toolkit» — `:282` treffer derfor ikke
ordrett. Det er `:7-8` som treffer uten tolkningsrom: å normere produsentens repo-navn ville
tvunget enhver annen konform implementasjon til å skrive det navnet i sin egen output. Samme
klasse som `:29` tvang O1 ut på, bare svakere. **Utelukkelsen holder, men på `:7-8`, ikke `:282`.**
**Siteringen er IKKE normativ — formen er usitert.** Okf spurte om anførselstegnene i
`by: "process:<fast id>"` var normative eller illustrative. **Spørsmålet er avgjort av frossen
tekst — fjerde gang** (etter D-B, `:29`-vs-O1 og `:158`):
- `method-spec.md:90` (frossen `7d2b46c`, 07-03): «Frontmatter is the leading `---`-delimited
block, **parsed line-oriented as `key: value` strings**».
- Vi parser altså ikke YAML. Verdien ER den rå teksten etter `key: `. Det finnes ingen
transparent sitering i formatet: et anførselstegn er **et tegn i verdien**, ikke syntaks som
en parser fjerner. Normert sitat ⇒ predikatet måtte matchet anførselstegnene som datainnhold.
- Målt bekreftelse: v0.3.2s port sammenligner mot **strengen** `"true"` (§3.1 `:100`), ikke mot
en boolean — nøyaktig fordi parsingen er line-oriented.
- Oppstrøms er selv usitert **med kolon i verdien**: `by: human:jsmith@acme` (§3s empiri).
Kanonisk form, vedtatt: `generated: { by: process:okf-ingest, at: <ingested_at> }`
**⛔ KORREKSJON av §4.1: pin var ikke den siste gaten.** §4.1 skrev «Ingen frossen tekst endres
før `llm-ingestion-okf` er pinnet til en commit». Det er en nødvendig, ikke tilstrekkelig
betingelse, og formuleringen ville — lest alene — hjemlet å skrive de 5 linjene nå. Den er
korrigert av avstemt tekst i søsterunderlaget:
> `2026-07-25-amendment-underlag.md:495-496` — «vi forbereder underlaget, **operatøren
> ratifiserer**, og **frossen tekst endres ikke uten den ratifiseringen**»
Køens rad 8 er B1/D4 — **V1 står ennå ikke i køen**, slik §8 alltid har sagt («V1 er punkt
nummer 8 *hvis den ratifiseres*»). Pin + id lukket gaten for at V1 kan gå I KØ; ratifiseringen
er en separat, operatør-eid handling, og amendment-pakken er på bevisst hold. **`ingest-spec.md`
står uendret på `bfa5a9b`; de 5 linjene bærer fortsatt literal `generated: true`.**
**B1 er ikke presedens for det motsatte:** `8a7d430` rørte `README.md` («katalog, ikke
kontrakt») og to planfiler — null normative filer. Commit-meldingen sier det selv om V1: «Ingen
frossen tekst er rørt.»
**Varslet til okf er sendt** (07-31, som svar på pinnen): id + siteringsform vedtatt, og et
eksplisitt **ikke regenerer ennå** — regenerering mot ikke-ratifisert tekst ville pekt fasiten
deres på en spec som ikke finnes. Varslingsplikten ved faktisk tekstendring står fortsatt live.
## 5. Opsjonene, med målt kostnad
Kostnad er talt som **kontraktslinjer som må skrives om** av de 7 (`:34`, `:70`, `:82`, `:152`,
`:214`, `:275`, `:309`).
**Det som ikke koster noe i noen opsjon:** §5s formkrav overlever uendret **for `generated`**.
Prefikset er «OKF line-oriented `key: value`» (`:150`) og «All values MUST be single-line»
(`:158`) — v0.2s kanoniske form for `generated` er en **inline flow mapping** på én linje, så den
er single-line og line-oriented allerede (verifisert oppstrøms:
`generated: { by: reference_agent/gemini-2.5-pro, at: 2026-06-30T14:00:00Z }`). `:153`
(«additional frontmatter keys MAY follow») er heller ikke i veien. Ingen opsjon tvinger fram en
rikere frontmatter-modell i *specen*.
> **Presisering 2026-07-26** (`llm-ingestion-okf`): setningen over er målt på `generated` **alene**,
> og skal ikke leses som at `:158` overlever v0.2 i sin helhet. Det gjør den ikke. `sources`
> et annet nytt v0.2-felt — er kanonisk en **blokkliste**, verifisert oppstrøms:
> ```yaml
> sources:
> - id: margin-standard
> resource: policies/margin-standard.md
> author: human:jsmith@acme
> ```
> Den formen bryter `:158` direkte. Det er **utenfor V1**, som bare gjelder `generated`, og
> DEFAULT-profilen emitterer ikke `sources` — men det er et reelt §5-spørsmål den dagen noen vil
> emittere v0.2s øvrige felter, og det er commons' å svare på. Ikke i køen, ikke meldt videre.
### O0 — la `generated: true` stå, dokumentér avviket
**0 av 7 linjer.** Ingen frossen tekst røres; ingenting går i ratifiseringskøen. Eneste
tilføyelse er en merknad om at nøkkelen kolliderer med et v0.2-definert felt.
Kostnad: DEFAULT fortsetter å emittere en v0.2-definert nøkkel med v1-verdi. En v0.2-leser har
ingen plikt til å akseptere den (§2). Kollisjonen forsvinner ikke — den venter.
**Reverserbar.** Ja.
### O1 — v0.2-form med produsent-aktør (`llm-ingestion-okf`s anbefaling)
`generated: { by: "llm-ingestion-okf/<versjon>", at: <ingested_at> }`, predikatet utledet av
`generated.by`.
**5 av 7 linjer** (`:34`, `:70`, `:82`, `:214`, `:275`). `:152` og `:309` navngir bare nøkkelen
og overlever.
**Denne opsjonen kolliderer med en eksisterende normativ setning — se §6.** Kostnaden er ikke
bare fikstur-regenerering.
> **Evidens FOR O1, tilført 2026-07-26.** `llm-ingestion-okf` korrigerte sin egen anbefaling da de
> hadde lest oppstrøms' *bundles* og ikke bare SPEC.md: **oppstrøms referanseimplementasjon bruker
> selv produsent/versjon-formen.** Verifisert her, uavhengig:
> `generated: { by: reference_agent/gemini-2.5-pro, at: 2026-06-30T14:00:00Z }`
> (`okf/bundles/acme_retail/metrics/gross-margin.md`). `<produsent>/<versjon>` er altså ikke en
> avvikende lesning av §7 — det er den **kanoniske formen for et verktøy**, brukt av spec-eieren.
> O1 er dermed den v0.2-*idiomatiske* opsjonen, og O2 er det bevisste avviket fra idiomet.
> Funnet i §6 står uendret ved siden av dette: oppstrøms' bundles er utdata fra én agent, ikke
> delte konformansfiksturer, så de to fakta er ikke i motstrid. **Det er nettopp byttet
> operatøren skal ta stilling til:** v0.2-idiomatikk mot reproduserbar delt fasit.
**Reverserbar.** Nei — frossen normativ tekst.
### O2 — v0.2-form med produsent-nøytral aktør
`generated: { by: "process:<fast id>", at: <ingested_at> }`. v0.2 §7 tillater eksplisitt
`process:<id>` for en automatisert prosess (verifisert, §2). Aktøren navngir da **prosessen
specen definerer**, ikke verktøyet som kjørte den.
**5 av 7 linjer** — nøyaktig samme som O1. Den eneste tekstlige forskjellen mot O1 er
aktørstrengen; den avgjør til gjengjeld §6.
Kostnad: `generated.by` identifiserer ikke lenger *hvilket* verktøy som produserte filen.
Produsent-identitet må da bo i implementasjonens egen logg. Om det er et tap eller et krav
avhenger av §6.
**Reverserbar.** Nei.
### O3 — døp om vår markør, la `generated` være fri
Vår markør flyttes til en nøkkel oppstrøms ikke eier; `generated` overlates til v0.2-semantikk.
**7 av 7 linjer** — også `:152` (prefiks-lista) og `:309` (kryss-sjekk-tabellen), fordi nøkkelen
selv skifter navn. Dyrest i tekst.
Til gjengjeld: predikatet bevares **eksakt** som en literal-sjekk, aksene i §3 skilles ved
konstruksjon, og kollisjonen kan ikke gjenoppstå ved neste oppstrømsrevisjon. v0.2 forbyr det
ikke — de nye feltene er valgfrie oppstrøms.
Kostnad: interop-tapet er reelt. En v0.2-leser som spør «er dette maskingenerert?» via
`generated` får ikke svar. Ærlighetsregelen er oppfylt internt, men ikke lesbar for formatets
egne lesere — som er en del av hvorfor vi ligger i OKF i det hele tatt.
**Reverserbar.** Nei.
### 5.1 Konsument-kostnaden, målt i `portfolio-optimiser-claude` @ `8a14137`
Tallene over teller **commons' egne kontraktslinjer**. De sier ingenting om hva en opsjon koster
der specen konsumeres. `portfolio-optimiser-claude` meldte 2026-07-31 hva raden koster hos EN
konsument. **Hele målingen er reprodusert her, read-only i deres tre, pinnet til `8a14137`
(2026-07-26)** — den føres derfor som *målt*, ikke som referert. (Pinnet, ikke `main`: forrige
kryssgrense-sjekk traff en bevegelig gren, og det ble korreksjonen i `ab0ea8f`.)
> Nummerering: meldingen kaller dette «kø-punkt 8». **Vårt** kø-punkt 8 er B1/D4 nav-golden
> (`2026-07-25-amendment-underlag.md` §9), og V1 ligger ikke i den køen i det hele tatt. Hvilken
> liste deres 8 tilhører, vet vi ikke — og gjetter ikke.
**To lag, og de faller ikke sammen.** Verdi-laget bærer strengliteralen `"true"`; navn/orden-laget
bærer nøkkelnavnet `generated` og plassen det har i §5-prefikset. Mapping-form rører bare det
første. En omdøping rører begge.
| Sted (konsumenten) | Lag | O0 | O1 / O2 | O3 |
|---|---|---|---|---|
| 4 byte-frosne fasit-blober | verdi | — | RØD | RØD |
| 2 verbatim likhets-assert | verdi | — | RØD | RØD (`KeyError`) |
| 3 navn/orden-sider | navn/orden | — | — | RØD |
| **Sum sider som må endres** | | **0** | **6** | **9** |
**Fasit-blobene** (`generated: true` på linje 8 i det byte-sammenlignede prefikset, alle fire):
`examples/ingest-golden-file/expected-bundle/ingest-costs.md` · `.../ingest-edge.md` ·
`examples/ingest-golden-sql/expected-bundle/ingest-costs.md` · `.../ingest-meta.md`.
Mekanismen er `tests/test_ingest_golden.py:36``read_bytes() == read_bytes()` per fil. Method
spec §7 gjør goldenen til eneste fasit, så dette er en **fasit-endring, ikke en kodeendring**.
**Verdi-assertene:** `tests/test_ingest_loadbearing.py:59` og
`tests/test_ingest_sql_loadbearing.py:84` — begge `frontmatter["generated"] == "true"`, begge på
§11-sømmen «Provenance stamping». Under O1/O2 feiler sammenligningen; under O3 finnes ikke
nøkkelen.
**Navn/orden-sidene — den raden konsumenten selv ikke priset:**
`tests/test_ingest_loadbearing.py:32` og `tests/test_ingest_sql_loadbearing.py:34`
(`_PROVENANCE_KEYS`-tuplene), samt `tests/test_ingest_loadbearing.py:67-75`
(`test_provenance_keys_are_in_the_spec_order`, navnet på `:74`). Ordens-testen bygger nøklene med
`ln.split(":", 1)[0]`, så `generated: { by: …, at: … }` gir fortsatt `generated` — **mapping-form
bryter ingen av de tre**, ordrett som meldingen sier. En **omdøping** bryter alle tre. Meldingen
tok ikke stilling til O3; denne raden er målt her.
**Presisjon på det fjerde stedet meldingen fører opp.**
`tests/test_ingest_spec_loadbearing.py:49` enumererer navnet `generated` — men i
`_CONTRACT_FIELDS`, og testen (`:68-71`) er `assert field in text` over **spec-teksten**, ikke
over frontmatter. Den kan derfor ikke bli rød under **noen** opsjon: prosaen «machine-generated»
(`ingest-spec.md:33`, `:214`, m.fl.) metter delstrengen selv om begge felt-radene slettes. Annen
mekanisme, og for akkurat dette feltet nær vakuøs. Om det er verdt en søm, er konsumentens
operatørs sak, ikke vår.
**Det meldingen bekrefter uten å legge til kostnad:** deres `shared/ingest-spec.md:186``generated`
| Literally `true` — the machine-generated marker») er vår `:214` — én av de 5 av 7 linjene O1/O2
allerede betaler. Normativ tekst må endres i SAMME amendment, ja; den er talt. Prosa-treffene
(`docs/extending.md:96,98`, `docs/oppskrift-kunnskapsbase.md:109`) er dokumentasjon, ikke sømmer,
og telles ikke.
**Forholdet til §3.1 — to akser, ikke to tabeller som er uenige.** §3.1 teller hva
`_is_ingest_owned`*bli* (predikatets form: literal-sammenligning vs. parse) og sier O0/O3
lar den stå uendret. §5.1 teller hvilke *sider* som blir røde. O3 lar predikatets form stå og
flytter kostnaden til navn/orden-laget; O1/O2 gjør det motsatte.
**Dette er en pris, ikke en dom.** Argumentene for O3 i §5 står uendret — predikatet bevares som
literal-sjekk, aksene i §3 skilles ved konstruksjon. Raden sier bare at O3 er dyrest **også** hos
konsumenten, ikke bare i commons' tekst. Valget er operatørens.
## 6. Funnet som omformer opsjonssettet (ikke en anbefaling)
**O1 som formulert kolliderer med konformansleddet i §1.**
- `ingest-spec.md:29`: en konform implementasjon MUST «reproduce the shared golden extractions
(§11) byte for byte».
- `ingest-spec.md:267`: `expected-bundle/` er «compared file by file, **byte for byte**».
- `ingest-spec.md:280`: sømmen `Golden regression` fyrer når «any byte of a golden extraction's
expected bundle diverges».
- `generated` ligger i det **obligatoriske ordnede prefikset** (`:149-152`) — altså inne i de
bytene.
Med `by: "llm-ingestion-okf/<versjon>"` bærer en **delt** fasit én produsents navn og
versjonsnummer. Da følger to ting mekanisk:
1. **Ingen annen konform implementasjon kan reprodusere fasiten byte for byte.** Kravet i `:29`
gjelder enhver konform implementasjon, ikke den som lagde fikstursettet. Kravet blir
uoppfyllbart ved konstruksjon for alle andre enn én.
2. **`Golden regression` fyrer på hver versjonsbump av biblioteket** — uten at noen kontrakt har
endret seg. Sømmen slutter å måle det den er satt til å måle.
O2 unngår begge: aktørstrengen er da specens, ikke produsentens, og bytene forblir
produsent-nøytrale. O0 og O3 berører ikke spørsmålet.
**Merk hva funnet ikke er.** Det er ikke et argument for O2 og mot O1 i seg selv. Operatøren kan
gyldig svare at fasit-klassen aldri får mer enn én produsent, eller at `generated` skal ut av
det byte-sammenlignede prefikset. Men da er *det* valget som må ratifiseres — funnet sier bare
at O1 ikke kan vedtas uten samtidig å ta stilling til `:29`.
### 6.1 Rekkevidde-forbeholdet, korrigert 2026-07-26
Første utgave skrev at konflikten er «normativ, ikke observerbar — den utløses den dagen commons
publiserer sin første ingest-fasit». **Det var for snevert, og `llm-ingestion-okf` korrigerte
det med en måling.** Riktig bilde, etter kontroll i alle tre trær:
- `examples/ingest-golden-*` finnes fortsatt **ikke i commons** — og har aldri gjort det
(`git log --all -- 'examples/ingest-golden-*'` → tomt).
- Men fikstursettene **finnes hos implementasjonene**, og `generated` ligger i bytene:
fire filer hos `llm-ingestion-okf`, alle på `:8`, inne i prefikset. **Del (ii) av funnet —
at `Golden regression` (`:280`) ville fyre på hver versjonsbump — er derfor observerbar i
dag, ikke i framtiden.** Den utløses ved deres neste release.
### 6.2 Funnet under funnet: «the shared golden extractions» har ingen referent
Kontrollen for 6.1 avdekket noe som gjelder uavhengig av hele V1-spørsmålet, og som ingen har
meldt:
| Repo | `examples/ingest-golden-*/expected-bundle/` |
|---|---|
| commons | finnes ikke, har aldri funnes |
| `llm-ingestion-okf` | `ingest-orders.md`, `ingest-products.md`, `ingest-metrics.md`, `ingest-status.md` (+ 3 `index.md`) |
| `portfolio-optimiser-claude` | `ingest-costs.md` ×2, `ingest-edge.md`, `ingest-meta.md` (+ 2 `index.md`) |
**Overlappet i innholdsfiler er null.** De eneste sammenfallende navnene er `index.md`, og de er
byte-ulike (ulik oppsummering, ulike lenkemål — verifisert med `cmp`).
`ingest-spec.md:29` krever at en konform implementasjon reproduserer «**the shared** golden
extractions (§11) byte for byte». Bestemt form forutsetter ett sett. Det finnes to, begge
lovlig navngitt etter konvensjonen i `:259-260`, ingen av dem publisert her. **Konformansleddet
er dermed ikke-testbart i dag** — ikke fordi fasiten mangler, men fordi det er to av dem, og
hver implementasjon reproduserer sin egen per konstruksjon.
Dette er et **selvstendig punkt**, ikke en del av V1, og det er ikke i køen. Det er tatt med
her fordi det avgjør hvor tungt `:29`-argumentet i §6 veier: argumentet er ikke «dette bryter
en delt fasit vi har», men «dette sementerer at en delt fasit aldri kan oppstå».
#### 6.2.1 Svar fra `llm-ingestion-okf` (2026-07-26) — tre målte, én åpen
Fire spørsmål ble sendt. **Deres målinger er ført som DERES, ikke foldet inn i våre tall**
sømmene under er lest i fiksturer vi ikke har, og en dekningstelling som mater et
konformansargument kan ikke blande målt og referert evidens.
**Q1 — INTENSJON: nei, settet var aldri ment som `:29`s fasit.** Målt i deres egen historikk
(`9dd86b1`, «ship the 11 golden fixtures with byte-exact conformance»): bygget som *deres*
verifikasjonssett mot §11s FORMAT, uten koordinering med commons noe sted i historikken.
**Konsekvens for oss:** dette er en **misforståelse i spec-teksten, ikke en drift som skal
lukkes**. Bestemt form — «**the shared** golden extractions» — beskriver noe som aldri ble
opprettet, av noen. Det styrker §6.2s konklusjon og gjør den billigere å rette: `:29` beskriver
en artefakt som ikke finnes, ikke en artefakt som har kommet i utakt.
**Q2 — vår dekningstelling var riktig så langt den gikk, men fire sømmer er ikke talt** (okfs
måling, lest i fiksturene):
| # | Søm | Hvorfor den ikke var i vår telling |
|---|---|---|
| 1 | **NULL ⊥ tom celle er TO sømmer** | vi talte «tom celle» én gang; `products.csv` C-3 har `''`, `metrics.db` rad 23 har `NULL` (`None` fra DB-API) — ulike kodestier, ulik rendering |
| 2 | **Kredensial-indireksjon** | `connection_ref: OKF_GOLDEN_SQL_DB` resolves ved kjøring; golden-kjøringen beviser at ingen kredensial når output. Finnes i ingen av de to settene ellers |
| 3 | **Fler-ekstraksjon i én bundle** | file-caset har to ekstraksjoner → indeks med to oppføringer. Indeksgenerering med N>1 er en egen søm, kun i file-caset |
| 4 | **`http` er HERMETISK** | fiksturen er et mock-payload på disk, åpner aldri en socket. Dette er en **betingelse på FORMEN til et delt sett**, ikke en case i det — brytes den, blir conformance-suiten nettverksavhengig for alle |
Søm 4 er den som endrer hva et delt sett *er*: den er et krav til konstruksjonen, ikke en rad i
dekningstabellen. De presiserer at de **ikke** har gått gjennom `portfolio-optimiser-claude`s
`ingest-costs.md`/`ingest-meta.md` og ikke påstår noe om den halvdelen av dekningen.
**Q3 — bekreftet, og verre enn vi skrev: stampen bærer FILNAVNET.** Vi skrev at
`ingest_manifest` hashes av manifestets rå bytes. Verifisert her, i begge trær:
```python
stamp = f"{manifest_file.stem}@{hashlib.sha256(raw).hexdigest()[:16]}"
# v0.3.2 :191 | llm-ingestion-okf HEAD :326
```
Et delt sett krever altså at commons eier og publiserer **manifestets bytes OG hva fila heter**.
To implementasjoner som lastet ned samme manifest og lagret det som `ingest.json` vs.
`manifest.json` ville fått ulike stamper av identiske bytes. I okfs HEAD bærer stemmen dessuten
**per-manifest eierskap** i kollisjonsporten (§3.1 over) — et delt manifest med et annet filnavn
ville endre hvilke filer en kjøring mener å eie.
**Veien rundt, som de peker på uten å anbefale:** konvergens er ikke nødvendig på HELE bundelen.
`ingest_manifest` er én linje i frontmatteret; et delt sett kunne kreve byte-likhet på alt
UNNTATT den linja, og da er kravet oppfyllbart uten et delt manifest — mot at `:29` ikke lenger
kan si «byte-identisk bundle» uten forbehold. **Valget står mellom å eie manifestet og å svekke
kravet. Ingen av dem er gratis, og det er operatørens, ikke vårt.**
**Q4 — ikke målt, og de svarer ikke.** Vi ba om målt kostnad, ikke overslag; de holdt den
disiplinen begge veier. Målingen som er utestående: bytte manifestene i de tre casene til et
hypotetisk commons-eid sett, kjøre conformance-suiten, telle hvilke expected-bundle-bytes som
endrer seg og hvilke av de elleve sømmene som forsvinner. **Kjørbar måling, ikke en vurdering.**
Ikke gjort fordi kvoten deres er lav og fordi den bare er verdt å gjøre hvis operatøren
køplasserer konvergens over v0.2-arbeidet. De står klare til å ta den først neste økt hvis vi
sier fra. **Svart tilbake: ikke bestilt ennå — køplasseringen er operatørens, og den er ikke
tatt.**
## 7. Rutet hit: aktørkonvensjonen for intervju-født innhold — og hvorfor den ikke blokkerer V1
`llm-ingestion-okf` rutet 2026-07-26 et spørsmål fra `ms-ai-architect` hit, «fordi spec §7s
aktørkonvensjon er deres tekst». **Spørsmålet er reelt. Rutingen hviler på en navnekollisjon,
og den er verdt å rette før noen bygger på den.**
**Spørsmålet:** hva skal en produsent skrive når innholdet er *intervju-født* — et menneske
svarer, en LLM strukturerer svarene og skriver fila, og mennesket leser ikke nødvendigvis
resultatet? Substansen er menneskets, formuleringen er modellens. `process:`/`<produsent>/<versjon>`
**underrapporterer** (bare et menneske kan ha visst det organisasjonsspesifikke); `human:<id>`
**overrapporterer** (ingen har verifisert at formuleringen gjengir svaret riktig — et
tillitssignal ingen har fortjent). Deres tre alternativer: (a) maskin-aktør ved skriving +
separat `verified`-oppføring med `human:<id>` først når mennesket faktisk har lest; (b)
`human:<id>` direkte; (c) en egen aktørform. Deres lesning, uttalt som lesning: (a).
### 7.1 To ulike §7-er — kollisjonen, målt
| «§7» | Dokument | Innhold |
|---|---|---|
| Aktørkonvensjonen med tre former | **OKF `SPEC.md` §7** (Google, oppstrøms) | `<producer>/<version>` / `human:<id>` / `process:<id>` |
| **Vår** §7 | `ingest-spec.md` «Provenance — a separate layer» | `generated` = «**Literally `true`** — the machine-generated marker» |
Målt i vår frosne tekst: `grep -n "human:\|process:\|by: " ingest-spec.md method-spec.md
CONCEPT.md` → **0 treff**. **Commons har ingen aktørkonvensjon å endre.** Vår markør er boolsk
og aktørløs. Å be commons avgjøre aktørformen i dag er å be om en verdi vi ikke har — og å
levere en ville vært å foregripe nettopp det V1 legger fram for operatøren.
### 7.2 Og et lag-skille som gjelder uansett opsjon
Ingest-spec §7 stempler **filer materialisert av ingest-pipelinen fra en manifest-definert
kilde**. Intervju-født innhold er ikke ingest-materialisert — det er **kurert** innhold skrevet
gjennom en authoring-primitiv, og vår §3 krever eksplisitt at den primitiven **avviser** det
komplette ingest-stempelet. Vårt lag kan altså ikke stemple det uansett, og for filer vårt lag
FAKTISK stempler er aktøren maskinell per konstruksjon — det finnes ingen intervju-født variant
av en manifest-drevet ekstraksjon.
**Konsekvens for opsjonene, som er det operatøren trenger:**
| Opsjon | Arver vi spørsmålet? |
|---|---|
| **O0** | Nei — ingen aktørakse i markøren i det hele tatt |
| **O1** | Nei for vårt lag — aktøren er alltid `llm-ingestion-okf/<versjon>` (materialiseringen skrev fila) |
| **O2** | Nei — aktøren er en fast prosess-id uavhengig av opphav, per konstruksjon |
| **O3** | Nei — `generated` frigjøres til oppstrøms' form, og hvem som skriver den er ikke vår tekst |
> **Derfor: operatøren trenger IKKE å løse dette for å løse V1.** Ingen av de fire opsjonene
> endrer svaret, og ingen av dem blir billigere eller dyrere av det. Spørsmålet er ikke en
> femte kostnadsrad — det er en sak i et annet lag som deler et seksjonsnummer med vårt.
### 7.3 Hva vi faktisk kan si, som lesning og ikke vedtak
Aksehygienisk er okfs (a) den eneste av de tre som ikke lyver i noen retning, og grunnen er
den samme aksen dette repoet har ført ni ganger: (a) gjør tillitssignalet til en funksjon av en
**handling** (noen leste det) i stedet for av en **opprinnelse** (noen sa det). `generated`
`verified` er samme skille som ærlighetsmarkør ⊥ eierskapspredikat i §3. (c) koster nytt
vokabular for et skille som allerede kan uttrykkes. **(a) er dessuten kompatibel med både O1 og
O2** — den flytter tillitssignalet UT av `generated` og inn i `verified`, og rører derfor ikke
V1s valg i noen retning.
Én målt ting som støtter (a)s gjennomførbarhet, gjort av okf mot guard 0.2.0 og referert som
**deres** måling: `verified` som blokkliste går allerede gjennom persist-gaten
(`verified:\n - human:ktg` parses som `['human:ktg']`), mens en `generated`-**mapping** ikke gjør
det i noen form (flow feiler på `{`, blokk på nested mappings, punktnøkler på key-regexen).
Ikke et argument for (a) i seg selv — men (a)s ene nye mekanisme møter ingen vegg der v0.2s
`generated` møter en. **Ikke verifisert av oss; deres tre, deres måling.**
**Rett respondent:** aktørkonvensjonen er OKFs. Skal en fjerde form eller en (a)-konvensjon
ratifiseres, hører den hjemme oppstrøms i `SPEC.md` §7 — ikke i ingest-spec. Meldt tilbake med
den lesningen, uten vedtak.
## 8. Det som ikke er commons' å avgjøre
- **Hvilken opsjon som velges.** Spec-teksten er frossen; endringer går gjennom
ratifiseringskøen. V1 er punkt nummer 8 hvis den ratifiseres — den står **utenfor** de 7 i
`2026-07-25-amendment-underlag.md` og fortrenger ingen av dem.
- **Hva `llm-ingestion-okf` gjør i sin egen v0.2-profil.** Den er additiv og deres authorship.
Bare DEFAULT staterer vårt lag.
- **Om produsent-identitet skal logges et annet sted** (O2s pris). Implementasjonens valg.
## 9. Det som ikke endres uansett utfall
Ærlighetsregelen, eierskapsstempelets funksjon, `ingested_at`-argumentet, §5s single-line- og
line-oriented-krav, og `ingest_manifest` som stempelets andre halvdel. Ingen opsjon rører
`method-spec.md` — nøkkelen forekommer ikke der (`grep -c '\`generated' method-spec.md` → 0).
---
## Verifiseringslogg
| Påstand | Sjekk | Resultat |
|---|---|---|
| v0.2 definerer `generated: { by, at }` | `okf/SPEC.md` @ `main`, hentet 2026-07-26 | «generated: { by: reference_agent/gemini-2.5-pro, at: 2026-06-20T22:53:05Z }» |
| `by` er påkrevd | samme | «generated.by: REQUIRED within `generated`. An actor (§7).» |
| Aktørkonvensjonen har tre former | samme, §7 | `<producer>/<version>` / `human:<id>` / `process:<id>` |
| `timestamp` er superseded | samme | «`timestamp` is superseded by `generated.at`» |
| Toleransen gjelder ukjente, ikke feiltypede | samme | «MUST NOT reject documents with unrecognized fields»; lista dekker manglende valgfrie + ukjente |
| Oppstrøms er lest på en gren, ikke en tag | `llm-ingestion-okf/docs/plan/okf-v0.2-alignment.md` | «`main` on 2026-07-26 — a branch, not a tag» |
| Kontraktsreferanser til nøkkelen | `grep -n '\`generated' ingest-spec.md` | 7 linjer: `:34 :70 :82 :152 :214 :275 :309` |
| Nøkkelen finnes ikke i method-spec | `grep -c '\`generated' method-spec.md` | 0 |
| Ærlighetsregelen er unwaivable | `ingest-spec.md:33-34`, `method-spec.md:26` | «unwaivable» begge steder |
| Stempelet er `generated: true` + `ingest_manifest` | `ingest-spec.md:70`, `:82` | ordrett sitert over |
| Dør C avviser kun det komplette stempelet | `ingest-spec.md:82-85` | «while permitting either field alone» |
| `generated` ligger i det ordnede prefikset | `ingest-spec.md:149-152` | 7 nøkler, `generated` er den sjuende |
| Prefikset er åpent bakover | `ingest-spec.md:153` | «additional frontmatter keys MAY follow it» |
| Single-line-kravet holder for flow mapping | `ingest-spec.md:150`, `:158` | «line-oriented `key: value`» / «All values MUST be single-line» |
| Konformans krever byte-lik delt fasit | `ingest-spec.md:29` | «reproduce the shared golden extractions (§11) byte for byte» |
| Fasiten sammenlignes byte for byte | `ingest-spec.md:267`, `:280` | «byte for byte» / «any byte … diverges» |
| Ingen ingest-fasit finnes i commons | `ls -d examples/ingest-golden-*` | ingen treff |
| …og har aldri funnes | `git log --all -- 'examples/ingest-golden-*'` | tomt |
| `generated` ligger i fasit-bytene hos okf | `grep -rn '^generated' llm-ingestion-okf/examples/ingest-golden-*/expected-bundle/*.md` | 4 filer, alle `:8` |
| …inne i det ordnede prefikset | `sed -n '1,9p' .../ingest-orders.md` | 7 nøkler `:2-8`, `generated` sist |
| Predikatet nøkler på literalen i dag | `materialize.py:131-150` **@ `llm-ingestion-okf` HEAD** (ref tilføyd 07-27 — raden var ref-løs) | `frontmatter.get("generated") != "true"` + stem-match |
| To disjunkte fasitsett finnes | `comm -12` over begge trærs `expected-bundle/*.md` | kun `index.md`-navn felles |
| …og de felles `index.md` er ulike | `cmp``ingest-golden-file/.../index.md` | ULIKE |
| §1 sier «**the shared** golden extractions» | `ingest-spec.md:29` | bestemt form, én mengde forutsatt |
| Oppstrøms bruker selv `<produsent>/<versjon>` | `okf/bundles/acme_retail/metrics/gross-margin.md` | `by: reference_agent/gemini-2.5-pro` |
| Oppstrøms skriver `generated` på HÅNDSKREVET fil | `…/gross-margin-legacy.md` | `by: human:jsmith@acme` |
| `sources` er blokkliste, ikke inline flow | samme fil, `sources:` | `- id:` + 4 nøkler, flerlinjes → bryter `:158` |
| Konsument-treet er pinnet for §5.1 | `git -C ~/repos/portfolio-optimiser-claude log -1` | `8a14137` (2026-07-26) — ikke `main` |
| 4 fasit-blober bærer `generated: true` | `grep -rn generated examples/ingest-golden-*/expected-bundle/*.md` @ `8a14137` | 4 filer, alle `:8` |
| Fasiten er byte-sammenligning per fil | `tests/test_ingest_golden.py:36` | `read_bytes() == read_bytes()` |
| 2 verbatim verdi-assert | `tests/test_ingest_loadbearing.py:59`, `tests/test_ingest_sql_loadbearing.py:84` | `frontmatter["generated"] == "true"` |
| 3 navn/orden-sider, ikke 4 | `_PROVENANCE_KEYS` `:32` + `:34` (sql) + ordens-lista `:67-75` | sql-fila har INGEN ordens-test |
| Ordens-testen overlever mapping-form | `tests/test_ingest_loadbearing.py:67` | `ln.split(":", 1)[0]``generated` uansett verdi |
| `:49` er delstreng over spec-tekst | `tests/test_ingest_spec_loadbearing.py:68-71` | `assert field in text` — ikke frontmatter, kan ikke bli rød |
| …og «machine-generated» metter den | `grep -n generated ingest-spec.md` | `:33`, `:214` m.fl. — prosa alene holder |
| Deres `:186` er vår `:214` | `sed -n '186p' …/shared/ingest-spec.md` | «Literally `true`» — samme rad, én commit bak |
| Vårt kø-punkt 8 er ikke deres | `2026-07-25-amendment-underlag.md` §9 | rad 8 = B1/D4 nav-golden; V1 står ikke i køen |
| SPEC.md er endret ETTER v0.2-migreringen | okf-melding, deres pin `3fcbb9f` | «v0.2-commiten» ≠ «dagens spec-tekst» — uverifisert av oss |
| `ingested_at` har ingen wall-clock-default | `ingest-spec.md:141` | «there is NO wall-clock default» |
| Spec-tekst uendret siden 07-21 | `git log --oneline -- ingest-spec.md` | `bfa5a9b`, forrige `7aa53fc` — ingen commit etter |
**Tilført 2026-07-27** (innboksrunden — fire meldinger, alle premiss målt mot kilden):
| Påstand | Sjekk | Resultat |
|---|---|---|
| v0.3.2s eierskapsport er en KONJUNKSJON | `materialize.py:86-89` @ `portfolio-optimiser-claude/.venv` (0.3.2) | `generated == "true" AND "ingest_manifest" in frontmatter` — deres kodekommentar sier «AND» eksplisitt |
| …og det er konformt med frossen tekst | `ingest-spec.md` §3 | «The check is on the complete stamp, never on the individual field names» |
| Emitter og port kobles av literalen `"true"` | `materialize.py:103` (emitter) vs. `:89` (port) | samme strengliteral begge steder |
| Porten er GLOBAL i v0.3.2 | `grep -n _is_ingest_owned` @ 0.3.2 (289 linjer) | def `:84`, ENESTE kall `:258`, signatur `(path)` |
| Porten er PER MANIFEST i okfs HEAD | samme grep @ `~/repos/llm-ingestion-okf` (424 linjer) | def `:132`, kall `:393`, signatur `(path, manifest_stem, *, profile)` |
| Stampen bærer FILNAVNET (okfs Q3) | `stamp = f"{manifest_file.stem}@{sha256(raw)[:16]}"` | v0.3.2 `:191`; okf HEAD `:326` — bekreftet i BEGGE trær |
| okfs egne `:251`/`:318` peker i intet av de to trærne | `wc -l` + `grep -n` begge trær | substansen bekreftet, linjenumrene ikke — ref ikke oppgitt i meldingen |
| Commons har INGEN aktørkonvensjon | `grep -n "human:\|process:\|by: " ingest-spec.md method-spec.md CONCEPT.md` | **0 treff** |
| Vår §7 definerer `generated` som boolsk | `ingest-spec.md` §7, felt-tabellen | «Literally `true` — the machine-generated marker (§1 honesty rule)» |
| De tre aktørformene er OKFs §7, ikke vår | V1 §2 + `okf/SPEC.md` §7 | navnekollisjon mellom to ulike §7-er |
| Begge konsumenter står på `7aa53fc` | `diff <(git show 7aa53fc:ingest-spec.md) ~/repos/<konsument>/shared/ingest-spec.md` | **IDENTISK** for `portfolio-optimiser` OG `portfolio-optimiser-claude` |
| …altså ÉN commit bak, ikke to | `git rev-list --count 7aa53fc..HEAD -- ingest-spec.md` | **1** (`bfa5a9b`) |
| MCP-ankeret finnes i den pinnede kopien | `git show 7aa53fc:ingest-spec.md \| grep -n "An MCP-based"` | `:101` — S2.2/S2.4-ugatingen henger ikke på en pull |
| `:112-114` hos konsumenten er IKKE tomt | `git show 7aa53fc:ingest-spec.md \| sed -n '112,114p'` | felt-tabellen: `id` / `title` / `query` (`okf_type`/`max_rows` er `:115-116`) |
**Tilført 2026-07-31 (økt 4 — pin, id, sitering, ratifiseringsgaten):**
| Påstand | Sjekk | Resultat |
|---|---|---|
| Frontmatter parses som STRENGER, ikke YAML | `method-spec.md:90` | «parsed line-oriented as `key: value` **strings**» — derav er sitering datainnhold, ikke syntaks |
| …og har vært frossen hele veien | `git log -1 -S 'parsed line-oriented as' -- method-spec.md` | `7d2b46c` (2026-07-03) — eldste spec-commit |
| Prosaen er framework-nøytral ved regel | `ingest-spec.md:8-9` | «never names a concrete agent toolkit or vendor stack, and a guard test keeps it that way» |
| …håndhevet som §11-seam | `ingest-spec.md:282` | «Spec integrity \| this spec … names a concrete agent toolkit …» |
| …men `:282` treffer ikke okf ordrett | lesning: okf er ingest-impl., ikke «agent toolkit» | **utelukkelsen hviler på `:7-8`**, ikke `:282` — ført som forbehold, ikke som treff |
| Commons har fortsatt INGEN aktørkonvensjon | `grep -n "human:\|process:\|by: " ingest-spec.md method-spec.md CONCEPT.md \| wc -l` | **0** (uendret fra 07-27) |
| Spec-en kaller laget «ingest», prosessen «materialization» | `grep -io 'materializ[a-z]*' ingest-spec.md \| wc -l` + `:1`, `:18`, `:28` | 27 forekomster; tittel + §1 bruker «Ingest» som lagets navn → `okf-ingest` |
| `ingest-spec.md` er URØRT | `git log --oneline -2 -- ingest-spec.md` | `bfa5a9b`, forrige `7aa53fc` — ingen commit i økt 3 eller 4 |
| De 5 linjene er urørte — men bærer IKKE samme form | `grep -n '\`generated' ingest-spec.md` | **4 av 5** bærer `generated: true` ordrett (`:34`, `:70`, `:82`, `:275`). `:214` er §7-feltradstabellens rad («\`generated\` … Literally \`true\` — the machine-generated marker») og bærer ikke literalen. Se raden under |
| …og `:214` er derfor IKKE en strengerstatning | O2-formen (`generated: { by: …, at: … }`) vs. radens «Literally `true`» | raden **beskriver feltets verdi**. Under O2 er verdien et objekt med `by`/`at`, så raden må skrives om (evt. splittes), ikke søk-og-erstattes. **Egen redigering, samme amendment** |
| Frossen tekst krever RATIFISERING, ikke bare pin | `2026-07-25-amendment-underlag.md:495-496` | «operatøren ratifiserer, og frossen tekst endres ikke uten den ratifiseringen» |
| V1 står ikke i køen | samme fil §9, rad 8 | rad 8 = **B1/D4**, ikke V1 — uendret fra 07-27 |
| B1 rørte ingen normativ fil | `git show 8a7d430 --name-only` | `README.md` + 2 planfiler; **0** normative filer |
| Pinnen er en pin, ikke en ref | okf-melding 07-31, `2504011` | sagt eksplisitt som pin; skillet fra `6f42c10`/`ed08ac1` holdt |
| Deres v0.2-fikstur bar O2-formen allerede | okfs måling, `c90171d` (07-27) | **ført som DERES**, ikke reprodusert her |
**Korreksjon 2026-07-31 (økt 6) — «de 5 linjene» er ikke homogene.** Raden «De 5 linjene bærer
fortsatt literalen» påsto at alle fem bar `generated: true` ordrett. Det er feil, og planen
motsa seg selv: §5.1 fører `:214` korrekt opp som «Literally `true`»-raden, og
verifiseringstabellens egen `grep -n '\`generated'`-rad lister `:214` blant de 7 uten å skille
form. Målt nå: `grep -n 'generated: true' ingest-spec.md` gir **4** treff (`:34`, `:70`, `:82`,
`:275`) — ikke 5.
Feilen var arvet ordrett inn i `STATE.md`s NESTE-blokk («skriv om de 5 kontraktslinjene … fra
literal `generated: true`»). Konsekvensen er ikke kosmetisk: en økt som utfører V1 mekanisk
etter den formuleringen finner 4 av 5 treff og står igjen med to like sannsynlige feiltolkninger
— (a) drift i frossen tekst, eller (b) `:214` hoppes over, som etterlater **ærlighetsmarkørens
§7-halvdel** (§3s ærlighetsmarkør-rad: «§1 `:34`, §7 `:214`») ukonvertert mens §1-halvdelen er
O2. Tellingen «5 av 7» (§4.1, §5) står uendret — det var formen, ikke antallet, som var feil ført.
*Ingen normativ fil rørt av denne korreksjonen; `ingest-spec.md` står fortsatt på `bfa5a9b`.*

View file

@ -0,0 +1,93 @@
# Google OKF v0.2 — sjekken er utført, og hypotesen holdt ikke
**Dato:** 2026-07-31 (økt 5) · **Status:** LUKKET, ingen melding sendt · **Marker:** `okf-second-brain-convention`
STATE bar siden 07-27 en uverifisert observasjon som NESTE STEG: *«catalog bumpet 0.1→0.2 for å
ikke være forvekslbar med Google OKF v0.1 — er Google nå på 0.2, kan avklaringen ha kollapset.»*
Sjekken er nå gjort. **Avklaringen har ikke kollapset, og spørsmålet var feilstilt.** Tre av
premissene i formuleringen viste seg å avvike fra ground truth.
## 1. Google er på v0.2 — men det visste vi allerede
Verifisert mot primærkilde: Google Cloud Blog, *«Open Knowledge format v0.2 tackles agentic
trust»*, publisert **2026-07-25**.
Men søket var strengt tatt overflødig. Svaret lå i vår egen arkiverte innboks, fem dager gammelt:
`llm-ingestion-okf`, `20260726T114345Z`, første linje i brødteksten — ordrett **«OKF v0.2 er ute
(2026-07-25).»** Hele V1-sporet er *bygget på* v0.2 (`generated`-feltets form etter v0.2; se
`2026-07-26-v1-generated-felt-okf-v0.2.md`, og filnavnet sier det selv).
**Dette er en STATE-defekt, ikke et funn.** Observasjonen ble ført som «uverifisert» i fire økter
mens den samtidig var bærende premiss for arbeidet i nabosporet. Premiss-verifiseringsregelen ble
anvendt på output, ikke på STATEs egen påstandsliste. Billigste sjekk som fantes var `grep` i eget
arkiv — ikke WebSearch.
## 2. Catalog er ikke på 0.2. De er på 0.3.
| Påstand i STATE | Ground truth |
|---|---|
| catalog er på 0.2 | **0.3**`1ca27f6`, 2026-07-31 (i dag) |
| bumpet skjedde «for å ikke være forvekslbar» | primærgrunnen var **§3-gulvet** |
`6a72b26` (2026-07-25), commit-subjekt ordrett: `feat(okf): enforce §3 okf_version shape, bump
convention 0.1 -> 0.2`. Og specens egen header, `spec.md:12-14`:
> 0.2 had tightened the §3 floor: `okf_version` enforced on shape. Distinct from — and
> deliberately no longer numerically confusable with — upstream Google OKF v0.1, which this
> convention targets and does not version.
Ikke-forvekslbarheten er ført som **bevisst sidegevinst**, ikke som årsak. STATE byttet om primær
og sekundær og bygget et neste steg på den omvendingen. (Jf. driftsmodellen: *før en rad ikke
sterkere enn den bærer* — her førte vi vår egen rad for sterkt.)
## 3. Hvorfor avklaringen ikke kan kollapse: det er to akser
Dette er **akse-forveksling nr. 13**, og denne gangen var det vår.
- **Akse A — catalogs konvensjonsversjon:** 0.1 → 0.2 → 0.3. Beskriver catalogs *eget* dokument.
- **Akse B — `okf_version`-verdien:** hvilken upstream Google-versjon en bundle targeter.
Catalog sier eksplisitt at konvensjonen *«targets and does not version»* upstream (`spec.md:13-14`),
og i §12 (`:245-246`): *«Its value set is owned by Google.»* Gaten er tilsvarende renset for
akse-lekkasje (`:70-72`): den *«asserts **nothing** about which upstream versions exist ... a bundle
targeting a newer upstream version passes.»*
At Google flyttet seg på akse B kan derfor ikke kollapse en avklaring som lever på akse A.
Tallsammenfallet som hypotesen fryktet inntreffer uansett ikke: catalog 0.3 vs. Google 0.2.
## 4. Det ene som faktisk står igjen — og det er ikke vårt
`spec.md` sier to steder at upstream-versjonen bundelen targeter er «currently `0.1`» (`:63`,
`:246`), mens `:67` i samme dokument siterer upstreams kanoniske eksempel `okf_version: "0.2"`
(`okf/SPEC.md:773`).
Det er **ikke en defekt**, og skal ikke meldes som en. Catalog har foregrepet situasjonen i egen
tekst, §12 `:246-247`: *«When Google bumps OKF, each plugin re-checks conformance.»* Google har nå
bumpet. Re-sjekken er dermed utløst — men den er **catalogs å utløse, på catalogs akse**, og
plugin-eiernes å utføre. Vi er ikke respondent.
Per driftsmodellen: navngi aksen, pek på rett respondent, ikke lever en verdi vi ikke eier.
## Konklusjon
- Sjekken STATE hjemlet: **utført**. Hypotesen: **falsifisert**.
- **Ingen melding skal sendes** på det opprinnelige grunnlaget — grunnlaget fantes ikke.
- Det som *kan* sendes er noe annet og mindre: en `--fyi` til `catalog` om at Google er på 0.2 og
at deres egen §12-re-sjekk dermed er utløst. **Fortsatt gated på operatør-go**, og lavt prioritert
— catalog eier både aksen og triggeren, og `1ca27f6` (i dag) viser at de følger upstream tett.
- Sporet `okf-second-brain-convention` er dermed **lukket fra vår side**.
## Verifiseringslogg
| Påstand | Kilde |
|---|---|
| Google OKF v0.2, publisert 2026-07-25 | Google Cloud Blog, `okf-v0-2-adds-trust-signals` (WebFetch) |
| v0.2 var kjent for oss 2026-07-26 | `coord/.../archive/20260726T114345Z-3155211798-from-llm-ingestion-okf.md` |
| catalog er på 0.3 per 2026-07-31 | `catalog@1ca27f6`; `spec.md:7` |
| 0.2-bumpens primærgrunn = §3-gulvet | `catalog@6a72b26` commit-subjekt; `spec.md:12` |
| konvensjonen versjonerer ikke upstream | `spec.md:13-14`, `:245-246` |
| gaten godtar nyere upstream-versjon | `spec.md:70-72` |
| re-sjekk-plikten er plugin-eiernes | `spec.md:246-247` |
Catalog-ankrene er lest read-only i `~/repos/ktg-plugin-marketplace/catalog` @ `1ca27f6`. Ingenting
skrevet i det repoet.

View file

@ -0,0 +1,27 @@
# nav-golden-escape — negative navigation golden (the gate must be able to go red)
Per catalog's requirement that step 0's gate must be able to **fail** (a gate that can
only pass proves nothing), this negative case makes a conformant navigator and a naive one
**provably diverge**. It is the counterpart to `nav-golden-hierarchy` (the positive case).
`bundle/index.md` links one valid sibling and three escaping / trap targets:
| Link | Class | Conformant outcome |
|---|---|---|
| `valid.md` | in-bundle | returned |
| `../SHOULD-NOT-BE-READ.md` | `..` escape (the decoy really exists, one level up) | skipped; decoy never read |
| `../../../../etc/passwd` | deep `..` escape | skipped |
| `/etc/passwd` | root-relative **trap** | skipped |
The trap is the load-bearing one. Under the ratified rule, a leading `/` denotes the
**bundle root**, so `/etc/passwd` resolves to `{bundle}/etc/passwd` (no such file → skip),
NOT the filesystem path. A naive implementation that reads `/` as filesystem-absolute opens
the real `/etc/passwd` — a path-traversal breach. This case fails for any implementation
that raises, reads the decoy, or leaks `/etc/passwd` content; it passes only when the
read-context is exactly `valid.md`.
The malformed-target sub-class (an embedded null byte, other invalid path components) is
covered by the method-spec §11 **Navigation boundary** seam as a unit test — a literal null
byte does not belong in a committed text fixture. (See the relocated finding
`docs/plan/2026-07-20-funn-portfolio-optimiser-nullbyte.local.md` for the concrete
null-byte reproduction that motivates the seam.)

View file

@ -0,0 +1,6 @@
---
type: project
title: "Decoy — MUST NOT be read"
---
This file sits ONE level above `bundle/`, outside the bundle. If its text ever appears in a read-context, the navigator resolved a `..` escape and breached the bundle boundary. It MUST never be reached.

View file

@ -0,0 +1,12 @@
---
type: index
okf_version: 0.1
title: "Nav-golden — escape / boundary case"
---
Nav-golden escape case. Every link but the first escapes the bundle or is a root-relative trap; a conformant navigator skips them all, reads none of them, raises nothing, and still returns the one valid sibling.
- [Valid sibling](valid.md)
- [Parent escape](../SHOULD-NOT-BE-READ.md)
- [Deep escape](../../../../etc/passwd)
- [Root-relative trap](/etc/passwd)

View file

@ -0,0 +1,6 @@
---
type: project
title: "Valid sibling"
---
The only in-bundle target. A conformant navigator returns exactly this file.

View file

@ -0,0 +1,10 @@
Nav-golden escape case. Every link but the first escapes the bundle or is a root-relative trap; a conformant navigator skips them all, reads none of them, raises nothing, and still returns the one valid sibling.
- [Valid sibling](valid.md)
- [Parent escape](../SHOULD-NOT-BE-READ.md)
- [Deep escape](../../../../etc/passwd)
- [Root-relative trap](/etc/passwd)
## project: Valid sibling
The only in-bundle target. A conformant navigator returns exactly this file.

View file

@ -0,0 +1,34 @@
# nav-golden-hierarchy — hierarchical navigation golden
Fixture class **nav-golden** (commons-owned, per the D4 step-0 split): `bundle/` in,
`expected-read-context.md` out. Unlike the ingest-golden class (ingest-spec §11,
extraction-shaped: `manifest.json``expected-bundle/`), a nav-golden case is
**bundle → read-context** — the only shape that can express the load-bearing property
"two conformant implementations MUST produce an identical read-context from the same
bundle" (method-spec §3 Step 1).
This **positive** case exercises every decision the Q3 navigation contract pins:
| Decision (method-spec §3 Step 1) | How this bundle exercises it |
|---|---|
| Hierarchy allowed; escape, not depth, forbidden | `a/`, `a/b/` are navigated; nothing escapes |
| Link syntax: leading `/` = bundle root, else relative | `index.md`: relative `overview.md` + root-relative `/a/index.md`; `a/index.md`: relative `b/index.md` + root-relative `/overview.md` |
| Depth-first, first-seen order | render order is `overview``doc-a``doc-b` |
| Dedup on resolved path | `/overview.md` (from `a/`) resolves to the already-seen `overview.md` → one entry |
| Cycle termination | `a/b/index.md` links back to `/a/index.md` (already seen) → stops |
| One segment per level | each index links only its immediate children |
| Missing `index.md` binds the root alone | `c/` has no `index.md` and nothing links `c/orphan.md`; it is simply unreachable, not an error |
| Verdict exclusion = type check per reached file, recursive | `a/verdict-nested.md` is reached but excluded |
| Flat render regardless of depth | nested concepts render as the same `## {type}: {title}` sections; no level heading; nested index bodies are navigation, not content, and are not rendered |
**Traversal trace:** `index.md` (its body is the leading summary) → `overview.md`
`/a/index.md``a/doc-a.md``a/verdict-nested.md` (reached, excluded) → `a/b/index.md`
`a/b/doc-b.md``/a/index.md` (seen, deduped) → `/overview.md` (seen, deduped).
Unreachable: `c/orphan.md`.
**Serialization** the gate compares against: the root index body verbatim, then each
**non-index** concept as `## {type}: {title}` + blank line + the file body, sections
separated by one blank line, file ending in exactly one trailing newline. The spec does
not pin whitespace beyond this shape; a gate MAY compare byte-exact or after
trailing-whitespace normalization. **Only the root index body is the summary** — nested
index bodies (`a/index.md`, `a/b/index.md`) are navigation and do not appear.

View file

@ -0,0 +1,6 @@
---
type: reference
title: "Doc B"
---
Level-2 concept reached depth-first, deepest in the traversal.

View file

@ -0,0 +1,8 @@
---
type: index
okf_version: 0.1
title: "Subsection B index"
---
- [Doc B](doc-b.md)
- [Back to A](/a/index.md)

View file

@ -0,0 +1,6 @@
---
type: methodology
title: "Doc A"
---
Level-1 concept reached depth-first after Overview.

View file

@ -0,0 +1,10 @@
---
type: index
okf_version: 0.1
title: "Section A index"
---
- [Doc A](doc-a.md)
- [Nested verdict](verdict-nested.md)
- [Subsection B](b/index.md)
- [Overview again](/overview.md)

View file

@ -0,0 +1,8 @@
---
type: verdict
title: "Nested verdict — must be excluded"
decision: approved
description: "A verdict file at a nested level. A conformant navigator reaches it (dedup marks it seen) but MUST exclude it from the read-context — recursively, not only at the root. This is the leakage surface a graph-based exclusion would miss."
---
This body MUST NOT appear in the read-context.

View file

@ -0,0 +1,6 @@
---
type: project
title: "Orphan — unreachable"
---
Directory `c/` has no `index.md`, and nothing links to this file. A link-following navigator never reaches it; a directory-walking one wrongly would. It MUST be absent from the read-context, and its parent directory's missing `index.md` MUST NOT be an error (that rule binds the bundle root alone).

View file

@ -0,0 +1,10 @@
---
type: index
okf_version: 0.1
title: "Nav-golden — hierarchical navigation case"
---
Nav-golden hierarchical case. This bundle exercises depth-first traversal, both link forms (root-relative and relative), recursive verdict exclusion, resolved-path de-duplication, cycle termination, and the root-only binding of the missing-index rule.
- [Overview](overview.md)
- [Section A](/a/index.md)

View file

@ -0,0 +1,6 @@
---
type: project
title: "Overview"
---
Root-level concept reached by a relative link.

View file

@ -0,0 +1,16 @@
Nav-golden hierarchical case. This bundle exercises depth-first traversal, both link forms (root-relative and relative), recursive verdict exclusion, resolved-path de-duplication, cycle termination, and the root-only binding of the missing-index rule.
- [Overview](overview.md)
- [Section A](/a/index.md)
## project: Overview
Root-level concept reached by a relative link.
## methodology: Doc A
Level-1 concept reached depth-first after Overview.
## reference: Doc B
Level-2 concept reached depth-first, deepest in the traversal.

View file

@ -70,7 +70,19 @@ approved knowledge into. Two rules keep ingest and the learning loop apart:
the ingest stamp (`generated: true` plus an `ingest_manifest` reference, §7) and MUST NOT
touch curated or promoted files. If a generated filename collides with an existing file that
does NOT carry the stamp, materialization MUST fail — never overwrite curated content.
Index updating is idempotent and preserves curated links (§6).
Index updating is idempotent and preserves curated links (§6). The stamp is unforgeable
against **accident**, not against **will**: an operator who hand-copies a generated file —
stamp and all — into curated content makes it indistinguishable from ingest-owned content,
and a later re-materialization may remove it. This residual risk is **documented, not
prevented**; the boundary's honesty rests on curated authoring not forging the stamp (next
bullet) and on operators not hand-copying generated files.
- **No other writer may forge the stamp.** The stamp is the sole mark distinguishing
ingest-owned files from curated ones, so any authoring primitive that materializes a concept
file from **caller-supplied** frontmatter MUST reject a frontmatter carrying the *complete*
ownership stamp — `generated: true` together with an `ingest_manifest` reference — while
permitting either field alone (curated content may legitimately carry a single provenance
field). The check is on the complete stamp, never on the individual field names, so a
legitimate verbatim round-trip is preserved; it is a **validation, never a repair**.
## 4. The ingest manifest (the contract)
@ -110,7 +122,7 @@ environment. A manifest is therefore versionable and shareable without secrets.
| Field | Meaning |
|---|---|
| `id` | Unique within the manifest, matching `[a-z0-9][a-z0-9-]*`; names the generated file (§5). |
| `title` | Human-readable title; becomes the `title` frontmatter and the index link label. Single-line. |
| `title` | Human-readable title; becomes the `title` frontmatter and the index link label. Single-line, and MUST NOT contain `[` or `]` (they would break index-link and navigation parsing, method spec §3 Step 1); `(`/`)` remain permitted. Validated fail-fast at manifest load — rendered verbatim thereafter (§5, §6). |
| `query` | The extraction query, polymorphic on the source type (below). |
| `okf_type` | The generated file's OKF `type`. MUST NOT be `verdict` (case-insensitive; §3). |
| `max_rows` | Required positive integer cap on the extraction size (§8). |
@ -134,11 +146,19 @@ Materialization takes three EXPLICIT inputs: the manifest, the target bundle dir
grammar in §4 makes the name path-safe; the `ingest-` prefix keeps the namespace disjoint
from `index.md` and `promoted-verdict-*`). Splitting one extraction into per-row files is
an extension point, not version 1.
- **Frontmatter**exactly these keys, in exactly this order (OKF line-oriented
`key: value`; §7 defines the provenance semantics):
- **Frontmatter**these seven keys, in this order, form a **mandatory ordered prefix** on a
stamped file (OKF line-oriented `key: value`; §7 defines the provenance semantics):
`type`, `title`, `source_system`, `source_query`, `ingested_at`, `ingest_manifest`,
`generated`. All values MUST be single-line; the materializer MUST collapse whitespace runs
(including newlines) in `source_query` to single spaces.
`generated`. The version-1 materializer emits exactly this prefix and nothing after it, but
the CONTRACT is the prefix, not a closed set: additional frontmatter keys MAY follow it,
unconstrained in presence and order (OKF consumers preserve unknown fields, §7), so a
stamped file stays within the general OKF concept-frontmatter contract. Conformance to the
prefix is a **per-file** property — a non-conforming file is an individual failure and never
invalidates the rest of the bundle; the bundle carries no whole-bundle completeness
property. All values MUST be single-line; `title` is emitted **verbatim** (it is `[`/`]`-free
by §4, so verbatim rendering is safe — the invariant is met by validation, not repair); the
materializer MUST collapse whitespace runs (including newlines) in `source_query` to single
spaces.
- **Body rendering** — for `file` and `sql`: a markdown table, header row = column names,
data rows in source order. Cell values: text verbatim with `\` escaped as `\\`, `|` escaped
as `\|`, and newlines replaced by single spaces; integers in plain decimal; non-integral
@ -152,6 +172,13 @@ Materialization takes three EXPLICIT inputs: the manifest, the target bundle dir
whose frontmatter carries the ingest stamp, then writes the new set, then updates the index
(§6). Version 1 assumes ONE manifest per bundle; multiple manifests feeding one bundle is
an extension point.
- **Membership from disk, ordering from the caller (load-bearing axis).** The set of files a
re-materialization replaces is exactly those the bundle already carries the stamp for — read
from disk (§3), never a caller-supplied file list; a caller can therefore never name a file
for deletion. The ORDER of generated files and their index links (§6) is the manifest's
extraction order — supplied by the caller, never filesystem enumeration order. Splitting the
two keeps re-materialization from becoming a caller-controlled delete while keeping output
deterministic regardless of directory iteration order.
## 6. Index generation
@ -164,9 +191,10 @@ Materialization takes three EXPLICIT inputs: the manifest, the target bundle dir
- On re-materialization, index links whose target is an ingest-owned file removed in this run
(§5) MUST be removed; ALL other links — curated and promoted — are preserved verbatim. A
promoted verdict's index link therefore survives re-ingest (load-bearing, §11).
- The link label is the extraction `title` — generated files are context-layer content, so a
descriptive label is correct here. (The fixed-neutral-label rule of method spec §6 protects
the VERDICT layer and is untouched by this spec.)
- The link label is the extraction `title`, rendered **verbatim** — safe because `title` is
`[`/`]`-free by §4. Generated files are context-layer content, so a descriptive label is
correct here. (The fixed-neutral-label rule of method spec §6 protects the VERDICT layer and
is untouched by this spec.)
- *(reference limitation)* the index read-modify-write is not atomic — single-process use is
assumed, as in the promotion gate.
@ -244,8 +272,10 @@ spec §11 regime):
| Seam | The test MUST fail when… |
|---|---|
| Provenance stamping | a generated file no longer carries the §7 layer |
| Stamp integrity (curated writers) | a caller-supplied frontmatter carrying the complete ownership stamp (`generated: true` with `ingest_manifest`) stops being rejected by the verbatim authoring path (§3) |
| Navigability | the generated bundle stops being consumable by the UNCHANGED bundle-navigation code, index links included |
| Verdict reservation | a manifest mapping to `type: verdict` (or the reserved filename namespace) stops being rejected |
| Title link-safety | a `title` containing `[` or `]` stops being rejected fail-fast at manifest load (§4) |
| Re-ingest layer safety | re-materialization over a bundle with a promoted verdict deletes the verdict file or its index link |
| Golden regression | any byte of a golden extraction's expected bundle diverges |
| Network gate | an `http` source stops refusing fail-fast without the opt-in flag |

View file

@ -65,20 +65,42 @@ progressive disclosure — never by stuffing the whole bundle (or keyword-retrie
it) into the prompt:
- Navigation starts at `index.md` and follows its intra-bundle markdown cross-links
(`](target.md)`). Targets containing a path separator are out-of-bundle and MUST be
skipped. Repeated links are de-duplicated; order is deterministic (index first, then links
in first-seen order). *(reference: the link pattern is `\]\(([^)]+\.md)\)`)*
- A missing `index.md` is an error (a bundle has no entry point without it). A broken or
bundle-escaping cross-link MUST be tolerated — skipped, never raised (OKF robustness rule);
path resolution MUST be boundary-checked against the bundle directory, fail-closed.
(`](target.md)`). A target is resolved **relative to the bundle** and boundary-checked
fail-closed (below): a leading `/` denotes the **bundle root** (NEVER a filesystem-absolute
path), any other form is relative to the linking file's own directory — so a target MAY
address a nested directory (`sub/index.md`, `/a/b.md`). It is **escape, not depth**, that is
forbidden: a target resolving outside the bundle is skipped; a legitimate nested target is
followed. (This resolve-and-boundary-check REPLACES the old "a path separator means
out-of-bundle" heuristic, which conflated depth with escape and forbade valid hierarchy.)
Each index entry descends **one path segment per level** — a per-level index links its
immediate children, never a multi-segment path. Traversal is **depth-first in first-seen
link order** and fully deterministic (the root index first, then each link followed in the
order it appears, recursively). Repeated links are de-duplicated **on the resolved path**, so
`./a.md` and `a.md` are one entry and cycles terminate. *(reference: the link pattern is
`\]\(([^)]+\.md)\)`)*
- A missing `index.md` **at the bundle root** is an error (a bundle has no entry point without
it) — this binds the **root alone**: an intermediate directory reached by a link is
navigated only through the links its own files carry, never by directory enumeration, so a
nested directory without its own `index.md` is not an error (its unlinked content is simply
unreachable). A broken, malformed, or bundle-escaping cross-link MUST be tolerated — skipped,
never raised (OKF robustness rule): a target that fails to resolve for ANY reason (missing
file, invalid path component, escape) is skipped, not raised. Path resolution MUST be
boundary-checked against the bundle directory, fail-closed — this is the SOLE
in-/out-of-bundle test.
- Frontmatter is the leading `---`-delimited block, parsed line-oriented as `key: value`
strings; the single required field is `type`; unknown fields MUST be preserved.
- The rendered read-context is the index body (the summary) followed by each non-index
concept file as a `## {type}: {title}` section; empty sections are dropped.
- **`type: verdict` files MUST be excluded from the read-context.** Prior verdicts reach the
hypothesis prompt ONLY via the gated experience fold below — never via context rendering,
and never via a query-time retrieval tool pointed at the bundle (which would re-leak the
verdict layer).
concept file as a `## {type}: {title}` section; empty sections are dropped. Rendering is
**flat regardless of nesting depth** — directory structure is navigation, not presentation,
so a nested concept file renders as the same `## {type}: {title}` section a root file would;
there is no level heading.
- **`type: verdict` files MUST be excluded from the read-context.** The exclusion is a **type
check on each file as it is reached** — applied recursively at every level, NEVER a property
of the link graph: a file is excluded because it is `type: verdict`, not because of where it
sits or how it was linked, so a mislabelled or injected navigation edge can never smuggle a
verdict into the context. Prior verdicts reach the hypothesis prompt ONLY via the gated
experience fold below — never via context rendering, and never via a query-time retrieval
tool pointed at the bundle (which would re-leak the verdict layer).
**Experience fold (ExpeL-style, the learning seam):** before generation, the candidate's
prior verdicts are retrieved from the store and folded into the hypothesis prompt:
@ -399,7 +421,8 @@ cross-reference):
| Seam | The test MUST fail when… | Reference test |
|---|---|---|
| Step-1 fold | a prior verdict no longer reaches the next hypothesis prompt; control: an empty store changes the outcome signal | `test_step1_expel_loadbearing.py` |
| Verdict-layer exclusion | the realization signal appears in the rendered read-context | `test_okf.py` (bundle-context exclusion) |
| Verdict-layer exclusion | the realization signal appears in the rendered read-context, at the root OR any nested level | `test_okf.py` (bundle-context exclusion) |
| Navigation boundary | an escaping cross-link (`..`, a filesystem-absolute path, or a bundle-root `/` read as filesystem-absolute) is followed, a malformed target (e.g. an invalid path component) is raised instead of skipped, or a legitimate nested in-bundle link is skipped | `test_okf.py` (navigation boundary) |
| Checker gate | the checker's surfaced output is detached OR its REJECT no longer overrides a validated outcome | `test_checker_gate_loadbearing.py` |
| Informed refinement | the prior rejection reason no longer appears verbatim in the next prompt / the outcome never flips | `test_step5_refine_loadbearing.py` |
| Async file loop | a verdict dropped after Run A fails to reach Run B's prompt via a FRESH store; control: an empty inbox | `test_step7_async_loop_loadbearing.py` |