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

@ -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)