feat(prepass): the excerpt's new fields reach the prompt, po stops dropping them twice

P2 measured that the producer's payload now carries title/req_number/sources/source_* on
every excerpt (14 members, was 9), but PrepassExcerpt ignored them (extra="ignore") and
_data_blocks rendered only concept_id/adjudication/trust_tier -- so (b') was a po verdict,
never a model verdict. title/req_number/sources are now named fields; source_* locators are
read via model_extra and a prefix scan (measured: the producer treats source_* as an
open-ended family, not a fixed allowlist), so a future producer's new source_foo key reaches
the prompt without a code change here. A P1-form payload renders byte-identical to before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-08 16:42:17 +02:00
commit 2453246d4b
5 changed files with 406 additions and 3 deletions

View file

@ -2171,6 +2171,28 @@ Python ≥3.10. MAF (`agent-framework-core` 1.16.0, `-orchestrations` 1.1.1 —
projeksjonene som forankret den; og ingen betalt kjøring er gjort — begge funn er målt offline mot projeksjonene som forankret den; og ingen betalt kjøring er gjort — begge funn er målt offline mot
basen og artefaktene økt 98 etterlot. Måling: basen og artefaktene økt 98 etterlot. Måling:
`docs/2026-09-08-f3-f4-nekten-og-forankringen.md` § 2. `docs/2026-09-08-f3-f4-nekten-og-forankringen.md` § 2.
- **`PrepassExcerpt` bærer utdragets `title`/`req_number`/`sources` som navngitte felt og hver
`source_*`-lokator via `model_extra` (P3, 08.09):** en re-måling av N100/N200/N500 mot okfs
oppdaterte produsent (14 medlemmer, var 9) fant at `po` droppet de fem nye feltene TO ganger —
`PrepassExcerpt` var `extra="ignore"` ved parsing, og `_data_blocks` rendrer bare
`concept_id`/`adjudication`/`trust_tier`. Konsekvensen var observerbar i modellens egne ord: et
svar siterte en UUID fra DATA-avgrenseren fordi det var eneste identifikator som nådde prompten,
mens kravnummeret lå urørt i payloaden. **`title`/`req_number`/`sources` er navngitte, valgfrie
felt** (SS-8-deklarerte, entallige — hvert payload skrevet før i dag mangler dem, så et påkrevd
felt ville refusert historikken). **`source_*`-lokatorene leses IKKE som navngitte felt**:
producerens egen melding kaller dem en PREFIKS-REGEL, ikke en allowlist (K2 bærer fem, N-bundlene
to), og målt er `extra="allow"` + `source_locators()`s prefikssøk mot `model_extra` det ENESTE av
de to formene en fremtidig tredje `source_*`-nøkkel når prompten gjennom UTEN en kodeendring her.
`_excerpt_header` legger feltene til DATA-avgrenserens parentes KUN når de finnes — et P1-form
utdrag (ingen av de fem) rendrer BYTE-IDENTISK med før, pinnet literalt (ikke en delstreng-assert,
som ikke ville fanget en lekkende tom klausul). `render_seed` og `render_context` deler
`_data_blocks` (ko-(p)); en fiks som traff bare den ene ville latt utforsknings-døra ligge bak
debatt-døra. Load-bearing MÅLT (`tests/test_prepass_excerpt_fields_loadbearing.py`, 10 armer): 8
røde før fiksen, 10/10 grønne etter, og en mutasjon (revert til § "to felt"-formen) rød på nøyaktig
de tre rendrings-armene mens parsing-armene forble grønne — beviser at de to fiksede stedene har
hver sin uavhengige gate. **Én betalt N100-arm med `--require-cost-baseline`: rc 1, 0 modellkall,
NOK 0,00** — (b) forblir STRUKTURELT umålbar under flagget (F4: en vegnormal bærer ingen
kostlinjer, uendret av denne fiksen). Måling: `docs/2026-09-08-n-bundlene-hypoteseform.md` § 10.
- **STATE.md er local-only** (gitignored). Voyage session-state er efemert; STATE.md er kanonisk kontinuitet. - **STATE.md er local-only** (gitignored). Voyage session-state er efemert; STATE.md er kanonisk kontinuitet.
- Prosess: Voyage-plugin (`/trekbrief → /trekplan → /trekexecute → /trekreview`) per større fase. - Prosess: Voyage-plugin (`/trekbrief → /trekplan → /trekexecute → /trekreview`) per større fase.

View file

@ -501,7 +501,10 @@ when the seam is detached, so the loop cannot silently degrade into theater.
``` ```
The cut is printed and written to `{run_id}-prepass.json`. **This buys a declared cut, not a The cut is printed and written to `{run_id}-prepass.json`. **This buys a declared cut, not a
cheaper run** — the delivered text costs more than a pointer does. And note the limit it inherits: cheaper run** — the delivered text costs more than a pointer does. When the payload's producer
declares a `title`, `req_number` or `sources` address for an excerpt, that reaches the prompt
alongside the text, so a proposal can cite the requirement a person would — not only the internal
identifier the base assigns it. And note the limit it inherits:
an *empty* delivery is evidence the base does not answer the question, but a *full* one is not an *empty* delivery is evidence the base does not answer the question, but a *full* one is not
evidence that it does. The excerpts are the closest matches, which is not the same as an answer; evidence that it does. The excerpts are the closest matches, which is not the same as an answer;
the run says so to the agents in as many words. the run says so to the agents in as many words.

View file

@ -474,3 +474,126 @@ avgrensning operatøren har tatt stilling til.
| 22 | Parse-feilen på N200 | `p2-n200-free-parse-failures.json` finnes; feilteksten sitert ordrett | | 22 | Parse-feilen på N200 | `p2-n200-free-parse-failures.json` finnes; feilteksten sitert ordrett |
| 23 | Prisene | Azure Retail Prices API hentet på nytt 08.09 | | 23 | Prisene | Azure Retail Prices API hentet på nytt 08.09 |
| 24 | 0 × 429 | `retries`-telleren i alle 18 poster | | 24 | 0 × 429 | `retries`-telleren i alle 18 poster |
---
## 10. P3 (ordre `20260908T141941Z`) — po bærer utdragets nye felt gjennom til prompten
PM-valg 08.09 14:20Z, på denne målingens egen anbefaling (§ 6): **ja, to linjer, rød test først.**
### 10.1 Reproduksjon (steg 1, uendret fra § 4)
```python
>>> sorted(json.load(open("scratchpad/nbundler-p2/payload-n500.json"))["excerpts"][0].keys())
['adjudication', 'bundle_id', 'bundle_id_inherited', 'concept_id', 'rank', 'req_number',
'sha256', 'source_element_id', 'source_sha256', 'sources', 'text', 'text_sha256', 'title',
'trust_tier'] # 14 medlemmer, i payloaden
>>> sorted(prepass.load_prepass_payload("scratchpad/nbundler-p2/payload-n500.json").excerpts[0]
... .model_dump().keys())
['adjudication', 'bundle_id', 'concept_id', 'sha256', 'text', 'text_sha256', 'trust_tier']
# 7 medlemmer, etter parsing
>>> "req_number" in prepass.render_context(...)
False # aldri i prompten (§ 4)
```
### 10.2 Feltform, valgt med måling
Spørsmålet ordren stiller: hvilken form overlever en ukjent `source_foo`-nøkkel fra en FRAMTIDIG
produsent, uten kodeendring her? okfs egen melding (§ 4, sitert) kaller `source_*` en **prefiks-
regel, ikke en allowlist** — K2 bærer fem slike lokatorer der N-bundlene bærer to. To former ble
sammenliknet mot nøyaktig det spørsmålet:
- **Navngitte felt** (`source_element_id: str | None`, `source_sha256: str | None`, …): en tredje
`source_*`-nøkkel krever et NYTT felt på `PrepassExcerpt` og en ny rendringslinje — en
kodeendring, nøyaktig det prefiksregelen sier man IKKE skal måtte gjøre.
- **`model_extra`** (`extra="allow"``PrepassExcerpt` ALENE, lest tilbake via et
`source_`-prefikssøk i `source_locators()`): en ny `source_*`-nøkkel havner unavngitt i
`model_extra` og plukkes opp av SAMME søk, null kodeendring.
**Valgt: `model_extra` for `source_*`-familien.** `title`, `req_number` og `sources` er derimot
navngitte felt — de er SS-8-deklarerte, entallige medlemmer (aldri en voksende familie), og en
`model_extra`-lesning av dem ville kastet bort pydantics egen validering for ingen gevinst.
Testen `test_a_future_source_star_key_survives_without_a_code_change`
(`tests/test_prepass_excerpt_fields_loadbearing.py`) legger til en TREDJE, aldri navngitt
`source_page_number`-nøkkel og viser at den når `source_locators()` uendret.
### 10.3 Fiksen: to steder, ikke to linjer
Anbefalingen i § 6 anslo "to linjer"; målt var det to STEDER (parsing + rendering), hver med mer
enn én linje fordi `sources` trengte en egen typet klasse (`PrepassSource`, med `resource` og en
valgfri `title`) og `source_locators()` trengte en egen metode:
1. **Parsing** (`PrepassExcerpt`): `title: str | None`, `req_number: str | None`,
`sources: tuple[PrepassSource, ...] | None`, alle default `None` (de fleste payloader i dette
repoet er fra FØR P2s produsent-endring og bærer ingen av dem — et påkrevd felt ville refusert
hver payload skrevet før i dag). `model_config = ConfigDict(extra="allow")` — kun på DENNE
klassen, `_Permissive`s `extra="ignore"` står urørt for `PrepassBundle`/`PrepassBudget`/osv.
2. **Rendering** (`_excerpt_header`, ny funksjon `_data_blocks` nå kaller): `req_number` og
`title` legges til i parentesen ved siden av `adjudication`/`trust_tier` NÅR de finnes;
`sources[0].resource` (adressen) og hver `source_*`-lokator (generisk, via
`source_locators()`) legges til deretter. **Kjent-negativ:** når ingen av de nye feltene
finnes, er sløyfa en no-op og headeren er BYTE-IDENTISK med før P3
(`test_a_p1_form_payload_renders_the_header_exactly_as_before`,
`test_a_p1_form_payload_render_seed_also_unchanged`) — pinnet LITERALT, ikke med en
delstreng-assert, fordi en lekkende tom-klausul (f.eks. en etterlatt `, `) ikke ville vist seg
i et substring-søk.
`render_seed` deler `_data_blocks` med `render_context` (ko-(p)); en fiks som bare traff den ene
armen ville latt utforsknings-døra ligge stille bak debatt-døra —
`test_render_seed_also_carries_the_new_fields` gater begge.
### 10.4 Rødt → grønt → mutasjon rød
Ny fil: `tests/test_prepass_excerpt_fields_loadbearing.py`, 10 arm. FØR fiksen: **8 røde, 2
grønne** (de to kjent-negative armene er trivielt grønne før fiksen òg — de er kontroller, ikke
mål). ETTER fiksen: **10 av 10 grønne.**
**Mutasjon** (revert `_excerpt_header`/`_data_blocks` til § 4s form, midlertidig, aldri committet):
3 av 10 røde — nøyaktig de tre rendrings-armene
(`test_the_data_block_renders_req_number_title_and_the_address`,
`test_the_data_block_renders_a_future_locator_too`, `test_render_seed_also_carries_the_new_fields`)
— parsing-armene forblir grønne (mutasjonen rørte ikke `PrepassExcerpt`), som beviser at de to
halvdelene av fiksen har HVER SIN uavhengige gate. Fiksen restaurert fra en kopi (`shasum -a 1`
verifisert identisk før/etter), aldri `git checkout`.
### 10.5 Suite, golden, lint
**1521 passed, 5 skipped** (var 1511 — 10 nye), `pytest -q`, 272 s. `ruff check src tests` og
`ruff format --check src tests`: rene (94 funn i `scratchpad/` er FØR-eksisterende, utracket,
utenfor scope — samme avgrensning som K3-ordrenes `ruff check src tests tools`). `mypy src`: 0
funn, 37 filer. Golden `demo-transcript.stdout` UENDRET: `shasum -a 1` = `ea8c534…` — forventet,
`simulation.py` importerer ikke `prepass` i det hele tatt.
### 10.6 Én betalt N100-kjøring med `--require-cost-baseline` — og (b) er STRUKTURELT ikke dekket
Samme oppsett som § 3 (`live_nbundler_p2.py n100 --require-cost-baseline`), med den FIKSEDE koden.
```
KJENT-POSITIV n100: navigate_bundle=446 (fasit 446) OK
run refused: this run was required to be anchored, but the knowledge base offers no cost baseline: …
===== ARM n100 [req] (rc=1) =====
MODELLKALL: 0 (gaten MAA fyre foer foerste kall)
```
**rc=1, 0 modellkall, NOK 0,00.** Dette REPRODUSERER § 2s funn ordrett — `--require-cost-baseline`
nekter N100 FØR første modellkall, fordi en vegnormal ikke bærer kostlinjer (strukturelt, uendret
av denne fiksen). **Konsekvens: (b) kan IKKE måles i denne armen** — det oppstår aldri en
hypotese å navngi et konsept for, uansett hvor godt utdraget bærer `req_number`/`title`. Dette er
IKKE en regresjon fra fiksen; det er § 2s "en N-bundle kan ikke samtidig være forankret og
produsere en hypotese" gjentatt på den fiksede koden.
**(b) er derfor "ikke dekket" i DENNE armen — strukturelt, ikke som en feil ved fiksen.**
Fiksens EGEN korrekthet er verifisert der den kan verifiseres gratis: offline, mot payloadens
faktiske felt (§ 10.110.4). En live gjenmåling av (b) på den FRIE armen (uten flagget, slik § 3
kjørte N100) ville kostet et nytt betalt kall utenfor denne ordrens scope (ordren navngir
eksplisitt `--require-cost-baseline`-armen som den ENE betalte kjøringen); det er ikke gjort her.
### 10.7 Funn — rapportert, ikke fikset
7. **(b) er umålbar under `--require-cost-baseline` for enhver N-bundle** (§ 10.6). Samme
struktur som funn 2 i § 7, nå bekreftet på den fiksede koden. Ingen handling — F4 gjør nøyaktig
det den ble bygget for.
8. **En live gjenmåling av (b) på den frie N100-armen (uten flagget) er ikke gjort** her — ordren
navngir `--require-cost-baseline` som den ene betalte kjøringen, og en fri kjøring ville vært
en ny, ubedt betalt handling.

View file

@ -110,6 +110,16 @@ class PrepassDenominators(_Permissive):
delivered: int delivered: int
class PrepassSource(_Permissive):
"""SS 8's declared address for an excerpt: where the underlying source document lives.
A fixed, singular member (never a growing family like ``source_*``) named on purpose.
"""
resource: str
title: str | None = None
class PrepassExcerpt(_Permissive): class PrepassExcerpt(_Permissive):
"""One delivered unit of bundle content. """One delivered unit of bundle content.
@ -117,8 +127,21 @@ class PrepassExcerpt(_Permissive):
names no content member at all a payload can be fully conformant and carry nothing to read, names no content member at all a payload can be fully conformant and carry nothing to read,
and this seam's entire value is the content. Requiring them refuses that case by name instead and this seam's entire value is the content. Requiring them refuses that case by name instead
of discovering it as an empty prompt. of discovering it as an empty prompt.
``title``, ``req_number`` and ``sources`` are named fields because they are SS-8-declared,
singular members every payload has at most one of each. ``source_*`` locators
(``source_element_id``, ``source_sha256``, and whatever a future producer adds) are NOT named
here: measured (P3, order 20260908T141941Z), the producer's own message calls that a PREFIX
RULE rather than a fixed allowlist a real corpus already carries five such members where
another carries two. Naming two of them would need a new field and a new render line for a
third; ``extra="allow"`` plus :meth:`source_locators`' prefix scan needs neither. Every field
here defaults to ``None`` (or, for ``sources``, absent) because most payloads in this
repository predate P2's producer change and carry none of them — a required field would
refuse every payload written before today.
""" """
model_config = ConfigDict(extra="allow")
bundle_id: str bundle_id: str
concept_id: str concept_id: str
sha256: str sha256: str
@ -126,6 +149,26 @@ class PrepassExcerpt(_Permissive):
trust_tier: str trust_tier: str
text: str text: str
text_sha256: str text_sha256: str
title: str | None = None
req_number: str | None = None
sources: tuple[PrepassSource, ...] | None = None
def source_locators(self) -> tuple[tuple[str, str], ...]:
"""Every ``source_*`` scalar the producer attached, sorted by key.
Reads ``model_extra`` rather than a named field for each one the prefix-rule reason
this class's docstring gives. ``sources`` itself is excluded: it is a named field (a list,
never a scalar) and does not land in ``model_extra`` in the first place, but the guard
is explicit rather than relying on that.
"""
extra = self.model_extra or {}
return tuple(
sorted(
(key, str(value))
for key, value in extra.items()
if key.startswith("source_") and key != "sources"
)
)
class PrepassWithheld(_Permissive): class PrepassWithheld(_Permissive):
@ -505,14 +548,39 @@ def _declaration_lines(payload: PrepassPayload, *, rest_reachable: bool = False)
] ]
def _excerpt_header(excerpt: PrepassExcerpt) -> str:
"""The BEGIN line for one excerpt: ``concept_id`` plus whatever the producer named it with.
**Known-negative, load-bearing:** a P1-form excerpt (none of the five new fields) renders
BYTE-IDENTICAL to before P3 the loop below appends nothing, and the line is exactly the
old ``(adjudication: ..., trust_tier: ...)`` form
(``test_a_p1_form_payload_renders_the_header_exactly_as_before``). Carrying a field through
must never change what an older payload renders as.
``req_number`` and ``title`` are what a person would cite; the address (``sources[0].resource``)
and every ``source_*`` locator are what lets a claim be traced back to the document that
produced it measured (P2 SS 4) to be the exact thing modelled proposals cited a UUID instead
of, because the UUID was the only identifier that reached the prompt.
"""
fields = [f"adjudication: {excerpt.adjudication}", f"trust_tier: {excerpt.trust_tier}"]
if excerpt.req_number is not None:
fields.append(f"req_number: {excerpt.req_number}")
if excerpt.title is not None:
fields.append(f"title: {excerpt.title}")
if excerpt.sources:
fields.append(f"source: {excerpt.sources[0].resource}")
for key, value in excerpt.source_locators():
fields.append(f"{key}: {value}")
return f"--- BEGIN DATA {excerpt.concept_id} ({', '.join(fields)}) ---"
def _data_blocks(payload: PrepassPayload) -> list[str]: def _data_blocks(payload: PrepassPayload) -> list[str]:
"""The delimited DATA blocks, one per delivered excerpt (SS 9.3), shared by both arms.""" """The delimited DATA blocks, one per delivered excerpt (SS 9.3), shared by both arms."""
lines: list[str] = [] lines: list[str] = []
for excerpt in payload.excerpts: for excerpt in payload.excerpts:
lines += [ lines += [
"", "",
f"--- BEGIN DATA {excerpt.concept_id} " _excerpt_header(excerpt),
f"(adjudication: {excerpt.adjudication}, trust_tier: {excerpt.trust_tier}) ---",
excerpt.text, excerpt.text,
f"--- END DATA {excerpt.concept_id} ---", f"--- END DATA {excerpt.concept_id} ---",
] ]

View file

@ -0,0 +1,187 @@
"""Load-bearing gate for order 20260908T141941Z (P3): the excerpt's new fields reach the prompt.
P2 (`docs/2026-09-08-n-bundlene-hypoteseform.md` SS 4) measured that llm-ingestion-okf's producer
now emits ``title``, ``req_number``, ``sources`` and per-excerpt ``source_*`` locators on every
excerpt (14 members, was 9) -- and that ``po`` drops all five TWICE: ``PrepassExcerpt`` is
``extra="ignore"`` so parsing throws them away, and ``_data_blocks`` renders only ``concept_id``,
``adjudication`` and ``trust_tier`` so even a field that survived parsing would not reach the
DATA block. (b') -- "did the model name the concept" -- was therefore a ``po`` verdict, never a
model verdict: no implementation of the model could have passed it while the field never arrived.
**Field-form decision, measured, not guessed.** The producer's OWN message (cited in P2 SS 4,
"differansen er at K2 baerer fem source_*-lokatorer der N-bundlene baerer to -- prefiks-regelen,
ikke en allowlist") states that ``source_*`` is an open-ended FAMILY, not a fixed two-member
allowlist. Two forms were compared against "does a future producer's new ``source_foo`` key reach
the prompt without a code change here":
- **Named fields** (``source_element_id: str | None``, ``source_sha256: str | None``, ...): a
producer adding a THIRD ``source_*`` member requires a new field declared on ``PrepassExcerpt``
and a new line in the renderer -- a code change, exactly what the prefix rule says not to expect.
- **``model_extra``** (``extra="allow"`` on ``PrepassExcerpt`` alone, read back via a
``source_``-prefix scan): a new ``source_*`` key lands in ``model_extra`` unnamed and is picked
up by the SAME scan with zero code change.
The second form is what P3 implements, for ``source_*`` locators only. ``title``, ``req_number``
and ``sources`` are SS-8-declared, singular, fixed members -- not a growing family -- so they are
named fields.
**Known-negative, gated separately below:** a payload without the five new fields (the P1 form,
9 members) must render BYTE-IDENTICAL to before this change. The header line's format is the
thing that could drift, so it is pinned literally, not diffed against a recorded golden string.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from portfolio_optimiser import prepass
FIXTURE = Path(__file__).parent / "fixtures" / "prepass" / "bygg-energi-mikro-fixture.payload.json"
def _raw() -> dict[str, Any]:
return json.loads(FIXTURE.read_text(encoding="utf-8"))
def _raw_with_new_fields() -> dict[str, Any]:
"""The P1-form fixture's first excerpt, upgraded to the shape P2 measured on a real payload."""
raw = _raw()
excerpt = raw["excerpts"][0]
assert excerpt["concept_id"] == "bygg-kontor-nord"
excerpt["title"] = "Kontorbygg Nord (BYGG-KONTOR-NORD)"
excerpt["req_number"] = "Krav 3.3.113"
excerpt["sources"] = [
{"resource": "https://viewers.vegnorm.vegvesen.no/api/nisosts/859984", "title": "N100:2023"}
]
excerpt["source_element_id"] = "id-4b61eee9-a149-42b3-863d-293b8320c15a"
excerpt["source_sha256"] = "c58e8bbc5fa9a5400c111e51b04c05f2cfd9edabd884ef5352a486fdab2cb5ab"
return raw
# --- the fields survive parsing ---------------------------------------------------------------
def test_the_excerpt_carries_title_req_number_and_sources() -> None:
payload = prepass.PrepassPayload.model_validate(_raw_with_new_fields())
excerpt = payload.excerpts[0]
assert excerpt.title == "Kontorbygg Nord (BYGG-KONTOR-NORD)"
assert excerpt.req_number == "Krav 3.3.113"
assert excerpt.sources is not None
assert excerpt.sources[0].resource == "https://viewers.vegnorm.vegvesen.no/api/nisosts/859984"
def test_a_future_source_star_key_survives_without_a_code_change() -> None:
"""The measured discriminator: a THIRD locator a future producer adds, never declared as a
named field anywhere in this module, still reaches the excerpt through the prefix scan."""
raw = _raw_with_new_fields()
raw["excerpts"][0]["source_page_number"] = "12"
payload = prepass.PrepassPayload.model_validate(raw)
excerpt = payload.excerpts[0]
locators = dict(excerpt.source_locators())
assert locators["source_element_id"] == "id-4b61eee9-a149-42b3-863d-293b8320c15a"
assert locators["source_sha256"] == (
"c58e8bbc5fa9a5400c111e51b04c05f2cfd9edabd884ef5352a486fdab2cb5ab"
)
assert locators["source_page_number"] == "12"
def test_source_locators_excludes_the_named_sources_field() -> None:
"""``sources`` (the list) is a named field and must not double up as its own locator entry --
it is not itself a ``source_*`` SCALAR."""
payload = prepass.PrepassPayload.model_validate(_raw_with_new_fields())
keys = {key for key, _ in payload.excerpts[0].source_locators()}
assert "sources" not in keys
def test_a_p1_form_excerpt_has_no_source_locators() -> None:
"""The absence control: an excerpt carrying none of the new fields reports an empty tuple,
never a fabricated entry."""
payload = prepass.load_prepass_payload(str(FIXTURE))
assert payload.excerpts[0].source_locators() == ()
assert payload.excerpts[0].title is None
assert payload.excerpts[0].req_number is None
assert payload.excerpts[0].sources is None
# --- the fields reach the DATA block ------------------------------------------------------------
def _header_for(rendering: str, concept_id: str) -> str:
for line in rendering.split("\n"):
if line.startswith(f"--- BEGIN DATA {concept_id} "):
return line
raise AssertionError(f"no BEGIN DATA line for {concept_id!r} in rendering")
def test_the_data_block_renders_req_number_title_and_the_address() -> None:
payload = prepass.PrepassPayload.model_validate(_raw_with_new_fields())
header = _header_for(prepass.render_context(payload), "bygg-kontor-nord")
assert "Krav 3.3.113" in header
assert "Kontorbygg Nord (BYGG-KONTOR-NORD)" in header
assert "https://viewers.vegnorm.vegvesen.no/api/nisosts/859984" in header
assert "id-4b61eee9-a149-42b3-863d-293b8320c15a" in header
def test_the_data_block_renders_a_future_locator_too() -> None:
"""The renderer reads the SAME ``source_locators()`` scan the parsing test proved is
code-change-free, so a new locator reaches the prompt without touching ``_data_blocks``."""
raw = _raw_with_new_fields()
raw["excerpts"][0]["source_page_number"] = "12"
payload = prepass.PrepassPayload.model_validate(raw)
header = _header_for(prepass.render_context(payload), "bygg-kontor-nord")
assert "source_page_number: 12" in header
def test_render_seed_also_carries_the_new_fields() -> None:
"""``render_seed`` shares ``_data_blocks`` with ``render_context`` (ko-(p)); a fix that patched
only one arm would leave the exploration door silently behind the debate door."""
payload = prepass.PrepassPayload.model_validate(_raw_with_new_fields())
header = _header_for(prepass.render_seed(payload), "bygg-kontor-nord")
assert "Krav 3.3.113" in header
assert "Kontorbygg Nord (BYGG-KONTOR-NORD)" in header
# --- known-negative: a P1-form payload renders byte-identical to before ------------------------
def test_a_p1_form_payload_renders_the_header_exactly_as_before() -> None:
"""The known-negative. Every excerpt in the shipped fixture carries none of the five new
fields, so the header must be EXACTLY the old two-field form -- pinned literally, because a
change to the conditional append logic that leaked an empty clause (e.g. a trailing ``, ``)
would not show up in a substring assert."""
payload = prepass.load_prepass_payload(str(FIXTURE))
rendering = prepass.render_context(payload)
for excerpt in payload.excerpts:
expected = (
f"--- BEGIN DATA {excerpt.concept_id} "
f"(adjudication: {excerpt.adjudication}, trust_tier: {excerpt.trust_tier}) ---"
)
assert expected in rendering, rendering
def test_a_p1_form_payload_render_seed_also_unchanged() -> None:
payload = prepass.load_prepass_payload(str(FIXTURE))
rendering = prepass.render_seed(payload)
for excerpt in payload.excerpts:
expected = (
f"--- BEGIN DATA {excerpt.concept_id} "
f"(adjudication: {excerpt.adjudication}, trust_tier: {excerpt.trust_tier}) ---"
)
assert expected in rendering, rendering
def test_a_partially_upgraded_payload_only_appends_the_fields_it_has() -> None:
"""One excerpt carries the new fields, a sibling does not (a real corpus mixes eras of
ingestion) -- the sibling's header must stay in the old two-field form."""
raw = _raw_with_new_fields()
payload = prepass.PrepassPayload.model_validate(raw)
rendering = prepass.render_context(payload)
sibling = payload.excerpts[1]
assert sibling.title is None
expected_sibling = (
f"--- BEGIN DATA {sibling.concept_id} "
f"(adjudication: {sibling.adjudication}, trust_tier: {sibling.trust_tier}) ---"
)
assert expected_sibling in rendering, rendering