- 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
211 lines
9 KiB
Python
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")
|