feat(navigation,validator): read_dir names the rung that reads a document, and a run can require its anchoring

F3 and F4, the two findings the S7 acid test (session 98) reported and left. The order required
both descriptions to be treated as PREMISES. One held; the other was felled before anything was
built on it.

F3 -- premise FELLED, asymmetry real. The order read arm C's two refused calls as "the path names
a document that EXISTS". Measured against the base that ran: its root holds 27 directories named
del-ii-bilag-N-... and 12 documents named inbox-del-ii-bilag-N-....md, and the requested path
matches NEITHER -- it is the directory naming convention applied to a document whose real name
carries an inbox- prefix. So the two live rounds were the UNKNOWN-path class, and this delivery
does NOT recover them (gated). What IS real: read_file on a directory has named read_dir since
session 95, while read_dir on a document named neither the rung nor the path.
okf.DocumentPathRefused closes that one direction -- a ValueError, a SIBLING of BundlePathNotFound
rather than a subclass, built from context_files (never files) and through the same in_dimension
predicate the listing uses, quoting the document's REAL name so what it hands back resolves.

F4 -- premise HELD, option (c) felled by measurement. All four live artefacts stamped
cost_baseline_anchored: False and each arm invented its cost codes. derive_cost_baseline refuses
against the delivered base: K2's price schedule is a pandoc SIMPLE table with ONE column header,
so making --derive-cost-baseline reachable there would mean inventing a rule for an unmeasured
form -- MAJOR-4's own honesty limit. Chose (b) over (a): --require-cost-baseline /
run_project(require_cost_baseline=...), OPT-IN and never default, so every bundle without a
cost-baseline.json runs unchanged. The gate sits where both branches have bound baseline and ABOVE
the dry-run cut, so it fires on the free trip too and, on the paid one, before the first model
call. Three CLI refusals by name, each with an rc-0 control.

12 mutations, all red against the WHOLE suite. Green control 1493/5 -> 1511/5 (+18 node-ids, 0
removed); golden demo-transcript.stdout BYTE-UNCHANGED (shasum -a 1 of the CONTENT =
ea8c534773acdbe41ae68f2c55724d69aaf8be4f). No paid run: both findings measured offline.

Measurement: docs/2026-09-08-f3-f4-nekten-og-forankringen.md
Order: 20260908T020419Z-5837110336-from-portfolio-optimiser

Co-Authored-By: Claude <Opus 5>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-08 05:32:46 +02:00
commit 9232f94041
8 changed files with 759 additions and 2 deletions

View file

@ -1067,6 +1067,27 @@ class BundlePathNotFound(ValueError):
"""
class DocumentPathRefused(ValueError):
"""``read_dir`` was asked for a DOCUMENT — the wrong rung of the navigation ladder.
The symmetry of ``explore.DirectoryPathRefused``, which has answered the other direction since
the live K2 run of session 95: ``read_file`` on a directory names ``read_dir``, while
``read_dir`` on a document named neither the rung that reads it nor the path it would take. A
refusal that only says "no" leaves the caller a model choosing a path with the same next
move it just made.
A ``ValueError``, the ``BundlePathNotFound``/``DimensionScopeRefused`` precedent, and a SIBLING
of ``BundlePathNotFound`` rather than a subclass: "this path is a document" and "this path is
nothing" are different facts, and a caller switching on the first must not be answered by the
second.
**Not the class the live two rounds fell into** (``docs/2026-09-07-syretest-s7-prepass-k2.md``
§ 4): there the path named no document at all it was a document's name with its ``inbox-``
prefix dropped and it is still refused as unknown. Naming the nearest look-alike would be
guessing what a caller meant, which is invention rather than validation.
"""
def directory_listing(
bundle: Bundle, path: str = "", *, dimension: str | None = None
) -> dict[str, Any]:
@ -1130,6 +1151,24 @@ def directory_listing(
}
)
if prefix and not directories and not documents:
# The wrong RUNG, answered as such — the direction ``explore.DirectoryPathRefused`` already
# covers, measured absent here (F3). Built from ``context_files`` and through the SAME
# ``in_dimension`` predicate the listing above uses: a lookup over ``files`` would name a
# ``type: verdict`` document by path, advertising in a refusal the one layer no listing
# mentions, and one that ignored the scope would name a document this run would then refuse
# to open. The document's REAL name is quoted, never the caller's path, so what the refusal
# hands back resolves.
stem = path.strip("/")
candidate = stem if stem.endswith(".md") else stem + ".md"
named = next(
(f for f in bundle.context_files if f.name == candidate and in_dimension(f, dimension)),
None,
)
if named is not None:
raise DocumentPathRefused(
f"{path!r} in knowledge base {bundle.dir!r} is a document, not a directory; "
f"use read_file to read {named.name!r} whole"
)
raise BundlePathNotFound(
f"knowledge base {bundle.dir!r} has no directory {path!r}; it holds no concept "
"document under that path"