feat(row6): a proposal whose approach declared no requirement is unsupported
Stress round 6 validated three falsification arms, and every validated approach rested only on run-level declarations nobody can attribute to one approach. declare_requirement now takes a required approach_id (a mandate id or own-proposal; an unknown id is refused naming the valid ones), and a ValidatedProposal whose approach has neither a mandate requirement nor a declaration under its own id becomes validator.Unsupported - a Rejection subclass carrying the validator's own ruling, reported as `unsupported` in coverage, the outcome artefact, the settlement and the judge, and never counted or summed. The rule is active whenever the debate held the declaration tool, the micro base included; the road and pre-pass paths are untouched. Declaration quality is not judged, so the rule can be satisfied by declaring any document the run read. The v1 gate's row 6 probes pass; its artefact half reads IKKE MÅLT because stress round 6 predates approach-addressed declarations, and IKKE MÅLT is never green - it fails the exit code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9847e014e7
commit
938a1ca30e
23 changed files with 718 additions and 115 deletions
|
|
@ -123,8 +123,10 @@ from portfolio_optimiser.provenance import ProvenanceStamp
|
|||
from portfolio_optimiser.reference_domain import Project, load_reference_projects
|
||||
from portfolio_optimiser.tracing import TracingConfigError, configure_tracing, tracing_notice
|
||||
from portfolio_optimiser.validator import (
|
||||
UNSUPPORTED_REASON,
|
||||
Grounding,
|
||||
Rejection,
|
||||
Unsupported,
|
||||
ValidatedProposal,
|
||||
baseline_from_project,
|
||||
classify_codes,
|
||||
|
|
@ -410,7 +412,10 @@ def _coverage_row(
|
|||
) -> ApproachOutcome:
|
||||
"""One coverage row from one evaluated approach. A rejection carries the validator's reason
|
||||
verbatim — a bare status would tell the expert their approach failed without telling them why,
|
||||
which is the part they can actually act on."""
|
||||
which is the part they can actually act on. An ``Unsupported`` outcome (row 6) is checked
|
||||
first: it subclasses ``Rejection``, and its own status is the whole point of the class."""
|
||||
if isinstance(outcome, Unsupported):
|
||||
return ApproachOutcome(id=row_id, label=label, status="unsupported", detail=outcome.reason)
|
||||
if isinstance(outcome, ValidatedProposal):
|
||||
return ApproachOutcome(
|
||||
id=row_id,
|
||||
|
|
@ -490,6 +495,15 @@ def evaluate_mandate_candidates(
|
|||
return tuple(rows)
|
||||
|
||||
|
||||
def _declarable_ids(mandate: Mandate | None) -> list[str]:
|
||||
"""The approach ids a declaration may be filed under (row 6): the mandate's own, plus the run's
|
||||
own proposal whenever the run makes one — always without a mandate."""
|
||||
ids = [a.id for a in mandate.approaches] if mandate is not None else []
|
||||
if mandate is None or mandate.allow_own_proposals:
|
||||
ids.append(OWN_PROPOSAL_ID)
|
||||
return ids
|
||||
|
||||
|
||||
def _select_outcome(
|
||||
produced: list[tuple[int, ValidatedProposal | Rejection]],
|
||||
) -> ValidatedProposal | Rejection:
|
||||
|
|
@ -511,6 +525,7 @@ async def _evaluate_mandate(
|
|||
mandate: Mandate,
|
||||
evaluate: Callable[[Approach | None], Awaitable[ValidatedProposal | Rejection]],
|
||||
budget_stops: list[str] | None = None,
|
||||
declared: Sequence[DeclaredRequirement] | None = None,
|
||||
) -> tuple[
|
||||
ValidatedProposal | Rejection,
|
||||
tuple[ApproachOutcome, ...],
|
||||
|
|
@ -532,6 +547,14 @@ async def _evaluate_mandate(
|
|||
become ``not_evaluated`` rows. But if the very first approach exhausts the budget there is
|
||||
nothing honest to return, so ``BudgetExceeded`` propagates exactly as it did before — a run
|
||||
that produced nothing must still fail loudly rather than hand back an empty report.
|
||||
|
||||
**Row 6: a validated outcome must rest on ITS approach's own declaration.** When ``declared`` is
|
||||
given (the rule is active — see ``run_project``), a ``ValidatedProposal`` whose approach carries
|
||||
no ``requirement`` of its own and has no declaration filed under its id becomes
|
||||
``Unsupported``: the numbers held, the ground was never named. A declaration made for another
|
||||
approach does not stand in, and nothing about the declaration's QUALITY is judged — a
|
||||
requirement the model declared is accepted whatever it says (P22 § 4: an overlap gate would
|
||||
refuse legitimate proposals). ``declared`` is read at evaluation time, after the debate.
|
||||
"""
|
||||
plan: list[tuple[str, str, Approach | None]] = [(a.id, a.label, a) for a in mandate.approaches]
|
||||
if mandate.allow_own_proposals:
|
||||
|
|
@ -563,6 +586,15 @@ async def _evaluate_mandate(
|
|||
for rid, lbl, _ in plan[index:]
|
||||
)
|
||||
break
|
||||
if (
|
||||
declared is not None
|
||||
and isinstance(outcome, ValidatedProposal)
|
||||
and not (approach is not None and approach.requirement is not None)
|
||||
and row_id not in {d.approach_id for d in declared}
|
||||
):
|
||||
outcome = Unsupported(
|
||||
proposal=outcome.proposal, reason=UNSUPPORTED_REASON, validated=outcome
|
||||
)
|
||||
produced.append((index, outcome))
|
||||
evaluated.append((row_id, outcome))
|
||||
rows.append(_coverage_row(row_id, label, outcome))
|
||||
|
|
@ -622,8 +654,10 @@ def _bundle_pointer(bundle: okf.Bundle, bundle_id: str, *, dimension: str | None
|
|||
"requirement number or path contains that word, and reports 'total_matches'.\n"
|
||||
"Before you settle on a measure, name the ONE requirement of this base that BINDS it: "
|
||||
"find it with a filter, read it with read_file, then call "
|
||||
f"declare_requirement({bundle_id!r}, path, ref) with the requirement's own number. A "
|
||||
"declaration naming a document this run never opened is refused; reading it is the fix."
|
||||
f"declare_requirement({bundle_id!r}, path, ref, approach_id) with the requirement's own "
|
||||
"number and the id of the approach it binds (the run's own proposal is 'own-proposal'). "
|
||||
"Declare once per approach: a proposal whose approach declared nothing is not validated. "
|
||||
"A declaration naming a document this run never opened is refused; reading it is the fix."
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1184,6 +1218,12 @@ async def run_project(
|
|||
#: documents above, so the gate's vocabulary and the gate's text describe one reading of one
|
||||
#: base. Empty on the road path, which is what keeps the rule unable to fire there.
|
||||
bundle_references: tuple[str, ...] = ()
|
||||
#: Row 6: whether the declaration rung was offered to the debate. The rule that a validated
|
||||
#: proposal needs its approach's own declaration is active exactly when it was: a run that
|
||||
#: could declare and did not is the silence the rule exists for. It is NOT relaxed for a base
|
||||
#: without requirement numbers — any document the run read can be declared, and a base that
|
||||
#: holds nothing worth declaring is a finding about the base, not a reason to validate.
|
||||
requirement_rung = False
|
||||
# S2c: a CALLER-OWNED sink for what the debate opens (the ``parse_failures``/``ExplorationTrace``
|
||||
# shape). A returned value would be lost on exactly the run that most needs the evidence — a
|
||||
# budget stop mid-debate raises out of ``debate.run`` and constructs no ``RunResult`` at all.
|
||||
|
|
@ -1300,8 +1340,11 @@ async def run_project(
|
|||
# names every direction the run carries rather than picking one it cannot
|
||||
# attribute. Without a mandate this is empty and the reply is unchanged.
|
||||
labels=[a.label for a in mandate.approaches] if mandate else (),
|
||||
# Row 6: the ids a declaration may be filed under.
|
||||
approach_ids=_declarable_ids(mandate),
|
||||
)
|
||||
)
|
||||
requirement_rung = True
|
||||
# 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
|
||||
|
|
@ -1588,7 +1631,10 @@ async def run_project(
|
|||
validator_outcome = await _evaluate(None)
|
||||
else:
|
||||
validator_outcome, coverage, evaluated = await _evaluate_mandate(
|
||||
mandate, _evaluate, budget_stops
|
||||
mandate,
|
||||
_evaluate,
|
||||
budget_stops,
|
||||
declared=debate_requirements if requirement_rung else None,
|
||||
)
|
||||
except BaseException as stop:
|
||||
# Recorded and re-raised UNTOUCHED. This arm decides nothing about the exception itself —
|
||||
|
|
@ -1679,7 +1725,9 @@ async def run_project(
|
|||
model=model,
|
||||
role="proposer",
|
||||
validator_decision=(
|
||||
"validated" if isinstance(validator_outcome, ValidatedProposal) else "rejected"
|
||||
"validated"
|
||||
if isinstance(validator_outcome, (ValidatedProposal, Unsupported))
|
||||
else "rejected"
|
||||
),
|
||||
token_usage=meter.tokens,
|
||||
# Whether stage 0 of the deterministic gate had a baseline to reconcile against. Read off
|
||||
|
|
@ -1799,7 +1847,7 @@ async def run_project(
|
|||
update={
|
||||
"validator_decision": (
|
||||
"validated"
|
||||
if isinstance(approach_outcome, ValidatedProposal)
|
||||
if isinstance(approach_outcome, (ValidatedProposal, Unsupported))
|
||||
else "rejected"
|
||||
),
|
||||
# P20: ``code_forms`` must follow ITS OWN approach too, for
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue