feat(prepass): the debate is handed the declared cut and the navigator tools are withdrawn [skip-docs]
[skip-docs]: CLI-flagget og README-blokka kommer i steg 6/7; `prepass_payload` er foreloepig bare naabar for en bibliotek-kaller. `run_project(prepass_payload=...)` forgrener bundle-armen. UTEN payload er hver linje uendret -- pekeren, de fire verktoeyene, siteringer over hele den navigerte basen. MED et payload faar debatten et DEKLARERT KUTT og verktoeyene trekkes (SS 2.2: kontekst pre-passet holdt tilbake ble holdt tilbake med vilje; en debatt som holder BEGGE er fri til aa gaa rundt kuttet den nettopp erklaerte). Nekten PROPAGERER, aldri en stille degradering tilbake til pekeren -- `load_mandate`s regel. Maalt paa NULL modellkall, ikke paa exit-koden. `delivered == 0` nektes ved NAVN foer debatten, med nevnerne, spoersmaalet og ref-en sitert: maalt er den tilstanden bare naabar naar hvert konsept feilet leksikalsk (den andre tomme saken nekter produsenten selv), altsaa bevis for FRAVAER. Uten den falt kjoeringen gjennom til `run.py`s siteringsvakt, hvis melding navngir `docs_dir` -- som er `None` paa denne stien. `bundle_excerpt_citations` (datasource) siterer de LEVERTE konseptene alene: et stempel som siterer hele korpuset for et forslag som saa fire, gjenoppfinner den uerklaerte paastanden sømmen finnes for. Kroppen tas fra den NAVIGERTE `BundleFile`, ikke fra payloadets `text` -- den leverte teksten er NFC-normalisert med strippet hale, saa en locator over den ville ikke indeksert fila den navngir. Deler dermed ogsaa `bundle_citations`' verdict-eksklusjon i stedet for aa gjenta den. MCP-appenden ligger BEVISST under forgreningen: dette trekker navigatoerverktoeyene, ikke verktoeylista. Maalt: en tom liste naar traaden som `tools: None`, saa ingen uproevd tom-array-form innfoeres. `RunResult.prepass` og `DryRunReport.prepass` DEFAULTER (`skipped_links`-halvdelen: `None` er det sanne utsagnet "ingen payload ble gitt"), bundet i BEGGE grener saa ingen `NameError` venter paa veg-stien. `ProvenanceStamp` er BEVISST urørt -- stempelet beskriver gaten som doemte EN kandidat, dette er et RUN-nivaa-faktum om hva kjoeringen i det hele tatt fikk lese. Tilbaketrekkingen asserteres paa `fresh_workflow(tools=...)`, ALDRI paa `debate_tool_calls`: maalt er det sporet allerede tomt MED alle fire verktoeyene, fordi en `ScriptedChatClient` aldri emitterer et verktoeykall. En arm skrevet paa det kan ikke skille de to implementasjonene. GATE, IKKE VEGG: en egen arm beviser at den gatede ExpeL-folden fortsatt naar hypotese-prompten (0.82) under et payload. 1440 passed / 5 skipped (fra 1425/5, +15, 0 fjernet). ruff + mypy rene. Golden `shasum -a 1` av INNHOLDET = ea8c534773acdbe41ae68f2c55724d69aaf8be4f, BYTE-UENDRET. Co-Authored-By: Claude <claude-opus-5>
This commit is contained in:
parent
651c83f9df
commit
7aa06d581f
3 changed files with 513 additions and 7 deletions
|
|
@ -49,6 +49,7 @@ from portfolio_optimiser.contracts import GoalConfig, GoalContract, load_contrac
|
|||
from portfolio_optimiser.ledger import SavingsLedger, to_ore
|
||||
from portfolio_optimiser.datasource import (
|
||||
bundle_citations,
|
||||
bundle_excerpt_citations,
|
||||
chunk_dict_to_citation,
|
||||
make_retrieval_tool,
|
||||
retrieve_chunks,
|
||||
|
|
@ -118,7 +119,7 @@ from portfolio_optimiser.validator import (
|
|||
baseline_from_project,
|
||||
validate_proposal,
|
||||
)
|
||||
from portfolio_optimiser import hitl, okf, outbox
|
||||
from portfolio_optimiser import hitl, okf, outbox, prepass
|
||||
from portfolio_optimiser.semretrieval import (
|
||||
SEMANTIC_WEIGHT_DEFAULT,
|
||||
Embedder,
|
||||
|
|
@ -234,6 +235,17 @@ class RunResult:
|
|||
#: Carried HERE and on neither other carrier: ``ProvenanceStamp`` describes the gate that
|
||||
#: judged ONE candidate, and ``DryRunReport`` returns above generation entirely.
|
||||
expert_revisions: tuple[ProposalReview, ...] = ()
|
||||
#: The cut this run was GIVEN, when it was given one (order 20260907T080223Z): the base's ref,
|
||||
#: the question it was computed for, the three denominators and the withheld rules by count.
|
||||
#:
|
||||
#: DEFAULTED, unlike ``cost_baseline_anchored``, and the difference is the one that row states:
|
||||
#: both of that field's possible defaults would assert something about an event, whereas
|
||||
#: ``None`` here is the true statement "no payload was supplied" — and there is exactly one way
|
||||
#: to supply one. This is ``skipped_links``' half of the rule.
|
||||
#:
|
||||
#: Carried HERE and not on ``ProvenanceStamp``: the stamp describes the gate that judged ONE
|
||||
#: candidate, while this is a RUN-level fact about what the run was allowed to read at all.
|
||||
prepass: prepass.PrepassDeclaration | None = None
|
||||
|
||||
@property
|
||||
def verdict_key(self) -> str:
|
||||
|
|
@ -294,6 +306,11 @@ class DryRunReport:
|
|||
#: (and both claims would sometimes be false), while a missing trace asserts only that the event
|
||||
#: list is empty. The road path navigates no bundle, so empty is literally true there too.
|
||||
skipped_links: tuple[okf.SkippedLink, ...] = ()
|
||||
#: The cut a dry run was given, when it was given one. Carried here for ``skipped_links``'
|
||||
#: reason and NOT for ``unkeyed_verdicts``' one: the dry-run cut returns BELOW the fork that
|
||||
#: resolves a payload, so a dry run can honestly report what it would have read — whereas a
|
||||
#: field resolved above that cut could only ever report zero.
|
||||
prepass: prepass.PrepassDeclaration | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -889,6 +906,14 @@ async def run_project(
|
|||
#: every commissioned approach, and a ``revise`` buys ONE more attempt out of the budget the
|
||||
#: loop already has. It mints no verdict and gates nothing (F2).
|
||||
proposal_reviewer: ProposalReviewer | None = None,
|
||||
#: A verified OKF consumption pre-pass payload (order 20260907T080223Z). Bundle path only, and
|
||||
#: OPT-IN by construction: ``None`` (the default) leaves the debate on ``_bundle_pointer``'s
|
||||
#: pointer and the four navigator tools, byte-identically. Given one, the debate is handed a
|
||||
#: DECLARED CUT and the tools are withdrawn — a debate holding both would be free to walk
|
||||
#: around the cut it just declared, which is the undeclared cut in a costume (contract SS 2.2).
|
||||
#: A LOADED object, never a path: the library seam takes the validated artefact and the CLI
|
||||
#: owns the file, exactly as ``mandate=`` and ``dimension=`` already do.
|
||||
prepass_payload: prepass.PrepassPayload | None = None,
|
||||
) -> RunResult | DryRunReport:
|
||||
"""Run the vertical slice for ONE project. ``client_factory`` is the test-injection seam
|
||||
(defaults to the real backend). ``verdict_input`` carries the expert decision/rationale
|
||||
|
|
@ -957,6 +982,14 @@ async def run_project(
|
|||
# * bundle path: anchored only when the bundle SHIPS a ``cost-baseline.json``. A bundle written
|
||||
# before the amendment (every commons-owned golden) is legitimately un-anchored -> None =
|
||||
# pre-S4.0 behaviour. A baseline that exists but is malformed still raises (fail-closed).
|
||||
if prepass_payload is not None and bundle_dir is None:
|
||||
# Hoisted above the arm below, and refused rather than ignored: a payload is a cut OF a
|
||||
# knowledge base, and the road path has none for it to agree with. A silently dropped
|
||||
# payload would leave the caller with a navigating run that reported a declared one.
|
||||
raise prepass.PrepassRefused(
|
||||
"a pre-pass payload declares a cut of a knowledge base, so it needs the bundle it "
|
||||
"was cut from; this run was given no bundle_dir"
|
||||
)
|
||||
if bundle_dir is not None:
|
||||
bundle = okf.navigate_bundle(bundle_dir)
|
||||
# ONE bundle-id rule (Step 10, slackened S7a-3 pkt. 1): the DECLARED id is the identity and
|
||||
|
|
@ -985,15 +1018,63 @@ async def run_project(
|
|||
# tools the exploration uses; ``gen_context = debate_output or context`` below means the
|
||||
# generation fallback is bounded by the same change rather than by a second policy.
|
||||
dimension_id = dimension.id if dimension else None
|
||||
context = _bundle_pointer(bundle, resolved.id, dimension=dimension_id)
|
||||
citations = bundle_citations(bundle)
|
||||
# The pre-pass fork (order 20260907T080223Z). WITHOUT a payload every line below is what
|
||||
# it was: the pointer, the four tools, and citations over the whole navigated base.
|
||||
#
|
||||
# WITH one, the debate is handed a DECLARED CUT and the tools are withdrawn. The refusal
|
||||
# PROPAGATES — never a silent degrade back to the pointer, which is ``load_mandate``'s
|
||||
# rule: a caller who asked for a declared cut and got a navigating run instead was
|
||||
# answered by a silently downgraded order.
|
||||
prepass_declaration: prepass.PrepassDeclaration | None = None
|
||||
debate_tools: list[Any]
|
||||
if prepass_payload is not None:
|
||||
prepass.check_payload_shape(prepass_payload)
|
||||
prepass.verify_against_bundle(
|
||||
prepass_payload,
|
||||
bundle_dir=bundle_dir,
|
||||
resolved_id=resolved,
|
||||
dimension=dimension_id,
|
||||
)
|
||||
if not prepass_payload.excerpts:
|
||||
# Measured: ``delivered == 0`` is reachable only when every concept failed to
|
||||
# match (the producer REFUSES the other empty case, where concepts matched and the
|
||||
# budget admitted none). So this is evidence of ABSENCE for this question at this
|
||||
# ref, and saying it is better than two silent alternatives: an empty prompt, or
|
||||
# falling through to the citation guard below, whose message names ``docs_dir`` —
|
||||
# ``None`` on this path. SS 7.3's own posture: the skill stops and says so.
|
||||
raise prepass.PrepassRefused(
|
||||
f"the pre-pass delivered 0 of {prepass_payload.denominators.considered} "
|
||||
f"concepts for the question {prepass_payload.question!r} at ref "
|
||||
f"{prepass_payload.bundle.ref}; an empty cut is evidence that this knowledge "
|
||||
"base does not answer that question, not something to run a debate over"
|
||||
)
|
||||
prepass_declaration = prepass.declaration_of(prepass_payload)
|
||||
context = prepass.render_context(prepass_payload)
|
||||
# Citations over the DELIVERED concepts alone: a stamp citing the whole corpus for a
|
||||
# proposal that saw eight documents re-creates the undeclared claim this seam removes.
|
||||
# Built from the MOUNTED bodies in ``bundle_citations``' own shape, so ``snippet ==
|
||||
# body[start:end]`` stays exact by construction rather than indexing normalised text.
|
||||
citations = bundle_excerpt_citations(
|
||||
bundle, [excerpt.concept_id for excerpt in prepass_payload.excerpts]
|
||||
)
|
||||
# Contract SS 2.2: "Context the pre-pass withheld was withheld deliberately." A debate
|
||||
# holding both the payload and the ladder could walk around the cut it just declared.
|
||||
# Measured: an empty list reaches the wire as ``tools: None``, so no untested empty-
|
||||
# array form is introduced. The MCP append BELOW this fork is deliberately untouched —
|
||||
# this withdraws the navigator tools, not the tool list.
|
||||
debate_tools = []
|
||||
else:
|
||||
context = _bundle_pointer(bundle, resolved.id, dimension=dimension_id)
|
||||
citations = bundle_citations(bundle)
|
||||
# §4.1a context-scope, carried over: the agents read ONLY dimension-matched knowledge.
|
||||
# The filter used to live in the rendering; with navigation it lives in the TOOLS, on
|
||||
# both rungs (``navigator_tools``' own gate), because that is now where the bytes
|
||||
# leave. Under a payload the SAME two gates are re-raised by
|
||||
# ``prepass.verify_against_bundle`` on the mounted documents instead.
|
||||
debate_tools = list(navigator_tools([bundle_dir], dimension=dimension_id))
|
||||
# What the navigation could NOT reach, taken from the run's ONE walk. The road path below
|
||||
# navigates no bundle at all, so its empty tuple is literally true rather than a stand-in.
|
||||
skipped_links: tuple[okf.SkippedLink, ...] = bundle.skipped
|
||||
# §4.1a context-scope, carried over: the agents read ONLY dimension-matched knowledge. The
|
||||
# filter used to live in the rendering; with navigation it lives in the TOOLS, on both
|
||||
# rungs (``navigator_tools``' own gate), because that is now where the bytes leave.
|
||||
debate_tools: list[Any] = list(navigator_tools([bundle_dir], dimension=dimension_id))
|
||||
else:
|
||||
project = _project_by_id(project_id)
|
||||
baseline = baseline_from_project(project)
|
||||
|
|
@ -1003,6 +1084,10 @@ async def run_project(
|
|||
skipped_links = ()
|
||||
# No knowledge base, so no bundle identity — said by ABSENCE rather than by minting one.
|
||||
resolved_bundle_id = None
|
||||
# Bound in BOTH branches for ``resolved_bundle_id``'s reason: the ``DryRunReport`` and the
|
||||
# ``RunResult`` below read it unconditionally, and a name bound in one arm only is a
|
||||
# ``NameError`` waiting for the other caller.
|
||||
prepass_declaration = None
|
||||
debate_tools = [make_retrieval_tool(docs_dir, top_k=top_k)]
|
||||
|
||||
# Trekk B2 (krav 3): configured MCP servers become tools the AGENTS can call during the debate.
|
||||
|
|
@ -1077,6 +1162,7 @@ async def run_project(
|
|||
cost_baseline_anchored=baseline is not None,
|
||||
bundle_id_source=resolved_bundle_id,
|
||||
skipped_links=skipped_links,
|
||||
prepass=prepass_declaration,
|
||||
)
|
||||
# The MCP lifecycle (Trekk B2): entered HERE, after the dry-run cut above, so a dry run never
|
||||
# opens a connection — its promise to stop before the first call covers egress too. Constructed
|
||||
|
|
@ -1407,6 +1493,7 @@ async def run_project(
|
|||
unkeyed_verdicts=unkeyed_verdicts,
|
||||
debate_tool_calls=tuple(debate_tool_calls),
|
||||
expert_revisions=tuple(expert_reviews),
|
||||
prepass=prepass_declaration,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue