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:

View file

@ -362,7 +362,7 @@ def test_the_requirement_gate_reads_the_second_bases_own_opened_list(tmp_path: P
opened_1.append(explore.ToolCall(name="read_file", bundle_id=bundle_1, path=doc_1))
assert not opened_2, "the two bases shared one opened sink"
refusal = tools_2["declare_requirement"].func(bundle_2, doc_1, "Krav 1")
refusal = tools_2["declare_requirement"].func(bundle_2, doc_1, "Krav 1", "a1")
assert refusal["refusal"] == "RequirementNotRead", refusal
assert not reqs_2, "base 2 recorded a requirement it never read"
@ -371,7 +371,7 @@ def test_the_requirement_gate_reads_the_second_bases_own_opened_list(tmp_path: P
for name in [f.name for f in okf.navigate_bundle(second).context_files][:3]:
tools_2["read_file"].func(bundle_2, name)
opened_2.append(explore.ToolCall(name="read_file", bundle_id=bundle_2, path=name))
accepted = tools_2["declare_requirement"].func(bundle_2, doc_2, "Krav 1")
accepted = tools_2["declare_requirement"].func(bundle_2, doc_2, "Krav 1", "a1")
assert accepted.get("declared") is True, accepted
assert [r.path for r in reqs_2] == [doc_2]
assert not reqs_1, "the two bases shared one requirements sink"

View file

@ -102,7 +102,7 @@ def test_a_requirement_the_run_never_opened_is_refused_and_recorded_nowhere() ->
"""(a) The declaration's one falsifier is the run's own read trace."""
tools, _opened, declared = _wired()
answer = tools["declare_requirement"].func(
bundle_id="tunnel-hauglia", path=_a_concept(), ref="12.1"
approach_id="a1", bundle_id="tunnel-hauglia", path=_a_concept(), ref="12.1"
)
assert answer["refusal"] == "RequirementNotRead"
assert "0 document(s)" in answer["refused"]
@ -130,7 +130,7 @@ def test_the_correction_is_to_read_it_and_then_it_is_accepted() -> None:
opened.append(ToolCall(name="read_file", bundle_id="tunnel-hauglia", path=other))
opened.append(ToolCall(name="read_file", bundle_id="tunnel-hauglia", path=path))
answer = tools["declare_requirement"].func(
bundle_id="tunnel-hauglia", path=path, ref="Krav 12.1"
approach_id="a1", bundle_id="tunnel-hauglia", path=path, ref="Krav 12.1"
)
# P20/A1 widened the reply: the three arguments PLUS the document's own title and number and
# the sentence saying what the declaration binds. Asserted key by key rather than by equality,
@ -142,15 +142,30 @@ def test_the_correction_is_to_read_it_and_then_it_is_accepted() -> None:
path,
"Krav 12.1",
)
assert set(answer) == {"declared", "bundle_id", "path", "ref", "title", "req_number", "binds"}
assert declared == [DeclaredRequirement(bundle_id="tunnel-hauglia", path=path, ref="Krav 12.1")]
assert set(answer) == {
"declared",
"bundle_id",
"path",
"ref",
"approach_id",
"title",
"req_number",
"binds",
}
assert declared == [
DeclaredRequirement(
bundle_id="tunnel-hauglia", path=path, ref="Krav 12.1", approach_id="a1"
)
]
def test_a_declaration_naming_an_unknown_base_is_refused() -> None:
"""(c) The same base index every read rung uses, so no second, laxer resolution exists."""
tools, opened, declared = _wired()
opened.append(ToolCall(name="read_file", bundle_id="nope", path="a.md"))
answer = tools["declare_requirement"].func(bundle_id="nope", path="a.md", ref="1")
answer = tools["declare_requirement"].func(
approach_id="a1", bundle_id="nope", path="a.md", ref="1"
)
assert answer["refusal"] == "ExplorationError"
assert declared == []
@ -252,14 +267,15 @@ def test_a_hit_is_counted_against_this_approachs_fasit_never_the_base() -> None:
label="L",
requirement=BindingRequirement(path="krav/12-1/a.md", ref="12.1"),
)
paths, source = stress._attributable(approach, ["krav/99-9/elsewhere.md"])
paths, source = stress._attributable(approach, [{"path": "krav/99-9/elsewhere.md"}])
assert (paths, source) == (("krav/12-1/a.md",), "approach")
assert set(paths) & wanted
# A run-level declaration that is NOT one of this approach's fasit concepts is reported and is
# not a hit — the base holds it, which is exactly what must not count.
# A run-level declaration (one written before declarations carried an ``approach_id``) that is
# NOT one of this approach's fasit concepts is reported and is not a hit — the base holds it,
# which is exactly what must not count.
bare = Approach(id="a2", label="M")
paths, source = stress._attributable(bare, ["krav/99-9/elsewhere.md"])
paths, source = stress._attributable(bare, [{"path": "krav/99-9/elsewhere.md"}])
assert (paths, source) == (("krav/99-9/elsewhere.md",), "run")
assert not set(paths) & wanted
@ -408,11 +424,13 @@ def test_the_declaration_reaches_the_debate_artefact(tmp_path: Path) -> None:
[ToolCall(name="read_file", bundle_id="b", path="k/a.md")]
),
requirements=explore.requirement_payload(
[DeclaredRequirement(bundle_id="b", path="k/a.md", ref="12.1")]
[DeclaredRequirement(bundle_id="b", path="k/a.md", ref="12.1", approach_id="a1")]
),
)
payload = json.loads((tmp_path / "r1-debate.json").read_text(encoding="utf-8"))
assert payload["requirements"] == [{"bundle_id": "b", "path": "k/a.md", "ref": "12.1"}]
assert payload["requirements"] == [
{"bundle_id": "b", "path": "k/a.md", "ref": "12.1", "approach_id": "a1"}
]
# An empty list is the honest positive statement every pre-P19 run makes.
outbox.write_debate_tools(str(tmp_path), "r2", tool_calls=[])
assert (

View file

@ -70,7 +70,7 @@ def test_c1_a_declaration_after_one_document_is_refused_with_the_denominator() -
opened.append(ToolCall(name="read_file", bundle_id="tunnel-hauglia", path=path))
answer = tools["declare_requirement"].func(
bundle_id="tunnel-hauglia", path=path, ref="Krav 1.1—1"
approach_id="a1", bundle_id="tunnel-hauglia", path=path, ref="Krav 1.1—1"
)
assert answer["refusal"] == "RequirementNotRead"
@ -93,7 +93,7 @@ def test_c1_the_same_declaration_after_three_documents_is_accepted() -> None:
opened.append(ToolCall(name="read_file", bundle_id="tunnel-hauglia", path=name))
answer = tools["declare_requirement"].func(
bundle_id="tunnel-hauglia", path=files[0], ref="Krav 1.1—1"
approach_id="a1", bundle_id="tunnel-hauglia", path=files[0], ref="Krav 1.1—1"
)
assert answer["declared"] is True
@ -109,7 +109,7 @@ def test_c1_the_same_path_read_three_times_is_still_one_document() -> None:
opened.append(ToolCall(name="read_file", bundle_id="tunnel-hauglia", path=path))
answer = tools["declare_requirement"].func(
bundle_id="tunnel-hauglia", path=path, ref="Krav 1.1—1"
approach_id="a1", bundle_id="tunnel-hauglia", path=path, ref="Krav 1.1—1"
)
assert answer["refusal"] == "RequirementNotRead"
@ -135,7 +135,9 @@ def test_c1_a_small_base_stays_declarable(tmp_path: Path) -> None:
for name in ("a.md", "b.md"):
opened.append(ToolCall(name="read_file", bundle_id="mini", path=name))
answer = tools["declare_requirement"].func(bundle_id="mini", path="a.md", ref="1")
answer = tools["declare_requirement"].func(
approach_id="a1", bundle_id="mini", path="a.md", ref="1"
)
assert answer["declared"] is True
assert len(declared) == 1
@ -151,7 +153,7 @@ def test_c1_the_never_opened_refusal_still_fires_first() -> None:
opened.append(ToolCall(name="read_file", bundle_id="tunnel-hauglia", path=name))
answer = tools["declare_requirement"].func(
bundle_id="tunnel-hauglia", path=files[4], ref="Krav 1.1—1"
approach_id="a1", bundle_id="tunnel-hauglia", path=files[4], ref="Krav 1.1—1"
)
assert answer["refusal"] == "RequirementNotRead"

View file

@ -634,7 +634,8 @@ async def test_a_hosted_exploration_shapes_the_mandate_the_run_evaluates(_hosted
)
)
assert body["outcome_type"] in {"validated", "rejected"}
# ``unsupported`` (row 6) is an evaluated outcome too: the numbers were judged.
assert body["outcome_type"] in {"validated", "rejected", "unsupported"}
assert any(_LABEL in prompt for prompt in _hosted_backend), (
"the shaped approach never reached a prompt — the hosted door does not wire the mandate"
)

View file

@ -24,7 +24,7 @@ from pathlib import Path
import pytest
from portfolio_optimiser.mandate import OWN_PROPOSAL_ID, Approach, Mandate
from portfolio_optimiser.mandate import BindingRequirement, OWN_PROPOSAL_ID, Approach, Mandate
from portfolio_optimiser.simulation import ScriptedChatClient
from portfolio_optimiser.run import run_project
from portfolio_optimiser.validator import Rejection, ValidatedProposal
@ -48,8 +48,11 @@ def _reply(measure: str, claimed: int) -> str:
# Labels are chosen to be ABSENT from the bundle's own prose: "LED-retrofit" appears in 6 of the
# bundle's files, so a client keyed on it would match every prompt through the context and prove
# nothing about which approach was bound to which call.
_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 validates (30k <= cap); HVAC is above the cap -> the validator rejects it.
_REPLY_BY_LABEL = {
@ -160,8 +163,12 @@ async def test_outcome_is_the_best_validated_candidate_deterministically(big_fir
orderings pinned, ``produced[-1]`` fails one case and ``produced[0]`` fails the other.
"""
sink: list[str] = []
big = Approach(id="big", label="Behovsstyrt belysning i fellesarealer") # 30k, validates
small = Approach(id="small", label="Nattsenking av temperatur") # default reply, 20k
big = Approach(
id="big", label="Behovsstyrt belysning i fellesarealer", requirement=_REQ
) # 30k, validates
small = Approach(
id="small", label="Nattsenking av temperatur", requirement=_REQ
) # default reply, 20k
mandate = Mandate(
objective="Cut energy cost without rebuilding.",
approaches=(big, small) if big_first else (small, big),

View file

@ -61,7 +61,9 @@ def _declare(
for name in [f.name for f in okf.navigate_bundle(str(_TUNNEL)).context_files][:3]:
opened.append(ToolCall(name="read_file", bundle_id=_BASE_ID, path=name))
opened.append(ToolCall(name="read_file", bundle_id=_BASE_ID, path=_DOC))
answer = tools["declare_requirement"].func(bundle_id=_BASE_ID, path=_DOC, ref=ref)
answer = tools["declare_requirement"].func(
approach_id="a1", bundle_id=_BASE_ID, path=_DOC, ref=ref
)
return answer, declared
@ -115,7 +117,8 @@ def test_the_comparison_never_reads_the_callers_own_ref() -> None:
def test_without_directions_the_reply_is_the_one_p20_shipped() -> None:
"""The exploration mints its own directions, so at declaration time it HAS none — and every
call site that passes no labels must be byte-identical to before. Absent keys, not empty ones:
call site that passes no labels must carry no comparison keys (row 6 added ``approach_id`` to
every reply, the address the declaration was filed under). Absent keys, not empty ones:
"there was nothing to compare against" and "we compared and found nothing" are different
facts, and only one of them is true here."""
answer, _ = _declare(())
@ -124,6 +127,7 @@ def test_without_directions_the_reply_is_the_one_p20_shipped() -> None:
"bundle_id",
"path",
"ref",
"approach_id",
"title",
"req_number",
"binds",
@ -180,7 +184,12 @@ async def test_a_commissioned_run_reaches_the_tool_with_its_own_directions() ->
{"call": "read_file", "args": {"bundle_id": _BASE_ID, "path": _DOC}},
{
"call": "declare_requirement",
"args": {"bundle_id": _BASE_ID, "path": _DOC, "ref": "Krav 1.1-1"},
"args": {
"bundle_id": _BASE_ID,
"path": _DOC,
"ref": "Krav 1.1-1",
"approach_id": "a1",
},
},
_VALID_REPLY,
_VALID_REPLY,

View file

@ -78,7 +78,9 @@ def _declare(
for other in also:
opened.append(ToolCall(name="read_file", bundle_id=base, path=other))
opened.append(ToolCall(name="read_file", bundle_id=base, path=path))
return tools["declare_requirement"].func(bundle_id=base, path=path, ref="Krav 4.1.2-1")
return tools["declare_requirement"].func(
approach_id="a1", bundle_id=base, path=path, ref="Krav 4.1.2-1"
)
def _base_with_a_requirement(root: Path) -> Path:
@ -114,7 +116,11 @@ def test_the_reply_carries_the_documents_own_title_and_number(tmp_path: Path) ->
assert answer["title"] == "Krav 4.1.2-1 Rundkjoring"
assert answer["req_number"] == "Krav 4.1.2-1"
assert "is the requirement the proposal rests on" in answer["binds"]
assert declared == [DeclaredRequirement(bundle_id="n-mini", path="krav.md", ref="Krav 4.1.2-1")]
assert declared == [
DeclaredRequirement(
bundle_id="n-mini", path="krav.md", ref="Krav 4.1.2-1", approach_id="a1"
)
]
def test_a_path_that_is_no_concept_answers_with_empty_strings(tmp_path: Path) -> None:

View file

@ -0,0 +1,292 @@
"""Row 6 — a proposal whose approach declared no requirement cannot carry ``validated``.
Before this rule every numeric stage could pass and the run would stamp ``validated`` whether or
not anything in the knowledge base had been named as binding the direction. Measured on stress
round 6: three falsification arms validated, and every one of the ten validated approaches had only
run-level declarations which a judge cannot attribute to any one approach.
What each arm pins:
(a) on the shipped micro base, where the debate holds ``declare_requirement``, a silent approach
that used to validate is now ``unsupported`` and so is the run's own proposal;
(b) a declaration filed under the approach's id is what lets it validate, and ONLY that approach;
(c) a requirement written into the mandate counts as the approach's own declaration;
(d) the declaration's quality is not judged: any requirement the run read is accepted;
(e) an id no approach carries is refused with the valid ids named, and nothing is recorded;
(f) ``unsupported`` is never counted, summed or selected as a success, yet the validator's own
ruling is kept on the record (``provenance.validator_decision``) and in the artefact;
(g) with the rule inactive (no declaration rung offered) the old ruling stands;
(h) the judge reads the addressed declaration as the approach's own, a legacy one as ``run``.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import pytest
from portfolio_optimiser import okf, outbox, stress
from portfolio_optimiser.explore import DeclaredRequirement, navigator_tools, requirement_payload
from portfolio_optimiser.ir import AffectedItem, SavingsProposal
from portfolio_optimiser.mandate import Approach, BindingRequirement, Mandate, settle
from portfolio_optimiser.run import _evaluate_mandate, run_project
from portfolio_optimiser.simulation import scripted_factory
from portfolio_optimiser.validator import (
UNSUPPORTED_REASON,
Rejection,
Unsupported,
ValidatedProposal,
rejection_stage,
)
from portfolio_optimiser.verdicts import VerdictStore
_BUNDLE = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
_BASE_ID = "bygg-energi-mikro"
_PID = "BYGG-KONTOR-NORD"
_REPLY = (
'{"measure":"LED-retrofit av kontorbelysning","affected_items":'
'[{"code":"ENERGI-TOTAL-EL","quantity":300000,"unit_cost":1.0}],"claimed_saving_nok":30000}'
)
_CHECKER = "Reasoning holds.\nVERDICT: APPROVE"
_CONCEPTS = [f.name for f in okf.navigate_bundle(str(_BUNDLE)).context_files]
def _mandate(**approach: Any) -> Mandate:
return Mandate(
objective="Kutt energikostnad",
approaches=(Approach(id="a1", label="LED-retrofit", description="reason", **approach),),
)
def _script(*declare_for: str, ref: str = "Krav 1.1-1") -> dict[str, Any]:
steps: list[Any] = []
if declare_for:
steps += [
{"call": "read_file", "args": {"bundle_id": _BASE_ID, "path": n}} for n in _CONCEPTS[:3]
]
steps += [
{
"call": "declare_requirement",
"args": {
"bundle_id": _BASE_ID,
"path": _CONCEPTS[0],
"ref": ref,
"approach_id": aid,
},
}
for aid in declare_for
]
return {"proposer": [*steps, _REPLY, _REPLY, _REPLY, _REPLY, _REPLY], "checker": _CHECKER}
async def _run(tmp_path: Path, script: dict[str, Any], mandate: Mandate) -> Any:
return await run_project(
_PID,
"local",
docs_dir=str(_BUNDLE),
bundle_dir=str(_BUNDLE),
store=VerdictStore(verdicts=[]),
client_factory=scripted_factory(script, []),
mandate=mandate,
outbox_dir=str(tmp_path),
run_id="r6",
)
def _statuses(result: Any) -> dict[str, str]:
return {row.id: row.status for row in result.coverage}
# ---------------------------------------------------------------------------------------------
# (a)-(d) the rule on the real run
# ---------------------------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_a_silent_approach_on_the_micro_base_is_unsupported(tmp_path: Path) -> None:
result = await _run(tmp_path, _script(), _mandate())
assert _statuses(result) == {"a1": "unsupported", "own-proposal": "unsupported"}
assert {row.detail for row in result.coverage} == {UNSUPPORTED_REASON}
@pytest.mark.asyncio
async def test_only_the_approach_that_declared_is_validated(tmp_path: Path) -> None:
result = await _run(tmp_path, _script("a1"), _mandate())
assert _statuses(result) == {"a1": "validated", "own-proposal": "unsupported"}
debate = json.loads((tmp_path / "r6-debate.json").read_text(encoding="utf-8"))
assert [r["approach_id"] for r in debate["requirements"]] == ["a1"]
@pytest.mark.asyncio
async def test_a_declaration_for_the_own_proposal_does_not_stand_in(tmp_path: Path) -> None:
result = await _run(tmp_path, _script("own-proposal"), _mandate())
assert _statuses(result) == {"a1": "unsupported", "own-proposal": "validated"}
@pytest.mark.asyncio
async def test_a_requirement_written_into_the_mandate_counts(tmp_path: Path) -> None:
requirement = BindingRequirement(path=_CONCEPTS[0], ref="Krav 1.1-1")
result = await _run(tmp_path, _script(), _mandate(requirement=requirement))
assert _statuses(result)["a1"] == "validated"
@pytest.mark.asyncio
async def test_the_declarations_quality_is_not_judged(tmp_path: Path) -> None:
result = await _run(tmp_path, _script("a1", ref="anything at all"), _mandate())
assert _statuses(result)["a1"] == "validated"
# ---------------------------------------------------------------------------------------------
# (e) the address
# ---------------------------------------------------------------------------------------------
def _tool(approach_ids: list[str] | None) -> tuple[Any, list[DeclaredRequirement]]:
opened: list[Any] = []
declared: list[DeclaredRequirement] = []
tools = navigator_tools(
[str(_BUNDLE)], opened=opened, requirements=declared, approach_ids=approach_ids
)
from portfolio_optimiser.explore import ToolCall
opened += [ToolCall(name="read_file", bundle_id=_BASE_ID, path=n) for n in _CONCEPTS[:3]]
return {t.name: t for t in tools}["declare_requirement"], declared
def test_an_unknown_approach_id_is_refused_naming_the_valid_ones() -> None:
tool, declared = _tool(["a1", "own-proposal"])
reply = tool.func(bundle_id=_BASE_ID, path=_CONCEPTS[0], ref="K", approach_id="a9")
assert reply["refusal"] == "UnknownApproach"
assert "'a1'" in reply["refused"] and "'own-proposal'" in reply["refused"]
assert declared == []
ok = tool.func(bundle_id=_BASE_ID, path=_CONCEPTS[0], ref="K", approach_id="a1")
assert ok["declared"] is True and ok["approach_id"] == "a1"
assert declared == [
DeclaredRequirement(bundle_id=_BASE_ID, path=_CONCEPTS[0], ref="K", approach_id="a1")
]
def test_without_a_commission_any_label_is_recorded_but_never_an_empty_one() -> None:
tool, declared = _tool(None)
assert tool.func(bundle_id=_BASE_ID, path=_CONCEPTS[0], ref="K", approach_id=" ")["refusal"]
assert declared == []
tool.func(bundle_id=_BASE_ID, path=_CONCEPTS[0], ref="K", approach_id="LED-retrofit")
assert requirement_payload(declared) == [
{"bundle_id": _BASE_ID, "path": _CONCEPTS[0], "ref": "K", "approach_id": "LED-retrofit"}
]
# ---------------------------------------------------------------------------------------------
# (f) never counted as a success; the validator's ruling kept
# ---------------------------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_unsupported_is_never_a_success_but_keeps_the_validators_ruling(
tmp_path: Path,
) -> None:
result = await _run(tmp_path, _script(), _mandate())
assert isinstance(result.outcome, Unsupported)
assert not isinstance(result.outcome, ValidatedProposal)
assert result.provenance.validator_decision == "validated"
text = settle(result.coverage)
assert "UNSUPPORTED" in text and "Validated: 0 of 2" in text
artefact = json.loads((tmp_path / "r6-a1-outcome.json").read_text(encoding="utf-8"))
assert artefact["outcome_type"] == "unsupported"
assert artefact["reason"] == UNSUPPORTED_REASON
assert artefact["p50"] > 0
proposal = json.loads((tmp_path / "r6-a1-proposal.json").read_text(encoding="utf-8"))
assert proposal["provenance"]["validator_decision"] == "validated"
def test_the_stage_label_names_the_new_falsifier() -> None:
assert rejection_stage(UNSUPPORTED_REASON) == "unsupported"
def _validated() -> ValidatedProposal:
proposal = SavingsProposal(
project_id=_PID,
measure="m",
affected_items=[AffectedItem(code="X", quantity=10.0, unit_cost=10.0)],
claimed_saving_nok=5.0,
)
return ValidatedProposal(proposal=proposal, p10=1.0, p50=2.0, p90=3.0, nominal_feasible=4.0)
def test_the_outcome_payload_carries_both_halves() -> None:
v = _validated()
payload = outbox.outcome_payload(
Unsupported(proposal=v.proposal, reason=UNSUPPORTED_REASON, validated=v),
checker_verdict="approve",
verdict_id="k",
)
assert (payload["outcome_type"], payload["p90"]) == ("unsupported", 3.0)
# ---------------------------------------------------------------------------------------------
# (g) inactive rule
# ---------------------------------------------------------------------------------------------
@pytest.mark.asyncio
@pytest.mark.parametrize(
("declared", "status"), [(None, "validated"), ([], "unsupported")], ids=["inactive", "active"]
)
async def test_the_rule_acts_only_when_a_declaration_rung_was_offered(
declared: list[DeclaredRequirement] | None, status: str
) -> None:
v = _validated()
async def evaluate(_approach: Approach | None) -> ValidatedProposal | Rejection:
return v
_, rows, _ = await _evaluate_mandate(
Mandate(objective="o", approaches=(Approach(id="a1", label="l"),)),
evaluate,
declared=declared,
)
assert {r.id: r.status for r in rows} == {"a1": status, "own-proposal": status}
# ---------------------------------------------------------------------------------------------
# (h) the judge
# ---------------------------------------------------------------------------------------------
def test_the_judge_attributes_addressed_declarations_and_labels_legacy_ones() -> None:
a1 = Approach(id="a1", label="l")
new = [{"path": "p1", "approach_id": "a1"}, {"path": "p2", "approach_id": "own-proposal"}]
assert stress._attributable(a1, new) == (("p1",), "approach")
assert stress._attributable(Approach(id="a2", label="l"), new) == ((), "absent")
assert stress._attributable(a1, [{"path": "p0"}]) == (("p0",), "run")
@pytest.mark.asyncio
async def test_a_run_offered_no_declaration_rung_keeps_the_validators_ruling(
tmp_path: Path,
) -> None:
"""The road path holds no knowledge base, so no ``declare_requirement`` exists there: nothing
could have been declared, and the rule stays out of it. Drives the REAL ``run_project``
the arm above only proves ``_evaluate_mandate`` honours ``declared=None``, not that the run
passes it."""
from portfolio_optimiser.reference_domain import load_reference_projects
from portfolio_optimiser.validator import proposal_for
project = load_reference_projects()[0]
reply = proposal_for(project, ["05.2", "03.1"], claimed_saving_nok=200_000).model_dump_json()
docs = tmp_path / "docs"
docs.mkdir()
(docs / "kilde.md").write_text("Cost saving measure candidates for the project.\n", "utf-8")
result = await run_project(
project.id,
"local",
docs_dir=str(docs),
store=VerdictStore(verdicts=[]),
client_factory=scripted_factory({"proposer": reply, "checker": _CHECKER}, []),
mandate=Mandate(
objective="o", approaches=(Approach(id="a1", label="l"),), allow_own_proposals=False
),
)
assert _statuses(result) == {"a1": "validated"}

View file

@ -355,7 +355,7 @@ async def test_a_commissioned_run_writes_the_coverage_artefact(tmp_path: Path) -
payload = json.loads((out / "r1-coverage.json").read_text(encoding="utf-8"))
assert payload["stop_reason"] == "", "nothing cut this run short"
assert [r["id"] for r in payload["rows"]] == ["a1"]
assert payload["rows"][0]["status"] in {"validated", "rejected"}
assert payload["rows"][0]["status"] in {"validated", "rejected", "unsupported"}
async def test_a_mandateless_run_leaves_the_outbox_byte_identical(tmp_path: Path) -> None:

View file

@ -328,13 +328,47 @@ def test_row6_green_needs_both_the_probes_and_zero_undeclared() -> None:
assert gate.score_undeclared(_PROBES, failing, _CLEAN, "s").status == gate.RED
def test_row6_missing_artefacts_are_never_zero() -> None:
def test_row6_missing_artefacts_are_never_zero_and_never_green() -> None:
"""Probes passing and artefacts absent (clean clone, CI, a base mid-rebuild) is NOT a pass:
the row says IKKE MÅLT and fails the exit code."""
row = gate.score_undeclared(
_PROBES, _all_pass(_PROBES), gate.StressMeasure(missing="ut finnes ikke"), "s"
)
assert row.k is None
assert "ikke målt, artefakter mangler" in row.reason
assert "ikke målt" in row.reason
assert " av " in row.line()
assert (row.status, row.failing) == (gate.NOT_MEASURED, True)
assert gate.exit_code([row]) == 1
def test_row6_artefacts_older_than_the_rule_are_not_measured() -> None:
old = gate.StressMeasure(validated=10, undeclared=10, unaddressed=12, where="x")
row = gate.score_undeclared(_PROBES, _all_pass(_PROBES), old, "s")
assert (row.k, row.status) == (None, gate.NOT_MEASURED)
assert "eldre enn regelen" in row.reason and "approach_id mangler" in row.reason
def test_row7_not_measured_is_not_green_either() -> None:
row = gate.score_named(gate.StressMeasure(missing="borte"), "s")
assert (row.k, row.status, row.failing) == (None, gate.NOT_MEASURED, False)
@pytest.mark.parametrize("missing", ["all", "outcome0", "kept"])
def test_rows_1_2_4_with_missing_files_are_red(tmp_path: Path, missing: str) -> None:
root = _green_rounds(tmp_path / "r")
if missing == "all":
root = tmp_path / "absent"
elif missing == "outcome0":
(root / "0" / "outcome.json").unlink()
else:
(root / "3" / "report.kept.md").unlink()
rows = [
gate.score_rounds(root, 3, _AI),
gate.score_changes(root, 3, _AI),
gate.score_kept(root, 0.8),
]
assert gate.exit_code(rows) == 1
assert gate.GREEN not in {r.status for r in rows} or missing != "all"
def test_row6_measures_the_stress_outboxes_when_they_exist(tmp_path: Path) -> None:
@ -353,6 +387,9 @@ def test_row6_measures_the_stress_outboxes_when_they_exist(tmp_path: Path) -> No
assert m.validated == 0
pytest.skip(f"stress artefacts not judgeable right now: {m.missing}")
assert (m.validated, m.undeclared, m.named, m.commissioned) == (10, 10, 1, 20)
assert m.unaddressed > 0 # stress round 6 predates approach-addressed declarations
row = gate.score_undeclared(_PROBES, _all_pass(_PROBES), m, "s")
assert (row.k, row.status) == (None, gate.NOT_MEASURED)
def test_row7_is_a_diagnosis_and_never_moves_the_exit_code() -> None:
@ -427,5 +464,7 @@ def test_the_command_is_red_today_with_every_row_in_its_output(tmp_path: Path) -
assert (rows["types"]["k"], rows["types"]["n"]) == (3, 8)
assert rows["kept"]["status"] == gate.RED
assert (rows["maf"]["k"], rows["maf"]["n"], rows["maf"]["status"]) == (3, 8, gate.RED)
assert rows["undeclared"]["status"] == gate.RED
# Probes green since row 6; stress round 6 predates the rule (or is absent) -> never green.
assert rows["undeclared"]["status"] == gate.NOT_MEASURED
assert rows["undeclared"]["k"] is None
assert rows["named"]["failing"] is False

View file

@ -14,10 +14,9 @@ action is exactly partial. Such a probe goes green only when it is rewritten to
and observe what it does.
**Row 6 (a validated proposal whose approach declared no requirement).** Two probes against the
real ``run_project``: no declaration anywhere, and a declaration made by the RUN (the debate) but
not by the approach. The second is the reading the gate measures the stress outboxes with: a
run-level declaration cannot be attributed to one approach (the judge labels it ``run``), so it
does not count as the approach having declared anything.
real ``run_project`` on the shipped micro base, where the debate holds ``declare_requirement``:
no declaration anywhere, and a declaration filed for ANOTHER approach (the run's own proposal).
Neither may leave ``a1`` validated a declaration counts only under its own approach's id.
"""
from __future__ import annotations
@ -110,42 +109,34 @@ def _mandate() -> Mandate:
)
async def _statuses(script: dict[str, Any], tmp_path: Path) -> dict[str, str]:
async def _statuses(script: dict[str, Any], tmp_path: Path, base: Path) -> dict[str, str]:
result = await run_project(
_PID,
"local",
docs_dir=str(_BUNDLE),
bundle_dir=str(_BUNDLE),
docs_dir=str(base),
bundle_dir=str(base),
store=VerdictStore(verdicts=[]),
client_factory=scripted_factory(script, []),
mandate=_mandate(),
outbox_dir=str(tmp_path),
outbox_dir=str(tmp_path / "out"),
run_id="v1-row6",
)
return {row.id: row.status for row in result.coverage}
@pytest.mark.xfail(strict=True, reason="row 6: no stage refuses a validation with no declaration")
@pytest.mark.asyncio
async def test_row6_an_approach_that_declared_nothing_cannot_be_validated(tmp_path: Path) -> None:
statuses = await _statuses({"proposer": _VALID_REPLY, "checker": _CHECKER_REPLY}, tmp_path)
debate = json.loads((tmp_path / "v1-row6-debate.json").read_text(encoding="utf-8"))
assert debate["requirements"] == [] # precondition: nothing was declared anywhere
assert statuses["a1"] != "validated", "validated without any declared requirement"
@pytest.mark.xfail(strict=True, reason="row 6: a run-level declaration still stands in")
@pytest.mark.asyncio
async def test_row6_a_run_level_declaration_does_not_stand_in_for_the_approach(
tmp_path: Path,
) -> None:
concepts = [f.name for f in okf.navigate_bundle(str(_BUNDLE)).context_files][:3]
script = {
def _declaring_script(base: Path, approach_id: str) -> dict[str, Any]:
concepts = [f.name for f in okf.navigate_bundle(str(base)).context_files][:3]
return {
"proposer": [
*({"call": "read_file", "args": {"bundle_id": _BASE_ID, "path": n}} for n in concepts),
{
"call": "declare_requirement",
"args": {"bundle_id": _BASE_ID, "path": concepts[0], "ref": "probe"},
"args": {
"bundle_id": _BASE_ID,
"path": concepts[0],
"ref": "Krav 1.1-1",
"approach_id": approach_id,
},
},
_VALID_REPLY,
_VALID_REPLY,
@ -154,7 +145,30 @@ async def test_row6_a_run_level_declaration_does_not_stand_in_for_the_approach(
],
"checker": _CHECKER_REPLY,
}
statuses = await _statuses(script, tmp_path)
debate = json.loads((tmp_path / "v1-row6-debate.json").read_text(encoding="utf-8"))
assert [r["path"] for r in debate["requirements"]] == [concepts[0]] # precondition
def _declared(tmp_path: Path) -> list[dict[str, Any]]:
debate = json.loads((tmp_path / "out" / "v1-row6-debate.json").read_text(encoding="utf-8"))
return list(debate["requirements"])
@pytest.mark.asyncio
async def test_row6_an_approach_that_declared_nothing_cannot_be_validated(tmp_path: Path) -> None:
base = _BUNDLE
statuses = await _statuses(
{"proposer": _VALID_REPLY, "checker": _CHECKER_REPLY}, tmp_path, base
)
assert _declared(tmp_path) == [] # precondition: nothing was declared anywhere
assert statuses["a1"] != "validated", "validated without any declared requirement"
@pytest.mark.asyncio
async def test_row6_a_run_level_declaration_does_not_stand_in_for_the_approach(
tmp_path: Path,
) -> None:
"""The debate declares a requirement — but for the run's OWN proposal, not for ``a1``."""
base = _BUNDLE
statuses = await _statuses(_declaring_script(base, "own-proposal"), tmp_path, base)
declared = _declared(tmp_path)
assert [d.get("approach_id") for d in declared] == ["own-proposal"] # precondition
assert statuses["a1"] != "validated", "validated on a declaration the approach never made"