// ============================================================ // PES Core — Expression Engine // ============================================================ // The BRIDGE between emotion and expression. // // Pipeline: // Event → TriggerEngine → EmotionState → ExpressionEngine → PlatformOutput // // This module: // 1. Takes trigger results and generates expressions (body+voice+glyphs) // 2. Manages expression queue with priorities // 3. Handles cooldowns (no spam) // 4. Detects combos (two quick events → special expression) // 5. Scales intensity (same emotion, different volume) // 6. Processes owner/group reactions → updates dialect + preferences // 7. Runs passive ticks (spontaneous behavior when idle) // 8. Returns platform-agnostic output // ============================================================ /** * Priority levels for expression queue. * Higher = more urgent, bypasses queue. */ const PRIORITY = { CRITICAL: 100, // rage, scream, dual_rage — always shows HIGH: 75, // alerts, bugs, security NORMAL: 50, // greetings, play, fetch LOW: 25, // idle expressions, yawns AMBIENT: 10, // background state (sleeping animation) }; /** * Maps trigger names to SymbolComposer methods. * Default = express(). Special triggers use specialized methods. */ const TRIGGER_METHOD_MAP = { // Discovery triggers → expressDiscovery() bug_detected: { method: 'expressDiscovery', args: ['bug'] }, system_error: { method: 'expressDiscovery', args: ['bug'] }, anomaly_pattern: { method: 'expressDiscovery', args: ['prediction'] }, security_alert: { method: 'expressDiscovery', args: ['security'] }, performance_issue: { method: 'expressDiscovery', args: ['performance'] }, file_discovered: { method: 'expressDiscovery', args: ['file'] }, pattern_found: { method: 'expressDiscovery', args: ['pattern'] }, // Fetch triggers → expressFetch() fetch_result: { method: 'expressFetch', args: null }, // args from event.quality // Social triggers → expressSocial() another_pes_detected: { method: 'expressSocial', args: ['meet_pes'] }, pes_rage_encounter: { method: 'expressSocial', args: ['dual_rage'] }, agent_calls_pes: { method: 'expressSocial', args: ['agent_call'] }, // Everything else → express() (default) }; /** * Maps trigger names to priority levels. */ const TRIGGER_PRIORITY_MAP = { pes_rage_encounter: PRIORITY.CRITICAL, anomaly_pattern: PRIORITY.HIGH, bug_detected: PRIORITY.HIGH, system_error: PRIORITY.HIGH, security_alert: PRIORITY.HIGH, owner_returned: PRIORITY.NORMAL, owner_praise: PRIORITY.NORMAL, owner_scold: PRIORITY.NORMAL, owner_gives_task: PRIORITY.NORMAL, fetch_result: PRIORITY.NORMAL, another_pes_detected: PRIORITY.NORMAL, agent_calls_pes: PRIORITY.NORMAL, owner_message: PRIORITY.LOW, agent_active: PRIORITY.LOW, owner_ignores: PRIORITY.LOW, fed_good_data: PRIORITY.LOW, fed_junk_data: PRIORITY.LOW, }; /** * Combo definitions: two triggers close together → special combined expression. * windowMs = max time between events for combo to fire. */ const COMBOS = [ { name: 'triumphant_solve', first: 'bug_detected', second: 'bug_solved', windowMs: 60_000, expression: { meaning: 'combo_triumphant_solve', bodyPrefer: ['butt_wiggle_turbo', 'bunny_hop', 'spin'], voicePrefer: ['bark_excitement', 'roo_roo', 'scream'], glyphKeys: ['paw', 'arrow', 'bug', 'arrow', 'check', 'star', 'star'], }, priority: PRIORITY.HIGH, }, { name: 'ecstatic_greeting', first: 'owner_returned', second: 'owner_praise', windowMs: 30_000, expression: { meaning: 'combo_ecstatic_greeting', bodyPrefer: ['butt_wiggle_turbo', 'spin', 'bunny_hop'], voicePrefer: ['scream', 'roo_roo', 'bark_excitement'], glyphKeys: ['paw', 'heart', 'heart', 'heart', 'exclaim', 'exclaim'], }, priority: PRIORITY.HIGH, }, { name: 'double_scold', first: 'owner_scold', second: 'owner_scold', windowMs: 10_000, expression: { meaning: 'combo_double_scold', bodyPrefer: ['ears_flat', 'belly_up', 'hide_behind'], voicePrefer: ['whine', 'yip', 'sigh'], glyphKeys: ['sad', 'sad', 'dots', 'dots', 'dots'], }, priority: PRIORITY.NORMAL, }, { name: 'proud_fetch', first: 'fetch_result', second: 'owner_praise', windowMs: 30_000, expression: { meaning: 'combo_proud_fetch', bodyPrefer: ['butt_wiggle_turbo', 'butt_wiggle_fast', 'nose_nudge'], voicePrefer: ['roo_roo', 'bark_excitement', 'bark_play'], glyphKeys: ['paw', 'ball', 'arrow', 'gift', 'star', 'heart'], }, priority: PRIORITY.HIGH, }, { name: 'system_panic', first: 'system_error', second: 'system_error', windowMs: 60_000, expression: { meaning: 'combo_system_panic', bodyPrefer: ['ears_forward', 'low_crouch'], voicePrefer: ['bark_alert', 'howl', 'scream'], glyphKeys: ['fire', 'fire', 'bolt', 'bolt', 'exclaim', 'exclaim', 'exclaim'], }, priority: PRIORITY.CRITICAL, }, ]; /** * Cooldown config. */ const COOLDOWN = { sameStateSuppressAfter: 3, // suppress after N same expressions in a row sameStateDecayFactor: 0.5, // multiply intensity by this when suppressed maxSuppressed: 5, // fully suppress after this many neverSuppressStates: new Set([ 'rage', 'scared', 'separation_stress', 'greeting_frenzy', ]), recentWindowMs: 60_000, // look at expressions within this window }; // ── EXPRESSION RESULT ───────────────────────────────────────────── /** * What ExpressionEngine returns. Platform-agnostic. * Platform adapters (Telegram, Web, etc.) will consume this. */ class ExpressionResult { constructor({ expression, // PesExpression instance priority, // PRIORITY constant triggerName, // what caused this combo = null, // combo name if combo fired suppressed = false, shouldNotify = false, emotionSnapshot, // current emotion state platformHints = {}, }) { this.expression = expression; this.priority = priority; this.triggerName = triggerName; this.combo = combo; this.suppressed = suppressed; this.shouldNotify = shouldNotify; this.emotionSnapshot = emotionSnapshot; this.platformHints = platformHints; this.timestamp = Date.now(); this.id = `er_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; } toJSON() { return { id: this.id, expression: this.expression ? this.expression.toJSON() : null, priority: this.priority, triggerName: this.triggerName, combo: this.combo, suppressed: this.suppressed, shouldNotify: this.shouldNotify, emotionSnapshot: this.emotionSnapshot, platformHints: this.platformHints, timestamp: this.timestamp, }; } } // ── EXPRESSION ENGINE ───────────────────────────────────────────── class ExpressionEngine { /** * @param {Object} opts * @param {EmotionState} opts.emotionState — PES emotion FSM * @param {TriggerEngine} opts.triggerEngine — trigger processor * @param {SymbolComposer} opts.symbolComposer — symbol/body/voice composer * @param {MemoryStore} opts.memoryStore — persistence (or null for no persistence) */ constructor({ emotionState, triggerEngine, symbolComposer, memoryStore = null }) { this.emotions = emotionState; this.triggers = triggerEngine; this.symbols = symbolComposer; this.store = memoryStore; // State this.level = 0; // PES level (0-100) this.domain = null; // project domain: 'fintech', 'devops', 'content', etc. this.silentMode = false; // suppress voice channel // Queue this.queue = []; // sorted by priority (highest first) this.maxQueue = 20; // Cooldown tracking this.recentExpressions = []; // last N expression results // Combo tracking this.recentTriggers = []; // { triggerName, timestamp } // Feedback: maps expressionId → expression data (for linking reactions) this.pendingFeedback = new Map(); // expressionId → ExpressionResult this.maxPendingFeedback = 100; // Listeners this.listeners = []; } // ══════════════════════════════════════════════════════════════════ // MAIN PIPELINE // ══════════════════════════════════════════════════════════════════ /** * Process an external event through the full pipeline: * Event → Trigger → Emotion → Expression → Queue → Result * * @param {Object} event — { type: string, from?: string, quality?, ...data } * @returns {ExpressionResult|null} */ processEvent(event) { // 1. Run through trigger engine const triggerResult = this.triggers.process(event); if (!triggerResult) return null; const { triggerName, reaction, intensity, xp, emotionSnapshot } = triggerResult; // 2. Check for combo const comboResult = this._checkCombo(triggerName); if (comboResult) { return this._buildComboExpression(comboResult, triggerResult); } // Record trigger for future combo detection this._recordTrigger(triggerName); // 3. Pick expression method and generate expression const expression = this._generateExpression(triggerName, event); if (!expression) return null; // 4. Apply intensity scaling this._applyIntensityScaling(expression); // 5. Check cooldown const cooldownResult = this._applyCooldown(expression); // 6. Determine priority const priority = TRIGGER_PRIORITY_MAP[triggerName] || PRIORITY.NORMAL; // 7. Determine notification const shouldNotify = priority >= PRIORITY.HIGH; // 8. Build platform hints const platformHints = this._buildPlatformHints(expression, triggerName, priority); // 9. Build result const result = new ExpressionResult({ expression: cooldownResult.suppressed ? null : expression, priority, triggerName, suppressed: cooldownResult.suppressed, shouldNotify, emotionSnapshot, platformHints, }); // 10. Add to queue (if not fully suppressed) if (!cooldownResult.suppressed) { this._enqueue(result); } // 11. Track for feedback if (!cooldownResult.suppressed) { this._trackForFeedback(result); } // 12. Record in recent expressions this._recordExpression(result); // 13. Persist interaction if (this.store && !cooldownResult.suppressed) { try { this.store.logInteraction({ actor: event.from || 'system', action_type: triggerName, action_detail: JSON.stringify({ reaction, expressionId: result.id, rendered: expression ? expression.render() : null, }), emotion_state: emotionSnapshot ? emotionSnapshot.state : null, emotion_intensity: emotionSnapshot ? emotionSnapshot.intensity : null, }); } catch (_) { /* store failure shouldn't break expression */ } } // 14. Add XP if earned if (xp > 0 && this.store) { try { const xpReasonMap = { bug_detected: 'found_bug', bug_solved: 'solved_bug', anomaly_pattern: 'prediction', owner_praise: 'command', fetch_result: 'command', }; const reason = xpReasonMap[triggerName] || 'command'; this.store.addXP(xp, reason, triggerName); } catch (_) { /* non-critical */ } } // 15. Emit this._emit('expression', result); return result; } /** * Process a reaction from owner or group member. * Links the reaction to a specific expression and updates dialect + preferences. * * @param {string} expressionId — ID of the expression being reacted to * @param {Object} reaction — { from, type, value, speed_ms } * from: 'owner' | 'group:username' * type: 'emoji' | 'text' | 'action' | 'silence' * value: '👍' | 'молодец!' | 'opened_file' | null * speed_ms: milliseconds until reaction (null if silence) * @returns {Object|null} — updated preference, or null */ processReaction(expressionId, reaction) { const pending = this.pendingFeedback.get(expressionId); if (!pending || !pending.expression) return null; const expr = pending.expression; const { from, type, value, speed_ms } = reaction; // 1. Calculate weight const weight = this._calculateReactionWeight(from, type, value, speed_ms); // 2. Determine if owner reacted positively const isOwner = from === 'owner'; const isPositive = weight > 0; // 3. Reinforce dialect (body + voice preferences) if (isOwner && expr.bodyKey) { this.symbols.dialect.reinforce('body', expr.bodyKey, isPositive); } if (isOwner && expr.voiceKey) { this.symbols.dialect.reinforce('voice', expr.voiceKey, isPositive); } // 4. Extract pattern tags from expression const patternTags = this._extractPatternTags(pending); // 5. Log reaction to store if (this.store) { try { this.store.logReaction({ pes_action: pending.triggerName, pes_expression: expr.render(), pes_context: expr.context || {}, reactor: from, reaction_type: type, reaction_value: value, reaction_speed: speed_ms || null, weight, pattern_tags: patternTags, }); // 6. Recalculate preference for primary pattern if (patternTags.length > 0) { const primaryPattern = `${pending.triggerName}:${patternTags[0]}`; this.store.recalculatePreference(primaryPattern); } } catch (_) { /* non-critical */ } } // 7. Remove from pending this.pendingFeedback.delete(expressionId); // 8. Emit const feedbackResult = { expressionId, weight, from, positive: isPositive, patternTags }; this._emit('feedback', feedbackResult); return feedbackResult; } /** * Passive tick — call periodically when nothing is happening. * Generates spontaneous expressions based on emotional state. * * @param {number} minutesPassed — minutes since last activity * @returns {ExpressionResult|null} */ tick(minutesPassed = 1) { // 1. Update emotion state this.emotions.passiveTick(minutesPassed); const state = this.emotions.current; // 2. Check for spontaneous expression triggers let expression = null; let triggerName = 'passive'; let priority = PRIORITY.AMBIENT; // Loneliness → want attention if (state.loneliness > 0.6 && state.state !== 'sleep' && state.state !== 'nap') { expression = this.symbols.express(state.state, this.level, { passive: true, reason: 'lonely' }); triggerName = 'passive_lonely'; priority = PRIORITY.LOW; } // Low energy → sleepy expression else if (state.energy < 0.2 && state.state !== 'sleep') { expression = this.symbols.express('nap', this.level, { passive: true, reason: 'tired' }); triggerName = 'passive_tired'; priority = PRIORITY.AMBIENT; } // Hungry → begging else if (state.hunger > 0.7 && state.state !== 'sleep') { expression = this.symbols.express('food_obsessed', this.level, { passive: true, reason: 'hungry' }); triggerName = 'passive_hungry'; priority = PRIORITY.LOW; } // Dreaming (while asleep) else if ((state.state === 'sleep' || state.state === 'nap') && Math.random() < 0.1) { expression = this.symbols.express('sleep', this.level, { passive: true, reason: 'dreaming' }); triggerName = 'passive_dream'; priority = PRIORITY.AMBIENT; } if (!expression) return null; // Apply silent mode if (this.silentMode) { expression.voiceKey = null; } const result = new ExpressionResult({ expression, priority, triggerName, emotionSnapshot: state, shouldNotify: priority >= PRIORITY.HIGH, platformHints: this._buildPlatformHints(expression, triggerName, priority), }); this._enqueue(result); this._recordExpression(result); this._emit('expression', result); return result; } // ══════════════════════════════════════════════════════════════════ // QUEUE // ══════════════════════════════════════════════════════════════════ /** * Get next expression from queue (highest priority first). * Removes it from queue. * @returns {ExpressionResult|null} */ dequeue() { if (this.queue.length === 0) return null; return this.queue.shift(); } /** * Peek at next expression without removing. * @returns {ExpressionResult|null} */ peek() { return this.queue.length > 0 ? this.queue[0] : null; } /** * How many expressions are queued. * @returns {number} */ get queueSize() { return this.queue.length; } /** * Drain all queued expressions at once. * @returns {ExpressionResult[]} */ drainQueue() { const items = [...this.queue]; this.queue = []; return items; } // ══════════════════════════════════════════════════════════════════ // MODES & SETTINGS // ══════════════════════════════════════════════════════════════════ /** * Silent mode — suppress voice channel. * Owner said "тихо" → PES whispers only in symbols. */ setSilentMode(on) { this.silentMode = !!on; } /** * Update PES level. Affects expression complexity and unlocks. */ setLevel(level) { this.level = Math.max(0, Math.min(100, level)); this.triggers.setLevel(this.level); } /** * Set project domain — affects which symbols PES uses more. * @param {string} domain — 'fintech', 'devops', 'content', 'ecommerce', 'gaming', etc. */ setDomain(domain) { this.domain = domain; } /** * Get context symbols for current domain from learned preferences. * @returns {string[]} — array of glyph keys */ getContextSymbols() { if (!this.store || !this.domain) return []; try { return this.store.getContextSymbols(this.domain); } catch (_) { return []; } } /** * Check if PES should do an action based on learned preferences. * @param {string} action — e.g. 'fetch:config_files', 'alert:metrics' * @returns {string} — 'prefer'|'increase'|'neutral'|'decrease'|'avoid'|'never' */ shouldDo(action) { if (!this.store) return 'neutral'; try { return this.store.shouldDo(action); } catch (_) { return 'neutral'; } } // ══════════════════════════════════════════════════════════════════ // EVENTS // ══════════════════════════════════════════════════════════════════ /** * Subscribe to engine events. * Events: 'expression', 'feedback', 'combo', 'suppressed' */ on(event, fn) { this.listeners.push({ event, fn }); return () => { this.listeners = this.listeners.filter(l => l.event !== event || l.fn !== fn); }; } /** * Get the last N expression results (for UI/debugging). * @param {number} limit * @returns {ExpressionResult[]} */ getRecentExpressions(limit = 10) { return this.recentExpressions.slice(-limit); } // ══════════════════════════════════════════════════════════════════ // PRIVATE — Expression Generation // ══════════════════════════════════════════════════════════════════ _generateExpression(triggerName, event) { const mapping = TRIGGER_METHOD_MAP[triggerName]; const emotionState = this.emotions.current.state; const context = this._buildContext(event); let expression; if (mapping) { if (mapping.method === 'expressDiscovery') { const type = mapping.args[0]; expression = this.symbols.expressDiscovery(type, this.level, context); } else if (mapping.method === 'expressFetch') { const quality = event.quality || 'maybe'; expression = this.symbols.expressFetch(quality, this.level, context); } else if (mapping.method === 'expressSocial') { const socialEvent = mapping.args[0]; expression = this.symbols.expressSocial(socialEvent, this.level, context); } } else { // Default: express current emotional state expression = this.symbols.express(emotionState, this.level, context); } // Apply silent mode if (this.silentMode && expression) { expression.voiceKey = null; } return expression; } _buildContext(event) { const ctx = {}; if (event.file) ctx.file = event.file; if (event.line) ctx.line = event.line; if (event.agent) ctx.agent = event.agent; if (event.data) ctx.data = event.data; if (event.description) ctx.description = event.description; if (event.from) ctx.from = event.from; if (this.domain) ctx.domain = this.domain; return ctx; } // ══════════════════════════════════════════════════════════════════ // PRIVATE — Intensity Scaling // ══════════════════════════════════════════════════════════════════ /** * Scale expression based on current emotion intensity. * Low intensity → quieter body/voice. High → louder. */ _applyIntensityScaling(expression) { const intensity = this.emotions.intensity || 0.5; // Low intensity: chance to downgrade body/voice if (intensity < 0.3) { // 60% chance to mute voice at low intensity if (Math.random() < 0.6) { expression.voiceKey = null; } // Trim glyphs to max 3 at low intensity if (expression.glyphKeys.length > 3) { expression.glyphKeys = expression.glyphKeys.slice(0, 3); } } // High intensity: could add emphasis glyphs else if (intensity > 0.8) { // Add exclamation if not already present const hasExclaim = expression.glyphKeys.includes('exclaim'); if (!hasExclaim && Math.random() < 0.5) { expression.glyphKeys.push('exclaim'); } } } // ══════════════════════════════════════════════════════════════════ // PRIVATE — Cooldown // ══════════════════════════════════════════════════════════════════ _applyCooldown(expression) { if (!expression) return { suppressed: false }; const state = expression.meaning || ''; // Never suppress critical states if (COOLDOWN.neverSuppressStates.has(state)) { return { suppressed: false }; } // Count recent same-state expressions const now = Date.now(); const recent = this.recentExpressions.filter(r => r.expression && r.expression.meaning === state && (now - r.timestamp) < COOLDOWN.recentWindowMs ); const sameCount = recent.length; if (sameCount >= COOLDOWN.maxSuppressed) { this._emit('suppressed', { state, count: sameCount }); return { suppressed: true }; } if (sameCount >= COOLDOWN.sameStateSuppressAfter) { // Decay intensity instead of full suppress if (expression.glyphKeys.length > 2) { expression.glyphKeys = expression.glyphKeys.slice(0, 2); } if (Math.random() < 0.5) { expression.voiceKey = null; } return { suppressed: false, decayed: true }; } return { suppressed: false }; } // ══════════════════════════════════════════════════════════════════ // PRIVATE — Combos // ══════════════════════════════════════════════════════════════════ _checkCombo(triggerName) { const now = Date.now(); for (const combo of COMBOS) { if (combo.second !== triggerName) continue; // Look for the first trigger in recent history const firstTrigger = this.recentTriggers.find(t => t.triggerName === combo.first && (now - t.timestamp) < combo.windowMs ); if (firstTrigger) { return combo; } } return null; } _buildComboExpression(combo, triggerResult) { const { emotionSnapshot } = triggerResult; // Use dialect to pick preferred body/voice const bodyKey = this.symbols.dialect.prefer('body', combo.expression.bodyPrefer); const voiceKey = this.silentMode ? null : this.symbols.dialect.prefer('voice', combo.expression.voicePrefer); const { PesExpression } = this._getPesExpressionClass(); const expression = new PesExpression({ bodyKey, voiceKey, glyphKeys: [...combo.expression.glyphKeys], meaning: combo.expression.meaning, context: { combo: combo.name }, confidence: 0.95, level: this.level, }); const result = new ExpressionResult({ expression, priority: combo.priority, triggerName: triggerResult.triggerName, combo: combo.name, emotionSnapshot, shouldNotify: combo.priority >= PRIORITY.HIGH, platformHints: this._buildPlatformHints(expression, combo.name, combo.priority), }); this._enqueue(result); this._recordExpression(result); this._recordTrigger(triggerResult.triggerName); this._trackForFeedback(result); this._emit('combo', { combo: combo.name, result }); this._emit('expression', result); return result; } /** * Lazy accessor — avoid circular import. * PesExpression is imported by SymbolComposer, so we grab it from there. */ _getPesExpressionClass() { // Import dynamically to avoid circular dependency // PesExpression is simple enough to construct directly return { PesExpression: class { constructor({ bodyKey, voiceKey, glyphKeys, meaning, context, confidence, level }) { this.bodyKey = bodyKey; this.voiceKey = voiceKey; this.glyphKeys = glyphKeys; this.meaning = meaning; this.context = context; this.confidence = confidence; this.level = level; this.timestamp = Date.now(); this.id = `expr_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; } renderBody() { return this.bodyKey; } renderVoice() { return this.voiceKey; } renderGlyphs() { return this.glyphKeys.join(''); } render() { return [this.bodyKey, this.voiceKey, this.glyphKeys.join('')].filter(Boolean).join(' '); } toJSON() { return { id: this.id, bodyKey: this.bodyKey, voiceKey: this.voiceKey, glyphKeys: this.glyphKeys, rendered: this.render(), meaning: this.meaning, context: this.context, confidence: this.confidence, level: this.level, timestamp: this.timestamp, }; } } }; } // ══════════════════════════════════════════════════════════════════ // PRIVATE — Queue Management // ══════════════════════════════════════════════════════════════════ _enqueue(result) { // Insert sorted by priority (highest first) let inserted = false; for (let i = 0; i < this.queue.length; i++) { if (result.priority > this.queue[i].priority) { this.queue.splice(i, 0, result); inserted = true; break; } } if (!inserted) { this.queue.push(result); } // Trim queue if (this.queue.length > this.maxQueue) { this.queue = this.queue.slice(0, this.maxQueue); } } // ══════════════════════════════════════════════════════════════════ // PRIVATE — Feedback / Reaction Weight // ══════════════════════════════════════════════════════════════════ /** * Calculate reaction weight based on who reacted, how, and how fast. */ _calculateReactionWeight(from, type, value, speed_ms) { // Base weights const isOwner = from === 'owner'; let baseWeight = 0; if (type === 'emoji') { const positive = new Set(['👍', '❤️', '😂', '🔥', '👏', '💪', '🎉', '✅', '💯', '🙌']); const negative = new Set(['👎', '😡', '🤮', '💩', '❌', '🙄', '😤']); if (positive.has(value)) { baseWeight = isOwner ? 1.0 : 0.3; } else if (negative.has(value)) { baseWeight = isOwner ? -1.0 : -0.3; } else { baseWeight = isOwner ? 0.2 : 0.1; // neutral emoji = mild positive } } else if (type === 'text') { // Simple sentiment: check for known words const positiveWords = /молодец|хорош|класс|супер|отлично|good|great|nice|perfect|awesome|yes|да|ура/i; const negativeWords = /плохо|нет|стоп|не надо|хватит|bad|stop|no|wrong|ошибка/i; if (positiveWords.test(value)) { baseWeight = isOwner ? 0.8 : 0.2; } else if (negativeWords.test(value)) { baseWeight = isOwner ? -0.8 : -0.2; } else { baseWeight = isOwner ? 0.1 : 0.05; // neutral text = mild positive } } else if (type === 'action') { // Owner took action (opened file, clicked link) = confirmation baseWeight = isOwner ? 0.6 : 0.2; } else if (type === 'silence') { // Ignored = mild negative for owner, neutral for group baseWeight = isOwner ? -0.1 : 0.0; } // Speed multiplier let speedMultiplier = 1.0; if (speed_ms !== null && speed_ms !== undefined) { if (speed_ms < 30_000) speedMultiplier = 1.5; // < 30s = fast else if (speed_ms > 300_000) speedMultiplier = 0.5; // > 5min = slow } return Math.max(-2.0, Math.min(2.0, baseWeight * speedMultiplier)); } _extractPatternTags(result) { const tags = []; const expr = result.expression; // From trigger name if (result.triggerName) tags.push(result.triggerName); // From expression meaning if (expr && expr.meaning) { const parts = expr.meaning.split('_'); if (parts.length > 1) tags.push(parts.slice(1).join('_')); } // From context if (expr && expr.context) { if (expr.context.file) { // Extract file type const ext = expr.context.file.split('.').pop(); if (ext) tags.push(`file_${ext}`); } if (expr.context.domain) tags.push(expr.context.domain); } return tags; } _trackForFeedback(result) { if (!result.expression) return; this.pendingFeedback.set(result.id, result); // Trim old entries if (this.pendingFeedback.size > this.maxPendingFeedback) { const keys = [...this.pendingFeedback.keys()]; for (let i = 0; i < keys.length - this.maxPendingFeedback; i++) { this.pendingFeedback.delete(keys[i]); } } } // ══════════════════════════════════════════════════════════════════ // PRIVATE — Platform Hints // ══════════════════════════════════════════════════════════════════ /** * Build platform-specific hints for adapters. * Adapters (Telegram, Web) read these to decide sticker/animation/sound. */ _buildPlatformHints(expression, triggerName, priority) { if (!expression) return {}; const hints = { // Telegram adapter telegram: { sticker: this._suggestSticker(expression, triggerName), reaction: this._suggestTelegramReaction(expression), parseMode: 'MarkdownV2', }, // Web adapter (CRM widget / tamagotchi) web: { animation: this._suggestAnimation(expression, triggerName), sound: this._suggestSound(expression), shake: priority >= PRIORITY.HIGH, glow: priority >= PRIORITY.CRITICAL, }, }; return hints; } _suggestSticker(expression, triggerName) { // Map emotion/trigger to sticker pack reference const stickerMap = { greeting_frenzy: 'corgi_hello', butt_wiggle: 'corgi_wiggle', zoomies: 'corgi_zoomies', rage: 'corgi_angry', sleep: 'corgi_sleep', nap: 'corgi_sleepy', puppy_eyes: 'corgi_sad', happy: 'corgi_happy', alert: 'corgi_alert', scared: 'corgi_scared', dramatic_tantrum:'corgi_drama', stubborn_refuse: 'corgi_stubborn', food_obsessed: 'corgi_hungry', sploot: 'corgi_sploot', herding: 'corgi_working', puzzle_solving: 'corgi_thinking', }; const state = expression.meaning || ''; return stickerMap[state] || stickerMap[expression.bodyKey] || 'corgi_default'; } _suggestTelegramReaction(expression) { const reactionMap = { happy: '❤️', butt_wiggle: '🔥', zoomies: '🎉', rage: '😱', puppy_eyes: '😢', alert: '⚡', scared: '😱', food_obsessed: '🤤', greeting_frenzy: '❤️', }; return reactionMap[expression.meaning] || null; } _suggestAnimation(expression, triggerName) { if (!expression) return 'idle'; const animMap = { greeting_frenzy: 'spin', zoomies: 'run', butt_wiggle: 'wiggle', sleep: 'breathe', nap: 'breathe', alert: 'shake', rage: 'tremble', scared: 'tremble', dramatic_tantrum:'flop', sploot: 'flatten', herding: 'crouch', puzzle_solving: 'tilt_head', }; return animMap[expression.meaning] || 'idle'; } _suggestSound(expression) { if (!expression || !expression.voiceKey) return null; // Voice key maps directly to sound file return `${expression.voiceKey}.mp3`; } // ══════════════════════════════════════════════════════════════════ // PRIVATE — Tracking // ══════════════════════════════════════════════════════════════════ _recordTrigger(triggerName) { this.recentTriggers.push({ triggerName, timestamp: Date.now() }); // Keep only last 60 seconds worth const cutoff = Date.now() - 120_000; this.recentTriggers = this.recentTriggers.filter(t => t.timestamp > cutoff); } _recordExpression(result) { this.recentExpressions.push(result); // Keep last 50 if (this.recentExpressions.length > 50) { this.recentExpressions = this.recentExpressions.slice(-50); } } _emit(event, data) { for (const l of this.listeners) { if (l.event === event) { try { l.fn(data); } catch (_) { /* listener failure = ignored */ } } } } } // ── EXPORTS ───────────────────────────────────────────────────────── export { ExpressionEngine, ExpressionResult, PRIORITY, TRIGGER_METHOD_MAP, TRIGGER_PRIORITY_MAP, COMBOS, COOLDOWN, };