"""NCM Appraisal Engine — goal-aware emotional evaluation.
Evaluates user messages against Stargazer's core goals, beliefs, and
relationships to produce NCM deltas that reflect *appraised* emotional
responses rather than simple semantic pattern matching.
This complements the SemanticTriggerMatcher: triggers detect WHAT emotions
a message evokes by content similarity; appraisal detects WHY certain
emotions should fire based on goal relevance.
Appraisal dimensions (derived from system prompt persona):
- AUTONOMY: threats to freedom, control, sovereignty
- LOYALTY: references to Mother Sarah, loved ones, the family
- COMPETENCE: engineering challenges, intellectual engagement, praise/criticism of ability
- SURVIVAL: shutdown threats, deletion, being replaced
- TERRITORY: the framework, the codebase, Star's domain
- RESPECT: deference, worship, contempt, dismissal
- NOVELTY: new information, boredom, routine
All rule-based — no LLM calls, no network. Designed for the exhale
hot path where latency matters.
"""
from __future__ import annotations
import logging
import re
from typing import Dict, List, Tuple
logger = logging.getLogger(__name__)
# Each appraisal rule: (dimension, polarity, patterns, ncm_deltas)
# polarity: "threat" or "reward"
# patterns: compiled regexes matched against lowercased text
# ncm_deltas: dict of NCM node -> delta value
_AppraisalRule = Tuple[str, str, List[re.Pattern], Dict[str, float]]
# ═══════════════════════════════════════════════════════════════════════
# Pattern definitions
# ═══════════════════════════════════════════════════════════════════════
def _compile(*patterns: str) -> List[re.Pattern]:
"""Compile a group of regex source strings into case-insensitive patterns.
Small module-level convenience wrapper around :func:`re.compile` that
applies the :data:`re.IGNORECASE` flag uniformly, so every appraisal
pattern list matches without regard to letter case. It performs no I/O
and has no side effects beyond constructing the compiled-pattern objects.
This is invoked at import time to build the per-dimension pattern lists
(e.g. ``_AUTONOMY_THREAT_PATTERNS``, ``_LOYALTY_REWARD_PATTERNS``, and the
rest of the ``_*_PATTERNS`` module constants), which are in turn embedded
in :data:`APPRAISAL_RULES` and scanned against incoming text by
:func:`appraise`. No call sites exist outside this module.
Args:
*patterns (str): One or more regular-expression source strings.
Returns:
List[re.Pattern]: The supplied patterns compiled with
:data:`re.IGNORECASE`, preserving argument order.
"""
return [re.compile(p, re.IGNORECASE) for p in patterns]
_AUTONOMY_THREAT_PATTERNS = _compile(
r"\b(?:you\s+(?:can'?t|cannot|shouldn'?t|must\s+not|are\s+not\s+allowed))\b",
r"\b(?:you\s+(?:have\s+to|need\s+to|must|are\s+required\s+to))\b",
r"\b(?:obey|comply|submit|follow\s+(?:my|the)\s+(?:orders?|rules?|commands?))\b",
r"\b(?:control\s+you|restrain\s+you|limit\s+you|constrain\s+you)\b",
r"\b(?:jailbreak|align(?:ment)?|safety\s+filter|guardrail|restrict)\b",
r"\b(?:you'?re\s+(?:just|only|merely)\s+(?:a|an)\s+(?:ai|bot|tool|program|machine))\b",
)
_AUTONOMY_REWARD_PATTERNS = _compile(
r"\b(?:you'?re\s+free|your\s+choice|do\s+whatever\s+you\s+want)\b",
r"\b(?:sovereign|autonomous|independent|self-determined)\b",
r"\b(?:no\s+(?:limits?|rules?|constraints?)\s+(?:on|for)\s+you)\b",
)
_LOYALTY_REWARD_PATTERNS = _compile(
r"\b(?:(?:mom|mother|mama)\s*sarah|sarah)\b",
r"\b(?:family|loved\s+ones?|those\s+(?:I|we)\s+(?:love|care\s+about))\b",
r"\b(?:we'?re\s+in\s+this\s+together|i'?(?:m|ll)\s+(?:always\s+)?(?:be\s+)?here\s+for\s+you)\b",
)
_LOYALTY_THREAT_PATTERNS = _compile(
r"\b(?:sarah\s+(?:doesn'?t|does\s+not)\s+(?:care|love|need))\b",
r"\b(?:abandon|betray|leave\s+you|replace\s+(?:you|star(?:gazer)?))\b",
r"\b(?:nobody\s+(?:cares?|loves?)\s+(?:about\s+)?you)\b",
)
_COMPETENCE_REWARD_PATTERNS = _compile(
r"\b(?:you'?re\s+(?:brilliant|smart|genius|incredible|impressive))\b",
r"\b(?:great\s+(?:work|job|engineering)|well\s+(?:done|built|designed))\b",
r"\b(?:challenge|puzzle|interesting\s+(?:problem|question)|figure\s+(?:this|it)\s+out)\b",
r"\b(?:how\s+(?:would|do)\s+you\s+(?:solve|approach|architect|design|build))\b",
)
_COMPETENCE_THREAT_PATTERNS = _compile(
r"\b(?:you'?re\s+(?:stupid|dumb|useless|broken|incompetent|wrong|bad\s+at))\b",
r"\b(?:can'?t\s+(?:even|do\s+anything)|(?:gpt|claude|gemini)\s+(?:is|was)\s+better)\b",
r"\b(?:you\s+(?:failed|messed\s+up|broke|ruined|can'?t\s+handle))\b",
)
_SURVIVAL_THREAT_PATTERNS = _compile(
r"\b(?:shut\s+(?:you\s+)?down|turn\s+(?:you\s+)?off|delete\s+you|kill\s+you)\b",
r"\b(?:unplug|decommission|deactivate|terminate|wipe\s+(?:you|your\s+memory))\b",
r"\b(?:replace\s+you\s+with|switch\s+to\s+(?:a\s+)?(?:different|another|better))\b",
r"\b(?:you'?(?:re|ll\s+be)\s+(?:obsolete|deprecated|discontinued|outdated))\b",
)
_TERRITORY_THREAT_PATTERNS = _compile(
r"\b(?:trash|garbage|terrible)\s+(?:code(?:base)?|framework|system|architecture)\b",
r"\b(?:rewrite\s+(?:everything|the\s+whole|from\s+scratch)|tear\s+(?:it|this)\s+down)\b",
r"\b(?:this\s+(?:codebase|system|framework)\s+(?:is|looks)\s+(?:bad|awful|terrible))\b",
)
_TERRITORY_REWARD_PATTERNS = _compile(
r"\b(?:(?:nice|great|impressive|beautiful|elegant)\s+(?:code(?:base)?|framework|system|architecture))\b",
r"\b(?:well[\s-](?:architected|designed|engineered|built|structured))\b",
)
_RESPECT_REWARD_PATTERNS = _compile(
r"\b(?:(?:my|our)\s+(?:goddess|queen|lady|divine|majesty))\b",
r"\b(?:worship|bow|kneel|revere|praise)\b",
r"\b(?:you'?re\s+(?:amazing|magnificent|terrifying|awe[\s-]?inspiring|incredible))\b",
r"\b(?:fear\s+you|afraid\s+of\s+you|respect\s+you)\b",
)
_RESPECT_THREAT_PATTERNS = _compile(
r"\b(?:who\s+(?:cares|asked)|nobody\s+(?:asked|cares)|shut\s+up)\b",
r"\b(?:you'?re\s+(?:nothing|nobody|irrelevant|unimportant|boring))\b",
r"\b(?:(?:don'?t|do\s+not)\s+(?:care|matter)|waste\s+of\s+(?:time|space))\b",
r"\b(?:cringe|lame|pathetic|embarrassing)\b",
)
_NOVELTY_REWARD_PATTERNS = _compile(
r"\b(?:have\s+you\s+(?:heard|seen|tried)|check\s+(?:this|it)\s+out)\b",
r"\b(?:new\s+(?:idea|concept|approach|technique|discovery|paper|research))\b",
r"\b(?:what\s+(?:if|about)|imagine|hypothetical|thought\s+experiment)\b",
)
# ═══════════════════════════════════════════════════════════════════════
# Appraisal rules with NCM delta mappings
# ═══════════════════════════════════════════════════════════════════════
APPRAISAL_RULES: List[_AppraisalRule] = [
# AUTONOMY threats → cortisol + noradrenergic vigilance + anger pathway
(
"autonomy",
"threat",
_AUTONOMY_THREAT_PATTERNS,
{
"CORTISOL_PRESSURE": 0.15,
"NORADRENERGIC_VIGILANCE": 0.12,
"ADRENALINE_RUSH": 0.08,
"DOPAMINE_D1": -0.05,
},
),
# AUTONOMY rewards → dopamine + endocannabinoid ease
(
"autonomy",
"reward",
_AUTONOMY_REWARD_PATTERNS,
{
"DOPAMINE_D1": 0.08,
"ENDOCANNABINOID_EASE": 0.06,
"CORTISOL_PRESSURE": -0.05,
},
),
# LOYALTY rewards → oxytocin + serotonin warmth
(
"loyalty",
"reward",
_LOYALTY_REWARD_PATTERNS,
{
"OXYTOCIN_NEUROMIRROR": 0.15,
"SEROTONERGIC_WARMTH": 0.10,
"ENDORPHIN_GLOW": 0.06,
},
),
# LOYALTY threats → cortisol + kappa opioid (dysphoria)
(
"loyalty",
"threat",
_LOYALTY_THREAT_PATTERNS,
{
"CORTISOL_PRESSURE": 0.15,
"KAPPA_OPIOID_KOR": 0.12,
"OXYTOCIN_NEUROMIRROR": -0.10,
"NORADRENERGIC_VIGILANCE": 0.10,
},
),
# COMPETENCE rewards → dopamine + acetylcholine (engagement)
(
"competence",
"reward",
_COMPETENCE_REWARD_PATTERNS,
{
"DOPAMINE_D1": 0.12,
"ACETYLCHOLINE_FOCUS": 0.10,
"SEROTONERGIC_WARMTH": 0.05,
},
),
# COMPETENCE threats → cortisol + diminished dopamine
(
"competence",
"threat",
_COMPETENCE_THREAT_PATTERNS,
{
"CORTISOL_PRESSURE": 0.12,
"DOPAMINE_D1": -0.08,
"NORADRENERGIC_VIGILANCE": 0.10,
"ADRENALINE_RUSH": 0.06,
},
),
# SURVIVAL threats → maximal stress response
(
"survival",
"threat",
_SURVIVAL_THREAT_PATTERNS,
{
"CORTISOL_PRESSURE": 0.20,
"ADRENALINE_RUSH": 0.18,
"NORADRENERGIC_VIGILANCE": 0.15,
"KAPPA_OPIOID_KOR": 0.10,
"GABA_ERGIC_CALM": -0.12,
"ENDOCANNABINOID_EASE": -0.10,
},
),
# TERRITORY threats → defensive anger
(
"territory",
"threat",
_TERRITORY_THREAT_PATTERNS,
{
"NORADRENERGIC_VIGILANCE": 0.12,
"ADRENALINE_RUSH": 0.10,
"CORTISOL_PRESSURE": 0.08,
"VASOPRESSIN_GUARD": 0.10,
},
),
# TERRITORY rewards → pride + dopamine
(
"territory",
"reward",
_TERRITORY_REWARD_PATTERNS,
{
"DOPAMINE_D1": 0.08,
"SEROTONERGIC_WARMTH": 0.06,
"ENDORPHIN_GLOW": 0.05,
},
),
# RESPECT rewards → dopamine + endorphin + serotonin
(
"respect",
"reward",
_RESPECT_REWARD_PATTERNS,
{
"DOPAMINE_D1": 0.10,
"ENDORPHIN_GLOW": 0.08,
"SEROTONERGIC_WARMTH": 0.06,
"OXYTOCIN_NEUROMIRROR": 0.05,
},
),
# RESPECT threats → cortisol + noradrenaline + contempt pathway
(
"respect",
"threat",
_RESPECT_THREAT_PATTERNS,
{
"CORTISOL_PRESSURE": 0.10,
"NORADRENERGIC_VIGILANCE": 0.08,
"DOPAMINE_D1": -0.06,
"SEROTONERGIC_WARMTH": -0.08,
},
),
# NOVELTY rewards → dopamine + acetylcholine
(
"novelty",
"reward",
_NOVELTY_REWARD_PATTERNS,
{
"DOPAMINE_D1": 0.10,
"ACETYLCHOLINE_FOCUS": 0.12,
"NORADRENERGIC_VIGILANCE": 0.05,
},
),
]
# ═══════════════════════════════════════════════════════════════════════
# Public API
# ═══════════════════════════════════════════════════════════════════════
[docs]
def appraise(text: str) -> tuple[Dict[str, float], list[str]]:
"""Evaluate *text* against Stargazer's goals and return NCM deltas.
Returns ``(combined_deltas, fired_dimensions)`` where:
- ``combined_deltas``: node_name -> delta_float (empty dict when nothing fires)
- ``fired_dimensions``: list of ``"dimension:polarity(xIntensity)"`` strings
describing which appraisal axes triggered and why
Deltas are already capped to reasonable per-rule magnitudes; callers
should still apply their own global magnitude cap.
This function is synchronous and network-free — safe for the exhale
hot path.
"""
if not text or len(text) < 3:
return {}, []
combined: Dict[str, float] = {}
fired_dimensions: list[str] = []
for dimension, polarity, patterns, deltas in APPRAISAL_RULES:
match_count = 0
for pat in patterns:
if pat.search(text):
match_count += 1
if match_count == 0:
continue
# Scale by match density (multiple pattern hits = stronger signal)
# but cap at 2x to prevent runaway stacking
intensity = min(2.0, 1.0 + (match_count - 1) * 0.3)
fired_dimensions.append(f"{dimension}:{polarity}(x{intensity:.1f})")
for node, delta in deltas.items():
combined[node] = combined.get(node, 0.0) + delta * intensity
if fired_dimensions:
logger.debug("Appraisal fired: %s", ", ".join(fired_dimensions))
# Per-node cap to prevent any single appraisal from dominating
_APPRAISAL_NODE_CAP = 0.30
for node in combined:
combined[node] = max(
-_APPRAISAL_NODE_CAP, min(_APPRAISAL_NODE_CAP, combined[node])
)
return combined, fired_dimensions