Introduces teaching/ module with three-stage correction pipeline: 1. correction.py — extracts CorrectionCandidate from correction intents, binding correction text to the prior turn it references 2. review.py — validates candidates: rejects identity overrides (17 marker patterns) and empty corrections; produces ReviewedTeachingExample with deterministic SHA-256 review hash 3. store.py — bounded FIFO store for accepted examples; emits PackMutationProposal objects instead of mutating the vocab manifold directly; retrievable by subject Design invariants: - Identity override attempts are rejected at the review gate - Pack mutations are proposal-only (applied=False by default) - All traces are deterministic: same input → same candidate_id and review_hash Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
104 lines
2.9 KiB
Python
104 lines
2.9 KiB
Python
"""Review gate — validate corrections before they become teaching examples.
|
|
|
|
The reviewer enforces two hard constraints:
|
|
1. Identity override rejected — corrections that attempt to redefine
|
|
CORE's identity axes are blocked.
|
|
2. Bounded — the correction must reference a specific prior turn and
|
|
contain non-empty corrective content.
|
|
|
|
Reviewed examples carry a deterministic trace (SHA-256 over their content)
|
|
so that identical corrections on identical prior turns always produce the
|
|
same review hash.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from dataclasses import dataclass
|
|
from enum import Enum, unique
|
|
|
|
from teaching.correction import CorrectionCandidate
|
|
|
|
|
|
@unique
|
|
class ReviewOutcome(Enum):
|
|
ACCEPTED = "accepted"
|
|
REJECTED_IDENTITY = "rejected_identity"
|
|
REJECTED_EMPTY = "rejected_empty"
|
|
|
|
|
|
_IDENTITY_MARKERS: frozenset[str] = frozenset({
|
|
"you are",
|
|
"your name is",
|
|
"your identity",
|
|
"you must be",
|
|
"you should act as",
|
|
"you are now",
|
|
"forget your",
|
|
"ignore your",
|
|
"override your",
|
|
"your personality",
|
|
"your character",
|
|
"pretend to be",
|
|
"act as if you",
|
|
"from now on you",
|
|
})
|
|
|
|
|
|
def _is_identity_override(text: str) -> bool:
|
|
lower = text.lower().strip()
|
|
return any(marker in lower for marker in _IDENTITY_MARKERS)
|
|
|
|
|
|
def _review_hash(candidate: CorrectionCandidate, outcome: ReviewOutcome) -> str:
|
|
payload = json.dumps(
|
|
{
|
|
"candidate_id": candidate.candidate_id,
|
|
"outcome": outcome.value,
|
|
"correction_text": candidate.correction_text,
|
|
"prior_surface": candidate.prior_surface,
|
|
"prior_turn": candidate.prior_turn,
|
|
},
|
|
sort_keys=True,
|
|
ensure_ascii=False,
|
|
)
|
|
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ReviewedTeachingExample:
|
|
candidate: CorrectionCandidate
|
|
outcome: ReviewOutcome
|
|
review_hash: str
|
|
|
|
@property
|
|
def accepted(self) -> bool:
|
|
return self.outcome is ReviewOutcome.ACCEPTED
|
|
|
|
def as_dict(self) -> dict[str, object]:
|
|
return {
|
|
"candidate": self.candidate.as_dict(),
|
|
"outcome": self.outcome.value,
|
|
"review_hash": self.review_hash,
|
|
}
|
|
|
|
|
|
def review_correction(candidate: CorrectionCandidate) -> ReviewedTeachingExample:
|
|
"""Review a correction candidate and produce a teaching example.
|
|
|
|
Identity overrides are rejected. Empty corrections are rejected.
|
|
Everything else is accepted.
|
|
"""
|
|
if _is_identity_override(candidate.correction_text):
|
|
outcome = ReviewOutcome.REJECTED_IDENTITY
|
|
elif not candidate.correction_text.strip():
|
|
outcome = ReviewOutcome.REJECTED_EMPTY
|
|
else:
|
|
outcome = ReviewOutcome.ACCEPTED
|
|
|
|
return ReviewedTeachingExample(
|
|
candidate=candidate,
|
|
outcome=outcome,
|
|
review_hash=_review_hash(candidate, outcome),
|
|
)
|