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