feat(p22): a refusal names the DOCUMENTS when the ancestor has no subdirectories

P21/C2 made a refusal for an absent path name the ancestor's SUBDIRECTORIES, and it bought what it
was built for: read_dir against a level the base does not hold went from 16 of 104 to 8 of 128. It
did nothing for documents -- read_file against a document the base does not hold went 2 of 38 to
7 of 52 -- and the reason is structural: the nearest listable ancestor of a guessed DOCUMENT path
often holds documents and no subdirectories, and then the neighbour clause was omitted, deliberately,
because an empty list is a sentence with nothing in it.

Measured over round 5's six read_file misses, THREE land on such an ancestor: krav/N100 with 445
documents, and R761/1 with exactly ONE -- which two separate guesses in one run were both reaching
for. The other three have subdirectories and were already answered.

okf.nearest_documents is the sibling of nearest_subdirectories, never a widening of it: never both
clauses, and the subdirectory branch stays FIRST, which is what keeps every C2 refusal byte-identical.
Built from context_files and through the same in_dimension predicate the listing uses, so a refusal
can never advertise the type: verdict layer by path, and every name it hands back resolves -- measured
by feeding each one back into read_file, not by asserting the list is non-empty.

A MUTATION FOUND THE RANKING UNWITNESSED, and that is recorded rather than dropped: replacing
_shared_prefix with a plain reverse sort left the whole suite green. The bound, the source and the
resolve property were all gated; the ORDER was not. For R761/1 that costs nothing, but a level of a
delivered corpus can hold 445, and then which five it names is the whole value of the clause. The new
arm builds a level where the closest name is also the LONGEST, so a length rule puts it last and an
alphabetical one puts another first -- only the prefix rule puts it first.

Load-bearing MEASURED (tests/test_document_neighbours_loadbearing.py, 10 arms), seven mutations all
red against the WHOLE suite + green control 1891/5 (from 1881/5, superset, 0 removed) and golden
demo-transcript.stdout BYTE-UNCHANGED (shasum -a 1 of the CONTENT =
ea8c534773acdbe41ae68f2c55724d69aaf8be4f): C1 detach the document branch in read_file (5 red) -
C2 detach it in read_dir (1) - C3 build from files (1) - C4 ignore the dimension (1) - C5 no bound
(1) - C6 both clauses at once (1) - C7 a second ranking rule (1, after the test was fixed; green
before, which is the finding).

Honesty limits, stated: the foreign-dimension arm was VACUOUSLY green before this change (nothing
was named, so nothing could leak) and is gated only now -- C4 is what makes it real; no LIVE model
has read the new clause (DEL D is the measurement); and the clause is help text, not a gate -- it
cannot make a guessed path right, only cheaper to correct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-16 01:17:39 +02:00
commit f8709ec228
5 changed files with 373 additions and 14 deletions

View file

@ -0,0 +1,245 @@
"""P22 DEL C — when the nearest listable ancestor holds documents and no subdirectories, name them.
P21/C2 made a refusal for an absent path name the ancestor's SUBDIRECTORIES, and it bought what it
was built for: ``read_dir`` against a level the base does not hold went from 16 of 104 to 8 of 128.
It did nothing for documents. Re-measured at the head of okt 126 over round 5's six debate traces,
``read_file`` against a document the base does not hold went from 2 of 38 to **7 of 52** under the
``context_files`` definition (2 of 38 to 6 of 52 under the judge's filesystem one — P21 declared
both definitions, and this file uses the ``context_files`` one because that is the property the
refusal is built from).
The reason is structural: the nearest listable ancestor of a guessed DOCUMENT path often holds
documents and no subdirectories, and then the neighbour clause was omitted deliberately, because
an empty list is a sentence with nothing in it. Measured over those six misses, THREE land on such
an ancestor (``krav/N100`` with 445 documents; ``R761/1`` with exactly ONE, which two separate
guesses ``R761/1/1-1.md`` and ``R761/1/R761-1-1_id-...md`` were both reaching for) and three
have subdirectories and were already answered.
Same source and same property as its sibling: built from ``context_files`` through ``in_dimension``,
so **every name it gives back resolves** arm (c) measures that by feeding each one back, rather
than asserting the list is non-empty.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from portfolio_optimiser import okf
from portfolio_optimiser.explore import navigator_tools
_EXAMPLES = Path(__file__).resolve().parents[1] / "shared" / "examples"
_NEIGHBOUR_MAX = 5
def _write_tree(base: Path, *, dirs: int, per_dir: int, top: int = 2) -> Path:
"""A base shaped like an ingested corpus: a root holding documents AND subdirectories, each
subdirectory holding documents and NOTHING else. The two branches this file separates."""
base.mkdir(parents=True)
root_lines = [f"# {base.name}", ""]
for t in range(top):
root_lines.append(f"- [Toppdokument {t}](topp-{t}.md)")
(base / f"topp-{t}.md").write_text(
f"---\ntype: concept\ntitle: Toppdokument {t}\n---\n\nkort tekst.\n", encoding="utf-8"
)
for d in range(dirs):
sub = base / f"kategori-{d:02d}"
sub.mkdir()
root_lines.append(f"- [kategori-{d:02d} (index)](kategori-{d:02d}/index.md)")
sub_lines = [f"# kategori-{d:02d}", ""]
for i in range(per_dir):
sub_lines.append(f"- [Dokument {i}](dok-{i:02d}.md)")
(sub / f"dok-{i:02d}.md").write_text(
f"---\ntype: concept\ntitle: Dokument {d:02d}-{i:02d}\n---\n\nkort tekst.\n",
encoding="utf-8",
)
(sub / "index.md").write_text(
"---\ntype: index\n---\n\n" + "\n".join(sub_lines) + "\n", encoding="utf-8"
)
(base / "index.md").write_text(
"---\ntype: index\n---\n\n" + "\n".join(root_lines) + "\n", encoding="utf-8"
)
return base
def _tools(base: Path) -> dict[str, Any]:
return {t.name: t for t in navigator_tools((str(base),))}
def _read_file(base: Path, path: str) -> str:
return str(_tools(base)["read_file"].func(bundle_id=base.name, path=path))
def _read_dir(base: Path, path: str) -> Any:
return _tools(base)["read_dir"].func(bundle_id=base.name, path=path)
# --- (a) the KNOWN-POSITIVE: a level with documents and no subdirectories ------------------------
def test_a_missing_document_is_answered_with_the_names_that_level_holds(tmp_path: Path) -> None:
"""RED before DEL C: the ancestor ``kategori-00`` holds documents and no subdirectories, so
P21/C2's clause was omitted and the refusal said only which rung to go back to. It now names
the documents that rung holds."""
base = _write_tree(tmp_path / "korpus", dirs=3, per_dir=4)
refusal = _read_file(base, "kategori-00/dok-99-gjettet.md")
assert refusal.startswith("REFUSED ("), refusal
assert "it holds the documents" in refusal, refusal
assert "kategori-00/dok-00.md" in refusal, refusal
def test_a_level_with_exactly_one_document_names_it(tmp_path: Path) -> None:
"""The measured ``R761/1`` case, which is the sharpest one in the round-5 trace: TWO separate
guesses at one document's name, in one run, at a level that holds exactly that one document.
Naming it answers both in a single step."""
base = _write_tree(tmp_path / "korpus", dirs=1, per_dir=1)
for guess in ("kategori-00/1-1.md", "kategori-00/kat-1-1_id-abc.md"):
refusal = _read_file(base, guess)
assert "kategori-00/dok-00.md" in refusal, (guess, refusal)
# --- (b) the KNOWN-NEGATIVE: a level that HAS subdirectories is answered as before ---------------
def test_a_level_with_subdirectories_still_names_the_subdirectories(tmp_path: Path) -> None:
"""The control, and the half that keeps DEL C from being a rewrite of C2. The root holds BOTH
documents and subdirectories; the subdirectory branch stays first, so every C2 refusal is
unchanged and the two clauses are never both present, because the ancestor is one level and
naming its documents when it also has subdirectories answers a different question."""
base = _write_tree(tmp_path / "korpus", dirs=3, per_dir=4)
refusal = _read_file(base, "finnes-ikke.md")
assert "its subdirectories include" in refusal, refusal
assert "it holds the documents" not in refusal, refusal
# --- (c) every name it gives back RESOLVES -------------------------------------------------------
def test_every_named_document_can_then_be_read(tmp_path: Path) -> None:
"""The ``_index_excerpt`` property, measured rather than assumed: a refusal that hands back a
path the next call refuses is worse than one that hands back nothing. Each named document is
fed straight back into ``read_file``."""
base = _write_tree(tmp_path / "korpus", dirs=2, per_dir=4)
refusal = _read_file(base, "kategori-01/dok-99.md")
named = refusal.split("it holds the documents ")[1].split(")")[0].split(", ")
assert named, refusal
for path in named:
answer = _read_file(base, path.strip())
assert not answer.startswith("REFUSED ("), f"{path!r} does not resolve: {answer}"
# --- (d) the bound ------------------------------------------------------------------------------
def test_the_document_list_is_bounded(tmp_path: Path) -> None:
"""A level of a delivered corpus can hold hundreds — ``krav/N100`` holds 445. The list is the
same fixed five its sibling uses, for the same reason: this is help text on a refusal, and its
price must not be set by how much the level contains."""
base = _write_tree(tmp_path / "korpus", dirs=1, per_dir=40)
refusal = _read_file(base, "kategori-00/dok-99-gjettet.md")
named = refusal.split("it holds the documents ")[1].split(")")[0].split(", ")
assert len(named) == _NEIGHBOUR_MAX, named
def test_the_closest_name_comes_first(tmp_path: Path) -> None:
"""The RANKING, and it is here because a mutation found it unwitnessed.
Replacing ``_shared_prefix`` with a plain reverse sort left the whole suite green: the bound,
the source and the resolve property were all gated, and the ORDER was not. For the measured
``R761/1`` case that costs nothing one document, one answer but a level of a delivered
corpus can hold 445, and then which five it names is the whole value of the clause.
The rule is the sibling's, through the SAME ``_shared_prefix`` helper rather than a second copy
(-(p)): longest common prefix with the segment that failed, then shortest, then name. A
ranking on help text cannot refuse anything, so its failure direction stays benign at worst
it names five real documents that are not the one meant.
"""
base = tmp_path / "korpus"
base.mkdir(parents=True)
names = ["asfalt-slitelag-2024.md", "asfalt-dekke.md", "betong.md", "grus.md"]
lines = [f"# {base.name}", ""]
for name in names:
lines.append(f"- [{name}]({name})")
(base / name).write_text(
f"---\ntype: concept\ntitle: {name}\n---\n\nkort tekst.\n", encoding="utf-8"
)
(base / "index.md").write_text(
"---\ntype: index\n---\n\n" + "\n".join(lines) + "\n", encoding="utf-8"
)
refusal = _read_file(base, "asfalt-slitelag.md")
named = refusal.split("it holds the documents ")[1].split(")")[0].split(", ")
# It shares the longest prefix AND is the longest name, so a length-only rule puts it last and
# a reverse-alphabetical one puts ``grus.md`` first. Only the prefix rule puts it first.
assert named[0] == "asfalt-slitelag-2024.md", named
assert named[1] == "asfalt-dekke.md", named
# --- (e) the other call site ---------------------------------------------------------------------
def test_read_dir_gets_the_same_answer(tmp_path: Path) -> None:
"""ONE helper for both refusal sites (kø-(p)): ``read_dir``'s own absent-path refusal and
``read_file``'s answer the same question about the same base, and two copies would be free to
give a caller two answers about one level."""
base = _write_tree(tmp_path / "korpus", dirs=1, per_dir=4)
answer = _read_dir(base, "kategori-00/finnes-ikke")
blob = json.dumps(answer, ensure_ascii=False)
assert "it holds the documents" in blob, blob
assert "kategori-00/dok-00.md" in blob, blob
# --- (f)/(g) built from context_files, through in_dimension --------------------------------------
def test_the_verdict_layer_is_never_named_in_a_refusal(tmp_path: Path) -> None:
"""Built from ``context_files``, never ``files``. A list read off the filesystem would
advertise, in a REFUSAL, the one layer no listing mentions and ``read_file`` then refuses
outright the same leak in an apology's clothing."""
base = _write_tree(tmp_path / "korpus", dirs=1, per_dir=2)
verdict = base / "kategori-00" / "dom.md"
verdict.write_text(
"---\ntype: verdict\ntitle: En tidligere dom\n---\n\nkropp.\n", encoding="utf-8"
)
index = base / "kategori-00" / "index.md"
index.write_text(index.read_text(encoding="utf-8") + "- [Dom](dom.md)\n", encoding="utf-8")
refusal = _read_file(base, "kategori-00/dok-99.md")
assert "dom.md" not in refusal, refusal
assert "kategori-00/dok-00.md" in refusal, refusal
def test_a_foreign_dimension_document_is_never_named(tmp_path: Path) -> None:
"""Through the SAME ``in_dimension`` predicate the listing uses. Naming a document the run
would refuse to open a moment later is S2c's "a filter in name only", one rung over."""
base = _write_tree(tmp_path / "korpus", dirs=1, per_dir=1)
foreign = base / "kategori-00" / "fremmed.md"
foreign.write_text(
"---\ntype: concept\ndimension: asfalt\ntitle: Fremmed\n---\n\nkropp.\n", encoding="utf-8"
)
index = base / "kategori-00" / "index.md"
index.write_text(
index.read_text(encoding="utf-8") + "- [Fremmed](fremmed.md)\n", encoding="utf-8"
)
tools = {t.name: t for t in navigator_tools((str(base),), dimension="energi")}
refusal = str(tools["read_file"].func(bundle_id=base.name, path="kategori-00/dok-99.md"))
assert "fremmed.md" not in refusal, refusal
# --- (h) the shipped nested golden is untouched --------------------------------------------------
def test_the_shipped_nested_golden_still_navigates_unchanged() -> None:
"""A commons-owned base, read the way the nav-goldens read it: the helper is additive, and the
property the byte-exact goldens rest on is that nothing about navigation moved."""
bundle = okf.navigate_bundle(str(_EXAMPLES / "nav-golden-hierarchy" / "bundle"))
assert [f.name for f in bundle.context_files] == [
"overview.md",
"a/doc-a.md",
"a/b/doc-b.md",
]