- 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
159 lines
6.2 KiB
Python
159 lines
6.2 KiB
Python
"""Persona artifact — LOAD-BEARING (method-spec §4.3, §11).
|
|
|
|
The seam this file keeps alive: the persona judgement is sourced from the
|
|
SHARED skill artifact at call time — never from an inlined copy — and the
|
|
example stays conformant with the pipeline schema (a persona-authored verdict
|
|
must round-trip the §4.2/§5 inbox unchanged). RED when the example drifts from
|
|
the schema or when the judgement is re-inlined instead of artifact-sourced.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from portfolio_optimiser_claude.experience import (
|
|
CandidateFeatures,
|
|
VerdictStore,
|
|
fold_experience,
|
|
mint_verdict_id,
|
|
)
|
|
from portfolio_optimiser_claude.inbox import load_inbox, merge_inbox_into_store
|
|
from portfolio_optimiser_claude.persona import drop_persona_verdict, load_persona_example
|
|
|
|
SHARED_ARTIFACT = (
|
|
Path(__file__).resolve().parents[1]
|
|
/ "shared"
|
|
/ "skills"
|
|
/ "expert-reviewer"
|
|
/ "references"
|
|
/ "example-verdict.json"
|
|
)
|
|
|
|
FEATURES = CandidateFeatures(
|
|
affected_codes=frozenset({"E01"}), measure_type="led-retrofit", claimed_saving_nok=25000.0
|
|
)
|
|
|
|
|
|
def _artifact(tmp_path: Path, marker: str = "rate=0.5", rationale: str | None = None) -> Path:
|
|
path = tmp_path / "example-verdict.json"
|
|
path.write_text(
|
|
json.dumps(
|
|
{
|
|
"decision": "approved",
|
|
"marker": marker,
|
|
"rationale": rationale if rationale is not None else f"Approved; {marker} holds.",
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
return path
|
|
|
|
|
|
class TestSharedArtifactConformance:
|
|
"""§4.3: the DELTE artifact itself — proving the shared core is shared."""
|
|
|
|
def test_the_shared_artifact_loads_and_is_run_path_approved(self) -> None:
|
|
persona = load_persona_example(SHARED_ARTIFACT)
|
|
assert persona.decision == "approved"
|
|
|
|
def test_the_marker_is_the_traceable_payload_inside_the_rationale(self) -> None:
|
|
persona = load_persona_example(SHARED_ARTIFACT)
|
|
assert persona.marker in persona.rationale
|
|
assert persona.marker # non-empty — there must be something to trace
|
|
|
|
|
|
class TestFailFastLoading:
|
|
"""§4.3: the artifact is REQUIRED input — fail-fast, never tolerant."""
|
|
|
|
def test_missing_artifact_raises(self, tmp_path: Path) -> None:
|
|
with pytest.raises(FileNotFoundError):
|
|
load_persona_example(tmp_path / "no-such-artifact.json")
|
|
|
|
def test_malformed_json_raises(self, tmp_path: Path) -> None:
|
|
path = tmp_path / "broken.json"
|
|
path.write_text("{not json", encoding="utf-8")
|
|
with pytest.raises(ValueError):
|
|
load_persona_example(path)
|
|
|
|
def test_seed_only_decision_vocabulary_is_rejected(self, tmp_path: Path) -> None:
|
|
# §4.3: decision MUST be a run-path value — approved_with_adjustment
|
|
# exists only in seed frontmatter and the promotion gate's accepted set.
|
|
path = tmp_path / "example-verdict.json"
|
|
path.write_text(
|
|
json.dumps(
|
|
{
|
|
"decision": "approved_with_adjustment",
|
|
"marker": "m",
|
|
"rationale": "m in here",
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
with pytest.raises(ValidationError):
|
|
load_persona_example(path)
|
|
|
|
def test_marker_not_a_substring_of_rationale_is_rejected(self, tmp_path: Path) -> None:
|
|
path = _artifact(tmp_path, marker="rate=0.5", rationale="no payload here")
|
|
with pytest.raises(ValidationError):
|
|
load_persona_example(path)
|
|
|
|
|
|
class TestArtifactSourcedJudgement:
|
|
"""LOAD-BEARING (§11): sourced at call time — RED if the judgement is re-inlined."""
|
|
|
|
def test_editing_the_artifact_changes_the_next_dropped_verdict(self, tmp_path: Path) -> None:
|
|
artifact = _artifact(tmp_path, marker="rate=0.5")
|
|
inbox = tmp_path / "inbox"
|
|
first = drop_persona_verdict(inbox, artifact, FEATURES, description="LED retrofit")
|
|
assert "rate=0.5" in first.rationale
|
|
_artifact(tmp_path, marker="rate=0.9") # the artifact is edited in place
|
|
second = drop_persona_verdict(
|
|
inbox,
|
|
artifact,
|
|
CandidateFeatures(
|
|
affected_codes=frozenset({"E02"}),
|
|
measure_type="led-retrofit",
|
|
claimed_saving_nok=25000.0,
|
|
),
|
|
description="LED retrofit",
|
|
)
|
|
assert "rate=0.9" in second.rationale # re-inlining would freeze the judgement
|
|
|
|
|
|
class TestPipelineSchemaConformance:
|
|
"""§4.3 + §4.2: a persona-authored verdict round-trips the inbox unchanged."""
|
|
|
|
def test_dropped_verdict_is_a_conformant_inbox_file(self, tmp_path: Path) -> None:
|
|
artifact = _artifact(tmp_path)
|
|
inbox = tmp_path / "inbox"
|
|
dropped = drop_persona_verdict(inbox, artifact, FEATURES, description="LED retrofit")
|
|
assert dropped.id == mint_verdict_id(FEATURES)
|
|
assert (inbox / f"{dropped.id}.json").is_file()
|
|
(loaded,) = load_inbox(inbox)
|
|
assert loaded == dropped
|
|
|
|
def test_the_persona_marker_reaches_a_later_prompt_via_the_inbox(self, tmp_path: Path) -> None:
|
|
# Persona → authoring primitive → tolerant load → merge → fold: the
|
|
# traceable payload follows the persona's judgement into the prompt.
|
|
artifact = _artifact(tmp_path, marker="rate=0.77")
|
|
inbox = tmp_path / "inbox"
|
|
drop_persona_verdict(inbox, artifact, FEATURES, description="LED retrofit")
|
|
store = VerdictStore()
|
|
merge_inbox_into_store(store, inbox)
|
|
prompt = fold_experience(store, FEATURES, "Base task.", k=3)
|
|
assert "rate=0.77" in prompt
|
|
|
|
def test_the_shared_artifact_feeds_the_pipeline_directly(self, tmp_path: Path) -> None:
|
|
# The canonical example itself flows end-to-end — schema drift in the
|
|
# shared artifact turns this RED.
|
|
inbox = tmp_path / "inbox"
|
|
drop_persona_verdict(inbox, SHARED_ARTIFACT, FEATURES, description="LED retrofit")
|
|
persona = load_persona_example(SHARED_ARTIFACT)
|
|
store = VerdictStore()
|
|
merge_inbox_into_store(store, inbox)
|
|
prompt = fold_experience(store, FEATURES, "Base task.", k=3)
|
|
assert persona.marker in prompt
|