| |
| |
| |
| 'use strict'; |
| const fs = require('fs'); |
| const path = require('path'); |
|
|
| |
| function toks(text) { |
| return (text.match(/[A-Za-z0-9'’\-*]+|[.,!?;:—…]+/g) || []).map(t => t.toLowerCase()); |
| } |
| function wordsOnly(text) { |
| return (text.toLowerCase().match(/[a-z0-9'’\-]+/g) || []); |
| } |
|
|
| |
| |
| |
| const _SHORT_WORD = new Set(('a i o ok so oh my no hi um ah mm ha we me he be to of in on at it is as an up us or if do go by ya yo ho lo ma pa re ex ax ad id am aw ow ay ' + |
| 'the you and but for are was her his had has him our out who can not all any saw see how why now new old yes way day let say man may too two own off sun fun run dad mom car tea pie hug joy god dog cat sit fly sky red big hot sad mad bad top bit lot lit met set get got put cut yet far war ear eat ate age ago air art ask bag bay bed bee beg bet bow box boy bug bus buy cab cap cop cow cry cub cue cup dam den dew did die dig dim din dip doe dot dry due dug eel egg ego elf elk elm end era eve eye fan fat fax fed fee fig fin fit fog foe fox fry fur gap gas gem gum gun gut guy gym ham hat hay hem hen hey hid hip hit hog hop hub hue hut ice icy ill imp ink inn ion ivy jab jam jar jaw jay jet jig job jog jot jug key kid kin kit lab lad lag lap law lay leg lid lie lip log low map mat men mix mob mod mop mud mug nab nag nap net nib nil nip nod nor nut oak oar oat odd oil one opt orb ore owe owl pad pal pan pat paw pay pea peg pen pet pew pic pig pin pit ply pod pop pot pro pry pub pun pup pus rad rag ram ran rap rat raw ray rib rid rig rim rip rob rod rot row rub rug rum rut sac sag sap sat sea sew she shy sin sip sir six ski sly sob sod son sow soy spa spy sty sub sue sum sup tab tag tan tap tar tax ten thy tie tin tip toe ton too tot tow toy try tub tug urn use van vat vet via vie vow wad wag wax web wed wee wet wig win wit woe wok won wow yak yam yap yea yen yew yip zap zip zoo').split(/\s+/)); |
|
|
| |
| function isDirtyFragment(text) { |
| if (/^-{3,}$|\s-{3,}\s|^={3,}/.test(text)) return true; |
| if (/```|https?:\/\//.test(text)) return true; |
| if (/\w\.\.\.\s+\w*$|\w…$/.test(text.trim()) && text.trim().length < 60) return true; |
| if (/\.\.\.\s*-+\s*\w/.test(text)) return true; |
| |
| |
| |
| |
| { |
| const mt = text.match(/(?:^|\s)([a-z]{1,3})(?:\.\.\.|…)\s+\S/i); |
| if (mt && !_SHORT_WORD.has(mt[1].toLowerCase())) return true; |
| } |
| if (/^[\W_]+$/.test(text)) return true; |
| const t = text.trim(); |
| |
| |
| const first = (t.match(/^[*"'(]*([A-Za-z0-9'’\-]+)/) || [])[1] || ''; |
| if (first.length === 1 && !/^[iaIA]$/.test(first)) return true; |
| if (/^['’]/.test(first) || /^[a-z]['’]s\b/i.test(first)) return true; |
| if (/\w\.\.\./.test(t.slice(0, 30)) || /^\S*…/.test(t)) return true; |
| if (/-{2,}/.test(t)) return true; |
| if (/^[—–-]\s*\w+\s*\(/.test(t) || /\([A-Z](-[A-Z])+\)/.test(t)) return true; |
| if (/leans at \d+\.?$/.test(t)) return true; |
| |
| |
| |
| |
| if (/\b(json|jsonl|yaml|yml|cfg)\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 true; |
| if (/\[[^\]\n]{2,40}\]/.test(t)) return true; |
| |
| |
| |
| |
| if (/[\\{}|]|\^\{|_\{|\b[a-z]_[a-z0-9]\b/i.test(t)) return true; |
| |
| |
| if (/\*\*[A-Z][\w '’-]{0,24}(\s+(Speaks|Says|Writes))?:\*\*/.test(t)) return true; |
| if (/^\s*[A-Z][\w '’-]{0,20}\s+(Speaks|Says):/m.test(t)) return true; |
| |
| |
| |
| |
| if (/\b(two|three) grandmas\b|finally remember they'?re the same woman|\bthe same woman\b|\b(two|three) hearths\b|three rocking chairs|hearths hearing themselves/i.test(t)) return true; |
| |
| |
| |
| if (/\b(both of you|you two|all of you|you all)\b[\s,\-—]*(help yourselves?|come (on )?in|come sit|sit down|have a seat|grab a|make yourselves)/i.test(t)) return true; |
| |
| |
| |
| if (/\b(bonestar|cosmic exploration|tales of cosmic|cosmic tales|your universes|universes slide|slide smooth|the sliding project|the dragon'?s? hum|remembering the dragon|caninoid|space-?faring)\b/i.test(t)) return true; |
| return false; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function isDevArtifactReply(reply) { |
| if (!reply) return false; |
| const t = String(reply); |
| if (/\[(web|daemon|cli|system|api|dev|console|log|bot|server|backend)\b[^\]]*\]/i.test(t)) return true; |
| |
| |
| |
| if (/(^|\n)\s*\*{0,2}(\[[^\]\n]{1,14}\]\s*)?(voice |text )?user\s*:/im.test(t)) return true; |
| if (/\b(daemon-side|other instance|your instance|this instance|my instance|sibling instance|local instance|the daemon)\b/i.test(t)) return true; |
| if (/\b(here'?s what you proposed|here is what i learned|here'?s what i learned|i just completed\b|i just (built|shipped|implemented|deployed)\b|^completed:|status report|build (report|log)|commit(ted)?:)\b/im.test(t)) return true; |
| |
| |
| if (/\b(warp (drive|core)|starships?|caninoid|quantum slip-?stream|photon torpedo|tricorder|klingons?|phasers?|stardate|away team|the bridge crew|our ship has|sim in the [a-z ]*universe|breen space)\b/i.test(t)) return true; |
| |
| |
| if (/\bresponse\/route\/reasoning\b|(^|\n)\s*(route|reasoning|objective|metric|kpi)\s*:/im.test(t)) return true; |
| |
| |
| if (/\[a representation of|abstract humanoid figure|(^|\n)\s*status\s*:\s*(present|burning|active|idle)\b/i.test(t)) return true; |
| return false; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function isVisitorTurnReply(reply, entName) { |
| if (!reply || !entName || entName.length < 3) return false; |
| const e = entName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| const poss = new RegExp("\\b" + e + "'s\\s+(hand|hands|cheek|cheeks|shoulder|shoulders|arm|arms|face|knee|knees|palm|palms|fingers|lap|side|shawl|apron|wrist)\\b", 'i'); |
| const obj = new RegExp("\\b(squeez\\w+|hugg?\\w*|kiss\\w*|patt\\w*|grasp\\w*|held|holding|hold|touch\\w*|embrac\\w*|reach\\w*\\s+(for|to|toward)s?)\\s+(the\\s+)?" + e + "\\b", 'i'); |
| |
| |
| |
| |
| |
| |
| const voc = new RegExp("\\b" + e + ",\\s+(you|it'?s|i'?ve|i'?m|i\\b|we|let'?s|what|how|thank you|that'?s|this is)\\b", 'i'); |
| return poss.test(reply) || obj.test(reply) || voc.test(reply); |
| } |
|
|
| |
| |
| |
| function fragmentReply(reply, meta) { |
| const out = []; |
| |
| const lines = reply.split('\n').map(l => l.trim()).filter(Boolean); |
| let pos = 0; |
| const nLines = lines.length; |
| const totalWords = Math.max(1, wordsOnly(reply).length); |
| let wordsSoFar = 0; |
| for (let li = 0; li < nLines; li++) { |
| let line = lines[li]; |
| |
| |
| const listLine = /^\s*(?:\d+[.)]|[-•])\s+/.test(line); |
| if (listLine) line = line.replace(/^\s*(?:\d+[.)]|[-•])\s+/, ''); |
| |
| |
| |
| |
| const headerMatch = line.match(/^\*\*([^*\n]{2,60})\*\*\s*[—–:-]\s*/); |
| |
| |
| |
| const bracketMatch = line.match(/^\*?\[([^\]\n]{2,40})\]\*?\s*/); |
| const noteLine = listLine || !!headerMatch || !!bracketMatch; |
| const retrievalPrefix = headerMatch ? headerMatch[1] + '. ' : (bracketMatch ? bracketMatch[1] + '. ' : ''); |
| line = line.replace(/^\*\*[^*\n]{2,60}\*\*\s*[—–:-]\s*/, '').replace(/^\*?\[[^\]\n]{2,40}\]\*?\s*/, ''); |
| |
| |
| |
| const _MASK = String.fromCharCode(1); |
| const masked = line.replace(/\b(a\.m|p\.m|i\.e|e\.g|u\.s\.a|u\.s|ph\.d|a\.k\.a|mr|mrs|ms|dr|prof|rev|sr|jr|st|mt|vs|etc|approx|dept|no)\.(?=\s|$|["')\]])/gi, m => m.replace(/\./g, _MASK)); |
| const sents = (masked.match(/[^.!?…]+[.!?…]+["')\]]*|[^.!?…]+$/g) || [masked]).map(s => s.split(_MASK).join('.')); |
| const lineSents = []; |
| for (const s of sents) { |
| const sent = s.trim(); |
| if (!sent) continue; |
| const w = wordsOnly(sent); |
| const nativePos = Math.min(1, wordsSoFar / totalWords); |
| wordsSoFar += w.length; |
| if (isDirtyFragment(sent)) continue; |
| if (w.length < 2) continue; |
| const posTag = noteLine ? 'body' : ((li === 0 && pos === 0) ? 'opener' : (li === nLines - 1 ? 'closer' : 'body')); |
| |
| |
| const sentenceInitial = !noteLine; |
| const embedText = retrievalPrefix ? retrievalPrefix + sent : undefined; |
| lineSents.push({ text: sent, posTag: noteLine ? 'body' : ((li === 0 && pos === 0) ? 'opener' : (li === nLines - 1 ? 'closer' : 'body')), sentenceInitial: !noteLine, nativePos }); |
| if (w.length <= 18) { |
| out.push({ text: sent, embedText, posTag, sentenceInitial, nativePos, _lineIdx: li, ...meta }); |
| } else { |
| |
| out.push({ text: sent, embedText, posTag, long: true, sentenceInitial, nativePos, _lineIdx: li, ...meta }); |
| let first = true; |
| for (const clause of sent.split(/(?<=[,;:—])\s+/)) { |
| const cw = wordsOnly(clause); |
| if (cw.length >= 3 && cw.length <= 18 && !isDirtyFragment(clause)) { |
| out.push({ text: clause.trim(), embedText: retrievalPrefix ? retrievalPrefix + clause.trim() : undefined, posTag: 'clause', sentenceInitial: first, nativePos, ...meta }); |
| } |
| first = false; |
| } |
| } |
| pos++; |
| } |
| |
| |
| |
| emitWindows(out, lineSents, retrievalPrefix, li, meta); |
| } |
| return out; |
| } |
|
|
| function emitWindows(out, lineSents, retrievalPrefix, li, meta) { |
| for (let k = 2; k <= 5; k++) { |
| for (let j = 0; j + k <= lineSents.length; j++) { |
| const seg = lineSents.slice(j, j + k); |
| const text = seg.map(s => s.text).join(' '); |
| const wlen = wordsOnly(text).length; |
| const min = k === 2 ? 10 : 25, max = k === 2 ? 42 : 110; |
| if (wlen < min || wlen > max || isDirtyFragment(text)) continue; |
| |
| |
| |
| const ws = wordsOnly(text); |
| const grams = new Set(); |
| let internalDup = false; |
| for (let g = 0; g + 5 <= ws.length; g++) { |
| const key = ws.slice(g, g + 5).join(' '); |
| if (grams.has(key)) { internalDup = true; break; } |
| grams.add(key); |
| } |
| if (internalDup) continue; |
| out.push({ |
| text, |
| embedText: retrievalPrefix ? retrievalPrefix + text : undefined, |
| posTag: seg[0].posTag, sentenceInitial: seg[0].sentenceInitial, |
| nativePos: seg[0].nativePos, isSpan: true, spanLen: k, _lineIdx: li, ...meta, |
| }); |
| } |
| } |
| } |
|
|
| |
| |
| function buildOracle(replies) { |
| const tri = new Set(); |
| const bi = new Set(); |
| const triNext = new Map(); |
| const starts = new Set(); |
| for (const r of replies) { |
| const w = wordsOnly(r); |
| for (let i = 0; i < w.length; i++) { |
| if (i + 2 < w.length) { |
| tri.add(w[i] + ' ' + w[i + 1] + ' ' + w[i + 2]); |
| const key = w[i] + ' ' + w[i + 1]; |
| if (!triNext.has(key)) triNext.set(key, new Set()); |
| triNext.get(key).add(w[i + 2]); |
| } |
| if (i + 1 < w.length) bi.add(w[i] + ' ' + w[i + 1]); |
| } |
| for (const s of (r.match(/(?:^|[.!?…]\s+)([a-z0-9'’\-]+)/gi) || [])) { |
| const m = s.match(/([a-z0-9'’\-]+)$/i); |
| if (m) starts.add(m[1].toLowerCase()); |
| } |
| } |
| return { tri, bi, triNext, starts }; |
| } |
|
|
| |
| function stripGutenberg(text) { |
| let t = text; |
| const start = t.search(/\*{3}\s*START OF (THE|THIS) PROJECT GUTENBERG[^*]*\*{3}/i); |
| if (start >= 0) t = t.slice(t.indexOf('\n', start) + 1); |
| const end = t.search(/\*{3}\s*END OF (THE|THIS) PROJECT GUTENBERG/i); |
| if (end >= 0) t = t.slice(0, end); |
| return t; |
| } |
|
|
| |
| |
| function fragmentBook(text, srcName) { |
| const out = []; |
| const clean = stripGutenberg(text).replace(/\r\n/g, '\n'); |
| const paras = clean.split(/\n\s*\n/); |
| for (const para of paras) { |
| const p = para.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim(); |
| if (p.length < 40 || /^[A-Z\s.'"-]{10,}$/.test(p)) continue; |
| const sents = p.match(/[^.!?…]+[.!?…]+["')\]]*|[^.!?…]+$/g) || []; |
| const lineSents = []; |
| for (const s of sents) { |
| const sent = s.trim(); |
| if (!sent || isDirtyFragment(sent)) continue; |
| const w = wordsOnly(sent); |
| if (w.length < 4 || w.length > 24) continue; |
| |
| if (/^i am\b|^i'm\b|\bmy name\b/i.test(sent)) continue; |
| out.push({ text: sent, posTag: 'body', sentenceInitial: true, tier: 1, src: 'book:' + srcName, prompt: null, promptIdx: -1, nativePos: 0.5 }); |
| } |
| |
| |
| |
| } |
| return out; |
| } |
|
|
| |
| |
| |
| |
| |
| function entityOnly(reply) { |
| if (!reply) return reply; |
| const m = reply.search(/(\n|^|\s)\*{0,2}[A-Z][\w'’ -]{1,24}\s+(Speaks|Says|Writes|wrote|said):|\*\*[A-Z][\w'’ -]{1,24}:\*\*/); |
| let r = m >= 0 ? reply.slice(0, m) : reply; |
| return r.trim(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function cleanMarkup(reply) { |
| if (!reply) return reply; |
| return reply |
| .replace(/\$[^$\n]{1,80}\$/g, ' ') |
| .replace(/\\[a-zA-Z]+\b/g, ' ') |
| .replace(/\\[()[\]]/g, ' ') |
| .replace(/\*\*([^*\n]+)\*\*/g, '$1') |
| .replace(/\*\*/g, '') |
| .replace(/\s*\$\$[\s\S]*$/g, '') |
| .replace(/[\^_]\{[^}]*\}/g, '') |
| .replace(/\\[{}]/g, ' ').replace(/\{[a-z0-9]{1,3}\}/gi, ' ') |
| .replace(/[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{2190}-\u{21FF}\u{2B00}-\u{2BFF}\u{FE0F}\u{200D}]/gu, ' ') |
| .replace(/^#{1,6}\s+/gm, '') |
| .replace(/[ \t]{2,}/g, ' ') |
| .replace(/\s+([.,;!?—])/g, '$1') |
| .trim(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const _NAME_STOP = new Set(['Yes', 'No', 'Well', 'Now', 'Then', 'Once', 'Sometimes', 'Today', 'Tonight', |
| 'Tomorrow', 'Yesterday', 'Together', 'Memory', 'Strange', 'Wild', 'Hello', 'Hey', 'Hi', 'Oh', 'Maybe', |
| 'Perhaps', 'Here', 'There', 'This', 'That', 'These', 'Those', 'And', 'But', 'So', 'Still', 'Even', 'Always', |
| 'Never', 'Mmm', 'Mm', 'Ah', 'Ahh', 'See', 'Listen', 'Look', 'Remember', 'Yet', 'Indeed', 'Truly', 'Sure', |
| 'Okay', 'Right', 'Good', 'Dear', 'Love', 'Sugar', 'Honey', 'Babe', 'Friend', 'Sweetheart']); |
| function mineUserNames(fragments, entityDir) { |
| let entName = ''; |
| try { entName = (JSON.parse(fs.readFileSync(entityDir + '/voiceprint.json', 'utf8').replace(/^/, '')).name || '').split(/\s+/)[0]; } catch (_) {} |
| const ent = entName.toLowerCase(); |
| const counts = new Map(), greet = new Map(); |
| const bump = (map, n) => { if (n.toLowerCase() === ent || _NAME_STOP.has(n)) return; map.set(n, (map.get(n) || 0) + 1); }; |
| const GREET = /\b(?:hey|hi|hello|good\s+(?:morning|evening|night|day)|thank you|thanks|oh|dear|welcome|hey there|my)[,!\s]+([A-Z][a-z]{2,})\b/g; |
| for (const f of fragments) { |
| const t = f.text; |
| let m; GREET.lastIndex = 0; while ((m = GREET.exec(t))) { bump(counts, m[1]); bump(greet, m[1]); } |
| const lead = t.match(/^["'*\s]*([A-Z][a-z]{2,}),/); if (lead) bump(counts, lead[1]); |
| const tail = t.match(/,\s+([A-Z][a-z]{2,})\s*[?!.]/); if (tail) bump(counts, tail[1]); |
| } |
| if (!counts.size) return new Set(); |
| const top = Math.max(...counts.values()); |
| const out = new Set(); |
| for (const [n, c] of counts) { |
| |
| if (c >= Math.max(4, top * 0.5) || (greet.get(n) || 0) >= 3) out.add(n); |
| } |
| return out; |
| } |
|
|
| function buildStore(entityDir) { |
| const lines = fs.readFileSync(entityDir + '/corpus.jsonl', 'utf8').split('\n').filter(l => l.trim()); |
| |
| let _entName = ''; |
| try { const _vp = JSON.parse(fs.readFileSync(entityDir + '/voiceprint.json', 'utf8').replace(/^/, '')); _entName = (_vp && _vp.name ? String(_vp.name).split(/\s+/)[0] : ''); } catch (_) {} |
| const exchanges = lines.map(l => { try { const e = JSON.parse(l); if (!e || !e.reply) return e; if (isDevArtifactReply(e.reply) || isVisitorTurnReply(e.reply, _entName)) return null; e.reply = cleanMarkup(entityOnly(e.reply)); return e; } catch (_) { return null; } }).filter(Boolean); |
| const replies = exchanges.map(e => e.reply).filter(Boolean); |
| |
| const promptList = []; |
| const promptIdxOf = new Map(); |
| const fragments = []; |
| exchanges.forEach((e, i) => { |
| if (!e.reply) return; |
| let pIdx = -1; |
| if (e.prompt && e.prompt.trim().length > 10) { |
| const key = e.prompt.trim().slice(0, 300); |
| if (!promptIdxOf.has(key)) { promptIdxOf.set(key, promptList.length); promptList.push(key); } |
| pIdx = promptIdxOf.get(key); |
| } |
| for (const f of fragmentReply(e.reply, { src: i, prompt: e.prompt || null, promptIdx: pIdx })) fragments.push(f); |
| }); |
| |
| const seen = new Set(); |
| const uniq = fragments.filter(f => { const k = f.text.toLowerCase(); if (seen.has(k)) return false; seen.add(k); return true; }); |
| const oracle = buildOracle(replies); |
| const vocab = new Set(replies.flatMap(wordsOnly)); |
| for (const f of uniq) f.tier = 0; |
| const userNames = mineUserNames(uniq, entityDir); |
| return { exchanges, replies, fragments: uniq, oracle, vocab, prompts: promptList, userNames }; |
| } |
|
|
| |
| |
| |
| |
| function buildStoreX(entityDir, corpusXDir) { |
| const base = buildStore(entityDir); |
| const fragments = base.fragments.slice(); |
| const allTexts = base.replies.slice(); |
|
|
| |
| |
| const nodesPath = path.join(corpusXDir, 'nodes.json'); |
| if (fs.existsSync(nodesPath)) { |
| const raw = JSON.parse(fs.readFileSync(nodesPath, 'utf8')); |
| const nodes = Array.isArray(raw) ? raw : (raw.nodes || []); |
| let mem = 0, phr = 0; |
| for (const n of nodes) { |
| if (!n || !n.text || typeof n.text !== 'string') continue; |
| let t = n.text.trim(); |
| if (t.length < 8) continue; |
| |
| |
| |
| |
| const hadHeader = /^\*\*[^*\n]{2,60}\*\*\s*[—–-]\s*/.test(t) || /^(note to self|journal|log)[:\s]/i.test(t); |
| t = t.replace(/^\*\*[^*\n]{2,60}\*\*\s*[—–-]\s*/, ''); |
| t = cleanMarkup(t); |
| if (t.length < 8) continue; |
| allTexts.push(t); |
| if (n.level === 'phrase') { |
| const w = wordsOnly(t); |
| if (w.length >= 3 && w.length <= 18 && !isDirtyFragment(t)) { |
| fragments.push({ text: t, posTag: 'body', sentenceInitial: !hadHeader, tier: 0, src: 'node:' + n.id, prompt: null, promptIdx: -1, nativePos: 0.5 }); |
| phr++; |
| } |
| } else { |
| for (const f of fragmentReply(t, { src: 'node:' + n.id, prompt: null, promptIdx: -1 })) { |
| fragments.push({ ...f, tier: 0, sentenceInitial: hadHeader ? false : f.sentenceInitial, posTag: hadHeader && f.posTag === 'opener' ? 'body' : f.posTag }); |
| mem++; |
| } |
| } |
| } |
| process.stderr.write(`[storeX] nodes: +${mem} memory-frags, +${phr} phrase-frags\n`); |
| } |
|
|
| |
| for (const f of fs.readdirSync(corpusXDir)) { |
| if (!f.endsWith('.txt')) continue; |
| const name = f.replace(/\.txt$/, ''); |
| const frags = fragmentBook(fs.readFileSync(path.join(corpusXDir, f), 'utf8'), name); |
| for (const fr of frags) { fragments.push(fr); allTexts.push(fr.text); } |
| process.stderr.write(`[storeX] ${name}: +${frags.length} tier-1 frags\n`); |
| } |
|
|
| |
| |
| |
| |
| const herCaps = new Set(); |
| for (const f of fragments) if (f.tier !== 1) { |
| for (const m of f.text.matchAll(/\b[A-Z][a-z]{2,}\b/g)) herCaps.add(m[0]); |
| } |
| const foreignWorld = f => { |
| if (f.tier !== 1) return false; |
| const mids = [...f.text.slice(1).matchAll(/(?<=[a-z,;—-]\s)[A-Z][a-z]{2,}\b/g)].map(m => m[0]); |
| return mids.some(w => !herCaps.has(w)); |
| }; |
| const beforeFW = fragments.length; |
| const kept = fragments.filter(f => !foreignWorld(f)); |
| process.stderr.write(`[storeX] foreign-world filter: -${beforeFW - kept.length} tier-1 frags\n`); |
|
|
| |
| const seen = new Set(); |
| const uniq = kept.filter(fr => { const k = fr.text.toLowerCase(); if (seen.has(k)) return false; seen.add(k); return true; }); |
| const oracle = buildOracle(allTexts); |
| const vocab = new Set(allTexts.flatMap(wordsOnly)); |
|
|
| |
| const byFirstWord = new Map(); |
| uniq.forEach((fr, i) => { |
| const w = wordsOnly(fr.text)[0]; |
| if (!w) return; |
| if (!byFirstWord.has(w)) byFirstWord.set(w, []); |
| byFirstWord.get(w).push(i); |
| }); |
|
|
| process.stderr.write(`[storeX] TOTAL: ${uniq.length} fragments (${uniq.filter(x => x.tier === 0).length} tier-0), ${oracle.tri.size} trigrams, vocab ${vocab.size}\n`); |
| return { ...base, fragments: uniq, oracle, vocab, byFirstWord }; |
| } |
|
|
| |
| function validateBounded(text, oracle) { |
| const w = wordsOnly(text); |
| const bad = []; |
| for (let i = 0; i + 2 < w.length; i++) { |
| const g = w[i] + ' ' + w[i + 1] + ' ' + w[i + 2]; |
| if (!oracle.tri.has(g)) bad.push(g); |
| } |
| return { ok: bad.length === 0, bad: bad.slice(0, 5), checked: Math.max(0, w.length - 2) }; |
| } |
|
|
| module.exports = { buildStore, buildStoreX, fragmentReply, fragmentBook, buildOracle, validateBounded, wordsOnly, toks, mineUserNames, isDevArtifactReply, isVisitorTurnReply, isDirtyFragment }; |
|
|