portfolio-optimiser/src/portfolio_optimiser/okf.py
Kjell Tore Guttormsen baae7507a9 feat(okf): den erklaerte bundle_id-en er identiteten - mountet er en filsystem-tilfeldighet
S7a-3 pkt. 1. Til i dag NEKTET reconcile_bundle_id en base som erklaerte en id
katalogen ikke bar. Maalt mot K2 - den foerste leverte basen som erklaerer sin
egen id (618 av 630 konseptfiler + rot-index, alle "k2-trinn1-20260903", levert
som "K2-bundle-20260903") - betydde det at basen ikke kunne aapnes slik den var
levert, og at eneste botemiddel var aa montere den paa nytt for haand, en gang
per leveranse. PM-beslutning: konsumenten slakker.

- Erklaert vinner (B1s rekkefoelge uroert), avviket REGISTRERES:
  ResolvedBundleId.mount + ProvenanceStamp.bundle_id_source +
  DryRunReport.bundle_id_source + run.bundle_id_notice (None ved enighet).
  Stempel-feltet er PAAKREVD uten default: None er en VERDI (veg-stien).
- Det som fortsatt nekter er den EKTE kollisjonen: to KONSEPTER i en base som
  erklaerer ULIKE id-er (okf.assert_declared_ids_agree, kalt ved hver doer som
  aapner en base). Rot-index er IKKE med i enighets-settet - konsept-slaar-index
  er en presedens-regel, saa en index i utakt er fallbacken som taper.
- KONSEKVENS, ikke scope-krype: explore._bundle_index loeser naa den erklaerte
  id-en. Den brukte Path(raw).name mens dispatcheren brukte reconcile...id; med
  erklaert-vinner ville explore() myntet approaches som navngir MOUNTET mens
  dispatcheren ruter paa ERKLAERINGEN - en utforskning med uruterbart mandat.

Load-bearing MAALT: 10 mutasjoner alle roede mot HELE suiten, groenn kontroll
1243 passed / 5 skipped og golden demo-transcript.stdout byte-uendret
(shasum -a 1 = ea8c534773acdbe41ae68f2c55724d69aaf8be4f).
M1 1 / M2 1 / M3 1 / M4 9 / M5 2 / M6 1 / M7 1 / M8 2 / M9 2 / M11 1.

Tre armer i test_bundle_id_reconciliation_loadbearing er SKREVET OM (ikke
slettet) - de pinnet nekten beslutningen fjernet. (j) ble skarpere enn den den
erstattet: erklaert id ruter, mountet nektes.

Kontrakt: docs/okf-konsum-kontrakter.md § 3.1. Invariantrad i CLAUDE.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 06:47:58 +02:00

1444 lines
75 KiB
Python

"""OKF (Open Knowledge Format) bundle navigation — framework-neutral, D7-portable context seam.
Reads a bundle the way OKF intends (progressive disclosure): start at ``index.md``, follow
intra-bundle cross-links **recursively, depth-first in first-seen link order**, parse each file's
YAML frontmatter, classify by the one required ``type`` field. **NO** ``agent_framework``, **NO**
``mcp`` — stdlib + ``pydantic`` only (as ``dimension.py``; the typed contracts this module loads
live in ``ir.py``), so the SAME navigation serves both the MAF and the Claude-SDK
implementations unchanged (målbilde §4 vendor-neutrality).
Link resolution follows ``shared/method-spec.md`` §3 Step 1: a leading ``/`` denotes the **bundle
root** (NEVER a filesystem-absolute path), any other form is relative to the LINKING FILE's own
directory — so a bundle may be a hierarchy. It is **escape, not depth**, that is forbidden; this
resolve-and-boundary-check replaced the old "a path separator means out-of-bundle" heuristic, which
conflated the two and forbade valid nesting. Repeated links de-duplicate on the RESOLVED path, so
``./a.md`` and ``a.md`` are one entry and cycles terminate.
Robustness is part of the spec (OKF SPEC §4): consumers MUST tolerate broken links and unknown
fields. A target that fails to resolve for ANY reason (missing file, invalid path component,
escape) is silently skipped, never raised. Path-safety reuses ``retrieval.safe_resolve`` (also pure
stdlib): each cross-link is canonicalised and boundary-checked against the bundle dir, fail-closed
— the SOLE in-/out-of-bundle test.
Skipped is not SILENT, though: every link the walk could not follow is recorded on
``Bundle.skipped`` as a ``SkippedLink`` (which file it was written in, the link text verbatim, and
which of the two reasons applied). The tolerance is unchanged — nothing raises — but a bundle whose
other half was never reached is no longer indistinguishable from one where those documents were
never written.
"""
from __future__ import annotations
import itertools
import json
import posixpath
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Final, Literal
from portfolio_optimiser.ir import CostBaseline, CostBaselineLine
from portfolio_optimiser.retrieval import PathSecurityError, safe_resolve
_INDEX_NAME = "index.md"
_IR_PROJECTION = "validator-input.json"
_COST_BASELINE = "cost-baseline.json"
# Intra-bundle markdown cross-links: ``](target.md)``. A path separator is NOT a rejection reason —
# ``_resolve_target`` decides in-/out-of-bundle, and only escape is refused (method-spec §3 Step 1).
_LINK_RE = re.compile(r"\]\(([^)]+\.md)\)")
def unquote_scalar(raw: str) -> str:
"""The ONE unquoting rule for a frontmatter scalar — ``parse_frontmatter`` preserves quotes
(OKF SPEC §4), so every consumer of a scalar has to take them off, and they must all take them
off the SAME way.
It lives here because this module owns ``parse_frontmatter``. It was a private copy in
``verdicts`` (structural key) while ``bundle_context`` stripped only ``"`` (rendered title) —
two rules, and the weaker one in the path that becomes the agent's read-context. Both quote
styles, and whitespace on either side of them, are ordinary YAML a hand-authoring curator
writes. Mirrors the (p) precedent: a duplicated conversion drifts, and the drifted copy decides
something. Gated by ``tests/test_frontmatter_unquote_loadbearing.py``."""
return raw.strip().strip('"').strip("'").strip()
def _split_frontmatter(text: str) -> tuple[list[str], str, bool]:
"""Scan the leading ``---``-delimited block ONCE: ``(frontmatter lines, body, terminated)``.
This is the module's ONLY place the delimiter is compared against. ``parse_frontmatter`` and
``_read_body`` each had their own loop over the same delimiter, which is the kø-(p) shape — and
here the two copies had already drifted, measured: given an opening ``---`` with no closing one,
``parse_frontmatter`` consumed every remaining line as frontmatter while ``_read_body`` fell
through and returned the WHOLE file, delimiter line included.
**That divergence is PINNED, not fixed.** Reconciling it would move the body-rendering path both
nav-golden fasits read, which nothing asks for. So this function reports FACTS and decides
nothing: ``terminated`` says whether a closing delimiter was found, and each caller keeps
applying its own existing rule to it. ``body`` is the text after a CLOSED block and is ``""``
whenever ``terminated`` is false — a caller that wants the whole-file fallback must say so,
rather than receive it silently from a scanner that cannot know which rule applies.
Frontmatter lines are returned VERBATIM apart from their line ending: leading indentation is
load-bearing for block-form values (``verified:`` as a sequence of mappings, SPEC §5.2), so the
decoder that consumes these lines gets a second READER of one parse, never a second parser.
Gated by ``tests/test_provenance_decoder_loadbearing.py``."""
lines = text.splitlines(keepends=True)
if not lines or lines[0].strip() != "---":
return [], "", False
for i in range(1, len(lines)):
if lines[i].strip() == "---":
return (
[line.rstrip("\r\n") for line in lines[1:i]],
"".join(lines[i + 1 :]).lstrip("\n"),
True,
)
return [line.rstrip("\r\n") for line in lines[1:]], "", False
def _frontmatter_from_text(text: str) -> dict[str, str]:
"""``parse_frontmatter``'s rule, applied to already-read text: every frontmatter line that
carries a colon becomes one ``key: value`` pair, last write winning. Unterminated blocks are
parsed as if closed — the pre-split behaviour, preserved deliberately."""
fm: dict[str, str] = {}
for line in _split_frontmatter(text)[0]:
key, sep, val = line.partition(":")
if sep:
fm[key.strip()] = val.strip()
return fm
def _body_from_text(text: str) -> str:
"""``_read_body``'s rule, applied to already-read text: the body after a CLOSED frontmatter
block, and otherwise the whole file — which covers both "no block at all" and "opened but never
closed". This is the caller-side rule ``_split_frontmatter`` deliberately refuses to apply."""
_, body, terminated = _split_frontmatter(text)
return body if terminated else text
def parse_frontmatter(path: str | Path) -> dict[str, str]:
"""Read the leading ``---``-delimited YAML frontmatter block as key:value strings.
Minimal by design (no ``yaml`` dependency): enough for the one required ``type`` field and the
verdict's scalar fields. List values (``tags: [...]``) are kept verbatim; unknown fields are
preserved (OKF SPEC §4). Returns ``{}`` when there is no frontmatter block."""
return _frontmatter_from_text(Path(path).read_text(encoding="utf-8"))
def _read_body(path: Path) -> str:
"""The markdown body after the frontmatter block (or the whole file if there is none)."""
return _body_from_text(path.read_text(encoding="utf-8"))
class FlowDecodeError(ValueError):
"""A frontmatter value is outside the accepted single-line flow subset.
A ``ValueError`` subclass on purpose (the ``IngestStampError`` precedent): the CLI's refusal
tuple and hosting's 400 arm both catch ``ValueError``, so a malformed knowledge base is a
refusal the caller can read, never a traceback on the crash channel."""
#: The pair separator INSIDE a flow mapping. Colon-SPACE, never a bare colon — ``by: human:jsmith``
#: and ``at: 2024-01-15T10:00:00Z`` both carry colons that are part of the value, and a decoder
#: that split on ``:`` would quietly truncate every actor and every timestamp it read.
_FLOW_PAIR_SEPARATOR = ": "
def _scan_flow(text: str, separator: str) -> tuple[list[str], str, int]:
"""Split ``text`` on ``separator`` at nesting depth 0 and OUTSIDE quotes.
Returns ``(parts, unclosed_quote, depth)`` — the two trailing values are how the caller tells a
complete value from a truncated one, rather than discovering it later as a wrong answer.
"Split on a separator" is where this class of decoder fails silently, so the scan is
character-by-character and quote-aware instead of ``str.split``."""
parts: list[str] = []
buf: list[str] = []
quote = ""
depth = 0
i = 0
while i < len(text):
ch = text[i]
if quote:
buf.append(ch)
if ch == quote:
quote = ""
i += 1
continue
if ch in "\"'":
quote = ch
buf.append(ch)
i += 1
continue
if ch in "[{":
depth += 1
buf.append(ch)
i += 1
continue
if ch in "]}":
depth -= 1
buf.append(ch)
i += 1
continue
if depth == 0 and text.startswith(separator, i):
parts.append("".join(buf))
buf = []
i += len(separator)
continue
buf.append(ch)
i += 1
parts.append("".join(buf))
return parts, quote, depth
def _has_nested_collection(text: str) -> bool:
"""True when ``text`` opens a ``[`` or ``{`` outside quotes. Nesting is outside the accepted
subset, and depth alone cannot detect it — a balanced ``[x, y]`` returns to depth 0."""
quote = ""
for ch in text:
if quote:
if ch == quote:
quote = ""
continue
if ch in "\"'":
quote = ch
continue
if ch in "[{":
return True
return False
def _find_pair_separator(pair: str) -> int:
"""Index of the FIRST ``": "`` outside quotes, or ``-1``."""
quote = ""
i = 0
while i < len(pair):
ch = pair[i]
if quote:
if ch == quote:
quote = ""
i += 1
continue
if ch in "\"'":
quote = ch
i += 1
continue
if pair.startswith(_FLOW_PAIR_SEPARATOR, i):
return i
i += 1
return -1
def _decode_flow_mapping(item: str, raw: str, key: str | None) -> dict[str, str]:
"""One ``{ k: v, ... }`` flow mapping into a dict, KEY-AGNOSTICALLY."""
inner = item[1:-1]
if _has_nested_collection(inner):
raise FlowDecodeError(
f"a nested flow collection inside {raw!r} is outside the accepted subset — the "
"decoder reads one level of `{ key: value }` pairs and refuses to guess at more"
)
pairs, quote, depth = _scan_flow(inner, ",")
if quote:
raise FlowDecodeError(f"an unterminated quoted scalar in {raw!r}")
if depth != 0:
raise FlowDecodeError(f"an unterminated flow mapping in {raw!r}")
entry: dict[str, str] = {}
for pair in pairs:
at = _find_pair_separator(pair)
if at < 0:
raise FlowDecodeError(
f"{pair.strip()!r} in {raw!r} is not a `key: value` pair (the separator is "
"colon-SPACE) — refusing rather than guessing what was meant"
)
name = unquote_scalar(pair[:at])
value = unquote_scalar(pair[at + len(_FLOW_PAIR_SEPARATOR) :])
if name in entry:
raise FlowDecodeError(
f"duplicate key {name!r} in {raw!r} — last-write-wins is precisely the silent "
"overwrite this decoder exists to remove, so it is refused here too"
)
entry[name] = value
if key == "verified" and not entry.get("by"):
raise FlowDecodeError(
f"a `verified` entry in {raw!r} names no actor — SPEC §5.2 makes `by` required within "
"a verification event, and tiering an entry that names nobody would mint provenance"
)
return entry
def decode_flow_value(raw: str, *, key: str | None = None) -> tuple[dict[str, str], ...]:
"""Decode the accepted single-line flow subset into a tuple of entries.
Two shapes are accepted and nothing else: a flow sequence of flow mappings
``[{ k: v }, { k: v }]``, and a bare flow mapping ``{ k: v }`` which normalises to a
ONE-ELEMENT tuple (SPEC §5.2: "Consumers MUST treat a bare mapping as a one-element list").
Entries decode **key-agnostically** — whatever keys the entry carries, never a hard-coded
``{id, resource}``. The agreed segmented shape adds ``segment_id`` and ``source_offset``, so a
two-key decoder would refuse the very bundles this seam is built for. This is the ONE named
seam: a future structured reader becomes a parameter here, not a refactor everywhere.
**No YAML-1.1 coercion.** Values come back as the strings they were written as: ``yes`` / ``no``
/ ``on`` stay strings and ``1`` stays ``"1"``. This is a DELIBERATE divergence from PyYAML's
resolver, which would return ``True`` and ``1`` — written down here rather than inherited
silently, because a value that changes type between the file and the consumer is exactly the
class of surprise a provenance reader must not import.
``key`` is optional and additive: it carries the ONE key-specific rule SPEC §5.2 imposes, that
a ``verified`` entry must name an actor. Everything else stays key-agnostic.
Raises ``FlowDecodeError`` — by name, with its own message — for a bare-scalar sequence, an
empty sequence, an unterminated flow, a value continued onto the next line, a nested collection,
a duplicate key within one entry, and a ``verified`` entry with no ``by``.
Gated by ``tests/test_provenance_decoder_loadbearing.py``."""
if "\n" in raw or "\r" in raw:
raise FlowDecodeError(
f"the flow value {raw!r} does not fit on one line — a continued value is outside the "
"accepted subset, and joining the lines would decode something nobody wrote"
)
text = raw.strip()
if text.startswith("["):
if not text.endswith("]"):
raise FlowDecodeError(f"an unterminated flow sequence: {raw!r}")
inner = text[1:-1].strip()
if not inner:
raise FlowDecodeError(
f"the flow sequence {raw!r} names no source — an empty list reads as a measured "
"absence when it is the absence of a measurement"
)
chunks, quote, depth = _scan_flow(inner, ",")
if quote:
raise FlowDecodeError(f"an unterminated quoted scalar in {raw!r}")
if depth != 0:
raise FlowDecodeError(f"an unterminated flow mapping in {raw!r}")
entries: list[dict[str, str]] = []
for chunk in chunks:
item = chunk.strip()
if not (item.startswith("{") and item.endswith("}")):
raise FlowDecodeError(
f"a bare scalar entry {item!r} in {raw!r} is not a conformant entry — SPEC "
"§5.1 makes `resource` REQUIRED within an entry, so a naked filename names a "
"resource without saying so"
)
entries.append(_decode_flow_mapping(item, raw, key))
return tuple(entries)
if text.startswith("{"):
if not text.endswith("}"):
raise FlowDecodeError(f"an unterminated flow mapping: {raw!r}")
return (_decode_flow_mapping(text, raw, key),)
raise FlowDecodeError(
f"{raw!r} is neither a flow sequence nor a flow mapping — the accepted subset is "
"`[{ k: v }, ...]` or `{ k: v }` on one line"
)
@dataclass(frozen=True)
class BundleFile:
"""One OKF file: its name, declared ``type`` (``""`` if absent), frontmatter, and body."""
name: str
type: str
frontmatter: dict[str, str]
body: str
#: Why navigation did not follow a cross-link. TWO values, because the two mean different things
#: to whoever has to fix the bundle: ``outside-bundle`` is a target that resolves OUTSIDE the bundle
#: root (frequently a deliberate link to a neighbouring base), ``missing`` is a target that resolves
#: INSIDE it with no readable file there (almost always a typo in the link). Collapsing them into
#: one "skipped" would answer neither question. De-duplication is NOT among them: a repeated link
#: and a cycle are correct navigation, never a skip.
SkipReason = Literal["outside-bundle", "missing"]
@dataclass(frozen=True)
class SkippedLink:
"""One cross-link the walk did NOT follow, and why.
STRUCTURED rather than a rendered string, for the reason ``BudgetExceeded`` carries
``kind``/``limit``/``observed`` as fields (kø-(y)): "which document is missing" and "why is it
missing" are two separate operative questions, and a caller that has to re-parse prose to tell
them apart has been handed a diagnostic it cannot act on.
``target`` is the link text VERBATIM as written in the source file, never the resolved path: the
operator fixing the bundle edits that text, and a normalised form would send them looking for a
string their file does not contain."""
#: Bundle-relative name of the file the link was written in.
from_file: str
#: The link target exactly as it appears in that file.
target: str
reason: SkipReason
#: Characters an actor may not contain, because each would RESTRUCTURE the flow mapping it is
#: written into. ``": "`` is in the set for a reason that is not decoration: ``decode_flow_value``
#: splits pairs on colon-SPACE, so an actor carrying it would be written out cleanly and then
#: mis-decoded on the way back. The writer and the reader must agree on this set, or the round trip
#: lies while every byte looks fine.
_VERIFIED_UNSAFE_TOKENS = (",", "{", "}", "[", "]", "\n", "\r", ": ")
def verified_field(actor: str, at: str) -> str:
"""Compose the SPEC §5.2 single-verifier flow shorthand ``{ by: <actor>, at: <at> }``.
The ENCODER half of this seam, and a separately named function on purpose: it gives the
round-trip property one site to be measured at, and ``render_frontmatter`` cannot serve —
it collapses every newline to a space and therefore cannot emit block form at all.
**The actor is written VERBATIM and is never prefixed with ``human:``.** A caller reaches the
human tier by saying so; minting the prefix on their behalf would fabricate a sign-off nobody
gave.
**Honesty limit, stated:** only ``actor`` is checked against the unsafe set. ``at`` is an ISO
timestamp in every caller today and is not validated here — a deliberate scope line, not an
oversight, and the place to revisit it is the invariant record rather than a silent widening.
Raises ``FlowDecodeError`` — the same named refusal the reader raises, which is what makes
"the writer refuses exactly what the reader cannot read" structural rather than a convention.
Gated by ``tests/test_provenance_decoder_loadbearing.py``."""
for token in _VERIFIED_UNSAFE_TOKENS:
if token in actor:
raise FlowDecodeError(
f"the actor {actor!r} contains {token!r}, which would restructure the `verified` "
"flow mapping — refusing to write a provenance record that parses cleanly into "
"something no one wrote"
)
return f"{{ by: {actor}, at: {at} }}"
#: A concept's trust level, derived from its ``verified`` actors (SPEC §5.3), lowest to highest.
#: Derived, never stored: OKF records objective signals and refuses to persist a subjective score,
#: so this is a reading of the actors and not a field any document carries.
TrustTier = Literal["unverified", "machine-confirmed", "human-reviewed"]
#: The ONE actor prefix that raises a concept to the human tier (SPEC §7 actor convention).
_HUMAN_ACTOR_PREFIX = "human:"
def trust_tier(entries: tuple[dict[str, str], ...] | None) -> TrustTier:
"""Derive the trust tier from ``verified`` entries (SPEC §5.3).
``None`` or empty ⇒ ``unverified``; any actor whose ``by`` STARTS WITH ``human:`` ⇒
``human-reviewed``; otherwise ⇒ ``machine-confirmed``.
**Prefix, never substring.** ``by: bot/human:2`` is a machine actor whose identifier merely
contains the token, and a substring test would promote it — minting a human sign-off nobody
gave, which is the fabricated-provenance defect one level down.
**An entry that names NO actor is REFUSED, never tiered.** "Otherwise ⇒ machine-confirmed"
would derive a trust level from an entry that identifies nobody. ``decode_flow_value`` already
refuses that shape when it reads a ``verified`` value; this function is public, so it ASSERTS
the invariant at its own door instead of assuming its caller came through that one. No new
exception type: the named refusal for this condition belongs to the decoder, and a second class
here would imply a second rule.
**Entry count is not a tier.** SPEC §5.3 derives the tier from the actor prefix alone, so a
two-entry machine list stays ``machine-confirmed``.
Gated by ``tests/test_provenance_decoder_loadbearing.py``."""
if not entries:
return "unverified"
actors: list[str] = []
for entry in entries:
actor = entry.get("by", "").strip()
if not actor:
raise ValueError(
f"a verification entry {entry!r} names no `by` actor — SPEC §5.2 makes it "
"required, and deriving a trust tier from an entry that identifies nobody would "
"mint the provenance it claims to read"
)
actors.append(actor)
if any(actor.startswith(_HUMAN_ACTOR_PREFIX) for actor in actors):
return "human-reviewed"
return "machine-confirmed"
class AdjudicationValueError(ValueError):
"""A concept declares an ``adjudication`` value outside the closed vocabulary.
A ``ValueError`` subclass (the ``IngestStampError`` precedent) so a malformed knowledge base
reaches the CLI's refusal tuple and hosting's 400 arm rather than the crash channel."""
#: Whether a concept has been ADJUDICATED, per the cross-repo contract. The value set on the wire
#: is CLOSED (``proposed`` | ``adjudicated``); the third token is this consumer's, and it is what
#: absence means. See ``docs/okf-konsum-kontrakter.md`` § 2, which is the source for this rule.
AdjudicationState = Literal["proposed", "adjudicated", "unknown"]
#: The closed on-the-wire vocabulary. ``unknown`` is deliberately NOT in it: it is what this reader
#: concludes from absence, never something a document may declare.
_ADJUDICATION_WIRE_VALUES = ("proposed", "adjudicated")
def adjudication_for(path: str | Path) -> AdjudicationState:
"""Read a concept's adjudication state, with absence as a FIRST-CLASS state.
A concept that does not carry the key is ``unknown`` — *we did not learn whether this was
adjudicated*, which is what an older bundle looks like. **Collapsing that into ``absent``**
(*it was not adjudicated*) **is the same defect as collapsing ``unreadable`` into ``absent``**
one layer up, and the same defect removed from ``RunResult.verdict``.
A value outside the two named ones is **refused by name**, never mapped to ``unknown``:
validation, never repair. An EMPTY value is refused for the same reason — present-but-empty is
a different fact from absent, and folding it in would recreate the collapse this function
exists to prevent.
**Read through ``parse_frontmatter``, NOT ``evidence_for`` — and that is a MEASUREMENT, not a
preference.** The plan specified ``evidence_for(path, key="adjudication")``; the contract as
delivered makes ``adjudication`` a plain SCALAR, and ``evidence_for`` routes its value through
``decode_flow_value``. Measured 2026-09-02: BOTH valid values come back as an identical
``state='unreadable', reason='unsupported-flow'`` record, so that route cannot tell ``proposed``
from ``adjudicated`` at all. ``parse_frontmatter`` is the repo's scalar reader over the SAME
``_split_frontmatter`` scan, so this is a second READER of one parse — never a second parser,
which is the rule that route was reaching for.
Gated by ``tests/test_falsification_verdict_loadbearing.py``."""
raw = parse_frontmatter(path).get("adjudication")
if raw is None:
return "unknown"
value = unquote_scalar(raw)
if value not in _ADJUDICATION_WIRE_VALUES:
raise AdjudicationValueError(
f"{value!r} is not an `adjudication` value — the vocabulary is closed to "
f"{' | '.join(_ADJUDICATION_WIRE_VALUES)}, and mapping an unrecognised one to "
"`unknown` would invent the very state that token exists to keep honest"
)
# ``value`` is one of the wire literals, which the Literal type also admits.
return value # type: ignore[return-value]
#: What a document says about ONE provenance key. Three states, and the third is the whole point:
#: collapsing ``unreadable`` into ``absent`` turns a verdict on missing evidence into evidence of
#: absence.
EvidenceState = Literal["present", "absent", "unreadable"]
@dataclass(frozen=True)
class FalsificationEvidence:
"""What one document offers a falsification verdict, and how much of it could be read.
Each field asserts only what its state can honestly carry: ``tier`` is ``None`` unless the state
is ``present`` (an unread value tiers nothing), and ``reason`` is ``None`` when it IS present
(there is nothing to explain). ``items_seen`` is its own field rather than folded into a token,
the ``BudgetExceeded`` kø-(y) rule: "which shape" and "how many" are two operative questions."""
#: Path of the document the evidence was read from.
file: str
state: EvidenceState
#: The derived trust tier — ``None`` unless ``state`` is ``present``.
tier: TrustTier | None
#: WHY the value could not be read — ``None`` unless ``state`` is ``unreadable``.
reason: ProvenanceReason | None
#: The decoded entries, empty unless ``state`` is ``present``.
entries: tuple[dict[str, str], ...]
#: How many entries were seen, INCLUDING ones that could not be decoded. Part of the
#: ``(state, reason, items_seen)`` triple a discounted concept is reported with.
items_seen: int
#: The ONE provenance key SPEC §5.3 derives a trust tier from. Named rather than spelled inline at
#: the two places that care, so "which key is tiered" cannot answer differently in each of them.
_TIERED_KEY: Final = "verified"
def evidence_for(path: str | Path, key: str = _TIERED_KEY) -> FalsificationEvidence:
"""Read one document's provenance into the three-state answer a falsification verdict needs.
**A library primitive, deliberately NOT wired into ``run_project`` or ``explore``**, mirroring
``promote_verdict`` and ``write_verdict``: the system reads, the caller decides. Wiring it into
the run surface would move the byte-pinned demo transcript, which nothing asks for.
**A tier is derived ONLY for the key SPEC §5.3 tiers.** ``tier`` used to be derived
unconditionally, so ``key="sources"`` — or the agreed segmented form's
``segment_id``/``source_offset`` — crashed in ``trust_tier``, which refuses an entry naming no
``by`` actor. That refusal is right, and it belongs to ``verified`` alone: ``by`` is required of
a verification entry (SPEC §5.2), not of every provenance key. So the guard stays exactly where
it is and this function stops applying it to keys it was never about. Measured at the close of
B4: all 11 call sites in the repo use the default, so the parameter had never been exercised.
Gated by ``tests/test_falsification_verdict_loadbearing.py`` and
``tests/test_evidence_key_parameter_loadbearing.py``."""
result = read_provenance(path, key)
if result is None:
return FalsificationEvidence(
file=str(path), state="absent", tier=None, reason=None, entries=(), items_seen=0
)
if isinstance(result, UnreadableProvenance):
return FalsificationEvidence(
file=str(path),
state="unreadable",
tier=None,
reason=result.reason,
entries=(),
items_seen=result.items_seen,
)
return FalsificationEvidence(
file=str(path),
state="present",
tier=trust_tier(result) if key == _TIERED_KEY else None,
reason=None,
entries=result,
items_seen=len(result),
)
def evidence_notice(evidence: FalsificationEvidence) -> str | None:
"""One line about evidence that could not be used, or ``None`` when there is nothing to say.
Omission, never an empty row (the ``cost_baseline_notice`` precedent). The reason TOKEN is
printed raw rather than translated into prose, so no second display vocabulary exists to drift
from ``ProvenanceReason``."""
if evidence.state == "present":
return None
detail = "" if evidence.reason is None else f"; reason {evidence.reason}"
return (
f"provenance {evidence.state} in {evidence.file}{detail}; items_seen={evidence.items_seen}"
)
def admits_falsification(evidence: FalsificationEvidence) -> bool:
"""The K5 threshold, expressed in ONE place: ``present`` AND a tier above ``unverified``.
An operator decision, not a default. A second copy of a threshold drifts (kø-(p)), and a
threshold spelled inline at each caller is a threshold nobody can find.
Everything it refuses is meant to be REPORTED with ``(state, reason, items_seen)`` and
explicitly discounted — never silently excluded, because a dropped concept and a discounted one
are different facts and only one of them is honest about what was read.
**``author`` / ``usage_count`` / ``last_modified`` are deliberately NOT required, and the
denominator is written down rather than implied:** SPEC §5.1 names SIX entry keys and the
producer writes TWO of them. Requiring keys the producer does not emit would make the threshold
unreachable in practice while looking strict on paper. The threshold names only what is
actually written. See ``docs/okf-konsum-kontrakter.md`` § 1, which is the source for this rule.
Gated by ``tests/test_falsification_verdict_loadbearing.py``."""
# NAMES the tiers that clear it, rather than excluding the one that does not. ``tier`` is now
# ``None`` for any key other than ``verified`` (SPEC §5.3 tiers that key alone), and
# ``None != "unverified"`` is TRUE — so the exclusion spelling would let a present ``sources``
# list clear a threshold about verification. For every value reachable before that change the
# two spellings are identical, which is why this is a tightening and not a new rule.
return evidence.state == "present" and evidence.tier in ("human-reviewed", "machine-confirmed")
#: WHY a provenance value could not be read. The tokens name the SHAPE the value is written in
#: and NOTHING else — the same discipline ``SkipReason`` carries. A block sequence and a block
#: mapping are both CONFORMANT OKF (SPEC §5.2 writes ``verified`` in exactly those forms); they are
#: simply outside the accepted single-line subset, and this decoder is not entitled to an opinion
#: about whether the author erred. ``unsupported-flow`` is the one token that does denote a
#: malformation, and it says so by naming the flow FORM rather than the author.
ProvenanceReason = Literal["block-sequence", "block-mapping", "unsupported-flow"]
@dataclass(frozen=True)
class UnreadableProvenance:
"""A provenance key that IS present and could NOT be read, and why — never silence.
Mirrors ``SkippedLink``: structured rather than a rendered string, for the reason
``BudgetExceeded`` carries ``kind``/``limit``/``observed`` as fields (kø-(y)). "Which shape is
this written in" and "how many entries were there" are two separate operative questions, and a
caller forced to re-parse a token to tell them apart has been handed a diagnostic it cannot act
on. That is why the COUNT is its own field and is never folded into the token.
``value`` is the offending text VERBATIM as written in the file, indentation included — the
operator fixing the document edits that text, and a normalised form would send them looking for
a string their file does not contain."""
#: Path of the document the key was read from.
file: str
#: The frontmatter key that could not be read.
key: str
#: The offending text exactly as it appears in that file.
value: str
reason: ProvenanceReason
#: Entries seen. ``0`` when nothing countable was there — an empty key, or a flow value the
#: decoder refused and therefore never enumerated.
items_seen: int
def read_provenance(
path: str | Path, key: str
) -> tuple[dict[str, str], ...] | UnreadableProvenance | None:
"""Read one provenance key into entries, or say why it could not be read.
Three outcomes, and the three-way split is the whole point:
* ``None`` — the document does not carry the key. **Absence is ``None``, never a default**:
the F2 principle one layer down, where an unread signal must not become an asserted absent
one.
* a tuple of entries — the value was in the accepted flow subset and decoded.
* ``UnreadableProvenance`` — the key is THERE and could not be read, with the shape and the
count that say what was seen.
Driven by ``_split_frontmatter``: this is a second READER of the one parse, never a second
parser. Only top-level (unindented) frontmatter lines are considered as key sites, so an
indented ``by:`` inside a block entry can never be mistaken for a document-level key.
Gated by ``tests/test_provenance_decoder_loadbearing.py``."""
lines = _split_frontmatter(Path(path).read_text(encoding="utf-8"))[0]
for i, line in enumerate(lines):
if line[:1] in (" ", "\t"):
continue
name, sep, raw = line.partition(":")
if not sep or name.strip() != key:
continue
value = raw.strip()
if value:
try:
return decode_flow_value(value, key=key)
except FlowDecodeError:
return UnreadableProvenance(
file=str(path), key=key, value=value, reason="unsupported-flow", items_seen=0
)
continuation = list(itertools.takewhile(lambda ln: ln[:1] in (" ", "\t"), lines[i + 1 :]))
if not continuation:
return UnreadableProvenance(
file=str(path), key=key, value="", reason="block-mapping", items_seen=0
)
# An ITEM is a continuation line whose STRIPPED form opens with ``- ``. A ``- `` occurring
# inside a value is text, not an item, and counting it would inflate the number the caller
# acts on.
items = sum(1 for ln in continuation if ln.strip().startswith("- "))
return UnreadableProvenance(
file=str(path),
key=key,
value="\n".join(continuation),
# SPEC §5.2's one-element MUST: a bare mapping IS one entry, so a mapping that is
# present counts 1 rather than 0 — 0 is reserved for "nothing was there at all".
reason="block-sequence" if items else "block-mapping",
items_seen=items or 1,
)
return None
@dataclass(frozen=True)
class Bundle:
"""A navigated OKF bundle: ``index.md`` plus every cross-linked file that resolves — and, in
``skipped``, every cross-link that did not.
``skipped`` DEFAULTS to the empty tuple, and the default is the honest reading rather than a
convenience: an empty trace is a positive statement ("every cross-link was followed"), in the
same class as ``ProvenanceStamp.external_calls`` ("nothing outside this process was contacted").
That is the opposite of ``ProvenanceStamp.cost_baseline_anchored``, which is REQUIRED precisely
because both of its defaults would lie. The difference is what each absent value would assert:
a missing bool has to claim something about an event, while a missing trace asserts only that
the event list is empty — which is exactly what a construction with no skips means."""
dir: str
files: tuple[BundleFile, ...]
#: Every link navigation could not follow, in walk order. Read by ``run`` to render the one line
#: a run prints about its own reachability; NEVER read by ``bundle_context``, whose rendering is
#: built from ``index_summary`` + ``context_files`` alone — which is what keeps the commons-owned
#: nav-golden fasit byte-identical.
skipped: tuple[SkippedLink, ...] = ()
@property
def index_summary(self) -> str:
"""The ROOT index body — the progressive-disclosure entry point (not whole-bundle stuffing).
Bound to the root alone: a nested ``a/index.md`` is navigation, never summary prose."""
return next((f.body for f in self.files if f.name == _INDEX_NAME), "")
@property
def verdicts(self) -> list[BundleFile]:
"""Every ``type: verdict`` file (the ExpeL seeds the Step-1 wiring retrieves)."""
return [f for f in self.files if f.type == "verdict"]
@property
def context_files(self) -> list[BundleFile]:
"""The non-index, non-``verdict`` concept files — the bodies that form the agent context.
The verdict layer is deliberately EXCLUDED: prior verdicts reach the hypothesis prompt only
through the gated ExpeL fold, never by stuffing them into the read-context (målbilde §4).
The exclusion is a TYPE CHECK on each reached file, applied at EVERY level — never a
property of the link graph, so a mislabelled or injected navigation edge cannot smuggle a
nested verdict in. Index files are dropped by BASENAME at every level too: a nested
``a/index.md`` is navigation, not content."""
return [
f
for f in self.files
if posixpath.basename(f.name) != _INDEX_NAME and f.type != "verdict"
]
@property
def hypothesis(self) -> BundleFile | None:
"""The candidate ``type: hypothesis`` file, if present."""
return next((f for f in self.files if f.type == "hypothesis"), None)
def _load_file(bundle_dir: str, name: str) -> BundleFile | None:
"""Resolve the bundle-relative ``name`` within ``bundle_dir`` and read it, or ``None`` if
missing / escaping the bundle (OKF §4 broken-link tolerance + fail-closed path-safety)."""
try:
resolved = Path(safe_resolve(bundle_dir, name))
except PathSecurityError:
return None
if not resolved.is_file():
return None
text = resolved.read_text(encoding="utf-8")
fm = _frontmatter_from_text(text)
return BundleFile(
name=name, type=fm.get("type", ""), frontmatter=fm, body=_body_from_text(text)
)
def _resolve_target(bundle_dir: str, from_name: str, target: str) -> tuple[str, str] | None:
"""Resolve one cross-link into ``(bundle-relative posix name, canonical path)``, or ``None``
when it fails to resolve for ANY reason (escape, invalid path component) — the caller skips,
never raises (method-spec §3 Step 1).
A leading ``/`` denotes the BUNDLE ROOT, not the filesystem root: an implementation that let
``os.path.join`` see an absolute target would either escape to the real filesystem path or
refuse a legitimate root-relative link. Any other form resolves against the LINKING FILE's own
directory, so an index links its immediate children one segment at a time."""
if target.startswith("/"):
rel = posixpath.normpath(target.lstrip("/"))
else:
rel = posixpath.normpath(posixpath.join(posixpath.dirname(from_name), target))
try:
return rel, safe_resolve(bundle_dir, rel)
except PathSecurityError:
return None
def _walk(
bundle_dir: str,
current: BundleFile,
files: list[BundleFile],
seen: set[str],
skipped: list[SkippedLink],
) -> None:
"""Follow ``current``'s cross-links depth-first in first-seen order, appending each newly
reached file and recursing into it. De-duplication is on the CANONICAL RESOLVED path (so
``./a.md``, ``a.md`` and ``/a.md`` are one entry), which is also what terminates cycles.
A link that cannot be followed is still SKIPPED, never raised (OKF §4) — the tolerance is the
spec — but it is now RECORDED in ``skipped``, with the reason distinguishing the two cases.
The dedup branch records NOTHING: a repeated link and a cycle are correct navigation, and an
implementation that logged every ``continue`` would report a healthy bundle as half-unread.
A caller-owned accumulator rather than a return value, for the reason ``generate``'s
parse-failure sink is one: the recursion is depth-first over an unbounded tree, so every frame
appends into the SAME list and the walk's shape stays unchanged."""
for target in _LINK_RE.findall(current.body):
resolved = _resolve_target(bundle_dir, current.name, target)
if resolved is None:
# The target left the bundle. Often deliberate (a link to a neighbouring base), so it is
# reported rather than refused — the tolerance is unchanged.
skipped.append(
SkippedLink(from_file=current.name, target=target, reason="outside-bundle")
)
continue
rel, canonical = resolved
if canonical in seen:
continue # de-duplication / cycle termination: correct navigation, NOT a skip
seen.add(canonical)
linked = _load_file(bundle_dir, rel)
if linked is None:
# In-bundle, but nothing readable is there: broken link, tolerated, never raised (§4).
# Recorded once per resolved target — the ``seen`` entry above absorbs repeats.
skipped.append(SkippedLink(from_file=current.name, target=target, reason="missing"))
continue
files.append(linked)
_walk(bundle_dir, linked, files, seen, skipped)
def navigate_bundle(bundle_dir: str) -> Bundle:
"""Navigate the OKF bundle from ``index.md``: parse the root index, then follow intra-bundle
``.md`` cross-links RECURSIVELY, depth-first in first-seen link order, reading each reached
file's frontmatter + body. Fully deterministic. Broken / escaping links are skipped (§4) — and
RECORDED on the returned ``Bundle.skipped``, so "this document was never written" and "the link
to it was wrong" stop looking identical from the outside.
Navigation follows LINKS ONLY — a directory is never enumerated. Hence the missing-``index.md``
error binds the bundle ROOT alone (a bundle has no entry point without it); an intermediate
directory reached by a link needs no ``index.md`` of its own, and content nothing links to is
simply unreachable, not an error. Raises ``ValueError`` only for the unreadable root index."""
index = _load_file(bundle_dir, _INDEX_NAME)
if index is None:
raise ValueError(f"OKF bundle has no readable {_INDEX_NAME}: {bundle_dir!r}")
files: list[BundleFile] = [index]
skipped: list[SkippedLink] = []
root = _resolve_target(bundle_dir, _INDEX_NAME, _INDEX_NAME)
seen = {root[1]} if root is not None else set()
_walk(bundle_dir, index, files, seen, skipped)
return Bundle(dir=bundle_dir, files=tuple(files), skipped=tuple(skipped))
class BundleIdMismatch(ValueError):
"""A base cannot say what it is: two of its CONCEPTS declare different ``bundle_id`` values.
**This used to mean something else, and the change is an operator decision (S7a-3 pkt. 1).**
Until 2026-09-03 it also covered a declared id that disagreed with the directory the base was
mounted under. That refusal was measured against the first delivered corpus that declares its
own id (K2: 618 of 630 concept files, mounted under a different name) and it made the base
unopenable at every door, with re-mounting by hand as the only remedy. The mount is a filesystem
accident; the declaration is the artefact speaking. A disagreement between them is now RECORDED
(``ResolvedBundleId.mount`` + ``ProvenanceStamp.bundle_id_source`` + one warning line), never
refused.
What is left is the disagreement no mount name could ever settle: within ONE base, two concepts
naming two different corpora. There is no fallback that makes that base coherent, and an
artefact stamped from it would name one of the two at random.
A ``ValueError`` DELIBERATELY: it must land on ``run.main``'s refusal tuple and on hosting's
400 arm rather than on the crash channel. ``ExplorationError`` is a ``RuntimeError`` and would
give an operator a traceback and an automated caller a 500 — a configuration mistake dressed
as a server fault.
"""
#: Which source answered. THREE values, not a boolean (operator decision B1): a caller that cannot
#: tell "the concept said so" from "we fell back twice" has been handed a stamp it cannot audit.
BundleIdOrigin = Literal["declared-concept", "declared-index", "mount-derived"]
_BUNDLE_ID_KEY = "bundle_id"
@dataclass(frozen=True)
class ResolvedBundleId:
"""A base's id, WHERE it came from, and the mount it was read from.
``origin`` is REQUIRED WITHOUT DEFAULT, for the reason ``ProvenanceStamp.cost_baseline_anchored``
is: both defaults would lie about an event. Defaulting to ``"mount-derived"`` would let a
resolver that never read the base claim it had; defaulting to a declared value would claim a
declaration that never happened.
``mount`` is CARRIED rather than discarded once the declaration wins (S7a-3 pkt. 1): the
warning a run prints names both, and a caller reading an artefact months later needs to know
which directory the corpus was read from. A resolver that kept only the winner could report
that something was overridden without being able to say what.
"""
id: str
origin: BundleIdOrigin
mount: str
def _declared_bundle_id(frontmatter: dict[str, str]) -> str:
"""The declared id, de-quoted through ``unquote_scalar`` — this repo's ONE de-quoting rule."""
return unquote_scalar(frontmatter.get(_BUNDLE_ID_KEY, "")).strip()
def reconcile_bundle_id(
bundle_dir: str | Path, *, concept_name: str | None = None
) -> ResolvedBundleId:
"""Resolve a base's ``bundle_id`` against the mount it was opened from — the ONE rule.
``bundle_id`` denotes the corpus **as a mounted artefact**, never the production event that made
it (operator decision D6). That denotation cannot be changed once artefacts bear the stamp,
which is why it is written down here rather than left to each call site.
Resolution order is operator decision B1 — identity is the pair ``(bundle_id, concept_id)``, and
the CONCEPT's own frontmatter is consulted first:
1. ``concept_name``'s frontmatter, when a concept is named and declares the key →
``"declared-concept"``;
2. the root ``index.md``'s frontmatter → ``"declared-index"``;
3. the mount's basename → ``"mount-derived"``.
A declared id that DISAGREES with the mount is NOT an error (operator decision, S7a-3 pkt. 1):
the declaration wins, the mount is carried alongside on ``ResolvedBundleId.mount``, and the
caller that opened the base is the one that reports it. What still refuses is a base whose
CONCEPTS disagree with each other — see ``assert_declared_ids_agree``, which is a separate,
unconditionally called check rather than a branch here, so a door that forgot it fails a test
of its own instead of quietly skipping a refusal.
A base whose root ``index.md`` cannot be read is **unknown, not undeclared**:
``navigate_bundle``'s fail-fast propagates unchanged, because reading an unreadable base as
"it declares nothing" would widen the answer on missing evidence.
MEASURED 2026-09-02: no file under ``shared/`` declares the key (zero ``^bundle_id`` hits
against a known-positive control of 31 files carrying ``^type:``), so every base in this repo
resolves ``mount-derived`` today and both declared branches are DEFENSIVE — the first corpus in
the wild to take a declared branch was K2 (measured 2026-09-03, 619 declaring files).
**Honesty limit, stated:** without ``concept_name`` the root ``index.md`` answers, so a base
whose index declares X while its concepts declare Y resolves to X. That is B1's resolution
ORDER, unchanged here; the agreement check below is about concepts colliding with each other.
:raises ValueError: the root ``index.md`` is missing or unreadable.
"""
root = str(bundle_dir)
mount = Path(root).name
if concept_name is not None:
concept = _load_file(root, concept_name)
if concept is None:
raise ValueError(f"OKF bundle has no readable concept {concept_name!r}: {root!r}")
declared = _declared_bundle_id(concept.frontmatter)
if declared:
return ResolvedBundleId(id=declared, origin="declared-concept", mount=mount)
index = _load_file(root, _INDEX_NAME)
if index is None:
raise ValueError(f"OKF bundle has no readable {_INDEX_NAME}: {root!r}")
declared = _declared_bundle_id(index.frontmatter)
if declared:
return ResolvedBundleId(id=declared, origin="declared-index", mount=mount)
return ResolvedBundleId(id=mount, origin="mount-derived", mount=mount)
def assert_declared_ids_agree(bundle: Bundle) -> None:
"""Refuse a navigated base whose CONCEPTS declare two different ``bundle_id`` values.
This is the collision the slacken (S7a-3 pkt. 1) deliberately keeps: a declared id that
disagrees with the MOUNT is a filesystem accident and is now recorded rather than refused, but
two concepts inside one base naming two different corpora is a base that cannot say what it is.
No fallback settles it, and every artefact stamped from it would name one of the two at random.
**Concepts only — the root ``index.md`` is NOT in the set, and that is decision B1 applied a
second time.** Concept-beats-index is a PRECEDENCE rule, so an index out of step with its
concepts is the fallback losing, not a collision. Folding the index in would newly refuse
exactly the K2-shaped bases the slacken exists to admit.
Reads ``context_files``, which is also what drops the ``type: verdict`` layer and nested
``index.md`` at every level — the same property ``read_bundle`` is built from, so the two
cannot disagree about which files count as concepts.
A SEPARATE function rather than a branch inside ``reconcile_bundle_id``: it needs a navigated
bundle, and ``reconcile_bundle_id`` must stay pure (``explore._bundle_index`` resolves ids for
directories that may not exist). Being separate also gives it its own mutation — detach the call
at a door and that door's own arm goes red, instead of a refusal quietly not happening.
:raises BundleIdMismatch: two concepts declare different ids.
"""
declared: dict[str, str] = {}
for f in bundle.context_files:
value = _declared_bundle_id(f.frontmatter)
if value:
declared.setdefault(value, f.name)
if len(declared) > 1:
named = ", ".join(f"{value!r} (in {name!r})" for value, name in sorted(declared.items()))
raise BundleIdMismatch(
f"knowledge base {bundle.dir!r} cannot say what it is: its concepts declare "
f"{len(declared)} different bundle_id values — {named}; an approach names a base by "
"that id, so a run would evaluate against one corpus and report the other"
)
def bundle_context(bundle: Bundle, *, dimension: str | None = None) -> str:
"""Render a navigated bundle as agent read-context via progressive disclosure: the ``index.md``
summary, then each concept file as ``## {type}: {title}\\n{body}``. ``type: verdict`` files are
EXCLUDED (målbilde §2 step 1 / §4: navigation, not chunk-stuffing — the verdict layer folds in
only via the gated ExpeL retrieval). Deterministic: index first, then context files in
navigation order; empty sections are dropped.
When ``dimension`` is given, only concept files whose frontmatter ``dimension`` matches — or that
carry no ``dimension`` at all (un-scoped knowledge is never dropped) — are rendered; the default
``dimension=None`` renders every concept file, byte-identical to the prior behavior. ``dimension``
is a plain ``str`` (not the ``Dimension`` type) so ``okf`` stays MAF-free and import-cycle-free.
Rendering is FLAT regardless of nesting depth (method-spec §3 Step 1): directory structure is
navigation, not presentation, so a nested concept file renders as the same
``## {type}: {title}`` section a root file would — there is no level heading, and nested index
bodies do not appear at all. The serialisation (heading, blank line, body; sections separated by
one blank line) is what the commons nav-golden ``expected-read-context.md`` fasit compares
against — see ``tests/test_okf.py`` nav-golden gates."""
sections = [bundle.index_summary.strip("\n")]
for f in bundle.context_files:
if dimension is not None:
file_dim = f.frontmatter.get("dimension")
if file_dim is not None and file_dim != dimension:
continue
title = unquote_scalar(f.frontmatter.get("title", f.name))
body = f.body.strip("\n")
sections.append(f"## {f.type or 'document'}: {title}\n\n{body}")
return "\n\n".join(s for s in sections if s.strip())
def render_frontmatter(frontmatter: dict[str, str]) -> str:
"""Render a frontmatter dict as ``key: value`` lines (the inverse direction of
``parse_frontmatter``, used by the Step-8 promotion writer). Scalar values are **single-lined**
(every newline/CR collapses to a space) because ``parse_frontmatter`` is line-oriented and stops
at the first ``---`` line — a multi-line value would otherwise corrupt the block or terminate it
early. NOT a bijection: this only guarantees that the single-line fields it writes re-parse to
the same strings; ``parse_frontmatter`` keeps quotes and treats ``tags: [...]`` as a literal
string, so callers pass already-formatted values. Keys are emitted in insertion order."""
return "\n".join(f"{key}: {' '.join(str(value).split())}" for key, value in frontmatter.items())
class IngestStampError(ValueError):
"""A curated writer was handed frontmatter carrying the COMPLETE ingest ownership stamp
(``shared/ingest-spec.md`` §3). The stamp is the sole mark separating ingest-owned files from
curated ones, and re-materialization replaces exactly what carries it — so a curated file that
forged it could be silently deleted by a later ingest run."""
_YAML_TRUE_LITERALS = frozenset({"true", "yes", "on"})
"""Every scalar a real YAML reader parses to boolean ``True`` (measured with PyYAML's ``safe_load``
core-schema resolver: ``true``/``yes``/``on``, any case, are bool; the same resolver reads bare
``y``/``n`` and ``1``/``0`` as string/int, never bool — so those are deliberately EXCLUDED here.
Widening past what a YAML reader actually resolves would over-block curated content no ingest
pipeline ever produces, on a form nothing downstream would honour as the stamp either."""
def _carries_complete_ingest_stamp(frontmatter: dict[str, str]) -> bool:
"""Whether ``frontmatter`` carries BOTH halves of the ingest ownership stamp: a ``generated``
value a YAML reader would read as boolean ``True`` (``_YAML_TRUE_LITERALS``) together with a
non-empty ``ingest_manifest`` reference (ingest-spec §7).
FAIL-CLOSED on the value literal: the field previously matched only the exact string ``"true"``,
so a pinned ingest writer emitting any other YAML-1.1 truthy form (``yes``, ``on``) would have
slipped the stamp past this gate undetected — inert only by the accident of the pinned writer's
current output, per the CLAUDE.md ingest-stamp invariant. The test is on the COMPLETE stamp,
never on the individual field names — curated content may legitimately carry a single provenance
field, and a verbatim round-trip of one half must keep working. Values are compared the way
``parse_frontmatter`` yields them (line-oriented strings, quotes retained), so surrounding quotes
and case are normalised away here."""
generated = str(frontmatter.get("generated", "")).strip().strip('"').lower()
manifest = str(frontmatter.get("ingest_manifest", "")).strip().strip('"')
return generated in _YAML_TRUE_LITERALS and bool(manifest)
def write_concept_file(bundle_dir: str, name: str, frontmatter: dict[str, str], body: str) -> Path:
"""Write a typed OKF concept file (``---`` frontmatter + markdown body) into ``bundle_dir``,
path-safe via ``safe_resolve`` (fail-closed: a ``name`` escaping the bundle raises
``PathSecurityError``). Pure stdlib — the D7-portable counterpart of ``navigate_bundle``'s read.
This is the repo's one authoring primitive that materialises a concept file from CALLER-SUPPLIED
frontmatter, so it is the surface ingest-spec §3's "no other writer may forge the stamp" binds:
a frontmatter carrying the complete ingest stamp raises ``IngestStampError`` and NOTHING is
written. A validation, never a repair — the caller is told, not silently corrected.
Returns the written path."""
verified = frontmatter.get("verified")
if verified is not None:
# Validation, never repair, and NOT a second copy of the rule: the check IS
# ``decode_flow_value``, so the writer refuses exactly what the reader cannot read. A
# duplicated rule here would be free to drift, and the drifted copy would decide what gets
# written. Scoped to ``verified`` BY NAME — ``render_frontmatter`` keeps collapsing
# newlines for every other key, which this step deliberately does not change.
decode_flow_value(verified, key="verified")
if _carries_complete_ingest_stamp(frontmatter):
raise IngestStampError(
"refusing to write a curated concept file carrying the COMPLETE ingest ownership stamp "
"(generated: true + ingest_manifest); only the ingest materializer may claim it "
"(ingest-spec §3) — either field alone is permitted"
)
resolved = Path(safe_resolve(bundle_dir, name))
resolved.parent.mkdir(parents=True, exist_ok=True)
resolved.write_text(f"---\n{render_frontmatter(frontmatter)}\n---\n\n{body}", encoding="utf-8")
return resolved
def link_in_index(bundle_dir: str, target_name: str, label: str) -> bool:
"""Append an intra-bundle cross-link ``- [label](target_name)`` to ``index.md`` so
``navigate_bundle`` (which follows ONLY index cross-links) reaches a newly written file.
Idempotent: if a link to ``target_name`` already exists the index is left untouched. Returns
whether a link was added. ``label`` is supplied by the caller and ends up in ``index_summary``
(hence ``bundle_context``) verbatim, so the promotion policy passes a NEUTRAL label carrying no
verdict signal (målbilde §3/§6). Known MVP limitation: the read-modify-write is not atomic."""
resolved = Path(safe_resolve(bundle_dir, _INDEX_NAME))
body = resolved.read_text(encoding="utf-8")
if f"]({target_name})" in body:
return False
prefix = body if body.endswith("\n") else body + "\n"
resolved.write_text(f"{prefix}- [{label}]({target_name})\n", encoding="utf-8")
return True
def load_cost_baseline(bundle_dir: str, name: str = _COST_BASELINE) -> CostBaseline:
"""Load the bundle's cost baseline (``cost-baseline.json`` by default): the project's ACTUAL
cost lines (``{code: {quantity, unit_cost}}``), which the deterministic validator reconciles a
proposal's ``affected_items`` against (S4.0, F3).
Fail-fast, mirroring ``load_ir_projection`` and ``dimension.load_dimension``: a missing file
raises ``FileNotFoundError`` and malformed content raises ``pydantic.ValidationError``. A cost
baseline is authoritative gate input — a tolerantly-degraded one would silently un-anchor the
gate, which is precisely the failure this stage exists to prevent. (The tolerant skip rule
belongs to the RAW verdict-inbox layer, never here.)
Use ``load_optional_cost_baseline`` where the ABSENCE of the file is legitimate."""
resolved = Path(safe_resolve(bundle_dir, name))
if not resolved.is_file():
raise FileNotFoundError(f"cost baseline not found in bundle: {name!r}")
return CostBaseline.model_validate_json(resolved.read_text(encoding="utf-8"))
def load_optional_cost_baseline(bundle_dir: str, name: str = _COST_BASELINE) -> CostBaseline | None:
"""``load_cost_baseline`` where a MISSING file is legitimate: returns ``None`` instead of
raising. This is the run path's loader — a bundle authored before the baseline amendment is
simply un-anchored (``None`` = pre-S4.0 behaviour), not an error, which is what keeps every
existing bundle (including the commons-owned goldens) running byte-identically.
The tolerance stops at absence: a baseline that EXISTS but is malformed still raises. Reading a
corrupt baseline as "no baseline" would hand back an un-anchored gate under the appearance of an
anchored one (the same reasoning as ``budget.read_spend``)."""
try:
return load_cost_baseline(bundle_dir, name)
except FileNotFoundError:
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 (kø-(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]:
"""Load the bundle's IR projection (``validator-input.json`` by default): the candidate
measure's cost-IR (``measure``, ``affected_items``, ``claimed_saving_nok``) — the
pre-hypothesis ExpeL query-key source. Raises if missing / escaping the bundle (fail-fast: it
is required input, not an optional cross-link)."""
resolved = Path(safe_resolve(bundle_dir, name))
if not resolved.is_file():
raise FileNotFoundError(f"IR projection not found in bundle: {name!r}")
data: dict[str, Any] = json.loads(resolved.read_text(encoding="utf-8"))
return data