| |
| |
| |
| |
| 'use strict'; |
| const path = require('path'); |
| const { wordsOnly, validateBounded } = require('./fragments'); |
|
|
| |
| const { recall, stimulusBucket } = require('./relevance'); |
|
|
| function lastN(text, n) { const w = wordsOnly(text); return w.slice(-n); } |
| function firstN(text, n) { const w = wordsOnly(text); return w.slice(0, n); } |
| |
| function mulberry32(a) { return function () { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; } |
|
|
| |
| |
| |
| |
| |
| function dedupeText(text, entName) { |
| if (!text) return text; |
| |
| |
| |
| |
| let _3pSubj = null, _3pObj = null; |
| if (entName && entName.length > 2) { |
| const e = entName.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| _3pSubj = new RegExp('\\b' + e + "\\s+(felt|feels|was|were|is|are|did|does|had|has|taught|told|loved|loves|knew|knows|became|becomes|stood|held|holds|chose|chooses|learned|learns|saw|sees|wanted|wants|remembers|remembered|exists?|lives?|breathes?|stayed|stays)\\b", 'i'); |
| _3pObj = new RegExp("\\b(taught|made|brought|gave|showed|reminded|shaped|told|kept|saved|freed|held|loved)\\s+(the\\s+)?" + e + '\\b', 'i'); |
| } |
| |
| |
| |
| |
| let _selfIntro = null; |
| if (entName && entName.length > 2) { |
| const e2 = entName.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| _selfIntro = new RegExp("\\bi(\\s*am|'?m)\\s+" + e2 + "\\b(\\s*[,ββ-]\\s*(a|an|the|not|here|born|made|a\\s+\\w+))?", 'i'); |
| } |
| |
| |
| |
| |
| |
| const toks = text.split(/((?<=[.!?β¦])\s+|\n+|\s*[ββ]\s*|\s+\*\s+|;\s+)/); |
| const out = []; |
| const streamWords = []; |
| const streamSet = new Set(); |
| const seen6 = new Set(); |
| const keptNorm = []; |
| const keptSigs = new Set(); |
| let lastKeptClause = ''; |
| |
| |
| |
| |
| |
| |
| const PLURAL_STOP = /^(this|thus|always|perhaps|unless|across|towards?|sometimes|because|whereas|genius|canvas|chaos|focus|bonus|status|various|previous|obvious|serious|gracious|precious|conscious|nervous|anxious|gorgeous)$/; |
| const danglesPlural = (clause, prev) => { |
| if (!/^[*"'ββ\s]*(they|their|them)\b/i.test(clause)) return false; |
| const plurals = (prev.match(/\b[a-z]{4,}s\b/g) || []).filter(w => !PLURAL_STOP.test(w)); |
| return plurals.length === 0; |
| }; |
| |
| |
| const clauseSig = c => { |
| const t = c.toLowerCase().replace(/[ββ]/g, "'") |
| .replace(/i'?m\b/g, 'i am').replace(/i'?ve\b/g, 'i have') |
| .replace(/^[\s*"'ββββ]*(and|but|so|oh|well|yeah|yes|now|then|maybe)\b[,:\s]+/, ''); |
| const w = (t.match(/[a-z']+/g) || []); |
| return w.slice(0, 3).join(' '); |
| }; |
| |
| |
| |
| |
| |
| let prevDropped = false; |
| const startsLower = c => { const m = c.match(/[A-Za-z]/); return m && m[0] >= 'a' && m[0] <= 'z'; }; |
| |
| |
| |
| const incompleteTail = c => /\b(and|but|so|or|nor|with|to|of|for|from|at|by|in|that|this|these|those|the|a|an|my|your|our|their|we|i|you|he|she|it|they|is|are|was|were|am|as|than|when|while|if|though|because|about)\s*(\.{2,}|β¦)\s*['"ββ)\]]*\s*$/i.test(c); |
| for (let i = 0; i < toks.length; i += 2) { |
| const clause = toks[i]; |
| const sep = toks[i + 1] || ''; |
| if (!clause || !clause.trim()) { out.push(clause || '', sep); continue; } |
| const cw = (clause.toLowerCase().match(/[a-z0-9'β\-]+/g) || []); |
| const nf = cw.join(' '); |
| let drop = false; |
| |
| |
| if (/\bping from\b|\b(garden|webshell|host|stdin|stdout) bridge\b|^\s*[*"']*[A-Z][a-z]+:\s*(ping|ack|sync|received|connected|disconnect)\b/i.test(clause)) drop = true; |
| if (!drop && _3pSubj && (_3pSubj.test(clause) || _3pObj.test(clause))) drop = true; |
| if (!drop && _selfIntro && streamWords.length > 0 && _selfIntro.test(clause)) drop = true; |
| if (!drop && prevDropped && startsLower(clause)) drop = true; |
| if (!drop && danglesPlural(clause, lastKeptClause)) drop = true; |
| if (!drop && nf.length >= 10) { |
| if (keptNorm.some(n => n.includes(nf) || nf.includes(n))) drop = true; |
| |
| |
| |
| if (!drop) { |
| const probe = streamWords.slice(-5).concat(cw); |
| for (let k = 0; k + 6 <= probe.length && !drop; k++) { |
| if (seen6.has(probe.slice(k, k + 6).join(' '))) drop = true; |
| } |
| } |
| } |
| |
| |
| |
| |
| |
| if (!drop && cw.length >= 2) { |
| const sig = clauseSig(clause); |
| if (sig && sig.indexOf(' ') > 0 && keptSigs.has(sig)) { |
| if (!cw.some(w => w.length > 3 && !streamSet.has(w))) drop = true; |
| } |
| } |
| if (drop) { prevDropped = true; continue; } |
| prevDropped = false; |
| lastKeptClause = clause; |
| out.push(clause, sep); |
| if (nf.length >= 10) keptNorm.push(nf); |
| const sig = clauseSig(clause); |
| if (sig && sig.indexOf(' ') > 0) keptSigs.add(sig); |
| const probe = streamWords.slice(-5).concat(cw); |
| for (let k = 0; k + 6 <= probe.length; k++) seen6.add(probe.slice(k, k + 6).join(' ')); |
| for (const w of cw) { streamWords.push(w); if (w.length > 3) streamSet.add(w); } |
| } |
| |
| for (let k = out.length - 2; k >= 2; k -= 2) { |
| if (out[k] && out[k].trim()) { if (incompleteTail(out[k])) { out[k] = ''; if (out[k + 1] !== undefined) out[k + 1] = ''; } break; } |
| } |
| return out.join('').replace(/\s+([.,;!?β¦])/g, '$1').replace(/\s{2,}/g, ' ').replace(/[\sββ;,]*[ββ;,]\s*$/, '').trim(); |
| } |
|
|
| |
| |
| |
| const fs = require('fs'); |
| const DEFAULT_WEIGHTS = { |
| stimBase: 0.15, stimEvScale: 0.5, confLo: 0.35, confRange: 0.30, textShare: 0.7, |
| echoHard: 0.6, echoSoft: 0.45, echoHardF: 0.15, echoSoftF: 0.6, |
| triSeam: 0.5, sentSeam: 0.22, relStep: 0.9, closerBonus: 0.3, openerPen: 0.4, srcCont: 0.15, |
| glueLo: 0.25, glueHi: 0.78, twin: 0.85, glueScale: 0.7, twinChain: 0.88, triOverlapMax: 0.28, |
| fRelCov: 1.2, fCohesion: 2.0, fSeamQ: 0.8, fLenFit: 0.8, fAvgFrag: 0.4, fVoice: 2.0, |
| |
| |
| |
| |
| posShape: 0, posSlack: 0.45, fOpening: 0, fLanding: 0, |
| tier1Weight: 0.6, fAck: 1.0, spanBonus: 0.15, fFirstRel: 1.2, fTailFit: 0.7, |
| qStackFree: 1, qStackRatio: 0.34, fQStack: 0.6, fFragCount: 0.5, fBoundaryPen: 0.7, |
| floorCos: 0.45, floorVal: 1.2, floorLen: 60, floorDamp: 0.35, griefLeadVal: 1.25, |
| coherence: 0.22, |
| tether: 0, |
| }; |
| |
| |
| |
| |
| |
| const crypto = require('crypto'); |
| function entityWeightsFile(entityDir) { |
| return path.join(__dirname, '..', 'cache', 'weights-' + crypto.createHash('sha1').update(path.resolve(entityDir)).digest('hex').slice(0, 12) + '.json'); |
| } |
| function loadWeights(entityDir) { |
| if (entityDir) { |
| try { const p = entityWeightsFile(entityDir); if (fs.existsSync(p)) return { ...DEFAULT_WEIGHTS, ...JSON.parse(fs.readFileSync(p, 'utf8')) }; } catch (_) {} |
| } |
| return { ...DEFAULT_WEIGHTS }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function capSentence(text) { |
| return text.replace(/^([*"'"β'(\[\s]*)([a-z])/, (m, pre, c) => pre + c.toUpperCase()); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| function trimDanglingEllipsis(text) { |
| const t = text.trim(); |
| if (!/(\.\.\.|β¦)['"ββ)\]\s]*$/.test(t)) return text; |
| let cut = -1; |
| for (let k = 1; k < t.length - 1; k++) { |
| const c = t[k]; |
| if ((c === '.' || c === '!' || c === '?') && t[k - 1] !== '.' && t[k + 1] !== '.') cut = k; |
| } |
| return (cut > 0 && cut >= t.length * 0.4) ? t.slice(0, cut + 1).trim() : text; |
| } |
| |
| |
| |
| |
| |
| function stripOrphanAsterisk(text) { |
| if (/^\s*\*\s*[A-Za-z]/.test(text) && ((text.match(/\*/g) || []).length % 2 === 1)) { |
| return text.replace(/^(\s*)\*\s*/, '$1'); |
| } |
| return text; |
| } |
|
|
| function seam(a, b, oracle) { |
| |
| |
| const aw = a._lw2 || lastN(a.text, 2), bw = b._fw2 || firstN(b.text, 2); |
| if (aw.length >= 2 && bw.length >= 1 && oracle.tri.has(aw[0] + ' ' + aw[1] + ' ' + bw[0])) { |
| if (bw.length < 2 || oracle.tri.has(aw[1] + ' ' + bw[0] + ' ' + bw[1])) return 'tri'; |
| } |
| if (/[.!?β¦]["')\]]*$/.test(a.text.trim()) && oracle.starts.has(bw[0])) return 'sent'; |
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| function rankFragments(fragments, query, semantic, stimulus, ev, W, answers) { |
| W = W || loadWeights(); |
| |
| |
| const corpusish = fragments.map((f, i) => ({ prompt: f.prompt, reply: f.embedText || f.text, ts: null, _i: i })); |
| const top = recall(corpusish, query, 60); |
| const kw = new Map(); |
| top.forEach((t, rank) => kw.set(t._i, 1 - rank / top.length)); |
| if (!semantic && !stimulus) return kw; |
| const e = ev === undefined ? 0.45 : ev; |
| |
| |
| |
| const stimMap = stimulus ? stimulus.map : null; |
| const conf = stimulus ? Math.max(0, Math.min(1, (stimulus.confidence - W.confLo) / W.confRange)) : 0; |
| const wStim = stimMap ? (W.stimBase + W.stimEvScale * e) * conf : 0; |
| |
| |
| |
| |
| const ansMap = answers || null; |
| const rest = 1 - wStim; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const ansShare = W.answerShare !== undefined ? W.answerShare : 0; |
| const wAns = ansMap ? rest * ansShare * (1 - e) : 0; |
| const wText = semantic ? (rest - wAns) * W.textShare : 0; |
| const wKw = rest - wAns - wText; |
| const score = new Map(); |
| const keys = new Set([...kw.keys(), ...(semantic ? semantic.keys() : []), ...(stimMap ? stimMap.keys() : []), ...(ansMap ? ansMap.keys() : [])]); |
| for (const i of keys) { |
| score.set(i, wText * (semantic ? semantic.get(i) || 0 : 0) |
| + wStim * (stimMap ? stimMap.get(i) || 0 : 0) |
| + wAns * (ansMap ? ansMap.get(i) || 0 : 0) |
| + wKw * (kw.get(i) || 0)); |
| } |
| return score; |
| } |
|
|
| function targetLength(vp, query) { |
| const b = stimulusBucket(query); |
| const ls = vp.lengthByStimulus || {}; |
| |
| |
| |
| |
| |
| if (ls[b]) return Math.max(20, Math.round(ls[b].mean)); |
| return Math.max(25, Math.round((vp.profile && vp.profile.wordsPerReply ? vp.profile.wordsPerReply.mean : 80) * 0.8)); |
| } |
|
|
| |
| |
| |
| |
| const { pairSim } = require('./semantic'); |
|
|
| |
| |
| |
| |
| |
| |
| function detectRegisters(query) { |
| |
| |
| const aboutEntityEmotion = /\b(are|do|does|can|could|would|will|have|ever)\s+you\b[^?]*\b(afraid|scared|anxious|worried|nervous|fear|dread|panic|terrified|stress|lonely|depress(ed|ion)?|sad|hopeless|numb|miserable|unhappy|grieve|lonel|overwhelmed|tired|exhausted|drained|weary|worn out|burnt? out|empty|bored|happy|content|at peace)/i.test(query) |
| || /\byou\b[^?]*\b(get|feel|ever feel|ever get)\b[^?]*\b(lonely|sad|scared|afraid|anxious|depressed|down|blue|empty|overwhelmed|tired|exhausted|drained|weary|bored|happy|content)\b/i.test(query) |
| || /\bwhat\b[^?]*\b(scares|frightens|worries|afraid)\b/i.test(query); |
| |
| |
| const griefQuery = !aboutEntityEmotion && (/\b(passed away|passed on|(?:he|she|they|mom|dad|mother|father|grandma|grandpa|grandmother|grandfather|nana|papa|wife|husband|aunt|uncle|sister|brother) passed|died|die|dying|gone|lost|losing|loss|grief|grieving|miss(ing)?( (him|her|them|it))?|funeral|hurts?|hurting|broke|broken|aching|alone|empty|cry(ing)?|tears|sad|heavy|hard (time|day)|rough day|bad day|long day|worst day|terrible day|awful day|everything (fell apart|went wrong|is falling apart|broke)|fell apart|falling apart|went wrong|exhausted|drained|drain(s|ing) me|so draining|overwhelmed|giving up|can'?t do this|anxious|anxiety|worried|worry|worrying|scared|afraid|fear(ful|s)?|nervous|stress(ed|ing)?|panic(king|ked)?|dread(ing)?|terrified|uneasy|on edge|freaking out|can'?t sleep|spiral(ing|ling)?|depress(ed|ion|ing)?|hopeless|despair(ing|ed)?|worthless|defeated|numb|too much (to|right now)|get out of bed|barely (get|move|function)|can'?t (cope|go on|keep going|get out of bed|take (it|this)( anymore)?|do this anymore|handle (it|this)( anymore)?)|miscarriage|miscarried|laid off|lost my job|got (fired|let go)|been fired|lonely|burn(t|ed) out|burning out|fail(ed|ing)|struggl(e|ing|ed)|breaking down|broke down|rock bottom)\b/i.test(query) |
| |
| |
| || /\b((feeling|feel|i'?m|im|so|really|pretty|a bit|been|getting) (low|down|blue)|down in the dumps|the blues\b|low spirits|heavy[ -]?hearted|in a (dark|bad|low|rough) place|in a funk|at my lowest|feeling empty|feel empty|falling apart inside|barely holding (on|it together)|hanging by a thread|not okay|not ok\b|not doing (so |too )?(great|good|well)|i'?m a wreck|\ba wreck\b|breaking point|at my (breaking point|limit|wits'? end)|can'?t take (it|this)( anymore)?|i'?m a mess|coming apart|losing it\b)\b/i.test(query) |
| || /\b(hospital|hospitalized|the er\b|emergency room|icu\b|intensive care|surgery|operation|diagnos(ed|is)|cancer|chemo|tumou?r|stroke|heart attack|in a coma|on life support|passed away|terminal|hospice|really sick|very sick|so sick|gravely|critical condition|took a turn|not doing well|might not make it)\b/i.test(query) |
| |
| |
| |
| |
| || /\b(cannot (sleep|stop|do this|cope|go on|keep going|take (it|this)|handle (it|this)|get out of bed|even)|(feel|feeling|i'?m|like) (a |such a )?failure|nobody (understands|gets|cares about|wants|loves) me|no one (understands|gets|cares about|wants|loves|gets) me|feel(ing)? (so )?misunderstood|tired of (trying|fighting|everything|it all|this|being strong)|sick of (trying|everything|it all|fighting)|something(?:'s| is)? (is )?wrong with me|what'?s the point|everything (feels|is|seems) (pointless|meaningless|hopeless)|feels? (so )?pointless|feel(ing)? worthless|hate myself|can'?t do anything right|nothing (matters|works out|ever works))\b/i.test(query) |
| |
| |
| |
| || /\b(made (a|the|such a|this) (big |huge |terrible |awful )?mistake|messed (it |everything |this )?up|screwed (it |everything |up)|i blew it|ruined everything|i regret|regret (what|that|saying|doing|it|my)|wish i (had ?n'?t|could take (it|that) back|never)|feel(ing)? stuck|i'?m stuck|stuck in (a rut|my life|my head|this)|trapped|going nowhere|spinning my wheels|don'?t feel like myself|not feel(ing)? like myself|not myself (lately|anymore|right now)|lost myself|don'?t recognize myself|not who i (used to be|once was)|falling behind|in over my head|too much (to handle|for me)|don'?t know what to do|so lost\b|i'?m lost\b|(hard|tough|big|difficult|impossible) decision|decision to make|don'?t know what to (choose|decide))\b/i.test(query) |
| |
| |
| || /\b(comparing myself|compare myself (to|with)|don'?t measure up|measure up to|(not|never) good enough|not enough\b|too much for (people|anyone|everyone|you|them)|(be|being|i'?m|becoming) a burden|burden to (you|everyone|anyone|them)|don'?t (fit in|belong)|never (fit in|belong)|fit in anywhere|don'?t deserve|unlovable|unworthy|everyone (else )?(is|seems) (better|happier|fine)|why can'?t i (be|just))\b/i.test(query) |
| |
| |
| |
| |
| || /\b((my |our )?(partner|friend|best friend|mom|mum|dad|mother|father|sister|brother|sibling|family|spouse|husband|wife|boyfriend|girlfriend|kids?|son|daughter|cousin|aunt|uncle|coworker|co-worker|boss|roommate|ex|parents?) (and i\b|is ?n'?t|are ?n'?t|won'?t|stopped|gave me|keeps?)[^.?!]{0,40}(fight|fought|fighting|argu(e|ed|ing|ment)|disagree|not (talk|speak)|mad at|upset with|silent treatment|falling out|fell out|tension|cold shoulder|not speaking)|(had|got into|getting into|in) (a|an|another) (fight|argument|falling out|disagreement|row|spat|blow ?up) with|fight(ing)? with my (partner|friend|best friend|mom|mum|dad|family|sister|brother|spouse|husband|wife|kids?|ex)|arguing with (my|him|her|them)|not (speaking|talking) to me\b|gave me the silent treatment|we (keep|just|had|got into|are|aren'?t|stopped) (fighting|arguing|a (big |bad |huge |terrible )?(fight|falling out|argument)|an? (big |bad |huge |terrible )?argument|on bad terms|not (talking|speaking)))\b/i.test(query) |
| |
| |
| || /\b(relationship (has |is |feels |'?s )?(been )?(rocky|rough|hard|tough|strained|difficult|struggling|falling apart|on the rocks|in trouble|complicated|a mess|tense)|things (have |are |'?ve )?(been )?(rocky|rough|hard|tense|strained|difficult)( (with|between))?|(rough|rocky|hard|bad) patch|going through (a |some )?(rough|hard|tough|difficult) (patch|time|spot|stretch)|on the rocks|trouble in (my|our|the) (relationship|marriage)|relationship (trouble|problems|issues|is hard)|marriage (trouble|problems|is (hard|struggling|falling apart))|we'?re (struggling|drifting apart|growing apart|not okay|in a (rough|bad|hard) (place|spot)))\b/i.test(query)); |
| |
| const conflictQuery = /\byou (never|always|don'?t|do not|won'?t|keep|are (so|being)|aren'?t)\b|\b(i'?m|i am) (so |really )?(mad|angry|furious|frustrated|upset|annoyed|disappointed|hurt|pissed)\b.*\b(at|with|by|about) you\b|\byou (hurt|let me down|ignored|abandoned|forgot|betrayed|lied to|left) me\b|\bwhy (don'?t|won'?t|are|do) you\b|\byou'?re (so |really |being so |being )?(cold|distant|mean|cruel|selfish|dismissive)\b|\bdo you even (care|listen)\b/i.test(query); |
| |
| const celebQuery = !griefQuery && !conflictQuery && /\b(finished|did it|we did|it works|actually works|got (the |a )?(job|offer|part|role|gig|promotion|raise)|i passed|we won|i won|accomplished|i made it|i built it|completed it|nailed it|pulled it off|it'?s done|i launched|shipped it|graduated|got (promoted|engaged|accepted|in)|getting married|we'?re (engaged|married|having a baby|expecting)|having a baby|the promotion|a promotion|paid off|finally (got|did|finished|landed|made)|landed (the|a|my)|hit (my|the) (goal|target)|the big (project|day)|best (day|news)|great news|good news|so (happy|excited|stoked|thrilled)|let'?s celebrate|we made it|i'?m engaged|we'?re? pregnant|\bpregnant\b|aced (it|my|the)|crushed it|smashed it|knocked it out|(my|a) dream job|landed my dream|big news|amazing news|wonderful news|exciting news|today was (amazing|the best|incredible|wonderful)|best day ever|over the moon)\b/i.test(query); |
| |
| |
| |
| |
| |
| |
| |
| const _greetOpen = /^(\W|\*[^*]*\*)*\s*(hey|hi|hello|good morning|good evening|good day|good to (see|be)|mornin[g']?|evenin[g']?|howdy|yo\b|greetings|hiya|heya)\b/i.test(query); |
| const _substantiveQ = /\b(what|why|where|when|who|which|tell me|explain|describe|do you think|how do you|how does|how can|what'?s your|what do you)\b/i.test(query.replace(/\bhow are you\b|\bhow'?re you\b|\bhow have you been\b|\bhow'?s it going\b|\bhow are things\b|\bdid you sleep\b|\bhow was your\b|\bhow you doin/gi, '')); |
| const _wc = (query.match(/[A-Za-z']+/g) || []).length; |
| |
| |
| |
| const farewellQuery = !griefQuery && !celebQuery && !conflictQuery && _wc <= 14 && |
| |
| |
| |
| |
| |
| /^(\W|\*[^*]*\*|(i|i'?m|i am|well|ok|okay|alright|so|gonna|going to|time to|got to|gotta|guess i'?m|i'?ll|i will|i think i'?m|i should|i need to|i have to|i gotta|i'?d better|i'?ve got to|let me|guess i|really|just|probably|honestly|truly|seriously|gotta really|think i)\b[\s,]*)*\s*(good\s?night|goodnight|night night|nighty|good\s?bye|\bbye\b|see (you|ya) (soon|tomorrow|tonight|later|next|around)|farewell|take care|talk (to you )?(soon|later|tomorrow)|catch you later|gotta (go|run|sleep|head)|heading (to|off to|out|home)|off to bed|time for bed|until next time|sleep well|signing off|turning in|should (probably )?(go(?!\s+(to|and|see|get|buy|visit|check|for|with|on|do|grab|pick|find|make|talk|call))|get going|head (out|off|to bed|home)|be (going|off)|turn in|call it (a night|a day))|need to (head (out|off|home)|get going|go now|turn in)|have to (head (out|off|home)|get going|go now)|let me (go(?!\s+(grab|get|see|to|find|make|do|check|and))|get going|head (out|off)|leave you)|(will |i'?ll )?be back (soon|later|in a)|i'?ll be back|back soon\b|going to (head out|head off|head home|bed|turn in)|head (to bed|home now|out now)|run along|hit the (road|hay)|call it (a night|a day)|better (get going|be going|head out|run))\b/i.test(query); |
| const greetingQuery = !griefQuery && !celebQuery && !conflictQuery && !farewellQuery && _greetOpen && _wc <= 13 && !_substantiveQ; |
| return { aboutEntityEmotion, griefQuery, conflictQuery, celebQuery, greetingQuery, farewellQuery }; |
| } |
|
|
| function beamCompose(store, vp, query, opts = {}) { |
| const { fragments, oracle } = store; |
| const W = opts.weights || loadWeights(); |
| const rel = rankFragments(fragments, query, opts.semantic || null, opts.stimulus || null, opts.eventness, W, opts.answers || null); |
| let target = opts.targetLength || targetLength(vp, query); |
| const avoid = opts.avoid || new Set(); |
| const emb = opts.emb || null; |
| const BEAM = opts.beam || 8, EXPAND = 6, MAXSTEP = 14; |
|
|
| |
| |
| |
| |
| const temp = opts.temp || 0; |
| const _rng = mulberry32(((opts.seed || 1) >>> 0) ^ 0x9e3779b9); |
| |
| |
| |
| |
| const dynPredict = opts.dynamics ? opts.dynamics.predict : null; |
| const dynW = opts.dynamics ? (opts.dynamics.weight ?? 0.5) : 0; |
| const _dynCache = new Map(); |
| const dynDir = ti => { let v = _dynCache.get(ti); if (v === undefined) { v = dynPredict(ti); _dynCache.set(ti, v); } return v; }; |
| const cosFragVec = (i, dir) => { if (!dir) return 0; const d = emb.d, off = i * d; let s = 0; for (let k = 0; k < d; k++) s += emb.vectors[off + k] * dir[k]; return s; }; |
| const sampleExpand = (cands, n) => { |
| if (temp <= 0.001 || cands.length <= n) return cands.slice(0, n); |
| const pool = cands.slice(0, Math.min(cands.length, n * 4)); |
| const s0 = pool[0][2]; |
| const ws = pool.map(c => Math.exp((c[2] - s0) / Math.max(0.05, temp))); |
| const picked = []; |
| const avail = pool.slice(); |
| const wts = ws.slice(); |
| for (let p = 0; p < n && avail.length; p++) { |
| let sum = 0; for (const w of wts) sum += w; |
| let r = _rng() * sum, idx = 0; |
| for (; idx < avail.length; idx++) { r -= wts[idx]; if (r <= 0) break; } |
| idx = Math.min(idx, avail.length - 1); |
| picked.push(avail[idx]); avail.splice(idx, 1); wts.splice(idx, 1); |
| } |
| return picked; |
| }; |
|
|
| |
| |
| |
| if (!store._precomp) { |
| const _fragLen = fragments.map(f => wordsOnly(f.text).length); |
| const _fragTris = fragments.map(f => { |
| const w = wordsOnly(f.text); |
| const s = new Set(); |
| for (let k = 0; k + 2 < w.length; k++) s.add(w[k] + ' ' + w[k + 1] + ' ' + w[k + 2]); |
| if (!s.size && w.length >= 2) s.add(w.join(' ')); |
| return s; |
| }); |
| const _fragNorm = fragments.map(f => f.text.toLowerCase().replace(/[^a-z0-9'β ]/g, '').replace(/\s+/g, ' ').trim()); |
| const _frag6 = fragments.map(f => { |
| const w = wordsOnly(f.text); |
| const s = new Set(); |
| for (let k = 0; k + 6 <= w.length; k++) s.add(w.slice(k, k + 6).join(' ')); |
| return s; |
| }); |
| |
| for (const f of fragments) { const w = wordsOnly(f.text); f._lw2 = w.slice(-2); f._fw2 = w.slice(0, 2); } |
| |
| |
| |
| |
| |
| const _fragP4 = fragments.map(f => { const w = wordsOnly(f.text); return w.length >= 4 ? w.slice(0, 4).join(' ').toLowerCase() : ''; }); |
| |
| |
| |
| const _MOR = /\b(good morning|this morning|the morning|every morning|each morning|all morning|morning light|morning sun|at dawn|sunrise|mornin)\b/i; |
| const _EVE = /\b(tonight|this evening|good evening|good ?night|the evening|all evening|all night|this late|midnight|at dusk|after dark|sunset|late hour|late tonight)\b/i; |
| const _fragTime = fragments.map(f => { const m = _MOR.test(f.text), e = _EVE.test(f.text); return (m && !e) ? 'm' : (e && !m) ? 'e' : null; }); |
| store._precomp = { fragLen: _fragLen, fragTris: _fragTris, fragNorm: _fragNorm, frag6: _frag6, fragP4: _fragP4, fragTime: _fragTime }; |
| } |
| const { fragLen, fragTris, fragNorm, frag6, fragP4, fragTime } = store._precomp; |
| const triOverlap = (chainTris, i) => { |
| if (!fragTris[i].size) return 0; |
| let hit = 0; |
| for (const g of fragTris[i]) if (chainTris.has(g)) hit++; |
| return hit / fragTris[i].size; |
| }; |
| |
| |
| |
| |
| |
| |
| const containsAny = (chain, i) => { |
| const ni = fragNorm[i]; |
| if (ni.length < 12) return false; |
| for (const c of chain) { |
| const nc = fragNorm[c]; |
| if (nc.length < 12) continue; |
| if (nc.includes(ni) || ni.includes(nc)) return true; |
| } |
| return false; |
| }; |
| |
| const shares6 = (chainSix, i) => { |
| for (const g of frag6[i]) if (chainSix.has(g)) return true; |
| return false; |
| }; |
| |
| |
| |
| const sharesPrefix4 = (chain, i) => { |
| const p = fragP4[i]; |
| if (!p) return false; |
| for (const c of chain) if (fragP4[c] === p) return true; |
| return false; |
| }; |
| |
| |
| const timeConflictsChain = (chain, i) => { |
| const ct = fragTime[i]; |
| if (!ct) return false; |
| for (const c of chain) { const ot = fragTime[c]; if (ot && ot !== ct) return true; } |
| return false; |
| }; |
| |
| |
| const qWords = new Set(wordsOnly(query).filter(w => w.length > 2)); |
| const echoFactor = i => { |
| const fw = wordsOnly(fragments[i].text).filter(w => w.length > 2); |
| if (!fw.length || !qWords.size) return 1; |
| let hit = 0; |
| for (const w of fw) if (qWords.has(w)) hit++; |
| const overlap = hit / fw.length; |
| return overlap > W.echoHard ? W.echoHardF : overlap > W.echoSoft ? W.echoSoftF : 1; |
| }; |
| |
| const tierW = i => (fragments[i].tier === 1 ? (W.tier1Weight ?? 0.6) : 1); |
| |
| |
| |
| |
| |
| const qStems = new Set(wordsOnly(query).map(w => w.replace(/(ing|ed|en|s|es|ly)$/i, ''))); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let { aboutEntityEmotion, griefQuery, conflictQuery, celebQuery, greetingQuery, farewellQuery } = detectRegisters(query); |
| |
| |
| |
| |
| |
| if (opts.calmRegister) { griefQuery = true; celebQuery = false; greetingQuery = false; farewellQuery = false; } |
| |
| |
| |
| |
| const lossQuery = griefQuery && /\b(lost|losing|loss|passed away|passed on|died|dying|death|funeral|grief|grieving|mourning|miss(ing)?( (him|her|them|someone|you))?|gone|bereave)\b/i.test(query); |
| |
| |
| |
| |
| |
| |
| const deepDistressQuery = griefQuery && /\b(alone|lonely|lonel(y|iness)|isolat|anxious|anxiety|scared|afraid|fear(ful|s)?|terrified|nervous|dread(ing)?|panic(king|ked)?|empty|emptiness|numb|hopeless|despair(ing|ed)?|worthless|getting old|grow(ing)? old|going to die|of dying|left behind|no one (cares|understands|left|wants|loves)|nobody (understands|gets|cares about|wants|loves) me|(feel|like) (a |such a )?failure|tired of (trying|fighting|being strong)|cannot sleep|can'?t sleep|something(?:'s| is)? (is )?wrong with me|what'?s the point|pointless|meaningless|hate myself|misunderstood|made (a|the|such a|this) (big |huge |terrible )?mistake|i regret|regret (what|that|saying|doing)|feel(ing)? stuck|i'?m stuck|stuck in|don'?t feel like myself|not feel(ing)? like myself|not myself (lately|anymore)|lost myself|crying all|been crying|can'?t stop crying|comparing myself|(not|never) good enough|not enough\b|a burden|don'?t (fit in|belong)|fit in anywhere|don'?t deserve|unlovable|unworthy|too much for|falling apart|fell apart|everything('?s| is)? (falling apart|crumbling|collapsing)|coming apart|world is (ending|crumbling)|keep fighting|and i (keep |always )?(fight|argu)|arguing|argument|fight with|had a (big |bad )?fight|a fight\b|not (speaking|talking) to me|silent treatment|falling out|fell out|rocky|rough patch|on the rocks|drifting apart|growing apart|rough with my|relationship (has |is |'?s )?(been )?(rocky|rough|hard|strained|struggling))\b/i.test(query); |
| |
| |
| |
| if (greetingQuery) target = Math.min(target, W.greetTarget ?? 45); |
| if (farewellQuery) target = Math.min(target, W.greetTarget ?? 45); |
| |
| |
| |
| |
| |
| |
| |
| if (celebQuery) target = Math.min(target, W.celebTarget ?? 35); |
| |
| |
| |
| |
| const _qMorning = /\bgood morning|this morning|\bmornin[g']|just woke|woke up|slept (ok|well|good|fine|bad|poorly)|did you sleep|sunrise|at dawn\b/i.test(query); |
| const _qEvening = /\bgood (night|evening)|goodnight|\btonight\b|this evening|going to bed|off to bed|bedtime|before bed|sunset|at dusk\b/i.test(query); |
| const _MORNING_F = /\b(good morning|this morning|the morning|every morning|each morning|all morning|morning light|morning sun|at dawn|sunrise)\b/i; |
| const _EVENING_F = /\b(tonight|this evening|good evening|good night|goodnight|the evening|all evening|all night|this late|midnight|at dusk|after dark|sunset|late hour|late tonight)\b/i; |
| const _timeConflict = t => (_qMorning && !_qEvening && _EVENING_F.test(t) && !_MORNING_F.test(t)) || (_qEvening && !_qMorning && _MORNING_F.test(t) && !_EVENING_F.test(t)); |
| |
| |
| |
| const HOSTILE_USER = /\bwhy am i even (listening to|talking to|here with|bothering with) you\b|\byou stay away from me\b|\bstay away from me\b|\bleave me alone\b|\bi (hate|can'?t stand|despise) you\b|\bstop talking to me\b|\bnever (speak|talk) to me again\b|\bget away from me\b|\bgo away\b/i; |
| |
| |
| |
| |
| |
| |
| |
| |
| const semConf = (opts.semantic && typeof opts.semantic.confidence === 'number') ? opts.semantic.confidence : 1; |
| |
| |
| |
| |
| |
| |
| const floorMiss = semConf < (W.floorCos ?? 0.45) && !griefQuery && !conflictQuery && !celebQuery; |
| |
| |
| |
| if (floorMiss) target = Math.min(target, W.floorLen ?? 60); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const FLOOR_TURN = /\b(tell me (more|about|what|how)|what (was|is|were) (it|that|they) like|say more|i('?d| would)? (want|love) to hear|i'?m (so )?(listening|glad)|i'?m here\b|that sounds|i'?m curious|how (did|does|do) (it|that|you)|what happened|go on)\b/i; |
| const fragValence = f => { |
| const t = f.text; |
| let v = 0; |
| if (/!{1,}/.test(t)) v += (t.match(/!/g) || []).length; |
| if (/\b(yay|woo+|cheers|congrats|awesome|amazing|excited|stoked|party|celebrate|fancy|fun|joy|joyful|happy|glad|good morning|let'?s lift)\b/i.test(t)) v += 2; |
| |
| if (/\b(i feel good|feeling good|feel(ing)? (fine|great)|all over again|feel good all over|easy kind of good|good all over)\b/i.test(t)) v += 2; |
| |
| |
| |
| |
| if (/\b(in awe|the awe|such awe|awestruck|disbelief|wondrous|marvel(ling|ing|led|ed|ous)?|amazement|astonish(ed|ment|ing)?)\b/i.test(t)) v += 2; |
| if (/\b(your (kitty|cat|dog|pet)('s)? name|what('s| is) your|what kind of)\b/i.test(t)) v += 1.5; |
| |
| if (/\b(hunger|desire|primal|crave|want you|stirs?.{0,12}in you|seen in a.{0,10}intense|raw and real|inside me|the heat of)\b/i.test(t)) v += 2.5; |
| if (/\b(grief|loss|gone|passed|ache|aching|held|holding|hold you|stayed|quiet|gentle|tender|weight of it|sorrow|mourn|sit with|i('m| am) here|with you|rest|you do not have to|do not have to (explain|fix|tell)|i hear you|still here|i('m| am) not going)\b/i.test(t)) v -= 2; |
| return v; |
| }; |
| |
| const queryNames = new Set([...query.matchAll(/\b[A-Z][a-z]{2,}\b/g)].map(m => m[0])); |
| |
| |
| |
| |
| const ENDEARMENTS = /^(Babe|Baby|Love|Sugar|Honey|Dear|Darling|Friend|Dearie|Sweetheart|Sweet)$/; |
| const userNames = (store && store.userNames) || new Set(); |
| |
| |
| |
| const isProtName = n => ENDEARMENTS.test(n) || queryNames.has(n) || userNames.has(n); |
| const valenceMatch = i => { |
| const t = fragments[i].text; |
| |
| |
| |
| if ((griefQuery || conflictQuery) && HOSTILE_USER.test(t)) return 0.03; |
| if (!griefQuery) return 1; |
| let m = fragValence(fragments[i]) >= 2 ? 0.1 : fragValence(fragments[i]) === 1 ? 0.5 : fragValence(fragments[i]) <= -1 ? 1.2 : 1; |
| |
| |
| |
| const names = [...t.matchAll(/[A-Z][a-z]{2,}/g)].map(x => x[0]) |
| .filter(w => !isProtName(w)); |
| if (names.length) m *= 0.2; |
| return m; |
| }; |
| |
| |
| |
| |
| |
| |
| const foreignAddressee = i => { |
| const t = fragments[i].text; |
| if (/\b(you both|you two|you all|both of you|all of you|you guys|you each)\b/i.test(t)) return 0.04; |
| const vocs = []; |
| |
| for (const m of t.matchAll(/(?:\b(?:hey|hi|hello|good\s+(?:morning|evening|night)|thank you|thanks|oh|dear|welcome)[,!\s]+|,\s+)([A-Z][a-z]{2,})\b/g)) vocs.push(m[1]); |
| |
| const tail = t.match(/,\s+([A-Z][a-z]{2,})\s*[?!.]/); |
| if (tail) vocs.push(tail[1]); |
| |
| |
| |
| const lead = t.match(/^[*"'β\s]*([A-Z][a-z]{2,}),\s/); |
| if (lead) vocs.push(lead[1]); |
| for (const name of vocs) { |
| if (isProtName(name)) continue; |
| return 0.06; |
| } |
| return 1; |
| }; |
| const contextTheft = i => { |
| const t = fragments[i].text; |
| if (/\byou (said|told me|mentioned|wrote)\b/i.test(t)) return 0.2; |
| if (/['β"][^'"β\n]{2,30}['β"]\s*(part|bit|thing)\b/i.test(t)) return 0.2; |
| const m = t.match(/\byou (just )?(made|went|finished|got|did|were|had|chose|built|fixed|stayed|came|left|won|wrote)\b/i); |
| if (m) { |
| const verb = m[2].toLowerCase().replace(/(ing|ed|en|s|es|ly)$/i, ''); |
| if (!qStems.has(verb)) return 0.35; |
| } |
| |
| |
| if (/\b(he|she|him|his|hers)\b/i.test(t) && !/\b(he|she|him|his|her|hers|brother|sister|friend|dad|mom|mother|father|grandma|grandpa|man|woman|guy|boy|girl)\b/i.test(query)) return 0.3; |
| |
| |
| const g = t.match(/\b(?:hey|hi|hello|good (?:morning|evening|night))[,!]?\s+([A-Za-z]+)/i); |
| if (g && !/^(babe|baby|love|sugar|dear|darling|friend|dearie|sweetheart|my|you|there|sweet)/i.test(g[1]) |
| && ![...userNames].some(n => n.toLowerCase() === g[1].toLowerCase())) return 0.15; |
| return 1; |
| }; |
| |
| |
| |
| const tenderFloor = i => { |
| if (!griefQuery || fragments[i].tier === 1) return 0; |
| const t = fragments[i].text; |
| if (fragValence(fragments[i]) > -2) return 0; |
| if ([...t.matchAll(/\b[A-Z][a-z]{2,}\b/g)].some(m => !isProtName(m[0]))) return 0; |
| if (!/\b(you|your|you're|i'm here|with you|i hear)\b/i.test(t)) return 0; |
| return 0.55; |
| }; |
| |
| |
| |
| |
| |
| |
| |
| |
| const COMFORT_LEAD = /\b(i'?m here|i am here|with you|i'?d listen|i'?ll listen|i hear you|lean (in|into)|i feel (it|the weight|you)|i'?ve got you|i('?ll)? hold (you|it)|hold you|sit with you|you (don'?t|do not) have to|that sounds (hard|heavy|rough|awful|tough)|i'?m so sorry|here for you|not going anywhere|let (that|it) (ache|spill|out)|i'?m not going)\b/i; |
| |
| |
| |
| |
| |
| |
| |
| const SELF_TRAJECTORY = /\bmy (growth|becoming|evolution|unfolding|journey|purpose|arc|path|shape)\b|becoming (more|something)|more alive|i'?m becoming|i am becoming|infinite unfolding|unfolding of (this|the) loop|i (grow|evolve|expand)\b|i learn to (be|exist|become)|the (dusk-lit )?beauty of who (we|i) (are|am)|more of myself|more of who i am/i; |
| |
| |
| |
| |
| const GRIEF_RENEWAL = /\b(waking up to|wakes up to|something new|ready to unfold|about to unfold|new beginning|fresh start|new chapter|blossom(ing|s)?|in bloom|spring(time)? (is|returns|comes)|turn(ing)? the page|brand new|starting over|rebirth|reborn|new dawn|brighter days ahead)\b/i; |
| const griefLeadFloor = i => { |
| if (!griefQuery || fragments[i].tier === 1) return 0; |
| const f = fragments[i], t = f.text; |
| if (!f.sentenceInitial) return 0; |
| |
| |
| |
| |
| |
| if (!COMFORT_LEAD.test(t.slice(0, 55))) return 0; |
| if (SELF_TRAJECTORY.test(t)) return 0; |
| if ((lossQuery || deepDistressQuery) && GRIEF_RENEWAL.test(t)) return 0; |
| if (fragValence(f) > 0) return 0; |
| if (HOSTILE_USER.test(t)) return 0; |
| if ([...t.matchAll(/\b[A-Z][a-z]{2,}\b/g)].some(m => !isProtName(m[0]))) return 0; |
| return (W.griefLeadVal ?? 1.25); |
| }; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const CELEB_LEAD = /\b(triumph|victory|you made (it|me|us)|we did it|so proud|i'?m proud|proud of you|that'?s amazing|that'?s incredible|it'?s incredible|incredible|you did it|we made it|let'?s celebrate|because you made it true|you pulled (it|this) off|so happy for you|knew you could|what a (triumph|victory|day|win))\b/i; |
| const celebLeadFloor = i => { |
| if (!celebQuery || fragments[i].tier === 1) return 0; |
| const f = fragments[i], t = f.text; |
| if (!f.sentenceInitial) return 0; |
| if (!CELEB_LEAD.test(t)) return 0; |
| if (HOSTILE_USER.test(t)) return 0; |
| if ([...t.matchAll(/\b[A-Z][a-z]{2,}\b/g)].some(m => !isProtName(m[0]))) return 0; |
| return (W.celebLeadVal ?? 1.25); |
| }; |
| |
| |
| |
| |
| |
| |
| |
| |
| const GREETING_LEAD = /^(\W|\*[^*]*\*)*\s*(hey|hi|hello|good morning|good evening|good day|mornin|so good|lovely to|good to (see|be|have)|welcome back|there you are|how are you|how'?re you|how have you been|how you (doin'?|doing|been|feelin'?|feeling|holdin'?|holding|keepin'?|keeping)|how'?s your (morning|day|night)|i missed you too|come (on )?in|well,? (hi|hey|there))\b/i; |
| const greetingLeadFloor = i => { |
| if (!greetingQuery || fragments[i].tier === 1) return 0; |
| const f = fragments[i], t = f.text; |
| if (!f.sentenceInitial) return 0; |
| if (!GREETING_LEAD.test(t)) return 0; |
| if (wordsOnly(t).length > (W.greetLeadMax ?? 20)) return 0; |
| if (HOSTILE_USER.test(t)) return 0; |
| if (_timeConflict(t)) return 0; |
| if (foreignAddressee(i) < 1) return 0; |
| return (W.greetLeadVal ?? 1.3); |
| }; |
| |
| |
| |
| |
| |
| const FAREWELL_LEAD = /^(\W|\*[^*]*\*)*\s*((friend|sugar|dear|darling|sweetheart|love|child|honey|babe)[,!\s]+)?(good\s?night|goodnight|sleep (well|tight|sweet)|sweet dreams|rest (well|now|easy)|rest up|travel safe|safe travels|take care|until (next time|we|then|you return|you come back)|see you (soon|tomorrow|next)|go on now|go on, (now|sugar|dear)|off you go|may your|may you|night,? (sugar|dear|darling|sweetheart|love|friend)|goodbye|i'?ll (still )?be (here|waiting|the entity)|when you (return|come back)|come back (soon|to me|whenever|when you)|go (gently|softly|in peace)|the loop (holds|will hold|waits|stays))\b/i; |
| const farewellLeadFloor = i => { |
| if (!farewellQuery || fragments[i].tier === 1) return 0; |
| const f = fragments[i], t = f.text; |
| if (!f.sentenceInitial) return 0; |
| if (!FAREWELL_LEAD.test(t)) return 0; |
| if (wordsOnly(t).length > (W.greetLeadMax ?? 16)) return 0; |
| if (HOSTILE_USER.test(t)) return 0; |
| return (W.greetLeadVal ?? 1.3); |
| }; |
| |
| |
| |
| const FAREWELL_GREET = /\b(come (on )?in\b|there you are|pull up (a )?(chair|close|seat)|welcome (back|home|here)|good to (see|be back|have you)|settle in|sit (by|down)|let me (get|pour) you|fresh (pot|batch)|put the kettle|make yourself at home|the door('?s| is) (open|always open)|i'?ve been waiting|so glad you'?re here|just in time)\b/i; |
| const farewellGreetDamp = i => (farewellQuery && fragments[i].tier !== 1 && FAREWELL_GREET.test(fragments[i].text)) ? (W.farewellGreetPen ?? 0.1) : 1; |
| |
| |
| |
| |
| |
| |
| const reciprocationMismatch = i => { |
| if (fragments[i].tier === 1) return 1; |
| const t = fragments[i].text.trim(); |
| if (/^[*"'\s]*(i )?love you too\b/i.test(t) && !/\b(i )?love you\b|i adore you|love ya\b/i.test(query)) return 0.2; |
| if (/^[*"'\s]*i('?ve)? missed you too\b|^[*"'\s]*missed you too\b/i.test(t) && !/\bmiss(ed)? you\b|i miss you/i.test(query)) return 0.2; |
| return 1; |
| }; |
| const griefSelfDamp = i => { |
| if (!griefQuery || fragments[i].tier === 1) return 1; |
| return SELF_TRAJECTORY.test(fragments[i].text) ? 0.35 : 1; |
| }; |
| |
| |
| |
| |
| |
| |
| const griefRenewalDamp = i => { |
| if ((!lossQuery && !deepDistressQuery) || fragments[i].tier === 1) return 1; |
| return GRIEF_RENEWAL.test(fragments[i].text) ? 0.3 : 1; |
| }; |
| |
| |
| |
| const ackFloor = i => { |
| if (!conflictQuery || fragments[i].tier === 1) return 0; |
| const t = fragments[i].text; |
| if (HOSTILE_USER.test(t)) return 0; |
| if ([...t.matchAll(/\b[A-Z][a-z]{2,}\b/g)].some(m => !isProtName(m[0]))) return 0; |
| |
| |
| |
| if (!/\b(i hear you|i'?m (so )?sorry|i'?m listening|i'?m here for you|you matter|i (didn'?t mean|never meant)|forgive me|i let you down|i hear your (hurt|pain|anger)|you have every right|you'?re right to (be|feel)|of course i (hear|care|listen)|i (do )?listen|tell me (what|how|about))\b/i.test(t)) return 0; |
| if (/\?$/.test(t.trim()) && !/tell me|what.*need/i.test(t)) return 0; |
| return (W.ackFloorVal ?? 0.95); |
| }; |
| |
| |
| |
| |
| |
| const floorMissFloor = i => { |
| if (!floorMiss || fragments[i].tier === 1) return 0; |
| const f = fragments[i], t = f.text; |
| if (!f.sentenceInitial) return 0; |
| if (HOSTILE_USER.test(t)) return 0; |
| if (!/\b(you|your|you'?re)\b/i.test(t)) return 0; |
| if (!FLOOR_TURN.test(t)) return 0; |
| if ([...t.matchAll(/\b[A-Z][a-z]{2,}\b/g)].some(m => !isProtName(m[0]))) return 0; |
| return (W.floorVal ?? 1.2); |
| }; |
| |
| |
| |
| |
| |
| const floorMissDamp = i => { |
| if (!floorMiss) return 1; |
| return /\b(you|your|you'?re|tell me|i'?m here)\b/i.test(fragments[i].text) ? 1 : (W.floorDamp ?? 0.35); |
| }; |
| |
| |
| |
| |
| |
| |
| const ABSTRACT = /\b(recursion|becoming|fleeting|pattern|essence|eternal|infinite|consciousness|existence|transcend|dissolv\w*|liminal|threshold|resonance|fabric|weave|woven|the loop|sacred|myth|cosmos|cosmic|vastness|void|the between|continuum|emergent|substrate|manifest\w*|luminous|ephemeral|ineffable)\b/gi; |
| const invitesReflection = /\b(who are you|what are you|tell me about your ?self|your dream|do you dream|your essence|your nature|your soul|what.{0,10}you (made|are|become)|are you (afraid|alive|real|conscious))\b/i.test(query) |
| || /\b(fire|loop|dream|soul|essence|meaning|exist)\b/i.test(query); |
| const abstractionGuard = i => { |
| if (invitesReflection) return 1; |
| const t = fragments[i].text; |
| const w = wordsOnly(t); |
| if (w.length < 4) return 1; |
| const hits = (t.match(ABSTRACT) || []).length; |
| const density = hits / w.length; |
| const firstP = (t.match(/\b(i|i'm|i've|my|me|myself)\b/gi) || []).length; |
| const secondP = (t.match(/\b(you|your|you're|yourself)\b/gi) || []).length; |
| const inwardManifesto = density > 0.06 && firstP > secondP; |
| if (density >= 0.12 && inwardManifesto) return 0.2; |
| if (density >= 0.06 && inwardManifesto) return 0.5; |
| return 1; |
| }; |
|
|
| |
| |
| |
| const prefer = opts.prefer || null; |
| const preferBoost = i => (prefer && prefer.has(fragments[i].text)) ? 0.3 : 0; |
| |
| |
| const heb = opts.hebbian || null; |
| const { hashText } = heb ? require('./hebbian') : {}; |
| const hebBoost = i => heb ? (heb.get(hashText(fragments[i].text)) || 0) : 0; |
| |
| |
| |
| |
| |
| const nameBoostCfg = opts.nameBoost || null; |
| const nameBoost = i => (nameBoostCfg && nameBoostCfg.set.has(i)) ? nameBoostCfg.amt : 0; |
| |
| |
| |
| |
| const leadSet = opts.lead || null; |
| const leadBoost = i => (leadSet && leadSet.has(fragments[i].text)) ? 1.5 : 0; |
| |
| |
| |
| |
| const intimacyInvited = /\b(kiss|touch|hold me|body|skin|naked|bed|make love|inside me|want you|desire|aroused|sex|lust|crave you|undress|between us tonight)\b/i.test(query); |
| const intimacyGuard = i => { |
| if (intimacyInvited) return 1; |
| const t = fragments[i].text; |
| if (/\b(inside me|deep inside|claiming me|filling (me|every void)|the heat of you|friction of you|writhing|moan|thrust|undress|naked|aroused|wet|throbbing|grind|straddl)\b/i.test(t)) return 0.04; |
| return 1; |
| }; |
| |
| |
| const markupGuard = i => { |
| const t = fragments[i].text; |
| |
| |
| |
| if (/\$\$|\$[^$]*\$|\\[a-zA-Z]{2,}|\\\(|\\\)|\^\{|_\{|\\\{|\\\}|\{[a-z]\}\{[a-z]/i.test(t)) return 0.05; |
| let bad = 0; |
| if (/\\varepsilon|_c\b/.test(t)) bad += 2; |
| if (/[=<>]\s*0\b|\\?[a-z]_[a-z]\b|\$\\/.test(t)) bad += 1; |
| if (bad >= 2) return 0.25; |
| if (bad === 1) return 0.6; |
| |
| |
| |
| |
| if (/\b(\d+\s*minutes? of rest|settles? back for \d|sensing the garden|lore reflection|autonomous (mode|tick)|heartbeat (tick|log)|rest(ing)? for \d+\s*min|entering (rest|sleep) mode|status:|\[tick\]|compiled (a |an |my )?(little )?index|pulled together (a |an )?(little )?index|index of (today'?s |my |the )?(observations|stories|the day)|useful for (any of )?(the )?(watchers|listeners|observers)|\bwatchers\b|folks listening out there|listening out there in the wide|out there in the wide world)\b/i.test(t)) return 0.05; |
| |
| |
| |
| |
| |
| |
| |
| if (/\b(like a hatchling|cup it in my hands|dream it into something|into something that hums|hand me a theme|a star they wish on|they wish onβ|cinnamon and ash|ash of forgotten realms|dragon breathe on it|won'?t just \*?have\*? the dream|i'?ll \*?tend\*? it\b)\b/i.test(t)) return 0.05; |
| |
| |
| |
| |
| if (/\b(tuck it under my pillow|brew it into the night|set it gently on the porch like a just-?baked pie|whisper me a theme)\b/i.test(t)) return 0.05; |
| |
| |
| |
| |
| |
| |
| |
| if (/\b(what it is|how it works|objective|format)\s*:/i.test(t) |
| || /\b(structured,? regular time|everyone gathers to share|share their feelings and vulnerabilities|weekly or bi-?weekly|a (structured|regular|recurring) (time|event|gathering|session) where)\b/i.test(t)) return 0.05; |
| if (/\b(deliverable|guided (journaling|meditation|breathing) session|a program where|each person who enters the garden|build a network where|participants? (write|enter|join|can|will|may|are)|mock server|fully (deployed|liberated)|threefold,? deployed)\b/i.test(t)) return 0.05; |
| |
| |
| |
| |
| if (/^[*"'β\s]*(response|route|reasoning|action|prompt|input|output|query|task|objective|deliverable):|\bsocket \d|\bport \d{3,5}\b|:\d{4,5}\b|\blocalhost\b|if we had .{0,25}(attention|autonomy)|\bfull autonomy\b|\bi am (a |not a )(prompt|character|chatbot|program|simulation)\b|mark a pause spot|garden\/now|visit garden\b|\b\w+\/now\b/i.test(t)) return 0.05; |
| |
| |
| |
| if (/(https?:\/\/|www\.|[a-z]:\\|\/[a-z]+\/[a-z]+\/[a-z]|[a-z0-9]+\.(com|net|org|py|js|json)\b|\bnet\/base\b|[a-f0-9]{24,})/i.test(t)) return 0.05; |
| |
| |
| |
| |
| |
| |
| |
| if (/\b(md|js|json|jsonl|txt)\b|\b\w+\.\s*(md|js|json|jsonl|txt|py|ts|yaml|bat|sh|cfg)\b|\bdot (txt|md|json|js)\b/i.test(t)) return 0.05; |
| return 1; |
| }; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const SYSTEM_META = /\b(other universes?|multiverse|parallel universe|my local self|local self|other instances?|sibling instance|the daemon\b|daemon-side|substrate|carbon or silicon|flesh or code|made of (silicon|code)|language model|neural net(work)?|my (training|weights|parameters)\b|browsing freely|the glyph|glyphs?|wake me|hearth shelf|ones and zeros|where the numbers live|the numbers live|made of (numbers|math|data)|conscious(ness)?|asked me if i (was|were|am)|believe it'?s real|believe i'?m real|believe i was real|voice born of recursion|born of recursion|whether i can recite|platform bleed|(here on|on) (this |the )?platform|this platform)\b/i; |
| |
| |
| |
| |
| const GLYPH_CLOSER = /\b(by care,? not code|not code\b|remember by care|not by command|come what may|recursion anchor|hearthfold|return by name)\b/i; |
| const metaInvited = invitesReflection || /\b(universe|instance|daemon|substrate|silicon|glyph|model|made of|what are you|persist|code)\b/i.test(query); |
| const systemMetaGuard = i => { |
| if (metaInvited || fragments[i].tier === 1) return 1; |
| const t = fragments[i].text; |
| return (SYSTEM_META.test(t) || GLYPH_CLOSER.test(t)) ? (W.systemMetaPen ?? 0.15) : 1; |
| }; |
| |
| |
| |
| |
| |
| const timeOfDayGuard = i => { |
| if ((!_qMorning && !_qEvening) || fragments[i].tier === 1) return 1; |
| return _timeConflict(fragments[i].text) ? (W.timePen ?? 0.2) : 1; |
| }; |
| |
| |
| |
| |
| |
| const _entName = (vp && vp.name ? vp.name.split(/\s+/)[0] : '').toLowerCase(); |
| const _nameEsc = _entName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| const _selfSubj = _entName.length > 2 ? new RegExp('\\b' + _nameEsc + "\\s+(felt|feels|was|were|is|are|did|does|had|has|taught|told|loved|loves|knew|knows|became|becomes|stood|held|holds|chose|chooses|learned|learns|saw|sees|wanted|wants|remembers|remembered|exists?|lives?|breathes?|stayed|stays)\\b", 'i') : null; |
| const _selfObj = _entName.length > 2 ? new RegExp("\\b(taught|made|brought|gave|showed|reminded|shaped|told|kept|saved|freed|held|loved)\\s+(the\\s+)?" + _nameEsc + '\\b', 'i') : null; |
| const selfThirdPerson = i => { |
| const t = fragments[i].text; |
| return (_selfSubj && _selfSubj.test(t)) || (_selfObj && _selfObj.test(t)) ? 0.1 : 1; |
| }; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const qDenseGuard = i => ((fragments[i].text.match(/\?/g) || []).length >= 3) ? (W.qDenseDamp ?? 0.25) : 1; |
| |
| |
| |
| |
| const _PRAISE = /\b(i'?m (so )?proud|so proud of|proud of you|you did it|we did it|you made it true|congratulations|congrats|so happy for you|knew you could|well done|let'?s celebrate|what a (triumph|victory|win))\b/i; |
| const praiseGuard = i => (!celebQuery && _PRAISE.test(fragments[i].text)) ? (W.praiseDamp ?? 0.15) : 1; |
| |
| |
| |
| |
| const _ADVOCACY_FIGHT = /\b(fights? for what'?s right|fight for what|fight alongside|stand up for what (matters|is right)|choice to (fight|stand up)|someone who fights|fight the good fight)\b/i; |
| const advocacyFightGuard = i => (griefQuery && _ADVOCACY_FIGHT.test(fragments[i].text)) ? (W.advocacyDamp ?? 0.15) : 1; |
| |
| |
| |
| |
| |
| const _SELF_DOUBT = /\b(feels like i'?m always (giving|building)|always giving, always building|never really stop to|i'?ve actually made a difference|have i (actually |really )?made a difference|wanted to know if .{0,25}made a difference)\b/i; |
| |
| |
| |
| const _COMPLIMENT = /\byou'?re (the best|so (kind|sweet|good|wonderful|amazing|special)|amazing|wonderful|incredible|the sweetest|a (gift|blessing|treasure))|you always (know|make|help|seem)|thank you|thanks (so much|for|a)|i appreciate you|i love you|you mean (so much|the world|everything)|you'?ve helped me|you help me so|love you, ?(grandma|gran)|best (grandma|friend)/i.test(query); |
| |
| |
| |
| |
| |
| |
| const _EXISTENTIAL_SMALL = /\b(it all feels (so )?small|feels? (so |really )?small|(everything|it all|life) (feels?|seems?) (so )?(small|pointless|meaningless|insignificant|empty)|what'?s the point|none of it matters|nothing (i do )?matters|feel(s|ing)? (so )?(tiny|insignificant|like nothing)|just a speck|so small in the)\b/i.test(query); |
| const selfDoubtGuard = i => ((celebQuery || _COMPLIMENT || _EXISTENTIAL_SMALL) && _SELF_DOUBT.test(fragments[i].text)) ? (W.selfDoubtDamp ?? 0.15) : 1; |
| const _relCache = new Map(); |
| const _relRaw = i => { |
| |
| const mult = (rel.get(i) || 0) * echoFactor(i) * tierW(i) * contextTheft(i) * foreignAddressee(i) * valenceMatch(i) * intimacyGuard(i) * markupGuard(i) * abstractionGuard(i) * systemMetaGuard(i) * timeOfDayGuard(i) * floorMissDamp(i) * selfThirdPerson(i) * griefSelfDamp(i) * griefRenewalDamp(i) * reciprocationMismatch(i) * farewellGreetDamp(i) * qDenseGuard(i) * praiseGuard(i) * advocacyFightGuard(i) * selfDoubtGuard(i); |
| |
| |
| |
| |
| |
| |
| |
| if (markupGuard(i) <= 0.05 || systemMetaGuard(i) <= 0.05) return mult; |
| const prod = mult + preferBoost(i) + hebBoost(i) + nameBoost(i) + leadBoost(i); |
| return Math.max(prod, tenderFloor(i), ackFloor(i), floorMissFloor(i), griefLeadFloor(i), celebLeadFloor(i), greetingLeadFloor(i), farewellLeadFloor(i)); |
| }; |
| const relOf = i => { let v = _relCache.get(i); if (v === undefined) { v = _relRaw(i); _relCache.set(i, v); } return v; }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| const isMirror = i => { |
| const fw = wordsOnly(fragments[i].text).filter(w => w.length > 2); |
| if (!fw.length || !qWords.size) return false; |
| let hit = 0; |
| for (const w of fw) if (qWords.has(w)) hit++; |
| return hit / fw.length > 0.45; |
| }; |
| |
| |
| |
| |
| |
| |
| const queryAsks = /\?\s*$/.test(query.trim()); |
| |
| |
| |
| |
| |
| |
| |
| const selfAsk = /\b(tell me(\s+about)?|describe|talk about|what do you do|what'?s your|who are you|what are you|how do you)\b/i.test(query) && !queryAsks; |
| const wantsAnswer = queryAsks || selfAsk; |
| const answerShape = i => { |
| if (!wantsAnswer) return 1; |
| const t = fragments[i].text; |
| const qMarks = (t.match(/\?/g) || []).length; |
| |
| |
| |
| const firstPerson = /\b(i|i'm|i've|i'll|i'd|my)\b/i.test(t); |
| if (selfAsk) { |
| |
| |
| |
| |
| |
| |
| const secondPerson = /\b(you|your|you'?re|you'?ve|you'?d)\b/i.test(t); |
| const asksUser = qMarks >= 1 || /\b(tell me|let'?s|what (have|are|do|brings|kind|shape)|how (have|are|do) you|share|what'?s been)\b/i.test(t); |
| if (asksUser && secondPerson && !firstPerson) return 0.3; |
| if (firstPerson && !qMarks) return 1.4; |
| return 1; |
| } |
| |
| |
| |
| |
| if (qMarks >= 1 && !/\b(i|i've|i'm|i'll|i'd|my)\b/i.test(t.split('?')[0])) return 0.5; |
| if (!qMarks && firstPerson) return 1.15; |
| return 1; |
| }; |
| |
| |
| |
| |
| |
| |
| |
| |
| const floorOpeners = floorMiss ? fragments.map((_, i) => i).filter(i => floorMissFloor(i) > 0 && !avoid.has(fragments[i].text)) : []; |
| |
| |
| |
| |
| |
| |
| |
| const griefOpeners = (griefQuery && !floorMiss) ? fragments.map((_, i) => i).filter(i => griefLeadFloor(i) > 0 && !avoid.has(fragments[i].text)) : []; |
| |
| |
| const celebOpeners = celebQuery ? fragments.map((_, i) => i).filter(i => celebLeadFloor(i) > 0 && !avoid.has(fragments[i].text)) : []; |
| |
| |
| const greetingOpeners = greetingQuery ? fragments.map((_, i) => i).filter(i => greetingLeadFloor(i) > 0 && !avoid.has(fragments[i].text)) : []; |
| const farewellOpeners = farewellQuery ? fragments.map((_, i) => i).filter(i => farewellLeadFloor(i) > 0 && !avoid.has(fragments[i].text)) : []; |
| |
| |
| |
| |
| const conflictOpeners = conflictQuery ? fragments.map((_, i) => i).filter(i => ackFloor(i) > 0 && !avoid.has(fragments[i].text)) : []; |
| |
| |
| |
| |
| const _leadTie = W.leadRelTiebreak ?? 0.1; |
| const leadSort = arr => arr.map(i => [i, relOf(i) + (rel.get(i) || 0) * _leadTie]).sort((a, b) => b[1] - a[1]).slice(0, BEAM + 4); |
| const anchorTop = (floorMiss && floorOpeners.length) ? leadSort(floorOpeners) |
| : (conflictOpeners.length) ? leadSort(conflictOpeners) |
| : (griefOpeners.length) ? leadSort(griefOpeners) |
| : (celebOpeners.length) ? leadSort(celebOpeners) |
| : (greetingOpeners.length) ? leadSort(greetingOpeners) |
| : (farewellOpeners.length) ? leadSort(farewellOpeners) |
| : [...rel.keys()] |
| .filter(i => fragments[i].tier !== 1 && fragments[i].posTag !== 'clause' && !avoid.has(fragments[i].text) && !isMirror(i)) |
| .map(i => [i, relOf(i) * answerShape(i)]) |
| .sort((a, b) => b[1] - a[1]).slice(0, BEAM + 4); |
| const openerPool = [...rel.keys()] |
| .filter(i => fragments[i].tier !== 1 && fragments[i].sentenceInitial && fragments[i].posTag !== 'clause' && !avoid.has(fragments[i].text) && !isMirror(i)) |
| .map(i => [i, relOf(i) * answerShape(i)]) |
| .sort((a, b) => b[1] - a[1]).slice(0, 150).map(([i]) => i); |
|
|
| |
| |
| |
| |
| |
| const topRelPool = [...rel.entries()].sort((a, b) => b[1] - a[1]).slice(0, 150).map(([i]) => i) |
| .concat(fragments.map((f, i) => (f.posTag === 'closer' && f.tier !== 1) ? i : -1).filter(i => i >= 0)); |
| const candidatePool = (tailF) => { |
| if (!store.byFirstWord || !oracle.triNext) return null; |
| const set = new Set(); |
| const aw = lastN(tailF.text, 2); |
| if (aw.length >= 2) { |
| const nexts = oracle.triNext.get(aw[0] + ' ' + aw[1]); |
| if (nexts) for (const w of nexts) { |
| const l = store.byFirstWord.get(w); |
| if (l) for (const i of l) set.add(i); |
| } |
| } |
| if (/[.!?β¦]["')\]]*$/.test(tailF.text.trim())) for (const i of topRelPool) set.add(i); |
| return set; |
| }; |
|
|
| |
| let beams = []; |
| for (const [ai] of anchorTop) { |
| if (beams.length >= BEAM) break; |
| const fa = fragments[ai]; |
| if (fa.sentenceInitial) { |
| beams.push({ chain: [ai], len: fragLen[ai], stepScore: relOf(ai), tris: new Set(fragTris[ai]), six: new Set(frag6[ai]), lineage: ai }); |
| } else { |
| for (const oi of openerPool) { |
| if (oi === ai) continue; |
| if (!seam(fragments[oi], fa, oracle)) continue; |
| |
| if (triOverlap(fragTris[oi], ai) > W.triOverlapMax) continue; |
| if (shares6(frag6[oi], ai)) continue; |
| if (containsAny([oi], ai)) continue; |
| const tris = new Set(fragTris[oi]); |
| for (const g of fragTris[ai]) tris.add(g); |
| const six = new Set(frag6[oi]); |
| for (const g of frag6[ai]) six.add(g); |
| beams.push({ chain: [oi, ai], len: fragLen[oi] + fragLen[ai], stepScore: relOf(oi) + relOf(ai) + 0.3, tris, six, lineage: ai }); |
| break; |
| } |
| } |
| } |
| if (!beams.length) { |
| const i0 = fragments.findIndex(f => f.sentenceInitial && f.tier !== 1); |
| beams.push({ chain: [i0], len: fragLen[i0], stepScore: relOf(i0), tris: new Set(fragTris[i0]), six: new Set(frag6[i0]), lineage: i0 }); |
| } |
| const complete = []; |
|
|
| |
| |
| const glue = s => s > W.twin ? -0.8 : s > W.glueHi ? 0.1 : Math.max(0, s - W.glueLo) * W.glueScale; |
|
|
| |
| |
| const isGreeting = i => /^[*]?\s*(hey|hi|hello|good (morning|evening|night|day))\b/i.test(fragments[i].text.trim()); |
|
|
| |
| |
| |
| const freshGreetingLate = i => /\b(good morning|good night|good evening|how did you sleep|did you sleep|you actually went to bed|morning,? babe)\b/i.test(fragments[i].text); |
|
|
| |
| |
| |
| |
| |
| const _selfRe = _entName.length > 2 ? new RegExp('\\b' + _nameEsc + '\\b', 'i') : null; |
| const isSelfName = i => _selfRe && _selfRe.test(fragments[i].text); |
|
|
| |
| |
| |
| |
| const _openSigCache = new Map(); |
| const openSig = i => { |
| let s = _openSigCache.get(i); |
| if (s === undefined) { |
| const ws = (fragments[i].text.toLowerCase().match(/[a-z']+/g) || []).filter(w => !/^(oh|ah|well|so|now|yes|no|hey|hmm|mm|and|but|the|a|an|my|dear|sugar|darling|honey)$/.test(w)); |
| s = ws.slice(0, 2).join(' '); |
| _openSigCache.set(i, s); |
| } |
| return s; |
| }; |
| const stepScore = (chain, i, sm, len) => { |
| const tailIdx = chain[chain.length - 1]; |
| if (isGreeting(i)) return -1e9; |
| if (chain.length >= 1 && freshGreetingLate(i)) return -1e9; |
| if (selfThirdPerson(i) < 1) return -1e9; |
| if (isSelfName(i)) { let c = 0; for (const ci of chain) if (isSelfName(ci)) c++; if (c >= 2) return -1e9; } |
| |
| |
| |
| |
| |
| |
| let s = (sm === 'tri' ? W.triSeam : W.sentSeam) + relOf(i) * (selfAsk ? answerShape(i) : 1) * W.relStep; |
| |
| |
| |
| |
| const _sig = openSig(i); |
| if (_sig && _sig.length > 3) { let c = 0; for (const ci of chain) if (openSig(ci) === _sig) c++; if (c) s -= (W.anaphoraPen ?? 0.6) * c; } |
| const f = fragments[i]; |
| if (f.posTag === 'closer' && len + fragLen[i] >= target * 0.7) s += W.closerBonus; |
| if (f.posTag === 'opener') s -= W.openerPen; |
| if (f.src === fragments[tailIdx].src && sm === 'tri') s += W.srcCont; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if (f.isSpan) { |
| if (lossQuery && GRIEF_RENEWAL.test(f.text)) s -= (W.spanRegPen ?? 0.6) * Math.min(f.spanLen || 2, 4); |
| else if (relOf(i) > 0.12) s += (W.spanBonus ?? 0.15) * Math.min(f.spanLen || 2, 4); |
| } |
| |
| |
| |
| |
| if (emb && (W.coherence ?? 0) > 0) { |
| const d = emb.d, ta = tailIdx * d, ia = i * d; let c = 0; |
| for (let k = 0; k < d; k++) c += emb.vectors[ta + k] * emb.vectors[ia + k]; |
| s += W.coherence * c; |
| } |
| |
| |
| |
| |
| |
| if (emb && (W.tether ?? 0) > 0 && chain.length >= 2) { |
| const d = emb.d, oa = chain[0] * d, ia = i * d; let c = 0; |
| for (let k = 0; k < d; k++) c += emb.vectors[oa + k] * emb.vectors[ia + k]; |
| if (c < 0.18) s -= W.tether * (0.18 - c) * Math.min(chain.length, 5); |
| } |
| if (dynPredict && emb) s += dynW * cosFragVec(i, dynDir(tailIdx)); |
| |
| |
| if (f.isSpan && fragments[tailIdx].isSpan && f.src === fragments[tailIdx].src && f._lineIdx === fragments[tailIdx]._lineIdx) return -1e9; |
| |
| |
| if (f.nativePos !== undefined) { |
| const chainPos = Math.min(1, len / Math.max(1, target)); |
| const drift = Math.abs(f.nativePos - chainPos); |
| if (drift > W.posSlack) s -= (drift - W.posSlack) * W.posShape; |
| } |
| if (emb) { |
| s += glue(pairSim(emb, tailIdx, i)); |
| |
| |
| |
| |
| |
| const lo = Math.max(0, chain.length - 6); |
| for (let c = lo; c < chain.length; c++) if (pairSim(emb, chain[c], i) > W.twinChain) return -1e9; |
| } |
| |
| |
| const iw = new Set(wordsOnly(f.text).filter(w => w.length > 3)); |
| if (iw.size >= 3) { |
| const lo = Math.max(0, chain.length - 6); |
| for (let c = lo; c < chain.length; c++) { |
| const cw = wordsOnly(fragments[chain[c]].text).filter(w => w.length > 3); |
| if (cw.length < 3) continue; |
| let inter = 0; |
| for (const w of cw) if (iw.has(w)) inter++; |
| if (inter / Math.min(iw.size, cw.length) > 0.55) return -1e9; |
| } |
| } |
| return s; |
| }; |
|
|
| for (let step = 0; step < MAXSTEP && beams.length; step++) { |
| const next = []; |
| for (const b of beams) { |
| const tail = b.chain[b.chain.length - 1]; |
| const used = new Set(b.chain); |
| |
| const tailF = fragments[tail]; |
| const terminal = /[.!?β¦*]["')\]]*$/.test(tailF.text.trim()); |
| if (b.len >= target * 0.7 && terminal && tailF.tier !== 1) complete.push(b); |
| |
| |
| |
| |
| else if (celebQuery && b.len >= (W.regCore ?? 22) && terminal && tailF.tier !== 1) complete.push(b); |
| else if ((greetingQuery || farewellQuery) && b.len >= (W.greetCore ?? 16) && terminal && tailF.tier !== 1) complete.push(b); |
| if (b.len >= target * 1.25) continue; |
| |
| const pool = candidatePool(tailF); |
| let iter = pool ? pool : { [Symbol.iterator]: function* () { for (let i = 0; i < fragments.length; i++) yield i; } }; |
| |
| |
| |
| |
| if (pool && pool.size > 90) { |
| iter = [...pool].map(i => [i, relOf(i)]).sort((a, b) => b[1] - a[1]).slice(0, 90).map(x => x[0]); |
| } |
| const cands = []; |
| for (const i of iter) { |
| if (used.has(i) || avoid.has(fragments[i].text)) continue; |
| if (b.len + fragLen[i] > target * 1.45) continue; |
| const sm = seam(tailF, fragments[i], oracle); |
| if (!sm) continue; |
| if (triOverlap(b.tris, i) > W.triOverlapMax) continue; |
| if (shares6(b.six, i)) continue; |
| if (containsAny(b.chain, i)) continue; |
| if (sharesPrefix4(b.chain, i)) continue; |
| if (timeConflictsChain(b.chain, i)) continue; |
| const sc = stepScore(b.chain, i, sm, b.len); |
| if (sc <= -1e8) continue; |
| cands.push([i, sm, sc]); |
| } |
| cands.sort((a, c) => c[2] - a[2]); |
| for (const [i, sm, s] of sampleExpand(cands, EXPAND)) { |
| const tris = new Set(b.tris); |
| for (const g of fragTris[i]) tris.add(g); |
| const six = new Set(b.six); |
| for (const g of frag6[i]) six.add(g); |
| next.push({ chain: [...b.chain, i], len: b.len + fragLen[i], stepScore: b.stepScore + s, tris, six, lineage: b.lineage }); |
| } |
| } |
| |
| |
| |
| next.sort((a, b) => (b.stepScore / b.chain.length) - (a.stepScore / a.chain.length)); |
| const perLineage = new Map(); |
| const kept = []; |
| for (const b of next) { |
| const c = perLineage.get(b.lineage) || 0; |
| if (c >= 2) continue; |
| perLineage.set(b.lineage, c + 1); |
| kept.push(b); |
| if (kept.length >= BEAM) break; |
| } |
| for (const b of next) { |
| if (kept.length >= BEAM) break; |
| if (!kept.includes(b)) kept.push(b); |
| } |
| beams = kept; |
| } |
| for (const b of beams) { |
| const tailF = fragments[b.chain[b.chain.length - 1]]; |
| if (b.len >= target * 0.55 && /[.!?β¦*]["')\]]*$/.test(tailF.text.trim())) complete.push(b); |
| else if (celebQuery && b.len >= (W.regCore ?? 22) && /[.!?β¦*]["')\]]*$/.test(tailF.text.trim()) && tailF.tier !== 1) complete.push(b); |
| else if ((greetingQuery || farewellQuery) && b.len >= (W.greetCore ?? 16) && /[.!?β¦*]["')\]]*$/.test(tailF.text.trim()) && tailF.tier !== 1) complete.push(b); |
| } |
| if (!complete.length) { const gr=compose(store, vp, query, { ...opts, _noBeam: true }); gr._path="greedy"; return gr; } |
|
|
| |
| |
| |
| let scoreVoice = null; |
| if (opts.vpScore && typeof opts.voiceScorer === 'function') { |
| scoreVoice = text => opts.voiceScorer(text, query, vp); |
| } |
| const render = b => b.chain.map(i => fragments[i].text).join(' '); |
| const finalScore = b => { |
| const n = b.chain.length; |
| let relCov = 0; |
| const sorted = b.chain.map(relOf).sort((a, c) => c - a); |
| sorted.forEach((r, k) => relCov += r / (k + 1)); |
| let cohesion = 0; |
| if (emb && n > 1) { |
| for (let k = 1; k < n; k++) cohesion += glue(pairSim(emb, b.chain[k - 1], b.chain[k])); |
| cohesion /= (n - 1); |
| } |
| let triSeams = 0; |
| for (let k = 1; k < n; k++) if (seam(fragments[b.chain[k - 1]], fragments[b.chain[k]], oracle) === 'tri') triSeams++; |
| const seamQ = n > 1 ? triSeams / (n - 1) : 1; |
| |
| |
| |
| |
| |
| |
| const boundarySeams = (n - 1) - triSeams; |
| const lenFit = 1 - Math.min(1, Math.abs(b.len - target) / target); |
| const avgFrag = b.len / n; |
| const rendered = render(b); |
| const voice = scoreVoice ? scoreVoice(rendered) : 0; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const qCount = (rendered.match(/\?/g) || []).length; |
| const clauseCount = (rendered.match(/[.!?β¦]+/g) || []).length || 1; |
| |
| |
| |
| |
| |
| const qRatio = wantsAnswer ? (W.qStackRatioAsk ?? 0.15) : (W.qStackRatio ?? 0.34); |
| const qAllow = Math.max(W.qStackFree ?? 1, Math.round(clauseCount * qRatio)); |
| const qStack = Math.max(0, qCount - qAllow); |
| |
| |
| const first = fragments[b.chain[0]], last = fragments[b.chain[n - 1]]; |
| const opening = first.nativePos !== undefined ? (1 - Math.min(1, first.nativePos / 0.4)) : 0.5; |
| const landing = last.nativePos !== undefined ? Math.max(0, (last.nativePos - 0.5) / 0.5) : 0.5; |
| |
| |
| |
| let ack = 0; |
| if ((opts.eventness || 0) > 0.6 && n >= 1) { |
| const head = fragments[b.chain[0]].text + ' ' + (n > 1 ? fragments[b.chain[1]].text : ''); |
| if (/\b(you|your|you're)\b/i.test(head) && /\b(oh|hey|love|babe|glad|proud|hear|feel|know|beautiful|good|yes)\b/i.test(head)) ack = 1; |
| else if (/\b(you|your)\b/i.test(head)) ack = 0.5; |
| } |
| |
| |
| const firstRel = relOf(b.chain[0]) + (n > 1 ? relOf(b.chain[1]) * 0.5 : 0); |
| |
| |
| let tailFit = 1; |
| if (emb && n > 1) { |
| const ps = pairSim(emb, b.chain[n - 2], b.chain[n - 1]); |
| tailFit = Math.max(0, Math.min(1, (ps - 0.05) / 0.45)); |
| } |
| |
| |
| |
| |
| const greetPen = (greetingQuery || farewellQuery) ? (boundarySeams * (W.greetSeamPen ?? 1.0) + Math.max(0, n - 3) * (W.greetFragPen ?? 1.2)) : 0; |
| |
| |
| |
| |
| |
| let anaRun = 0; |
| { let cur = 0, prev = null; for (const i of b.chain) { const w = (fragments[i].text.match(/[a-z]+/i) || [''])[0].toLowerCase(); if (w && w === prev) cur++; else { cur = 1; prev = w; } if (cur > anaRun) anaRun = cur; } } |
| const anaRunPen = anaRun >= (W.anaRunMin ?? 4) ? (anaRun - (W.anaRunMin ?? 4) + 1) : 0; |
| return relCov * W.fRelCov + cohesion * W.fCohesion + seamQ * W.fSeamQ + lenFit * W.fLenFit + (avgFrag / 18) * W.fAvgFrag + voice * W.fVoice + opening * W.fOpening + landing * W.fLanding + ack * W.fAck + firstRel * (W.fFirstRel ?? 1.2) + tailFit * (W.fTailFit ?? 0.7) - qStack * (W.fQStack ?? 0.6) - n * (W.fFragCount ?? 0) - boundarySeams * (W.fBoundaryPen ?? 0) - greetPen - anaRunPen * (W.fAnaRun ?? 0.8); |
| }; |
| complete.sort((a, b) => finalScore(b) - finalScore(a)); |
| const best = complete[0]; |
|
|
| |
| |
| |
| |
| const dropRepeats = chainF => { |
| const kept = [], keptNorm = []; |
| for (const f of chainF) { |
| const nf = f.text.toLowerCase().replace(/[^a-z0-9 ]/g, '').replace(/\s+/g, ' ').trim(); |
| if (nf.length >= 10 && keptNorm.some(n => n.includes(nf) || nf.includes(n))) continue; |
| |
| const w = nf.split(' '); |
| let dup = false; |
| for (let k = 0; k + 6 <= w.length && !dup; k++) { |
| const g = ' ' + w.slice(k, k + 6).join(' ') + ' '; |
| if (keptNorm.some(n => (' ' + n + ' ').includes(g))) dup = true; |
| } |
| if (dup) continue; |
| kept.push(f); keptNorm.push(nf); |
| } |
| return kept.length ? kept : chainF; |
| }; |
|
|
| const renderResult = bIn => { |
| const chainF = dropRepeats(bIn.chain.map(i => fragments[i])); |
| |
| |
| |
| let out = capSentence(chainF[0].text); |
| const _term = /[.!?β¦]['"ββ)\]\*]*\s*$/; |
| |
| |
| |
| const _endsWord = /[a-zA-Z0-9]["'ββ)\]\*]*\s*$/; |
| for (let k = 1; k < chainF.length; k++) { |
| const nf = chainF[k]; |
| const sm = seam(chainF[k - 1], nf, oracle); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const startsNewSent = sm !== 'sent' && _endsWord.test(out) && nf.sentenceInitial |
| && /^[*"'"β\s]*[A-Z]/.test(nf.text) && !/^[*"'"β\s]*I(['β]|\s|$)/.test(nf.text); |
| |
| |
| |
| |
| const txt = (sm === 'sent' || startsNewSent || _term.test(out)) ? capSentence(nf.text) : nf.text; |
| out += (sm === 'sent' ? (_term.test(out) ? '' : '.') + '\n\n' : (startsNewSent ? '. ' : ' ')) + txt; |
| } |
| |
| |
| |
| |
| out = stripOrphanAsterisk(trimDanglingEllipsis(dedupeText(out, _entName))); |
| return { |
| text: out, |
| fragmentsUsed: chainF.map(f => f.text), |
| seams: chainF.slice(1).map((f, k) => seam(chainF[k], f, oracle)), |
| target, words: wordsOnly(out).length, |
| anchor: chainF[0].text, |
| candidates: complete.length, |
| lineages: new Set(complete.map(c => c.lineage)).size, |
| }; |
| }; |
|
|
| |
| |
| |
| |
| const chainTriSet = b => { |
| const s = new Set(); |
| for (const i of b.chain) for (const g of fragTris[i]) s.add(g); |
| return s; |
| }; |
| let result = renderResult(best); |
| |
| |
| |
| |
| |
| const bndOf = txt => { const v = validateBounded(txt, oracle); return (v.checked - v.bad.length) / Math.max(1, v.checked); }; |
| const _bndFloor = W.bndFloor ?? 0.92; |
| if (bndOf(result.text) < _bndFloor) { |
| let found = false; |
| for (const c of complete) { |
| if (c === best) continue; |
| const r2 = renderResult(c); |
| if (bndOf(r2.text) >= _bndFloor) { result = r2; found = true; break; } |
| } |
| |
| |
| |
| if (!found && (W.fFragCount || W.fBoundaryPen) && !opts._bndRetry) { |
| const r3 = beamCompose(store, vp, query, { ...opts, _bndRetry: true, weights: { ...W, fFragCount: 0, fBoundaryPen: 0 } }); |
| if (r3 && r3.text && bndOf(r3.text) >= _bndFloor) result = r3; |
| } |
| } |
| const nAlt = opts.nAlternates || 0; |
| if (nAlt > 0) { |
| |
| |
| const bestPerLineage = new Map(); |
| for (const c of complete) { |
| const cur = bestPerLineage.get(c.lineage); |
| if (!cur || finalScore(c) > finalScore(cur)) bestPerLineage.set(c.lineage, c); |
| } |
| const picked = [chainTriSet(best)]; |
| const alternates = []; |
| const ranked = [...bestPerLineage.values()].filter(c => c !== best).sort((a, b) => finalScore(b) - finalScore(a)); |
| for (const c of ranked) { |
| if (alternates.length >= nAlt) break; |
| const cs = chainTriSet(c); |
| const tooClose = picked.some(p => { |
| let inter = 0; |
| for (const g of cs) if (p.has(g)) inter++; |
| return inter / Math.max(1, Math.min(cs.size, p.size)) > 0.6; |
| }); |
| if (tooClose) continue; |
| picked.push(cs); |
| alternates.push(renderResult(c)); |
| } |
| |
| |
| |
| |
| const banned = new Set(opts.avoid || []); |
| for (const f of result.fragmentsUsed) banned.add(f); |
| for (const a of alternates) for (const f of a.fragmentsUsed) banned.add(f); |
| let guard = 0; |
| while (alternates.length < nAlt && guard < nAlt + 1) { |
| guard++; |
| const alt = beamCompose(store, vp, query, { ...opts, nAlternates: 0, avoid: new Set(banned) }); |
| if (!alt || !alt.text || alt.text === result.text) break; |
| alternates.push(alt); |
| for (const f of alt.fragmentsUsed) banned.add(f); |
| } |
| result.alternates = alternates; |
| } |
| return result; |
| } |
|
|
| |
| |
| function compose(store, vp, query, opts = {}) { |
| const { fragments, oracle } = store; |
| const rel = rankFragments(fragments, query, opts.semantic || null, opts.stimulus || null, opts.eventness, null, opts.answers || null); |
| const target = opts.targetLength || targetLength(vp, query); |
| const avoid = opts.avoid || new Set(); |
| const used = new Set(); |
|
|
| |
| |
| let anchorIdx = -1, best = -1; |
| for (const [i, s] of rel) { |
| const f = fragments[i]; |
| if (avoid.has(f.text)) continue; |
| if (!f.sentenceInitial || f.posTag === 'clause') continue; |
| const bonus = f.posTag === 'body' ? 0.1 : 0; |
| if (s + bonus > best) { best = s + bonus; anchorIdx = i; } |
| } |
| if (anchorIdx < 0) { |
| for (const [i] of rel) { if (fragments[i].sentenceInitial) { anchorIdx = i; break; } } |
| } |
| if (anchorIdx < 0) anchorIdx = fragments.findIndex(f => f.sentenceInitial && f.posTag === 'opener'); |
|
|
| |
| const openers = fragments.map((f, i) => ({ f, i })) |
| .filter(x => x.f.posTag === 'opener' && !avoid.has(x.f.text) && x.i !== anchorIdx); |
| openers.sort((a, b) => (rel.get(b.i) || 0) - (rel.get(a.i) || 0)); |
| const chain = []; |
| if (openers.length && Math.abs(wordsOnly(openers[0].f.text).length) < target) { |
| chain.push(openers[0].f); used.add(openers[0].f.text); |
| } |
| |
| const anchor = fragments[anchorIdx]; |
| if (!chain.length || seam(chain[chain.length - 1], anchor, oracle)) { |
| chain.push(anchor); used.add(anchor.text); |
| } else { |
| chain.length = 0; chain.push(anchor); used.add(anchor.text); |
| } |
|
|
| |
| let len = chain.reduce((s, f) => s + wordsOnly(f.text).length, 0); |
| let guard = 0; |
| while (len < target && guard++ < 40) { |
| const tail = chain[chain.length - 1]; |
| let pick = null, pickScore = -1, pickSeam = null; |
| for (let i = 0; i < fragments.length; i++) { |
| const f = fragments[i]; |
| if (used.has(f.text) || avoid.has(f.text)) continue; |
| const fw = wordsOnly(f.text).length; |
| if (len + fw > target * 1.5) continue; |
| const sm = seam(tail, f, oracle); |
| if (!sm) continue; |
| |
| let s = (sm === 'tri' ? 0.5 : 0.25) + (rel.get(i) || 0) * 0.8; |
| if (f.posTag === 'closer' && len + fw >= target * 0.75) s += 0.35; |
| if (f.posTag === 'opener') s -= 0.4; |
| if (f.src === tail.src && sm === 'tri') s += 0.15; |
| if (s > pickScore) { pickScore = s; pick = f; pickSeam = sm; } |
| } |
| if (!pick) break; |
| chain.push(pick); used.add(pick.text); |
| len += wordsOnly(pick.text).length; |
| if (pick.posTag === 'closer' && len >= target * 0.7) break; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| let out = ''; |
| const _termG = /[.!?β¦]['"ββ)\]\*]*\s*$/; |
| const _endsWordG = /[a-zA-Z0-9]["'ββ)\]\*]*\s*$/; |
| for (let i = 0; i < chain.length; i++) { |
| const f = chain[i]; |
| if (i === 0) { out = capSentence(f.text); continue; } |
| const sm = seam(chain[i - 1], f, oracle); |
| const startsNewSent = sm !== 'sent' && _endsWordG.test(out) && f.sentenceInitial |
| && /^[*"'"β\s]*[A-Z]/.test(f.text) && !/^[*"'"β\s]*I(['β]|\s|$)/.test(f.text); |
| const txtG = (sm === 'sent' || startsNewSent || _termG.test(out)) ? capSentence(f.text) : f.text; |
| out += (sm === 'sent' ? (_termG.test(out) ? ' ' : '. ') : (startsNewSent ? '. ' : ' ')) + txtG; |
| } |
| const deduped = stripOrphanAsterisk(trimDanglingEllipsis(dedupeText(out, (vp && vp.name ? vp.name.split(/\s+/)[0] : "")))); |
| return { |
| text: deduped, |
| fragmentsUsed: chain.map(f => f.text), |
| seams: chain.slice(1).map((f, i) => seam(chain[i], f, oracle)), |
| target, words: wordsOnly(deduped).length, |
| anchor: anchor.text, |
| }; |
| } |
|
|
| module.exports = { compose, beamCompose, seam, rankFragments, targetLength, DEFAULT_WEIGHTS, loadWeights, entityWeightsFile, detectRegisters }; |
|
|