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:
parent
b6397229ca
commit
56c48f6f65
7 changed files with 547 additions and 28 deletions
|
|
@ -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 (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
|
||||
|
||||
|
||||
@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:
|
||||
|
|
|
|||
|
|
@ -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 (kø-(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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue