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:
parent
c6e6066b1b
commit
2453246d4b
5 changed files with 406 additions and 3 deletions
22
CLAUDE.md
22
CLAUDE.md
|
|
@ -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
|
||||
basen og artefaktene økt 98 etterlot. Måling:
|
||||
`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.
|
||||
- Prosess: Voyage-plugin (`/trekbrief → /trekplan → /trekexecute → /trekreview`) per større fase.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
| 23 | Prisene | Azure Retail Prices API hentet på nytt 08.09 |
|
||||
| 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"` på `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.1–10.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.
|
||||
|
||||
|
|
|
|||
|
|
@ -110,6 +110,16 @@ class PrepassDenominators(_Permissive):
|
|||
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):
|
||||
"""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,
|
||||
and this seam's entire value is the content. Requiring them refuses that case by name instead
|
||||
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
|
||||
concept_id: str
|
||||
sha256: str
|
||||
|
|
@ -126,6 +149,26 @@ class PrepassExcerpt(_Permissive):
|
|||
trust_tier: str
|
||||
text: 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):
|
||||
|
|
@ -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]:
|
||||
"""The delimited DATA blocks, one per delivered excerpt (SS 9.3), shared by both arms."""
|
||||
lines: list[str] = []
|
||||
for excerpt in payload.excerpts:
|
||||
lines += [
|
||||
"",
|
||||
f"--- BEGIN DATA {excerpt.concept_id} "
|
||||
f"(adjudication: {excerpt.adjudication}, trust_tier: {excerpt.trust_tier}) ---",
|
||||
_excerpt_header(excerpt),
|
||||
excerpt.text,
|
||||
f"--- END DATA {excerpt.concept_id} ---",
|
||||
]
|
||||
|
|
|
|||
187
tests/test_prepass_excerpt_fields_loadbearing.py
Normal file
187
tests/test_prepass_excerpt_fields_loadbearing.py
Normal 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.1–13"
|
||||
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.1–13"
|
||||
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.1–13" 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.1–13" 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue