feat(run,generate): a run says what its delivered input can ground, before it spends an attempt [skip-docs]
P7 is right and landed, but re-measuring it exposed a consequence no row stated: with the gate live, 29 of 29 cost codes in 13 of 13 delivered proposals fall across the three free recordings (PM's denominator; 14 of 14 in 8 proposals on the PARSEABLE one -- the five blobs that separate the numbers are refused by pydantic's `claimed <= total` and never reach stage 0b). All 29 were invented, so the gate is right; but a gate that always refuses is as useless as one that never does. The cause is that the PROMPT asks for something the input cannot supply. `_build_messages` requires each affected_item to "restate a cost line as the project's price schedule already carries it", while K2's delivered input carries 9 occurrences / 2 distinct code-shaped tokens -- `SHA-01`/`SHA-10`, both document numbers off a page footer -- and `derive_cost_baseline` refuses the base outright. There is no cost line in it to restate. `GroundingOffer(chars, identifiers, cost_lines)` reports it. The PAIR is the diagnosis: "50 identifiers, 0 cost lines" says what neither number says alone. A REPORT, never a gate -- it blocks nothing, because a blocking requirement IS `--require-cost-baseline` (F4/D-3, opt-in, untouched), and `_ground_against_input` is untouched. The callsite is MEASURED, not chosen: `generate.py` composes the grounding per attempt, after `await _fetch_parsed`, so a report there could only speak once an attempt had been paid for; `run.py` binds both halves above the `--live-dry-run` cut and before the first `debate.run`, so the FREE trip says it. `delivered` is bound ONCE and the same variable feeds the report and `_evaluate`; the report composes THROUGH `_grounding_text`, the gate's own composer. A pattern is admissible here and not in the gate, and that is the difference between a report and a falsifier: an unknown form is a token left uncounted -- an under-count, never a false rejection. The forms are transcribed from the measurement; bare numbers are excluded with the number (46 394 / 2 117 in K2). `grounding_offer_notice` is the ONE renderer and is silent when the run CAN anchor -- omission, never an empty row. Load-bearing MEASURED (tests/test_grounding_offer_loadbearing.py, 12 arms), nine mutations all red against the WHOLE suite + green control 1570/5 (from 1558/5, strict superset, 0 removed) and the golden byte-unchanged (shasum -a 1 of the CONTENT = ea8c534773acdbe41ae68f2c55724d69aaf8be4f). Measurement: docs/2026-09-09-p8-forankringstilbudet.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
277bb95777
commit
455d611660
5 changed files with 767 additions and 2 deletions
|
|
@ -80,7 +80,12 @@ from portfolio_optimiser.explore import (
|
|||
tool_call_payload,
|
||||
trace_payload,
|
||||
)
|
||||
from portfolio_optimiser.generate import ParseFailure, generate_via_llm
|
||||
from portfolio_optimiser.generate import (
|
||||
GroundingOffer,
|
||||
ParseFailure,
|
||||
generate_via_llm,
|
||||
grounding_offer,
|
||||
)
|
||||
from portfolio_optimiser.ir import SavingsProposal
|
||||
from portfolio_optimiser.mandate import (
|
||||
OWN_PROPOSAL_ID,
|
||||
|
|
@ -248,6 +253,15 @@ class RunResult:
|
|||
#: 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
|
||||
#: What this run's DELIVERED input could ground an ``affected_item`` code in (P8): the size of
|
||||
#: the text P7's gate searched, how many distinct identifiers of a measured form it carries,
|
||||
#: and how many cost lines the run can anchor one of them AS.
|
||||
#:
|
||||
#: A RUN-level fact settled ONCE, before any candidate exists, which is why it is here and not
|
||||
#: on ``ProvenanceStamp`` (that describes the gate that judged ONE candidate) — ``skipped_links``'
|
||||
#: own placement rule. DEFAULTED, ``prepass``' half of the rule: ``None`` is the true statement
|
||||
#: "no measurement was made", and there is exactly one place that makes it.
|
||||
grounding_offer: GroundingOffer | None = None
|
||||
|
||||
@property
|
||||
def verdict_key(self) -> str:
|
||||
|
|
@ -313,6 +327,12 @@ class DryRunReport:
|
|||
#: 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
|
||||
#: What the delivered input of a REAL run of this configuration could ground an
|
||||
#: ``affected_item`` code in (P8). Carried here for ``cost_baseline_anchored``'s reason and,
|
||||
#: more sharply, because this is the surface on which "before it spends its three attempts"
|
||||
#: is provable at all: the dry-run cut returns before the first model call, so a dry run that
|
||||
#: reports a null offer has said the run cannot succeed WITHOUT paying to find out.
|
||||
grounding_offer: GroundingOffer | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -781,6 +801,37 @@ def cost_baseline_notice(anchored: bool) -> str | None:
|
|||
return None if anchored else _UNANCHORED_NOTICE
|
||||
|
||||
|
||||
def grounding_offer_notice(offer: GroundingOffer | None) -> str | None:
|
||||
"""Render the one line that says what this run's delivered input can ground, or ``None`` when
|
||||
there is nothing to warn about (P8).
|
||||
|
||||
ONE renderer with N callsites, never N copies of the wording (kø-(p)), taking the
|
||||
ALREADY-MEASURED value rather than a text: a renderer that re-composed the grounding would be
|
||||
a second resolution of the same rule, free to drift from the run it describes
|
||||
(``cost_baseline_notice``'s rule).
|
||||
|
||||
``None`` when the run CAN anchor a cost line — omission, never an empty row
|
||||
(``mandate.announce``'s rule) — and ``None`` on ``None``, which is the honest reading of "no
|
||||
measurement was made". This is deliberately NOT ``proposal_review_notice``'s deviation: there,
|
||||
silence on zero was ambiguous; here, a run that can anchor its lines has nothing to report that
|
||||
the outcome does not already say.
|
||||
|
||||
BOTH numbers, because the pair is the diagnosis. "0 cost lines" alone reads as a restatement of
|
||||
``cost_baseline_notice``; "50 identifiers" alone reads as good news. Together they say the
|
||||
thing P8 measured: the input offers plenty to cite and nothing to cost, while the proposer
|
||||
prompt asks for a cost line.
|
||||
|
||||
English, like every other line this CLI prints."""
|
||||
if offer is None or offer.cost_lines > 0:
|
||||
return None
|
||||
return (
|
||||
f" Grounding offer: {offer.identifiers} distinct identifier(s) and "
|
||||
f"{offer.cost_lines} cost line(s) in the {offer.chars} characters this run was given — "
|
||||
"the proposer is asked to restate a cost line this input does not carry, so every "
|
||||
"candidate it invents will be refused as ungrounded"
|
||||
)
|
||||
|
||||
|
||||
def bundle_id_notice(resolved: okf.ResolvedBundleId | None) -> str | None:
|
||||
"""Render the one line that says a base was mounted under a name it does not answer to, or
|
||||
``None`` when there is nothing to say.
|
||||
|
|
@ -1166,6 +1217,17 @@ async def run_project(
|
|||
"--derive-cost-baseline when the base carries a priced schedule"
|
||||
)
|
||||
|
||||
# P8: what this run was GIVEN, composed ONCE. Bound HERE and not inside ``_evaluate`` below,
|
||||
# and that placement is the measurement this seam rests on: this is the first point at which
|
||||
# both halves exist AND it is above the ``--live-dry-run`` cut, so the offer can be reported
|
||||
# on the FREE trip — before the first model call at ``debate.run``, let alone the three
|
||||
# generation attempts. ``generate.py``'s own composition happens per attempt, AFTER
|
||||
# ``_fetch_parsed`` has returned, so a report from there could only ever speak once an attempt
|
||||
# had been paid for. ONE binding feeding both the report and the gate: two compositions of one
|
||||
# text are free to disagree, which is exactly what a report must not be able to do (kø-(p)).
|
||||
delivered = "\n".join([context, bundle_grounding])
|
||||
offer = grounding_offer(project, baseline, delivered)
|
||||
|
||||
# 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
|
||||
|
|
@ -1239,6 +1301,9 @@ async def run_project(
|
|||
bundle_id_source=resolved_bundle_id,
|
||||
skipped_links=skipped_links,
|
||||
prepass=prepass_declaration,
|
||||
# P8, and this surface is the point: the offer is measured ABOVE this cut, so a dry
|
||||
# run reports it having made no model call at all.
|
||||
grounding_offer=offer,
|
||||
)
|
||||
# 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
|
||||
|
|
@ -1378,7 +1443,7 @@ async def run_project(
|
|||
# ``context`` is the DELIVERED rendering (pre-pass cut / bundle pointer / retrieved
|
||||
# chunks) — never ``gen_context``, which on the debate path is the model's own
|
||||
# summary and would let a code the debate invented ground the proposal repeating it.
|
||||
grounding="\n".join([context, bundle_grounding]),
|
||||
grounding=delivered,
|
||||
)
|
||||
refinements.extend(generated.refinements)
|
||||
return generated.outcome
|
||||
|
|
@ -1587,6 +1652,9 @@ async def run_project(
|
|||
debate_tool_calls=tuple(debate_tool_calls),
|
||||
expert_revisions=tuple(expert_reviews),
|
||||
prepass=prepass_declaration,
|
||||
# P8: read off the SAME single measurement the gate's own grounding descends from, so the
|
||||
# record and the refusals cannot describe different inputs.
|
||||
grounding_offer=offer,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -3836,6 +3904,12 @@ def main(argv: list[str] | None = None) -> int:
|
|||
notice = cost_baseline_notice(report.cost_baseline_anchored)
|
||||
if notice is not None:
|
||||
print(notice)
|
||||
# P8, printed next to the line it qualifies: "stage 0 is skipped" says the gate lost a
|
||||
# falsifier; this says what the input could have offered it instead. On the FREE trip, so
|
||||
# an operator learns a run cannot be grounded without paying three attempts to find out.
|
||||
offer_notice = grounding_offer_notice(report.grounding_offer)
|
||||
if offer_notice is not None:
|
||||
print(offer_notice)
|
||||
# The second measured silence on this surface: a bundle with an unfollowable cross-link
|
||||
# dry-ran to rc 0 with nothing said, so a half-read base looked exactly like a small one.
|
||||
nav_notice = skipped_links_notice(report.skipped_links)
|
||||
|
|
@ -3925,6 +3999,11 @@ def main(argv: list[str] | None = None) -> int:
|
|||
notice = cost_baseline_notice(result.provenance.cost_baseline_anchored)
|
||||
if notice is not None:
|
||||
print(notice)
|
||||
# Same renderer on the full run, read off the run's OWN measurement: a run that spent every
|
||||
# attempt being refused as ungrounded is exactly where the input-side fact costs the most.
|
||||
offer_notice = grounding_offer_notice(result.grounding_offer)
|
||||
if offer_notice is not None:
|
||||
print(offer_notice)
|
||||
# Same renderer on the full run, and deliberately so: a run that PRODUCED a proposal from a
|
||||
# half-read base is where the silence cost the most — the dry run at least produced nothing.
|
||||
nav_notice = skipped_links_notice(result.skipped_links)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue