"""P21 DEL C — a declaration that must have LOOKED, and a refusal that names the neighbours. **C1, and the order asked for a measurement rather than a preference.** P19 DEL A made a direction name the requirement that binds it, and the rung works — round 4 produced 13 declarations over six runs. Not one named a fasit concept, and the distinct documents opened before each were ``1,1,1,1,1,1,1,2,5,5,6,13,13``: seven declared the base's FIRST requirement after opening exactly ONE document. The order offered two rules and asked which discriminates. Replayed against the real listings: * **"the declared document must have been returned by a ``read_dir`` filtered on a word from the approach's label"** refuses **13 of 13** — including Søråsen's ``12.11``, which the order names as the closest any run came. Measured, ZERO of the 13 declarations were reached through a filtered listing at all. A gate that refuses every measured case, right and wrong alike, cannot discriminate; it is the vacuous gate's mirror image. * **"fewer than k distinct documents opened"** at k=3 refuses **8 of 13** and keeps the five that navigated, ``12.11`` among them. k=3, 4 and 5 refuse the SAME eight — the distribution has a gap between 2 and 5 — so the threshold is not on a cliff, and 3 is the lowest of that plateau. The second is built. It is capped by the base's own document count, so a two-document base stays declarable rather than becoming a level nobody can declare on. **C2.** Over the same six traces, 18 of 143 path-bearing tool calls named a path the base does not hold, and ELEVEN of them are one run walking ``R761/4-3``, ``4.3``, ``4-2``, ``4-1``, ``4-0``, ``4-5``, ``4-6`` — guessing a chapter-number spelling the corpus does not use, while the real names are ``R761/4``, ``R761/41``, ``R761/42``. The refusal already named the nearest LISTABLE ancestor, which is the right rung; what it could not say is which of that rung's names was meant. Measured after: **16 of 18** refusals now name at least one neighbour, and the two that do not are paths whose ancestor holds documents and no subdirectories — an empty list is omitted rather than rendered as a sentence with nothing in it. """ from __future__ import annotations from pathlib import Path from typing import Any import pytest from portfolio_optimiser import okf from portfolio_optimiser.explore import ToolCall, navigator_tools _EXAMPLES = Path(__file__).resolve().parents[1] / "shared" / "examples" _TUNNEL = _EXAMPLES / "tunnel-hauglia" def _wired(bundle_dir: Path = _TUNNEL) -> tuple[dict[str, Any], list[ToolCall], list[Any]]: opened: list[ToolCall] = [] declared: list[Any] = [] tools = navigator_tools((str(bundle_dir),), opened=opened, requirements=declared) return {t.name: t for t in tools}, opened, declared def _concepts(bundle_dir: Path = _TUNNEL) -> list[str]: return [f.name for f in okf.navigate_bundle(str(bundle_dir)).context_files] # --- C1 ------------------------------------------------------------------------------------- def test_c1_a_declaration_after_one_document_is_refused_with_the_denominator() -> None: """The KNOWN-POSITIVE: exactly the measured failure — open one, declare it. Both numbers are in the refusal, because the pair is the diagnosis: "1 of 5" says something "1" alone does not, and a refusal that stated neither would leave the model guessing how much more is wanted (``BudgetExceeded``'s kø-(y) rule, one rung down). """ tools, opened, declared = _wired() path = _concepts()[0] opened.append(ToolCall(name="read_file", bundle_id="tunnel-hauglia", path=path)) answer = tools["declare_requirement"].func( bundle_id="tunnel-hauglia", path=path, ref="Krav 1.1—1" ) assert answer["refusal"] == "RequirementNotRead" assert "1 distinct document(s)" in answer["refused"] assert "of the 5" in answer["refused"], answer["refused"] assert "at least 3" in answer["refused"] assert declared == [], "a refused declaration must leave no record" def test_c1_the_same_declaration_after_three_documents_is_accepted() -> None: """The KNOWN-NEGATIVE, and the half that keeps C1 from being a gate that only refuses. Same base, same declared document, same ``ref`` — the ONLY difference is how much of the base the run had opened. Without this arm the refusal above would be indistinguishable from one that fires on everything, which is exactly what the rule the measurement rejected does. """ tools, opened, declared = _wired() files = _concepts() for name in files[:3]: opened.append(ToolCall(name="read_file", bundle_id="tunnel-hauglia", path=name)) answer = tools["declare_requirement"].func( bundle_id="tunnel-hauglia", path=files[0], ref="Krav 1.1—1" ) assert answer["declared"] is True assert len(declared) == 1 def test_c1_the_same_path_read_three_times_is_still_one_document() -> None: """DISTINCT paths, not calls. Re-reading one document is not navigation, and a rule counting calls would be satisfied by the cheapest possible loop.""" tools, opened, declared = _wired() path = _concepts()[0] for _ in range(3): opened.append(ToolCall(name="read_file", bundle_id="tunnel-hauglia", path=path)) answer = tools["declare_requirement"].func( bundle_id="tunnel-hauglia", path=path, ref="Krav 1.1—1" ) assert answer["refusal"] == "RequirementNotRead" assert "1 distinct document(s)" in answer["refused"] assert declared == [] def test_c1_a_small_base_stays_declarable(tmp_path: Path) -> None: """Capped by the base's OWN size: two documents can be read whole in two. A fixed floor above a base's size would make declaration impossible there — a gate that can only refuse, on exactly the small fixtures this repo is built on. """ base = tmp_path / "mini" base.mkdir() (base / "index.md").write_text( "---\nbundle_id: mini\n---\n\n- [A](a.md) — one.\n- [B](b.md) — two.\n", encoding="utf-8" ) (base / "a.md").write_text("---\ntype: Krav\ntitle: A\n---\n\nbody A\n", encoding="utf-8") (base / "b.md").write_text("---\ntype: Krav\ntitle: B\n---\n\nbody B\n", encoding="utf-8") tools, opened, declared = _wired(base) for name in ("a.md", "b.md"): opened.append(ToolCall(name="read_file", bundle_id="mini", path=name)) answer = tools["declare_requirement"].func(bundle_id="mini", path="a.md", ref="1") assert answer["declared"] is True assert len(declared) == 1 def test_c1_the_never_opened_refusal_still_fires_first() -> None: """The P19 gate is UNCHANGED and still the first thing checked: "you never read this" and "you have barely read anything" are different facts, and the first is the one the caller can fix with one call.""" tools, opened, declared = _wired() files = _concepts() for name in files[:3]: opened.append(ToolCall(name="read_file", bundle_id="tunnel-hauglia", path=name)) answer = tools["declare_requirement"].func( bundle_id="tunnel-hauglia", path=files[4], ref="Krav 1.1—1" ) assert answer["refusal"] == "RequirementNotRead" assert "is not one of them" in answer["refused"] assert declared == [] # --- C2 ------------------------------------------------------------------------------------- def _r761_shaped(root: Path) -> Path: """A base shaped like the delivered R761: numbered chapter directories under one top level. Crafted rather than mounted, for MAJOR-3's reason — R761 lives outside this repository and cannot be a test dependency — and shaped from the MEASURED names (``R761/4``, ``R761/41``, ``R761/42``) so the arm tests the ranking the real corpus provoked. """ base = root / "proc" (base / "R761" / "4").mkdir(parents=True) (base / "R761" / "41").mkdir() (base / "R761" / "42").mkdir() links = "".join(f"- [{d}](R761/{d}/k.md) — chapter {d}.\n" for d in ("4", "41", "42")) (base / "index.md").write_text(f"---\nbundle_id: proc\n---\n\n{links}", encoding="utf-8") for d in ("4", "41", "42"): (base / "R761" / d / "k.md").write_text( f"---\ntype: Krav\ntitle: Chapter {d}\nprosessnr: '{d}'\n---\n\nbody {d}\n", encoding="utf-8", ) return base def test_c2_a_guessed_chapter_spelling_is_answered_with_the_real_names(tmp_path: Path) -> None: """The order's known-positive: ``R761/4-3`` must come back with ``R761/4…`` candidates. ``R761/4`` is FIRST, because the ranking is longest-common-prefix with the segment that failed — which is the one thing that distinguishes "the names at this level" from "the name you were reaching for". """ base = _r761_shaped(tmp_path) bundle = okf.navigate_bundle(str(base)) neighbours = okf.nearest_subdirectories(bundle, "R761/4-3") assert neighbours[0] == "R761/4" assert set(neighbours) == {"R761/4", "R761/41", "R761/42"} def test_c2_the_read_file_refusal_carries_them(tmp_path: Path) -> None: """The seam: the message the MODEL sees. A helper nothing calls would leave every arm above green and the measured defect untouched.""" base = _r761_shaped(tmp_path) tools, _opened, _declared = _wired(base) answer = tools["read_file"].func(bundle_id="proc", path="R761/4-3/k.md") assert answer.startswith("REFUSED (BundlePathNotFound)") assert "'R761'" in answer assert "subdirectories include R761/4, R761/41, R761/42" in answer def test_c2_the_read_dir_refusal_carries_them_too(tmp_path: Path) -> None: """The OTHER rung, and the reason both were changed: a caller that guesses a directory gets the same help as one that guesses a document. One question about one base, one answer.""" base = _r761_shaped(tmp_path) tools, _opened, _declared = _wired(base) answer = tools["read_dir"].func(bundle_id="proc", path="R761/4-3") assert answer["refusal"] == "BundlePathNotFound" assert "R761/4, R761/41, R761/42" in answer["refused"] def test_c2_an_ancestor_with_no_subdirectories_names_none(tmp_path: Path) -> None: """Omission, never an empty clause. MEASURED: 2 of the 18 guessed paths land here — their ancestor holds documents and no directories — and a trailing "its subdirectories include " with nothing after it is a sentence that says nothing.""" base = _r761_shaped(tmp_path) tools, _opened, _declared = _wired(base) answer = tools["read_file"].func(bundle_id="proc", path="R761/4/missing.md") assert answer.startswith("REFUSED (BundlePathNotFound)") assert "subdirectories include" not in answer def test_c2_every_name_it_hands_back_resolves(tmp_path: Path) -> None: """The property that makes the help worth having (``_index_excerpt``'s rule): a path that never was is worse than no path. Each suggestion is fed straight back to ``read_dir``.""" base = _r761_shaped(tmp_path) tools, _opened, _declared = _wired(base) bundle = okf.navigate_bundle(str(base)) for name in okf.nearest_subdirectories(bundle, "R761/4-3"): listing = tools["read_dir"].func(bundle_id="proc", path=name) assert "refused" not in listing, f"{name} does not resolve: {listing}" def test_c2_the_verdict_layer_is_never_advertised_by_name(tmp_path: Path) -> None: """Built from ``context_files``, never ``files``: the one layer no listing mentions must not appear in a REFUSAL either, which would be the same leak wearing an apology.""" base = tmp_path / "withverdict" (base / "judged").mkdir(parents=True) (base / "open").mkdir() (base / "index.md").write_text( "---\nbundle_id: wv\n---\n\n- [A](open/a.md) — one.\n- [V](judged/v.md) — two.\n", encoding="utf-8", ) (base / "open" / "a.md").write_text( "---\ntype: Krav\ntitle: A\n---\n\nbody\n", encoding="utf-8" ) (base / "judged" / "v.md").write_text( "---\ntype: verdict\ntitle: V\n---\n\nprior judgement\n", encoding="utf-8" ) bundle = okf.navigate_bundle(str(base)) neighbours = okf.nearest_subdirectories(bundle, "nope") assert neighbours == ("open",), neighbours @pytest.mark.parametrize("path", ["", "a"]) def test_c2_a_top_level_guess_names_the_top_level_directories(tmp_path: Path, path: str) -> None: """The fallback: no ancestor at all means the base's own top level, which ``directory_listing`` always answers. ``krav`` and ``r761`` were both measured as top-level guesses on R761.""" base = _r761_shaped(tmp_path) bundle = okf.navigate_bundle(str(base)) assert okf.nearest_subdirectories(bundle, path) == ("R761",)