portfolio-optimiser-claude/src/portfolio_optimiser_claude/persona.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

70 lines
2.7 KiB
Python

"""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