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:
Kjell Tore Guttormsen 2026-09-17 16:40:54 +02:00
commit 938a1ca30e
23 changed files with 718 additions and 115 deletions

View file

@ -38,7 +38,7 @@ from collections.abc import Callable
from pathlib import Path
from portfolio_optimiser import hitl
from portfolio_optimiser.mandate import OWN_PROPOSAL_ID, Approach, Mandate
from portfolio_optimiser.mandate import BindingRequirement, OWN_PROPOSAL_ID, Approach, Mandate
from portfolio_optimiser.run import run_project
from portfolio_optimiser.simulation import ScriptedChatClient
from portfolio_optimiser.verdicts import (
@ -66,8 +66,11 @@ def _reply(measure: str, claimed: int) -> str:
# Labels ABSENT from the bundle's own prose (the "LED-retrofit" trap: it appears in 6 bundle files,
# so a client keyed on it would match every prompt through the context and prove nothing).
_LED = Approach(id="led-retrofit", label="Behovsstyrt belysning i fellesarealer")
_HVAC = Approach(id="hvac-swap", label="Utskifting av ventilasjonsaggregat")
#: Row 6: a commissioned approach validates only on a requirement of its own. These tests are about
#: selection and per-approach artefacts, not about declarations, so the commission names one.
_REQ = BindingRequirement(path="tiltak-led-retrofit.md", ref="Krav 1")
_LED = Approach(id="led-retrofit", label="Behovsstyrt belysning i fellesarealer", requirement=_REQ)
_HVAC = Approach(id="hvac-swap", label="Utskifting av ventilasjonsaggregat", requirement=_REQ)
_LED_MEASURE = "Behovsstyrt belysning i fellesarealer"
_HVAC_MEASURE = "Utskifting av ventilasjonsaggregat"
@ -181,7 +184,8 @@ async def test_each_approach_outcome_is_written_with_its_own_status(tmp_path: Pa
assert by_approach["led-retrofit"]["outcome_type"] == "validated"
assert by_approach["hvac-swap"]["outcome_type"] == "rejected"
assert by_approach["hvac-swap"]["reason"], "a rejected approach must carry the reason"
assert by_approach[OWN_PROPOSAL_ID]["outcome_type"] == "validated"
# The run's own proposal names no requirement and none was declared for it (row 6).
assert by_approach[OWN_PROPOSAL_ID]["outcome_type"] == "unsupported"
async def test_each_artefact_stamps_its_own_validator_decision(tmp_path: Path) -> None: