UNPKG

morphic-engine-lukaswolfden

Version:

Advanced relationship analysis engine with psychological insights, momentum scoring, and seduction psychology framework

191 lines 8.98 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.calculateMomentumMorphoScore = calculateMomentumMorphoScore; exports.calculateRelationshipScore = calculateRelationshipScore; exports.applyTimeDecay = applyTimeDecay; // ===== Tunables (safe defaults; tweak to taste) ===== const MAX_NEG_AMP = 3.0; // ceiling for negativity amplification const NEG_KNEE = -0.30; // where amplification begins const NEG_SOFTNESS = 0.25; // smoothness of the curve (bigger = smoother) const RECENT_HOURS = 24; // time-weight emphasis window const SAMPLE_CAP = 50; // last N messages considered per update const EARLY_PRIOR_K = 12; // Bayesian-ish guard: n/(n+K) const MOMENTUM_SAT_AT = 50; // momentum reaches 1 by ~50 msgs const STABILITY_SAT_AT = 300; // stability approaches max by ~300 msgs const STABILITY_MAX = 0.985; // veterans barely move per tick const POS_DELTA_CAP = 2.0; // per-update caps (points) const ENG_DELTA_CAP = 1.5; const EMP_DELTA_CAP = 1.2; const AUTH_DELTA_CAP = 0.7; const AUTH_GAIN_WEIGHT = 0.5; // authenticity grows slowly const EMPATHY_PROSOCIAL_BONUS = 0.5; // small bump when prosocial cues present // ===== Small helpers ===== const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v)); /** Smooth amplification that increases as sentiment drops below NEG_KNEE */ function negAmplification(s) { if (s >= NEG_KNEE) return 1.0; // Map (-1..NEG_KNEE) → (1..MAX_NEG_AMP) smoothly const x = clamp((NEG_KNEE - s) / (1 - Math.abs(NEG_KNEE)), 0, 1); // Soft quadratic easing with softness control const eased = Math.pow(x, 1 + NEG_SOFTNESS); return 1 + (MAX_NEG_AMP - 1) * eased; } /** Time weight for recency (last RECENT_HOURS dominate) */ function timeWeight(createdAt, nowMs) { const t = new Date(createdAt).getTime(); const hrs = clamp((nowMs - t) / (1000 * 60 * 60), 0, 1e6); // Linear fade over RECENT_HOURS with a floor to avoid zeroing out return Math.max(0.1, 1 - hrs / RECENT_HOURS); } /** Momentum grows with history; stability dampens deltas for veterans */ function momentumFactor(totalMsg) { return clamp(totalMsg / MOMENTUM_SAT_AT, 0, 1); } function stabilityFactor(totalMsg) { // approaches STABILITY_MAX asymptotically as history grows const f = 1 - Math.exp(-totalMsg / STABILITY_SAT_AT); return clamp(f * STABILITY_MAX, 0, STABILITY_MAX); } /** Early-sample guard (Bayesian prior): scales effect when evidence is small */ function evidenceScale(n, k = EARLY_PRIOR_K) { return n / (n + k); } /** Light prosocial heuristic (no heavy NLP): reward positive valence with any emotions */ function prosocialCue(s) { if (!s.emotions || s.emotions.length === 0) return 0; // only reward when valence is non-negative to avoid mistaking anger/fear for empathy return s.score >= 0 ? EMPATHY_PROSOCIAL_BONUS : 0; } /** Consistency in [0,1], higher = steadier behavior */ function consistency01(scores) { if (scores.length < 2) return 0; const avg = scores.reduce((a, b) => a + b, 0) / scores.length; const varRaw = scores.reduce((a, b) => a + Math.pow(b - avg, 2), 0) / scores.length; // Normalize variance by theoretical max (score range is [-1,1], worst-case variance ~1) const normVar = clamp(varRaw, 0, 1); return 1 - normVar; // 1 = perfectly consistent, 0 = highly erratic } /** Safe sampler: last N messages that have sentiment */ function sampleRecent(arr, cap = SAMPLE_CAP) { if (arr.length <= cap) return arr; return arr.slice(arr.length - cap); } // ===== Main: momentum-based MorphoScore ===== function calculateMomentumMorphoScore(previousScore, recentMessages, totalMessageCount) { const base = previousScore ?? { overall: 50, positivity: 50, engagement: 50, empathy: 50, authenticity: 50 }; const now = Date.now(); const sampled = sampleRecent(recentMessages, SAMPLE_CAP).filter(m => m.sentiment); const n = sampled.length; if (n === 0) return base; // One O(n) pass: accumulate weighted metrics let posDelta = 0; let engDelta = 0; let empDelta = 0; const posScores = []; const evidScale = evidenceScale(n); const mFactor = momentumFactor(totalMessageCount); const stab = stabilityFactor(totalMessageCount); const changeRate = 1 - stab; for (const m of sampled) { const s = m.sentiment; const val = clamp(s.score, -1, 1); const eng = clamp(s.engagement ?? 0, 0, 1); const w = timeWeight(m.createdAt, now) * (0.5 + 0.5 * mFactor); // couple to momentum (active users weigh more) // Smooth negative amplification (recency-aware via w) const amp = negAmplification(val); posDelta += val * amp * w; // Engagement: prefer normalized, cheap features only engDelta += eng * w; // Empathy: small bonus when emotions present & valence non-negative empDelta += prosocialCue(s) * w; posScores.push(val); } // Normalize aggregate deltas by total weight (approximate with n) const norm = n > 0 ? (1 / Math.sqrt(n)) : 1; // diminish volatility with more samples posDelta *= norm * evidScale * changeRate; engDelta *= norm * evidScale * changeRate * 0.5; // engagement swings milder than positivity empDelta *= norm * evidScale * changeRate * 0.4; // empathy grows slowly // Apply per-tick caps for human-stable feel const newPos = clamp(base.positivity + clamp(posDelta * 10, -POS_DELTA_CAP, POS_DELTA_CAP), 0, 100); const newEng = clamp(base.engagement + clamp(engDelta * 10, -ENG_DELTA_CAP, ENG_DELTA_CAP), 0, 100); const newEmp = clamp(base.empathy + clamp(empDelta * 10, -EMP_DELTA_CAP, EMP_DELTA_CAP), 0, 100); // Authenticity: grows with consistency, never punished downward here const cons = consistency01(posScores); const authDelta = cons * evidScale * changeRate * AUTH_GAIN_WEIGHT; const newAuth = clamp(base.authenticity + Math.min(authDelta * 10, AUTH_DELTA_CAP), 0, 100); const overall = Math.round((newPos + newEng + newEmp + newAuth) / 4); return { overall, positivity: Math.round(newPos), engagement: Math.round(newEng), empathy: Math.round(newEmp), authenticity: Math.round(newAuth), }; } // ===== Relationship score with momentum & graceful negatives ===== function calculateRelationshipScore(currentScore, recentInteractions, user1Id, user2Id) { if (!recentInteractions.length) return { score: currentScore, momentum: 0 }; const u1 = recentInteractions.filter(i => i.userId === user1Id && i.sentiment); const u2 = recentInteractions.filter(i => i.userId === user2Id && i.sentiment); if (!u1.length || !u2.length) return { score: currentScore, momentum: 0 }; const now = Date.now(); const samp = sampleRecent(recentInteractions, SAMPLE_CAP).filter(i => i.sentiment); // Compute average valence per user const avg = (xs) => xs.reduce((a, i) => a + clamp(i.sentiment.score, -1, 1), 0) / xs.length; const u1Avg = avg(u1); const u2Avg = avg(u2); const avgVal = (u1Avg + u2Avg) / 2; // Negative severity × recency based penalty (smooth, bounded) let negPenalty = 0; let recentPosPush = 0; let negCount = 0; for (const i of samp) { const v = clamp(i.sentiment.score, -1, 1); const w = timeWeight(i.createdAt ?? now, now); if (v < NEG_KNEE) { const amp = negAmplification(v); negPenalty += Math.abs(v) * amp * w; negCount++; } else if (v > 0) { // reward consistent positive exchanges recentPosPush += v * w; } } // Convert aggregates to bounded deltas // Negatives: one sharp incident hurts, but severity & recency matter const negDrop = clamp(negPenalty * 10, 0, 18); // cap large drops // Positives: gradual growth with diminishing returns const strength = Math.min(u1.length, u2.length); const posGain = clamp((avgVal > 0 ? avgVal : 0) * (2.2 * Math.log(1 + strength)) + recentPosPush * 0.8, 0.5, 9); let scoreDelta = 0; let momentum = 0; if (negCount > 0) { scoreDelta = -negDrop; momentum = -Math.min(20, Math.round(negPenalty * 25)); // negative momentum proportional to penalty } else { scoreDelta = posGain; momentum = Math.min(10, Math.round(avgVal * 8 + strength * 0.5)); } const newScore = clamp(Math.round(currentScore + scoreDelta), 0, 100); return { score: newScore, momentum }; } // ===== Time decay: EMA towards 50 after inactivity ===== function applyTimeDecay(score, daysSinceLastInteraction) { if (daysSinceLastInteraction <= 1) return score; const alpha = clamp(0.06 + 0.01 * Math.min(daysSinceLastInteraction, 21), 0.06, 0.27); // faster after long idle const target = 50; return Math.round(score + alpha * (target - score)); } //# sourceMappingURL=momentum-scoring.js.map