Source code for ncm_appraisal

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