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:
parent
76b939b3b8
commit
9232f94041
8 changed files with 759 additions and 2 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -727,6 +727,27 @@ def _default_factory(profile: Profile | str) -> Callable[[str], BaseChatClient]:
|
|||
return factory
|
||||
|
||||
|
||||
class UnanchoredRunRefused(ValueError):
|
||||
"""A run was required to be anchored (F4) and the bundle offered no cost baseline.
|
||||
|
||||
Measured live (``docs/2026-09-07-syretest-s7-prepass-k2.md`` § 8): all three paid arms stamped
|
||||
``cost_baseline_anchored: False``, so the validator's stage 0 — the one stage that tells a
|
||||
fabricated cost line from a real one — was skipped, and each arm invented its codes
|
||||
(``ENGRAVE_MARK``, ``RITB-HOURS``, ``Material_Cost_Concrete``). The run SAID so on stdout
|
||||
(``cost_baseline_notice``), so this was never a silence; what visibility cannot do is stop a
|
||||
machine-readable artefact reading ``validator_decision: validated`` over lines nothing anchored.
|
||||
|
||||
OPT-IN, never a default: a bundle written before the S4.0 amendment — every commons-owned
|
||||
golden — is legitimately un-anchored, and making the requirement the default would refuse them
|
||||
all. A caller who needs the guarantee asks for it by name and composes it with
|
||||
``--derive-cost-baseline`` when the base carries a priced schedule instead of the file.
|
||||
|
||||
A ``ValueError``, the ``BundleIdMismatch``/``CostBaselineDerivationError`` precedent: an argv
|
||||
that is wrong about what this base can offer belongs on the CLI's refusal tuple and hosting's
|
||||
400 arm, never on the crash channel.
|
||||
"""
|
||||
|
||||
|
||||
#: The one line a run prints about its own anchoring. Rendered ONLY when the run is un-anchored:
|
||||
#: an anchored run has nothing to warn about, and ``mandate.announce``'s rule is that a line for
|
||||
#: something the run does not have is OMITTED rather than rendered empty.
|
||||
|
|
@ -923,6 +944,7 @@ async def run_project(
|
|||
#: Bundle path only, and OPT-IN by construction: the default leaves every existing run on the
|
||||
#: file loader, byte-identically.
|
||||
derive_cost_baseline: bool = False,
|
||||
require_cost_baseline: bool = False,
|
||||
dimension: Dimension | None = None,
|
||||
store: VerdictStore | None = None,
|
||||
verdict_dir: str | None = None,
|
||||
|
|
@ -1122,6 +1144,19 @@ async def run_project(
|
|||
prepass_declaration = None
|
||||
debate_tools = [make_retrieval_tool(docs_dir, top_k=top_k)]
|
||||
|
||||
# F4: the anchoring REQUIREMENT, opt-in and checked here — the one point at which both
|
||||
# branches have bound ``baseline``, and above the dry-run cut below, so the FREE trip refuses
|
||||
# too. Before the first model call by construction: at the exit code a refusal after the spend
|
||||
# is indistinguishable from one before it (session 57's rule). The road path is anchored by
|
||||
# construction, so on it this can only pass.
|
||||
if require_cost_baseline and baseline is None:
|
||||
raise UnanchoredRunRefused(
|
||||
"this run was required to be anchored, but the knowledge base offers no cost "
|
||||
"baseline: without one the validator's stage 0 is skipped and nothing ties a proposed "
|
||||
"cost line to this project. Ship a cost-baseline.json, or pass "
|
||||
"--derive-cost-baseline when the base carries a priced schedule"
|
||||
)
|
||||
|
||||
# Trekk B2 (krav 3): configured MCP servers become tools the AGENTS can call during the debate.
|
||||
# Appended to BOTH paths — on the bundle path they are the first tools that path has ever had.
|
||||
# Constructed here but NOT connected: an ``MCPTool`` is an async context manager, so the run
|
||||
|
|
@ -2554,6 +2589,16 @@ def main(argv: list[str] | None = None) -> int:
|
|||
"guesses: an unpriced or ambiguous schedule stops the run"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--require-cost-baseline",
|
||||
action="store_true",
|
||||
help=(
|
||||
"REFUSE the run unless the validator's stage 0 has a cost baseline to reconcile "
|
||||
"against (F4). Opt-in: a bundle that ships none is legitimately un-anchored and runs "
|
||||
"unchanged without this flag. Combine with --derive-cost-baseline to satisfy it from "
|
||||
"a priced schedule inside the base"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--proposals-from-mandate",
|
||||
action="store_true",
|
||||
|
|
@ -2646,6 +2691,9 @@ def main(argv: list[str] | None = None) -> int:
|
|||
# Report mode returns before the run dispatch, so an omission here is a SILENT DROP,
|
||||
# not a refusal — the gap F4 measured on --plan-review.
|
||||
"--derive-cost-baseline": args.derive_cost_baseline,
|
||||
# Same reason, same rung: report mode returns above every run dispatch, so leaving it
|
||||
# out is a SILENT DROP of a guarantee the operator asked for by name.
|
||||
"--require-cost-baseline": args.require_cost_baseline,
|
||||
# Same reason, one flag later: report mode returns above the S7b dispatch too.
|
||||
"--proposals-from-mandate": args.proposals_from_mandate,
|
||||
"PROJECT_ID": args.project_id is not None,
|
||||
|
|
@ -2737,6 +2785,10 @@ def main(argv: list[str] | None = None) -> int:
|
|||
# who wrote --portfolio --derive-cost-baseline to add the one flag this mode also
|
||||
# refuses. Same reason --explore is listed here rather than left to fall through.
|
||||
"--derive-cost-baseline": args.derive_cost_baseline,
|
||||
# It guards ONE base's anchoring, and the portfolio pass takes the road path, which is
|
||||
# anchored by construction — so here the flag could only ever pass. BY NAME rather
|
||||
# than falling through to the --bundle-dir requirement, its neighbours' reason.
|
||||
"--require-cost-baseline": args.require_cost_baseline,
|
||||
# It reads ONE base's schedule and settles ONE commission against it, so it sits on the
|
||||
# same side of the partition as the flag it requires. BY NAME rather than falling
|
||||
# through to "requires --derive-cost-baseline": an operator who wrote --portfolio
|
||||
|
|
@ -2818,6 +2870,17 @@ def main(argv: list[str] | None = None) -> int:
|
|||
)
|
||||
return 1
|
||||
|
||||
# The road path's baseline comes from ``Project.cost_items`` and is anchored by construction,
|
||||
# so without a bundle this flag could never fire — and a flag that cannot fire is a claim the
|
||||
# surface makes about itself (the Fase-3 class). Refused BY NAME, its neighbour's reason.
|
||||
if not args.portfolio and args.require_cost_baseline and args.bundle_dir is None:
|
||||
print(
|
||||
"run refused: --require-cost-baseline requires --bundle-dir (the road path is already "
|
||||
"anchored by its own cost_items, so on it the requirement could never fire)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
# The SEEDING arm's three refusals, placed ABOVE the replacing arm's block on purpose: given
|
||||
# both flags, the block below would answer with "--prepass-payload and --explore cannot be
|
||||
# combined", which names neither of the two flags the operator actually put in conflict. At
|
||||
|
|
@ -3711,6 +3774,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
prepass_payload=prepass_payload,
|
||||
verdict_input=_verdict_input_from_args(args),
|
||||
derive_cost_baseline=args.derive_cost_baseline,
|
||||
require_cost_baseline=args.require_cost_baseline,
|
||||
mcp_servers=mcp_servers,
|
||||
live_dry_run=True,
|
||||
)
|
||||
|
|
@ -3781,6 +3845,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
verdict_input=_verdict_input_from_args(args),
|
||||
semantic_retrieval=args.semantic_retrieval,
|
||||
derive_cost_baseline=args.derive_cost_baseline,
|
||||
require_cost_baseline=args.require_cost_baseline,
|
||||
client_factory=scripted_client_factory,
|
||||
mandate=mandate,
|
||||
mcp_servers=mcp_servers,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue