portfolio-optimiser/tests/test_navigation_visibility_loadbearing.py
Kjell Tore Guttormsen 56c48f6f65 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.
2026-08-21 17:18:18 +02:00

328 lines
15 KiB
Python

"""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 (kø-(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