portfolio-optimiser-claude/tests/test_step8_promotion_loadbearing.py
Kjell Tore Guttormsen 22bfc80dda feat(learning): S9 — D7 læringssløyfe: verdict-inbox, fail-closed promoteringsgate, artefakt-sourced persona
- inbox.py (§4.2+§5): VerdictDocument med verbatim-id-regel; write_verdict
  authoring-primitiv (deterministisk JSON); load_inbox tolerant (skip, aldri
  raise; sortert på filnavn); merge_inbox_into_store first-write-wins,
  idempotent, skriver aldri (rolle-splitt §3 steg 7)
- promotion.py (§6): promote fail-closed mot {approved,
  approved_with_adjustment}; eksplisitt påkrevd timestamp; minimal frontmatter
  (rationale → description, aldri strukturerte læringsfelt); path-safe token
  med content-hash-fallback; idempotent index-lenking med fast nøytral label
- persona.py (§4.3): load_persona_example fail-fast (run-path-vokabular,
  marker ⊆ rationale); drop_persona_verdict artefakt-sourced ved kalltid mot
  delt shared/-artefakt
- experience.py (kirurgisk): seeding leser verdict_id VERBATIM fra frontmatter
  — re-minting ville kollidert distinkte promoterte kandidater
- 43 nye load-bearing tester (step7/step8/persona), 164/164 uten API-nøkkel;
  to-runs-bevis med fersk store + tom-inbox-kontroll; fire detach-bevis kjørt
  røde og revertert grønne

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
2026-07-03 07:36:15 +02:00

211 lines
9 KiB
Python

"""Step-8 promotion gate — LOAD-BEARING (method-spec §3 Step 8, §6, §11).
The seam this file keeps alive: only APPROVED knowledge enters the wiki
(fail-closed), the promoted file is NAVIGABLE (index-linked — an unlinked file
is unreachable), and the index label is NEUTRAL (the index body flows verbatim
into the rendered read-context, so a descriptive label would leak the learning
signal around the gated fold). RED when a non-approved verdict reaches the
wiki, when an approved one is not navigable, or when the label leaks.
"""
from __future__ import annotations
import shutil
from pathlib import Path
import pytest
from portfolio_optimiser_claude.experience import (
CandidateFeatures,
VerdictStore,
fold_experience,
seed_store_from_bundle,
)
from portfolio_optimiser_claude.inbox import VerdictDocument
from portfolio_optimiser_claude.ir import load_validator_input
from portfolio_optimiser_claude.okf import bundle_context, navigate_bundle, parse_concept_file
from portfolio_optimiser_claude.promotion import PromotionError, promote
SHARED_BUNDLE = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
MARKER = "realiseringsgrad=0.79"
RATIONALE = f"Godkjent med korreksjon: i drift realiseres ~79% ({MARKER})."
BASE_PROMPT = "Propose exactly one cost-saving measure for this project."
# Structured learning fields a promoted file MUST NOT reproduce (§6) — the raw
# verdict model carries the signal only as rationale prose.
_SEED_ONLY_FIELDS = (
"realization_rate",
"expected_actual_saving_nok",
"modelled_saving_nok",
"gap_source",
"context_key",
)
def _document(
decision: str = "approved",
rationale: str = RATIONALE,
codes: frozenset[str] = frozenset({"E01"}),
verdict_id: str | None = None,
) -> VerdictDocument:
doc = VerdictDocument.from_candidate(
CandidateFeatures(
affected_codes=codes, measure_type="led-retrofit", claimed_saving_nok=25000.0
),
decision=decision,
rationale=rationale,
description="LED retrofit",
)
if verdict_id is not None:
doc = doc.model_copy(update={"id": verdict_id})
return doc
def _promote(doc: VerdictDocument, bundle: Path) -> Path:
return promote(
doc,
bundle,
approved_by="persona:expert-reviewer",
experiment="S9-offline-sim",
timestamp="2026-07-03T12:00:00Z",
)
@pytest.fixture()
def bundle(tmp_path: Path) -> Path:
target = tmp_path / "bundle"
shutil.copytree(SHARED_BUNDLE, target)
return target
class TestFailClosed:
"""§6: a non-approved verdict is refused — writing and linking NOTHING."""
@pytest.mark.parametrize("decision", ["rejected", "maybe"])
def test_non_approved_decisions_are_refused_writing_nothing(
self, bundle: Path, decision: str
) -> None:
index_before = (bundle / "index.md").read_bytes()
files_before = sorted(p.name for p in bundle.iterdir())
with pytest.raises(PromotionError):
_promote(_document(decision=decision), bundle)
assert (bundle / "index.md").read_bytes() == index_before
assert sorted(p.name for p in bundle.iterdir()) == files_before
@pytest.mark.parametrize("decision", ["approved", "approved_with_adjustment"])
def test_the_accepted_set_is_exactly_the_two_approval_forms(
self, bundle: Path, decision: str
) -> None:
assert _promote(_document(decision=decision), bundle).is_file()
class TestPromotedFile:
"""§6: minimal promoted file — signal as rationale prose, provenance-stamped."""
def test_frontmatter_carries_the_contract_fields(self, bundle: Path) -> None:
doc = _document()
concept = parse_concept_file(_promote(doc, bundle))
assert concept.type == "verdict"
assert concept.frontmatter["decision"] == "approved"
assert concept.frontmatter["description"] == RATIONALE
assert concept.frontmatter["verdict_id"] == doc.id # verbatim
provenance = concept.frontmatter["provenance"]
for part in ("persona:expert-reviewer", "S9-offline-sim", "2026-07-03T12:00:00Z"):
assert part in provenance
def test_no_structured_learning_fields_are_reproduced(self, bundle: Path) -> None:
concept = parse_concept_file(_promote(_document(), bundle))
for field in _SEED_ONLY_FIELDS:
assert field not in concept.frontmatter
def test_timestamp_is_an_explicit_required_argument(self, bundle: Path) -> None:
# No wall-clock default — promotion is deterministic and reproducible.
with pytest.raises(TypeError):
promote( # type: ignore[call-arg]
_document(), bundle, approved_by="x", experiment="y"
)
def test_same_candidate_id_is_last_write_wins_per_file(self, bundle: Path) -> None:
first = _promote(_document(rationale=f"first ({MARKER})"), bundle)
second = _promote(_document(rationale=f"second ({MARKER})"), bundle)
assert first == second
assert parse_concept_file(second).frontmatter["description"] == f"second ({MARKER})"
class TestNavigability:
"""LOAD-BEARING (§11): an approved verdict must be navigable — and fold back."""
def test_promoted_file_is_reachable_via_index_navigation(self, bundle: Path) -> None:
path = _promote(_document(), bundle)
assert path.name in {c.path.name for c in navigate_bundle(bundle)}
def test_promoted_verdict_closes_the_loop_through_seeding(self, bundle: Path) -> None:
# Promotion → navigation → seeding → fold: the expert's rationale (the
# learning signal as prose) reaches the next run's hypothesis prompt.
_promote(_document(), bundle)
store = VerdictStore()
seed_store_from_bundle(store, bundle)
features = CandidateFeatures.from_proposal(load_validator_input(bundle))
prompt = fold_experience(store, features, BASE_PROMPT, k=3)
assert MARKER in prompt
def test_two_promoted_candidates_both_seed_verbatim_ids(self, bundle: Path) -> None:
# §4.2 read-VERBATIM at the seeding layer: distinct candidates get
# distinct store entries — re-minting from bundle features would
# collide them (first-write-wins) and silently drop one rationale.
_promote(_document(codes=frozenset({"E01"}), rationale=f"one ({MARKER})"), bundle)
_promote(_document(codes=frozenset({"E02"}), rationale=f"two ({MARKER})"), bundle)
store = VerdictStore()
seed_store_from_bundle(store, bundle)
features = CandidateFeatures.from_proposal(load_validator_input(bundle))
rationales = {r.rationale for r in store.retrieve(features, k=10)}
assert f"one ({MARKER})" in rationales
assert f"two ({MARKER})" in rationales
def test_linking_is_idempotent(self, bundle: Path) -> None:
path = _promote(_document(), bundle)
_promote(_document(), bundle)
index_text = (bundle / "index.md").read_text(encoding="utf-8")
assert index_text.count(f"]({path.name})") == 1
class TestNeutralLabel:
"""LOAD-BEARING (§11): the index label carries NO verdict signal."""
def test_index_never_carries_the_rationale_or_marker(self, bundle: Path) -> None:
_promote(_document(), bundle)
index_text = (bundle / "index.md").read_text(encoding="utf-8")
assert MARKER not in index_text
assert RATIONALE not in index_text
def test_label_is_fixed_across_verdicts(self, bundle: Path) -> None:
a = _promote(_document(codes=frozenset({"E01"})), bundle)
b = _promote(_document(codes=frozenset({"E02"})), bundle)
index_lines = (bundle / "index.md").read_text(encoding="utf-8").splitlines()
line_a = next(line for line in index_lines if a.name in line)
line_b = next(line for line in index_lines if b.name in line)
assert line_a.replace(a.name, "") == line_b.replace(b.name, "")
def test_rendered_read_context_never_carries_the_marker(self, bundle: Path) -> None:
# Belt and braces: the index body flows verbatim into the rendered
# read-context — after promotion it must still exclude the signal.
_promote(_document(), bundle)
assert MARKER not in bundle_context(bundle)
class TestPathSafety:
"""§6: path-safe, fail-closed against escaping names."""
def test_escaping_id_is_sanitised_into_the_bundle(self, bundle: Path, tmp_path: Path) -> None:
path = _promote(_document(verdict_id="../../evil"), bundle)
assert path.parent == bundle
assert not (tmp_path / "evil").exists()
assert "/" not in path.name and "\\" not in path.name
def test_degenerate_token_falls_back_to_a_content_hash(self, bundle: Path) -> None:
path = _promote(_document(verdict_id="///"), bundle)
assert path.parent == bundle
token = path.name.removeprefix("promoted-verdict-").removesuffix(".md")
assert token, "degenerate token must fall back to a non-empty content hash"
assert set(token) <= set("0123456789abcdef")