wiki-plugin-similarity 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- /* wiki-plugin-similarity - 0.1.8 - Tue, 09 Jun 2026 20:28:12 GMT */
1
+ /* wiki-plugin-similarity - 0.1.9 - Tue, 09 Jun 2026 20:32:03 GMT */
2
2
  (()=>{var R={high:.78,medium:.68,low:.58},v=R.medium,I=10,z=t=>{let e=[],n=null,s=null,i="search",d=(o,a)=>o===a||o.startsWith(a)&&/^[\s:]/.test(o.slice(a.length)),r=(o,a)=>o.slice(a.length).replace(/^\s*:?\s*/,"").trim();for(let o of t.split(`
3
3
  `)){let a=o.trim();if(!a||a.startsWith("#"))continue;let l=a.toUpperCase();if(d(l,"LIST")){!e.length&&i==="search"&&(i="list");continue}if(d(l,"SIMILAR")){let h=r(l,"SIMILAR").toLowerCase();n=R[h]||v,!e.length&&i==="search"&&(i="similar");continue}if(d(l,"THRESHOLD")){n=parseFloat(r(a,"THRESHOLD"))||v;continue}if(d(l,"LIMIT")){s=parseInt(r(a,"LIMIT"))||I;continue}e.push(a)}return{mode:i,specs:e,threshold:n??v,limit:s??I}},F=t=>t.includes("*")||t.includes("?"),U=t=>t.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,""),S=new Map,A=async(t,e)=>{if(S.has(t))return S.get(t);let n=`${e}/system/indexed-domains.json?pattern=${encodeURIComponent(t)}`,s=await fetch(n);if(!s.ok)throw new Error(`indexed-domains failed: ${s.status}`);let i=await s.json();return S.set(t,i),i},P=async(t,e)=>{t.length||(t=[window.location.hostname]);let n=new Set,s=[];for(let i of t)if(i==="*"||F(i))for(let d of await A(i,e))n.has(d.domain)||(n.add(d.domain),s.push(d));else n.has(i)||(n.add(i),s.push({domain:i,page_count:null}));return s},E=new Map,_=()=>{let t=window.location.hostname;return t==="localhost"||t.endsWith(".localhost")||t==="127.0.0.1"},O=t=>_()?`${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(t)}`:`http://${t}/system/semantic-vectors.json`,H=async t=>{if(E.has(t))return E.get(t);let e=await fetch(O(t));if(!e.ok)return[];let n=await e.json();return E.set(t,n),n},k=async(t,e)=>{let n=await fetch(`${e}/system/embed.json?text=${encodeURIComponent(t)}`);if(!n.ok)throw new Error(`embed failed: ${n.status}`);return(await n.json()).vector},q=async(t,e)=>{let s=(await H(e)).find(i=>i.slug===t);return s?s.vector:null},D=(t,e,{threshold:n,limit:s,excludeSlug:i,excludeDomain:d})=>{let r=[];for(let{domain:o,pages:a}of e)for(let{slug:l,title:h,vector:p}of a){if(l===i&&o===d)continue;let g=0;for(let c=0;c<t.length;c++)g+=t[c]*p[c];g>=n&&r.push({domain:o,slug:l,title:h,score:g})}return r.sort((o,a)=>a.score-o.score),r.slice(0,s)},T=async(t,e)=>{let n=await P(t,e);return(await Promise.all(n.map(async({domain:i})=>({domain:i,pages:await H(i)})))).filter(i=>i.pages.length>0)},C=`
4
4
  .sim-form { display:flex; gap:6px; margin-bottom:8px; }
@@ -45,7 +45,7 @@
45
45
  </div>
46
46
  <div class="sim-status">Domains: ${r}</div>
47
47
  <div class="sim-results"></div>
48
- </div>`)}},W=(t,e)=>{let{mode:n,specs:s,threshold:i,limit:d}=z(e?.text||""),r=window.location.origin,o=t.find(".sim-status")[0];if(t.on("dblclick",()=>window.wiki.textEditor(t,e)),t.on("click",".sim-link",function(a){a.preventDefault();let l=$(this);window.wiki.doInternalLink(l.data("title"),t.parents(".page"),l.data("site"))}),n==="list"){let a=t.find(".sim-list")[0],l=s.length?s.join(","):"*";(async()=>{try{let p=`${r}/system/indexed-domains.json?pattern=${encodeURIComponent(l)}`,g=await fetch(p);if(!g.ok)throw new Error(`indexed-domains failed: ${g.status}`);let c=await g.json();if(!c.length){o.textContent="No indexed domains found";return}let m=c.slice(0,d),u=c.reduce((x,w)=>x+(w.page_count||0),0),f=m.length<c.length?`showing ${m.length} of ${c.length}`:`${c.length} domains`;o.style.display="none",a.innerHTML=`<h3>Indexed Farm Domains</h3>
48
+ </div>`)}},W=(t,e)=>{let{mode:n,specs:s,threshold:i,limit:d}=z(e?.text||""),r=window.location.origin,o=t.find(".sim-status")[0];if(t.on("dblclick",()=>window.wiki.textEditor(t,e)),t.on("click",".sim-link",function(a){a.preventDefault();let l=$(this);window.wiki.doInternalLink(l.data("title"),t.parents(".page"),l.data("site"))}),n==="list"){let a=t.find(".sim-list")[0],l=s.length?s.join(","):"*";(async()=>{try{let p=`${r}/system/indexed-domains.json?pattern=${encodeURIComponent(l)}&limit=${d}`,g=await fetch(p);if(!g.ok)throw new Error(`indexed-domains failed: ${g.status}`);let c=await g.json();if(!c.length){o.textContent="No indexed domains found";return}let m=c.slice(0,d),u=c.reduce((x,w)=>x+(w.page_count||0),0),f=m.length<c.length?`showing ${m.length} of ${c.length}`:`${c.length} domains`;o.style.display="none",a.innerHTML=`<h3>Indexed Farm Domains</h3>
49
49
  <table>
50
50
  <tr><th>Domain</th><th>Pages</th></tr>
51
51
  ${m.map(({domain:x,page_count:w})=>`
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/client/similarity.js"],
4
- "sourcesContent": ["// wiki-plugin-similarity\n//\n// One item type: similarity\n//\n// DSL in item.text (newline-separated lines):\n// * all indexed domains on this server\n// david.* glob pattern\n// *.fish glob pattern\n// david.hitchhikers.earth explicit domain name\n// # comment ignored\n// LIST list indexed domains (mode directive)\n// SIMILAR: high threshold preset AND ambient mode trigger\n// THRESHOLD: 0.72 exact cosine threshold (overrides SIMILAR:)\n// LIMIT: 8 max results shown (default 10)\n//\n// Mode is determined by the FIRST meaningful line (Ward's ALL-CAPS convention):\n// LIST \u2192 show a table of all indexed domains and their page counts;\n// optional glob patterns on subsequent lines filter the list\n// SIMILAR: \u2192 ambient mode \u2014 automatically find pages similar to this page\n// (other) \u2192 search form mode \u2014 user types a query, results appear\n//\n// Server endpoints required:\n// GET /system/indexed-domains.json?pattern=glob1,glob2\n// GET /system/semantic-vectors.json?domain=\n// GET /system/embed.json?text=query\n\n// \u2500\u2500 DSL Parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst SIMILAR_THRESHOLDS = { high: 0.78, medium: 0.68, low: 0.58 }\nconst DEFAULT_THRESHOLD = SIMILAR_THRESHOLDS.medium\nconst DEFAULT_LIMIT = 10\n\n// parseDSL returns { mode, specs, threshold, limit }\n//\n// mode: 'similar' if SIMILAR: is the first meaningful line (ambient auto-run),\n// 'search' otherwise (interactive search form).\n//\n// Ward's convention: ALL-CAPS keyword as first word signals a mode switch.\n// Placing SIMILAR: first makes the item ambient; placing domain specs first\n// (or leaving text empty) keeps it as an interactive search form.\n\nconst parseDSL = text => {\n const specs = []\n let threshold = null\n let limit = null\n let mode = 'search' // default: interactive search form\n\n // Match a keyword at the start of a line (case-insensitive), requiring it to\n // be followed by end-of-string, whitespace, or colon \u2014 not by more word chars.\n // This prevents domain specs like \"similarity.example.com\" matching SIMILAR.\n const isCmd = (upper, kw) => upper === kw || (upper.startsWith(kw) && /^[\\s:]/.test(upper.slice(kw.length)))\n // Extract the value after the keyword, tolerating optional colon and whitespace.\n // Returns '' for bare commands (e.g. \"SIMILAR\" alone), callers use their default.\n const val = (line, kw) => line.slice(kw.length).replace(/^\\s*:?\\s*/, '').trim()\n\n for (const raw of text.split('\\n')) {\n const line = raw.trim()\n if (!line || line.startsWith('#')) continue\n\n const upper = line.toUpperCase()\n if (isCmd(upper, 'LIST')) {\n if (!specs.length && mode === 'search') mode = 'list'\n continue\n }\n if (isCmd(upper, 'SIMILAR')) {\n const level = val(upper, 'SIMILAR').toLowerCase()\n threshold = SIMILAR_THRESHOLDS[level] || DEFAULT_THRESHOLD // '' \u2192 medium\n if (!specs.length && mode === 'search') mode = 'similar'\n continue\n }\n if (isCmd(upper, 'THRESHOLD')) {\n threshold = parseFloat(val(line, 'THRESHOLD')) || DEFAULT_THRESHOLD // '' \u2192 medium\n continue\n }\n if (isCmd(upper, 'LIMIT')) {\n limit = parseInt(val(line, 'LIMIT')) || DEFAULT_LIMIT // '' \u2192 10\n continue\n }\n // Anything else is a domain spec (glob or explicit domain)\n specs.push(line)\n }\n\n return {\n mode,\n specs,\n threshold: threshold ?? DEFAULT_THRESHOLD,\n limit: limit ?? DEFAULT_LIMIT,\n }\n}\n\nconst isGlob = spec => spec.includes('*') || spec.includes('?')\n\n// \u2500\u2500 Slug \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst slugify = title => title.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '')\n\n// \u2500\u2500 Domain resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst domainCache = new Map()\n\nconst resolveDomainsForSpec = async (spec, origin) => {\n if (domainCache.has(spec)) return domainCache.get(spec)\n const url = `${origin}/system/indexed-domains.json?pattern=${encodeURIComponent(spec)}`\n const res = await fetch(url)\n if (!res.ok) throw new Error(`indexed-domains failed: ${res.status}`)\n const list = await res.json()\n domainCache.set(spec, list)\n return list\n}\n\nconst resolveDomains = async (specs, origin) => {\n if (!specs.length) specs = [window.location.hostname]\n const seen = new Set()\n const result = []\n for (const spec of specs) {\n if (spec === '*' || isGlob(spec)) {\n for (const item of await resolveDomainsForSpec(spec, origin)) {\n if (!seen.has(item.domain)) { seen.add(item.domain); result.push(item) }\n }\n } else if (!seen.has(spec)) {\n seen.add(spec); result.push({ domain: spec, page_count: null })\n }\n }\n return result\n}\n\n// \u2500\u2500 Vector loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst vectorCache = new Map()\n\n// Detect whether the wiki is running on localhost (or a .localhost subdomain).\n// On localhost we proxy all domain vector fetches through the local server's\n// ?domain= parameter \u2014 this reads vectors directly from the farm folder on disk,\n// avoiding any need for remote servers to have this plugin installed, and\n// sidestepping CORS entirely.\nconst isLocalhost = () => {\n const h = window.location.hostname\n return h === 'localhost' || h.endsWith('.localhost') || h === '127.0.0.1'\n}\n\nconst vectorUrl = domain => {\n if (isLocalhost()) {\n // Route through our local server \u2014 serves any farm domain from disk\n return `${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(domain)}`\n }\n // On a real server, fetch directly from the remote wiki\n return `http://${domain}/system/semantic-vectors.json`\n}\n\nconst loadVectors = async domain => {\n if (vectorCache.has(domain)) return vectorCache.get(domain)\n const res = await fetch(vectorUrl(domain))\n if (!res.ok) return []\n const data = await res.json()\n vectorCache.set(domain, data)\n return data\n}\n\n// \u2500\u2500 Embedding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst getEmbedding = async (text, origin) => {\n const res = await fetch(`${origin}/system/embed.json?text=${encodeURIComponent(text)}`)\n if (!res.ok) throw new Error(`embed failed: ${res.status}`)\n return (await res.json()).vector\n}\n\n// Look up an existing page vector from the current domain's cached index.\n// Returns the float[] vector if found, null otherwise.\nconst lookupPageVector = async (slug, domain) => {\n const pages = await loadVectors(domain)\n const entry = pages.find(p => p.slug === slug)\n return entry ? entry.vector : null\n}\n\n// \u2500\u2500 Cosine search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst cosineScan = (queryVec, domainEntries, { threshold, limit, excludeSlug, excludeDomain }) => {\n const results = []\n for (const { domain, pages } of domainEntries) {\n for (const { slug, title, vector } of pages) {\n if (slug === excludeSlug && domain === excludeDomain) continue\n let dot = 0\n for (let i = 0; i < queryVec.length; i++) dot += queryVec[i] * vector[i]\n if (dot >= threshold) results.push({ domain, slug, title, score: dot })\n }\n }\n results.sort((a, b) => b.score - a.score)\n return results.slice(0, limit)\n}\n\n// \u2500\u2500 Shared domain loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst loadDomainEntries = async (specs, origin) => {\n const domains = await resolveDomains(specs, origin)\n const entries = await Promise.all(\n domains.map(async ({ domain }) => ({ domain, pages: await loadVectors(domain) }))\n )\n return entries.filter(e => e.pages.length > 0)\n}\n\n// \u2500\u2500 Styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst STYLES = `\n .sim-form { display:flex; gap:6px; margin-bottom:8px; }\n .sim-input { flex:1; padding:6px 8px; font-size:14px; border:1px solid #ccc; border-radius:3px; }\n .sim-btn { padding:6px 14px; background:#c4561d; color:white; border:none;\n border-radius:3px; cursor:pointer; font-size:14px; }\n .sim-btn:disabled { opacity:0.5; cursor:not-allowed; }\n .sim-status { font-size:12px; color:#888; margin-bottom:6px; min-height:16px; }\n .sim-results { margin-top:4px; }\n .sim-result { display:flex; align-items:center; gap:8px; padding:3px 0;\n border-bottom:1px solid #f0f0f0; }\n .sim-flag { width:16px; height:16px; vertical-align:middle; margin-right:4px; }\n .sim-link { font-size:14px; color:#406; flex:1; }\n .sim-domain { font-size:11px; color:#999; }\n .similar-results h3, .sim-list h3 { margin:4px 0 6px; font-size:14px; color:#555; }\n .similar-results ul { margin:0; padding-left:18px; }\n .similar-results li { font-size:14px; padding:2px 0; }\n .similar-results .sim-domain { margin-left:6px; }\n .sim-count { font-size:12px; color:#888; margin:4px 0 0; }\n .sim-list table { border-collapse:collapse; width:100%; font-size:13px; }\n .sim-list th { text-align:left; font-size:11px; color:#888; padding:2px 8px 4px 0;\n border-bottom:1px solid #ddd; }\n .sim-list td { padding:3px 8px 3px 0; border-bottom:1px solid #f0f0f0; vertical-align:middle; }\n .sim-list td:last-child { text-align:right; color:#999; font-size:11px; }\n .sim-list .sim-flag { margin-right:6px; }\n`\n\nconst siteFlag = (domain, score) =>\n `<img class=\"sim-flag remote\" src=\"${window.wiki.site(domain).flag()}\"\n title=\"${domain} \u2014 score ${score.toFixed(3)}\"\n data-site=\"${domain}\">`\n\n// \u2500\u2500 similarity item \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst simLink = (domain, slug, title, score) =>\n `<a class=\"sim-link\" data-title=\"${title}\" data-slug=\"${slug}\" data-site=\"${domain}\" href=\"#\">` +\n `${siteFlag(domain, score)} ${title}</a>`\n\nexport const emit = (div, item) => {\n const { mode, specs, threshold, limit } = parseDSL(item?.text || '')\n if (mode === 'list') {\n const label = specs.length ? specs.join(', ') : '*'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-status\">Loading indexed domains (${label})\u2026</div>\n <div class=\"sim-list\"></div>\n </div>`)\n } else if (mode === 'similar') {\n const label = specs.length ? specs.join(', ') : 'current domain'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-status\">Finding similar pages across ${label}\u2026</div>\n <div class=\"sim-results\"></div>\n </div>`)\n } else {\n const label = specs.length ? specs.join(', ') : '(current domain)'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-form\">\n <input class=\"sim-input\" type=\"text\" placeholder=\"Search wiki pages\u2026\" />\n <button class=\"sim-btn\">Search</button>\n </div>\n <div class=\"sim-status\">Domains: ${label}</div>\n <div class=\"sim-results\"></div>\n </div>`)\n }\n}\n\nexport const bind = (div, item) => {\n const { mode, specs, threshold, limit } = parseDSL(item?.text || '')\n const origin = window.location.origin\n const status = div.find('.sim-status')[0]\n\n div.on('dblclick', () => window.wiki.textEditor(div, item))\n\n div.on('click', '.sim-link', function (e) {\n e.preventDefault()\n const $a = $(this)\n window.wiki.doInternalLink($a.data('title'), div.parents('.page'), $a.data('site'))\n })\n\n if (mode === 'list') {\n // List all indexed domains matching the spec patterns (default: all)\n const listDiv = div.find('.sim-list')[0]\n const patterns = specs.length ? specs.join(',') : '*'\n const run = async () => {\n try {\n const url = `${origin}/system/indexed-domains.json?pattern=${encodeURIComponent(patterns)}`\n const res = await fetch(url)\n if (!res.ok) throw new Error(`indexed-domains failed: ${res.status}`)\n const allDomains = await res.json()\n if (!allDomains.length) {\n status.textContent = 'No indexed domains found'\n return\n }\n const shown = allDomains.slice(0, limit)\n const totalPages = allDomains.reduce((n, d) => n + (d.page_count || 0), 0)\n const countNote = shown.length < allDomains.length\n ? `showing ${shown.length} of ${allDomains.length}`\n : `${allDomains.length} domains`\n status.style.display = 'none'\n listDiv.innerHTML = `<h3>Indexed Farm Domains</h3>\n <table>\n <tr><th>Domain</th><th>Pages</th></tr>\n ${shown.map(({ domain, page_count }) => `\n <tr>\n <td><img class=\"sim-flag remote\" src=\"${window.wiki.site(domain).flag()}\"\n title=\"${domain}\" data-site=\"${domain}\"> ${domain}</td>\n <td>${page_count != null ? page_count.toLocaleString() : '\u2014'}</td>\n </tr>`).join('')}\n </table>\n <p class=\"sim-count\">${countNote} \u2014 ${totalPages.toLocaleString()} pages</p>`\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n }\n }\n run()\n } else if (mode === 'similar') {\n // Ambient mode \u2014 run automatically, no search form\n const run = async () => {\n try {\n const $page = div.parents('.page')\n const pageTitle = $page.find('.title').text().trim() || document.title\n const currentSlug = slugify(pageTitle)\n const currentDomain = window.location.hostname\n\n const domainEntries = await loadDomainEntries(specs, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = `Searching ${total.toLocaleString()} pages\u2026`\n\n let qVec = await lookupPageVector(currentSlug, currentDomain)\n if (!qVec) {\n status.textContent = 'Embedding page (not yet indexed)\u2026'\n const pageText = $page.find('.item').map((_, el) => $(el).text().trim()).get().filter(Boolean).join('\\n')\n qVec = await getEmbedding(pageText || pageTitle, origin)\n }\n\n const scored = cosineScan(qVec, domainEntries, {\n threshold, limit, excludeSlug: currentSlug, excludeDomain: currentDomain,\n })\n\n if (!scored.length) {\n status.textContent = `No similar pages found above threshold ${threshold}`\n return\n }\n const scopeLabel = !specs.length || (specs.length === 1 && specs[0] === '*')\n ? 'on farm'\n : specs.length === 1 ? `on ${specs[0]}` : 'in domains'\n const results = div.find('.sim-results')[0]\n results.innerHTML = `<h3>Similar Pages</h3><ul>${\n scored.map(({ domain, slug, title, score }) =>\n `<li>${simLink(domain, slug, title, score)}</li>`).join('')\n }</ul><p class=\"sim-count\">${scored.length} found ${scopeLabel}</p>`\n status.style.display = 'none'\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n }\n }\n run()\n } else {\n // Search form mode \u2014 preload domains, then wait for user input\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n const results = div.find('.sim-results')[0]\n let domainEntries = null\n\n const preload = async () => {\n try {\n status.textContent = 'Resolving domains\u2026'\n domainEntries = await loadDomainEntries(specs, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = `Ready \u2014 ${total.toLocaleString()} pages across ${domainEntries.length} domains`\n } catch (e) {\n status.textContent = `Load error: ${e.message}`\n }\n }\n preload()\n\n const doSearch = async () => {\n const query = input.value.trim()\n if (!query || !domainEntries) return\n btn.disabled = true\n status.textContent = 'Embedding query\u2026'\n results.innerHTML = ''\n try {\n const qVec = await getEmbedding(query, origin)\n const scored = cosineScan(qVec, domainEntries,\n { threshold: 0, limit, excludeSlug: null, excludeDomain: null })\n results.innerHTML = scored.map(({ domain, slug, title, score }) =>\n `<div class=\"sim-result\">${simLink(domain, slug, title, score)}</div>`).join('')\n status.textContent = `Top ${scored.length} results for \"${query}\"`\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doSearch)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch() })\n }\n}\n\n// \u2500\u2500 Register item type with the wiki \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nif (typeof window !== 'undefined') {\n window.plugins = window.plugins || {}\n window.plugins.similarity = { emit, bind }\n}\n"],
5
- "mappings": ";MA4BA,IAAMA,EAAqB,CAAE,KAAM,IAAM,OAAQ,IAAM,IAAK,GAAK,EAC3DC,EAAqBD,EAAmB,OACxCE,EAAqB,GAWrBC,EAAWC,GAAQ,CACvB,IAAMC,EAAU,CAAC,EACbC,EAAY,KACZC,EAAY,KACZC,EAAY,SAKVC,EAAS,CAACC,EAAOC,IAAOD,IAAUC,GAAOD,EAAM,WAAWC,CAAE,GAAK,SAAS,KAAKD,EAAM,MAAMC,EAAG,MAAM,CAAC,EAGrGC,EAAS,CAACC,EAAMF,IAAOE,EAAK,MAAMF,EAAG,MAAM,EAAE,QAAQ,YAAa,EAAE,EAAE,KAAK,EAEjF,QAAWG,KAAOV,EAAK,MAAM;AAAA,CAAI,EAAG,CAClC,IAAMS,EAAOC,EAAI,KAAK,EACtB,GAAI,CAACD,GAAQA,EAAK,WAAW,GAAG,EAAG,SAEnC,IAAMH,EAAQG,EAAK,YAAY,EAC/B,GAAIJ,EAAMC,EAAO,MAAM,EAAG,CACpB,CAACL,EAAM,QAAUG,IAAS,WAAUA,EAAO,QAC/C,QACF,CACA,GAAIC,EAAMC,EAAO,SAAS,EAAG,CAC3B,IAAMK,EAAQH,EAAIF,EAAO,SAAS,EAAE,YAAY,EAChDJ,EAAYN,EAAmBe,CAAK,GAAKd,EACrC,CAACI,EAAM,QAAUG,IAAS,WAAUA,EAAO,WAC/C,QACF,CACA,GAAIC,EAAMC,EAAO,WAAW,EAAG,CAC7BJ,EAAY,WAAWM,EAAIC,EAAM,WAAW,CAAC,GAAKZ,EAClD,QACF,CACA,GAAIQ,EAAMC,EAAO,OAAO,EAAG,CACzBH,EAAQ,SAASK,EAAIC,EAAM,OAAO,CAAC,GAAKX,EACxC,QACF,CAEAG,EAAM,KAAKQ,CAAI,CACjB,CAEA,MAAO,CACL,KAAAL,EACA,MAAAH,EACA,UAAWC,GAAaL,EACxB,MAAWM,GAAaL,CAC1B,CACF,EAEMc,EAASC,GAAQA,EAAK,SAAS,GAAG,GAAKA,EAAK,SAAS,GAAG,EAIxDC,EAAUC,GAASA,EAAM,YAAY,EAAE,QAAQ,OAAQ,GAAG,EAAE,QAAQ,cAAe,EAAE,EAIrFC,EAAc,IAAI,IAElBC,EAAwB,MAAOJ,EAAMK,IAAW,CACpD,GAAIF,EAAY,IAAIH,CAAI,EAAG,OAAOG,EAAY,IAAIH,CAAI,EACtD,IAAMM,EAAM,GAAGD,CAAM,wCAAwC,mBAAmBL,CAAI,CAAC,GAC/EO,EAAM,MAAM,MAAMD,CAAG,EAC3B,GAAI,CAACC,EAAI,GAAI,MAAM,IAAI,MAAM,2BAA2BA,EAAI,MAAM,EAAE,EACpE,IAAMC,EAAO,MAAMD,EAAI,KAAK,EAC5B,OAAAJ,EAAY,IAAIH,EAAMQ,CAAI,EACnBA,CACT,EAEMC,EAAiB,MAAOrB,EAAOiB,IAAW,CACzCjB,EAAM,SAAQA,EAAQ,CAAC,OAAO,SAAS,QAAQ,GACpD,IAAMsB,EAAO,IAAI,IACXC,EAAS,CAAC,EAChB,QAAWX,KAAQZ,EACjB,GAAIY,IAAS,KAAOD,EAAOC,CAAI,EAC7B,QAAWY,KAAQ,MAAMR,EAAsBJ,EAAMK,CAAM,EACpDK,EAAK,IAAIE,EAAK,MAAM,IAAKF,EAAK,IAAIE,EAAK,MAAM,EAAGD,EAAO,KAAKC,CAAI,QAE7DF,EAAK,IAAIV,CAAI,IACvBU,EAAK,IAAIV,CAAI,EAAGW,EAAO,KAAK,CAAE,OAAQX,EAAM,WAAY,IAAK,CAAC,GAGlE,OAAOW,CACT,EAIME,EAAc,IAAI,IAOlBC,EAAc,IAAM,CACxB,IAAMC,EAAI,OAAO,SAAS,SAC1B,OAAOA,IAAM,aAAeA,EAAE,SAAS,YAAY,GAAKA,IAAM,WAChE,EAEMC,EAAYC,GACZH,EAAY,EAEP,GAAG,OAAO,SAAS,MAAM,wCAAwC,mBAAmBG,CAAM,CAAC,GAG7F,UAAUA,CAAM,gCAGnBC,EAAc,MAAMD,GAAU,CAClC,GAAIJ,EAAY,IAAII,CAAM,EAAG,OAAOJ,EAAY,IAAII,CAAM,EAC1D,IAAMV,EAAM,MAAM,MAAMS,EAAUC,CAAM,CAAC,EACzC,GAAI,CAACV,EAAI,GAAI,MAAO,CAAC,EACrB,IAAMY,EAAO,MAAMZ,EAAI,KAAK,EAC5B,OAAAM,EAAY,IAAII,EAAQE,CAAI,EACrBA,CACT,EAIMC,EAAe,MAAOjC,EAAMkB,IAAW,CAC3C,IAAME,EAAM,MAAM,MAAM,GAAGF,CAAM,2BAA2B,mBAAmBlB,CAAI,CAAC,EAAE,EACtF,GAAI,CAACoB,EAAI,GAAI,MAAM,IAAI,MAAM,iBAAiBA,EAAI,MAAM,EAAE,EAC1D,OAAQ,MAAMA,EAAI,KAAK,GAAG,MAC5B,EAIMc,EAAmB,MAAOC,EAAML,IAAW,CAE/C,IAAMM,GADQ,MAAML,EAAYD,CAAM,GAClB,KAAKO,GAAKA,EAAE,OAASF,CAAI,EAC7C,OAAOC,EAAQA,EAAM,OAAS,IAChC,EAIME,EAAa,CAACC,EAAUC,EAAe,CAAE,UAAAtC,EAAW,MAAAC,EAAO,YAAAsC,EAAa,cAAAC,CAAc,IAAM,CAChG,IAAMC,EAAU,CAAC,EACjB,OAAW,CAAE,OAAAb,EAAQ,MAAAc,CAAM,IAAKJ,EAC9B,OAAW,CAAE,KAAAL,EAAM,MAAApB,EAAO,OAAA8B,CAAO,IAAKD,EAAO,CAC3C,GAAIT,IAASM,GAAeX,IAAWY,EAAe,SACtD,IAAII,EAAM,EACV,QAASC,EAAI,EAAGA,EAAIR,EAAS,OAAQQ,IAAKD,GAAOP,EAASQ,CAAC,EAAIF,EAAOE,CAAC,EACnED,GAAO5C,GAAWyC,EAAQ,KAAK,CAAE,OAAAb,EAAQ,KAAAK,EAAM,MAAApB,EAAO,MAAO+B,CAAI,CAAC,CACxE,CAEF,OAAAH,EAAQ,KAAK,CAACK,EAAGC,IAAMA,EAAE,MAAQD,EAAE,KAAK,EACjCL,EAAQ,MAAM,EAAGxC,CAAK,CAC/B,EAIM+C,EAAoB,MAAOjD,EAAOiB,IAAW,CACjD,IAAMiC,EAAU,MAAM7B,EAAerB,EAAOiB,CAAM,EAIlD,OAHgB,MAAM,QAAQ,IAC5BiC,EAAQ,IAAI,MAAO,CAAE,OAAArB,CAAO,KAAO,CAAE,OAAAA,EAAQ,MAAO,MAAMC,EAAYD,CAAM,CAAE,EAAE,CAClF,GACe,OAAOsB,GAAKA,EAAE,MAAM,OAAS,CAAC,CAC/C,EAIMC,EAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA0BTC,EAAW,CAACxB,EAAQyB,IACxB,qCAAqC,OAAO,KAAK,KAAKzB,CAAM,EAAE,KAAK,CAAC;AAAA,iBACrDA,CAAM,iBAAYyB,EAAM,QAAQ,CAAC,CAAC;AAAA,qBAC9BzB,CAAM,KAIrB0B,EAAU,CAAC1B,EAAQK,EAAMpB,EAAOwC,IACpC,mCAAmCxC,CAAK,gBAAgBoB,CAAI,gBAAgBL,CAAM,cAC/EwB,EAASxB,EAAQyB,CAAK,CAAC,IAAIxC,CAAK,OAExB0C,EAAO,CAACC,EAAKjC,IAAS,CACjC,GAAM,CAAE,KAAArB,EAAM,MAAAH,EAAO,UAAAC,EAAW,MAAAC,CAAM,EAAIJ,EAAS0B,GAAM,MAAQ,EAAE,EACnE,GAAIrB,IAAS,OAAQ,CACnB,IAAMuD,EAAQ1D,EAAM,OAASA,EAAM,KAAK,IAAI,EAAI,IAChDyD,EAAI,KAAK;AAAA,eACEL,CAAM;AAAA,yCACoB5B,EAAK,EAAE;AAAA,2DACWkC,CAAK;AAAA;AAAA,aAEnD,CACX,SAAWvD,IAAS,UAAW,CAC7B,IAAMuD,EAAQ1D,EAAM,OAASA,EAAM,KAAK,IAAI,EAAI,iBAChDyD,EAAI,KAAK;AAAA,eACEL,CAAM;AAAA,yCACoB5B,EAAK,EAAE;AAAA,+DACekC,CAAK;AAAA;AAAA,aAEvD,CACX,KAAO,CACL,IAAMA,EAAQ1D,EAAM,OAASA,EAAM,KAAK,IAAI,EAAI,mBAChDyD,EAAI,KAAK;AAAA,eACEL,CAAM;AAAA,yCACoB5B,EAAK,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA,2CAKLkC,CAAK;AAAA;AAAA,aAEnC,CACX,CACF,EAEaC,EAAO,CAACF,EAAKjC,IAAS,CACjC,GAAM,CAAE,KAAArB,EAAM,MAAAH,EAAO,UAAAC,EAAW,MAAAC,CAAM,EAAIJ,EAAS0B,GAAM,MAAQ,EAAE,EAC7DP,EAAU,OAAO,SAAS,OAC1B2C,EAAUH,EAAI,KAAK,aAAa,EAAE,CAAC,EAUzC,GARAA,EAAI,GAAG,WAAY,IAAM,OAAO,KAAK,WAAWA,EAAKjC,CAAI,CAAC,EAE1DiC,EAAI,GAAG,QAAS,YAAa,SAAUN,EAAG,CACxCA,EAAE,eAAe,EACjB,IAAMU,EAAK,EAAE,IAAI,EACjB,OAAO,KAAK,eAAeA,EAAG,KAAK,OAAO,EAAGJ,EAAI,QAAQ,OAAO,EAAGI,EAAG,KAAK,MAAM,CAAC,CACpF,CAAC,EAEG1D,IAAS,OAAQ,CAEnB,IAAM2D,EAAUL,EAAI,KAAK,WAAW,EAAE,CAAC,EACjCM,EAAW/D,EAAM,OAASA,EAAM,KAAK,GAAG,EAAI,KACtC,SAAY,CACtB,GAAI,CACF,IAAMkB,EAAM,GAAGD,CAAM,wCAAwC,mBAAmB8C,CAAQ,CAAC,GACnF5C,EAAM,MAAM,MAAMD,CAAG,EAC3B,GAAI,CAACC,EAAI,GAAI,MAAM,IAAI,MAAM,2BAA2BA,EAAI,MAAM,EAAE,EACpE,IAAM6C,EAAa,MAAM7C,EAAI,KAAK,EAClC,GAAI,CAAC6C,EAAW,OAAQ,CACtBJ,EAAO,YAAc,2BACrB,MACF,CACA,IAAMK,EAAQD,EAAW,MAAM,EAAG9D,CAAK,EACjCgE,EAAaF,EAAW,OAAO,CAACG,EAAGC,IAAMD,GAAKC,EAAE,YAAc,GAAI,CAAC,EACnEC,EAAYJ,EAAM,OAASD,EAAW,OACxC,WAAWC,EAAM,MAAM,OAAOD,EAAW,MAAM,GAC/C,GAAGA,EAAW,MAAM,WACxBJ,EAAO,MAAM,QAAU,OACvBE,EAAQ,UAAY;AAAA;AAAA;AAAA,YAGhBG,EAAM,IAAI,CAAC,CAAE,OAAApC,EAAQ,WAAAyC,CAAW,IAAM;AAAA;AAAA,sDAEI,OAAO,KAAK,KAAKzC,CAAM,EAAE,KAAK,CAAC;AAAA,gCACrDA,CAAM,gBAAgBA,CAAM,MAAMA,CAAM;AAAA,oBACpDyC,GAAc,KAAOA,EAAW,eAAe,EAAI,QAAG;AAAA,kBACxD,EAAE,KAAK,EAAE,CAAC;AAAA;AAAA,+BAEGD,CAAS,WAAMH,EAAW,eAAe,CAAC,YACnE,OAASf,EAAG,CACVS,EAAO,YAAc,UAAUT,EAAE,OAAO,EAC1C,CACF,GACI,CACN,SAAWhD,IAAS,WAEN,SAAY,CACtB,GAAI,CACF,IAAMoE,EAAYd,EAAI,QAAQ,OAAO,EAC/Be,EAAYD,EAAM,KAAK,QAAQ,EAAE,KAAK,EAAE,KAAK,GAAK,SAAS,MAC3DE,EAAgB5D,EAAQ2D,CAAS,EACjCE,EAAgB,OAAO,SAAS,SAEhCnC,EAAgB,MAAMU,EAAkBjD,EAAOiB,CAAM,EACrD0D,EAAQpC,EAAc,OAAO,CAAC4B,EAAGhB,IAAMgB,EAAIhB,EAAE,MAAM,OAAQ,CAAC,EAClES,EAAO,YAAc,aAAae,EAAM,eAAe,CAAC,eAExD,IAAIC,EAAO,MAAM3C,EAAiBwC,EAAaC,CAAa,EAC5D,GAAI,CAACE,EAAM,CACThB,EAAO,YAAc,yCACrB,IAAMiB,EAAWN,EAAM,KAAK,OAAO,EAAE,IAAI,CAACO,EAAGC,IAAO,EAAEA,CAAE,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE,OAAO,OAAO,EAAE,KAAK;AAAA,CAAI,EACxGH,EAAO,MAAM5C,EAAa6C,GAAYL,EAAWvD,CAAM,CACzD,CAEA,IAAM+D,EAAS3C,EAAWuC,EAAMrC,EAAe,CAC7C,UAAAtC,EAAW,MAAAC,EAAO,YAAauE,EAAa,cAAeC,CAC7D,CAAC,EAED,GAAI,CAACM,EAAO,OAAQ,CAClBpB,EAAO,YAAc,0CAA0C3D,CAAS,GACxE,MACF,CACA,IAAMgF,EAAa,CAACjF,EAAM,QAAWA,EAAM,SAAW,GAAKA,EAAM,CAAC,IAAM,IACpE,UACAA,EAAM,SAAW,EAAI,MAAMA,EAAM,CAAC,CAAC,GAAK,aACtC0C,EAAUe,EAAI,KAAK,cAAc,EAAE,CAAC,EAC1Cf,EAAQ,UAAY,6BAClBsC,EAAO,IAAI,CAAC,CAAE,OAAAnD,EAAQ,KAAAK,EAAM,MAAApB,EAAO,MAAAwC,CAAM,IACvC,OAAOC,EAAQ1B,EAAQK,EAAMpB,EAAOwC,CAAK,CAAC,OAAO,EAAE,KAAK,EAAE,CAC9D,6BAA6B0B,EAAO,MAAM,UAAUC,CAAU,OAC9DrB,EAAO,MAAM,QAAU,MACzB,OAAST,EAAG,CACVS,EAAO,YAAc,UAAUT,EAAE,OAAO,EAC1C,CACF,GACI,MACC,CAEL,IAAM+B,EAAUzB,EAAI,KAAK,YAAY,EAAE,CAAC,EAClC0B,EAAU1B,EAAI,KAAK,UAAU,EAAE,CAAC,EAChCf,EAAUe,EAAI,KAAK,cAAc,EAAE,CAAC,EACtClB,EAAgB,MAEJ,SAAY,CAC1B,GAAI,CACFqB,EAAO,YAAc,0BACrBrB,EAAgB,MAAMU,EAAkBjD,EAAOiB,CAAM,EACrD,IAAM0D,EAAQpC,EAAc,OAAO,CAAC4B,EAAGhB,IAAMgB,EAAIhB,EAAE,MAAM,OAAQ,CAAC,EAClES,EAAO,YAAc,gBAAWe,EAAM,eAAe,CAAC,iBAAiBpC,EAAc,MAAM,UAC7F,OAASY,EAAG,CACVS,EAAO,YAAc,eAAeT,EAAE,OAAO,EAC/C,CACF,GACQ,EAER,IAAMiC,EAAW,SAAY,CAC3B,IAAMC,EAAQH,EAAM,MAAM,KAAK,EAC/B,GAAI,GAACG,GAAS,CAAC9C,GACf,CAAA4C,EAAI,SAAW,GACfvB,EAAO,YAAc,wBACrBlB,EAAQ,UAAY,GACpB,GAAI,CACF,IAAMkC,EAAO,MAAM5C,EAAaqD,EAAOpE,CAAM,EACvC+D,EAAS3C,EAAWuC,EAAMrC,EAC9B,CAAE,UAAW,EAAG,MAAArC,EAAO,YAAa,KAAM,cAAe,IAAK,CAAC,EACjEwC,EAAQ,UAAYsC,EAAO,IAAI,CAAC,CAAE,OAAAnD,EAAQ,KAAAK,EAAM,MAAApB,EAAO,MAAAwC,CAAM,IAC3D,2BAA2BC,EAAQ1B,EAAQK,EAAMpB,EAAOwC,CAAK,CAAC,QAAQ,EAAE,KAAK,EAAE,EACjFM,EAAO,YAAc,OAAOoB,EAAO,MAAM,iBAAiBK,CAAK,GACjE,OAASlC,EAAG,CACVS,EAAO,YAAc,UAAUT,EAAE,OAAO,EAC1C,QAAE,CACAgC,EAAI,SAAW,EACjB,EACF,EAEAA,EAAI,iBAAiB,QAASC,CAAQ,EACtCF,EAAM,iBAAiB,UAAW/B,GAAK,CAAMA,EAAE,MAAQ,SAASiC,EAAS,CAAE,CAAC,CAC9E,CACF,EAII,OAAO,OAAW,MACpB,OAAO,QAAU,OAAO,SAAW,CAAC,EACpC,OAAO,QAAQ,WAAa,CAAE,KAAA5B,EAAM,KAAAG,CAAK",
4
+ "sourcesContent": ["// wiki-plugin-similarity\n//\n// One item type: similarity\n//\n// DSL in item.text (newline-separated lines):\n// * all indexed domains on this server\n// david.* glob pattern\n// *.fish glob pattern\n// david.hitchhikers.earth explicit domain name\n// # comment ignored\n// LIST list indexed domains (mode directive)\n// SIMILAR: high threshold preset AND ambient mode trigger\n// THRESHOLD: 0.72 exact cosine threshold (overrides SIMILAR:)\n// LIMIT: 8 max results shown (default 10)\n//\n// Mode is determined by the FIRST meaningful line (Ward's ALL-CAPS convention):\n// LIST \u2192 show a table of all indexed domains and their page counts;\n// optional glob patterns on subsequent lines filter the list\n// SIMILAR: \u2192 ambient mode \u2014 automatically find pages similar to this page\n// (other) \u2192 search form mode \u2014 user types a query, results appear\n//\n// Server endpoints required:\n// GET /system/indexed-domains.json?pattern=glob1,glob2\n// GET /system/semantic-vectors.json?domain=\n// GET /system/embed.json?text=query\n\n// \u2500\u2500 DSL Parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst SIMILAR_THRESHOLDS = { high: 0.78, medium: 0.68, low: 0.58 }\nconst DEFAULT_THRESHOLD = SIMILAR_THRESHOLDS.medium\nconst DEFAULT_LIMIT = 10\n\n// parseDSL returns { mode, specs, threshold, limit }\n//\n// mode: 'similar' if SIMILAR: is the first meaningful line (ambient auto-run),\n// 'search' otherwise (interactive search form).\n//\n// Ward's convention: ALL-CAPS keyword as first word signals a mode switch.\n// Placing SIMILAR: first makes the item ambient; placing domain specs first\n// (or leaving text empty) keeps it as an interactive search form.\n\nconst parseDSL = text => {\n const specs = []\n let threshold = null\n let limit = null\n let mode = 'search' // default: interactive search form\n\n // Match a keyword at the start of a line (case-insensitive), requiring it to\n // be followed by end-of-string, whitespace, or colon \u2014 not by more word chars.\n // This prevents domain specs like \"similarity.example.com\" matching SIMILAR.\n const isCmd = (upper, kw) => upper === kw || (upper.startsWith(kw) && /^[\\s:]/.test(upper.slice(kw.length)))\n // Extract the value after the keyword, tolerating optional colon and whitespace.\n // Returns '' for bare commands (e.g. \"SIMILAR\" alone), callers use their default.\n const val = (line, kw) => line.slice(kw.length).replace(/^\\s*:?\\s*/, '').trim()\n\n for (const raw of text.split('\\n')) {\n const line = raw.trim()\n if (!line || line.startsWith('#')) continue\n\n const upper = line.toUpperCase()\n if (isCmd(upper, 'LIST')) {\n if (!specs.length && mode === 'search') mode = 'list'\n continue\n }\n if (isCmd(upper, 'SIMILAR')) {\n const level = val(upper, 'SIMILAR').toLowerCase()\n threshold = SIMILAR_THRESHOLDS[level] || DEFAULT_THRESHOLD // '' \u2192 medium\n if (!specs.length && mode === 'search') mode = 'similar'\n continue\n }\n if (isCmd(upper, 'THRESHOLD')) {\n threshold = parseFloat(val(line, 'THRESHOLD')) || DEFAULT_THRESHOLD // '' \u2192 medium\n continue\n }\n if (isCmd(upper, 'LIMIT')) {\n limit = parseInt(val(line, 'LIMIT')) || DEFAULT_LIMIT // '' \u2192 10\n continue\n }\n // Anything else is a domain spec (glob or explicit domain)\n specs.push(line)\n }\n\n return {\n mode,\n specs,\n threshold: threshold ?? DEFAULT_THRESHOLD,\n limit: limit ?? DEFAULT_LIMIT,\n }\n}\n\nconst isGlob = spec => spec.includes('*') || spec.includes('?')\n\n// \u2500\u2500 Slug \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst slugify = title => title.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '')\n\n// \u2500\u2500 Domain resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst domainCache = new Map()\n\nconst resolveDomainsForSpec = async (spec, origin) => {\n if (domainCache.has(spec)) return domainCache.get(spec)\n const url = `${origin}/system/indexed-domains.json?pattern=${encodeURIComponent(spec)}`\n const res = await fetch(url)\n if (!res.ok) throw new Error(`indexed-domains failed: ${res.status}`)\n const list = await res.json()\n domainCache.set(spec, list)\n return list\n}\n\nconst resolveDomains = async (specs, origin) => {\n if (!specs.length) specs = [window.location.hostname]\n const seen = new Set()\n const result = []\n for (const spec of specs) {\n if (spec === '*' || isGlob(spec)) {\n for (const item of await resolveDomainsForSpec(spec, origin)) {\n if (!seen.has(item.domain)) { seen.add(item.domain); result.push(item) }\n }\n } else if (!seen.has(spec)) {\n seen.add(spec); result.push({ domain: spec, page_count: null })\n }\n }\n return result\n}\n\n// \u2500\u2500 Vector loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst vectorCache = new Map()\n\n// Detect whether the wiki is running on localhost (or a .localhost subdomain).\n// On localhost we proxy all domain vector fetches through the local server's\n// ?domain= parameter \u2014 this reads vectors directly from the farm folder on disk,\n// avoiding any need for remote servers to have this plugin installed, and\n// sidestepping CORS entirely.\nconst isLocalhost = () => {\n const h = window.location.hostname\n return h === 'localhost' || h.endsWith('.localhost') || h === '127.0.0.1'\n}\n\nconst vectorUrl = domain => {\n if (isLocalhost()) {\n // Route through our local server \u2014 serves any farm domain from disk\n return `${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(domain)}`\n }\n // On a real server, fetch directly from the remote wiki\n return `http://${domain}/system/semantic-vectors.json`\n}\n\nconst loadVectors = async domain => {\n if (vectorCache.has(domain)) return vectorCache.get(domain)\n const res = await fetch(vectorUrl(domain))\n if (!res.ok) return []\n const data = await res.json()\n vectorCache.set(domain, data)\n return data\n}\n\n// \u2500\u2500 Embedding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst getEmbedding = async (text, origin) => {\n const res = await fetch(`${origin}/system/embed.json?text=${encodeURIComponent(text)}`)\n if (!res.ok) throw new Error(`embed failed: ${res.status}`)\n return (await res.json()).vector\n}\n\n// Look up an existing page vector from the current domain's cached index.\n// Returns the float[] vector if found, null otherwise.\nconst lookupPageVector = async (slug, domain) => {\n const pages = await loadVectors(domain)\n const entry = pages.find(p => p.slug === slug)\n return entry ? entry.vector : null\n}\n\n// \u2500\u2500 Cosine search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst cosineScan = (queryVec, domainEntries, { threshold, limit, excludeSlug, excludeDomain }) => {\n const results = []\n for (const { domain, pages } of domainEntries) {\n for (const { slug, title, vector } of pages) {\n if (slug === excludeSlug && domain === excludeDomain) continue\n let dot = 0\n for (let i = 0; i < queryVec.length; i++) dot += queryVec[i] * vector[i]\n if (dot >= threshold) results.push({ domain, slug, title, score: dot })\n }\n }\n results.sort((a, b) => b.score - a.score)\n return results.slice(0, limit)\n}\n\n// \u2500\u2500 Shared domain loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst loadDomainEntries = async (specs, origin) => {\n const domains = await resolveDomains(specs, origin)\n const entries = await Promise.all(\n domains.map(async ({ domain }) => ({ domain, pages: await loadVectors(domain) }))\n )\n return entries.filter(e => e.pages.length > 0)\n}\n\n// \u2500\u2500 Styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst STYLES = `\n .sim-form { display:flex; gap:6px; margin-bottom:8px; }\n .sim-input { flex:1; padding:6px 8px; font-size:14px; border:1px solid #ccc; border-radius:3px; }\n .sim-btn { padding:6px 14px; background:#c4561d; color:white; border:none;\n border-radius:3px; cursor:pointer; font-size:14px; }\n .sim-btn:disabled { opacity:0.5; cursor:not-allowed; }\n .sim-status { font-size:12px; color:#888; margin-bottom:6px; min-height:16px; }\n .sim-results { margin-top:4px; }\n .sim-result { display:flex; align-items:center; gap:8px; padding:3px 0;\n border-bottom:1px solid #f0f0f0; }\n .sim-flag { width:16px; height:16px; vertical-align:middle; margin-right:4px; }\n .sim-link { font-size:14px; color:#406; flex:1; }\n .sim-domain { font-size:11px; color:#999; }\n .similar-results h3, .sim-list h3 { margin:4px 0 6px; font-size:14px; color:#555; }\n .similar-results ul { margin:0; padding-left:18px; }\n .similar-results li { font-size:14px; padding:2px 0; }\n .similar-results .sim-domain { margin-left:6px; }\n .sim-count { font-size:12px; color:#888; margin:4px 0 0; }\n .sim-list table { border-collapse:collapse; width:100%; font-size:13px; }\n .sim-list th { text-align:left; font-size:11px; color:#888; padding:2px 8px 4px 0;\n border-bottom:1px solid #ddd; }\n .sim-list td { padding:3px 8px 3px 0; border-bottom:1px solid #f0f0f0; vertical-align:middle; }\n .sim-list td:last-child { text-align:right; color:#999; font-size:11px; }\n .sim-list .sim-flag { margin-right:6px; }\n`\n\nconst siteFlag = (domain, score) =>\n `<img class=\"sim-flag remote\" src=\"${window.wiki.site(domain).flag()}\"\n title=\"${domain} \u2014 score ${score.toFixed(3)}\"\n data-site=\"${domain}\">`\n\n// \u2500\u2500 similarity item \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst simLink = (domain, slug, title, score) =>\n `<a class=\"sim-link\" data-title=\"${title}\" data-slug=\"${slug}\" data-site=\"${domain}\" href=\"#\">` +\n `${siteFlag(domain, score)} ${title}</a>`\n\nexport const emit = (div, item) => {\n const { mode, specs, threshold, limit } = parseDSL(item?.text || '')\n if (mode === 'list') {\n const label = specs.length ? specs.join(', ') : '*'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-status\">Loading indexed domains (${label})\u2026</div>\n <div class=\"sim-list\"></div>\n </div>`)\n } else if (mode === 'similar') {\n const label = specs.length ? specs.join(', ') : 'current domain'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-status\">Finding similar pages across ${label}\u2026</div>\n <div class=\"sim-results\"></div>\n </div>`)\n } else {\n const label = specs.length ? specs.join(', ') : '(current domain)'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-form\">\n <input class=\"sim-input\" type=\"text\" placeholder=\"Search wiki pages\u2026\" />\n <button class=\"sim-btn\">Search</button>\n </div>\n <div class=\"sim-status\">Domains: ${label}</div>\n <div class=\"sim-results\"></div>\n </div>`)\n }\n}\n\nexport const bind = (div, item) => {\n const { mode, specs, threshold, limit } = parseDSL(item?.text || '')\n const origin = window.location.origin\n const status = div.find('.sim-status')[0]\n\n div.on('dblclick', () => window.wiki.textEditor(div, item))\n\n div.on('click', '.sim-link', function (e) {\n e.preventDefault()\n const $a = $(this)\n window.wiki.doInternalLink($a.data('title'), div.parents('.page'), $a.data('site'))\n })\n\n if (mode === 'list') {\n // List all indexed domains matching the spec patterns (default: all)\n const listDiv = div.find('.sim-list')[0]\n const patterns = specs.length ? specs.join(',') : '*'\n const run = async () => {\n try {\n const url = `${origin}/system/indexed-domains.json?pattern=${encodeURIComponent(patterns)}&limit=${limit}`\n const res = await fetch(url)\n if (!res.ok) throw new Error(`indexed-domains failed: ${res.status}`)\n const allDomains = await res.json()\n if (!allDomains.length) {\n status.textContent = 'No indexed domains found'\n return\n }\n const shown = allDomains.slice(0, limit)\n const totalPages = allDomains.reduce((n, d) => n + (d.page_count || 0), 0)\n const countNote = shown.length < allDomains.length\n ? `showing ${shown.length} of ${allDomains.length}`\n : `${allDomains.length} domains`\n status.style.display = 'none'\n listDiv.innerHTML = `<h3>Indexed Farm Domains</h3>\n <table>\n <tr><th>Domain</th><th>Pages</th></tr>\n ${shown.map(({ domain, page_count }) => `\n <tr>\n <td><img class=\"sim-flag remote\" src=\"${window.wiki.site(domain).flag()}\"\n title=\"${domain}\" data-site=\"${domain}\"> ${domain}</td>\n <td>${page_count != null ? page_count.toLocaleString() : '\u2014'}</td>\n </tr>`).join('')}\n </table>\n <p class=\"sim-count\">${countNote} \u2014 ${totalPages.toLocaleString()} pages</p>`\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n }\n }\n run()\n } else if (mode === 'similar') {\n // Ambient mode \u2014 run automatically, no search form\n const run = async () => {\n try {\n const $page = div.parents('.page')\n const pageTitle = $page.find('.title').text().trim() || document.title\n const currentSlug = slugify(pageTitle)\n const currentDomain = window.location.hostname\n\n const domainEntries = await loadDomainEntries(specs, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = `Searching ${total.toLocaleString()} pages\u2026`\n\n let qVec = await lookupPageVector(currentSlug, currentDomain)\n if (!qVec) {\n status.textContent = 'Embedding page (not yet indexed)\u2026'\n const pageText = $page.find('.item').map((_, el) => $(el).text().trim()).get().filter(Boolean).join('\\n')\n qVec = await getEmbedding(pageText || pageTitle, origin)\n }\n\n const scored = cosineScan(qVec, domainEntries, {\n threshold, limit, excludeSlug: currentSlug, excludeDomain: currentDomain,\n })\n\n if (!scored.length) {\n status.textContent = `No similar pages found above threshold ${threshold}`\n return\n }\n const scopeLabel = !specs.length || (specs.length === 1 && specs[0] === '*')\n ? 'on farm'\n : specs.length === 1 ? `on ${specs[0]}` : 'in domains'\n const results = div.find('.sim-results')[0]\n results.innerHTML = `<h3>Similar Pages</h3><ul>${\n scored.map(({ domain, slug, title, score }) =>\n `<li>${simLink(domain, slug, title, score)}</li>`).join('')\n }</ul><p class=\"sim-count\">${scored.length} found ${scopeLabel}</p>`\n status.style.display = 'none'\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n }\n }\n run()\n } else {\n // Search form mode \u2014 preload domains, then wait for user input\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n const results = div.find('.sim-results')[0]\n let domainEntries = null\n\n const preload = async () => {\n try {\n status.textContent = 'Resolving domains\u2026'\n domainEntries = await loadDomainEntries(specs, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = `Ready \u2014 ${total.toLocaleString()} pages across ${domainEntries.length} domains`\n } catch (e) {\n status.textContent = `Load error: ${e.message}`\n }\n }\n preload()\n\n const doSearch = async () => {\n const query = input.value.trim()\n if (!query || !domainEntries) return\n btn.disabled = true\n status.textContent = 'Embedding query\u2026'\n results.innerHTML = ''\n try {\n const qVec = await getEmbedding(query, origin)\n const scored = cosineScan(qVec, domainEntries,\n { threshold: 0, limit, excludeSlug: null, excludeDomain: null })\n results.innerHTML = scored.map(({ domain, slug, title, score }) =>\n `<div class=\"sim-result\">${simLink(domain, slug, title, score)}</div>`).join('')\n status.textContent = `Top ${scored.length} results for \"${query}\"`\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doSearch)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch() })\n }\n}\n\n// \u2500\u2500 Register item type with the wiki \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nif (typeof window !== 'undefined') {\n window.plugins = window.plugins || {}\n window.plugins.similarity = { emit, bind }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["SIMILAR_THRESHOLDS", "DEFAULT_THRESHOLD", "DEFAULT_LIMIT", "parseDSL", "text", "specs", "threshold", "limit", "mode", "isCmd", "upper", "kw", "val", "line", "raw", "level", "isGlob", "spec", "slugify", "title", "domainCache", "resolveDomainsForSpec", "origin", "url", "res", "list", "resolveDomains", "seen", "result", "item", "vectorCache", "isLocalhost", "h", "vectorUrl", "domain", "loadVectors", "data", "getEmbedding", "lookupPageVector", "slug", "entry", "p", "cosineScan", "queryVec", "domainEntries", "excludeSlug", "excludeDomain", "results", "pages", "vector", "dot", "i", "a", "b", "loadDomainEntries", "domains", "e", "STYLES", "siteFlag", "score", "simLink", "emit", "div", "label", "bind", "status", "$a", "listDiv", "patterns", "allDomains", "shown", "totalPages", "n", "d", "countNote", "page_count", "$page", "pageTitle", "currentSlug", "currentDomain", "total", "qVec", "pageText", "_", "el", "scored", "scopeLabel", "input", "btn", "doSearch", "query"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-plugin-similarity",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Federated Wiki plugin — semantic similarity search across wiki domains using prebuilt vector indexes",
5
5
  "keywords": [
6
6
  "wiki",
package/server/server.js CHANGED
@@ -157,9 +157,11 @@ export const startServer = ({ argv, app }) => {
157
157
  // ── GET /system/indexed-domains.json?pattern=glob1,glob2 ──────────────────
158
158
  app.get('/system/indexed-domains.json', (req, res) => {
159
159
  cors(res)
160
- const raw = req.query.pattern || '*'
160
+ const raw = req.query.pattern || '*'
161
161
  const patterns = raw.split(',').map(p => p.trim()).filter(Boolean)
162
- const results = findIndexedDomains(farmRoot, patterns)
162
+ const limit = parseInt(req.query.limit) || null
163
+ let results = findIndexedDomains(farmRoot, patterns)
164
+ if (limit) results = results.slice(0, limit)
163
165
  res.json(results)
164
166
  })
165
167