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:
parent
078a099898
commit
eb4137415c
13 changed files with 975 additions and 69 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
184
tests/test_explore_read_tools_refusal.py
Normal file
184
tests/test_explore_read_tools_refusal.py
Normal 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 på 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"]
|
||||
|
|
@ -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 ----------------------------------------------
|
||||
|
|
|
|||
179
tests/test_prepass_padding_collapse_loadbearing.py
Normal file
179
tests/test_prepass_padding_collapse_loadbearing.py
Normal 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 (kø-(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()
|
||||
|
|
@ -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 ------------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue