feat(visibility): en lenke som ikke ble fulgt sier det - spor + betinget linje (ORDRE 20260821T142704Z)

okf._walk toleret en ulesbar/utenfor-basen lenke uten aa etterlate spor (okf.py:182
"continue  # broken link"), og navigate_bundle returnerte kun filene den FANT. En base
der halve innholdet aldri ble lest var derfor umulig aa skille fra en base der de
dokumentene aldri ble skrevet - og toerrkjoeringen sa ingenting.

Toleransen er UROERT: OKF SPEC §4 krever at navigasjonen ikke kaster, og den kaster
fortsatt ikke. Dette er synlighet, ikke en ny nekt.

To tenner (samme form som ordre 20260821T092039Z, synlig uforankring):

1. okf.SkippedLink + Bundle.skipped - strukturert spor, aldri en streng: hvilken fil
   lenken sto i, lenketeksten ORDRETT (operatoeren redigerer den teksten, ikke den
   resolverte stien), og hvilken av de TO grunnene som gjaldt - outside-bundle (escape,
   ofte bevisst) eller missing (inne i basen, ingen lesbar fil, nesten alltid en
   skrivefeil). Dedup-grenen (canonical in seen) registreres ALDRI: den er korrekt
   navigasjon og det som terminerer sykler.
2. run.skipped_links_notice - EN renderer, tar den alt opploeste tuppelen, returnerer
   None naar ingenting ble hoppet over. Printes paa BEGGE flater: --live-dry-run og
   den fulle enkeltkjoeringen (en kjoering som PRODUSERTE et forslag fra en halvlest
   base er der tausheten kostet mest).

Defaulten er MOTSATT forrige ordres, og forskjellen er innsikten: cost_baseline_anchored
er paakrevd fordi begge defaults lyver, mens en TOM tuppel her er et aerlig positivt
utsagn ("hver lenke ble fulgt") - external_calls-presedensen. Vei-stien navigerer ingen
base, saa tom er bokstavelig sant der ogsaa.

Sporet bor paa RunResult.skipped_links (RUN-nivaa: navigasjonen skjer EN gang per
kjoering, foer noe forslag finnes), aldri paa ProvenanceStamp, som beskriver gaten som
doemte EN kandidat. Ingenting av dette naar bundle_context - derfor er de commons-eide
nav-goldenene byte-uendret, og Bundle( har fortsatt EN konstruksjons-sted (maalt).

Load-bearing MAALT (tests/test_navigation_visibility_loadbearing.py), aatte mutasjoner
alle roede mot HELE suiten + groenn kontroll 897 passed / 5 skipped:
  detach missing-registreringen (6 roede) · detach outside-bundle (2) · kollaps de to
  grunnene til en (2) · registrer dedup-grenen (1) · renderer returnerer alltid linja
  (3, inkl. kontrollene - omisjonen er selv gatet) · detach dry-run-printen (1) ·
  detach full-run-printen (1) · konstant tom trace ut av run_project (4).

Docs rettet der de paasto det motsatte: kunnskapsbase-for-en-kjoring.md §5.7 + §6,
presentasjon-bygge-kunnskapsbase.html (steg 8, steg 9, fallgruve 3, avslutningen),
README-ens navigasjonsavsnitt, og CLAUDE.md-ens navigasjons-kontrakt-invariant.
This commit is contained in:
Kjell Tore Guttormsen 2026-08-21 17:18:18 +02:00
commit 56c48f6f65
7 changed files with 547 additions and 28 deletions

View file

@ -63,6 +63,30 @@ Python ≥3.10. MAF (`agent-framework-core` 1.9.0). Pakkehåndtering: `uv`. To b
commons-eide nav-goldens (`shared/examples/nav-golden-*/expected-read-context.md`, byte-nivå
fasit): `test_nav_golden_hierarchy_*` (positiv) + `test_nav_golden_escape_*` (negativ — en gate
som bare kan bli grønn beviser ingenting).
**Et hopp er TOLERERT, men ikke lenger TAUST (21.08):** `_walk` registrerer hver lenke den ikke
fulgte på `Bundle.skipped` — hvilken fil lenken sto i, lenketeksten ORDRETT (operatøren redigerer
den teksten, ikke den resolverte stien), og hvilken av de TO grunnene som gjaldt: `outside-bundle`
(escape — ofte bevisst, en lenke til nabobasen) eller `missing` (inne i basen, ingen lesbar fil —
nesten alltid en skrivefeil). Den tredje grenen, `canonical in seen`, er DEDUP og registreres
ALDRI — den er korrekt navigasjon og dét som terminerer sykler; en implementasjon som logget hvert
`continue` ville rapportert en frisk base som halvlest. Toleransen er URØRT (§4 krever at det ikke
kastes) — dette er synlighet, ikke en ny nekt. **Feltet DEFAULTER til tom tuppel, og det er
MOTSATT av `cost_baseline_anchored`s «påkrevd uten default»:** en tom trace er et ærlig POSITIVT
utsagn («hver lenke ble fulgt», `external_calls`-presedensen), mens en manglende bool måtte påstå
noe om en hendelse og begge påstandene ville iblant vært usanne. Sporet forlater kjøringen på
`RunResult.skipped_links` (RUN-nivå — navigasjonen skjer ÉN gang per kjøring, før noe forslag
finnes) og `DryRunReport.skipped_links`, aldri på `ProvenanceStamp`, som beskriver gaten som dømte
ÉN kandidat. `run.skipped_links_notice` er ENESTE renderer, tar den alt oppløste tuppelen og
returnerer `None` når ingenting ble hoppet over (omisjon, aldri tom rad — `announce`-regelen);
reason-TOKENET printes rått, så det finnes ingen andre display-vokabular å drifte fra feltet.
Ingenting av dette når `bundle_context` (som bygges av `index_summary` + `context_files` alene) —
dét er hva som holder nav-goldenene byte-uendret, og `Bundle(` har fortsatt ÉN konstruksjons-sted
(`okf.py`, i `navigate_bundle`). Load-bearing MÅLT
(`tests/test_navigation_visibility_loadbearing.py`), åtte mutasjoner alle røde mot HELE suiten +
grønn kontroll 897/5: detach `missing`-registreringen (6 røde) · detach `outside-bundle` (2 røde) ·
kollaps de to grunnene til én (2 røde) · registrer dedup-grenen (1 rød) · renderer returnerer alltid
linja (3 røde — inkl. kontrollene, altså er omisjonen selv gatet) · detach dry-run-printen (1 rød) ·
detach full-run-printen (1 rød) · konstant tom trace ut av `run_project` (4 røde).
- **Kuraterte skrivere kan ikke forfalske ingest-stempelet** (`ingest-spec` §3): `write_concept_file`
er repoets ene authoring-primitiv som materialiserer en konseptfil fra CALLER-oppgitt frontmatter,
og avviser derfor det KOMPLETTE eierskaps-stempelet (`generated: true` + `ingest_manifest`) med

View file

@ -300,6 +300,13 @@ cross-links, with progressive disclosure — never by keyword retrieval or stuff
bundle into a prompt. Query-time retrieval against the bundle is explicitly forbidden by the
method spec: it would leak the verdict layer around the learning gate.
A cross-link that cannot be followed is **tolerated** (OKF SPEC §4 — navigation never raises) but
no longer **silent**: each one is recorded on `Bundle.skipped` with the file it was written in, the
link text verbatim, and which of the two reasons applied (`missing` — resolves inside the bundle
with no readable file there; `outside-bundle` — resolves outside the bundle root). `--live-dry-run`
and a full run both print the list, and print nothing when every link was followed — so a bundle
that was only half read stops looking like a bundle that was simply smaller.
## AI-first, humans on top
A traditional wiki is built for *people* — optimized for humans finding and reading

View file

@ -496,9 +496,25 @@ modellene er oppløst, takene er satt, og ingen ekstern tjeneste vil bli kontakt
skjer før kuttet, uten et eneste modellkall (VERIFISERT: `run.py:513-516` ligger før
dry-run-returen på `:565-588`).
**Hva `OK` sier fra om:** en lenke kjøringen ikke klarte å følge. Tørrkjøringen skriver da én
blokk under `OK`-linja — én linje per lenke, med filen lenken sto i, lenketeksten ordrett, og
hvorfor: `missing` (målet ligger inne i basen, men det er ingen lesbar fil der — nesten alltid en
skrivefeil i lenken) eller `outside-bundle` (målet peker ut av basen). Er alle lenkene fulgt,
skrives ingen linje i det hele tatt. Målt på en base med begge feilene:
```
Knowledge base: 2 cross-link(s) NOT followed — the agents never read the document(s) behind them:
- index.md -> metodikk-som-aldri-ble-skrevet.md (missing)
- index.md -> ../nabobase/index.md (outside-bundle)
```
Selve toleransen er uendret: en brutt lenke stopper ikke kjøringen (OKF SPEC §4 krever det). Det
som er nytt er at den ikke lenger er taus — før dette var «dokumentet ble aldri skrevet» og «lenken
til det var feil» umulig å skille utenfra. Den samme blokka skrives på en full kjøring.
**Hva `OK` ikke beviser:** at `cost-baseline.json` *finnes* (se [§4.1](#41-den-skarpeste-mangelen-cost-baselinejson)
— sjekk det med `ls`), at lenkene i `index.md` når alle filene (en brutt lenke hoppes stille over),
eller at innholdet er godt. `--docs-dir` må oppgis, men leses ikke på bundle-stien (VERIFISERT:
— sjekk det med `ls`), at filene faktisk er lenket fra `index.md` (en fil ingen lenker til er
unåbar, og da finnes det ingen lenke å rapportere), eller at innholdet er godt. `--docs-dir` må oppgis, men leses ikke på bundle-stien (VERIFISERT:
`run.py:513-530` bruker bare `bundle_dir`; `:1504` krever flagget likevel). Pek den på samme
mappe.
@ -538,10 +554,11 @@ så dokumentet ikke lover mer enn det som kan leveres.
tilnærming i bestillingen (VERIFISERT: `run.py:634-638`; én `expel_query` for hele
kjøringen). En bestilling med to tilnærminger mot to ulike kostlinjer får tidligere dommer
hentet for den kandidaten `validator-input.json` beskriver.
- **Det finnes ingen «er basen klar?»-kommando.** `--live-dry-run` beviser at basen *åpner*, og
sier nå fra hvis `cost-baseline.json` mangler — men ingenting om brutte lenker, og ingenting om
innholdets kvalitet. `preflight` sjekker Azure-konfigurasjon, ikke basen (VERIFISERT:
`preflight.py` docstring).
- **Det finnes ingen «er basen klar?»-kommando.** `--live-dry-run` beviser at basen *åpner*, sier
fra hvis `cost-baseline.json` mangler, og lister lenkene den ikke klarte å følge
([§5.7](#57-kjøreklar-sjekken)) — men ingenting om innholdets kvalitet, og ingenting om filer
ingen lenker til (de er unåbare, så det finnes ingen brutt lenke å rapportere). `preflight`
sjekker Azure-konfigurasjon, ikke basen (VERIFISERT: `preflight.py` docstring).
- **Uforankret kjøring er synlig, men ikke summert.** Feltet og linja finnes per kjøring
([§4.1](#41-den-skarpeste-mangelen-cost-baselinejson)); det finnes ingen rapport som teller opp
hvor mange kjøringer i et porteføljepass som gikk uforankret.

View file

@ -313,7 +313,7 @@
du vil at det skal se.</p>
<div class="done"><b>Ferdig når</b>Hvert dokument du har laget er lenket. Et dokument uten lenke
eksisterer ikke, uansett hvor det ligger. Og trenger du fagstoff fra en annen base: kopier det
inn — lenker ut av basen følges aldri, uten feilmelding.</div>
inn — lenker ut av basen følges aldri, men tørrkjøringen lister dem, så du ser at det skjedde.</div>
</section>
<section class="slide">
@ -323,15 +323,19 @@
stopper der — den koster ingenting, og kan kjøres hver gang basen endres.</p>
<table>
<tr><th>Tørrkjøringen beviser</th><th>Den beviser ikke</th></tr>
<tr><td>at basen åpner</td><td>at lenkene når alle dokumentene</td></tr>
<tr><td>at navnet stemmer overalt</td><td>at innholdet er godt</td></tr>
<tr><td>at kostnadsgrunnlaget er lesbart</td><td>at forslagene blir kloke</td></tr>
<tr><td>at basen åpner</td><td>at innholdet er godt</td></tr>
<tr><td>at navnet stemmer overalt</td><td>at forslagene blir kloke</td></tr>
<tr><td>at kostnadsgrunnlaget er lesbart</td><td>at alt du laget faktisk er lenket</td></tr>
<tr><td>hvilke lenker den ikke klarte å følge</td><td></td></tr>
<tr><td>at ingen ekstern tjeneste kontaktes</td><td></td></tr>
</table>
<p>Mangler kostnadsgrunnlaget, sier tørrkjøringen fra. Er en lenke brutt, hoppes dokumentet stille
over — det er den ene feilen du må lete etter selv.</p>
<div class="done"><b>Ferdig når</b>Tørrkjøringen er grønn, og du har lest gjennom listen over
dokumenter den faktisk fant.</div>
<p>Mangler kostnadsgrunnlaget, sier tørrkjøringen fra. Er en lenke brutt, stopper den fortsatt
ikke kjøringen — men den skrives ut, med dokumentet den pekte på og hvorfor den ikke ble fulgt
(<code>missing</code> = ingen fil der, <code>outside-bundle</code> = peker ut av basen). Er alle
lenker fulgt, står det ingenting. Den feilen den fortsatt ikke kan se, er dokumentet
<em>ingen</em> lenker til — da finnes det ingen lenke å rapportere.</p>
<div class="done"><b>Ferdig når</b>Tørrkjøringen er grønn, den lister ingen lenker den ikke fulgte,
og du har lest gjennom listen over dokumenter den faktisk fant.</div>
</section>
<section class="slide">
@ -416,8 +420,10 @@
dømmer bare mot tall forslaget fant på selv. Dette er den dyreste feilen, fordi den er usynlig.</li>
<li><strong>Gjennomførte tiltak står ikke i basen.</strong> Systemet foreslår dem på nytt, og
besparelsen dobbelttelles.</li>
<li><strong>Dokumenter uten lenke.</strong> De leses aldri, og ingen feilmelding sier fra.
Symptomet er at forslagene virker uinformerte uten at noen skjønner hvorfor.</li>
<li><strong>Dokumenter uten lenke.</strong> De leses aldri, og ingen feilmelding sier fra —
for det finnes ingen lenke å klage på. Er lenken der, men feil, sier tørrkjøringen nå fra;
er den aldri skrevet, er dokumentet usynlig. Symptomet er at forslagene virker uinformerte
uten at noen skjønner hvorfor.</li>
<li><strong>For mye materiale.</strong> Alt som lenkes leses i sin helhet. Ti sider støy koster
like mye oppmerksomhet som ti sider substans.</li>
<li><strong>Utledede tall.</strong> Et tall ingen kan peke på en kilde for, forurenser alt som
@ -433,8 +439,9 @@
kostnadstallene og å få en fagperson til å skrive ned rammene sine.</li>
<li><strong>Det finnes ingen automatikk</strong> som lager basen av regnearkene deres. Det er
håndarbeid, og det er derfor det tar tid.</li>
<li><strong>Det finnes ingen «er basen klar»-knapp.</strong> Tørrkjøringen er det nærmeste, og
den sier ingenting om brutte lenker eller om innholdet er godt.</li>
<li><strong>Det finnes ingen «er basen klar»-knapp.</strong> Tørrkjøringen er det nærmeste: den
sier fra om kostnadsgrunnlaget mangler og lister lenkene den ikke klarte å følge, men den sier
ingenting om et dokument ingen har lenket til, og ingenting om innholdet er godt.</li>
<li><strong>Systemet dømmer ikke.</strong> Det foreslår og kontrollerer regnestykket.
Vurderingen av om tiltaket er klokt, gjør fagpersonen — og den vurderingen er det basen lærer av.</li>
</ul>

View file

@ -19,6 +19,12 @@ fields. A target that fails to resolve for ANY reason (missing file, invalid pat
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
@ -28,7 +34,7 @@ import posixpath
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from typing import Any, Literal
from portfolio_optimiser.ir import CostBaseline
from portfolio_optimiser.retrieval import PathSecurityError, safe_resolve
@ -94,12 +100,55 @@ class BundleFile:
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 (-(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
@dataclass(frozen=True)
class Bundle:
"""A navigated OKF bundle: ``index.md`` plus every cross-linked file that resolves."""
"""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:
@ -165,29 +214,54 @@ def _resolve_target(bundle_dir: str, from_name: str, target: str) -> tuple[str,
return None
def _walk(bundle_dir: str, current: BundleFile, files: list[BundleFile], seen: set[str]) -> 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.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
continue # de-duplication / cycle termination: correct navigation, NOT a skip
seen.add(canonical)
linked = _load_file(bundle_dir, rel)
if linked is None:
continue # broken link: tolerated, never raised (OKF §4)
# 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)
_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).
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
@ -197,10 +271,11 @@ def navigate_bundle(bundle_dir: str) -> Bundle:
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)
return Bundle(dir=bundle_dir, files=tuple(files))
_walk(bundle_dir, index, files, seen, skipped)
return Bundle(dir=bundle_dir, files=tuple(files), skipped=tuple(skipped))
def bundle_context(bundle: Bundle, *, dimension: str | None = None) -> str:

View file

@ -140,6 +140,12 @@ class RunResult:
#: what ``_evaluate_mandate`` deliberately avoids. It defaults, so every existing constructor
#: call is unaffected (mirrors ``coverage``).
refinements: tuple[Rejection, ...] = ()
#: Every cross-link the bundle navigation could not follow. A RUN-level fact, carried here and
#: NOT on ``provenance``: navigation happens ONCE per run, before any proposal exists, and the
#: same walk backs every refinement attempt — whereas ``ProvenanceStamp.cost_baseline_anchored``
#: describes the gate that judged ONE candidate. EMPTY on the road path (no bundle is navigated)
#: and on any bundle that was read whole; it defaults for the same reason ``coverage`` does.
skipped_links: tuple[okf.SkippedLink, ...] = ()
@dataclass(frozen=True)
@ -177,6 +183,12 @@ class DryRunReport:
#: because a dry run stops before any proposal exists, so there is no stamp to read it off —
#: and this surface is precisely where the un-anchored case was measured to be silent.
cost_baseline_anchored: bool
#: Every cross-link the bundle navigation could not follow (``okf.Bundle.skipped``). EMPTY is a
#: positive statement — "every cross-link was followed" — which is why it DEFAULTS, unlike
#: ``cost_baseline_anchored`` above: a missing bool would have to claim something about an event
#: (and both claims would sometimes be false), while a missing trace asserts only that the event
#: list is empty. The road path navigates no bundle, so empty is literally true there too.
skipped_links: tuple[okf.SkippedLink, ...] = ()
@dataclass(frozen=True)
@ -460,6 +472,39 @@ def cost_baseline_notice(anchored: bool) -> str | None:
return None if anchored else _UNANCHORED_NOTICE
def skipped_links_notice(skipped: tuple[okf.SkippedLink, ...]) -> str | None:
"""Render what the run could NOT read, or ``None`` when every cross-link was followed.
The measured silence this closes: ``okf._walk`` tolerates an unfollowable link exactly as OKF
SPEC §4 requires (skip, never raise) correct, and unchanged here but it left no trace, so a
knowledge base whose other half was never reached looked identical to one where those documents
were never written, and ``--live-dry-run`` exited 0 over both.
ONE renderer with N callsites, never N copies of the wording (-(p)), and it takes the
already-resolved trace rather than a bundle path: a renderer that re-navigated the bundle would
be a second resolution of the same walk, free to disagree with the run it describes. Both
callsites read it off the value ``run_project`` returned from its ONE
``okf.navigate_bundle`` call.
``None`` when the trace is empty omission, never an empty row (``mandate.announce``'s rule,
the same one ``cost_baseline_notice`` follows). A run that reached everything has nothing to
report.
The per-link line prints the reason TOKEN itself rather than a prose translation of it: a second
display vocabulary keyed off ``SkipReason`` would be the duplicate free to drift, and the token
is already the operative word ("missing" vs "outside-bundle"). English, like every other line
this CLI prints; the Norwegian explanation belongs in
``docs/kunnskapsbase-for-en-kjoring.md``, next to the domain expert."""
if not skipped:
return None
lines = [
f" Knowledge base: {len(skipped)} cross-link(s) NOT followed — "
"the agents never read the document(s) behind them:"
]
lines += [f" - {s.from_file} -> {s.target} ({s.reason})" for s in skipped]
return "\n".join(lines)
async def run_project(
project_id: str,
profile: Profile | str = Profile.LOCAL,
@ -555,6 +600,9 @@ async def run_project(
# dimension=None keeps the full context, byte-identical to before.
context = okf.bundle_context(bundle, dimension=dimension.id if dimension else None)
citations = bundle_citations(bundle)
# What the navigation could NOT reach, taken from the run's ONE walk. The road path below
# navigates no bundle at all, so its empty tuple is literally true rather than a stand-in.
skipped_links: tuple[okf.SkippedLink, ...] = bundle.skipped
debate_tools: list[Any] = []
else:
project = _project_by_id(project_id)
@ -562,6 +610,7 @@ async def run_project(
chunks = retrieve_chunks("cost saving measure", docs_dir, top_k)
citations = [chunk_dict_to_citation(c) for c in chunks]
context = "\n".join(c["snippet"] for c in chunks)
skipped_links = ()
debate_tools = [make_retrieval_tool(docs_dir, top_k=top_k)]
# Trekk B2 (krav 3): configured MCP servers become tools the AGENTS can call during the debate.
@ -623,6 +672,7 @@ async def run_project(
max_tokens=max_tokens,
top_k=top_k,
cost_baseline_anchored=baseline is not None,
skipped_links=skipped_links,
)
# The MCP lifecycle (Trekk B2): entered HERE, after the dry-run cut above, so a dry run never
# opens a connection — its promise to stop before the first call covers egress too. Constructed
@ -861,6 +911,7 @@ async def run_project(
checker_verdict=checker_decision,
coverage=coverage,
refinements=tuple(refinements),
skipped_links=skipped_links,
)
@ -1833,6 +1884,11 @@ def main(argv: list[str] | None = None) -> int:
notice = cost_baseline_notice(report.cost_baseline_anchored)
if notice is not None:
print(notice)
# The second measured silence on this surface: a bundle with an unfollowable cross-link
# dry-ran to rc 0 with nothing said, so a half-read base looked exactly like a small one.
nav_notice = skipped_links_notice(report.skipped_links)
if nav_notice is not None:
print(nav_notice)
return 0
try:
@ -1875,6 +1931,11 @@ def main(argv: list[str] | None = None) -> int:
notice = cost_baseline_notice(result.provenance.cost_baseline_anchored)
if notice is not None:
print(notice)
# Same renderer on the full run, and deliberately so: a run that PRODUCED a proposal from a
# half-read base is where the silence cost the most — the dry run at least produced nothing.
nav_notice = skipped_links_notice(result.skipped_links)
if nav_notice is not None:
print(nav_notice)
# The settlement against the commission (Trekk A4). Empty without a mandate, so an
# un-commissioned run prints exactly what it printed before.
settlement = settle(result.coverage)

View file

@ -0,0 +1,328 @@
"""A knowledge base that was only HALF read SAYS so — a structured trace, and one line when it fired.
``okf._walk`` tolerates a cross-link it cannot follow, exactly as OKF SPEC §4 requires: it skips and
never raises. **That tolerance is correct and is not what this file changes.** What it changes is
that the skip left NO TRACE. ``navigate_bundle`` returned only the files it FOUND, so a base whose
other half was never reached is indistinguishable from a base where those documents were never
written and ``--live-dry-run`` exited 0 in silence over both.
Measured (session 51, 2026-08-21, against ``b639722``): ``okf.py:182`` was
``continue # broken link: tolerated, never raised (OKF §4)``, ``Bundle`` carried ``dir`` and
``files`` alone, and ``grep -rn "Bundle(" src tests`` returned exactly ONE construction site
(``okf.py:203``, inside ``navigate_bundle``) so the trace has a single producer and cannot be
forged by a second constructor.
Same defect class and same SHAPE as order ``20260821T092039Z`` (visible un-anchoring, delivered in
``156312c``): a tolerance that is right, plus an absence that is not.
Two teeth:
1. ``okf.SkippedLink`` + ``Bundle.skipped`` a STRUCTURED trace, never a string, because "which
document is missing" and "why" are two different operative questions (kø-(y)). ``_walk`` has TWO
distinct skip reasons and they mean different things: ``outside-bundle`` (the target resolves
outside the bundle root often a deliberate link to a neighbouring base) and ``missing`` (it
resolves INSIDE and no readable file is there almost always a typo in the link). The third
branch, ``canonical in seen``, is DE-DUPLICATION: correct behaviour, also what terminates cycles,
and never a skip arm (d) exists to keep it out of the trace.
2. ``run.skipped_links_notice`` ONE renderer, taking the already-resolved value, returning ``None``
when nothing was skipped (omission, never an empty row ``mandate.announce``'s rule, reused by
``cost_baseline_notice``).
**The default DIFFERS from the previous order's, and that difference is the insight.**
``ProvenanceStamp.cost_baseline_anchored`` is REQUIRED with no default because both defaults lie:
``True`` claims an anchoring that may never have happened, ``False`` under-claims a real one. Here
the honest reading is the opposite: an EMPTY tuple is a positive statement "every cross-link was
followed" — in the same class as ``ProvenanceStamp.external_calls`` ("nothing outside this process
was contacted"). A caller that constructs a ``Bundle`` without a trace is not withholding a fact; it
is stating one. So ``skipped`` defaults to ``()``, and the road path (which navigates nothing) is
honestly empty rather than dishonestly required to invent a value.
Arms:
(a) a MISSING target yields exactly one entry, carrying its own reason;
(b) an ESCAPING target yields exactly one entry, carrying the OTHER reason asserted on the
structured ``reason`` field, never on shared prose (the 08-09 class), and the two reasons are
asserted to DIFFER so a single collapsed reason cannot pass both;
(c) an intact base yields an EMPTY tuple and NO line (the control without it (a) passes on a
constant), with the navigation proved to have happened first;
(d) the dedup branch (a repeated link, and a cycle) yields NO entry at all;
(e) both CLI surfaces carry it ``--live-dry-run`` and the full run plus the typed carriers.
The commons-owned ``nav-golden-escape`` fasit is a free independent witness that the SEMANTICS did
not move: every link but one escapes there, and it must still render byte-identically.
"""
from __future__ import annotations
import json
import shutil
from pathlib import Path
import pytest
from conftest import SyntheticUsageChatClient
from portfolio_optimiser import okf, run
from portfolio_optimiser.okf import SkippedLink
from portfolio_optimiser.run import DryRunReport, RunResult, run_project, skipped_links_notice
_DATA = Path(__file__).resolve().parents[1] / "src" / "portfolio_optimiser" / "data" / "bundles"
INTACT_BUNDLE = _DATA / "bygg-energi-mikro-a"
_VERDICT_INPUT = {"decision": "approved", "rationale": "expert reviewed (sim)"}
#: A reply the pre-amendment fixture's un-anchored gate accepts far enough to produce an outcome.
_REPLY = json.dumps(
{
"measure": "LED-retrofit",
"affected_items": [{"code": "ENERGI-TOTAL-EL", "quantity": 180000, "unit_cost": 1.0}],
"claimed_saving_nok": 30000,
}
)
#: The phrase the notice carries and an intact run cannot: an intact run prints NO line at all.
_SENTINEL = "NOT followed"
#: The link an arm appends to a COPY of the fixture index — a name nothing in the bundle provides.
_DANGLING = "fantes-aldri.md"
def _factory(reply: str = _REPLY):
def factory(role: str):
return SyntheticUsageChatClient(default_reply=reply)
return factory
@pytest.fixture(autouse=True)
def _isolate_model_env(monkeypatch: pytest.MonkeyPatch) -> None:
"""Hermetic env: the operator's Foundry overrides must not reach the CLI arms."""
monkeypatch.delenv("PORTFOLIO_MODEL_MAP", raising=False)
monkeypatch.delenv("PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT", raising=False)
def _write(path: Path, body: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(body, encoding="utf-8")
def _bundle_copy(tmp_path: Path, *, dangling: bool) -> Path:
"""A throwaway copy of the repo-local fixture — the shipped bundle is never touched. With
``dangling`` the root index gains ONE link to a file that does not exist."""
dest = tmp_path / "bundle"
shutil.copytree(INTACT_BUNDLE, dest)
if dangling:
index = dest / "index.md"
index.write_text(
index.read_text(encoding="utf-8") + f"\n- [Borte]({_DANGLING})\n", encoding="utf-8"
)
return dest
# --- Arm (a): a MISSING target is recorded, with its own reason -----------------------------------
def test_missing_target_is_recorded_as_one_entry(tmp_path) -> None:
"""RED: the base still navigates and still raises nothing (§4 tolerance UNTOUCHED) — but the
link it could not follow is now on the bundle, naming the file it was written in, the link text
VERBATIM, and why. Detach the recording and the walk goes silent again."""
_write(tmp_path / "index.md", "---\ntype: index\n---\n\n- [Borte](fantes-aldri.md)\n")
bundle = okf.navigate_bundle(str(tmp_path))
assert [f.name for f in bundle.files] == ["index.md"] # tolerated, not raised
assert bundle.skipped == (
SkippedLink(from_file="index.md", target="fantes-aldri.md", reason="missing"),
)
# --- Arm (b): an ESCAPING target is recorded, with the OTHER reason -------------------------------
def test_escaping_target_is_recorded_with_a_different_reason(tmp_path) -> None:
"""The two skip branches mean different things and must not be collapsed: this target EXISTS,
one level up, and is refused because it is outside the bundle root not because it is absent.
Asserted on the structured ``reason``, never on prose the two branches could share."""
_write(tmp_path / "outside.md", "---\ntype: reference\n---\n\nSHOULD NOT BE READ\n")
root = tmp_path / "bundle"
_write(root / "index.md", "---\ntype: index\n---\n\n- [Ute](../outside.md)\n")
bundle = okf.navigate_bundle(str(root))
assert [f.name for f in bundle.files] == ["index.md"]
assert bundle.skipped == (
SkippedLink(from_file="index.md", target="../outside.md", reason="outside-bundle"),
)
def test_the_two_reasons_are_distinct_values(tmp_path) -> None:
"""A single collapsed reason would pass BOTH arms above if they were read in isolation. This
pins the discrimination itself: same shape of bundle, two skips, two different values."""
_write(tmp_path / "outside.md", "---\ntype: reference\n---\n\nx\n")
root = tmp_path / "bundle"
_write(
root / "index.md",
"---\ntype: index\n---\n\n- [Borte](fantes-aldri.md)\n- [Ute](../outside.md)\n",
)
reasons = [s.reason for s in okf.navigate_bundle(str(root)).skipped]
assert len(reasons) == 2
assert reasons[0] != reasons[1]
# --- Arm (c): the control — an intact base records nothing and prints nothing ---------------------
def test_intact_bundle_records_nothing(tmp_path) -> None:
"""Causality control. Without it arm (a) would pass on an implementation that records a constant
entry for every base. The navigation is proved to have HAPPENED first (two files reached), so
the empty trace is a measured absence rather than a base that was never walked."""
_write(tmp_path / "index.md", "---\ntype: index\n---\n\n- [A](a.md)\n")
_write(tmp_path / "a.md", "---\ntype: project\n---\n\nA body\n")
bundle = okf.navigate_bundle(str(tmp_path))
assert [f.name for f in bundle.files] == ["index.md", "a.md"] # the walk really ran
assert bundle.skipped == ()
assert skipped_links_notice(bundle.skipped) is None
# --- Arm (d): de-duplication is NOT a skip --------------------------------------------------------
def test_dedup_and_cycles_produce_no_entry(tmp_path) -> None:
"""``canonical in seen`` is correct behaviour, not a failure: it is what makes a repeated link
one entry and what terminates a cycle. An implementation that recorded every ``continue`` would
report a healthy base as half-unread. Both forms are exercised: ``a.md`` is linked twice from
the index (once as ``./a.md``, deduped on the RESOLVED path) and links back to the index."""
_write(tmp_path / "index.md", "---\ntype: index\n---\n\n- [A](a.md)\n- [A again](./a.md)\n")
_write(tmp_path / "a.md", "---\ntype: project\n---\n\nA body\n\n- [Back](index.md)\n")
bundle = okf.navigate_bundle(str(tmp_path))
assert [f.name for f in bundle.files] == ["index.md", "a.md"] # deduped + cycle terminated
assert bundle.skipped == ()
# --- Arm (e): the renderer and both CLI surfaces --------------------------------------------------
def test_notice_is_rendered_only_when_something_was_skipped() -> None:
"""One renderer, two branches sharing NO wording: a non-empty trace returns a line carrying the
sentinel AND the operative facts; an empty trace returns ``None`` (omitted, never an empty row).
The reason token printed is the STRUCTURED value itself, so there is no second display
vocabulary free to drift from the field (-(p))."""
rendered = skipped_links_notice(
(SkippedLink(from_file="index.md", target="fantes-aldri.md", reason="missing"),)
)
assert rendered is not None
assert _SENTINEL in rendered
assert "index.md" in rendered
assert "fantes-aldri.md" in rendered
assert "missing" in rendered
assert skipped_links_notice(()) is None
async def test_dry_run_report_carries_the_trace(tmp_path, fresh_store) -> None:
"""The dry-run type is the carrier for the surface the order measured: a run that stops before
the first model call already knows what it could not read."""
assert "skipped_links" in DryRunReport.__dataclass_fields__
bundle = _bundle_copy(tmp_path, dangling=True)
report = await run_project(
"BYGG-ENERGI-MIKRO-A",
"local",
docs_dir=str(bundle),
bundle_dir=str(bundle),
verdict_input=_VERDICT_INPUT,
client_factory=_factory(),
store=fresh_store,
live_dry_run=True,
)
assert isinstance(report, DryRunReport)
assert [s.target for s in report.skipped_links] == [_DANGLING]
async def test_run_result_carries_the_trace(tmp_path, fresh_store) -> None:
"""The full run too: navigation happens ONCE per run, before any proposal exists, so the trace
is a RUN-level fact carried on ``RunResult`` not on the per-proposal ``ProvenanceStamp``,
which describes the gate that judged one candidate."""
assert "skipped_links" in RunResult.__dataclass_fields__
bundle = _bundle_copy(tmp_path, dangling=True)
result = await run_project(
"BYGG-ENERGI-MIKRO-A",
"local",
docs_dir=str(bundle),
bundle_dir=str(bundle),
verdict_input=_VERDICT_INPUT,
client_factory=_factory(),
store=fresh_store,
)
assert [s.target for s in result.skipped_links] == [_DANGLING]
async def test_road_path_has_an_empty_trace(docs_dir, fresh_store) -> None:
"""The road path navigates no bundle, so "nothing was skipped" is literally true there — which
is exactly why the empty tuple is an honest DEFAULT rather than a withheld fact."""
result = await run_project(
"FV42-GSV-E1",
"local",
docs_dir=docs_dir,
verdict_input=_VERDICT_INPUT,
client_factory=_factory(
json.dumps(
{
"measure": "Reduce scope",
"affected_items": [{"code": "05.2", "quantity": 4300.0, "unit_cost": 215.0}],
"claimed_saving_nok": 200000.0,
}
)
),
store=fresh_store,
)
assert result.skipped_links == ()
def _dry_run_argv(bundle: Path) -> list[str]:
return [
"BYGG-ENERGI-MIKRO-A",
"--docs-dir",
str(bundle),
"--bundle-dir",
str(bundle),
"--live-dry-run",
]
def test_cli_dry_run_announces_the_skipped_link(tmp_path, capsys) -> None:
"""RED (the measured defect, verbatim): a dry run over a base with an unfollowable cross-link
exited 0 with nothing said. It now names the document it never reached."""
rc = run.main(_dry_run_argv(_bundle_copy(tmp_path, dangling=True)))
assert rc == 0
out = capsys.readouterr().out
assert _SENTINEL in out
assert _DANGLING in out
def test_cli_dry_run_says_nothing_when_every_link_was_followed(tmp_path, capsys) -> None:
"""Control: the same base with its links intact prints NO navigation line at all. A line for
something the run does not have is omitted, never rendered blank."""
rc = run.main(_dry_run_argv(_bundle_copy(tmp_path, dangling=False)))
assert rc == 0
out = capsys.readouterr().out
assert _SENTINEL not in out
assert "Knowledge base:" not in out
def test_cli_full_run_announces_the_skipped_link(tmp_path, capsys) -> None:
"""The full-run surface too, through the offline scripted door — so the notice is a property of
a RUN, not of the dry-run branch alone. A run that PRODUCED a proposal from a half-read base is
the case where the silence cost the most."""
bundle = _bundle_copy(tmp_path, dangling=True)
replies = tmp_path / "replies.json"
replies.write_text(
json.dumps({"proposer": _REPLY, "checker": "Holder. VERDICT: APPROVE"}), encoding="utf-8"
)
rc = run.main(
[
"BYGG-ENERGI-MIKRO-A",
"--docs-dir",
str(bundle),
"--bundle-dir",
str(bundle),
"--scripted-replies",
str(replies),
]
)
assert rc == 0
out = capsys.readouterr().out
assert _SENTINEL in out
assert _DANGLING in out