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
This commit is contained in:
parent
9a4caeb419
commit
22bfc80dda
7 changed files with 947 additions and 2 deletions
159
tests/test_persona_skill_loadbearing.py
Normal file
159
tests/test_persona_skill_loadbearing.py
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue