The context sets, the packaged knowledge bases and the example bundles are replaced by one fictitious example set about IT operations in an invented organisation: three context sets (serverrom-2027, driftsavtale-2027 and the two-base drift-og-avtale-2027), two synthetic knowledge bases under src/portfolio_optimiser/data/kunnskapsbaser and two example bundles under src/portfolio_optimiser/data/bundles. Numbers, codes and structural values in tests and fixtures are kept; names, ids and wording change. Dated measurement documents that only recorded runs on the replaced material are deleted. Gate figures measured on the new set are not comparable with earlier ones. The exclusion gate from the previous commit is green: 0 tracked files hit outside the shared/ subtree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
292 lines
12 KiB
Python
292 lines
12 KiB
Python
"""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 reference 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"}
|