"""The persona example artifact loader (method-spec §4.3). The expert-reviewer persona is a shared skill artifact: one canonical example verdict JSON (``decision``, ``marker``, ``rationale``). The persona judgement is sourced from that artifact AT CALL TIME — never from an inlined copy — and loading is fail-fast: the artifact is required input (contrast the tolerant inbox, §5). In simulation the persona plays the human on the WRITE side of the inbox role split: ``drop_persona_verdict`` authors a §4.2 verdict file through the same folder interface a production expert uses. """ from __future__ import annotations import json from pathlib import Path from typing import Literal from pydantic import BaseModel, Field, model_validator from portfolio_optimiser_claude.experience import CandidateFeatures from portfolio_optimiser_claude.inbox import VerdictDocument, write_verdict class PersonaVerdict(BaseModel): """The canonical persona example (§4.3): a run-path decision + traceable payload.""" # §4.3: MUST be a run-path value (§4.1) — the seed-only vocabulary is rejected. decision: Literal["approved", "rejected"] marker: str = Field(min_length=1) rationale: str = Field(min_length=1) @model_validator(mode="after") def _marker_is_traceable(self) -> PersonaVerdict: # The marker is the payload a simulation follows from the persona's # judgement into a later run's prompt — it must live in the rationale. if self.marker not in self.rationale: raise ValueError( f"persona example: marker {self.marker!r} is not a substring of the rationale" ) return self def load_persona_example(artifact_path: Path) -> PersonaVerdict: """Load the shared artifact — FAIL-FAST on a missing or malformed file (§4.3).""" return PersonaVerdict.model_validate(json.loads(artifact_path.read_text(encoding="utf-8"))) def drop_persona_verdict( inbox_dir: Path, artifact_path: Path, features: CandidateFeatures, *, description: str, ) -> VerdictDocument: """The persona judges a candidate and drops a verdict file into the inbox. The judgement (decision + rationale) is read from the artifact at call time; the id is minted from the candidate features (§4.2); the file is written through the authoring primitive (§5) — the same interface a production expert uses. """ persona = load_persona_example(artifact_path) verdict = VerdictDocument.from_candidate( features, decision=persona.decision, rationale=persona.rationale, description=description, ) write_verdict(inbox_dir, verdict) return verdict