feat(prepass,explore): padding dies at the prompt; a refusal the model can act on is a return value [skip-docs]

Order 20260908T195801Z. Findings 4 and 5 from the S7 acid test, then the two things
finding 99 measured and deliberately did not fix (D3, D2).

No user-facing surface changes: no new flag, no new command, no changed output
contract. Both seams are internal (the pre-pass rendering, and the shape a tool
answers a model with), so [skip-docs] rather than a README edit that would describe
nothing an operator can do differently.

FINDING 4 -- MEASURED, NOTHING BUILT. K2's price schedule IS readable without
guessing (8 column spans, 71 of 91 non-blank rows give >= 2 cells, the split stable
for K = 2..64). But 0 of 92 rows name all three of code/quantity/unit_cost -- also
under a looser substring match -- and 0 of 91 data rows carry code + quantity +
amount. The triple is not formatted away; it is not in the document. It is a price
SUMMARY plus nine rate cards whose unit-price columns are empty (pre-award). The
order's binding decision rule therefore falls against building:
--derive-cost-baseline keeps refusing, and MAJOR-4's own honesty limit holds.

FINDING 5 -- BUILT. Measured on the actual rendering path (concept_text, not the
raw file): the delivered excerpt is 104 lines / 67 245 chars, carrying 208 interior
whitespace runs, 117 of them >= 100 and the longest 887 -- 56 806 of 67 245
characters = 84.5 %, over 72 of 104 lines. collapse_padding, called from
_data_blocks (the one renderer both arms share, and therefore AFTER
verify_against_bundle -- collapsing in concept_text would break every payload's own
digest), gives -72.4 %: line count invariant, non-whitespace byte-identical, leading
indentation untouched, no number changed.

F99-D3 -- read_file / read_dir / read_bundle now RETURN their refusal. MAF turns a
tool raise into "Error: Function failed." (_tools.py:1410-1432, :1427) and counts it
against DEFAULT_MAX_CONSECUTIVE_ERRORS_PER_REQUEST = 3, so everything the refusing
arm knows is destroyed on the way out. The gates are unchanged; the property they
exist for -- the reason travels, the bytes never do -- is now asserted explicitly on
the returned value. The arm is keyed on named classes, never bare Exception, because
ExplorationError is itself a RuntimeError subclass.

F99-D2 -- the invariant row, plus one for finding 5 (a stated deviation from "one
row only": finding 5 is a separately built seam and the ledger's standing rule
requires its own row).

19 existing arms rewritten, never deleted and never weakened: where the class
carried a distinction, the refusal KIND carries it now.

13 mutations, all red against the whole suite (W1-W5, M1-M8), each restored from
scratchpad with shasum -c. Control 1543 passed / 5 skipped (from 1529/5, a strict
superset, 0 removed). Golden demo-transcript.stdout unchanged
(shasum -a 1 of the CONTENT = ea8c534773acdbe41ae68f2c55724d69aaf8be4f).

Measurement: docs/2026-09-08-funn-4-5-og-read-nekt.md

Co-Authored-By: Claude <Opus 5>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-08 23:36:54 +02:00
commit eb4137415c
13 changed files with 975 additions and 69 deletions

View file

@ -311,9 +311,13 @@ def test_explore_addresses_a_base_by_its_declared_id(tmp_path: Path) -> None:
catalogue = tools["list_bundles"].func()
assert [entry["id"] for entry in catalogue] == [_DECLARED]
assert tools["read_bundle"].func(bundle_id=_DECLARED), "the declared id must OPEN the base"
with pytest.raises(explore.ExplorationError):
tools["read_bundle"].func(bundle_id="explore-mount-name")
opened = tools["read_bundle"].func(bundle_id=_DECLARED)
assert opened and "refused" not in opened, "the declared id must OPEN the base"
# REWRITTEN, not weakened (F99-D3): the tool returns its refusal instead of raising it. Both
# halves stand -- the mount name does NOT open the base, and the answer is a refusal rather
# than a listing, which is what "two schemes" would have produced.
refusal = tools["read_bundle"].func(bundle_id="explore-mount-name")
assert "refused" in refusal and "directories" not in refusal
def test_read_bundle_refuses_a_base_that_cannot_say_what_it_is(tmp_path: Path) -> None:
@ -325,8 +329,12 @@ def test_read_bundle_refuses_a_base_that_cannot_say_what_it_is(tmp_path: Path) -
_declare(second, "corpus-beta")
tools = {t.name: t for t in explore.navigator_tools((str(base),))}
with pytest.raises(okf.BundleIdMismatch):
tools["read_bundle"].func(bundle_id="two-minds-explored")
# REWRITTEN, not weakened (F99-D3): the refusal is returned, and it is pinned to the KIND, so
# a base whose concepts name two corpora is still told apart from one that merely does not
# exist -- the distinction ``pytest.raises(okf.BundleIdMismatch)`` used to carry.
refusal = tools["read_bundle"].func(bundle_id="two-minds-explored")
assert refusal["refusal"] == okf.BundleIdMismatch.__name__
assert "directories" not in refusal
async def test_two_mounts_declaring_one_id_collide_in_the_dispatcher(tmp_path: Path) -> None:

View file

@ -42,7 +42,6 @@ from collections.abc import Callable
from pathlib import Path
from typing import Any
import pytest
from agent_framework import BaseChatClient
from portfolio_optimiser import okf
@ -333,8 +332,11 @@ async def test_a_foreign_dimension_document_is_neither_listed_nor_readable(tmp_p
assert _ASFALT_FILE not in listing, (
"a document from another dimension is still listed to the agents"
)
with pytest.raises(DimensionScopeRefused):
await _invoke(tools["read_file"], bundle_id=bundle_id, path=_ASFALT_FILE)
answer = await _invoke(tools["read_file"], bundle_id=bundle_id, path=_ASFALT_FILE)
assert answer.startswith(f"REFUSED ({DimensionScopeRefused.__name__})")
# F99-D3 returns the refusal, so §4.1a's property is asserted on the value: the reason
# travels, the out-of-scope document's bytes do not.
assert _ASFALT_SENTINEL not in answer
async def test_without_a_dimension_the_same_document_is_listed_and_readable(tmp_path) -> None:
@ -380,5 +382,6 @@ async def test_a_dimension_scoped_run_gives_the_debate_scoped_tools(tmp_path, mo
)
read_file = next(t for t in captured[0] if getattr(t, "name", "") == "read_file")
with pytest.raises(DimensionScopeRefused):
await _invoke(read_file, bundle_id=bundle_id, path=_ASFALT_FILE)
answer = await _invoke(read_file, bundle_id=bundle_id, path=_ASFALT_FILE)
assert answer.startswith(f"REFUSED ({DimensionScopeRefused.__name__})")
assert _ASFALT_SENTINEL not in answer

View file

@ -758,8 +758,13 @@ def test_an_unknown_knowledge_base_is_refused_by_name() -> None:
failed."`` and counts three of them as grounds to stop function calling for the whole request —
so the one thing this refusal knows and the model does not (which ids exist) never reached it.
The property T20 was written for is untouched and asserted here in both halves: the answer is
still a refusal, and it still names what IS configured. The ``navigator_tools`` half is what
keeps the change SCOPED those three tools still raise.
still a refusal, and it still names what IS configured.
REWRITTEN A SECOND TIME (F99-D3, order 20260908T195801Z), for the reason the note above gave
for the first rewrite: the three navigator tools no longer raise either, so the same
measurement now applies to them and the arm asserts the same two halves through their return
value. What is NOT relaxed is the class the arm is keyed on a failure nobody named still
propagates, gated by ``test_explore_read_tools_refusal``'s known-negative.
"""
validate = explore.quick_validate_tool(("/tmp/base-a",))
verdict = validate.func(bundle_id="base-b", proposal_json="{}")
@ -767,9 +772,8 @@ def test_an_unknown_knowledge_base_is_refused_by_name() -> None:
assert "base-b" in verdict["reason"] and "base-a" in verdict["reason"]
read_file = next(t for t in explore.navigator_tools(("/tmp/base-a",)) if t.name == "read_file")
with pytest.raises(explore.ExplorationError) as excinfo:
read_file.func(bundle_id="base-b", path="x.md")
assert "base-a" in str(excinfo.value)
answer = read_file.func(bundle_id="base-b", path="x.md")
assert answer.startswith("REFUSED (") and "base-a" in answer
def test_two_bases_with_the_same_name_are_refused() -> None:

View file

@ -0,0 +1,184 @@
"""``read_file`` / ``read_dir`` / ``read_bundle`` RETURN their refusal; they no longer raise.
**F99-D3.** Finding 99 closed exactly one half of this and said so
(``docs/2026-09-08-funn-99-chatclient-refusal.md`` lines 132-135): ``quick_validate`` was made to
return ``{"decision": "refused", ...}``, while "``read_file`` / ``read_dir`` / ``read_bundle``
raiser fortsatt, og deres raises telles nøyaktig samme måte ... **MÅLT, RAPPORTERT, IKKE
FIKSET.**" The trio measured immediately before the live 400 was three ``read_file`` refusals on
``del-ii-bilag-7-prisskjema*`` -- the same document findings 4 and 5 of this order are about.
**Why a raise is worse than useless here, MEASURED in the framework's own source.** MAF turns a
tool's exception into the opaque string ``"Error: Function failed."``
(``agent_framework/_tools.py:1410-1432``), suppressing the detail unless ``include_detailed_errors``
is set (``:1427``), and counts it against ``DEFAULT_MAX_CONSECUTIVE_ERRORS_PER_REQUEST = 3``
(``:96``, ``:2718-2731``). So everything the refusing arm KNOWS -- which base ids exist, which
document the path really names, which rung reads it -- is destroyed on the way out, and three of
them end the request. The model is left guessing, which is what finding 99 recorded it doing in its
own words.
**The gates themselves are UNCHANGED.** This is the shape of the answer, never its content: the
verdict layer and the §4.1a dimension scope refuse exactly what they refused before, and the
property they exist for -- **the bytes never reach the model** -- is asserted explicitly on the
returned value, not assumed from the fact that an exception used to be raised.
**Form per tool, and the difference is forced by the return type.** ``read_bundle`` and ``read_dir``
return ``dict``, so the refusal is a mapping with no key a successful listing has. ``read_file``
returns ``str``: making it a mapping would change the shape of every SUCCESSFUL read and therefore
every prompt byte S2c measured, so the refusal is a string with a fixed leading sentinel. A
``str`` refusal a caller cannot tell from a document would be worthless, and the honesty limit --
a document whose first characters happen to be that sentinel -- is stated in the docstring rather
than pretended away.
**Keyed on the class, never on bare ``Exception``.** The known-negative below raises a plain
``RuntimeError`` from inside a tool body and requires it to propagate: we are not in the business of
hiding failures we did not name, and ``ExplorationError`` being itself a ``RuntimeError`` subclass
is exactly why the arm has to be a list of names rather than a base class.
"""
from __future__ import annotations
import shutil
from pathlib import Path
from typing import Any
import pytest
from portfolio_optimiser import okf
from portfolio_optimiser.explore import navigator_tools
BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
_VERDICT_FILE = "verdict-led-fro.md"
_CONCEPT_FILE = "metode-ipmvp-a.md"
_SENTINEL = "READ-TOOL-LEAK-SENTINEL-z9y8x7"
def _tools(bundle_dir: str | Path, dimension: str | None = None) -> dict[str, Any]:
return {t.name: t for t in navigator_tools([str(bundle_dir)], dimension=dimension)}
def _marked_copy(tmp_path: Path, target: str) -> Path:
"""A copy of the fixture base with ``_SENTINEL`` appended to one document's body, so a leak of
that document's CONTENT into a refusal is detectable by a string that exists nowhere else."""
copy = tmp_path / "bundle"
shutil.copytree(BUNDLE_DIR, copy)
doc = copy / target
doc.write_text(doc.read_text(encoding="utf-8") + f"\n{_SENTINEL}\n", encoding="utf-8")
return copy
# --- (1) the unknown base id: the trio measured in front of the live 400 ------------------------
def test_an_unknown_base_is_returned_as_a_refusal_by_all_three_tools() -> None:
"""(1) The measured case. All three name the configured ids, so the model can correct itself
instead of receiving ``"Error: Function failed."`` three times and ending the request."""
tools = _tools(BUNDLE_DIR)
guessed = "renholdstekniske_funksjonskrav"
listing = tools["read_bundle"].func(bundle_id=guessed)
assert isinstance(listing, dict) and "refused" in listing
assert BUNDLE_DIR.name in listing["refused"]
directory = tools["read_dir"].func(bundle_id=guessed, path="x")
assert isinstance(directory, dict) and "refused" in directory
assert BUNDLE_DIR.name in directory["refused"]
document = tools["read_file"].func(bundle_id=guessed, path="index.md")
assert isinstance(document, str) and document.startswith("REFUSED")
assert BUNDLE_DIR.name in document
# --- (2) the verdict layer: the reason travels, the bytes never do ------------------------------
def test_the_verdict_layer_refusal_carries_the_reason_and_never_the_content(
tmp_path: Path,
) -> None:
"""(2) The gate is unchanged; only the shape of the answer is. The assertion the order requires
is the second one: a returned refusal must not become a channel for the bytes the gate exists
to withhold."""
base = _marked_copy(tmp_path, _VERDICT_FILE)
answer = _tools(base)["read_file"].func(bundle_id=base.name, path=_VERDICT_FILE)
assert isinstance(answer, str) and answer.startswith("REFUSED")
assert "verdict" in answer.lower()
assert _SENTINEL not in answer, "the refusal leaked the document it refused"
def test_an_ordinary_document_in_the_same_base_still_reads_whole(tmp_path: Path) -> None:
"""(2) CONTROL. Without this, arm (2) is satisfied by a tool that refuses everything."""
base = _marked_copy(tmp_path, _CONCEPT_FILE)
body = _tools(base)["read_file"].func(bundle_id=base.name, path=_CONCEPT_FILE)
assert _SENTINEL in body
assert not body.startswith("REFUSED")
# --- (3) the §4.1a dimension scope --------------------------------------------------------------
def test_the_dimension_refusal_carries_the_reason_and_never_the_content(tmp_path: Path) -> None:
"""(3) The other gate this change touches. Same two halves: the reason travels, the body does
not."""
base = _marked_copy(tmp_path, _CONCEPT_FILE)
foreign = base / "fremmed-dimensjon.md"
foreign.write_text(
f"---\ntype: methodology\ntitle: Asfalt\ndimension: asfalt\n---\n\n{_SENTINEL}\n",
encoding="utf-8",
)
index = base / "index.md"
index.write_text(
index.read_text(encoding="utf-8") + "\n- [Asfalt](fremmed-dimensjon.md)\n", encoding="utf-8"
)
answer = _tools(base, dimension="tunnel")["read_file"].func(
bundle_id=base.name, path="fremmed-dimensjon.md"
)
assert isinstance(answer, str) and answer.startswith("REFUSED")
assert "tunnel" in answer
assert _SENTINEL not in answer, "the refusal leaked the out-of-scope document"
# --- (4) the wrong rung, both directions --------------------------------------------------------
def test_the_wrong_rung_is_returned_by_both_tools() -> None:
"""(4) F3's asymmetry, preserved through the change: each refusal still names the rung that
WOULD read the thing, and the two branches still share no wording."""
tools = _tools(BUNDLE_DIR)
as_document = tools["read_dir"].func(bundle_id=BUNDLE_DIR.name, path=_CONCEPT_FILE)
assert "refused" in as_document and "read_file" in as_document["refused"]
unknown = tools["read_dir"].func(bundle_id=BUNDLE_DIR.name, path="kategori-99")
assert "refused" in unknown and "read_file" not in unknown["refused"]
assert unknown["refusal"] != as_document["refusal"], (
"a wrong rung and a path that does not exist are different facts; collapsing them into one "
"kind leaves a caller unable to tell 'use the other tool' from 'this does not exist'"
)
# --- (5) KNOWN-NEGATIVE: an unnamed failure still propagates ------------------------------------
def test_a_plain_runtime_error_still_propagates(monkeypatch: pytest.MonkeyPatch) -> None:
"""(5) KNOWN-NEGATIVE, and the reason the arm is a list of NAMES. ``ExplorationError`` is itself
a ``RuntimeError`` subclass, so an arm written as ``except RuntimeError`` would swallow this
one too -- turning an unknown failure into a confident-looking answer, which is worse than the
opaque string this change removes."""
tools = _tools(BUNDLE_DIR)
def boom(*args: Any, **kwargs: Any) -> Any:
raise RuntimeError("something we never named")
monkeypatch.setattr(okf, "navigate_bundle", boom)
with pytest.raises(RuntimeError, match="something we never named"):
tools["read_bundle"].func(bundle_id=BUNDLE_DIR.name)
with pytest.raises(RuntimeError, match="something we never named"):
tools["read_dir"].func(bundle_id=BUNDLE_DIR.name, path="x")
# --- (6) a successful answer can never be mistaken for a refusal --------------------------------
def test_a_successful_listing_carries_no_refusal_key() -> None:
"""(6) The other half of "a refusal a caller cannot tell from an answer is worthless"."""
listing = _tools(BUNDLE_DIR)["read_bundle"].func(bundle_id=BUNDLE_DIR.name)
assert "refused" not in listing and "refusal" not in listing
assert listing["documents"]

View file

@ -54,7 +54,6 @@ import json
from pathlib import Path
from typing import Any
import pytest
from portfolio_optimiser import explore, okf
from portfolio_optimiser.explore import navigator_tools
@ -269,11 +268,18 @@ def test_an_unknown_directory_is_refused_by_name(tmp_path: Path) -> None:
Pinned to ``BundlePathNotFound`` by NAME since F3 added its sibling: both refusals quote the
caller's path, so a match on the path alone could no longer tell "this is nothing" from "this
is a document" - the substring two branches share."""
is a document" - the substring two branches share.
REWRITTEN, not weakened (F99-D3): the tool RETURNS the refusal instead of raising it, so the
name it is pinned to is the ``refusal`` kind. Both halves stand - the path is quoted, and the
kind still separates it from ``DocumentPathRefused``."""
base = _write_tree(tmp_path, "korpus", dirs=2, per_dir=2)
with pytest.raises(okf.BundlePathNotFound, match="kategori-99"):
_read_dir(base, "kategori-99")
refusal = _read_dir(base, "kategori-99")
assert refusal["refusal"] == okf.BundlePathNotFound.__name__
assert refusal["refusal"] != okf.DocumentPathRefused.__name__
assert "kategori-99" in refusal["refused"]
# --- (h) the description must not lie about the body ----------------------------------------------

View file

@ -0,0 +1,179 @@
"""A spreadsheet render's PADDING is dropped on the way into the prompt; nothing else is.
**Finding 5 of the S7 acid test** (``docs/2026-09-07-syretest-s7-prepass-k2.md`` § 10 pkt. 5), after
S7c had already felled the obvious cure. Opening both locks DID deliver K2's price schedule -- it
came back at rank 10 and its bytes stood in 2 of 11 prompts -- and the model still cited
``5647500`` / ``prissammenstilling`` / ``prisskjema`` / ``pris`` in **0 of 4** and **0 of 11**
answers (``docs/2026-09-08-syretest-s7c-begge-laaser-k2.md`` § 5). The blind spot had moved off the
ranking and onto the READING, and what po owns there is the rendering of the excerpt that reaches
the prompt.
**MEASURED FIRST, on the actual rendering path** (``prepass.concept_text``, the local re-derivation
that ``verify_against_bundle`` binds a payload to -- not the raw file), against
``~/repos/portfolio-optimiser/scratchpad/s7-prepass/k2-bundle-s7/del-ii-bilag-7-prisskjema/prissammenstilling-sheet-1.md``:
* raw on disk 119 lines / 101 188 B; delivered excerpt **104 lines / 67 245 characters** -- which
reconciles the two denominators the order carried: the same document, before and after the
frontmatter split and the per-line ``rstrip`` the producer does.
* the delivered text still holds **208** interior whitespace runs of >= 2 characters, **117** of
them >= 100 and the longest **887**;
* those >= 100 runs are **56 806 of 67 245 characters = 84.5 %** of the excerpt, spread over
**72 of 104 lines**.
So a label and its amount reach the model hundreds of characters apart -- ``82`` then 593 spaces
then ``Prosjektering (tiltransport av prosjekterende)`` then 250 spaces then ``5647500.0`` -- and
84.5 % of what the run pays for is a column width pandoc chose.
**The collapse is DETERMINISTIC and carries no judgement**, and each half of that is gated below:
* interior runs of spaces/tabs collapse to ONE space -- K = 2, i.e. there is no threshold to argue
about; "two or more" is the whole rule.
* LEADING whitespace is untouched. Indentation is markdown structure (nested lists, indented code),
not padding, and a rule that flattened it would rewrite documents rather than unpad them.
* no newline is touched, so the line count is invariant and no row is merged with its neighbour.
* no non-whitespace character is touched, so no number can change -- asserted as the per-line
non-whitespace sequence, not merely as "the digits are still somewhere".
**WHERE it lives is the load-bearing choice.** It is in ``_data_blocks`` -- the ONE renderer both
arms share (-(p)) -- and therefore AFTER ``verify_against_bundle``. ``concept_text`` is the
re-derivation a payload's bytes are checked against, so collapsing there would make every payload
ever written fail its own digest, and would defeat the gate that stops a payload delivering bytes
the base does not hold. Arm (f) is that separation.
**Scope limit, stated.** That a model then USES the numbers cannot be shown here -- it would take a
paid run, and none is ordered. What is delivered is the deterministic property; a paid confirmation
is a later, separate order.
**Honesty limit, stated.** Collapsing to a single space loses the CELL BOUNDARY: after the collapse
``Post SUM`` is indistinguishable from prose that happens to contain those two words. That is a
real loss. Inventing a delimiter to carry the boundary would be exactly the judgement this rule is
written to avoid, and no measurement here says which delimiter a model reads better; so the loss is
recorded rather than papered over.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import Any
from portfolio_optimiser import prepass
FIXTURE = Path(__file__).parent / "fixtures" / "prepass" / "bygg-energi-mikro-fixture.payload.json"
#: A K2-shaped row: code, 593 spaces, description, 250 spaces, amount. The widths are the ones
#: measured on line 34 of the real price schedule, so the fixture is the measurement rather than a
#: round number chosen for the test.
_CODE = "82"
_DESC = "Prosjektering (tiltransport av prosjekterende)"
_AMOUNT = "5647500.0"
_PADDED_ROW = f" {_CODE}{' ' * 593}{_DESC}{' ' * 250}{_AMOUNT}"
#: An indented fenced block: leading whitespace is structure and must survive untouched.
_INDENTED = " indented code line"
_PADDED_TEXT = "\n".join(["## Prissammenstilling", "", _PADDED_ROW, _INDENTED, ""])
def _payload_with(text: str) -> prepass.PrepassPayload:
raw: dict[str, Any] = json.loads(FIXTURE.read_text(encoding="utf-8"))
raw["excerpts"] = [raw["excerpts"][0]]
raw["excerpts"][0]["text"] = text
return prepass.PrepassPayload.model_validate(raw)
def _longest_interior_run(text: str) -> int:
"""The longest run of horizontal whitespace that is NOT a line's leading indentation."""
longest = 0
for line in text.split("\n"):
body = line.lstrip(" \t")
for match in re.finditer(r"[ \t]{2,}", body):
longest = max(longest, len(match.group(0)))
return longest
# --- (a) the delivered excerpt reaches the prompt with a BOUNDED separator ---------------------
def test_the_rendered_excerpt_separates_label_and_amount_by_a_bounded_separator() -> None:
"""(a) The property finding 5 is about. Detach point: remove the collapse from
``_data_blocks`` -> RED, because the 593- and 250-character runs ride into the prompt."""
rendered = prepass.render_context(_payload_with(_PADDED_TEXT))
assert _longest_interior_run(rendered) <= 1, (
"the rendered excerpt still carries a padding run; a label and its amount reach the "
"model hundreds of characters apart"
)
assert f"{_CODE} {_DESC} {_AMOUNT}" in rendered, (
"the row's three cells no longer read as one row"
)
def test_a_text_that_needs_no_collapse_renders_unchanged() -> None:
"""(a) CONTROL. An excerpt with no padding must render exactly as before, or arm (a) would be
satisfied by a renderer that mangles every payload equally."""
plain = "## Heading\n\nOne ordinary sentence with single spaces.\n"
assert plain in prepass.render_context(_payload_with(plain))
# --- (b) known-negative: no number, and no other character, is ever changed --------------------
def test_no_non_whitespace_character_is_changed() -> None:
"""(b) KNOWN-NEGATIVE. The per-line sequence of non-whitespace characters is identical before
and after. Stronger than "the digits are still somewhere": a rule that stripped a trailing
``.0``, or reordered two cells, would pass a looser assertion and fail this one."""
collapsed = prepass.collapse_padding(_PADDED_TEXT)
for before, after in zip(_PADDED_TEXT.split("\n"), collapsed.split("\n"), strict=True):
assert re.sub(r"\s+", "", before) == re.sub(r"\s+", "", after)
assert _AMOUNT in collapsed
# --- (c) known-negative: no line is merged with its neighbour ----------------------------------
def test_no_line_is_merged_with_its_neighbour() -> None:
"""(c) KNOWN-NEGATIVE. Newlines are not whitespace this rule may touch: merging rows is how a
naive ``re.sub(r"\\s+", " ", text)`` would silently turn a table into a paragraph."""
collapsed = prepass.collapse_padding(_PADDED_TEXT)
assert collapsed.split("\n") != []
assert len(collapsed.split("\n")) == len(_PADDED_TEXT.split("\n"))
# --- (d) leading indentation is structure, not padding -----------------------------------------
def test_leading_indentation_survives() -> None:
"""(d) An indented line keeps its indentation: markdown reads it as structure, and collapsing
it would rewrite the document rather than unpad it."""
collapsed = prepass.collapse_padding(_PADDED_TEXT)
assert _INDENTED in collapsed.split("\n")
# --- (e) ONE seam, both arms --------------------------------------------------------------------
def test_both_arms_collapse_because_they_share_one_renderer() -> None:
"""(e) ``render_seed`` is the exploration's arm and ``render_context`` the debate's; they share
``_data_blocks``. A fix in one only would leave the other reading padding."""
payload = _payload_with(_PADDED_TEXT)
for rendered in (prepass.render_context(payload), prepass.render_seed(payload)):
assert _longest_interior_run(rendered) <= 1
# --- (f) the gate is untouched: verification reads the UNCOLLAPSED text -------------------------
def test_the_local_re_derivation_is_not_collapsed() -> None:
"""(f) ``concept_text`` is what ``verify_against_bundle`` binds a payload's bytes to. Collapsing
THERE would make every payload ever written fail its own digest and would break the gate that
stops a payload delivering bytes the base does not hold. Detach point: move the collapse into
``concept_text`` -> RED."""
padded = Path(__file__).parent / "fixtures" / "prepass" / "_padded-concept.md"
padded.parent.mkdir(parents=True, exist_ok=True)
padded.write_text(f"---\ntype: reference\n---\n\n{_PADDED_ROW}\n", encoding="utf-8")
try:
derived = prepass.concept_text(padded)
assert " " * 593 in derived, (
"the local re-derivation collapsed padding; a payload's own digest can no longer match"
)
finally:
padded.unlink()

View file

@ -37,7 +37,6 @@ from __future__ import annotations
from pathlib import Path
from typing import Any
import pytest
from portfolio_optimiser import okf
from portfolio_optimiser.explore import navigator_tools
@ -106,10 +105,10 @@ def test_read_dir_on_a_documents_exact_name_names_read_file_and_the_path(tmp_pat
refusal that only says "no" leaves the caller with the same next move it just made."""
base = _base(tmp_path)
with pytest.raises(okf.DocumentPathRefused) as excinfo:
_read_dir(base, f"{_ALPHA}.md")
refusal = _read_dir(base, f"{_ALPHA}.md")
message = str(excinfo.value)
assert refusal["refusal"] == okf.DocumentPathRefused.__name__
message = refusal["refused"]
assert "read_file" in message, "the refusal does not name the rung that reads a document"
assert f"{_ALPHA}.md" in message, "the refusal does not name the path read_file would take"
@ -119,10 +118,10 @@ def test_the_suffix_is_not_what_makes_it_a_document(tmp_path: Path) -> None:
``.md`` form would answer one of them and not the other."""
base = _base(tmp_path)
with pytest.raises(okf.DocumentPathRefused) as excinfo:
_read_dir(base, _ALPHA)
refusal = _read_dir(base, _ALPHA)
message = str(excinfo.value)
assert refusal["refusal"] == okf.DocumentPathRefused.__name__
message = refusal["refused"]
assert "read_file" in message
assert f"{_ALPHA}.md" in message, (
"the refusal echoed the caller's path instead of the document's real name; a path that "
@ -138,15 +137,18 @@ def test_an_unknown_path_keeps_its_own_wording(tmp_path: Path) -> None:
unknown-path branch is unchanged and must stay unable to claim a document exists."""
base = _base(tmp_path)
with pytest.raises(okf.BundlePathNotFound) as excinfo:
_read_dir(base, "kategori-99")
refusal = _read_dir(base, "kategori-99")
message = str(excinfo.value)
message = refusal["refused"]
assert "read_file" not in message, (
"the unknown-path refusal names read_file, so the two branches say the same thing about "
"two different facts"
)
assert not isinstance(excinfo.value, okf.DocumentPathRefused)
# F99-D3: the class distinction that used to be observable through ``isinstance`` is carried
# by the ``refusal`` KIND now that the refusal is returned rather than raised. Dropping it
# would have been a silent weakening of exactly this assertion.
assert refusal["refusal"] == okf.BundlePathNotFound.__name__
assert refusal["refusal"] != okf.DocumentPathRefused.__name__
# --- (d) anti-vacuity: the named path must WORK ---------------------------------------------------
@ -157,9 +159,7 @@ def test_the_named_path_is_usable_verbatim(tmp_path: Path) -> None:
that does not resolve is worse than none. Proven by feeding it back."""
base = _base(tmp_path)
with pytest.raises(okf.DocumentPathRefused) as excinfo:
_read_dir(base, _ALPHA)
named = str(excinfo.value).split("'")[-2]
named = _read_dir(base, _ALPHA)["refused"].split("'")[-2]
assert "alpha body." in _read_file(base, named)
@ -173,11 +173,10 @@ def test_the_verdict_layer_is_never_named(tmp_path: Path) -> None:
``read_file`` refuses outright (order 20260904T172353Z)."""
base = _base(tmp_path)
with pytest.raises(okf.BundlePathNotFound) as excinfo:
_read_dir(base, "dom-beta.md")
refusal = _read_dir(base, "dom-beta.md")
assert "read_file" not in str(excinfo.value)
assert not isinstance(excinfo.value, okf.DocumentPathRefused)
assert "read_file" not in refusal["refused"]
assert refusal["refusal"] == okf.BundlePathNotFound.__name__
def test_a_foreign_dimension_document_is_never_named(tmp_path: Path) -> None:
@ -185,14 +184,13 @@ def test_a_foreign_dimension_document_is_never_named(tmp_path: Path) -> None:
name only" S2c measured. ONE predicate (``in_dimension``) serves the listing and this."""
base = _base(tmp_path)
with pytest.raises(okf.BundlePathNotFound) as excinfo:
_read_dir(base, "energi-notat.md", dimension="tunnel")
scoped = _read_dir(base, "energi-notat.md", dimension="tunnel")
assert "read_file" not in str(excinfo.value)
assert "read_file" not in scoped["refused"]
assert scoped["refusal"] == okf.BundlePathNotFound.__name__
# The control: without a scope the SAME path is the wrong-rung class, so the arm above is the
# dimension deciding rather than the document being invisible.
with pytest.raises(okf.DocumentPathRefused):
_read_dir(base, "energi-notat.md")
assert _read_dir(base, "energi-notat.md")["refusal"] == okf.DocumentPathRefused.__name__
# --- (g) the LIVE case is not this class ----------------------------------------------------------
@ -207,11 +205,10 @@ def test_the_measured_live_path_is_still_the_unknown_class(tmp_path: Path) -> No
base = _base(tmp_path)
assert (base / f"{_GAMMA}.md").exists(), "the fixture must hold the prefixed document"
with pytest.raises(okf.BundlePathNotFound) as excinfo:
_read_dir(base, _GAMMA.removeprefix("inbox-"))
refusal = _read_dir(base, _GAMMA.removeprefix("inbox-"))
assert "read_file" not in str(excinfo.value)
assert not isinstance(excinfo.value, okf.DocumentPathRefused)
assert "read_file" not in refusal["refused"]
assert refusal["refusal"] == okf.BundlePathNotFound.__name__
# --- (h) the channel ------------------------------------------------------------------------------

View file

@ -72,9 +72,10 @@ def _a_directory(bundle_dir: Path) -> str:
def test_a_directory_path_is_refused_and_the_other_rung_is_named() -> None:
"""(1) the refusal says WHAT the path is and WHICH tool reads it."""
path = _a_directory(_NESTED)
with pytest.raises(DirectoryPathRefused) as excinfo:
_tools(_NESTED)["read_file"].func(bundle_id=_NESTED.name, path=path)
message = str(excinfo.value)
message = _tools(_NESTED)["read_file"].func(bundle_id=_NESTED.name, path=path)
assert message.startswith(f"REFUSED ({DirectoryPathRefused.__name__})"), (
"the refusal is not distinguishable from a document's body"
)
assert path in message
assert "directory" in message
assert "read_dir" in message, "a refusal that does not name the other rung strands the caller"
@ -88,9 +89,14 @@ def test_the_refusal_is_a_value_error_not_an_os_error() -> None:
wrapper: re-raising ``IsADirectoryError`` with a better message would pass arm (1).
"""
path = _a_directory(_NESTED)
with pytest.raises(ValueError) as excinfo:
_tools(_NESTED)["read_file"].func(bundle_id=_NESTED.name, path=path)
assert not isinstance(excinfo.value, OSError)
answer = _tools(_NESTED)["read_file"].func(bundle_id=_NESTED.name, path=path)
# F99-D3: the refusal is RETURNED now, so the branch is identified by the kind it names. The
# channel assertion moves onto the class itself and keeps both halves: it is on the CLI's
# refusal tuple, and it is NOT a re-raised ``IsADirectoryError`` wearing a better message --
# an ``OSError`` is outside ``_RETURNABLE_REFUSALS`` and would propagate rather than answer.
assert answer.startswith(f"REFUSED ({DirectoryPathRefused.__name__})")
assert issubclass(DirectoryPathRefused, ValueError)
assert not issubclass(DirectoryPathRefused, OSError)
async def test_the_refusal_holds_through_the_real_tool_surface() -> None:
@ -100,8 +106,9 @@ async def test_the_refusal_holds_through_the_real_tool_surface() -> None:
would be absent from the one caller it was written for.
"""
path = _a_directory(_NESTED)
with pytest.raises(DirectoryPathRefused):
await _invoke(_tools(_NESTED)["read_file"], bundle_id=_NESTED.name, path=path)
answer = await _invoke(_tools(_NESTED)["read_file"], bundle_id=_NESTED.name, path=path)
assert answer.startswith(f"REFUSED ({DirectoryPathRefused.__name__})")
assert "read_dir" in answer
def test_a_listing_already_separates_directories_from_documents() -> None:

View file

@ -42,7 +42,6 @@ from collections.abc import Callable
from pathlib import Path
from typing import Any
import pytest
from agent_framework import BaseChatClient
from portfolio_optimiser import okf
@ -155,8 +154,11 @@ async def test_the_exploration_path_refuses_a_verdict_document(tmp_path) -> None
bundle_id = okf.reconcile_bundle_id(bundle_dir).id
tools = _tools(bundle_dir)
with pytest.raises(VerdictLayerRefused):
await _invoke(tools["read_file"], bundle_id=bundle_id, path=_VERDICT_FILE)
answer = await _invoke(tools["read_file"], bundle_id=bundle_id, path=_VERDICT_FILE)
assert answer.startswith(f"REFUSED ({VerdictLayerRefused.__name__})")
# F99-D3 made the refusal a RETURN VALUE, so the property the gate exists for has to be
# asserted on that value: the reason travels, the bytes never do.
assert _VERDICT_SENTINEL not in answer
async def test_an_ordinary_document_in_the_same_base_still_reads(tmp_path) -> None:
@ -215,8 +217,9 @@ async def test_the_debate_path_refuses_a_verdict_document(tmp_path, monkeypatch)
assert captured, "the debate was never built"
read_file = next(t for t in captured[0] if getattr(t, "name", "") == "read_file")
with pytest.raises(VerdictLayerRefused):
await _invoke(read_file, bundle_id=bundle_id, path=_VERDICT_FILE)
answer = await _invoke(read_file, bundle_id=bundle_id, path=_VERDICT_FILE)
assert answer.startswith(f"REFUSED ({VerdictLayerRefused.__name__})")
assert _VERDICT_SENTINEL not in answer
# --------------------------------------------------------------- (3) the gated route survives
@ -324,10 +327,11 @@ async def test_an_unlinked_verdict_is_refused_too(tmp_path) -> None:
assert not any(f.name == "verdict-orphan.md" for f in okf.navigate_bundle(bundle_dir).files), (
"precondition: the orphan must be OUTSIDE the walk, or this arm proves nothing"
)
with pytest.raises(VerdictLayerRefused):
await _invoke(
_tools(bundle_dir)["read_file"], bundle_id=bundle_id, path="verdict-orphan.md"
)
answer = await _invoke(
_tools(bundle_dir)["read_file"], bundle_id=bundle_id, path="verdict-orphan.md"
)
assert answer.startswith(f"REFUSED ({VerdictLayerRefused.__name__})")
assert _VERDICT_SENTINEL not in answer
# ------------------------------------------------------------------------ (6) ladder intact