feat(okf): derive a cost baseline from a priced schedule, or refuse

MAJOR-4 (misjonsreview v2 section 7), owner B = the consumer. Neither existing
projection into ir.CostBaseline can serve an ingested tender corpus:
cost-baseline.json is hand-written per project and baseline_from_project belongs
to the road domain, so a K2-shaped bundle could be navigated and never anchored.
okf.derive_cost_baseline reads the numbers already in the bundle.

The premise was MEASURED before anything was built on it, and the order's two
pointers named two different forms. examples/*/expected-bundle/ carry pipe
tables, but every one of them is csv- or sql-sourced via render.render_table --
the xlsx path never reaches render_table at all. Measured with pandoc 3.10.2
under the producer's own writer and arguments: extract._extract_office converts,
and inbox.py hands that text to render_inbox_concept untouched, so an
xlsx-sourced concept file carries a pandoc SIMPLE table whose dash rule defines
the column spans. A pipe-only reader would have been inert on exactly the corpus
this exists for. Both forms are read, by two scanners over one role mapping and
one number grammar.

No judgement anywhere: the header vocabulary and the number grammar are closed,
and every ambiguity refuses -- no candidate table, more than one, two columns
claiming one role, two rows sharing a cost code, a row that prices nothing. A
partly-priced schedule refuses in full, because a half-derived baseline anchors
some codes while cost_baseline_anchored reports True.

Wired behind --derive-cost-baseline and never silently: one resolution in
run.py's bundle arm serves both the full run and the dry run, and the refusal
propagates rather than degrading to the file loader.

The two fixtures are pandoc's output verbatim, not hand-typed. The unpriced one
is K2's actual pre-award shape, and the columns survive as blanks -- so the
table IS a candidate and the refusal is the sharp one.

Load-bearing MEASURED: 18 mutations all red against the WHOLE suite, green
control 1230 passed / 5 skipped (from 1208/5, superset, 0 removed), golden
demo-transcript.stdout byte-unchanged (ea8c534773acdbe41ae68f2c55724d69aaf8be4f).
M17 is the one that matters for arm (b): dropping the positivity guard makes the
mutant raise pydantic ValidationError, which IS a ValueError but is NOT the named
class -- so pytest.raises(ValueError) would have stayed green against exactly the
mutation the arm exists to catch. Verified directly, not argued.

Honesty limits stated in the invariant row: NS 3451 section rows are not
classified (K2 itself was not available to measure), the stamp records that a run
was anchored and never which projection anchored it, the hosted surface is
deliberately untouched, and no live K2 file was read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-03 03:15:39 +02:00
commit 0add73531b
11 changed files with 870 additions and 7 deletions

View file

@ -1408,6 +1408,71 @@ Python ≥3.10. MAF (`agent-framework-core` 1.16.0, `-orchestrations` 1.1.1 —
parameteren har aldri vært øvet, og det er RAPPORTERT, ikke fikset (utenfor denne ordren); parameteren har aldri vært øvet, og det er RAPPORTERT, ikke fikset (utenfor denne ordren);
`verified_field` validerer ikke `at`; og at en LEVENDE modell kaller falsifiseringsskillen godt er `verified_field` validerer ikke `at`; og at en LEVENDE modell kaller falsifiseringsskillen godt er
ikke bevist (structured-output-grensens klasse). ikke bevist (structured-output-grensens klasse).
- **Den TREDJE projeksjonen inn i `CostBaseline` DERIVERES fra en tabell som alt er i basen — og
den nekter heller enn å oppfinne (MAJOR-4, økt 78):** `cost-baseline.json` er håndskrevet per
prosjekt og `baseline_from_project` tilhører veg-domenet, så et INGESTERT anbudskorpus kunne
navigeres og aldri forankres. `okf.derive_cost_baseline(bundle, *, project_id)` leser
prisskjemaet. **`project_id` er PÅKREVD keyword, ikke lest av basen:** `run._project_from_bundle`
fail-faster alt kjøringens id mot basens `validator-input.json`, så en andre lesing her ville vært
kø-(p) — og ville dratt en IR-projeksjon inn i en funksjon hvis hele input er en tabell.
**PREMISSET BLE MÅLT FØR NOE BLE BYGGET PÅ DET, og ordrens to pekere navnga TO ULIKE FORMER:**
`examples/*/expected-bundle/` bærer PIPE-tabeller, men hver eneste av dem er csv-/sql-kilt og
rendret av `render.render_table`**xlsx-stien når ALDRI `render_table`.** Målt (pandoc 3.10.2,
produsentens egen `_PANDOC_WRITER = "markdown"` + `_PANDOC_ARGS = ("--eol=lf", "--wrap=none")`,
mot produsentens egen `tests/fixtures/two-line-krav.xlsx` og et håndlagt Prisskjema):
`extract._extract_office` konverterer, og `inbox.py` gir teksten videre til
`render_inbox_concept(sanitized_text, …)` URØRT — resultatet er en pandoc **SIMPLE table**, der
dash-linja definerer kolonne-spennene. En pipe-only leser ville altså vært INERT på nøyaktig det
korpuset MAJOR-4 finnes for, og en simple-only leser inert på eksemplene ordren pekte på. Begge
leses, av TO skannere over ÉN rollemapping og ÉN tallgrammatikk (`_split_frontmatter`-formen: ett
skann, to lesere), aldri to kopier av semantikken. **Span-slicing, ALDRI splitt på whitespace:**
K2s hele form er tomme priseceller, og en whitespace-splitt kollapser dem og skyver hver senere
verdi én kolonne til venstre — en feilmapping som leses som DATA (M16 → 3 røde).
**Ingen skjønn noe sted:** header-vokabularet er lukket og matches EKSAKT (en delstrengregel ville
latt `Enhetspris eks. mva` og `… inkl. mva` kreve samme rolle, og leseren ville valgt mellom to
priser), tallgrammatikken er lukket og **dot-desimal** (målt output er `1250.0`/`42.5`, så et
komma ville kjøpt ingenting og importert `1,250`s tusenskille-vs-desimal-tvetydighet gratis — et
håndskrevet norsk `1 250,50` er en NEKTET celle, uttalt), og hver tvetydighet er en nekt: ingen
kandidat-tabell, mer enn én, to kolonner om samme rolle, to rader med samme kostkode.
**Delvis priset skjema nekter i SIN HELHET** — ikke forsiktighet: en halvderivert base forankrer
noen koder mens `ProvenanceStamp.cost_baseline_anchored` rapporterer `True`, og den biten er
påkrevd-uten-default nettopp fordi begge defaults ville løyet. Skannet over `context_files`, aldri
`files` (MAJOR-3-regelen). **`CostBaselineDerivationError` subklasser `ValueError`**
(`BundleIdMismatch`-presedensen) — CLI-ens nekt-tuppel og hostings 400-arm, aldri krasj-kanalen.
Wiret bak `--derive-cost-baseline`, ALDRI stille: ÉN oppløsning i `run.py`s bundle-arm tjener både
full kjøring og `live_dry_run`, og **nekten PROPAGERER** i stedet for å degradere til fil-lasteren
(`load_mandate`-regelen — en kaller som ba om derivasjon og fikk en uforankret kjøring er besvart
av en stille nedgradert ordre). Tre CLI-nekter, alle ved NAVN: krever `--bundle-dir`, refusert i
`--portfolio` (ved navn, ikke ved gjennomfall til `--bundle-dir`-kravet — `--explore`-presedensen)
og i `report_forbidden` (report-modus returnerer FØR nekten, så en utelatelse er et stille DROPP,
ikke en nekt — F4-gapet). **FIXTUREN er MÅLT, ikke håndskrevet:** begge basene under
`tests/fixtures/` er pandocs output ORDRETT, og den uprisede er K2s faktiske pre-award-form —
**kolonnene OVERLEVER som blanke** (målt), så tabellen ER en kandidat og nekten blir den skarpe:
leseren identifiserer kolonnene, leser tomme celler og nekter ved navn. `validator-input.json`
skrives KUN i testen, aldri inn i den innsjekkede fixturen (`measure`/`claimed_saving_nok` er
menneskets/mandatets). **`pytest.raises(ValueError)` ville IKKE gatet arm (b), og det er MÅLT, ikke
resonnert:** M17 (drop positivitetsvakten) får mutanten til å reise
`pydantic_core.ValidationError``isinstance(e, ValueError) is True`,
`isinstance(e, CostBaselineDerivationError) is False` — så en løsere arm ville stått grønn mot
nøyaktig den mutasjonen den finnes for. Load-bearing MÅLT
(`tests/test_cost_baseline_derivation_loadbearing.py`, 22 armer), **18 mutasjoner alle røde mot
HELE suiten** + grønn kontroll **1230 passed / 5 skipped** (fra 1208/5, supersett, 0 fjernet) og
golden `demo-transcript.stdout` BYTE-UENDRET (`ea8c534773acdbe41ae68f2c55724d69aaf8be4f`):
M1 detach run-wiringen (2 røde) · M2 fallback ved nekt (1) · M3 hopp over uprisede rader (2) ·
M4 fabrikkér null (1) · M4b fabrikkér null uten tom-celle-nekten (1) · M17 drop
positivitetsvakten (1) · M5 transponer mengde/enhetspris (3) · M6 skann `files` (1) · M7 ta første
tabell ved tvetydighet (1) · M8 ta første kolonne ved duplisert rolle (1) · M9 last-write-wins på
duplisert kode (1) · M10 utvid tallgrammatikken (1) · M11 drop simple-skanneren (3) · M12 drop
pipe-skanneren (7) · M13 detach `--bundle-dir`-nekten (1) · M14 drop fra `report_forbidden` (1) ·
M15 drop fra portefølje-partisjonen (1) · M16 whitespace-splitt (3). **Ærlighets-grenser, uttalt:**
NS 3451 seksjonsrader (kode + overskrift, ingen mengde, inne i et ellers priset ark) er IKKE
klassifisert — K2 selv var ikke tilgjengelig å måle her, så et slikt ark NEKTER, og regelen skrives
når noen kan måle den ekte formen (en rad som er tom over alle tre kolonner er derimot en blank
spacer og hoppes over); stempelet sier AT en kjøring var forankret, aldri HVILKEN av de tre
projeksjonene forankret den; den hostede flaten er BEVISST urørt (feltet er ikke i whitelisten, så
4e-halvdelene står uendret); og ingen LEVENDE K2-fil er lest — fixturen er syntetisk, og at
produsenten faktisk emitterer denne formen for K2s Prisskjema er MÅLT på pandoc-stien, ikke på K2.
- **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

@ -421,7 +421,9 @@ when the seam is detached, so the loop cannot silently degrade into theater.
[Walk the whole chain offline](#walk-the-whole-chain-offline); mutually exclusive with [Walk the whole chain offline](#walk-the-whole-chain-offline); mutually exclusive with
`--live-dry-run`, which stops before the first model call rather than answering it), and `--live-dry-run`, which stops before the first model call rather than answering it), and
`--explore "<prompt>" --explore-config <file>` (opt-in: run an exploration first and let it `--explore "<prompt>" --explore-config <file>` (opt-in: run an exploration first and let it
shape the mandate this run evaluates — see below). shape the mandate this run evaluates — see below), and `--derive-cost-baseline` (opt-in:
anchor the validator on a priced schedule already inside `--bundle-dir` instead of a
hand-written `cost-baseline.json` — see below).
- **Portfolio**`--portfolio`, plus optional `--goals`, `--ledger`, `--dimension-config`, - **Portfolio**`--portfolio`, plus optional `--goals`, `--ledger`, `--dimension-config`,
`--semantic-retrieval`; it stops early and prints a `goal reached: …` line when the `--semantic-retrieval`; it stops early and prints a `goal reached: …` line when the
accumulated ledger meets a goal. accumulated ledger meets a goal.
@ -509,6 +511,30 @@ when the seam is detached, so the loop cannot silently degrade into theater.
`--checkpoint-dir` are refused together (two doors onto one review), as are `--resume` and `--checkpoint-dir` are refused together (two doors onto one review), as are `--resume` and
`--explore` (two sources of one exploration). `--explore` (two sources of one exploration).
- **Deriving the cost baseline from the knowledge base**`--derive-cost-baseline` (opt-in,
requires `--bundle-dir`). The deterministic validator anchors a proposal's `affected_items` to
the project's actual cost lines. Normally those come from a hand-written `cost-baseline.json` in
the bundle, or — on the road reference path — from the project's own `cost_items`. Neither is
available when the bundle was *ingested* from tender documents, so this third route reads the
priced schedule that is already in the base: one markdown table whose header names a cost code, a
quantity and a unit price. Both table forms the producer emits are read — the pipe tables its
csv/sql connectors render, and the pandoc simple tables an `.xlsx` becomes.
**It refuses rather than guesses, and that is the point.** A pre-award schedule has its price
column empty, and completing it with a zero or an invented number would hand the gate a baseline
nobody wrote. So the run stops — by name — when no table names all three columns, when more than
one does, when two columns claim the same role, when two rows share a cost code, or when any row
prices nothing. A partly-priced schedule is refused in full: a half-derived baseline would anchor
some codes while the run still reported itself anchored. The header vocabulary and the number
grammar are both closed (plain dot-decimal numbers; a thousands separator or a comma decimal is
refused, not interpreted).
```bash
uv run python -m portfolio_optimiser.run PROSJEKT-1 --docs-dir <bundle> --bundle-dir <bundle> \
--derive-cost-baseline
```
`--explore` is refused together with `--mandate` — they are two sources of one mandate, and `--explore` is refused together with `--mandate` — they are two sources of one mandate, and
merging would silently overwrite what you wrote. To seed an exploration with a domain expert's merging would silently overwrite what you wrote. To seed an exploration with a domain expert's
own hypotheses, use `explore(..., seed_approaches=[Approach(...)])`; seeds are always preserved own hypotheses, use `explore(..., seed_approaches=[Approach(...)])`; seeds are always preserved

View file

@ -36,10 +36,13 @@ class CostBaseline(BaseModel):
"""A project's cost baseline, keyed by cost code — the ground truth the deterministic """A project's cost baseline, keyed by cost code — the ground truth the deterministic
validator anchors ``affected_items`` to, so the gate cannot be fed hallucinated cost lines. validator anchors ``affected_items`` to, so the gate cannot be fed hallucinated cost lines.
Deliberately a typed IR contract (not a loader-private shape): both sources project INTO Deliberately a typed IR contract (not a loader-private shape): THREE sources project INTO
it an OKF bundle's ``cost-baseline.json`` (``okf.load_cost_baseline``) and the road it an OKF bundle's hand-written ``cost-baseline.json`` (``okf.load_cost_baseline``), the road
reference domain's ``cost_items`` (``validator.baseline_from_project``) — so the validator reference domain's ``cost_items`` (``validator.baseline_from_project``), and a priced schedule
sees ONE representation regardless of path, and the Claude-SDK sibling can mirror it (D7). DERIVED from a table already in the bundle (``okf.derive_cost_baseline``, MAJOR-4) so the
validator sees ONE representation regardless of path, and the Claude-SDK sibling can mirror it
(D7). The third exists because neither of the first two can serve an INGESTED tender corpus:
one is hand-written per project, the other belongs to the road domain.
The projection/tolerance semantics were decided HERE: the commons amendment specifying The projection/tolerance semantics were decided HERE: the commons amendment specifying
``cost-baseline.json`` never arrived, exactly as in S3.2. D7 mirroring stays OPEN. ``cost-baseline.json`` never arrived, exactly as in S3.2. D7 mirroring stays OPEN.

View file

@ -37,7 +37,7 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Final, Literal from typing import Any, Final, Literal
from portfolio_optimiser.ir import CostBaseline from portfolio_optimiser.ir import CostBaseline, CostBaselineLine
from portfolio_optimiser.retrieval import PathSecurityError, safe_resolve from portfolio_optimiser.retrieval import PathSecurityError, safe_resolve
_INDEX_NAME = "index.md" _INDEX_NAME = "index.md"
@ -1113,6 +1113,270 @@ def load_optional_cost_baseline(bundle_dir: str, name: str = _COST_BASELINE) ->
return None return None
class CostBaselineDerivationError(ValueError):
"""A priced schedule could not be read into a ``CostBaseline`` WITHOUT judgement (MAJOR-4).
A ``ValueError`` subclass on purpose, the ``BundleIdMismatch`` precedent: it lands on
``run.main``'s refusal tuple and the hosted surface's 400 arm rather than on the crash channel,
because a schedule this reader cannot map is a caller's input being wrong, not the framework
failing.
Every raise here is VALIDATION, never repair. That is the whole point of the third projection:
K2 is pre-award, so its ``Prisskjema`` carries codes and descriptions with the price column
empty, and the one thing a deriver must never do is answer that with a zero or an invented unit
cost. A baseline is the deterministic gate's ground truth — an invented line would let the gate
reconcile a hallucinated proposal against a hallucinated baseline and report itself anchored."""
#: Header text -> the role it fills, as a CLOSED vocabulary. Matching is on the lower-cased,
#: stripped header cell and is EXACT: a substring rule would let ``Enhetspris eks. mva`` and
#: ``Enhetspris inkl. mva`` both claim the same role and the reader would be choosing between two
#: prices, which is exactly the judgement this function may not exercise. An unrecognised header is
#: simply not a role, and a table missing any role is not a candidate.
_BASELINE_ROLES: Final[dict[str, tuple[str, ...]]] = {
"code": ("postnr", "post", "kode", "kostkode", "code", "cost_code"),
"quantity": ("mengde", "antall", "quantity"),
"unit_cost": ("enhetspris", "unit_cost", "unit price", "unit_price"),
}
#: The number grammar, and it is CLOSED: optional sign, digits, optional dot-decimal. Measured
#: (pandoc 3.10.2, the producer's own writer and arguments) the xlsx path emits ``1250.0`` / ``42.5``
#: — no thousands separators and no comma decimals — so admitting a comma would buy nothing and
#: import ``1,250``'s thousands-versus-decimal ambiguity for free. A hand-authored Norwegian
#: ``1 250,50`` is therefore a REFUSED cell, by name, rather than a guess.
_BASELINE_NUMBER_RE: Final = re.compile(r"-?\d+(?:\.\d+)?")
#: A pandoc SIMPLE table's dash rule: two or more dash groups, whose spans define the columns.
_SIMPLE_RULE_RE: Final = re.compile(r" *-+(?: +-+)+ *")
#: A pipe table's separator row cell, e.g. ``---`` or ``:---:``.
_PIPE_RULE_CELL_RE: Final = re.compile(r":?-+:?")
@dataclass(frozen=True)
class _ScheduleTable:
"""One markdown table found in a concept file: its header cells, its data rows, and the
bundle-relative file it was read from (so a refusal can name the document)."""
file: str
header: tuple[str, ...]
rows: tuple[tuple[str, ...], ...]
def _split_pipe_row(line: str) -> tuple[str, ...]:
"""Split one pipe-table row on UNESCAPED ``|``. ``render.render_table`` escapes ``\\`` first and
then ``|``, so the reader has to undo them in the opposite order or a cell containing a literal
backslash comes back wrong."""
cells, current, escaped = [], [], False
for char in line.strip().strip("|"):
if escaped:
current.append(char)
escaped = False
elif char == "\\":
escaped = True
elif char == "|":
cells.append("".join(current).strip())
current = []
else:
current.append(char)
cells.append("".join(current).strip())
return tuple(cells)
def _pipe_tables(file: str, body: str) -> list[_ScheduleTable]:
"""The csv/sql-sourced form (``render.render_table``): ``| a | b |`` over ``| --- | --- |``."""
tables, lines = [], body.split("\n")
for i in range(len(lines) - 1):
header_line, rule_line = lines[i].strip(), lines[i + 1].strip()
if not header_line.startswith("|") or not rule_line.startswith("|"):
continue
rule = _split_pipe_row(rule_line)
if not rule or not all(_PIPE_RULE_CELL_RE.fullmatch(cell) for cell in rule):
continue
rows = []
for line in lines[i + 2 :]:
if not line.strip().startswith("|"):
break
rows.append(_split_pipe_row(line))
tables.append(
_ScheduleTable(file=file, header=_split_pipe_row(header_line), rows=tuple(rows))
)
return tables
def _simple_tables(file: str, body: str) -> list[_ScheduleTable]:
"""The xlsx form. MEASURED, not assumed: ``extract._extract_office`` converts through pandoc
with ``_PANDOC_WRITER = "markdown"`` and ``_PANDOC_ARGS = ("--eol=lf", "--wrap=none")``, and
``inbox.py`` hands that text to ``render_inbox_concept`` untouched it never reaches
``render_table``. The result is a pandoc SIMPLE table, and its dash rule is the column
definition: each dash group's span slices the same columns out of the header and every row.
Span-slicing rather than splitting on whitespace runs is what makes an EMPTY cell readable. K2's
whole shape is empty price cells, and a whitespace split would collapse them and silently shift
every later value one column left a mis-mapping that reads as data rather than as a defect."""
tables, lines = [], body.split("\n")
for i in range(1, len(lines)):
if not _SIMPLE_RULE_RE.fullmatch(lines[i]) or not lines[i - 1].strip():
continue
spans = [(m.start(), m.end()) for m in re.finditer(r"-+", lines[i])]
rows = []
for line in lines[i + 1 :]:
if not line.strip():
break
rows.append(tuple(line[a:b].strip() for a, b in spans))
tables.append(
_ScheduleTable(
file=file,
header=tuple(lines[i - 1][a:b].strip() for a, b in spans),
rows=tuple(rows),
)
)
return tables
def _role_columns(table: _ScheduleTable) -> dict[str, list[int]] | None:
"""Map each role to the column indices claiming it, or ``None`` when the table is not a
candidate at all (some role is named by no column)."""
found: dict[str, list[int]] = {role: [] for role in _BASELINE_ROLES}
for index, cell in enumerate(table.header):
for role, names in _BASELINE_ROLES.items():
if cell.strip().lower() in names:
found[role].append(index)
return None if any(not columns for columns in found.values()) else found
def _cell(row: tuple[str, ...], index: int) -> str:
"""A row shorter than its header has an EMPTY cell there, never a missing one: a truncated row
is exactly how an unfilled trailing column arrives, and it must reach the same refusal."""
return row[index].strip() if index < len(row) else ""
def _number(raw: str, *, header: str, code: str) -> float:
if not _BASELINE_NUMBER_RE.fullmatch(raw):
raise CostBaselineDerivationError(
f"row {code!r}: column {header!r} holds {raw!r}, which is not a plain decimal number "
"(the grammar is optional sign, digits, optional dot-decimal — a thousands separator "
"or a comma decimal is refused rather than guessed at)"
)
return float(raw)
def derive_cost_baseline(bundle: Bundle, *, project_id: str) -> CostBaseline:
"""Derive a ``CostBaseline`` from a priced schedule the producer already rendered into a concept
file the THIRD projection into the type whose docstring names the other two (MAJOR-4).
Neither existing projection can serve an ingested tender corpus: ``cost-baseline.json`` is
hand-written per project and ``validator.baseline_from_project`` belongs to the road reference
domain. So a K2-shaped bundle could be navigated and never anchored. This reads the numbers that
are already IN the bundle.
**``project_id`` is a REQUIRED keyword rather than something read from the bundle.** The bundle's
``validator-input.json`` carries one, but ``run._project_from_bundle`` already fail-fasts the
run's requested id against it — reading it a second time here would make this a second reader of
a fact that has an owner (-(p)), and would drag an IR projection into a function whose whole
input is a table.
**NO judgement anywhere.** The header vocabulary is closed and matched exactly, the number
grammar is closed, and every ambiguity is a refusal rather than a choice:
* no table in the bundle names all three roles, or more than one does;
* two columns of one table claim the same role;
* two rows carry the same cost code (a dict would last-write-win, and the baseline would then
describe one of two lines the operator can see in the document);
* a row prices nothing empty, unparseable, or a non-positive unit cost.
**A partly-priced schedule refuses in FULL.** Not conservatism: a half-derived baseline anchors
some codes while ``ProvenanceStamp.cost_baseline_anchored`` reports ``True``, and that bit is
required-without-default precisely because both of its defaults would lie.
Scanned over ``context_files``, never ``files`` (MAJOR-3's rule): a ``type: verdict`` file is a
prior judgement, not project cost data, and a reader over ``files`` would let one decide a
project's ground truth outside the gated ExpeL fold.
**Honesty limits, stated.** A row that is empty across all three mapped columns asserts nothing
and is skipped that is a blank spacer, not a price. But NS 3451 section rows (a code and a
heading with no quantity, inside an otherwise priced sheet) are NOT classified, because K2 itself
was not available to measure here; such a sheet refuses, and the rule for it should be written
when someone can measure the real form. And the provenance stamp records THAT a run was
anchored, never WHICH of the three projections anchored it.
Gated by ``tests/test_cost_baseline_derivation_loadbearing.py``."""
tables: list[tuple[_ScheduleTable, dict[str, list[int]]]] = []
for concept in bundle.context_files:
for table in _pipe_tables(concept.name, concept.body) + _simple_tables(
concept.name, concept.body
):
roles = _role_columns(table)
if roles is not None:
tables.append((table, roles))
if not tables:
raise CostBaselineDerivationError(
f"no cost table found in bundle {bundle.dir!r}: no concept file carries a markdown "
f"table whose header names all three of {sorted(_BASELINE_ROLES)} "
f"(recognised headers: {_BASELINE_ROLES})"
)
if len(tables) > 1:
named = ", ".join(sorted({table.file for table, _ in tables}))
raise CostBaselineDerivationError(
f"{len(tables)} cost tables found in bundle {bundle.dir!r} ({named}); which one prices "
"the project is a question about the documents, not one this reader answers by order "
"of appearance"
)
table, roles = tables[0]
for role, columns in roles.items():
if len(columns) > 1:
duplicated = ", ".join(repr(table.header[index]) for index in columns)
raise CostBaselineDerivationError(
f"{table.file}: {len(columns)} columns claim the {role!r} role ({duplicated}); "
"a reader that took the first would be choosing between them"
)
code_at, quantity_at, unit_cost_at = (
roles[role][0] for role in ("code", "quantity", "unit_cost")
)
items: dict[str, CostBaselineLine] = {}
for row in table.rows:
code = _cell(row, code_at)
quantity_raw = _cell(row, quantity_at)
unit_cost_raw = _cell(row, unit_cost_at)
if not code and not quantity_raw and not unit_cost_raw:
continue # a blank spacer row asserts nothing
if not code:
raise CostBaselineDerivationError(
f"{table.file}: a row carries numbers under an empty {table.header[code_at]!r} "
"cell, so the line it prices cannot be named"
)
for raw, index in ((quantity_raw, quantity_at), (unit_cost_raw, unit_cost_at)):
if not raw:
raise CostBaselineDerivationError(
f"{table.file}: row {code!r} has an empty {table.header[index]!r} cell. This "
"schedule is not priced (K2's pre-award shape); a baseline is refused rather "
"than completed with a value nobody wrote"
)
quantity = _number(quantity_raw, header=table.header[quantity_at], code=code)
unit_cost = _number(unit_cost_raw, header=table.header[unit_cost_at], code=code)
if unit_cost <= 0:
raise CostBaselineDerivationError(
f"{table.file}: row {code!r} has {table.header[unit_cost_at]!r} = {unit_cost}, "
"which prices nothing; a baseline line must carry a positive unit cost"
)
if quantity < 0:
raise CostBaselineDerivationError(
f"{table.file}: row {code!r} has {table.header[quantity_at]!r} = {quantity}"
)
if code in items:
raise CostBaselineDerivationError(
f"{table.file}: cost code {code!r} appears twice; keeping either row would make "
"the baseline describe one of two lines the document shows"
)
items[code] = CostBaselineLine(quantity=quantity, unit_cost=unit_cost)
if not items:
raise CostBaselineDerivationError(f"{table.file}: the cost table has no priced rows at all")
return CostBaseline(project_id=project_id, items=items)
def load_ir_projection(bundle_dir: str, name: str = _IR_PROJECTION) -> dict[str, Any]: def load_ir_projection(bundle_dir: str, name: str = _IR_PROJECTION) -> dict[str, Any]:
"""Load the bundle's IR projection (``validator-input.json`` by default): the candidate """Load the bundle's IR projection (``validator-input.json`` by default): the candidate
measure's cost-IR (``measure``, ``affected_items``, ``claimed_saving_nok``) — the measure's cost-IR (``measure``, ``affected_items``, ``claimed_saving_nok``) — the

View file

@ -601,6 +601,11 @@ async def run_project(
docs_dir: str, docs_dir: str,
verdict_input: dict[str, str] | None = None, verdict_input: dict[str, str] | None = None,
bundle_dir: str | None = None, bundle_dir: str | None = None,
#: Derive the validator's cost baseline from a priced schedule IN the bundle
#: (``okf.derive_cost_baseline``) instead of loading a hand-written ``cost-baseline.json``.
#: Bundle path only, and OPT-IN by construction: the default leaves every existing run on the
#: file loader, byte-identically.
derive_cost_baseline: bool = False,
dimension: Dimension | None = None, dimension: Dimension | None = None,
store: VerdictStore | None = None, store: VerdictStore | None = None,
verdict_dir: str | None = None, verdict_dir: str | None = None,
@ -691,7 +696,17 @@ async def run_project(
okf.reconcile_bundle_id(bundle_dir) okf.reconcile_bundle_id(bundle_dir)
bundle = okf.navigate_bundle(bundle_dir) bundle = okf.navigate_bundle(bundle_dir)
project = _project_from_bundle(bundle_dir, project_id, bundle=bundle) project = _project_from_bundle(bundle_dir, project_id, bundle=bundle)
baseline = okf.load_optional_cost_baseline(bundle_dir) # The THIRD projection into ``CostBaseline`` (MAJOR-4), behind an EXPLICIT commission and
# never silent. The refusal PROPAGATES rather than degrading to the file loader: a caller
# who asked for derivation and got an un-anchored run instead would have been answered by a
# silently downgraded order, which is what ``load_mandate`` fail-fasts against. This one
# resolution serves BOTH the full run and the ``live_dry_run`` report below, so the dry-run
# arm cannot drift away from what a real run would anchor on.
baseline = (
okf.derive_cost_baseline(bundle, project_id=project_id)
if derive_cost_baseline
else okf.load_optional_cost_baseline(bundle_dir)
)
# §4.1a context-scope: agents read ONLY dimension-scoped bundle knowledge (Step-3 filter); # §4.1a context-scope: agents read ONLY dimension-scoped bundle knowledge (Step-3 filter);
# dimension=None keeps the full context, byte-identical to before. # dimension=None keeps the full context, byte-identical to before.
context = okf.bundle_context(bundle, dimension=dimension.id if dimension else None) context = okf.bundle_context(bundle, dimension=dimension.id if dimension else None)
@ -1956,6 +1971,15 @@ def main(argv: list[str] | None = None) -> int:
action="store_true", action="store_true",
help="offline drill: build contracts/clients/budget, STOP before the first model call", help="offline drill: build contracts/clients/budget, STOP before the first model call",
) )
parser.add_argument(
"--derive-cost-baseline",
action="store_true",
help=(
"derive the validator's cost baseline from a priced schedule inside --bundle-dir "
"instead of loading a hand-written cost-baseline.json (MAJOR-4). Refuses rather than "
"guesses: an unpriced or ambiguous schedule stops the run"
),
)
parser.add_argument( parser.add_argument(
"--scripted-replies", "--scripted-replies",
default=None, default=None,
@ -2033,6 +2057,9 @@ def main(argv: list[str] | None = None) -> int:
report_forbidden = { report_forbidden = {
"--portfolio": args.portfolio, "--portfolio": args.portfolio,
"--live-dry-run": args.live_dry_run, "--live-dry-run": args.live_dry_run,
# Report mode returns before the run dispatch, so an omission here is a SILENT DROP,
# not a refusal — the gap F4 measured on --plan-review.
"--derive-cost-baseline": args.derive_cost_baseline,
"PROJECT_ID": args.project_id is not None, "PROJECT_ID": args.project_id is not None,
"--goals": args.goals is not None, "--goals": args.goals is not None,
"--docs-dir": args.docs_dir is not None, "--docs-dir": args.docs_dir is not None,
@ -2101,6 +2128,11 @@ def main(argv: list[str] | None = None) -> int:
"--outbox-dir": args.outbox_dir, "--outbox-dir": args.outbox_dir,
"--run-id": args.run_id, "--run-id": args.run_id,
"--live-dry-run": args.live_dry_run, "--live-dry-run": args.live_dry_run,
# BY NAME, not by falling through to "--derive-cost-baseline requires --bundle-dir":
# --bundle-dir is already single-project-only, so that message would tell an operator
# who wrote --portfolio --derive-cost-baseline to add the one flag this mode also
# refuses. Same reason --explore is listed here rather than left to fall through.
"--derive-cost-baseline": args.derive_cost_baseline,
# One exploration shapes ONE mandate against ONE knowledge base, and --bundle-dir (its # One exploration shapes ONE mandate against ONE knowledge base, and --bundle-dir (its
# only source of bases here) is already single-project-only. Refusing it by NAME beats # only source of bases here) is already single-project-only. Refusing it by NAME beats
# letting it fall through to the --bundle-dir requirement below: an operator who wrote # letting it fall through to the --bundle-dir requirement below: an operator who wrote
@ -2149,6 +2181,19 @@ def main(argv: list[str] | None = None) -> int:
) )
return 1 return 1
# The third projection reads a table INSIDE a bundle, so without one there is nothing to derive
# from: the road path's baseline comes from ``Project.cost_items`` and is anchored by
# construction. Refused by NAME here rather than left to surface later as a project-lookup
# failure, which names neither the flag nor what it needs.
if not args.portfolio and args.derive_cost_baseline and args.bundle_dir is None:
print(
"run refused: --derive-cost-baseline requires --bundle-dir (the schedule it derives "
"from is a concept file in the knowledge base; the road path is already anchored by "
"its own cost_items)",
file=sys.stderr,
)
return 1
# --semantic-retrieval is refused, never silently ignored (the repo's flag contract). In # --semantic-retrieval is refused, never silently ignored (the repo's flag contract). In
# single-project mode it can only do observable work with BOTH of these: the Step-1 fold is # single-project mode it can only do observable work with BOTH of these: the Step-1 fold is
# gated on ``bundle_dir``, and ``--verdict-dir`` is the only route by which ``main()`` can hand # gated on ``bundle_dir``, and ``--verdict-dir`` is the only route by which ``main()`` can hand
@ -2761,6 +2806,7 @@ def main(argv: list[str] | None = None) -> int:
outbox_dir=args.outbox_dir, outbox_dir=args.outbox_dir,
run_id=args.run_id, run_id=args.run_id,
verdict_input=_verdict_input_from_args(args), verdict_input=_verdict_input_from_args(args),
derive_cost_baseline=args.derive_cost_baseline,
mcp_servers=mcp_servers, mcp_servers=mcp_servers,
live_dry_run=True, live_dry_run=True,
) )
@ -2820,6 +2866,7 @@ def main(argv: list[str] | None = None) -> int:
run_id=args.run_id, run_id=args.run_id,
verdict_input=_verdict_input_from_args(args), verdict_input=_verdict_input_from_args(args),
semantic_retrieval=args.semantic_retrieval, semantic_retrieval=args.semantic_retrieval,
derive_cost_baseline=args.derive_cost_baseline,
client_factory=scripted_client_factory, client_factory=scripted_client_factory,
mandate=mandate, mandate=mandate,
mcp_servers=mcp_servers, mcp_servers=mcp_servers,

31
tests/fixtures/README.md vendored Normal file
View file

@ -0,0 +1,31 @@
# Test fixtures
## `k2-prisskjema-SYNTETISK/` and `k2-prisskjema-uprisert-SYNTETISK/`
**SYNTETISK — every number in these bundles is invented.** They copy the *structure* of a
public-tender `Prisskjema.xlsx` (NS 3451 posts: code, description, unit, quantity, unit price,
sum) and nothing else. **No private tender document is reproduced here**, and the unit prices in
the priced variant are round synthetic values chosen to be obviously not a real bid — they must
never be read as anyone's pricing. This is the first building block of the S7b corpus.
The two variants are the two halves of the same measurement:
| bundle | `Enhetspris` column | what it stands for |
| --- | --- | --- |
| `k2-prisskjema-SYNTETISK/` | filled | a priced schedule a baseline CAN be derived from |
| `k2-prisskjema-uprisert-SYNTETISK/` | empty | K2's actual pre-award state — codes and descriptions, no quantities or prices |
### The table form is MEASURED, not hand-typed
Both bodies are the **verbatim output** of the producer's own office conversion, so the fixture
states something about the format rather than recording our own idea of it:
pandoc -f xlsx -t markdown --eol=lf --wrap=none <sheet>.xlsx
with `pandoc 3.10.2` — the writer and the two arguments are `llm_ingestion_okf.extract`'s
`_PANDOC_WRITER` / `_PANDOC_ARGS`. That is a pandoc **simple table** (a dash rule whose groups
define the column spans), NOT the pipe table `render_table` emits: measured in session 78, the
xlsx path reaches a concept body through `inbox.py`'s `render_inbox_concept(sanitized_text, …)`
and never touches `render_table` at all. An empty spreadsheet cell therefore arrives as blank
space inside its column span — which is exactly what the unpriced variant has to carry for the
refusal it gates to be the real one.

View file

@ -0,0 +1,8 @@
---
type: index
title: K2 Prisskjema (SYNTETISK, priset)
---
# K2 Prisskjema (SYNTETISK)
Syntetisk priset prisskjema. Se [prisskjema-SYNTETISK.md](prisskjema-SYNTETISK.md).

View file

@ -0,0 +1,12 @@
---
type: dataset
title: Prisskjema (SYNTETISK, priset)
---
## Prisskjema {#sheet-1}
Postnr Beskrivelse Enhet Mengde Enhetspris Sum
-------- ---------------------------------------------------- ------- --------- ------------ -----------
21.1 Grunnarbeider, utgraving og bortkjoering av masser m3 1250.0 850.0 1062500.0
24.2 Baerekonstruksjon i staal, levert og montert kg 48000.0 42.5 2040000.0
36.1 Ventilasjonsaggregat med varmegjenvinning stk 4.0 187500.0 750000.0

View file

@ -0,0 +1,9 @@
---
type: index
title: K2 Prisskjema (SYNTETISK, upriset)
---
# K2 Prisskjema (SYNTETISK, upriset)
Syntetisk gjengivelse av K2s pre-award-form. Se
[prisskjema-uprisert-SYNTETISK.md](prisskjema-uprisert-SYNTETISK.md).

View file

@ -0,0 +1,12 @@
---
type: dataset
title: Prisskjema (SYNTETISK, upriset)
---
## Prisskjema {#sheet-1}
Postnr Beskrivelse Enhet Mengde Enhetspris Sum
-------- ---------------------------------------------------- ------- -------- ------------ -----
21.1 Grunnarbeider, utgraving og bortkjoering av masser m3
24.2 Baerekonstruksjon i staal, levert og montert kg
36.1 Ventilasjonsaggregat med varmegjenvinning stk

View file

@ -0,0 +1,386 @@
"""``okf.derive_cost_baseline`` — the THIRD projection into ``ir.CostBaseline``, and the one that
must refuse rather than invent (MAJOR-4, misjonsreview v2 §7; owner B = the consumer, PM decision
2026-09-02).
``ir.CostBaseline``'s docstring names two projections INTO it — a bundle's hand-written
``cost-baseline.json`` (``okf.load_cost_baseline``) and the road reference domain's ``cost_items``
(``validator.baseline_from_project``). Neither can be produced from an ingested tender corpus, so
K2 could be navigated but never RUN: ``run_project``'s bundle arm resolves an OPTIONAL baseline and
a bundle carrying only ingested documents is simply un-anchored. This adds the third: derive the
baseline from a priced schedule the producer already rendered into a concept file.
**The premise was measured before anything was built on it, and the order's two pointers named two
DIFFERENT forms.** The order says "markdown-tabell rendret av produsenten fra xlsx via pandoc" and
also "les examples/ i llm-ingestion-okf for formen":
* ``examples/*/expected-bundle/`` carry **pipe** tables but every one of them is csv- or
sql-sourced, rendered by ``render.render_table``.
* The **xlsx** path never reaches ``render_table``. ``extract._extract_office`` converts through
``pypandoc`` with ``_PANDOC_WRITER = "markdown"`` and ``_PANDOC_ARGS = ("--eol=lf",
"--wrap=none")``, and ``inbox.py`` hands that text straight to
``render_inbox_concept(decision.sanitized_text, )``. Measured with pandoc 3.10.2 against the
producer's own ``tests/fixtures/two-line-krav.xlsx`` and against a hand-laid Prisskjema: the
result is a pandoc **simple table**, whose dash rule defines the column spans.
So a pipe-only reader would have been INERT on exactly the corpus MAJOR-4 exists for, and a
simple-only reader would be inert on the examples the order pointed at. Both forms are read, by two
scanners over ONE role mapping and ONE number grammar the ``_split_frontmatter`` shape (one
scan, two readers), never two copies of the semantics.
**Arm (b) is the point of the whole thing.** K2 is pre-award: measured by the PM agent on
2026-09-02, ``Prisskjema.xlsx`` has 0/28 unit-price cells filled and the NS 3451 posts 0192 carry
codes and descriptions with no quantities or prices. The columns still exist measured here: an
empty spreadsheet cell arrives as blank space inside its span, so the header row still names
``Mengde`` and ``Enhetspris`` and the table IS a candidate. That is what makes the refusal sharp:
the deriver identifies the columns, reads empty cells, and must refuse BY NAME rather than emit a
``CostBaseline`` with an invented or zero ``unit_cost``.
``pytest.raises(ValueError)`` would NOT have gated that. ``pydantic.ValidationError`` subclasses
``ValueError`` and ``CostBaselineLine.unit_cost`` is ``Field(gt=0)``, so a deriver that fabricated
``0.0`` would raise a ``ValueError`` too and the arm would stay green against the single mutation
it exists to catch. Every refusal arm asserts the NAMED class and the token that distinguishes it.
The fixtures' numbers are chosen so a role-swap is visible: no row has ``quantity ==
unit_cost`` and the two magnitudes differ per row (-(y)'s ``observed != limit`` rule — a fixture
where they coincide cannot tell a correct mapping from a transposed one).
Gates ``okf.derive_cost_baseline`` + ``run.run_project(derive_cost_baseline=)`` + the CLI door.
"""
from __future__ import annotations
import json
import shutil
from pathlib import Path
import pytest
from portfolio_optimiser import okf, run
from portfolio_optimiser.ir import CostBaseline
_FIXTURES = Path(__file__).parent / "fixtures"
_PRICED = str(_FIXTURES / "k2-prisskjema-SYNTETISK")
_UNPRICED = str(_FIXTURES / "k2-prisskjema-uprisert-SYNTETISK")
#: What the priced fixture's table says, transcribed from the fixture rather than from the
#: spreadsheet it was rendered from: arm (a) asserts the derivation equals the TABLE.
_PRICED_ROWS = {
"21.1": (1250.0, 850.0),
"24.2": (48000.0, 42.5),
"36.1": (4.0, 187500.0),
}
def _bundle(path: str) -> okf.Bundle:
return okf.navigate_bundle(path)
def _write_bundle(root: Path, body: str, *, name: str = "table.md") -> str:
root.mkdir(parents=True, exist_ok=True)
(root / "index.md").write_text(
f"---\ntype: index\ntitle: T\n---\n\n[t]({name})\n", encoding="utf-8"
)
(root / name).write_text(f"---\ntype: dataset\ntitle: T\n---\n\n{body}", encoding="utf-8")
return str(root)
# --------------------------------------------------------------------------------------------
# ARM (b) FIRST — the K2 form must refuse, typed and by name.
# --------------------------------------------------------------------------------------------
def test_b_unpriced_k2_form_refuses_by_name() -> None:
"""Codes and descriptions, EMPTY quantity and price columns: the refusal, not a baseline."""
with pytest.raises(okf.CostBaselineDerivationError) as excinfo:
okf.derive_cost_baseline(_bundle(_UNPRICED), project_id="K2")
message = str(excinfo.value)
assert "empty" in message.lower()
# Names the row AND the column: "which post" and "which cell" are two operative questions.
assert "21.1" in message
assert "Enhetspris" in message or "Mengde" in message
def test_b_the_refusal_is_the_named_class_not_a_bare_value_error() -> None:
"""``pytest.raises(ValueError)`` cannot gate this arm: ``pydantic.ValidationError`` IS a
``ValueError``, so a deriver fabricating ``unit_cost=0.0`` would satisfy it. The named class is
what separates 'we refused' from 'pydantic caught our invention on the way out'."""
assert issubclass(okf.CostBaselineDerivationError, ValueError)
with pytest.raises(okf.CostBaselineDerivationError):
okf.derive_cost_baseline(_bundle(_UNPRICED), project_id="K2")
# And the refusal is OURS, raised before any CostBaseline is constructed.
try:
okf.derive_cost_baseline(_bundle(_UNPRICED), project_id="K2")
except okf.CostBaselineDerivationError as exc:
assert type(exc).__name__ == "CostBaselineDerivationError"
def test_b_a_zero_unit_cost_is_refused_by_name_never_carried(tmp_path: Path) -> None:
"""A cell that PARSES but prices nothing. ``CostBaselineLine`` would reject it anyway — the
point is that the refusal names the column instead of surfacing as a schema error about a
number we chose to pass on."""
body = "| Postnr | Mengde | Enhetspris |\n| --- | --- | --- |\n| 21.1 | 1250 | 0 |\n"
with pytest.raises(okf.CostBaselineDerivationError, match="Enhetspris"):
okf.derive_cost_baseline(_bundle(_write_bundle(tmp_path / "zero", body)), project_id="P")
def test_b_partial_pricing_refuses_the_WHOLE_derivation(tmp_path: Path) -> None:
"""One priced row and one unpriced row is not "a baseline with one line". A half-derived
baseline anchors some codes while ``ProvenanceStamp.cost_baseline_anchored`` reports ``True``
a false positive on the one bit made REQUIRED-without-default precisely because both of its
defaults would lie."""
body = (
"| Postnr | Mengde | Enhetspris |\n"
"| --- | --- | --- |\n"
"| 21.1 | 1250 | 850 |\n"
"| 24.2 | 48000 | |\n"
)
with pytest.raises(okf.CostBaselineDerivationError, match="24.2"):
okf.derive_cost_baseline(_bundle(_write_bundle(tmp_path / "half", body)), project_id="P")
# --------------------------------------------------------------------------------------------
# ARM (a) — a priced table derives, and derives what the table SAYS.
# --------------------------------------------------------------------------------------------
def test_a_priced_pandoc_table_derives_the_table_verbatim() -> None:
baseline = okf.derive_cost_baseline(_bundle(_PRICED), project_id="K2")
assert isinstance(baseline, CostBaseline)
assert baseline.project_id == "K2"
assert set(baseline.items) == set(_PRICED_ROWS)
for code, (quantity, unit_cost) in _PRICED_ROWS.items():
assert baseline.items[code].quantity == quantity, code
assert baseline.items[code].unit_cost == unit_cost, code
def test_a_no_row_has_quantity_equal_to_unit_cost() -> None:
"""The fixture's own discriminating property, asserted rather than assumed: if any row had
``quantity == unit_cost``, the arm above would pass against a deriver that transposed the two
columns, and the fixture would be gating nothing."""
for code, (quantity, unit_cost) in _PRICED_ROWS.items():
assert quantity != unit_cost, code
def test_a_the_pipe_table_form_derives_identically(tmp_path: Path) -> None:
"""The csv/sql-sourced form (``render.render_table``) reads through the SAME role mapping."""
body = (
"| Postnr | Beskrivelse | Mengde | Enhetspris |\n"
"| --- | --- | --- | --- |\n"
"| 21.1 | Grunnarbeider | 1250 | 850 |\n"
"| 24.2 | Staal | 48000 | 42.5 |\n"
)
baseline = okf.derive_cost_baseline(
_bundle(_write_bundle(tmp_path / "pipe", body)), project_id="P"
)
assert baseline.items["21.1"].quantity == 1250.0
assert baseline.items["21.1"].unit_cost == 850.0
assert baseline.items["24.2"].unit_cost == 42.5
# --------------------------------------------------------------------------------------------
# Determinism: no judgement. Where the columns cannot be identified UNAMBIGUOUSLY, refuse.
# --------------------------------------------------------------------------------------------
def test_no_identifiable_table_refuses(tmp_path: Path) -> None:
body = "| Fritekst | Notat |\n| --- | --- |\n| a | b |\n"
with pytest.raises(okf.CostBaselineDerivationError, match="no"):
okf.derive_cost_baseline(_bundle(_write_bundle(tmp_path / "none", body)), project_id="P")
def test_two_candidate_tables_refuse_rather_than_pick(tmp_path: Path) -> None:
"""Two priced schedules in one bundle is a DATA question, never one the reader answers by
order of appearance (``scripted_proposer``'s ambiguity rule)."""
root = tmp_path / "two"
body = "| Postnr | Mengde | Enhetspris |\n| --- | --- | --- |\n| 21.1 | 1250 | 850 |\n"
_write_bundle(root, body, name="a.md")
(root / "b.md").write_text(
f"---\ntype: dataset\ntitle: B\n---\n\n{body}",
encoding="utf-8",
)
(root / "index.md").write_text(
"---\ntype: index\ntitle: T\n---\n\n[a](a.md)\n[b](b.md)\n", encoding="utf-8"
)
with pytest.raises(okf.CostBaselineDerivationError) as excinfo:
okf.derive_cost_baseline(_bundle(str(root)), project_id="P")
assert "a.md" in str(excinfo.value) and "b.md" in str(excinfo.value)
def test_a_duplicated_role_in_one_header_refuses(tmp_path: Path) -> None:
"""Two columns claiming the same role: a reader that took the first would be choosing."""
body = (
"| Postnr | Mengde | Enhetspris | Enhetspris |\n"
"| --- | --- | --- | --- |\n"
"| 21.1 | 1250 | 850 | 900 |\n"
)
with pytest.raises(okf.CostBaselineDerivationError, match="Enhetspris"):
okf.derive_cost_baseline(_bundle(_write_bundle(tmp_path / "dup", body)), project_id="P")
def test_a_duplicated_cost_code_refuses(tmp_path: Path) -> None:
"""A dict would silently last-write-win, and the baseline would quietly describe one of two
rows an operator can see in the document."""
body = (
"| Postnr | Mengde | Enhetspris |\n"
"| --- | --- | --- |\n"
"| 21.1 | 1250 | 850 |\n"
"| 21.1 | 20 | 30 |\n"
)
with pytest.raises(okf.CostBaselineDerivationError, match="21.1"):
okf.derive_cost_baseline(_bundle(_write_bundle(tmp_path / "dupcode", body)), project_id="P")
def test_a_number_outside_the_grammar_refuses(tmp_path: Path) -> None:
"""The grammar is dot-decimal, and it is closed. The measured pandoc output emits ``1250.0`` /
``42.5``, so no comma handling is needed and admitting one would import ``1,250``'s
thousands-versus-decimal ambiguity for free, which is exactly the judgement this function is
forbidden to exercise. A hand-authored Norwegian ``1 250,50`` is a REFUSED case, stated."""
for cell in ("1 250", "850,50", "850 kr", "n/a"):
body = f"| Postnr | Mengde | Enhetspris |\n| --- | --- | --- |\n| 21.1 | 1250 | {cell} |\n"
root = _write_bundle(tmp_path / f"num-{abs(hash(cell))}", body)
with pytest.raises(okf.CostBaselineDerivationError, match="Enhetspris"):
okf.derive_cost_baseline(_bundle(root), project_id="P")
def test_the_verdict_layer_is_not_scanned(tmp_path: Path) -> None:
"""Built from ``context_files``, never ``files`` (MAJOR-3's rule): a ``type: verdict`` file is
not project cost data, and a reader over ``files`` would let a prior verdict's table decide a
project's ground truth."""
root = tmp_path / "verdict"
root.mkdir()
(root / "index.md").write_text(
"---\ntype: index\ntitle: T\n---\n\n[v](v.md)\n", encoding="utf-8"
)
(root / "v.md").write_text(
"---\ntype: verdict\ntitle: V\n---\n\n"
"| Postnr | Mengde | Enhetspris |\n| --- | --- | --- |\n| 21.1 | 1250 | 850 |\n",
encoding="utf-8",
)
with pytest.raises(okf.CostBaselineDerivationError):
okf.derive_cost_baseline(_bundle(str(root)), project_id="P")
# --------------------------------------------------------------------------------------------
# CONTROL — the two existing projections are untouched, and the file remains PREFERRED.
# --------------------------------------------------------------------------------------------
def test_control_a_bundle_shipping_a_handwritten_baseline_is_untouched() -> None:
"""``load_optional_cost_baseline`` still answers, unchanged, and remains what the run path
reaches for when the flag is absent."""
shipped = okf.load_optional_cost_baseline("shared/examples/tunnel-hauglia")
assert shipped is not None
assert shipped.project_id == "TUNNEL-HAUGLIA"
assert "ENERGI-TUNNEL-EL" in shipped.items
def test_control_the_fixture_bundles_ship_no_cost_baseline_json() -> None:
"""Otherwise arm (a) could be satisfied by the file loader and prove nothing about derivation."""
for path in (_PRICED, _UNPRICED):
assert not (Path(path) / "cost-baseline.json").exists()
assert okf.load_optional_cost_baseline(path) is None
def test_control_the_fixtures_are_marked_synthetic() -> None:
readme = (_FIXTURES / "README.md").read_text(encoding="utf-8")
assert "SYNTETISK" in readme
assert "SYNTETISK" in Path(_PRICED).name
assert "SYNTETISK" in Path(_UNPRICED).name
# --------------------------------------------------------------------------------------------
# WIRING — the third projection is reachable only behind the explicit flag, never silently.
# --------------------------------------------------------------------------------------------
#: The IR projection ``_project_from_bundle`` fail-fasts on. Written HERE, in the test, and never
#: into the checked-in fixture: ``measure`` and ``claimed_saving_nok`` are the human's or the
#: mandate's to state, so a SYNTETISK K2 bundle carrying one would look like a runnable tender
#: whose candidate measure somebody authored. The derivation reads the priced TABLE; this file only
#: satisfies the pre-existing bundle contract so the wiring can be exercised at all.
_IR_PROJECTION = {
"project_id": "K2",
"measure": "PLACEHOLDER — authored by this test purely to satisfy the bundle contract",
"affected_items": [{"code": "21.1", "quantity": 1250, "unit_cost": 850.0}],
"claimed_saving_nok": 1000.0,
}
def _runnable(source: str, tmp_path: Path) -> str:
"""A copy of a fixture bundle plus the hand-authored IR projection the bundle arm requires."""
root = tmp_path / "runnable"
shutil.copytree(source, root)
(root / "validator-input.json").write_text(json.dumps(_IR_PROJECTION), encoding="utf-8")
return str(root)
async def test_run_project_uses_the_derived_baseline_when_the_flag_is_given(tmp_path: Path) -> None:
bundle_dir = _runnable(_PRICED, tmp_path)
report = await run.run_project(
"K2",
"local",
docs_dir=bundle_dir,
bundle_dir=bundle_dir,
derive_cost_baseline=True,
live_dry_run=True,
)
assert isinstance(report, run.DryRunReport)
assert report.cost_baseline_anchored is True
async def test_run_project_without_the_flag_leaves_the_fixture_unanchored(tmp_path: Path) -> None:
"""The default is the unchanged path: no ``cost-baseline.json``, so no anchoring — and the
derivation is NOT reached. Silence never derives."""
bundle_dir = _runnable(_PRICED, tmp_path)
report = await run.run_project(
"K2", "local", docs_dir=bundle_dir, bundle_dir=bundle_dir, live_dry_run=True
)
assert isinstance(report, run.DryRunReport)
assert report.cost_baseline_anchored is False
async def test_run_project_propagates_the_refusal_rather_than_falling_back(tmp_path: Path) -> None:
"""The flag is an explicit commission. A derivation that fails must not quietly degrade to the
(absent) file and report an un-anchored run: a run that goes ahead on a silently degraded
order is the defect ``load_mandate`` fail-fasts against."""
bundle_dir = _runnable(_UNPRICED, tmp_path)
with pytest.raises(okf.CostBaselineDerivationError):
await run.run_project(
"K2",
"local",
docs_dir=bundle_dir,
bundle_dir=bundle_dir,
derive_cost_baseline=True,
live_dry_run=True,
)
def test_cli_derive_requires_bundle_dir(capsys: pytest.CaptureFixture[str]) -> None:
rc = run.main(["P1", "--docs-dir", "docs", "--derive-cost-baseline"])
assert rc == 1
err = capsys.readouterr().err
# Keyed on the DISTINGUISHING token: every refusal below also contains the flag name.
assert "--bundle-dir" in err
def test_cli_derive_is_refused_in_portfolio_mode(capsys: pytest.CaptureFixture[str]) -> None:
rc = run.main(["--portfolio", "--derive-cost-baseline"])
assert rc == 1
err = capsys.readouterr().err
assert "--portfolio" in err
assert "single-project mode" in err
def test_cli_derive_is_refused_in_report_mode(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Report mode returns BEFORE every downstream refusal, so a flag missing from
``report_forbidden`` is silently DROPPED rather than refused (F4 found exactly that gap). The
argv is one report mode would otherwise ACCEPT, so rc 1 is the mutant's opposite outcome."""
ledger = tmp_path / "ledger.json"
ledger.write_text(json.dumps([]), encoding="utf-8")
assert run.main(["--report", "--ledger", str(ledger)]) == 0
capsys.readouterr()
rc = run.main(["--report", "--ledger", str(ledger), "--derive-cost-baseline"])
assert rc == 1
assert "mode-exclusive" in capsys.readouterr().err