wiki-plugin-similarity 0.11.0 → 0.12.0

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,7 +1,7 @@
1
- /* wiki-plugin-similarity - 0.11.0 - Tue, 28 Jul 2026 23:22:02 GMT */
2
- (()=>{var B={high:.78,medium:.68,low:.58},M=B.medium,K=10,W=t=>{let e=[],s=[],i=[],g=null,S=null,f="search",y=!1,L=!1,b=!1,k=null,C=null,E=(a,r)=>a===r||a.startsWith(r)&&/^[\s:]/.test(a.slice(r.length)),w=(a,r)=>a.slice(r.length).replace(/^\s*:?\s*/,"").trim();for(let a of t.split(`
3
- `)){let r=a.trim();if(!r||r.startsWith("#"))continue;let p=r.toUpperCase();if(E(p,"LIVE")){y=!0;continue}if(E(p,"SUBJECT")){L=!0;continue}if(E(p,"AUTHOR")){!e.length&&f==="search"&&(f="author");continue}if(E(p,"REPORT")){f==="search"&&(f="report");continue}if(E(p,"KEYWORD")){f==="search"&&(f="keyword");continue}if(E(p,"SITES")){f==="search"&&(f="sites");continue}if(E(p,"BUILD")){f==="search"&&(f="build");continue}if(E(p,"FORCE")){b=!0;continue}if(E(p,"GHOST")){k=w(r,"GHOST"),f==="search"&&(f="ghost");continue}if(E(p,"BUTTON")){C=w(r,"BUTTON");continue}if(E(p,"ROSTER")){let v=w(r,"ROSTER");v&&s.push(v);continue}if(E(p,"FARM")){let v=w(r,"FARM");v&&i.push(v);continue}if(E(p,"LIST")){!e.length&&f==="search"&&(f="list");continue}if(E(p,"SIMILAR")){let v=w(p,"SIMILAR").toLowerCase();g=B[v]||M,!e.length&&f==="search"&&(f="similar");continue}if(E(p,"THRESHOLD")){let v=w(r,"THRESHOLD");g=B[v.toLowerCase()]??(parseFloat(v)||M);continue}if(E(p,"LIMIT")){S=parseInt(w(r,"LIMIT"))||K;continue}e.push(["PUBLIC","LOCAL","PRIVATE","GALAXY"].includes(p)?p:r)}return{mode:f,specs:e,rosterRefs:s,farms:i,threshold:g??M,limit:S??K,live:y,subject:L,force:b,ghostUrl:k,label:C,thresholdSet:g!==null}},ot=t=>t.includes("*")||t.includes("?"),at=t=>t==="PUBLIC"||t==="LOCAL"||t==="PRIVATE"||t==="GALAXY",rt=t=>t.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,""),z=new Map,lt=async(t,e)=>{if(z.has(t))return z.get(t);let s=`${e}/system/indexed-domains.json?pattern=${encodeURIComponent(t)}`,i=await fetch(s);if(!i.ok)throw new Error(`indexed-domains failed: ${i.status}`);let g=await i.json();return z.set(t,g),g},G=async(t,e)=>{t.length||(t=[window.location.hostname]);let s=new Set,i=[];for(let g of t)if(g==="*"||ot(g)||at(g))for(let S of await lt(g,e))s.has(S.domain)||(s.add(S.domain),i.push(S));else s.has(g)||(s.add(g),i.push({domain:g,page_count:null}));return i},ct=/^([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+|localhost)(:\d+)?$/,dt=/^ROSTER ([A-Za-z0-9.\-:]+\/[a-z0-9-]+)$/,ft=/^REFERENCES ([A-Za-z0-9.\-:]+\/[a-z0-9-]+)$/,J=async t=>{let e=t.indexOf("/"),s=await fetch(`//${t.slice(0,e)}/${t.slice(e+1)}.json`);if(!s.ok)throw new Error(`roster page ${t} failed: ${s.status}`);return s.json()},Y=async(t,e,s)=>{if(s.has(t))return;s.add(t);let i;try{i=await J(t)}catch(g){console.warn(g.message);return}for(let g of i.story||[])if(g.type==="roster")for(let S of(g.text||"").split(/\r?\n/)){let f=S.trim();if(!f)continue;let y=f.match(ct);if(y){e.add(y[0]);continue}let L=f.match(dt);if(L){await Y(L[1],e,s);continue}let b=f.match(ft);if(b)try{let k=await J(b[1]);for(let C of k.story||[])C.type==="reference"&&C.site&&e.add(C.site)}catch(k){console.warn(k.message)}}},ut=async t=>{let e=new Set,s=new Set;for(let i of t)await Y(i,e,s);return[...e]},mt=async(t,e)=>e.length?[...t,...await ut(e)]:t,H=new Map,gt=t=>`${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(t)}`,Z=async t=>{if(H.has(t))return H.get(t);let e=await fetch(gt(t));if(!e.ok)return[];let s=await e.json();return H.set(t,s),s},F=async(t,e)=>{let s=await fetch(`${e}/system/embed.json`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text:t})});if(!s.ok)throw new Error(`embed failed: ${s.status}`);return(await s.json()).vector},pt=async(t,e)=>{let i=(await Z(e)).find(g=>g.slug===t);return i?i.vector:null},ht=t=>{let e=t.parents(".page:first"),s=e.prev(".page"),i=s.length?s:e,g=(i.attr("id")||"").split("_rev")[0],S=i.data("site")||window.location.hostname,f=i.data("data")||{},y=f.title||i.find(".title").text().trim()||g,L=[y,...(f.story||[]).filter(b=>b.type==="markdown"||b.type==="paragraph").map(b=>(b.text||"").trim()).filter(Boolean)].join(`
4
- `).slice(0,2e3);return{slug:g,site:S,title:y,text:L,isSelf:!s.length}},I=t=>`Subject: ${t.title}${t.isSelf?" (this page)":` @ ${t.site}`}`,N=(t,e,{threshold:s,limit:i,excludeSlug:g,excludeDomain:S})=>{let f=[];for(let{domain:y,pages:L}of e)for(let{slug:b,title:k,vector:C}of L){if(b===g&&y===S)continue;let E=0;for(let w=0;w<t.length;w++)E+=t[w]*C[w];E>=s&&f.push({domain:y,slug:b,title:k,score:E})}return f.sort((y,L)=>L.score-y.score),f.slice(0,i)},q=async(t,e)=>{let s=await G(t,e);return(await Promise.all(s.map(async({domain:g})=>({domain:g,pages:await Z(g)})))).filter(g=>g.pages.length>0)},X=t=>`sim-cache-${t}`,wt=t=>{try{let e=JSON.parse(localStorage.getItem(X(t.id))||"null");return e?.text===(t.text||"")?e:null}catch{return null}},P=(t,e)=>{try{localStorage.setItem(X(t.id),JSON.stringify({text:t.text||"",ts:Date.now(),...e}))}catch{}},yt=t=>{let e=Math.floor((Date.now()-t)/1e3);return e<60?`${e}s ago`:e<3600?`${Math.floor(e/60)}m ago`:e<86400?`${Math.floor(e/3600)}h ago`:`${Math.floor(e/86400)}d ago`},O=`
1
+ /* wiki-plugin-similarity - 0.12.0 - Thu, 30 Jul 2026 07:42:20 GMT */
2
+ (()=>{var q={high:.78,medium:.68,low:.58},N=q.medium,J=10,_=t=>{let e=[],s=[],i=[],p=null,E=null,f="search",y=!1,L=!1,b=!1,k=null,C=null,S=(a,r)=>a===r||a.startsWith(r)&&/^[\s:]/.test(a.slice(r.length)),w=(a,r)=>a.slice(r.length).replace(/^\s*:?\s*/,"").trim();for(let a of t.split(`
3
+ `)){let r=a.trim();if(!r||r.startsWith("#"))continue;let g=r.toUpperCase();if(S(g,"LIVE")){y=!0;continue}if(S(g,"SUBJECT")){L=!0;continue}if(S(g,"AUTHOR")){!e.length&&f==="search"&&(f="author");continue}if(S(g,"REPORT")){f==="search"&&(f="report");continue}if(S(g,"KEYWORD")){f==="search"&&(f="keyword");continue}if(S(g,"SITES")){f==="search"&&(f="sites");continue}if(S(g,"BUILD")){f==="search"&&(f="build");continue}if(S(g,"FORCE")){b=!0;continue}if(S(g,"GHOST")){k=w(r,"GHOST"),f==="search"&&(f="ghost");continue}if(S(g,"BUTTON")){C=w(r,"BUTTON");continue}if(S(g,"ROSTER")){let v=w(r,"ROSTER");v&&s.push(v);continue}if(S(g,"FARM")){let v=w(r,"FARM");v&&i.push(v);continue}if(S(g,"LIST")){!e.length&&f==="search"&&(f="list");continue}if(S(g,"SIMILAR")){let v=w(g,"SIMILAR").toLowerCase();p=q[v]||N,!e.length&&f==="search"&&(f="similar");continue}if(S(g,"THRESHOLD")){let v=w(r,"THRESHOLD");p=q[v.toLowerCase()]??(parseFloat(v)||N);continue}if(S(g,"LIMIT")){E=parseInt(w(r,"LIMIT"))||J;continue}e.push(["PUBLIC","LOCAL","PRIVATE","GALAXY"].includes(g)?g:r)}return{mode:f,specs:e,rosterRefs:s,farms:i,threshold:p??N,limit:E??J,live:y,subject:L,force:b,ghostUrl:k,label:C,thresholdSet:p!==null}},Y=t=>t.includes("*")||t.includes("?"),W=t=>t==="PUBLIC"||t==="LOCAL"||t==="PRIVATE"||t==="GALAXY";var X=t=>t.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,""),B=new Map,pt=async(t,e)=>{if(B.has(t))return B.get(t);let s=`${e}/system/indexed-domains.json?pattern=${encodeURIComponent(t)}`,i=await fetch(s);if(!i.ok)throw new Error(`indexed-domains failed: ${i.status}`);let p=await i.json();return B.set(t,p),p},O=async(t,e)=>{t.length||(t=[window.location.hostname]);let s=new Set,i=[];for(let p of t)if(p==="*"||Y(p)||W(p))for(let E of await pt(p,e))s.has(E.domain)||(s.add(E.domain),i.push(E));else s.has(p)||(s.add(p),i.push({domain:p,page_count:null}));return i},gt=/^([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+|localhost)(:\d+)?$/,ht=/^ROSTER ([A-Za-z0-9.\-:]+\/[a-z0-9-]+)$/,wt=/^REFERENCES ([A-Za-z0-9.\-:]+\/[a-z0-9-]+)$/,Z=async t=>{let e=t.indexOf("/"),s=await fetch(`//${t.slice(0,e)}/${t.slice(e+1)}.json`);if(!s.ok)throw new Error(`roster page ${t} failed: ${s.status}`);return s.json()},Q=async(t,e,s)=>{if(s.has(t))return;s.add(t);let i;try{i=await Z(t)}catch(p){console.warn(p.message);return}for(let p of i.story||[])if(p.type==="roster")for(let E of(p.text||"").split(/\r?\n/)){let f=E.trim();if(!f)continue;let y=f.match(gt);if(y){e.add(y[0]);continue}let L=f.match(ht);if(L){await Q(L[1],e,s);continue}let b=f.match(wt);if(b)try{let k=await Z(b[1]);for(let C of k.story||[])C.type==="reference"&&C.site&&e.add(C.site)}catch(k){console.warn(k.message)}}},yt=async t=>{let e=new Set,s=new Set;for(let i of t)await Q(i,e,s);return[...e]},tt=async(t,e)=>e.length?[...t,...await yt(e)]:t;var G=new Map,$t=t=>`${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(t)}`,et=async t=>{if(G.has(t))return G.get(t);let e=await fetch($t(t));if(!e.ok)return[];let s=await e.json();return G.set(t,s),s},U=async(t,e)=>{let s=await fetch(`${e}/system/embed.json`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text:t})});if(!s.ok)throw new Error(`embed failed: ${s.status}`);return(await s.json()).vector},st=async(t,e)=>{let i=(await et(e)).find(p=>p.slug===t);return i?i.vector:null},M=(t,e,{threshold:s,limit:i,excludeSlug:p,excludeDomain:E})=>{let f=[];for(let{domain:y,pages:L}of e)for(let{slug:b,title:k,vector:C}of L){if(b===p&&y===E)continue;let S=0;for(let w=0;w<t.length;w++)S+=t[w]*C[w];S>=s&&f.push({domain:y,slug:b,title:k,score:S})}return f.sort((y,L)=>L.score-y.score),f.slice(0,i)},z=async(t,e)=>{let s=await O(t,e);return(await Promise.all(s.map(async({domain:p})=>({domain:p,pages:await et(p)})))).filter(p=>p.pages.length>0)};var nt=t=>{let e=t.parents(".page:first"),s=e.prev(".page"),i=s.length?s:e,p=(i.attr("id")||"").split("_rev")[0],E=i.data("site")||window.location.hostname,f=i.data("data")||{},y=f.title||i.find(".title").text().trim()||p,L=[y,...(f.story||[]).filter(b=>b.type==="markdown"||b.type==="paragraph").map(b=>(b.text||"").trim()).filter(Boolean)].join(`
4
+ `).slice(0,2e3);return{slug:p,site:E,title:y,text:L,isSelf:!s.length}},T=t=>`Subject: ${t.title}${t.isSelf?" (this page)":` @ ${t.site}`}`;var it=t=>`sim-cache-${t}`,ot=t=>{try{let e=JSON.parse(localStorage.getItem(it(t.id))||"null");return e?.text===(t.text||"")?e:null}catch{return null}},P=(t,e)=>{try{localStorage.setItem(it(t.id),JSON.stringify({text:t.text||"",ts:Date.now(),...e}))}catch{}},at=t=>{let e=Math.floor((Date.now()-t)/1e3);return e<60?`${e}s ago`:e<3600?`${Math.floor(e/60)}m ago`:e<86400?`${Math.floor(e/3600)}h ago`:`${Math.floor(e/86400)}d ago`};var D=`
5
5
  .sim-form { display:flex; gap:6px; margin-bottom:8px; }
6
6
  .sim-input { flex:1; padding:6px 8px; font-size:14px; border:1px solid #ccc; border-radius:3px; }
7
7
  .sim-btn { padding:6px 14px; background:#c4561d; color:white; border:none;
@@ -25,34 +25,34 @@
25
25
  .sim-list td { padding:3px 8px 3px 0; border-bottom:1px solid #f0f0f0; vertical-align:middle; }
26
26
  .sim-list td:last-child { text-align:right; color:#999; font-size:11px; }
27
27
  .sim-list .sim-flag { margin-right:6px; }
28
- `,$t=(t,e)=>`<img class="sim-flag remote" src="${window.wiki.site(t).flag()}"
28
+ `,rt=(t,e)=>`<img class="sim-flag remote" src="${window.wiki.site(t).flag()}"
29
29
  title="${t} \u2014 score ${e.toFixed(3)}"
30
- data-site="${t}">`,_=(t,e,s,i)=>`<a class="sim-link" data-title="${s}" data-slug="${e}" data-site="${t}" href="#">${$t(t,i)} ${s}</a>`,xt=(t,e)=>{let{mode:s,specs:i,threshold:g,limit:S,force:f,ghostUrl:y,label:L}=W(e?.text||"");if(s==="ghost")t.html(`
31
- <style>${O}</style>
30
+ data-site="${t}">`;var V=(t,e,s,i)=>`<a class="sim-link" data-title="${s}" data-slug="${e}" data-site="${t}" href="#">${rt(t,i)} ${s}</a>`,xt=(t,e)=>{let{mode:s,specs:i,threshold:p,limit:E,force:f,ghostUrl:y,label:L}=_(e?.text||"");if(s==="ghost")t.html(`
31
+ <style>${D}</style>
32
32
  <div class="similarity" data-id="${e.id}">
33
33
  <div class="sim-form">
34
34
  <button class="sim-btn">${L||"Open"}</button>
35
35
  </div>
36
36
  <div class="sim-status"></div>
37
37
  </div>`);else if(s==="build")t.html(`
38
- <style>${O}</style>
38
+ <style>${D}</style>
39
39
  <div class="similarity" data-id="${e.id}">
40
40
  <div class="sim-form">
41
41
  <button class="sim-btn">${L||`Index ${i.length?i.join(", "):"*"}${f?" (force)":""}`}</button>
42
42
  </div>
43
43
  <div class="sim-status"></div>
44
44
  </div>`);else if(s==="list"){let b=i.length?i.join(", "):"*";t.html(`
45
- <style>${O}</style>
45
+ <style>${D}</style>
46
46
  <div class="similarity" data-id="${e.id}">
47
47
  <div class="sim-status">Loading indexed domains (${b})\u2026</div>
48
48
  <div class="sim-list"></div>
49
49
  </div>`)}else if(s==="similar"){let b=i.length?i.join(", "):"current domain";t.html(`
50
- <style>${O}</style>
50
+ <style>${D}</style>
51
51
  <div class="similarity" data-id="${e.id}">
52
52
  <div class="sim-status">Finding similar pages across ${b}\u2026</div>
53
53
  <div class="sim-results"></div>
54
54
  </div>`)}else if(s==="author"||s==="report"||s==="keyword"||s==="sites"){let b=i.length?i.join(", "):"(current domain)",k=s==="report"?"Report":s==="keyword"?"Keyword":s==="sites"?"Sites":"Author",C=s==="sites"?"Where should this page go? Title + first paragraph\u2026":"Search wiki pages\u2026";t.html(`
55
- <style>${O}</style>
55
+ <style>${D}</style>
56
56
  <div class="similarity" data-id="${e.id}">
57
57
  <div class="sim-form">
58
58
  <input class="sim-input" type="text" placeholder="${C}" />
@@ -61,7 +61,7 @@
61
61
  <div class="sim-status">Domains: ${b}</div>
62
62
  <div class="sim-results"></div>
63
63
  </div>`)}else{let b=i.length?i.join(", "):"(current domain)";t.html(`
64
- <style>${O}</style>
64
+ <style>${D}</style>
65
65
  <div class="similarity" data-id="${e.id}">
66
66
  <div class="sim-form">
67
67
  <input class="sim-input" type="text" placeholder="Search wiki pages\u2026" />
@@ -69,7 +69,7 @@
69
69
  </div>
70
70
  <div class="sim-status">Domains: ${b}</div>
71
71
  <div class="sim-results"></div>
72
- </div>`)}},bt=(t,e)=>{let{mode:s,specs:i,rosterRefs:g,farms:S,threshold:f,limit:y,live:L,subject:b,force:k,ghostUrl:C,thresholdSet:E}=W(e?.text||""),w=window.location.origin,a=t.find(".sim-status")[0],r=b?ht(t):null,p=L||r?null:wt(e),v=mt(i,g),V=()=>r?{seed:{site:r.site,slug:r.slug},text:r.text,excludePage:{site:r.site,slug:r.slug}}:{},D=(c,h,o)=>{let l=[`${c} \u2014 ${h.toLocaleString()} pages across ${o} domains`];return E&&l.push(`threshold ${f}`),l.push(`limit ${y}`),L&&l.push("LIVE"),l.join(" \xB7 ")};t.on("dblclick",c=>{$(c.target).closest(".sim-input").length||window.wiki.textEditor(t,e)}),t.on("click",".sim-link",function(c){c.preventDefault();let h=$(this),o=c.shiftKey?null:t.parents(".page");window.wiki.doInternalLink(h.data("title"),o,h.data("site"))});let Q=!i.length||i.length===1&&i[0]==="*"?"on farm":i.length===1?`on ${i[0]}`:"in domains",A=c=>c?` \xB7 cached ${yt(c)}`:"";if(s==="list"){let c=t.find(".sim-list")[0],h=(o,l)=>{let d=o.reduce((n,u)=>n+(u.page_count||0),0);a.style.display="none",c.innerHTML=`<h3>Indexed Farm Domains</h3>
72
+ </div>`)}},bt=(t,e)=>{let{mode:s,specs:i,rosterRefs:p,farms:E,threshold:f,limit:y,live:L,subject:b,force:k,ghostUrl:C,thresholdSet:S}=_(e?.text||""),w=window.location.origin,a=t.find(".sim-status")[0],r=b?nt(t):null,g=L||r?null:ot(e),v=tt(i,p),K=()=>r?{seed:{site:r.site,slug:r.slug},text:r.text,excludePage:{site:r.site,slug:r.slug}}:{},A=(c,h,o)=>{let l=[`${c} \u2014 ${h.toLocaleString()} pages across ${o} domains`];return S&&l.push(`threshold ${f}`),l.push(`limit ${y}`),L&&l.push("LIVE"),l.join(" \xB7 ")};t.on("dblclick",c=>{$(c.target).closest(".sim-input").length||window.wiki.textEditor(t,e)}),t.on("click",".sim-link",function(c){c.preventDefault();let h=$(this),o=c.shiftKey?null:t.parents(".page");window.wiki.doInternalLink(h.data("title"),o,h.data("site"))});let lt=!i.length||i.length===1&&i[0]==="*"?"on farm":i.length===1?`on ${i[0]}`:"in domains",F=c=>c?` \xB7 cached ${at(c)}`:"";if(s==="list"){let c=t.find(".sim-list")[0],h=(o,l)=>{let d=o.reduce((n,u)=>n+(u.page_count||0),0);a.style.display="none",c.innerHTML=`<h3>Indexed Farm Domains</h3>
73
73
  <table>
74
74
  <tr><th>Domain</th><th>Pages</th></tr>
75
75
  ${o.map(({domain:n,page_count:u})=>`
@@ -79,9 +79,9 @@
79
79
  <td>${u!=null?u.toLocaleString():"\u2014"}</td>
80
80
  </tr>`).join("")}
81
81
  </table>
82
- <p class="sim-count">${o.length} domains \u2014 ${d.toLocaleString()} pages${A(l)}</p>`};p?.domains?h(p.domains,p.ts):(async()=>{try{let o=await v,l=o.length?o.join(","):"*",d=`${w}/system/indexed-domains.json?pattern=${encodeURIComponent(l)}&limit=${y}`,n=await fetch(d);if(!n.ok)throw new Error(`indexed-domains failed: ${n.status}`);let u=await n.json();if(!u.length){a.textContent="No indexed domains found";return}h(u,null),P(e,{domains:u})}catch(o){a.textContent=`Error: ${o.message}`}})()}else if(s==="similar"){let c=t.find(".sim-results")[0],h=(o,l)=>{if(!o.length){a.textContent=`No similar pages found above threshold ${f}`;return}c.innerHTML=`<h3>Similar Pages</h3><ul>${o.map(({domain:d,slug:n,title:u,score:m})=>`<li>${_(d,n,u,m)}</li>`).join("")}</ul><p class="sim-count">${o.length} found ${Q}${A(l)}</p>`,a.style.display="none"};p?.scored?h(p.scored,p.ts):(async()=>{try{let o=t.parents(".page"),l=r||(()=>{let x=o.find(".title").text().trim()||document.title;return{slug:rt(x),site:window.location.hostname,title:x,text:null,isSelf:!0}})(),d=await q(await v,w),n=d.reduce((x,j)=>x+j.pages.length,0);a.textContent=(r?`${I(l)} \xB7 `:"")+`Searching ${n.toLocaleString()} pages\u2026`;let u=await pt(l.slug,l.site);if(!u){a.textContent="Embedding page (not yet indexed)\u2026";let x=l.text||o.find(".item").map((j,R)=>$(R).text().trim()).get().filter(Boolean).join(`
83
- `);u=await F(x||l.title,w)}let m=N(u,d,{threshold:f,limit:y,excludeSlug:l.slug,excludeDomain:l.site});h(m,null),r&&(a.textContent=I(l),a.style.display=""),m.length&&!r&&P(e,{scored:m})}catch(o){a.textContent=`Error: ${o.message}`}})()}else if(s==="ghost"){let c=t.find(".sim-btn")[0],h=async()=>{if(!C){a.textContent="No URL \u2014 GHOST needs a page-json URL";return}c.disabled=!0,a.textContent="Fetching\u2026";try{let o=await fetch(C);if(!o.ok)throw new Error(`fetch failed: ${o.status}`);let l=await o.json();window.wiki.showResult(window.wiki.newPage(l),{$page:t.parents(".page")}),a.textContent=""}catch(o){a.textContent=`Error: ${o.message}`}finally{c.disabled=!1}};c.addEventListener("click",h)}else if(s==="build"){let c=t.find(".sim-btn")[0],h=async()=>{c.disabled=!0,a.textContent="Building index\u2026 (may take a while for large scopes)";try{let o=encodeURIComponent((i.length?i:["*"]).join(",")),l=await fetch(`${w}/system/build-index.json?domains=${o}&force=${k?1:0}`);if(l.status===501){let n=await l.json().catch(()=>({}));throw new Error(n.hint||"indexing runs on the farm indexer, not this server")}if(!l.ok)throw new Error(`build-index failed: ${l.status}`);let d=await l.json();window.wiki.showResult(window.wiki.newPage(d),{$page:t.parents(".page")}),a.textContent=""}catch(o){a.textContent=`Error: ${o.message}`}finally{c.disabled=!1}};c.addEventListener("click",h)}else if(s==="report"){let c=t.find(".sim-input")[0],h=t.find(".sim-btn")[0],o=`Domains: ${i.length?i.join(", "):"*"}`;r&&!c.value&&(c.value=r.title),(async()=>{try{let d=await v,n=await G(d.length?d:["*"],w),u=n.reduce((m,x)=>m+(x.page_count||0),0);o=D("Report ready",u,n.length),S.length&&(o+=` \xB7 +${S.length} peer farm${S.length>1?"s":""}`),r&&(o=`${I(r)} \xB7 ${o}`),a.textContent=o}catch(d){a.textContent=`Domain listing unavailable: ${d.message}`}})();let l=async()=>{let d=c.value.trim();if(d){h.disabled=!0,a.textContent=S.length?"Generating report (asking peer farms)\u2026":"Generating report\u2026";try{let n=await v,u=r&&d===r.title?V():{},m={query:d,domains:n.length?n:["*"],limit:y,live:L,...u};S.length&&(m.farms=S),E&&(m.threshold=f);let x=await fetch(`${w}/system/search-report.json`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(m)});if(!x.ok)throw new Error(`search-report failed: ${x.status}`);let j=await x.json(),R=window.wiki.newPage(j);window.wiki.showResult(R,{$page:t.parents(".page")}),a.textContent=o}catch(n){a.textContent=`Error: ${n.message}`}finally{h.disabled=!1}}};h.addEventListener("click",l),c.addEventListener("keydown",d=>{d.key==="Enter"&&l()})}else if(s==="sites"){let c=t.find(".sim-input")[0],h=t.find(".sim-btn")[0],o=`Domains: ${i.length?i.join(", "):"*"}`;r&&!c.value&&(c.value=r.title),(async()=>{try{let d=await v,n=await G(d.length?d:["*"],w),u=n.reduce((m,x)=>m+(x.page_count||0),0);o=D("Site report ready",u,n.length),r&&(o=`${I(r)} \xB7 ${o}`),a.textContent=o}catch(d){a.textContent=`Domain listing unavailable: ${d.message}`}})();let l=async()=>{let d=c.value.trim();if(d){h.disabled=!0,a.textContent="Ranking sites\u2026";try{let n=await v,u=r&&d===r.title?V():{},m=await fetch(`${w}/system/site-report.json`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:d,domains:n.length?n:["*"],limit:y,...u})});if(!m.ok)throw new Error(`site-report failed: ${m.status}`);let x=await m.json();window.wiki.showResult(window.wiki.newPage(x),{$page:t.parents(".page")}),a.textContent=o}catch(n){a.textContent=`Error: ${n.message}`}finally{h.disabled=!1}}};h.addEventListener("click",l),c.addEventListener("keydown",d=>{d.key==="Enter"&&l()})}else if(s==="keyword"){let c=t.find(".sim-input")[0],h=t.find(".sim-btn")[0];r&&!c.value&&(c.value=r.title),a.textContent=(r?`${I(r)} \xB7 `:"")+`Keyword search ready \u2014 domains: ${i.length?i.join(", "):"*"} \xB7 limit ${y}`;let o=async()=>{let l=c.value.trim();if(l){h.disabled=!0,a.textContent="Searching live site indexes\u2026";try{let d=await v,n=encodeURIComponent((d.length?d:["*"]).join(",")),u=S.length?`&farms=${encodeURIComponent(S.join(","))}`:"",m=await fetch(`${w}/system/farm-search.json?q=${encodeURIComponent(l)}&pattern=${n}&limit=${y}${u}`);if(!m.ok)throw new Error(`farm-search failed: ${m.status}`);let x=await m.json();window.wiki.showResult(window.wiki.newPage(x),{$page:t.parents(".page")}),a.textContent=`Keyword search ready \u2014 domains: ${i.length?i.join(", "):"*"} \xB7 limit ${y}`}catch(d){a.textContent=`Error: ${d.message}`}finally{h.disabled=!1}}};h.addEventListener("click",o),c.addEventListener("keydown",l=>{l.key==="Enter"&&o()})}else if(s==="author"){let c=t.find(".sim-input")[0],h=t.find(".sim-btn")[0],o=t.find(".sim-results")[0],l=null;r&&!c.value&&(c.value=r.title),(async()=>{try{p||(a.textContent="Resolving domains\u2026"),l=await q(await v,w);let n=l.reduce((u,m)=>u+m.pages.length,0);a.textContent=(r?`${I(r)} \xB7 `:"")+D("Author ready",n,l.length)}catch(n){a.textContent=`Load error: ${n.message}`}})();let d=async()=>{let n=c.value.trim();if(!(!n||!l)){h.disabled=!0,a.textContent="Embedding query\u2026",o.innerHTML="";try{let u=await F(n,w),m=N(u,l,{threshold:f,limit:y,excludeSlug:null,excludeDomain:null}),x=new Set,j=[];for(let{title:T}of m)x.has(T)||(x.add(T),j.push(T));let R=()=>Math.floor(Math.random()*18446744073709552e3).toString(16).padStart(16,"0"),U=j.map(T=>`- [[${T}]]`).join(`
84
- `),tt=[{type:"markdown",id:R(),text:`# Similar Pages
82
+ <p class="sim-count">${o.length} domains \u2014 ${d.toLocaleString()} pages${F(l)}</p>`};g?.domains?h(g.domains,g.ts):(async()=>{try{let o=await v,l=o.length?o.join(","):"*",d=`${w}/system/indexed-domains.json?pattern=${encodeURIComponent(l)}&limit=${y}`,n=await fetch(d);if(!n.ok)throw new Error(`indexed-domains failed: ${n.status}`);let u=await n.json();if(!u.length){a.textContent="No indexed domains found";return}h(u,null),P(e,{domains:u})}catch(o){a.textContent=`Error: ${o.message}`}})()}else if(s==="similar"){let c=t.find(".sim-results")[0],h=(o,l)=>{if(!o.length){a.textContent=`No similar pages found above threshold ${f}`;return}c.innerHTML=`<h3>Similar Pages</h3><ul>${o.map(({domain:d,slug:n,title:u,score:m})=>`<li>${V(d,n,u,m)}</li>`).join("")}</ul><p class="sim-count">${o.length} found ${lt}${F(l)}</p>`,a.style.display="none"};g?.scored?h(g.scored,g.ts):(async()=>{try{let o=t.parents(".page"),l=r||(()=>{let x=o.find(".title").text().trim()||document.title;return{slug:X(x),site:window.location.hostname,title:x,text:null,isSelf:!0}})(),d=await z(await v,w),n=d.reduce((x,j)=>x+j.pages.length,0);a.textContent=(r?`${T(l)} \xB7 `:"")+`Searching ${n.toLocaleString()} pages\u2026`;let u=await st(l.slug,l.site);if(!u){a.textContent="Embedding page (not yet indexed)\u2026";let x=l.text||o.find(".item").map((j,R)=>$(R).text().trim()).get().filter(Boolean).join(`
83
+ `);u=await U(x||l.title,w)}let m=M(u,d,{threshold:f,limit:y,excludeSlug:l.slug,excludeDomain:l.site});h(m,null),r&&(a.textContent=T(l),a.style.display=""),m.length&&!r&&P(e,{scored:m})}catch(o){a.textContent=`Error: ${o.message}`}})()}else if(s==="ghost"){let c=t.find(".sim-btn")[0],h=async()=>{if(!C){a.textContent="No URL \u2014 GHOST needs a page-json URL";return}c.disabled=!0,a.textContent="Fetching\u2026";try{let o=await fetch(C);if(!o.ok)throw new Error(`fetch failed: ${o.status}`);let l=await o.json();window.wiki.showResult(window.wiki.newPage(l),{$page:t.parents(".page")}),a.textContent=""}catch(o){a.textContent=`Error: ${o.message}`}finally{c.disabled=!1}};c.addEventListener("click",h)}else if(s==="build"){let c=t.find(".sim-btn")[0],h=async()=>{c.disabled=!0,a.textContent="Building index\u2026 (may take a while for large scopes)";try{let o=encodeURIComponent((i.length?i:["*"]).join(",")),l=await fetch(`${w}/system/build-index.json?domains=${o}&force=${k?1:0}`);if(l.status===501){let n=await l.json().catch(()=>({}));throw new Error(n.hint||"indexing runs on the farm indexer, not this server")}if(!l.ok)throw new Error(`build-index failed: ${l.status}`);let d=await l.json();window.wiki.showResult(window.wiki.newPage(d),{$page:t.parents(".page")}),a.textContent=""}catch(o){a.textContent=`Error: ${o.message}`}finally{c.disabled=!1}};c.addEventListener("click",h)}else if(s==="report"){let c=t.find(".sim-input")[0],h=t.find(".sim-btn")[0],o=`Domains: ${i.length?i.join(", "):"*"}`;r&&!c.value&&(c.value=r.title),(async()=>{try{let d=await v,n=await O(d.length?d:["*"],w),u=n.reduce((m,x)=>m+(x.page_count||0),0);o=A("Report ready",u,n.length),E.length&&(o+=` \xB7 +${E.length} peer farm${E.length>1?"s":""}`),r&&(o=`${T(r)} \xB7 ${o}`),a.textContent=o}catch(d){a.textContent=`Domain listing unavailable: ${d.message}`}})();let l=async()=>{let d=c.value.trim();if(d){h.disabled=!0,a.textContent=E.length?"Generating report (asking peer farms)\u2026":"Generating report\u2026";try{let n=await v,u=r&&d===r.title?K():{},m={query:d,domains:n.length?n:["*"],limit:y,live:L,...u};E.length&&(m.farms=E),S&&(m.threshold=f);let x=await fetch(`${w}/system/search-report.json`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(m)});if(!x.ok)throw new Error(`search-report failed: ${x.status}`);let j=await x.json(),R=window.wiki.newPage(j);window.wiki.showResult(R,{$page:t.parents(".page")}),a.textContent=o}catch(n){a.textContent=`Error: ${n.message}`}finally{h.disabled=!1}}};h.addEventListener("click",l),c.addEventListener("keydown",d=>{d.key==="Enter"&&l()})}else if(s==="sites"){let c=t.find(".sim-input")[0],h=t.find(".sim-btn")[0],o=`Domains: ${i.length?i.join(", "):"*"}`;r&&!c.value&&(c.value=r.title),(async()=>{try{let d=await v,n=await O(d.length?d:["*"],w),u=n.reduce((m,x)=>m+(x.page_count||0),0);o=A("Site report ready",u,n.length),r&&(o=`${T(r)} \xB7 ${o}`),a.textContent=o}catch(d){a.textContent=`Domain listing unavailable: ${d.message}`}})();let l=async()=>{let d=c.value.trim();if(d){h.disabled=!0,a.textContent="Ranking sites\u2026";try{let n=await v,u=r&&d===r.title?K():{},m=await fetch(`${w}/system/site-report.json`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:d,domains:n.length?n:["*"],limit:y,...u})});if(!m.ok)throw new Error(`site-report failed: ${m.status}`);let x=await m.json();window.wiki.showResult(window.wiki.newPage(x),{$page:t.parents(".page")}),a.textContent=o}catch(n){a.textContent=`Error: ${n.message}`}finally{h.disabled=!1}}};h.addEventListener("click",l),c.addEventListener("keydown",d=>{d.key==="Enter"&&l()})}else if(s==="keyword"){let c=t.find(".sim-input")[0],h=t.find(".sim-btn")[0];r&&!c.value&&(c.value=r.title),a.textContent=(r?`${T(r)} \xB7 `:"")+`Keyword search ready \u2014 domains: ${i.length?i.join(", "):"*"} \xB7 limit ${y}`;let o=async()=>{let l=c.value.trim();if(l){h.disabled=!0,a.textContent="Searching live site indexes\u2026";try{let d=await v,n=encodeURIComponent((d.length?d:["*"]).join(",")),u=E.length?`&farms=${encodeURIComponent(E.join(","))}`:"",m=await fetch(`${w}/system/farm-search.json?q=${encodeURIComponent(l)}&pattern=${n}&limit=${y}${u}`);if(!m.ok)throw new Error(`farm-search failed: ${m.status}`);let x=await m.json();window.wiki.showResult(window.wiki.newPage(x),{$page:t.parents(".page")}),a.textContent=`Keyword search ready \u2014 domains: ${i.length?i.join(", "):"*"} \xB7 limit ${y}`}catch(d){a.textContent=`Error: ${d.message}`}finally{h.disabled=!1}}};h.addEventListener("click",o),c.addEventListener("keydown",l=>{l.key==="Enter"&&o()})}else if(s==="author"){let c=t.find(".sim-input")[0],h=t.find(".sim-btn")[0],o=t.find(".sim-results")[0],l=null;r&&!c.value&&(c.value=r.title),(async()=>{try{g||(a.textContent="Resolving domains\u2026"),l=await z(await v,w);let n=l.reduce((u,m)=>u+m.pages.length,0);a.textContent=(r?`${T(r)} \xB7 `:"")+A("Author ready",n,l.length)}catch(n){a.textContent=`Load error: ${n.message}`}})();let d=async()=>{let n=c.value.trim();if(!(!n||!l)){h.disabled=!0,a.textContent="Embedding query\u2026",o.innerHTML="";try{let u=await U(n,w),m=M(u,l,{threshold:f,limit:y,excludeSlug:null,excludeDomain:null}),x=new Set,j=[];for(let{title:I}of m)x.has(I)||(x.add(I),j.push(I));let R=()=>Math.floor(Math.random()*18446744073709552e3).toString(16).padStart(16,"0"),H=j.map(I=>`- [[${I}]]`).join(`
84
+ `),ct=[{type:"markdown",id:R(),text:`# Similar Pages
85
85
 
86
- ${U}`},{type:"markdown",id:R(),text:"# Reference Links"},...m.map(({domain:T,slug:st,title:nt,score:it})=>({type:"reference",id:R(),site:T,slug:st,title:nt,text:`score ${it.toFixed(3)}`}))],et=window.wiki.newPage({title:`${n} Results`,story:tt,journal:[]});window.wiki.showResult(et,{$page:t.parents(".page")}),a.textContent=`${m.length} pages found`,P(e,{scored:m,query:n})}catch(u){a.textContent=`Error: ${u.message}`}finally{h.disabled=!1}}};h.addEventListener("click",d),c.addEventListener("keydown",n=>{n.key==="Enter"&&d()})}else{let c=t.find(".sim-input")[0],h=t.find(".sim-btn")[0],o=t.find(".sim-results")[0],l=null;p?.scored&&(c.value=p.query||"",o.innerHTML=p.scored.map(({domain:n,slug:u,title:m,score:x})=>`<div class="sim-result">${_(n,u,m,x)}</div>`).join("")+`<p class="sim-count">Top ${p.scored.length} for "${p.query||""}"${A(p.ts)}</p>`,a.textContent=""),r&&!c.value&&(c.value=r.title),(async()=>{try{p||(a.textContent="Resolving domains\u2026"),l=await q(await v,w);let n=l.reduce((u,m)=>u+m.pages.length,0);a.textContent=(r?`${I(r)} \xB7 `:"")+D("Search ready",n,l.length)}catch(n){a.textContent=`Load error: ${n.message}`}})();let d=async()=>{let n=c.value.trim();if(!(!n||!l)){h.disabled=!0,a.textContent="Embedding query\u2026",o.innerHTML="";try{let u=await F(n,w),m=N(u,l,{threshold:f,limit:y,excludeSlug:null,excludeDomain:null});o.innerHTML=m.map(({domain:x,slug:j,title:R,score:U})=>`<div class="sim-result">${_(x,j,R,U)}</div>`).join("")+`<p class="sim-count">Top ${m.length} for "${n}"</p>`,a.textContent="",P(e,{scored:m,query:n})}catch(u){a.textContent=`Error: ${u.message}`}finally{h.disabled=!1}}};h.addEventListener("click",d),c.addEventListener("keydown",n=>{n.key==="Enter"&&d()})}};typeof window<"u"&&(window.plugins=window.plugins||{},window.plugins.similarity={emit:xt,bind:bt});})();
86
+ ${H}`},{type:"markdown",id:R(),text:"# Reference Links"},...m.map(({domain:I,slug:ft,title:ut,score:mt})=>({type:"reference",id:R(),site:I,slug:ft,title:ut,text:`score ${mt.toFixed(3)}`}))],dt=window.wiki.newPage({title:`${n} Results`,story:ct,journal:[]});window.wiki.showResult(dt,{$page:t.parents(".page")}),a.textContent=`${m.length} pages found`,P(e,{scored:m,query:n})}catch(u){a.textContent=`Error: ${u.message}`}finally{h.disabled=!1}}};h.addEventListener("click",d),c.addEventListener("keydown",n=>{n.key==="Enter"&&d()})}else{let c=t.find(".sim-input")[0],h=t.find(".sim-btn")[0],o=t.find(".sim-results")[0],l=null;g?.scored&&(c.value=g.query||"",o.innerHTML=g.scored.map(({domain:n,slug:u,title:m,score:x})=>`<div class="sim-result">${V(n,u,m,x)}</div>`).join("")+`<p class="sim-count">Top ${g.scored.length} for "${g.query||""}"${F(g.ts)}</p>`,a.textContent=""),r&&!c.value&&(c.value=r.title),(async()=>{try{g||(a.textContent="Resolving domains\u2026"),l=await z(await v,w);let n=l.reduce((u,m)=>u+m.pages.length,0);a.textContent=(r?`${T(r)} \xB7 `:"")+A("Search ready",n,l.length)}catch(n){a.textContent=`Load error: ${n.message}`}})();let d=async()=>{let n=c.value.trim();if(!(!n||!l)){h.disabled=!0,a.textContent="Embedding query\u2026",o.innerHTML="";try{let u=await U(n,w),m=M(u,l,{threshold:f,limit:y,excludeSlug:null,excludeDomain:null});o.innerHTML=m.map(({domain:x,slug:j,title:R,score:H})=>`<div class="sim-result">${V(x,j,R,H)}</div>`).join("")+`<p class="sim-count">Top ${m.length} for "${n}"</p>`,a.textContent="",P(e,{scored:m,query:n})}catch(u){a.textContent=`Error: ${u.message}`}finally{h.disabled=!1}}};h.addEventListener("click",d),c.addEventListener("keydown",n=>{n.key==="Enter"&&d()})}};typeof window<"u"&&(window.plugins=window.plugins||{},window.plugins.similarity={emit:xt,bind:bt});})();
87
87
  //# sourceMappingURL=similarity.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 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// SUBJECT modifier: act on the PREVIOUS page in the lineup\n// (the host a tool page was opened beside; falls\n// back to the containing page when none)\n// THRESHOLD: 0.72 exact cosine threshold (overrides SIMILAR:)\n// LIMIT: 8 max results shown (default 10)\n// ROSTER site/slug add the sites of a roster page to the scope\n// FARM other.farm.tld ask a peer farm to continue the search (experimental)\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// REPORT \u2192 server-side ranked/bundled semantic report (ghost page)\n// KEYWORD \u2192 galactic MiniSearch over live site-index.json files (ghost page)\n// SITES \u2192 which site should this page go on? \u2014 per-domain aggregation of\n// the page-vector scan (ghost page)\n// (other) \u2192 search form mode \u2014 user types a query, results appear\n//\n// Server endpoints required (all same-origin, served by this plugin's server\n// component \u2014 works on any host, including the public farm):\n// GET /system/indexed-domains.json?pattern=glob1,glob2\n// GET /system/semantic-vectors.json?domain=\n// POST /system/embed.json {text}\n// POST /system/search-report.json\n// POST /system/site-report.json\n// GET /system/farm-search.json?q=&pattern=&limit=\n// GET /system/build-index.json?domains=&force=\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 const rosterRefs = [] // ROSTER site/slug \u2014 roster pages whose sites join the scope\n const farms = [] // FARM domain \u2014 peer farms asked to continue the search\n let threshold = null\n let limit = null\n let mode = 'search' // default: interactive search form\n let live = false // default: cache results in localStorage\n let subject = false // SUBJECT modifier: act on the previous lineup page\n let force = false // BUILD mode: re-embed even when index is fresh\n let ghostUrl = null // GHOST mode: page-json URL to open as a ghost page\n let label = null // BUTTON: custom button caption (GHOST / BUILD modes)\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, 'LIVE')) { live = true; continue }\n if (isCmd(upper, 'SUBJECT')) { subject = true; continue }\n if (isCmd(upper, 'AUTHOR')) {\n if (!specs.length && mode === 'search') mode = 'author'\n continue\n }\n if (isCmd(upper, 'REPORT')) {\n if (mode === 'search') mode = 'report'\n continue\n }\n if (isCmd(upper, 'KEYWORD')) {\n if (mode === 'search') mode = 'keyword'\n continue\n }\n if (isCmd(upper, 'SITES')) {\n if (mode === 'search') mode = 'sites'\n continue\n }\n if (isCmd(upper, 'BUILD')) {\n if (mode === 'search') mode = 'build'\n continue\n }\n if (isCmd(upper, 'FORCE')) { force = true; continue }\n if (isCmd(upper, 'GHOST')) {\n ghostUrl = val(line, 'GHOST')\n if (mode === 'search') mode = 'ghost'\n continue\n }\n if (isCmd(upper, 'BUTTON')) { label = val(line, 'BUTTON'); continue }\n if (isCmd(upper, 'ROSTER')) {\n const ref = val(line, 'ROSTER')\n if (ref) rosterRefs.push(ref)\n continue\n }\n if (isCmd(upper, 'FARM')) {\n const peer = val(line, 'FARM')\n if (peer) farms.push(peer)\n continue\n }\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 const tv = val(line, 'THRESHOLD')\n threshold = SIMILAR_THRESHOLDS[tv.toLowerCase()] ?? (parseFloat(tv) || DEFAULT_THRESHOLD)\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, explicit domain, or scope keyword)\n specs.push(['PUBLIC', 'LOCAL', 'PRIVATE', 'GALAXY'].includes(upper) ? upper : line)\n }\n\n return {\n mode,\n specs,\n rosterRefs,\n farms,\n threshold: threshold ?? DEFAULT_THRESHOLD,\n limit: limit ?? DEFAULT_LIMIT,\n live,\n subject,\n force,\n ghostUrl,\n label,\n thresholdSet: threshold !== null, // explicit THRESHOLD/SIMILAR in the DSL\n }\n}\n\nconst isGlob = spec => spec.includes('*') || spec.includes('?')\n\n// Scope keywords expand server-side: PUBLIC (Nextcloud mirror farms),\n// LOCAL (primary farm), PRIVATE (public domains with restricted: true),\n// GALAXY (off-farm federation sites indexed into the galaxy tree)\nconst isScope = spec => spec === 'PUBLIC' || spec === 'LOCAL' || spec === 'PRIVATE' ||\n spec === 'GALAXY'\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) || isScope(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 Roster 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// ROSTER site/slug resolves a roster page into domain names, following the\n// roster plugin's own line semantics: bare domains are sites; category and\n// blank lines are skipped; nested `ROSTER site/slug` includes another roster\n// page; `REFERENCES site/slug` collects the .site of a page's reference items.\n// Includes are followed transitively with a visited-set loop guard.\n\nconst SITE_LINE = /^([a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)+|localhost)(:\\d+)?$/\nconst ROSTER_LINE = /^ROSTER ([A-Za-z0-9.\\-:]+\\/[a-z0-9-]+)$/\nconst REFS_LINE = /^REFERENCES ([A-Za-z0-9.\\-:]+\\/[a-z0-9-]+)$/\n\nconst fetchPage = async ref => { // ref = site/slug\n const i = ref.indexOf('/')\n const res = await fetch(`//${ref.slice(0, i)}/${ref.slice(i + 1)}.json`)\n if (!res.ok) throw new Error(`roster page ${ref} failed: ${res.status}`)\n return res.json()\n}\n\nconst resolveRoster = async (ref, out, visited) => {\n if (visited.has(ref)) return\n visited.add(ref)\n let page\n try { page = await fetchPage(ref) } catch (e) { console.warn(e.message); return }\n for (const item of page.story || []) {\n if (item.type !== 'roster') continue\n for (const raw of (item.text || '').split(/\\r?\\n/)) {\n const line = raw.trim()\n if (!line) continue\n const site = line.match(SITE_LINE)\n if (site) { out.add(site[0]); continue }\n const nested = line.match(ROSTER_LINE)\n if (nested) { await resolveRoster(nested[1], out, visited); continue }\n const refs = line.match(REFS_LINE)\n if (refs) {\n try {\n const refPage = await fetchPage(refs[1])\n for (const it of refPage.story || []) {\n if (it.type === 'reference' && it.site) out.add(it.site)\n }\n } catch (e) { console.warn(e.message) }\n }\n // anything else is a category name \u2014 skipped\n }\n }\n}\n\nconst resolveRosters = async rosterRefs => {\n const out = new Set()\n const visited = new Set()\n for (const ref of rosterRefs) await resolveRoster(ref, out, visited)\n return [...out]\n}\n\n// Effective specs: DSL specs plus any domains contributed by ROSTER lines.\nconst effectiveSpecs = async (specs, rosterRefs) =>\n rosterRefs.length ? [...specs, ...await resolveRosters(rosterRefs)] : specs\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// Route vector requests through the current wiki's plugin server. This keeps\n// public HTTPS pages same-origin, avoids CORS failures, and lets the server read\n// indices from its configured farm roots.\nconst vectorUrl = domain =>\n `${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(domain)}`\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\n// POST, never GET: page-length text in a query string overflows Node's header\n// limit and dies with 431 before any handler runs (server \u2265 0.11.0).\nconst getEmbedding = async (text, origin) => {\n const res = await fetch(`${origin}/system/embed.json`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ text }),\n })\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 Subject 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\n// SUBJECT makes an item act on the page BEFORE its own in the lineup \u2014 the\n// host a tool page was opened beside \u2014 falling back to the containing page so\n// a tool page opened alone still works (and announces the fallback).\n// The page div's id IS the slug (possibly _rev-suffixed on a historical view);\n// data('data') carries the full raw page JSON; data('site') is set only on\n// remote pages. `text` is title + opening prose, capped: the BGE-small\n// embedder truncates around 512 tokens, so more would be discarded anyway.\n\nconst resolveSubject = div => {\n const $self = div.parents('.page:first')\n const $host = $self.prev('.page')\n const $page = $host.length ? $host : $self\n const slug = ($page.attr('id') || '').split('_rev')[0]\n const site = $page.data('site') || window.location.hostname\n const data = $page.data('data') || {}\n const title = data.title || $page.find('.title').text().trim() || slug\n const text = [title, ...(data.story || [])\n .filter(i => i.type === 'markdown' || i.type === 'paragraph')\n .map(i => (i.text || '').trim()).filter(Boolean)].join('\\n').slice(0, 2000)\n return { slug, site, title, text, isSelf: !$host.length }\n}\n\nconst subjectNote = s =>\n `Subject: ${s.title}${s.isSelf ? ' (this page)' : ` @ ${s.site}`}`\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 Result cache (localStorage) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Cache is keyed by item id. Invalidated when item.text changes (DSL edited).\n// LIVE mode bypasses the cache entirely.\n\nconst cacheKey = id => `sim-cache-${id}`\n\nconst readCache = item => {\n try {\n const c = JSON.parse(localStorage.getItem(cacheKey(item.id)) || 'null')\n return c?.text === (item.text || '') ? c : null\n } catch { return null }\n}\n\nconst writeCache = (item, data) => {\n try {\n localStorage.setItem(cacheKey(item.id), JSON.stringify({\n text: item.text || '',\n ts: Date.now(),\n ...data,\n }))\n } catch { /* storage unavailable or full */ }\n}\n\nconst cacheAge = ts => {\n const s = Math.floor((Date.now() - ts) / 1000)\n if (s < 60) return `${s}s ago`\n if (s < 3600) return `${Math.floor(s / 60)}m ago`\n if (s < 86400) return `${Math.floor(s / 3600)}h ago`\n return `${Math.floor(s / 86400)}d ago`\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, force, ghostUrl, label } = parseDSL(item?.text || '')\n if (mode === 'ghost') {\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-form\">\n <button class=\"sim-btn\">${label || 'Open'}</button>\n </div>\n <div class=\"sim-status\"></div>\n </div>`)\n } else if (mode === 'build') {\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-form\">\n <button class=\"sim-btn\">${label || `Index ${specs.length ? specs.join(', ') : '*'}${force ? ' (force)' : ''}`}</button>\n </div>\n <div class=\"sim-status\"></div>\n </div>`)\n } else 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 if (mode === 'author' || mode === 'report' || mode === 'keyword' || mode === 'sites') {\n const label = specs.length ? specs.join(', ') : '(current domain)'\n const btnLabel = mode === 'report' ? 'Report' : mode === 'keyword' ? 'Keyword'\n : mode === 'sites' ? 'Sites' : 'Author'\n const hint = mode === 'sites' ? 'Where should this page go? Title + first paragraph\u2026'\n : 'Search wiki pages\u2026'\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=\"${hint}\" />\n <button class=\"sim-btn\">${btnLabel}</button>\n </div>\n <div class=\"sim-status\">Domains: ${label}</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, rosterRefs, farms, threshold, limit, live, subject: subjectFlag,\n force, ghostUrl, thresholdSet } = parseDSL(item?.text || '')\n const origin = window.location.origin\n const status = div.find('.sim-status')[0]\n // The subject depends on where in the lineup this page was opened, so\n // cached results from one host must never replay beside another.\n const subject = subjectFlag ? resolveSubject(div) : null\n const cache = (live || subject) ? null : readCache(item)\n // Scope = DSL specs plus roster-page domains (resolved once, shared by modes)\n const specsP = effectiveSpecs(specs, rosterRefs)\n // Seed params for the server report routes \u2014 the stored page vector wins\n // server-side; text is the fallback for remote or unindexed subjects.\n const seedParams = () => subject ? {\n seed: { site: subject.site, slug: subject.slug },\n text: subject.text,\n excludePage: { site: subject.site, slug: subject.slug },\n } : {}\n\n // Standardised pre-search status: what will run, over how much, with what config.\n // e.g. \"Report ready \u2014 18,583 pages across 267 domains \u00B7 threshold 0.68 \u00B7 limit 20 \u00B7 LIVE\"\n const configSummary = (verb, pages, nDomains) => {\n const parts = [`${verb} \u2014 ${pages.toLocaleString()} pages across ${nDomains} domains`]\n if (thresholdSet) parts.push(`threshold ${threshold}`)\n parts.push(`limit ${limit}`)\n if (live) parts.push('LIVE')\n return parts.join(' \u00B7 ')\n }\n\n div.on('dblclick', e => {\n if ($(e.target).closest('.sim-input').length) return\n window.wiki.textEditor(div, item)\n })\n div.on('click', '.sim-link', function (e) {\n e.preventDefault()\n const $a = $(this)\n // shift-click appends at the end of the lineup instead of truncating after this page\n const $page = e.shiftKey ? null : div.parents('.page')\n window.wiki.doInternalLink($a.data('title'), $page, $a.data('site'))\n })\n\n const scopeLabel = !specs.length || (specs.length === 1 && specs[0] === '*')\n ? 'on farm'\n : specs.length === 1 ? `on ${specs[0]}` : 'in domains'\n\n const cacheNote = ts => ts ? ` \u00B7 cached ${cacheAge(ts)}` : ''\n\n if (mode === 'list') {\n const listDiv = div.find('.sim-list')[0]\n\n const renderList = (domains, ts) => {\n const totalPages = domains.reduce((n, d) => n + (d.page_count || 0), 0)\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 ${domains.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\">${domains.length} domains \u2014 ${totalPages.toLocaleString()} pages${cacheNote(ts)}</p>`\n }\n\n if (cache?.domains) {\n renderList(cache.domains, cache.ts)\n } else {\n ;(async () => {\n try {\n const eff = await specsP\n const patterns = eff.length ? eff.join(',') : '*'\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 domains = await res.json()\n if (!domains.length) { status.textContent = 'No indexed domains found'; return }\n renderList(domains, null)\n writeCache(item, { domains })\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n }\n })()\n }\n\n } else if (mode === 'similar') {\n const results = div.find('.sim-results')[0]\n\n const renderScored = (scored, ts) => {\n if (!scored.length) {\n status.textContent = `No similar pages found above threshold ${threshold}`\n return\n }\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}${cacheNote(ts)}</p>`\n status.style.display = 'none'\n }\n\n if (cache?.scored) {\n renderScored(cache.scored, cache.ts)\n } else {\n ;(async () => {\n try {\n // SUBJECT: act on the previous lineup page; otherwise this page.\n const $page = div.parents('.page')\n const s = subject || (() => {\n const pageTitle = $page.find('.title').text().trim() || document.title\n return { slug: slugify(pageTitle), site: window.location.hostname,\n title: pageTitle, text: null, isSelf: true }\n })()\n\n const domainEntries = await loadDomainEntries(await specsP, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = (subject ? `${subjectNote(s)} \u00B7 ` : '') +\n `Searching ${total.toLocaleString()} pages\u2026`\n\n let qVec = await lookupPageVector(s.slug, s.site)\n if (!qVec) {\n status.textContent = 'Embedding page (not yet indexed)\u2026'\n const pageText = s.text || $page.find('.item')\n .map((_, el) => $(el).text().trim()).get().filter(Boolean).join('\\n')\n qVec = await getEmbedding(pageText || s.title, origin)\n }\n\n const scored = cosineScan(qVec, domainEntries, {\n threshold, limit, excludeSlug: s.slug, excludeDomain: s.site,\n })\n renderScored(scored, null)\n if (subject) { // keep the subject visible above the results\n status.textContent = subjectNote(s)\n status.style.display = ''\n }\n if (scored.length && !subject) writeCache(item, { scored })\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n }\n })()\n }\n\n } else if (mode === 'ghost') {\n // Ghost mode \u2014 fetch page-json from any URL and open it as a ghost page\n const btn = div.find('.sim-btn')[0]\n\n const doGhost = async () => {\n if (!ghostUrl) { status.textContent = 'No URL \u2014 GHOST needs a page-json URL'; return }\n btn.disabled = true\n status.textContent = 'Fetching\u2026'\n try {\n const res = await fetch(ghostUrl)\n if (!res.ok) throw new Error(`fetch failed: ${res.status}`)\n const page = await res.json()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n status.textContent = ''\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doGhost)\n\n } else if (mode === 'build') {\n // Build mode \u2014 trigger a semantic index build; result opens as a ghost page\n const btn = div.find('.sim-btn')[0]\n\n const doBuild = async () => {\n btn.disabled = true\n status.textContent = 'Building index\u2026 (may take a while for large scopes)'\n try {\n const domains = encodeURIComponent((specs.length ? specs : ['*']).join(','))\n const res = await fetch(\n `${origin}/system/build-index.json?domains=${domains}&force=${force ? 1 : 0}`)\n if (res.status === 501) {\n const body = await res.json().catch(() => ({}))\n throw new Error(body.hint || 'indexing runs on the farm indexer, not this server')\n }\n if (!res.ok) throw new Error(`build-index failed: ${res.status}`)\n const page = await res.json()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n status.textContent = ''\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doBuild)\n\n } else if (mode === 'report') {\n // Report mode \u2014 server-side ranked/bundled search, opens result as ghost page\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n let readyLine = `Domains: ${specs.length ? specs.join(', ') : '*'}`\n\n // SUBJECT: the host page IS the query \u2014 prefill its title (editable) and\n // seed the server search with its stored vector.\n if (subject && !input.value) input.value = subject.title\n\n // Preload the lightweight domain listing (counts only, no vectors) so the\n // status line shows scope and config before any search is issued.\n ;(async () => {\n try {\n const eff = await specsP\n const domains = await resolveDomains(eff.length ? eff : ['*'], origin)\n const pages = domains.reduce((n, d) => n + (d.page_count || 0), 0)\n readyLine = configSummary('Report ready', pages, domains.length)\n if (farms.length) readyLine += ` \u00B7 +${farms.length} peer farm${farms.length > 1 ? 's' : ''}`\n if (subject) readyLine = `${subjectNote(subject)} \u00B7 ${readyLine}`\n status.textContent = readyLine\n } catch (e) {\n status.textContent = `Domain listing unavailable: ${e.message}`\n }\n })()\n\n const doReport = async () => {\n const query = input.value.trim()\n if (!query) return\n btn.disabled = true\n status.textContent = farms.length ? 'Generating report (asking peer farms)\u2026' : 'Generating report\u2026'\n try {\n const eff = await specsP\n // A hand-edited query is a new question \u2014 drop the page seed then.\n const seeded = subject && query === subject.title ? seedParams() : {}\n const body = { query, domains: eff.length ? eff : ['*'], limit, live, ...seeded }\n if (farms.length) body.farms = farms\n if (thresholdSet) body.threshold = threshold\n const res = await fetch(`${origin}/system/search-report.json`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!res.ok) throw new Error(`search-report failed: ${res.status}`)\n const page = await res.json()\n const pageObj = window.wiki.newPage(page)\n window.wiki.showResult(pageObj, { $page: div.parents('.page') })\n status.textContent = readyLine\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doReport)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doReport() })\n\n } else if (mode === 'sites') {\n // Sites mode \u2014 which site should this page go on? Server aggregates the\n // page-vector scan per domain; result opens as a ghost page.\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n let readyLine = `Domains: ${specs.length ? specs.join(', ') : '*'}`\n\n if (subject && !input.value) input.value = subject.title\n\n ;(async () => {\n try {\n const eff = await specsP\n const domains = await resolveDomains(eff.length ? eff : ['*'], origin)\n const pages = domains.reduce((n, d) => n + (d.page_count || 0), 0)\n readyLine = configSummary('Site report ready', pages, domains.length)\n if (subject) readyLine = `${subjectNote(subject)} \u00B7 ${readyLine}`\n status.textContent = readyLine\n } catch (e) {\n status.textContent = `Domain listing unavailable: ${e.message}`\n }\n })()\n\n const doSites = async () => {\n const query = input.value.trim()\n if (!query) return\n btn.disabled = true\n status.textContent = 'Ranking sites\u2026'\n try {\n const eff = await specsP\n const seeded = subject && query === subject.title ? seedParams() : {}\n const res = await fetch(`${origin}/system/site-report.json`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ query, domains: eff.length ? eff : ['*'], limit, ...seeded }),\n })\n if (!res.ok) throw new Error(`site-report failed: ${res.status}`)\n const page = await res.json()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n status.textContent = readyLine\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doSites)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doSites() })\n\n } else if (mode === 'keyword') {\n // Keyword mode \u2014 galactic MiniSearch: the server reads each site's own\n // per-edit site-index.json; result opens as a ghost page.\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n\n // SUBJECT: keyword search wants words, not vectors \u2014 the host title is\n // the prewired query (editable), finding forks and namesakes alike.\n if (subject && !input.value) input.value = subject.title\n\n status.textContent = (subject ? `${subjectNote(subject)} \u00B7 ` : '') +\n `Keyword search ready \u2014 domains: ${specs.length ? specs.join(', ') : '*'} \u00B7 limit ${limit}`\n\n const doKeyword = async () => {\n const query = input.value.trim()\n if (!query) return\n btn.disabled = true\n status.textContent = 'Searching live site indexes\u2026'\n try {\n const eff = await specsP\n const pattern = encodeURIComponent((eff.length ? eff : ['*']).join(','))\n const farmsParam = farms.length ? `&farms=${encodeURIComponent(farms.join(','))}` : ''\n const res = await fetch(\n `${origin}/system/farm-search.json?q=${encodeURIComponent(query)}&pattern=${pattern}&limit=${limit}${farmsParam}`)\n if (!res.ok) throw new Error(`farm-search failed: ${res.status}`)\n const page = await res.json()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n status.textContent = `Keyword search ready \u2014 domains: ${specs.length ? specs.join(', ') : '*'} \u00B7 limit ${limit}`\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doKeyword)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doKeyword() })\n\n } else if (mode === 'author') {\n // Author mode \u2014 same search form but creates a ghost page from results\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 if (subject && !input.value) input.value = subject.title\n\n ;(async () => {\n try {\n if (!cache) status.textContent = 'Resolving domains\u2026'\n domainEntries = await loadDomainEntries(await specsP, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = (subject ? `${subjectNote(subject)} \u00B7 ` : '') +\n configSummary('Author ready', total, domainEntries.length)\n } catch (e) {\n status.textContent = `Load error: ${e.message}`\n }\n })()\n\n const doAuthor = 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, limit, excludeSlug: null, excludeDomain: null })\n\n // Unique titles for wiki-link list\n const seenTitles = new Set()\n const uniqueTitles = []\n for (const { title } of scored) {\n if (!seenTitles.has(title)) { seenTitles.add(title); uniqueTitles.push(title) }\n }\n\n const hexId = () => Math.floor(Math.random() * 0xffffffffffffffff).toString(16).padStart(16, '0')\n const primaryLines = uniqueTitles.map(t => `- [[${t}]]`).join('\\n')\n\n const story = [\n { type: 'markdown', id: hexId(), text: `# Similar Pages\\n\\n${primaryLines}` },\n { type: 'markdown', id: hexId(), text: '# Reference Links' },\n ...scored.map(({ domain, slug, title, score }) => ({\n type: 'reference', id: hexId(), site: domain, slug, title,\n text: `score ${score.toFixed(3)}`,\n })),\n ]\n\n const pageObj = window.wiki.newPage({ title: `${query} Results`, story, journal: [] })\n window.wiki.showResult(pageObj, { $page: div.parents('.page') })\n\n status.textContent = `${scored.length} pages found`\n writeCache(item, { scored, query })\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doAuthor)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doAuthor() })\n\n } else {\n // Search form mode\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 // Show cached results immediately while domains preload in background\n if (cache?.scored) {\n input.value = cache.query || ''\n results.innerHTML = cache.scored.map(({ domain, slug, title, score }) =>\n `<div class=\"sim-result\">${simLink(domain, slug, title, score)}</div>`).join('') +\n `<p class=\"sim-count\">Top ${cache.scored.length} for \"${cache.query || ''}\"${cacheNote(cache.ts)}</p>`\n status.textContent = ''\n }\n\n if (subject && !input.value) input.value = subject.title\n\n ;(async () => {\n try {\n if (!cache) status.textContent = 'Resolving domains\u2026'\n domainEntries = await loadDomainEntries(await specsP, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = (subject ? `${subjectNote(subject)} \u00B7 ` : '') +\n configSummary('Search ready', total, domainEntries.length)\n } catch (e) {\n status.textContent = `Load error: ${e.message}`\n }\n })()\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, 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 `<p class=\"sim-count\">Top ${scored.length} for \"${query}\"</p>`\n status.textContent = ''\n writeCache(item, { scored, 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
- "names": ["SIMILAR_THRESHOLDS", "DEFAULT_THRESHOLD", "DEFAULT_LIMIT", "parseDSL", "text", "specs", "rosterRefs", "farms", "threshold", "limit", "mode", "live", "subject", "force", "ghostUrl", "label", "isCmd", "upper", "kw", "val", "line", "raw", "ref", "peer", "level", "tv", "isGlob", "spec", "isScope", "slugify", "title", "domainCache", "resolveDomainsForSpec", "origin", "url", "res", "list", "resolveDomains", "seen", "result", "item", "SITE_LINE", "ROSTER_LINE", "REFS_LINE", "fetchPage", "i", "resolveRoster", "out", "visited", "page", "e", "site", "nested", "refs", "refPage", "it", "resolveRosters", "effectiveSpecs", "vectorCache", "vectorUrl", "domain", "loadVectors", "data", "getEmbedding", "lookupPageVector", "slug", "entry", "p", "resolveSubject", "div", "$self", "$host", "$page", "subjectNote", "s", "cosineScan", "queryVec", "domainEntries", "excludeSlug", "excludeDomain", "results", "pages", "vector", "dot", "a", "b", "loadDomainEntries", "domains", "cacheKey", "id", "readCache", "c", "writeCache", "cacheAge", "ts", "STYLES", "siteFlag", "score", "simLink", "emit", "btnLabel", "hint", "bind", "subjectFlag", "thresholdSet", "status", "cache", "specsP", "seedParams", "configSummary", "verb", "nDomains", "parts", "$a", "scopeLabel", "cacheNote", "listDiv", "renderList", "totalPages", "d", "page_count", "eff", "patterns", "renderScored", "scored", "pageTitle", "total", "n", "qVec", "pageText", "_", "el", "btn", "doGhost", "doBuild", "body", "input", "readyLine", "doReport", "query", "seeded", "pageObj", "doSites", "doKeyword", "pattern", "farmsParam", "doAuthor", "seenTitles", "uniqueTitles", "hexId", "primaryLines", "t", "story", "doSearch"]
3
+ "sources": ["../src/client/dsl.js", "../src/client/scope.js", "../src/client/vectors.js", "../src/client/subject.js", "../src/client/cache.js", "../src/client/styles.js", "../src/client/similarity.js"],
4
+ "sourcesContent": ["// wiki-plugin-similarity \u2014 the item's line-oriented DSL: parse it, and classify a spec\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\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 const rosterRefs = [] // ROSTER site/slug \u2014 roster pages whose sites join the scope\n const farms = [] // FARM domain \u2014 peer farms asked to continue the search\n let threshold = null\n let limit = null\n let mode = 'search' // default: interactive search form\n let live = false // default: cache results in localStorage\n let subject = false // SUBJECT modifier: act on the previous lineup page\n let force = false // BUILD mode: re-embed even when index is fresh\n let ghostUrl = null // GHOST mode: page-json URL to open as a ghost page\n let label = null // BUTTON: custom button caption (GHOST / BUILD modes)\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, 'LIVE')) { live = true; continue }\n if (isCmd(upper, 'SUBJECT')) { subject = true; continue }\n if (isCmd(upper, 'AUTHOR')) {\n if (!specs.length && mode === 'search') mode = 'author'\n continue\n }\n if (isCmd(upper, 'REPORT')) {\n if (mode === 'search') mode = 'report'\n continue\n }\n if (isCmd(upper, 'KEYWORD')) {\n if (mode === 'search') mode = 'keyword'\n continue\n }\n if (isCmd(upper, 'SITES')) {\n if (mode === 'search') mode = 'sites'\n continue\n }\n if (isCmd(upper, 'BUILD')) {\n if (mode === 'search') mode = 'build'\n continue\n }\n if (isCmd(upper, 'FORCE')) { force = true; continue }\n if (isCmd(upper, 'GHOST')) {\n ghostUrl = val(line, 'GHOST')\n if (mode === 'search') mode = 'ghost'\n continue\n }\n if (isCmd(upper, 'BUTTON')) { label = val(line, 'BUTTON'); continue }\n if (isCmd(upper, 'ROSTER')) {\n const ref = val(line, 'ROSTER')\n if (ref) rosterRefs.push(ref)\n continue\n }\n if (isCmd(upper, 'FARM')) {\n const peer = val(line, 'FARM')\n if (peer) farms.push(peer)\n continue\n }\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 const tv = val(line, 'THRESHOLD')\n threshold = SIMILAR_THRESHOLDS[tv.toLowerCase()] ?? (parseFloat(tv) || DEFAULT_THRESHOLD)\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, explicit domain, or scope keyword)\n specs.push(['PUBLIC', 'LOCAL', 'PRIVATE', 'GALAXY'].includes(upper) ? upper : line)\n }\n\n return {\n mode,\n specs,\n rosterRefs,\n farms,\n threshold: threshold ?? DEFAULT_THRESHOLD,\n limit: limit ?? DEFAULT_LIMIT,\n live,\n subject,\n force,\n ghostUrl,\n label,\n thresholdSet: threshold !== null, // explicit THRESHOLD/SIMILAR in the DSL\n }\n}\n\nconst isGlob = spec => spec.includes('*') || spec.includes('?')\n\n// Scope keywords expand server-side: PUBLIC (Nextcloud mirror farms),\n// LOCAL (primary farm), PRIVATE (public domains with restricted: true),\n// GALAXY (off-farm federation sites indexed into the galaxy tree)\nconst isScope = spec => spec === 'PUBLIC' || spec === 'LOCAL' || spec === 'PRIVATE' ||\n spec === 'GALAXY'\n\nexport { parseDSL, isGlob, isScope, SIMILAR_THRESHOLDS, DEFAULT_THRESHOLD, DEFAULT_LIMIT }\n", "// wiki-plugin-similarity \u2014 turning DSL specs into a concrete list of domains\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\nimport { isGlob, isScope } from './dsl.js'\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) || isScope(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 Roster 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// ROSTER site/slug resolves a roster page into domain names, following the\n// roster plugin's own line semantics: bare domains are sites; category and\n// blank lines are skipped; nested `ROSTER site/slug` includes another roster\n// page; `REFERENCES site/slug` collects the .site of a page's reference items.\n// Includes are followed transitively with a visited-set loop guard.\n\nconst SITE_LINE = /^([a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)+|localhost)(:\\d+)?$/\nconst ROSTER_LINE = /^ROSTER ([A-Za-z0-9.\\-:]+\\/[a-z0-9-]+)$/\nconst REFS_LINE = /^REFERENCES ([A-Za-z0-9.\\-:]+\\/[a-z0-9-]+)$/\n\nconst fetchPage = async ref => { // ref = site/slug\n const i = ref.indexOf('/')\n const res = await fetch(`//${ref.slice(0, i)}/${ref.slice(i + 1)}.json`)\n if (!res.ok) throw new Error(`roster page ${ref} failed: ${res.status}`)\n return res.json()\n}\n\nconst resolveRoster = async (ref, out, visited) => {\n if (visited.has(ref)) return\n visited.add(ref)\n let page\n try { page = await fetchPage(ref) } catch (e) { console.warn(e.message); return }\n for (const item of page.story || []) {\n if (item.type !== 'roster') continue\n for (const raw of (item.text || '').split(/\\r?\\n/)) {\n const line = raw.trim()\n if (!line) continue\n const site = line.match(SITE_LINE)\n if (site) { out.add(site[0]); continue }\n const nested = line.match(ROSTER_LINE)\n if (nested) { await resolveRoster(nested[1], out, visited); continue }\n const refs = line.match(REFS_LINE)\n if (refs) {\n try {\n const refPage = await fetchPage(refs[1])\n for (const it of refPage.story || []) {\n if (it.type === 'reference' && it.site) out.add(it.site)\n }\n } catch (e) { console.warn(e.message) }\n }\n // anything else is a category name \u2014 skipped\n }\n }\n}\n\nconst resolveRosters = async rosterRefs => {\n const out = new Set()\n const visited = new Set()\n for (const ref of rosterRefs) await resolveRoster(ref, out, visited)\n return [...out]\n}\n\n// Effective specs: DSL specs plus any domains contributed by ROSTER lines.\nconst effectiveSpecs = async (specs, rosterRefs) =>\n rosterRefs.length ? [...specs, ...await resolveRosters(rosterRefs)] : specs\n\nexport { slugify, resolveDomains, resolveRosters, effectiveSpecs, SITE_LINE, ROSTER_LINE, REFS_LINE }\n", "// wiki-plugin-similarity \u2014 page vectors, embedding, and the cosine scan over them\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\nimport { resolveDomains } from './scope.js'\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// Route vector requests through the current wiki's plugin server. This keeps\n// public HTTPS pages same-origin, avoids CORS failures, and lets the server read\n// indices from its configured farm roots.\nconst vectorUrl = domain =>\n `${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(domain)}`\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\n// POST, never GET: page-length text in a query string overflows Node's header\n// limit and dies with 431 before any handler runs (server \u2265 0.11.0).\nconst getEmbedding = async (text, origin) => {\n const res = await fetch(`${origin}/system/embed.json`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ text }),\n })\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// \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\nexport { loadVectors, vectorUrl, getEmbedding, lookupPageVector, cosineScan, loadDomainEntries }\n", "// wiki-plugin-similarity \u2014 SUBJECT \u2014 the page an item acts on\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\n// \u2500\u2500 Subject 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\n// SUBJECT makes an item act on the page BEFORE its own in the lineup \u2014 the\n// host a tool page was opened beside \u2014 falling back to the containing page so\n// a tool page opened alone still works (and announces the fallback).\n// The page div's id IS the slug (possibly _rev-suffixed on a historical view);\n// data('data') carries the full raw page JSON; data('site') is set only on\n// remote pages. `text` is title + opening prose, capped: the BGE-small\n// embedder truncates around 512 tokens, so more would be discarded anyway.\n\nconst resolveSubject = div => {\n const $self = div.parents('.page:first')\n const $host = $self.prev('.page')\n const $page = $host.length ? $host : $self\n const slug = ($page.attr('id') || '').split('_rev')[0]\n const site = $page.data('site') || window.location.hostname\n const data = $page.data('data') || {}\n const title = data.title || $page.find('.title').text().trim() || slug\n const text = [title, ...(data.story || [])\n .filter(i => i.type === 'markdown' || i.type === 'paragraph')\n .map(i => (i.text || '').trim()).filter(Boolean)].join('\\n').slice(0, 2000)\n return { slug, site, title, text, isSelf: !$host.length }\n}\n\nconst subjectNote = s =>\n `Subject: ${s.title}${s.isSelf ? ' (this page)' : ` @ ${s.site}`}`\n\nexport { resolveSubject, subjectNote }\n", "// wiki-plugin-similarity \u2014 per-item result cache in localStorage\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\n// \u2500\u2500 Result cache (localStorage) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Cache is keyed by item id. Invalidated when item.text changes (DSL edited).\n// LIVE mode bypasses the cache entirely.\n\nconst cacheKey = id => `sim-cache-${id}`\n\nconst readCache = item => {\n try {\n const c = JSON.parse(localStorage.getItem(cacheKey(item.id)) || 'null')\n return c?.text === (item.text || '') ? c : null\n } catch { return null }\n}\n\nconst writeCache = (item, data) => {\n try {\n localStorage.setItem(cacheKey(item.id), JSON.stringify({\n text: item.text || '',\n ts: Date.now(),\n ...data,\n }))\n } catch { /* storage unavailable or full */ }\n}\n\nconst cacheAge = ts => {\n const s = Math.floor((Date.now() - ts) / 1000)\n if (s < 60) return `${s}s ago`\n if (s < 3600) return `${Math.floor(s / 60)}m ago`\n if (s < 86400) return `${Math.floor(s / 3600)}h ago`\n return `${Math.floor(s / 86400)}d ago`\n}\n\nexport { readCache, writeCache, cacheAge }\n", "// wiki-plugin-similarity \u2014 item styles and the site flag\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\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\nexport { STYLES, siteFlag }\n", "// 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// SUBJECT modifier: act on the PREVIOUS page in the lineup\n// (the host a tool page was opened beside; falls\n// back to the containing page when none)\n// THRESHOLD: 0.72 exact cosine threshold (overrides SIMILAR:)\n// LIMIT: 8 max results shown (default 10)\n// ROSTER site/slug add the sites of a roster page to the scope\n// FARM other.farm.tld ask a peer farm to continue the search (experimental)\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// REPORT \u2192 server-side ranked/bundled semantic report (ghost page)\n// KEYWORD \u2192 galactic MiniSearch over live site-index.json files (ghost page)\n// SITES \u2192 which site should this page go on? \u2014 per-domain aggregation of\n// the page-vector scan (ghost page)\n// (other) \u2192 search form mode \u2014 user types a query, results appear\n//\n// Server endpoints required (all same-origin, served by this plugin's server\n// component \u2014 works on any host, including the public farm):\n// GET /system/indexed-domains.json?pattern=glob1,glob2\n// GET /system/semantic-vectors.json?domain=\n// POST /system/embed.json {text}\n// POST /system/search-report.json\n// POST /system/site-report.json\n// GET /system/farm-search.json?q=&pattern=&limit=\n// GET /system/build-index.json?domains=&force=\n\nimport { parseDSL, DEFAULT_LIMIT } from './dsl.js'\nimport { slugify, effectiveSpecs, resolveDomains } from './scope.js'\nimport { loadVectors, getEmbedding, lookupPageVector, cosineScan, loadDomainEntries } from './vectors.js'\nimport { resolveSubject, subjectNote } from './subject.js'\nimport { readCache, writeCache, cacheAge } from './cache.js'\nimport { STYLES, siteFlag } from './styles.js'\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, force, ghostUrl, label } = parseDSL(item?.text || '')\n if (mode === 'ghost') {\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-form\">\n <button class=\"sim-btn\">${label || 'Open'}</button>\n </div>\n <div class=\"sim-status\"></div>\n </div>`)\n } else if (mode === 'build') {\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-form\">\n <button class=\"sim-btn\">${label || `Index ${specs.length ? specs.join(', ') : '*'}${force ? ' (force)' : ''}`}</button>\n </div>\n <div class=\"sim-status\"></div>\n </div>`)\n } else 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 if (mode === 'author' || mode === 'report' || mode === 'keyword' || mode === 'sites') {\n const label = specs.length ? specs.join(', ') : '(current domain)'\n const btnLabel = mode === 'report' ? 'Report' : mode === 'keyword' ? 'Keyword'\n : mode === 'sites' ? 'Sites' : 'Author'\n const hint = mode === 'sites' ? 'Where should this page go? Title + first paragraph\u2026'\n : 'Search wiki pages\u2026'\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=\"${hint}\" />\n <button class=\"sim-btn\">${btnLabel}</button>\n </div>\n <div class=\"sim-status\">Domains: ${label}</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, rosterRefs, farms, threshold, limit, live, subject: subjectFlag,\n force, ghostUrl, thresholdSet } = parseDSL(item?.text || '')\n const origin = window.location.origin\n const status = div.find('.sim-status')[0]\n // The subject depends on where in the lineup this page was opened, so\n // cached results from one host must never replay beside another.\n const subject = subjectFlag ? resolveSubject(div) : null\n const cache = (live || subject) ? null : readCache(item)\n // Scope = DSL specs plus roster-page domains (resolved once, shared by modes)\n const specsP = effectiveSpecs(specs, rosterRefs)\n // Seed params for the server report routes \u2014 the stored page vector wins\n // server-side; text is the fallback for remote or unindexed subjects.\n const seedParams = () => subject ? {\n seed: { site: subject.site, slug: subject.slug },\n text: subject.text,\n excludePage: { site: subject.site, slug: subject.slug },\n } : {}\n\n // Standardised pre-search status: what will run, over how much, with what config.\n // e.g. \"Report ready \u2014 18,583 pages across 267 domains \u00B7 threshold 0.68 \u00B7 limit 20 \u00B7 LIVE\"\n const configSummary = (verb, pages, nDomains) => {\n const parts = [`${verb} \u2014 ${pages.toLocaleString()} pages across ${nDomains} domains`]\n if (thresholdSet) parts.push(`threshold ${threshold}`)\n parts.push(`limit ${limit}`)\n if (live) parts.push('LIVE')\n return parts.join(' \u00B7 ')\n }\n\n div.on('dblclick', e => {\n if ($(e.target).closest('.sim-input').length) return\n window.wiki.textEditor(div, item)\n })\n div.on('click', '.sim-link', function (e) {\n e.preventDefault()\n const $a = $(this)\n // shift-click appends at the end of the lineup instead of truncating after this page\n const $page = e.shiftKey ? null : div.parents('.page')\n window.wiki.doInternalLink($a.data('title'), $page, $a.data('site'))\n })\n\n const scopeLabel = !specs.length || (specs.length === 1 && specs[0] === '*')\n ? 'on farm'\n : specs.length === 1 ? `on ${specs[0]}` : 'in domains'\n\n const cacheNote = ts => ts ? ` \u00B7 cached ${cacheAge(ts)}` : ''\n\n if (mode === 'list') {\n const listDiv = div.find('.sim-list')[0]\n\n const renderList = (domains, ts) => {\n const totalPages = domains.reduce((n, d) => n + (d.page_count || 0), 0)\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 ${domains.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\">${domains.length} domains \u2014 ${totalPages.toLocaleString()} pages${cacheNote(ts)}</p>`\n }\n\n if (cache?.domains) {\n renderList(cache.domains, cache.ts)\n } else {\n ;(async () => {\n try {\n const eff = await specsP\n const patterns = eff.length ? eff.join(',') : '*'\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 domains = await res.json()\n if (!domains.length) { status.textContent = 'No indexed domains found'; return }\n renderList(domains, null)\n writeCache(item, { domains })\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n }\n })()\n }\n\n } else if (mode === 'similar') {\n const results = div.find('.sim-results')[0]\n\n const renderScored = (scored, ts) => {\n if (!scored.length) {\n status.textContent = `No similar pages found above threshold ${threshold}`\n return\n }\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}${cacheNote(ts)}</p>`\n status.style.display = 'none'\n }\n\n if (cache?.scored) {\n renderScored(cache.scored, cache.ts)\n } else {\n ;(async () => {\n try {\n // SUBJECT: act on the previous lineup page; otherwise this page.\n const $page = div.parents('.page')\n const s = subject || (() => {\n const pageTitle = $page.find('.title').text().trim() || document.title\n return { slug: slugify(pageTitle), site: window.location.hostname,\n title: pageTitle, text: null, isSelf: true }\n })()\n\n const domainEntries = await loadDomainEntries(await specsP, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = (subject ? `${subjectNote(s)} \u00B7 ` : '') +\n `Searching ${total.toLocaleString()} pages\u2026`\n\n let qVec = await lookupPageVector(s.slug, s.site)\n if (!qVec) {\n status.textContent = 'Embedding page (not yet indexed)\u2026'\n const pageText = s.text || $page.find('.item')\n .map((_, el) => $(el).text().trim()).get().filter(Boolean).join('\\n')\n qVec = await getEmbedding(pageText || s.title, origin)\n }\n\n const scored = cosineScan(qVec, domainEntries, {\n threshold, limit, excludeSlug: s.slug, excludeDomain: s.site,\n })\n renderScored(scored, null)\n if (subject) { // keep the subject visible above the results\n status.textContent = subjectNote(s)\n status.style.display = ''\n }\n if (scored.length && !subject) writeCache(item, { scored })\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n }\n })()\n }\n\n } else if (mode === 'ghost') {\n // Ghost mode \u2014 fetch page-json from any URL and open it as a ghost page\n const btn = div.find('.sim-btn')[0]\n\n const doGhost = async () => {\n if (!ghostUrl) { status.textContent = 'No URL \u2014 GHOST needs a page-json URL'; return }\n btn.disabled = true\n status.textContent = 'Fetching\u2026'\n try {\n const res = await fetch(ghostUrl)\n if (!res.ok) throw new Error(`fetch failed: ${res.status}`)\n const page = await res.json()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n status.textContent = ''\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doGhost)\n\n } else if (mode === 'build') {\n // Build mode \u2014 trigger a semantic index build; result opens as a ghost page\n const btn = div.find('.sim-btn')[0]\n\n const doBuild = async () => {\n btn.disabled = true\n status.textContent = 'Building index\u2026 (may take a while for large scopes)'\n try {\n const domains = encodeURIComponent((specs.length ? specs : ['*']).join(','))\n const res = await fetch(\n `${origin}/system/build-index.json?domains=${domains}&force=${force ? 1 : 0}`)\n if (res.status === 501) {\n const body = await res.json().catch(() => ({}))\n throw new Error(body.hint || 'indexing runs on the farm indexer, not this server')\n }\n if (!res.ok) throw new Error(`build-index failed: ${res.status}`)\n const page = await res.json()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n status.textContent = ''\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doBuild)\n\n } else if (mode === 'report') {\n // Report mode \u2014 server-side ranked/bundled search, opens result as ghost page\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n let readyLine = `Domains: ${specs.length ? specs.join(', ') : '*'}`\n\n // SUBJECT: the host page IS the query \u2014 prefill its title (editable) and\n // seed the server search with its stored vector.\n if (subject && !input.value) input.value = subject.title\n\n // Preload the lightweight domain listing (counts only, no vectors) so the\n // status line shows scope and config before any search is issued.\n ;(async () => {\n try {\n const eff = await specsP\n const domains = await resolveDomains(eff.length ? eff : ['*'], origin)\n const pages = domains.reduce((n, d) => n + (d.page_count || 0), 0)\n readyLine = configSummary('Report ready', pages, domains.length)\n if (farms.length) readyLine += ` \u00B7 +${farms.length} peer farm${farms.length > 1 ? 's' : ''}`\n if (subject) readyLine = `${subjectNote(subject)} \u00B7 ${readyLine}`\n status.textContent = readyLine\n } catch (e) {\n status.textContent = `Domain listing unavailable: ${e.message}`\n }\n })()\n\n const doReport = async () => {\n const query = input.value.trim()\n if (!query) return\n btn.disabled = true\n status.textContent = farms.length ? 'Generating report (asking peer farms)\u2026' : 'Generating report\u2026'\n try {\n const eff = await specsP\n // A hand-edited query is a new question \u2014 drop the page seed then.\n const seeded = subject && query === subject.title ? seedParams() : {}\n const body = { query, domains: eff.length ? eff : ['*'], limit, live, ...seeded }\n if (farms.length) body.farms = farms\n if (thresholdSet) body.threshold = threshold\n const res = await fetch(`${origin}/system/search-report.json`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!res.ok) throw new Error(`search-report failed: ${res.status}`)\n const page = await res.json()\n const pageObj = window.wiki.newPage(page)\n window.wiki.showResult(pageObj, { $page: div.parents('.page') })\n status.textContent = readyLine\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doReport)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doReport() })\n\n } else if (mode === 'sites') {\n // Sites mode \u2014 which site should this page go on? Server aggregates the\n // page-vector scan per domain; result opens as a ghost page.\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n let readyLine = `Domains: ${specs.length ? specs.join(', ') : '*'}`\n\n if (subject && !input.value) input.value = subject.title\n\n ;(async () => {\n try {\n const eff = await specsP\n const domains = await resolveDomains(eff.length ? eff : ['*'], origin)\n const pages = domains.reduce((n, d) => n + (d.page_count || 0), 0)\n readyLine = configSummary('Site report ready', pages, domains.length)\n if (subject) readyLine = `${subjectNote(subject)} \u00B7 ${readyLine}`\n status.textContent = readyLine\n } catch (e) {\n status.textContent = `Domain listing unavailable: ${e.message}`\n }\n })()\n\n const doSites = async () => {\n const query = input.value.trim()\n if (!query) return\n btn.disabled = true\n status.textContent = 'Ranking sites\u2026'\n try {\n const eff = await specsP\n const seeded = subject && query === subject.title ? seedParams() : {}\n const res = await fetch(`${origin}/system/site-report.json`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ query, domains: eff.length ? eff : ['*'], limit, ...seeded }),\n })\n if (!res.ok) throw new Error(`site-report failed: ${res.status}`)\n const page = await res.json()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n status.textContent = readyLine\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doSites)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doSites() })\n\n } else if (mode === 'keyword') {\n // Keyword mode \u2014 galactic MiniSearch: the server reads each site's own\n // per-edit site-index.json; result opens as a ghost page.\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n\n // SUBJECT: keyword search wants words, not vectors \u2014 the host title is\n // the prewired query (editable), finding forks and namesakes alike.\n if (subject && !input.value) input.value = subject.title\n\n status.textContent = (subject ? `${subjectNote(subject)} \u00B7 ` : '') +\n `Keyword search ready \u2014 domains: ${specs.length ? specs.join(', ') : '*'} \u00B7 limit ${limit}`\n\n const doKeyword = async () => {\n const query = input.value.trim()\n if (!query) return\n btn.disabled = true\n status.textContent = 'Searching live site indexes\u2026'\n try {\n const eff = await specsP\n const pattern = encodeURIComponent((eff.length ? eff : ['*']).join(','))\n const farmsParam = farms.length ? `&farms=${encodeURIComponent(farms.join(','))}` : ''\n const res = await fetch(\n `${origin}/system/farm-search.json?q=${encodeURIComponent(query)}&pattern=${pattern}&limit=${limit}${farmsParam}`)\n if (!res.ok) throw new Error(`farm-search failed: ${res.status}`)\n const page = await res.json()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n status.textContent = `Keyword search ready \u2014 domains: ${specs.length ? specs.join(', ') : '*'} \u00B7 limit ${limit}`\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doKeyword)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doKeyword() })\n\n } else if (mode === 'author') {\n // Author mode \u2014 same search form but creates a ghost page from results\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 if (subject && !input.value) input.value = subject.title\n\n ;(async () => {\n try {\n if (!cache) status.textContent = 'Resolving domains\u2026'\n domainEntries = await loadDomainEntries(await specsP, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = (subject ? `${subjectNote(subject)} \u00B7 ` : '') +\n configSummary('Author ready', total, domainEntries.length)\n } catch (e) {\n status.textContent = `Load error: ${e.message}`\n }\n })()\n\n const doAuthor = 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, limit, excludeSlug: null, excludeDomain: null })\n\n // Unique titles for wiki-link list\n const seenTitles = new Set()\n const uniqueTitles = []\n for (const { title } of scored) {\n if (!seenTitles.has(title)) { seenTitles.add(title); uniqueTitles.push(title) }\n }\n\n const hexId = () => Math.floor(Math.random() * 0xffffffffffffffff).toString(16).padStart(16, '0')\n const primaryLines = uniqueTitles.map(t => `- [[${t}]]`).join('\\n')\n\n const story = [\n { type: 'markdown', id: hexId(), text: `# Similar Pages\\n\\n${primaryLines}` },\n { type: 'markdown', id: hexId(), text: '# Reference Links' },\n ...scored.map(({ domain, slug, title, score }) => ({\n type: 'reference', id: hexId(), site: domain, slug, title,\n text: `score ${score.toFixed(3)}`,\n })),\n ]\n\n const pageObj = window.wiki.newPage({ title: `${query} Results`, story, journal: [] })\n window.wiki.showResult(pageObj, { $page: div.parents('.page') })\n\n status.textContent = `${scored.length} pages found`\n writeCache(item, { scored, query })\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doAuthor)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doAuthor() })\n\n } else {\n // Search form mode\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 // Show cached results immediately while domains preload in background\n if (cache?.scored) {\n input.value = cache.query || ''\n results.innerHTML = cache.scored.map(({ domain, slug, title, score }) =>\n `<div class=\"sim-result\">${simLink(domain, slug, title, score)}</div>`).join('') +\n `<p class=\"sim-count\">Top ${cache.scored.length} for \"${cache.query || ''}\"${cacheNote(cache.ts)}</p>`\n status.textContent = ''\n }\n\n if (subject && !input.value) input.value = subject.title\n\n ;(async () => {\n try {\n if (!cache) status.textContent = 'Resolving domains\u2026'\n domainEntries = await loadDomainEntries(await specsP, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = (subject ? `${subjectNote(subject)} \u00B7 ` : '') +\n configSummary('Search ready', total, domainEntries.length)\n } catch (e) {\n status.textContent = `Load error: ${e.message}`\n }\n })()\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, 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 `<p class=\"sim-count\">Top ${scored.length} for \"${query}\"</p>`\n status.textContent = ''\n writeCache(item, { scored, 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": ";MAKA,IAAMA,EAAqB,CAAE,KAAM,IAAM,OAAQ,IAAM,IAAK,GAAK,EAC3DC,EAAqBD,EAAmB,OACxCE,EAAqB,GAWrBC,EAAWC,GAAQ,CACvB,IAAMC,EAAU,CAAC,EACXC,EAAa,CAAC,EACdC,EAAU,CAAC,EACbC,EAAY,KACZC,EAAY,KACZC,EAAY,SACZC,EAAY,GACZC,EAAY,GACZC,EAAY,GACZC,EAAY,KACZC,EAAY,KAKVC,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,KAAOjB,EAAK,MAAM;AAAA,CAAI,EAAG,CAClC,IAAMgB,EAAOC,EAAI,KAAK,EACtB,GAAI,CAACD,GAAQA,EAAK,WAAW,GAAG,EAAG,SAEnC,IAAMH,EAAQG,EAAK,YAAY,EAC/B,GAAIJ,EAAMC,EAAO,MAAM,EAAI,CAAEN,EAAO,GAAM,QAAS,CACnD,GAAIK,EAAMC,EAAO,SAAS,EAAG,CAAEL,EAAU,GAAM,QAAS,CACxD,GAAII,EAAMC,EAAO,QAAQ,EAAG,CACtB,CAACZ,EAAM,QAAUK,IAAS,WAAUA,EAAO,UAC/C,QACF,CACA,GAAIM,EAAMC,EAAO,QAAQ,EAAG,CACtBP,IAAS,WAAUA,EAAO,UAC9B,QACF,CACA,GAAIM,EAAMC,EAAO,SAAS,EAAG,CACvBP,IAAS,WAAUA,EAAO,WAC9B,QACF,CACA,GAAIM,EAAMC,EAAO,OAAO,EAAG,CACrBP,IAAS,WAAUA,EAAO,SAC9B,QACF,CACA,GAAIM,EAAMC,EAAO,OAAO,EAAG,CACrBP,IAAS,WAAUA,EAAO,SAC9B,QACF,CACA,GAAIM,EAAMC,EAAO,OAAO,EAAG,CAAEJ,EAAQ,GAAM,QAAS,CACpD,GAAIG,EAAMC,EAAO,OAAO,EAAG,CACzBH,EAAWK,EAAIC,EAAM,OAAO,EACxBV,IAAS,WAAUA,EAAO,SAC9B,QACF,CACA,GAAIM,EAAMC,EAAO,QAAQ,EAAG,CAAEF,EAAQI,EAAIC,EAAM,QAAQ,EAAG,QAAS,CACpE,GAAIJ,EAAMC,EAAO,QAAQ,EAAG,CAC1B,IAAMK,EAAMH,EAAIC,EAAM,QAAQ,EAC1BE,GAAKhB,EAAW,KAAKgB,CAAG,EAC5B,QACF,CACA,GAAIN,EAAMC,EAAO,MAAM,EAAG,CACxB,IAAMM,EAAOJ,EAAIC,EAAM,MAAM,EACzBG,GAAMhB,EAAM,KAAKgB,CAAI,EACzB,QACF,CACA,GAAIP,EAAMC,EAAO,MAAM,EAAG,CACpB,CAACZ,EAAM,QAAUK,IAAS,WAAUA,EAAO,QAC/C,QACF,CACA,GAAIM,EAAMC,EAAO,SAAS,EAAG,CAC3B,IAAMO,EAAQL,EAAIF,EAAO,SAAS,EAAE,YAAY,EAChDT,EAAYR,EAAmBwB,CAAK,GAAKvB,EACrC,CAACI,EAAM,QAAUK,IAAS,WAAUA,EAAO,WAC/C,QACF,CACA,GAAIM,EAAMC,EAAO,WAAW,EAAG,CAC7B,IAAMQ,EAAKN,EAAIC,EAAM,WAAW,EAChCZ,EAAYR,EAAmByB,EAAG,YAAY,CAAC,IAAM,WAAWA,CAAE,GAAKxB,GACvE,QACF,CACA,GAAIe,EAAMC,EAAO,OAAO,EAAG,CACzBR,EAAQ,SAASU,EAAIC,EAAM,OAAO,CAAC,GAAKlB,EACxC,QACF,CAEAG,EAAM,KAAK,CAAC,SAAU,QAAS,UAAW,QAAQ,EAAE,SAASY,CAAK,EAAIA,EAAQG,CAAI,CACpF,CAEA,MAAO,CACL,KAAAV,EACA,MAAAL,EACA,WAAAC,EACA,MAAAC,EACA,UAAWC,GAAaP,EACxB,MAAWQ,GAAaP,EACxB,KAAAS,EACA,QAAAC,EACA,MAAAC,EACA,SAAAC,EACA,MAAAC,EACA,aAAcP,IAAc,IAC9B,CACF,EAEMkB,EAASC,GAAQA,EAAK,SAAS,GAAG,GAAKA,EAAK,SAAS,GAAG,EAKxDC,EAAUD,GAAQA,IAAS,UAAYA,IAAS,SAAWA,IAAS,WACxEA,IAAS,SCzHX,IAAME,EAAUC,GAASA,EAAM,YAAY,EAAE,QAAQ,OAAQ,GAAG,EAAE,QAAQ,cAAe,EAAE,EAIrFC,EAAc,IAAI,IAElBC,GAAwB,MAAOC,EAAMC,IAAW,CACpD,GAAIH,EAAY,IAAIE,CAAI,EAAG,OAAOF,EAAY,IAAIE,CAAI,EACtD,IAAME,EAAM,GAAGD,CAAM,wCAAwC,mBAAmBD,CAAI,CAAC,GAC/EG,EAAM,MAAM,MAAMD,CAAG,EAC3B,GAAI,CAACC,EAAI,GAAI,MAAM,IAAI,MAAM,2BAA2BA,EAAI,MAAM,EAAE,EACpE,IAAMC,EAAO,MAAMD,EAAI,KAAK,EAC5B,OAAAL,EAAY,IAAIE,EAAMI,CAAI,EACnBA,CACT,EAEMC,EAAiB,MAAOC,EAAOL,IAAW,CACzCK,EAAM,SAAQA,EAAQ,CAAC,OAAO,SAAS,QAAQ,GACpD,IAAMC,EAAO,IAAI,IACXC,EAAS,CAAC,EAChB,QAAWR,KAAQM,EACjB,GAAIN,IAAS,KAAOS,EAAOT,CAAI,GAAKU,EAAQV,CAAI,EAC9C,QAAWW,KAAQ,MAAMZ,GAAsBC,EAAMC,CAAM,EACpDM,EAAK,IAAII,EAAK,MAAM,IAAKJ,EAAK,IAAII,EAAK,MAAM,EAAGH,EAAO,KAAKG,CAAI,QAE7DJ,EAAK,IAAIP,CAAI,IACvBO,EAAK,IAAIP,CAAI,EAAGQ,EAAO,KAAK,CAAE,OAAQR,EAAM,WAAY,IAAK,CAAC,GAGlE,OAAOQ,CACT,EASMI,GAAe,uDACfC,GAAe,0CACfC,GAAe,8CAEfC,EAAY,MAAMC,GAAO,CAC7B,IAAMC,EAAID,EAAI,QAAQ,GAAG,EACnBb,EAAM,MAAM,MAAM,KAAKa,EAAI,MAAM,EAAGC,CAAC,CAAC,IAAID,EAAI,MAAMC,EAAI,CAAC,CAAC,OAAO,EACvE,GAAI,CAACd,EAAI,GAAI,MAAM,IAAI,MAAM,eAAea,CAAG,YAAYb,EAAI,MAAM,EAAE,EACvE,OAAOA,EAAI,KAAK,CAClB,EAEMe,EAAgB,MAAOF,EAAKG,EAAKC,IAAY,CACjD,GAAIA,EAAQ,IAAIJ,CAAG,EAAG,OACtBI,EAAQ,IAAIJ,CAAG,EACf,IAAIK,EACJ,GAAI,CAAEA,EAAO,MAAMN,EAAUC,CAAG,CAAE,OAASM,EAAG,CAAE,QAAQ,KAAKA,EAAE,OAAO,EAAG,MAAO,CAChF,QAAWX,KAAQU,EAAK,OAAS,CAAC,EAChC,GAAIV,EAAK,OAAS,SAClB,QAAWY,KAAQZ,EAAK,MAAQ,IAAI,MAAM,OAAO,EAAG,CAClD,IAAMa,EAAOD,EAAI,KAAK,EACtB,GAAI,CAACC,EAAM,SACX,IAAMC,EAAOD,EAAK,MAAMZ,EAAS,EACjC,GAAIa,EAAM,CAAEN,EAAI,IAAIM,EAAK,CAAC,CAAC,EAAG,QAAS,CACvC,IAAMC,EAASF,EAAK,MAAMX,EAAW,EACrC,GAAIa,EAAQ,CAAE,MAAMR,EAAcQ,EAAO,CAAC,EAAGP,EAAKC,CAAO,EAAG,QAAS,CACrE,IAAMO,EAAOH,EAAK,MAAMV,EAAS,EACjC,GAAIa,EACF,GAAI,CACF,IAAMC,EAAU,MAAMb,EAAUY,EAAK,CAAC,CAAC,EACvC,QAAWE,KAAMD,EAAQ,OAAS,CAAC,EAC7BC,EAAG,OAAS,aAAeA,EAAG,MAAMV,EAAI,IAAIU,EAAG,IAAI,CAE3D,OAASP,EAAG,CAAE,QAAQ,KAAKA,EAAE,OAAO,CAAE,CAG1C,CAEJ,EAEMQ,GAAiB,MAAMC,GAAc,CACzC,IAAMZ,EAAM,IAAI,IACVC,EAAU,IAAI,IACpB,QAAWJ,KAAOe,EAAY,MAAMb,EAAcF,EAAKG,EAAKC,CAAO,EACnE,MAAO,CAAC,GAAGD,CAAG,CAChB,EAGMa,GAAiB,MAAO1B,EAAOyB,IACnCA,EAAW,OAAS,CAAC,GAAGzB,EAAO,GAAG,MAAMwB,GAAeC,CAAU,CAAC,EAAIzB,ECvFxE,IAAM2B,EAAc,IAAI,IAKlBC,GAAYC,GAChB,GAAG,OAAO,SAAS,MAAM,wCAAwC,mBAAmBA,CAAM,CAAC,GAEvFC,GAAc,MAAMD,GAAU,CAClC,GAAIF,EAAY,IAAIE,CAAM,EAAG,OAAOF,EAAY,IAAIE,CAAM,EAC1D,IAAME,EAAM,MAAM,MAAMH,GAAUC,CAAM,CAAC,EACzC,GAAI,CAACE,EAAI,GAAI,MAAO,CAAC,EACrB,IAAMC,EAAO,MAAMD,EAAI,KAAK,EAC5B,OAAAJ,EAAY,IAAIE,EAAQG,CAAI,EACrBA,CACT,EAMMC,EAAe,MAAOC,EAAMC,IAAW,CAC3C,IAAMJ,EAAM,MAAM,MAAM,GAAGI,CAAM,qBAAsB,CACrD,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CAAE,KAAAD,CAAK,CAAC,CAC/B,CAAC,EACD,GAAI,CAACH,EAAI,GAAI,MAAM,IAAI,MAAM,iBAAiBA,EAAI,MAAM,EAAE,EAC1D,OAAQ,MAAMA,EAAI,KAAK,GAAG,MAC5B,EAIMK,GAAmB,MAAOC,EAAMR,IAAW,CAE/C,IAAMS,GADQ,MAAMR,GAAYD,CAAM,GAClB,KAAK,GAAK,EAAE,OAASQ,CAAI,EAC7C,OAAOC,EAAQA,EAAM,OAAS,IAChC,EAGMC,EAAa,CAACC,EAAUC,EAAe,CAAE,UAAAC,EAAW,MAAAC,EAAO,YAAAC,EAAa,cAAAC,CAAc,IAAM,CAChG,IAAMC,EAAU,CAAC,EACjB,OAAW,CAAE,OAAAjB,EAAQ,MAAAkB,CAAM,IAAKN,EAC9B,OAAW,CAAE,KAAAJ,EAAM,MAAAW,EAAO,OAAAC,CAAO,IAAKF,EAAO,CAC3C,GAAIV,IAASO,GAAef,IAAWgB,EAAe,SACtD,IAAIK,EAAM,EACV,QAASC,EAAI,EAAGA,EAAIX,EAAS,OAAQW,IAAKD,GAAOV,EAASW,CAAC,EAAIF,EAAOE,CAAC,EACnED,GAAOR,GAAWI,EAAQ,KAAK,CAAE,OAAAjB,EAAQ,KAAAQ,EAAM,MAAAW,EAAO,MAAOE,CAAI,CAAC,CACxE,CAEF,OAAAJ,EAAQ,KAAK,CAACM,EAAGC,IAAMA,EAAE,MAAQD,EAAE,KAAK,EACjCN,EAAQ,MAAM,EAAGH,CAAK,CAC/B,EAIMW,EAAoB,MAAOC,EAAOpB,IAAW,CACjD,IAAMqB,EAAU,MAAMC,EAAeF,EAAOpB,CAAM,EAIlD,OAHgB,MAAM,QAAQ,IAC5BqB,EAAQ,IAAI,MAAO,CAAE,OAAA3B,CAAO,KAAO,CAAE,OAAAA,EAAQ,MAAO,MAAMC,GAAYD,CAAM,CAAE,EAAE,CAClF,GACe,OAAO6B,GAAKA,EAAE,MAAM,OAAS,CAAC,CAC/C,ECzDA,IAAMC,GAAiBC,GAAO,CAC5B,IAAMC,EAAQD,EAAI,QAAQ,aAAa,EACjCE,EAAQD,EAAM,KAAK,OAAO,EAC1BE,EAAQD,EAAM,OAASA,EAAQD,EAC/BG,GAASD,EAAM,KAAK,IAAI,GAAK,IAAI,MAAM,MAAM,EAAE,CAAC,EAChDE,EAAQF,EAAM,KAAK,MAAM,GAAK,OAAO,SAAS,SAC9CG,EAAQH,EAAM,KAAK,MAAM,GAAK,CAAC,EAC/BI,EAAQD,EAAK,OAASH,EAAM,KAAK,QAAQ,EAAE,KAAK,EAAE,KAAK,GAAKC,EAC5DI,EAAQ,CAACD,EAAO,IAAID,EAAK,OAAS,CAAC,GACtC,OAAOG,GAAKA,EAAE,OAAS,YAAcA,EAAE,OAAS,WAAW,EAC3D,IAAIA,IAAMA,EAAE,MAAQ,IAAI,KAAK,CAAC,EAAE,OAAO,OAAO,CAAC,EAAE,KAAK;AAAA,CAAI,EAAE,MAAM,EAAG,GAAI,EAC5E,MAAO,CAAE,KAAAL,EAAM,KAAAC,EAAM,MAAAE,EAAO,KAAAC,EAAM,OAAQ,CAACN,EAAM,MAAO,CAC1D,EAEMQ,EAAcC,GAClB,YAAYA,EAAE,KAAK,GAAGA,EAAE,OAAS,eAAiB,MAAMA,EAAE,IAAI,EAAE,GCpBlE,IAAMC,GAAWC,GAAM,aAAaA,CAAE,GAEhCC,GAAYC,GAAQ,CACxB,GAAI,CACF,IAAMC,EAAI,KAAK,MAAM,aAAa,QAAQJ,GAASG,EAAK,EAAE,CAAC,GAAK,MAAM,EACtE,OAAOC,GAAG,QAAUD,EAAK,MAAQ,IAAMC,EAAI,IAC7C,MAAQ,CAAE,OAAO,IAAK,CACxB,EAEMC,EAAa,CAACF,EAAMG,IAAS,CACjC,GAAI,CACF,aAAa,QAAQN,GAASG,EAAK,EAAE,EAAG,KAAK,UAAU,CACrD,KAAMA,EAAK,MAAQ,GACnB,GAAM,KAAK,IAAI,EACf,GAAGG,CACL,CAAC,CAAC,CACJ,MAAQ,CAAoC,CAC9C,EAEMC,GAAWC,GAAM,CACrB,IAAMC,EAAI,KAAK,OAAO,KAAK,IAAI,EAAID,GAAM,GAAI,EAC7C,OAAIC,EAAI,GAAc,GAAGA,CAAC,QACtBA,EAAI,KAAc,GAAG,KAAK,MAAMA,EAAI,EAAE,CAAC,QACvCA,EAAI,MAAc,GAAG,KAAK,MAAMA,EAAI,IAAI,CAAC,QACtC,GAAG,KAAK,MAAMA,EAAI,KAAK,CAAC,OACjC,EC3BA,IAAMC,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,GAAW,CAACC,EAAQC,IACxB,qCAAqC,OAAO,KAAK,KAAKD,CAAM,EAAE,KAAK,CAAC;AAAA,iBACrDA,CAAM,iBAAYC,EAAM,QAAQ,CAAC,CAAC;AAAA,qBAC9BD,CAAM,KCe3B,IAAME,EAAU,CAACC,EAAQC,EAAMC,EAAOC,IACpC,mCAAmCD,CAAK,gBAAgBD,CAAI,gBAAgBD,CAAM,cAC/EI,GAASJ,EAAQG,CAAK,CAAC,IAAID,CAAK,OAExBG,GAAO,CAACC,EAAKC,IAAS,CACjC,GAAM,CAAE,KAAAC,EAAM,MAAAC,EAAO,UAAAC,EAAW,MAAAC,EAAO,MAAAC,EAAO,SAAAC,EAAU,MAAAC,CAAM,EAAIC,EAASR,GAAM,MAAQ,EAAE,EAC3F,GAAIC,IAAS,QACXF,EAAI,KAAK;AAAA,eACEU,CAAM;AAAA,yCACoBT,EAAK,EAAE;AAAA;AAAA,oCAEZO,GAAS,MAAM;AAAA;AAAA;AAAA,aAGtC,UACAN,IAAS,QAClBF,EAAI,KAAK;AAAA,eACEU,CAAM;AAAA,yCACoBT,EAAK,EAAE;AAAA;AAAA,oCAEZO,GAAS,SAASL,EAAM,OAASA,EAAM,KAAK,IAAI,EAAI,GAAG,GAAGG,EAAQ,WAAa,EAAE,EAAE;AAAA;AAAA;AAAA,aAG1G,UACAJ,IAAS,OAAQ,CAC1B,IAAMM,EAAQL,EAAM,OAASA,EAAM,KAAK,IAAI,EAAI,IAChDH,EAAI,KAAK;AAAA,eACEU,CAAM;AAAA,yCACoBT,EAAK,EAAE;AAAA,2DACWO,CAAK;AAAA;AAAA,aAEnD,CACX,SAAWN,IAAS,UAAW,CAC7B,IAAMM,EAAQL,EAAM,OAASA,EAAM,KAAK,IAAI,EAAI,iBAChDH,EAAI,KAAK;AAAA,eACEU,CAAM;AAAA,yCACoBT,EAAK,EAAE;AAAA,+DACeO,CAAK;AAAA;AAAA,aAEvD,CACX,SAAWN,IAAS,UAAYA,IAAS,UAAYA,IAAS,WAAaA,IAAS,QAAS,CAC3F,IAAMM,EAAQL,EAAM,OAASA,EAAM,KAAK,IAAI,EAAI,mBAC1CQ,EAAWT,IAAS,SAAW,SAAWA,IAAS,UAAY,UACjEA,IAAS,QAAU,QAAU,SAC3BU,EAAOV,IAAS,QAAU,2DAC5B,0BACJF,EAAI,KAAK;AAAA,eACEU,CAAM;AAAA,yCACoBT,EAAK,EAAE;AAAA;AAAA,8DAEcW,CAAI;AAAA,oCAC9BD,CAAQ;AAAA;AAAA,2CAEDH,CAAK;AAAA;AAAA,aAEnC,CACX,KAAO,CACL,IAAMA,EAAQL,EAAM,OAASA,EAAM,KAAK,IAAI,EAAI,mBAChDH,EAAI,KAAK;AAAA,eACEU,CAAM;AAAA,yCACoBT,EAAK,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA,2CAKLO,CAAK;AAAA;AAAA,aAEnC,CACX,CACF,EAEaK,GAAO,CAACb,EAAKC,IAAS,CACjC,GAAM,CAAE,KAAAC,EAAM,MAAAC,EAAO,WAAAW,EAAY,MAAAC,EAAO,UAAAX,EAAW,MAAAC,EAAO,KAAAW,EAAM,QAASC,EACvE,MAAAX,EAAO,SAAAC,EAAU,aAAAW,CAAa,EAAIT,EAASR,GAAM,MAAQ,EAAE,EACvDkB,EAAU,OAAO,SAAS,OAC1BC,EAAUpB,EAAI,KAAK,aAAa,EAAE,CAAC,EAGnCqB,EAAUJ,EAAcK,GAAetB,CAAG,EAAI,KAC9CuB,EAAWP,GAAQK,EAAW,KAAOG,GAAUvB,CAAI,EAEnDwB,EAAUC,GAAevB,EAAOW,CAAU,EAG1Ca,EAAa,IAAMN,EAAU,CACjC,KAAM,CAAE,KAAMA,EAAQ,KAAM,KAAMA,EAAQ,IAAK,EAC/C,KAAMA,EAAQ,KACd,YAAa,CAAE,KAAMA,EAAQ,KAAM,KAAMA,EAAQ,IAAK,CACxD,EAAI,CAAC,EAICO,EAAgB,CAACC,EAAMC,EAAOC,IAAa,CAC/C,IAAMC,EAAQ,CAAC,GAAGH,CAAI,WAAMC,EAAM,eAAe,CAAC,iBAAiBC,CAAQ,UAAU,EACrF,OAAIb,GAAcc,EAAM,KAAK,aAAa5B,CAAS,EAAE,EACrD4B,EAAM,KAAK,SAAS3B,CAAK,EAAE,EACvBW,GAAMgB,EAAM,KAAK,MAAM,EACpBA,EAAM,KAAK,QAAK,CACzB,EAEAhC,EAAI,GAAG,WAAYiC,GAAK,CAClB,EAAEA,EAAE,MAAM,EAAE,QAAQ,YAAY,EAAE,QACtC,OAAO,KAAK,WAAWjC,EAAKC,CAAI,CAClC,CAAC,EACDD,EAAI,GAAG,QAAS,YAAa,SAAUiC,EAAG,CACxCA,EAAE,eAAe,EACjB,IAAMC,EAAK,EAAE,IAAI,EAEXC,EAAQF,EAAE,SAAW,KAAOjC,EAAI,QAAQ,OAAO,EACrD,OAAO,KAAK,eAAekC,EAAG,KAAK,OAAO,EAAGC,EAAOD,EAAG,KAAK,MAAM,CAAC,CACrE,CAAC,EAED,IAAME,GAAa,CAACjC,EAAM,QAAWA,EAAM,SAAW,GAAKA,EAAM,CAAC,IAAM,IACpE,UACAA,EAAM,SAAW,EAAI,MAAMA,EAAM,CAAC,CAAC,GAAK,aAEtCkC,EAAYC,GAAMA,EAAK,gBAAaC,GAASD,CAAE,CAAC,GAAK,GAE3D,GAAIpC,IAAS,OAAQ,CACnB,IAAMsC,EAAUxC,EAAI,KAAK,WAAW,EAAE,CAAC,EAEjCyC,EAAa,CAACC,EAASJ,IAAO,CAClC,IAAMK,EAAaD,EAAQ,OAAO,CAAC,EAAGE,IAAM,GAAKA,EAAE,YAAc,GAAI,CAAC,EACtExB,EAAO,MAAM,QAAU,OACvBoB,EAAQ,UAAY;AAAA;AAAA;AAAA,YAGdE,EAAQ,IAAI,CAAC,CAAE,OAAAhD,EAAQ,WAAAmD,CAAW,IAAM;AAAA;AAAA,sDAEE,OAAO,KAAK,KAAKnD,CAAM,EAAE,KAAK,CAAC;AAAA,gCACrDA,CAAM,gBAAgBA,CAAM,MAAMA,CAAM;AAAA,oBACpDmD,GAAc,KAAOA,EAAW,eAAe,EAAI,QAAG;AAAA,kBACxD,EAAE,KAAK,EAAE,CAAC;AAAA;AAAA,+BAEGH,EAAQ,MAAM,mBAAcC,EAAW,eAAe,CAAC,SAASN,EAAUC,CAAE,CAAC,MACxG,EAEIf,GAAO,QACTkB,EAAWlB,EAAM,QAASA,EAAM,EAAE,GAEhC,SAAY,CACZ,GAAI,CACF,IAAMuB,EAAM,MAAMrB,EACZsB,EAAWD,EAAI,OAASA,EAAI,KAAK,GAAG,EAAI,IACxCE,EAAM,GAAG7B,CAAM,wCAAwC,mBAAmB4B,CAAQ,CAAC,UAAU1C,CAAK,GAClG4C,EAAM,MAAM,MAAMD,CAAG,EAC3B,GAAI,CAACC,EAAI,GAAI,MAAM,IAAI,MAAM,2BAA2BA,EAAI,MAAM,EAAE,EACpE,IAAMP,EAAU,MAAMO,EAAI,KAAK,EAC/B,GAAI,CAACP,EAAQ,OAAQ,CAAEtB,EAAO,YAAc,2BAA4B,MAAO,CAC/EqB,EAAWC,EAAS,IAAI,EACxBQ,EAAWjD,EAAM,CAAE,QAAAyC,CAAQ,CAAC,CAC9B,OAAST,EAAG,CACVb,EAAO,YAAc,UAAUa,EAAE,OAAO,EAC1C,CACF,GAAG,CAGP,SAAW/B,IAAS,UAAW,CAC7B,IAAMiD,EAAUnD,EAAI,KAAK,cAAc,EAAE,CAAC,EAEpCoD,EAAe,CAACC,EAAQf,IAAO,CACnC,GAAI,CAACe,EAAO,OAAQ,CAClBjC,EAAO,YAAc,0CAA0ChB,CAAS,GACxE,MACF,CACA+C,EAAQ,UAAY,6BAClBE,EAAO,IAAI,CAAC,CAAE,OAAA3D,EAAQ,KAAAC,EAAM,MAAAC,EAAO,MAAAC,CAAM,IACvC,OAAOJ,EAAQC,EAAQC,EAAMC,EAAOC,CAAK,CAAC,OAAO,EAAE,KAAK,EAAE,CAC9D,6BAA6BwD,EAAO,MAAM,UAAUjB,EAAU,GAAGC,EAAUC,CAAE,CAAC,OAC9ElB,EAAO,MAAM,QAAU,MACzB,EAEIG,GAAO,OACT6B,EAAa7B,EAAM,OAAQA,EAAM,EAAE,GAEjC,SAAY,CACZ,GAAI,CAEF,IAAMY,EAAQnC,EAAI,QAAQ,OAAO,EAC3BsD,EAAIjC,IAAY,IAAM,CAC1B,IAAMkC,EAAYpB,EAAM,KAAK,QAAQ,EAAE,KAAK,EAAE,KAAK,GAAK,SAAS,MACjE,MAAO,CAAE,KAAMqB,EAAQD,CAAS,EAAG,KAAM,OAAO,SAAS,SACvD,MAAOA,EAAW,KAAM,KAAM,OAAQ,EAAK,CAC/C,GAAG,EAEGE,EAAgB,MAAMC,EAAkB,MAAMjC,EAAQN,CAAM,EAC5DwC,EAAQF,EAAc,OAAO,CAACG,EAAG3B,IAAM2B,EAAI3B,EAAE,MAAM,OAAQ,CAAC,EAClEb,EAAO,aAAeC,EAAU,GAAGwC,EAAYP,CAAC,CAAC,SAAQ,IACvD,aAAaK,EAAM,eAAe,CAAC,eAErC,IAAIG,EAAO,MAAMC,GAAiBT,EAAE,KAAMA,EAAE,IAAI,EAChD,GAAI,CAACQ,EAAM,CACT1C,EAAO,YAAc,yCACrB,IAAM4C,EAAWV,EAAE,MAAQnB,EAAM,KAAK,OAAO,EAC1C,IAAI,CAAC8B,EAAGC,IAAO,EAAEA,CAAE,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE,OAAO,OAAO,EAAE,KAAK;AAAA,CAAI,EACtEJ,EAAO,MAAMK,EAAaH,GAAYV,EAAE,MAAOnC,CAAM,CACvD,CAEA,IAAMkC,EAASe,EAAWN,EAAML,EAAe,CAC7C,UAAArD,EAAW,MAAAC,EAAO,YAAaiD,EAAE,KAAM,cAAeA,EAAE,IAC1D,CAAC,EACDF,EAAaC,EAAQ,IAAI,EACrBhC,IACFD,EAAO,YAAcyC,EAAYP,CAAC,EAClClC,EAAO,MAAM,QAAU,IAErBiC,EAAO,QAAU,CAAChC,GAAS6B,EAAWjD,EAAM,CAAE,OAAAoD,CAAO,CAAC,CAC5D,OAASpB,EAAG,CACVb,EAAO,YAAc,UAAUa,EAAE,OAAO,EAC1C,CACF,GAAG,CAGP,SAAW/B,IAAS,QAAS,CAE3B,IAAMmE,EAAMrE,EAAI,KAAK,UAAU,EAAE,CAAC,EAE5BsE,EAAU,SAAY,CAC1B,GAAI,CAAC/D,EAAU,CAAEa,EAAO,YAAc,4CAAwC,MAAO,CACrFiD,EAAI,SAAW,GACfjD,EAAO,YAAc,iBACrB,GAAI,CACF,IAAM6B,EAAM,MAAM,MAAM1C,CAAQ,EAChC,GAAI,CAAC0C,EAAI,GAAI,MAAM,IAAI,MAAM,iBAAiBA,EAAI,MAAM,EAAE,EAC1D,IAAMsB,EAAO,MAAMtB,EAAI,KAAK,EAC5B,OAAO,KAAK,WAAW,OAAO,KAAK,QAAQsB,CAAI,EAAG,CAAE,MAAOvE,EAAI,QAAQ,OAAO,CAAE,CAAC,EACjFoB,EAAO,YAAc,EACvB,OAASa,EAAG,CACVb,EAAO,YAAc,UAAUa,EAAE,OAAO,EAC1C,QAAE,CACAoC,EAAI,SAAW,EACjB,CACF,EAEAA,EAAI,iBAAiB,QAASC,CAAO,CAEvC,SAAWpE,IAAS,QAAS,CAE3B,IAAMmE,EAAMrE,EAAI,KAAK,UAAU,EAAE,CAAC,EAE5BwE,EAAU,SAAY,CAC1BH,EAAI,SAAW,GACfjD,EAAO,YAAc,2DACrB,GAAI,CACF,IAAMsB,EAAU,oBAAoBvC,EAAM,OAASA,EAAQ,CAAC,GAAG,GAAG,KAAK,GAAG,CAAC,EACrE8C,EAAM,MAAM,MAChB,GAAG9B,CAAM,oCAAoCuB,CAAO,UAAUpC,EAAQ,EAAI,CAAC,EAAE,EAC/E,GAAI2C,EAAI,SAAW,IAAK,CACtB,IAAMwB,EAAO,MAAMxB,EAAI,KAAK,EAAE,MAAM,KAAO,CAAC,EAAE,EAC9C,MAAM,IAAI,MAAMwB,EAAK,MAAQ,oDAAoD,CACnF,CACA,GAAI,CAACxB,EAAI,GAAI,MAAM,IAAI,MAAM,uBAAuBA,EAAI,MAAM,EAAE,EAChE,IAAMsB,EAAO,MAAMtB,EAAI,KAAK,EAC5B,OAAO,KAAK,WAAW,OAAO,KAAK,QAAQsB,CAAI,EAAG,CAAE,MAAOvE,EAAI,QAAQ,OAAO,CAAE,CAAC,EACjFoB,EAAO,YAAc,EACvB,OAASa,EAAG,CACVb,EAAO,YAAc,UAAUa,EAAE,OAAO,EAC1C,QAAE,CACAoC,EAAI,SAAW,EACjB,CACF,EAEAA,EAAI,iBAAiB,QAASG,CAAO,CAEvC,SAAWtE,IAAS,SAAU,CAE5B,IAAMwE,EAAQ1E,EAAI,KAAK,YAAY,EAAE,CAAC,EAChCqE,EAAQrE,EAAI,KAAK,UAAU,EAAE,CAAC,EAChC2E,EAAY,YAAYxE,EAAM,OAASA,EAAM,KAAK,IAAI,EAAI,GAAG,GAI7DkB,GAAW,CAACqD,EAAM,QAAOA,EAAM,MAAQrD,EAAQ,QAIjD,SAAY,CACZ,GAAI,CACF,IAAMyB,EAAM,MAAMrB,EACZiB,EAAU,MAAMkC,EAAe9B,EAAI,OAASA,EAAM,CAAC,GAAG,EAAG3B,CAAM,EAC/DW,EAAQY,EAAQ,OAAO,CAACkB,EAAGhB,IAAMgB,GAAKhB,EAAE,YAAc,GAAI,CAAC,EACjE+B,EAAY/C,EAAc,eAAgBE,EAAOY,EAAQ,MAAM,EAC3D3B,EAAM,SAAQ4D,GAAa,UAAO5D,EAAM,MAAM,aAAaA,EAAM,OAAS,EAAI,IAAM,EAAE,IACtFM,IAASsD,EAAY,GAAGd,EAAYxC,CAAO,CAAC,SAAMsD,CAAS,IAC/DvD,EAAO,YAAcuD,CACvB,OAAS1C,EAAG,CACVb,EAAO,YAAc,+BAA+Ba,EAAE,OAAO,EAC/D,CACF,GAAG,EAEH,IAAM4C,EAAW,SAAY,CAC3B,IAAMC,EAAQJ,EAAM,MAAM,KAAK,EAC/B,GAAKI,EACL,CAAAT,EAAI,SAAW,GACfjD,EAAO,YAAcL,EAAM,OAAS,8CAA2C,0BAC/E,GAAI,CACF,IAAM+B,EAAM,MAAMrB,EAEZsD,EAAS1D,GAAWyD,IAAUzD,EAAQ,MAAQM,EAAW,EAAI,CAAC,EAC9D8C,EAAO,CAAE,MAAAK,EAAO,QAAShC,EAAI,OAASA,EAAM,CAAC,GAAG,EAAG,MAAAzC,EAAO,KAAAW,EAAM,GAAG+D,CAAO,EAC5EhE,EAAM,SAAQ0D,EAAK,MAAQ1D,GAC3BG,IAAcuD,EAAK,UAAYrE,GACnC,IAAM6C,EAAM,MAAM,MAAM,GAAG9B,CAAM,6BAA8B,CAC7D,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAUsD,CAAI,CAC3B,CAAC,EACD,GAAI,CAACxB,EAAI,GAAI,MAAM,IAAI,MAAM,yBAAyBA,EAAI,MAAM,EAAE,EAClE,IAAMsB,EAAO,MAAMtB,EAAI,KAAK,EACtB+B,EAAU,OAAO,KAAK,QAAQT,CAAI,EACxC,OAAO,KAAK,WAAWS,EAAS,CAAE,MAAOhF,EAAI,QAAQ,OAAO,CAAE,CAAC,EAC/DoB,EAAO,YAAcuD,CACvB,OAAS1C,EAAG,CACVb,EAAO,YAAc,UAAUa,EAAE,OAAO,EAC1C,QAAE,CACAoC,EAAI,SAAW,EACjB,EACF,EAEAA,EAAI,iBAAiB,QAASQ,CAAQ,EACtCH,EAAM,iBAAiB,UAAWzC,GAAK,CAAMA,EAAE,MAAQ,SAAS4C,EAAS,CAAE,CAAC,CAE9E,SAAW3E,IAAS,QAAS,CAG3B,IAAMwE,EAAQ1E,EAAI,KAAK,YAAY,EAAE,CAAC,EAChCqE,EAAQrE,EAAI,KAAK,UAAU,EAAE,CAAC,EAChC2E,EAAY,YAAYxE,EAAM,OAASA,EAAM,KAAK,IAAI,EAAI,GAAG,GAE7DkB,GAAW,CAACqD,EAAM,QAAOA,EAAM,MAAQrD,EAAQ,QAEjD,SAAY,CACZ,GAAI,CACF,IAAMyB,EAAM,MAAMrB,EACZiB,EAAU,MAAMkC,EAAe9B,EAAI,OAASA,EAAM,CAAC,GAAG,EAAG3B,CAAM,EAC/DW,EAAQY,EAAQ,OAAO,CAACkB,EAAGhB,IAAMgB,GAAKhB,EAAE,YAAc,GAAI,CAAC,EACjE+B,EAAY/C,EAAc,oBAAqBE,EAAOY,EAAQ,MAAM,EAChErB,IAASsD,EAAY,GAAGd,EAAYxC,CAAO,CAAC,SAAMsD,CAAS,IAC/DvD,EAAO,YAAcuD,CACvB,OAAS1C,EAAG,CACVb,EAAO,YAAc,+BAA+Ba,EAAE,OAAO,EAC/D,CACF,GAAG,EAEH,IAAMgD,EAAU,SAAY,CAC1B,IAAMH,EAAQJ,EAAM,MAAM,KAAK,EAC/B,GAAKI,EACL,CAAAT,EAAI,SAAW,GACfjD,EAAO,YAAc,sBACrB,GAAI,CACF,IAAM0B,EAAM,MAAMrB,EACZsD,EAAS1D,GAAWyD,IAAUzD,EAAQ,MAAQM,EAAW,EAAI,CAAC,EAC9DsB,EAAM,MAAM,MAAM,GAAG9B,CAAM,2BAA4B,CAC3D,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CAAE,MAAA2D,EAAO,QAAShC,EAAI,OAASA,EAAM,CAAC,GAAG,EAAG,MAAAzC,EAAO,GAAG0E,CAAO,CAAC,CACrF,CAAC,EACD,GAAI,CAAC9B,EAAI,GAAI,MAAM,IAAI,MAAM,uBAAuBA,EAAI,MAAM,EAAE,EAChE,IAAMsB,EAAO,MAAMtB,EAAI,KAAK,EAC5B,OAAO,KAAK,WAAW,OAAO,KAAK,QAAQsB,CAAI,EAAG,CAAE,MAAOvE,EAAI,QAAQ,OAAO,CAAE,CAAC,EACjFoB,EAAO,YAAcuD,CACvB,OAAS1C,EAAG,CACVb,EAAO,YAAc,UAAUa,EAAE,OAAO,EAC1C,QAAE,CACAoC,EAAI,SAAW,EACjB,EACF,EAEAA,EAAI,iBAAiB,QAASY,CAAO,EACrCP,EAAM,iBAAiB,UAAWzC,GAAK,CAAMA,EAAE,MAAQ,SAASgD,EAAQ,CAAE,CAAC,CAE7E,SAAW/E,IAAS,UAAW,CAG7B,IAAMwE,EAAQ1E,EAAI,KAAK,YAAY,EAAE,CAAC,EAChCqE,EAAQrE,EAAI,KAAK,UAAU,EAAE,CAAC,EAIhCqB,GAAW,CAACqD,EAAM,QAAOA,EAAM,MAAQrD,EAAQ,OAEnDD,EAAO,aAAeC,EAAU,GAAGwC,EAAYxC,CAAO,CAAC,SAAQ,IAC7D,wCAAmClB,EAAM,OAASA,EAAM,KAAK,IAAI,EAAI,GAAG,eAAYE,CAAK,GAE3F,IAAM6E,EAAY,SAAY,CAC5B,IAAMJ,EAAQJ,EAAM,MAAM,KAAK,EAC/B,GAAKI,EACL,CAAAT,EAAI,SAAW,GACfjD,EAAO,YAAc,oCACrB,GAAI,CACF,IAAM0B,EAAM,MAAMrB,EACZ0D,EAAU,oBAAoBrC,EAAI,OAASA,EAAM,CAAC,GAAG,GAAG,KAAK,GAAG,CAAC,EACjEsC,EAAarE,EAAM,OAAS,UAAU,mBAAmBA,EAAM,KAAK,GAAG,CAAC,CAAC,GAAK,GAC9EkC,EAAM,MAAM,MAChB,GAAG9B,CAAM,8BAA8B,mBAAmB2D,CAAK,CAAC,YAAYK,CAAO,UAAU9E,CAAK,GAAG+E,CAAU,EAAE,EACnH,GAAI,CAACnC,EAAI,GAAI,MAAM,IAAI,MAAM,uBAAuBA,EAAI,MAAM,EAAE,EAChE,IAAMsB,EAAO,MAAMtB,EAAI,KAAK,EAC5B,OAAO,KAAK,WAAW,OAAO,KAAK,QAAQsB,CAAI,EAAG,CAAE,MAAOvE,EAAI,QAAQ,OAAO,CAAE,CAAC,EACjFoB,EAAO,YAAc,wCAAmCjB,EAAM,OAASA,EAAM,KAAK,IAAI,EAAI,GAAG,eAAYE,CAAK,EAChH,OAAS4B,EAAG,CACVb,EAAO,YAAc,UAAUa,EAAE,OAAO,EAC1C,QAAE,CACAoC,EAAI,SAAW,EACjB,EACF,EAEAA,EAAI,iBAAiB,QAASa,CAAS,EACvCR,EAAM,iBAAiB,UAAWzC,GAAK,CAAMA,EAAE,MAAQ,SAASiD,EAAU,CAAE,CAAC,CAE/E,SAAWhF,IAAS,SAAU,CAE5B,IAAMwE,EAAU1E,EAAI,KAAK,YAAY,EAAE,CAAC,EAClCqE,EAAUrE,EAAI,KAAK,UAAU,EAAE,CAAC,EAChCmD,EAAUnD,EAAI,KAAK,cAAc,EAAE,CAAC,EACtCyD,EAAgB,KAEhBpC,GAAW,CAACqD,EAAM,QAAOA,EAAM,MAAQrD,EAAQ,QAEjD,SAAY,CACZ,GAAI,CACGE,IAAOH,EAAO,YAAc,2BACjCqC,EAAgB,MAAMC,EAAkB,MAAMjC,EAAQN,CAAM,EAC5D,IAAMwC,EAAQF,EAAc,OAAO,CAACG,EAAG3B,IAAM2B,EAAI3B,EAAE,MAAM,OAAQ,CAAC,EAClEb,EAAO,aAAeC,EAAU,GAAGwC,EAAYxC,CAAO,CAAC,SAAQ,IAC7DO,EAAc,eAAgB+B,EAAOF,EAAc,MAAM,CAC7D,OAASxB,EAAG,CACVb,EAAO,YAAc,eAAea,EAAE,OAAO,EAC/C,CACF,GAAG,EAEH,IAAMoD,EAAW,SAAY,CAC3B,IAAMP,EAAQJ,EAAM,MAAM,KAAK,EAC/B,GAAI,GAACI,GAAS,CAACrB,GACf,CAAAY,EAAI,SAAW,GACfjD,EAAO,YAAc,wBACrB+B,EAAQ,UAAY,GACpB,GAAI,CACF,IAAMW,EAAQ,MAAMK,EAAaW,EAAO3D,CAAM,EACxCkC,EAASe,EAAWN,EAAML,EAC9B,CAAE,UAAArD,EAAW,MAAAC,EAAO,YAAa,KAAM,cAAe,IAAK,CAAC,EAGxDiF,EAAa,IAAI,IACjBC,EAAe,CAAC,EACtB,OAAW,CAAE,MAAA3F,CAAM,IAAKyD,EACjBiC,EAAW,IAAI1F,CAAK,IAAK0F,EAAW,IAAI1F,CAAK,EAAG2F,EAAa,KAAK3F,CAAK,GAG9E,IAAM4F,EAAQ,IAAM,KAAK,MAAM,KAAK,OAAO,EAAI,mBAAkB,EAAE,SAAS,EAAE,EAAE,SAAS,GAAI,GAAG,EAC1FC,EAAeF,EAAa,IAAIG,GAAK,OAAOA,CAAC,IAAI,EAAE,KAAK;AAAA,CAAI,EAE5DC,GAAQ,CACZ,CAAE,KAAM,WAAY,GAAIH,EAAM,EAAG,KAAM;AAAA;AAAA,EAAsBC,CAAY,EAAG,EAC5E,CAAE,KAAM,WAAY,GAAID,EAAM,EAAG,KAAM,mBAAoB,EAC3D,GAAGnC,EAAO,IAAI,CAAC,CAAE,OAAA3D,EAAQ,KAAAC,GAAM,MAAAC,GAAO,MAAAC,EAAM,KAAO,CACjD,KAAM,YAAa,GAAI2F,EAAM,EAAG,KAAM9F,EAAQ,KAAAC,GAAM,MAAAC,GACpD,KAAM,SAASC,GAAM,QAAQ,CAAC,CAAC,EACjC,EAAE,CACJ,EAEMmF,GAAU,OAAO,KAAK,QAAQ,CAAE,MAAO,GAAGF,CAAK,WAAY,MAAAa,GAAO,QAAS,CAAC,CAAE,CAAC,EACrF,OAAO,KAAK,WAAWX,GAAS,CAAE,MAAOhF,EAAI,QAAQ,OAAO,CAAE,CAAC,EAE/DoB,EAAO,YAAc,GAAGiC,EAAO,MAAM,eACrCH,EAAWjD,EAAM,CAAE,OAAAoD,EAAQ,MAAAyB,CAAM,CAAC,CACpC,OAAS7C,EAAG,CACVb,EAAO,YAAc,UAAUa,EAAE,OAAO,EAC1C,QAAE,CACAoC,EAAI,SAAW,EACjB,EACF,EAEAA,EAAI,iBAAiB,QAASgB,CAAQ,EACtCX,EAAM,iBAAiB,UAAWzC,GAAK,CAAMA,EAAE,MAAQ,SAASoD,EAAS,CAAE,CAAC,CAE9E,KAAO,CAEL,IAAMX,EAAU1E,EAAI,KAAK,YAAY,EAAE,CAAC,EAClCqE,EAAUrE,EAAI,KAAK,UAAU,EAAE,CAAC,EAChCmD,EAAUnD,EAAI,KAAK,cAAc,EAAE,CAAC,EACtCyD,EAAgB,KAGhBlC,GAAO,SACTmD,EAAM,MAAQnD,EAAM,OAAS,GAC7B4B,EAAQ,UAAY5B,EAAM,OAAO,IAAI,CAAC,CAAE,OAAA7B,EAAQ,KAAAC,EAAM,MAAAC,EAAO,MAAAC,CAAM,IACjE,2BAA2BJ,EAAQC,EAAQC,EAAMC,EAAOC,CAAK,CAAC,QAAQ,EAAE,KAAK,EAAE,EAC/E,4BAA4B0B,EAAM,OAAO,MAAM,SAASA,EAAM,OAAS,EAAE,IAAIc,EAAUd,EAAM,EAAE,CAAC,OAClGH,EAAO,YAAc,IAGnBC,GAAW,CAACqD,EAAM,QAAOA,EAAM,MAAQrD,EAAQ,QAEjD,SAAY,CACZ,GAAI,CACGE,IAAOH,EAAO,YAAc,2BACjCqC,EAAgB,MAAMC,EAAkB,MAAMjC,EAAQN,CAAM,EAC5D,IAAMwC,EAAQF,EAAc,OAAO,CAACG,EAAG3B,IAAM2B,EAAI3B,EAAE,MAAM,OAAQ,CAAC,EAClEb,EAAO,aAAeC,EAAU,GAAGwC,EAAYxC,CAAO,CAAC,SAAQ,IAC7DO,EAAc,eAAgB+B,EAAOF,EAAc,MAAM,CAC7D,OAASxB,EAAG,CACVb,EAAO,YAAc,eAAea,EAAE,OAAO,EAC/C,CACF,GAAG,EAEH,IAAM2D,EAAW,SAAY,CAC3B,IAAMd,EAAQJ,EAAM,MAAM,KAAK,EAC/B,GAAI,GAACI,GAAS,CAACrB,GACf,CAAAY,EAAI,SAAW,GACfjD,EAAO,YAAc,wBACrB+B,EAAQ,UAAY,GACpB,GAAI,CACF,IAAMW,EAAO,MAAMK,EAAaW,EAAO3D,CAAM,EACvCkC,EAASe,EAAWN,EAAML,EAC9B,CAAE,UAAArD,EAAW,MAAAC,EAAO,YAAa,KAAM,cAAe,IAAK,CAAC,EAC9D8C,EAAQ,UAAYE,EAAO,IAAI,CAAC,CAAE,OAAA3D,EAAQ,KAAAC,EAAM,MAAAC,EAAO,MAAAC,CAAM,IAC3D,2BAA2BJ,EAAQC,EAAQC,EAAMC,EAAOC,CAAK,CAAC,QAAQ,EAAE,KAAK,EAAE,EAC/E,4BAA4BwD,EAAO,MAAM,SAASyB,CAAK,QACzD1D,EAAO,YAAc,GACrB8B,EAAWjD,EAAM,CAAE,OAAAoD,EAAQ,MAAAyB,CAAM,CAAC,CACpC,OAAS7C,EAAG,CACVb,EAAO,YAAc,UAAUa,EAAE,OAAO,EAC1C,QAAE,CACAoC,EAAI,SAAW,EACjB,EACF,EAEAA,EAAI,iBAAiB,QAASuB,CAAQ,EACtClB,EAAM,iBAAiB,UAAWzC,GAAK,CAAMA,EAAE,MAAQ,SAAS2D,EAAS,CAAE,CAAC,CAC9E,CACF,EAII,OAAO,OAAW,MACpB,OAAO,QAAU,OAAO,SAAW,CAAC,EACpC,OAAO,QAAQ,WAAa,CAAE,KAAA7F,GAAM,KAAAc,EAAK",
6
+ "names": ["SIMILAR_THRESHOLDS", "DEFAULT_THRESHOLD", "DEFAULT_LIMIT", "parseDSL", "text", "specs", "rosterRefs", "farms", "threshold", "limit", "mode", "live", "subject", "force", "ghostUrl", "label", "isCmd", "upper", "kw", "val", "line", "raw", "ref", "peer", "level", "tv", "isGlob", "spec", "isScope", "slugify", "title", "domainCache", "resolveDomainsForSpec", "spec", "origin", "url", "res", "list", "resolveDomains", "specs", "seen", "result", "isGlob", "isScope", "item", "SITE_LINE", "ROSTER_LINE", "REFS_LINE", "fetchPage", "ref", "i", "resolveRoster", "out", "visited", "page", "e", "raw", "line", "site", "nested", "refs", "refPage", "it", "resolveRosters", "rosterRefs", "effectiveSpecs", "vectorCache", "vectorUrl", "domain", "loadVectors", "res", "data", "getEmbedding", "text", "origin", "lookupPageVector", "slug", "entry", "cosineScan", "queryVec", "domainEntries", "threshold", "limit", "excludeSlug", "excludeDomain", "results", "pages", "title", "vector", "dot", "i", "a", "b", "loadDomainEntries", "specs", "domains", "resolveDomains", "e", "resolveSubject", "div", "$self", "$host", "$page", "slug", "site", "data", "title", "text", "i", "subjectNote", "s", "cacheKey", "id", "readCache", "item", "c", "writeCache", "data", "cacheAge", "ts", "s", "STYLES", "siteFlag", "domain", "score", "simLink", "domain", "slug", "title", "score", "siteFlag", "emit", "div", "item", "mode", "specs", "threshold", "limit", "force", "ghostUrl", "label", "parseDSL", "STYLES", "btnLabel", "hint", "bind", "rosterRefs", "farms", "live", "subjectFlag", "thresholdSet", "origin", "status", "subject", "resolveSubject", "cache", "readCache", "specsP", "effectiveSpecs", "seedParams", "configSummary", "verb", "pages", "nDomains", "parts", "e", "$a", "$page", "scopeLabel", "cacheNote", "ts", "cacheAge", "listDiv", "renderList", "domains", "totalPages", "d", "page_count", "eff", "patterns", "url", "res", "writeCache", "results", "renderScored", "scored", "s", "pageTitle", "slugify", "domainEntries", "loadDomainEntries", "total", "n", "subjectNote", "qVec", "lookupPageVector", "pageText", "_", "el", "getEmbedding", "cosineScan", "btn", "doGhost", "page", "doBuild", "body", "input", "readyLine", "resolveDomains", "doReport", "query", "seeded", "pageObj", "doSites", "doKeyword", "pattern", "farmsParam", "doAuthor", "seenTitles", "uniqueTitles", "hexId", "primaryLines", "t", "story", "doSearch"]
7
7
  }
package/factory.json CHANGED
@@ -1 +1,8 @@
1
- {"name": "Similarity", "title": "Semantic similarity search across wiki domains", "category": "other"}
1
+ {
2
+ "name": "Similarity",
3
+ "title": "Semantic similarity search across wiki domains",
4
+ "category": "other",
5
+ "pages": [
6
+ "Semantic Search"
7
+ ]
8
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "wiki-plugin-similarity",
3
- "version": "0.11.0",
4
- "description": "Federated Wiki plugin \u2014 semantic similarity search across wiki domains using prebuilt vector indexes",
3
+ "version": "0.12.0",
4
+ "description": "Federated Wiki plugin — semantic similarity search across wiki domains using prebuilt vector indexes",
5
5
  "keywords": [
6
6
  "wiki",
7
7
  "federated wiki",
@@ -58,4 +58,4 @@
58
58
  "spec": "api/openapi.json"
59
59
  }
60
60
  }
61
- }
61
+ }
@@ -64,12 +64,12 @@
64
64
  {
65
65
  "type": "markdown",
66
66
  "id": "d9a96dfebb9d1692",
67
- "text": "## Server Routes\n\nAll same-origin, registered by the plugin's server component:\n\n- `GET /system/indexed-domains.json?pattern=`\n- `GET /system/semantic-vectors.json?domain=`\n- `GET /system/embed.json?text=`\n- `POST /system/search-report.json`\n- `POST /system/site-report.json` \u2014 which site should this page go on? Per-domain aggregation of the page-vector scan: 0.7 \u00d7 mean of the top 5 page similarities + 0.3 \u00d7 site-centroid similarity; forked pages count for every site that carries them\n- `GET /system/farm-search.json?q=&pattern=&limit=`\n- `GET /system/build-index.json?domains=` (proxied to the farm indexer)\n- `GET /system/galaxy-search.json?q=&sites=` \u2014 keyword search over off-farm federation sites\n- `GET /system/galaxy-registry.json` \u2014 the galaxy site registry: every federation site known to this host, with provenance, liveness, page counts and scraped site owner (read-only; written by the galaxy indexer host)\n- `GET /system/peer-hello.json` \u2014 capability probe: version, model, federation status (404 = plugin absent)\n- `GET /system/similarity-health.json` \u2014 embedder supervisor state: child-process / semindex / url, breaker status, recent crashes\n- `POST /system/peer-search.json` \u2014 answer another farm's search. Two keys: the admin's `WIKI_PEER_FEDERATION` ceiling (off / grants / open) and each site's Federated Farm Search grants page; scope = union of granting sites; hop limit, replay dedup, IP-keyed + global rate limits; restricted sites always excluded\n\nIndexes are `status/semantic-vectors.json` per domain \u2014 built by the farm indexer, distributed by sync, read from farm disk. Keyword search reads each site's per-edit `status/site-index.json`; no separate index is built. Query embedding is BAAI bge-small-en-v1.5, 384-dim, CLS-pooled, unit-normalised \u2014 byte-compatible with the index builder.\n\nSince 0.9.0 all query embedding runs in a supervised child process, never in the wiki server itself: a crash in the ONNX runtime fails that one search, and repeated crashes trip a circuit breaker that answers 503 while cooling down. The worst case for a wiki running this plugin is \"semantic search temporarily unavailable\" \u2014 never a dead wiki.\n\nSince 0.11.0 a third index tier sits beside the farm: the **galaxy** \u2014 a farm-shaped tree of off-farm federation sites, built by a separate indexer and read through a byte-capped vector cache (`WIKI_GALAXY_VECTOR_CACHE_MB`, default 512). Galaxy entries carry their enrichment precomputed, because off-farm pages have no local page JSON to read; the stub filter, fork bundling and scoring then run unchanged, so federation results rank directly against farm results."
67
+ "text": "## Server Routes\n\nAll same-origin, registered by the plugin's server component:\n\n- `GET /system/indexed-domains.json?pattern=`\n- `GET /system/semantic-vectors.json?domain=`\n- `GET /system/embed.json?text=`\n- `POST /system/search-report.json` \u2014 accepts an optional `author`: a display name or account username, resolved against this farm's own site ownership (each site's `owner.json`) and intersected with the requested scope. An ambiguous name answers 409 with the competing usernames rather than guessing\n- `POST /system/site-report.json` \u2014 which site should this page go on? Per-domain aggregation of the page-vector scan: 0.7 \u00d7 mean of the top 5 page similarities + 0.3 \u00d7 site-centroid similarity; forked pages count for every site that carries them\n- `GET /system/farm-search.json?q=&pattern=&limit=`\n- `GET /system/build-index.json?domains=` (proxied to the farm indexer)\n- `GET /system/galaxy-search.json?q=&sites=` \u2014 keyword search over off-farm federation sites\n- `GET /system/galaxy-registry.json` \u2014 the galaxy site registry: every federation site known to this host, with provenance, liveness, page counts and scraped site owner (read-only; written by the galaxy indexer host)\n- `GET /system/peer-hello.json` \u2014 capability probe: version, model, federation status (404 = plugin absent)\n- `GET /system/similarity-health.json` \u2014 embedder supervisor state: child-process / semindex / url, breaker status, recent crashes\n- `POST /system/peer-search.json` \u2014 answer another farm's search. An optional `author` travels as the plain name asked for, never a resolved username: the answering farm resolves it against its OWN records and reports `applied`, `ignored` (no records kept) or `ambiguous`. Two keys: the admin's `WIKI_PEER_FEDERATION` ceiling (off / grants / open) and each site's Federated Farm Search grants page; scope = union of granting sites; hop limit, replay dedup, IP-keyed + global rate limits; restricted sites always excluded\n\nIndexes are `status/semantic-vectors.json` per domain \u2014 built by the farm indexer, distributed by sync, read from farm disk. Keyword search reads each site's per-edit `status/site-index.json`; no separate index is built. Query embedding is BAAI bge-small-en-v1.5, 384-dim, CLS-pooled, unit-normalised \u2014 byte-compatible with the index builder.\n\nSince 0.9.0 all query embedding runs in a supervised child process, never in the wiki server itself: a crash in the ONNX runtime fails that one search, and repeated crashes trip a circuit breaker that answers 503 while cooling down. The worst case for a wiki running this plugin is \"semantic search temporarily unavailable\" \u2014 never a dead wiki.\n\nSince 0.11.0 a third index tier sits beside the farm: the **galaxy** \u2014 a farm-shaped tree of off-farm federation sites, built by a separate indexer and read through a byte-capped vector cache (`WIKI_GALAXY_VECTOR_CACHE_MB`, default 512). Galaxy entries carry their enrichment precomputed, because off-farm pages have no local page JSON to read; the stub filter, fork bundling and scoring then run unchanged, so federation results rank directly against farm results.\n\nSince 0.12.0 the plugin also offers a **Semantic Search** page through the wiki's \u2630 menu, declared by a `pages` entry in `factory.json` and shipped in the package."
68
68
  },
69
69
  {
70
70
  "type": "markdown",
71
71
  "id": "21eb5354d2adb29e",
72
- "text": "**Version:** 0.11.0\n\n# Links\n\n- [https://github.com/Hitchhikers-Guide-to-the-Galaxy/wiki-plugin-similarity GitHub repository]\n- [https://www.npmjs.com/package/wiki-plugin-similarity npm package]\n- [https://plugin.fedwiki.club/similarity-plugin.html Plugin documentation]\n- [https://search.fedwiki.club/ Search hub] \u2014 architecture, indexing, freshness."
72
+ "text": "**Version:** 0.12.0\n\n# Links\n\n- [https://github.com/Hitchhikers-Guide-to-the-Galaxy/wiki-plugin-similarity GitHub repository]\n- [https://www.npmjs.com/package/wiki-plugin-similarity npm package]\n- [https://plugin.fedwiki.club/similarity-plugin.html Plugin documentation]\n- [https://search.fedwiki.club/ Search hub] \u2014 architecture, indexing, freshness."
73
73
  }
74
74
  ],
75
75
  "plugin": "similarity",
@@ -263,6 +263,26 @@
263
263
  "text": "**Version:** 0.11.0\n\n# Links\n\n- [https://github.com/Hitchhikers-Guide-to-the-Galaxy/wiki-plugin-similarity GitHub repository]\n- [https://www.npmjs.com/package/wiki-plugin-similarity npm package]\n- [https://plugin.fedwiki.club/similarity-plugin.html Plugin documentation]\n- [https://search.fedwiki.club/ Search hub] \u2014 architecture, indexing, freshness."
264
264
  },
265
265
  "date": 1785280898931
266
+ },
267
+ {
268
+ "type": "edit",
269
+ "id": "d9a96dfebb9d1692",
270
+ "item": {
271
+ "type": "markdown",
272
+ "id": "d9a96dfebb9d1692",
273
+ "text": "## Server Routes\n\nAll same-origin, registered by the plugin's server component:\n\n- `GET /system/indexed-domains.json?pattern=`\n- `GET /system/semantic-vectors.json?domain=`\n- `GET /system/embed.json?text=`\n- `POST /system/search-report.json` \u2014 accepts an optional `author`: a display name or account username, resolved against this farm's own site ownership (each site's `owner.json`) and intersected with the requested scope. An ambiguous name answers 409 with the competing usernames rather than guessing\n- `POST /system/site-report.json` \u2014 which site should this page go on? Per-domain aggregation of the page-vector scan: 0.7 \u00d7 mean of the top 5 page similarities + 0.3 \u00d7 site-centroid similarity; forked pages count for every site that carries them\n- `GET /system/farm-search.json?q=&pattern=&limit=`\n- `GET /system/build-index.json?domains=` (proxied to the farm indexer)\n- `GET /system/galaxy-search.json?q=&sites=` \u2014 keyword search over off-farm federation sites\n- `GET /system/galaxy-registry.json` \u2014 the galaxy site registry: every federation site known to this host, with provenance, liveness, page counts and scraped site owner (read-only; written by the galaxy indexer host)\n- `GET /system/peer-hello.json` \u2014 capability probe: version, model, federation status (404 = plugin absent)\n- `GET /system/similarity-health.json` \u2014 embedder supervisor state: child-process / semindex / url, breaker status, recent crashes\n- `POST /system/peer-search.json` \u2014 answer another farm's search. An optional `author` travels as the plain name asked for, never a resolved username: the answering farm resolves it against its OWN records and reports `applied`, `ignored` (no records kept) or `ambiguous`. Two keys: the admin's `WIKI_PEER_FEDERATION` ceiling (off / grants / open) and each site's Federated Farm Search grants page; scope = union of granting sites; hop limit, replay dedup, IP-keyed + global rate limits; restricted sites always excluded\n\nIndexes are `status/semantic-vectors.json` per domain \u2014 built by the farm indexer, distributed by sync, read from farm disk. Keyword search reads each site's per-edit `status/site-index.json`; no separate index is built. Query embedding is BAAI bge-small-en-v1.5, 384-dim, CLS-pooled, unit-normalised \u2014 byte-compatible with the index builder.\n\nSince 0.9.0 all query embedding runs in a supervised child process, never in the wiki server itself: a crash in the ONNX runtime fails that one search, and repeated crashes trip a circuit breaker that answers 503 while cooling down. The worst case for a wiki running this plugin is \"semantic search temporarily unavailable\" \u2014 never a dead wiki.\n\nSince 0.11.0 a third index tier sits beside the farm: the **galaxy** \u2014 a farm-shaped tree of off-farm federation sites, built by a separate indexer and read through a byte-capped vector cache (`WIKI_GALAXY_VECTOR_CACHE_MB`, default 512). Galaxy entries carry their enrichment precomputed, because off-farm pages have no local page JSON to read; the stub filter, fork bundling and scoring then run unchanged, so federation results rank directly against farm results.\n\nSince 0.12.0 the plugin also offers a **Semantic Search** page through the wiki's \u2630 menu, declared by a `pages` entry in `factory.json` and shipped in the package."
274
+ },
275
+ "date": 1785397304700
276
+ },
277
+ {
278
+ "type": "edit",
279
+ "id": "21eb5354d2adb29e",
280
+ "item": {
281
+ "type": "markdown",
282
+ "id": "21eb5354d2adb29e",
283
+ "text": "**Version:** 0.12.0\n\n# Links\n\n- [https://github.com/Hitchhikers-Guide-to-the-Galaxy/wiki-plugin-similarity GitHub repository]\n- [https://www.npmjs.com/package/wiki-plugin-similarity npm package]\n- [https://plugin.fedwiki.club/similarity-plugin.html Plugin documentation]\n- [https://search.fedwiki.club/ Search hub] \u2014 architecture, indexing, freshness."
284
+ },
285
+ "date": 1785397304700
266
286
  }
267
287
  ]
268
288
  }
@@ -0,0 +1,60 @@
1
+ {
2
+ "title": "Semantic Search",
3
+ "story": [
4
+ {
5
+ "type": "paragraph",
6
+ "id": "6a46bc37dd15ddfb",
7
+ "text": "Semantic search across every indexed domain this wiki can reach. Type a phrase \u2014 the plugin embeds it, compares it against the prebuilt page vectors, and ranks the closest pages by meaning rather than by word. Add a line reading GALAXY to search off-farm federation sites instead, where this host keeps a galaxy index."
8
+ },
9
+ {
10
+ "type": "similarity",
11
+ "id": "ece1644752fdbfb5",
12
+ "text": "*\nLIMIT: 10"
13
+ },
14
+ {
15
+ "type": "paragraph",
16
+ "id": "3088fca5e9418d87",
17
+ "text": "More [[About Similarity Plugin]]"
18
+ }
19
+ ],
20
+ "journal": [
21
+ {
22
+ "type": "create",
23
+ "item": {
24
+ "title": "Semantic Search",
25
+ "story": []
26
+ },
27
+ "date": 1785390072037
28
+ },
29
+ {
30
+ "type": "add",
31
+ "id": "6a46bc37dd15ddfb",
32
+ "item": {
33
+ "type": "paragraph",
34
+ "id": "6a46bc37dd15ddfb",
35
+ "text": "Semantic search across every indexed domain this wiki can reach. Type a phrase \u2014 the plugin embeds it, compares it against the prebuilt page vectors, and ranks the closest pages by meaning rather than by word. Add a line reading GALAXY to search off-farm federation sites instead, where this host keeps a galaxy index."
36
+ },
37
+ "date": 1785390072037
38
+ },
39
+ {
40
+ "type": "add",
41
+ "id": "ece1644752fdbfb5",
42
+ "item": {
43
+ "type": "similarity",
44
+ "id": "ece1644752fdbfb5",
45
+ "text": "*\nLIMIT: 10"
46
+ },
47
+ "date": 1785390072037
48
+ },
49
+ {
50
+ "type": "add",
51
+ "id": "3088fca5e9418d87",
52
+ "item": {
53
+ "type": "paragraph",
54
+ "id": "3088fca5e9418d87",
55
+ "text": "More [[About Similarity Plugin]]"
56
+ },
57
+ "date": 1785390072037
58
+ }
59
+ ]
60
+ }
@@ -0,0 +1,90 @@
1
+ // wiki-plugin-similarity — author resolution (CommonJS)
2
+ //
3
+ // A farm's identity map is not a file we ship: it is every site's own
4
+ // status/owner.json, which carries a display name alongside the account that
5
+ // owns the site. This module reads those, groups sites by account, and answers
6
+ // one question — "which of this farm's sites belong to the person called X?".
7
+ //
8
+ // Two rules the data forced:
9
+ //
10
+ // * A username IS an identity; a display name is only a claim to one. One
11
+ // account can answer to many display names, and two accounts can use the
12
+ // same one. So an exact username match wins outright, and only a shared
13
+ // display name counts as ambiguous.
14
+ // * The map never leaves the farm. Nothing here is exposed on a route, and
15
+ // callers get sites or a verdict — never the map, and never the other
16
+ // names an account is known by. A peer asking on behalf of a person sends
17
+ // the name they typed, and this farm answers from its own records.
18
+ //
19
+ // Private-farm sites have no oauth2 block; their friend.secret is hashed for
20
+ // grouping and never read out.
21
+
22
+ const fs = require('node:fs')
23
+ const path = require('node:path')
24
+ const crypto = require('node:crypto')
25
+
26
+ const TTL_MS = parseInt(process.env.WIKI_AUTHOR_INDEX_TTL_MS || '', 10) || 5 * 60_000
27
+
28
+ // Display names drift between a person's own sites — spacing, case, and dots or
29
+ // underscores standing in for spaces.
30
+ const fold = s => (s || '').toLowerCase().replace(/[._-]+/g, ' ').replace(/\s+/g, ' ').trim()
31
+
32
+ const identityKey = owner => {
33
+ const oauth2 = owner.oauth2 || {}
34
+ if (oauth2.username) return 'oauth2:' + oauth2.username
35
+ if (oauth2.id) return 'oauth2:' + oauth2.id
36
+ const secret = (owner.friend || {}).secret
37
+ if (secret) return 'friend:' + crypto.createHash('sha256').update(secret).digest('hex').slice(0, 12)
38
+ return null
39
+ }
40
+
41
+ let cache = null // {at, identities: Map(key → {names:Set, sites:Set})}
42
+
43
+ const buildIndex = farms => {
44
+ if (cache && Date.now() - cache.at < TTL_MS) return cache.identities
45
+ const identities = new Map()
46
+ for (const [farm] of farms) {
47
+ let entries
48
+ try { entries = fs.readdirSync(farm, { withFileTypes: true }) } catch { continue }
49
+ for (const ent of entries) {
50
+ if (!ent.isDirectory()) continue
51
+ let owner
52
+ try {
53
+ owner = JSON.parse(fs.readFileSync(
54
+ path.join(farm, ent.name, 'status', 'owner.json'), 'utf8'))
55
+ } catch { continue } // unowned or unreadable — simply not in the map
56
+ const key = identityKey(owner)
57
+ if (!key) continue
58
+ if (!identities.has(key)) identities.set(key, { names: new Set(), sites: new Set() })
59
+ const rec = identities.get(key)
60
+ rec.sites.add(ent.name)
61
+ if (owner.name) rec.names.add(owner.name)
62
+ }
63
+ }
64
+ cache = { at: Date.now(), identities }
65
+ return identities
66
+ }
67
+
68
+ // → {available:false} this farm keeps no ownership records
69
+ // {ambiguous:[username, …]} the name belongs to more than one account
70
+ // {sites:[…]} resolved (possibly empty — owns nothing here)
71
+ const resolveAuthor = (farms, who) => {
72
+ const identities = buildIndex(farms)
73
+ if (!identities.size) return { available: false }
74
+
75
+ const want = fold(who)
76
+ const byUsername = [], byDisplay = []
77
+ for (const [key, rec] of identities) {
78
+ const username = key.slice(key.indexOf(':') + 1)
79
+ if (fold(username) === want || key.toLowerCase() === want) byUsername.push({ key, username, rec })
80
+ else if ([...rec.names].some(n => fold(n) === want)) byDisplay.push({ key, username, rec })
81
+ }
82
+ const hits = byUsername.length ? byUsername : byDisplay
83
+ const accounts = [...new Set(hits.filter(h => h.key.startsWith('oauth2:')).map(h => h.username))]
84
+ if (accounts.length > 1) return { ambiguous: accounts }
85
+ return { sites: [...new Set(hits.flatMap(h => [...h.rec.sites]))] }
86
+ }
87
+
88
+ const clearCache = () => { cache = null }
89
+
90
+ module.exports = { resolveAuthor, clearCache, fold }
package/server/peer.js ADDED
@@ -0,0 +1,166 @@
1
+ // wiki-plugin-similarity — peer federation transport and guards (CommonJS)
2
+ //
3
+ // Everything a farm needs to ASK another farm, and to survive being asked: the
4
+ // HTTP transport, the hello probe and its cache, replay dedup, and the rate
5
+ // limits. Split out of server.js so the part carrying a security surface can be
6
+ // read on its own — the routes stay beside the search they call.
7
+ //
8
+ // Nothing here knows how a search works; it moves envelopes and enforces limits.
9
+
10
+ const http = require('node:http')
11
+ const https = require('node:https')
12
+ const crypto = require('node:crypto')
13
+
14
+ let MODEL_META = {}
15
+ const setModelMeta = m => { MODEL_META = m }
16
+
17
+ const { makeDedup, makeBucket } = require('./peer-guard')
18
+
19
+ // POST JSON to a peer farm's plugin server. Public peers speak https;
20
+ // *.localhost peers speak http via a loopback lookup (Node's resolver does
21
+ // not know RFC 6761 subdomains). Public domains follow /etc/hosts, so
22
+ // Offline Edit Mode routes peer calls to the local mirror by construction.
23
+ const postToPeer = (peer, routePath, body, timeoutMs = 30_000) =>
24
+ new Promise((resolve, reject) => {
25
+ const isLocal = peer.endsWith('.localhost') || peer.startsWith('localhost')
26
+ const mod = isLocal ? http : https
27
+ const payload = JSON.stringify(body)
28
+ const opts = {
29
+ hostname: peer.split(':')[0],
30
+ port: peer.includes(':') ? peer.split(':')[1] : (isLocal ? 80 : 443),
31
+ path: routePath,
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ 'Content-Length': Buffer.byteLength(payload),
36
+ },
37
+ timeout: timeoutMs,
38
+ }
39
+ if (isLocal) opts.lookup = (h, o, cb) => cb(null, [{ address: '127.0.0.1', family: 4 }])
40
+ const req = mod.request(opts, res => {
41
+ let data = ''
42
+ res.on('data', chunk => { data += chunk })
43
+ res.on('end', () => {
44
+ try { resolve({ status: res.statusCode, body: JSON.parse(data) }) }
45
+ catch {
46
+ resolve({ status: res.statusCode,
47
+ body: { error: `no similarity server answered (${res.statusCode})` } })
48
+ }
49
+ })
50
+ })
51
+ req.on('timeout', () => { req.destroy(new Error('peer timeout')) })
52
+ req.on('error', reject)
53
+ req.write(payload)
54
+ req.end()
55
+ })
56
+
57
+ // Append peer farms' results to a locally-built report page. Each peer gets
58
+ // its own section; provenance rides on the reference items' site field.
59
+ // Scores merge visually only when the peer declares the same embedding model.
60
+ const appendPeerSections = (page, peerOutcomes) => {
61
+ const mk = () => crypto.randomBytes(8).toString('hex')
62
+ for (const { peer, status, body } of peerOutcomes) {
63
+ if (status === 200 && body.page) {
64
+ const sameModel = body.meta && body.meta.model === MODEL_META.model
65
+ page.story.push({
66
+ type: 'markdown', id: mk(),
67
+ text: `# From ${peer}\n\n<small>${body.meta?.count ?? '?'} results — ` +
68
+ `model ${body.meta?.model || 'undeclared'}${sameModel ? '' :
69
+ ' (different model: scores not comparable with local results)'}</small>`,
70
+ })
71
+ for (const item of body.page.story || []) {
72
+ if (item.type === 'reference') page.story.push({ ...item, id: mk() })
73
+ }
74
+ } else {
75
+ page.story.push({
76
+ type: 'markdown', id: mk(),
77
+ text: `<small>Peer ${peer}: ${body?.error || `failed (${status})`}</small>`,
78
+ })
79
+ }
80
+ }
81
+ return page
82
+ }
83
+
84
+ // ── One desk per server ───────────────────────────────────────────────────────
85
+ // The buckets and the dedup window are per-farm state, built once at startup.
86
+ // MODEL_META is injected because appendPeerSections compares a peer's declared
87
+ // model against ours before letting scores sit together.
88
+
89
+ const makePeerDesk = () => {
90
+ // Peer federation guards (shared across peer requests). Rate limits are
91
+ // keyed by remote IP — never by the asserted origin — plus a global cap.
92
+ const isDuplicate = makeDedup()
93
+ const takeIpToken = makeBucket()
94
+ const takeGlobalToken = makeBucket({ capacity: 120, refillPerSec: 2, maxKeys: 1 })
95
+
96
+ const requestIp = req =>
97
+ (req.headers['x-forwarded-for'] || '').split(',')[0].trim() ||
98
+ req.socket.remoteAddress || 'unknown'
99
+
100
+ // GET JSON from a peer (hello probe) — same transport rules as postToPeer.
101
+ const getFromPeer = (peer, routePath, timeoutMs = 10_000) =>
102
+ new Promise((resolve, reject) => {
103
+ const isLocal = peer.endsWith('.localhost') || peer.startsWith('localhost')
104
+ const mod = isLocal ? http : https
105
+ const opts = {
106
+ hostname: peer.split(':')[0],
107
+ port: peer.includes(':') ? peer.split(':')[1] : (isLocal ? 80 : 443),
108
+ path: routePath,
109
+ timeout: timeoutMs,
110
+ }
111
+ if (isLocal) opts.lookup = (h, o, cb) => cb(null, [{ address: '127.0.0.1', family: 4 }])
112
+ const req = mod.get(opts, res => {
113
+ let data = ''
114
+ res.on('data', chunk => { data += chunk })
115
+ res.on('end', () => {
116
+ try { resolve({ status: res.statusCode, body: JSON.parse(data) }) }
117
+ catch { resolve({ status: res.statusCode, body: null }) }
118
+ })
119
+ })
120
+ req.on('timeout', () => { req.destroy(new Error('peer timeout')) })
121
+ req.on('error', reject)
122
+ })
123
+
124
+ // Hello probe cache: peer → {at, ok, body}. TTL 1h, small LRU.
125
+ const helloCache = new Map()
126
+ const HELLO_TTL = 60 * 60_000
127
+
128
+ const probePeer = async peer => {
129
+ const hit = helloCache.get(peer)
130
+ if (hit && Date.now() - hit.at < HELLO_TTL) return hit
131
+ let entry
132
+ try {
133
+ const { status, body } = await getFromPeer(peer, '/system/peer-hello.json')
134
+ entry = { at: Date.now(), ok: status === 200 && body && body.plugin, body }
135
+ } catch {
136
+ entry = { at: Date.now(), ok: false, body: null }
137
+ }
138
+ helloCache.set(peer, entry)
139
+ if (helloCache.size > 200) helloCache.delete(helloCache.keys().next().value)
140
+ return entry
141
+ }
142
+
143
+ const askPeers = async (peers, envelope) => {
144
+ const outcomes = await Promise.all(peers.map(async peer => {
145
+ const hello = await probePeer(peer)
146
+ if (!hello.ok) {
147
+ return { peer, status: 0,
148
+ body: { error: 'peer does not answer hello — no similarity federation there' } }
149
+ }
150
+ if (hello.body.federation && hello.body.federation.enabled === false) {
151
+ return { peer, status: 0, body: { error: 'peer has federation switched off' } }
152
+ }
153
+ try {
154
+ const { status, body } = await postToPeer(peer, '/system/peer-search.json', envelope)
155
+ return { peer, status, body }
156
+ } catch (e) {
157
+ return { peer, status: 0, body: { error: e.message } }
158
+ }
159
+ }))
160
+ return outcomes
161
+ }
162
+ return { requestIp, getFromPeer, probePeer, askPeers,
163
+ isDuplicate, takeIpToken, takeGlobalToken }
164
+ }
165
+
166
+ module.exports = { postToPeer, appendPeerSections, makePeerDesk, setModelMeta }
package/server/server.js CHANGED
@@ -71,7 +71,9 @@ const { buildSiteReport } = require('./site-report')
71
71
  const { searchFarm, keywordReportPage, findTwins } = require('./farm-search')
72
72
  const { searchGalaxy } = require('./galaxy-search')
73
73
  const { galaxyRoot, galaxyCacheStats } = require('./galaxy-vectors')
74
- const { ceiling, grantingDomains, makeDedup, makeBucket, guardEnvelope } = require('./peer-guard')
74
+ const { resolveAuthor } = require('./author-index')
75
+ const { ceiling, grantingDomains, guardEnvelope } = require('./peer-guard')
76
+ const { postToPeer, appendPeerSections, makePeerDesk, setModelMeta } = require('./peer')
75
77
 
76
78
  const MODEL_META = { model: 'BAAI/bge-small-en-v1.5', dim: 384 }
77
79
  const PLUGIN_VERSION = (() => {
@@ -140,73 +142,11 @@ const readBody = req =>
140
142
  req.on('error', reject)
141
143
  })
142
144
 
143
- // POST JSON to a peer farm's plugin server. Public peers speak https;
144
- // *.localhost peers speak http via a loopback lookup (Node's resolver does
145
- // not know RFC 6761 subdomains). Public domains follow /etc/hosts, so
146
- // Offline Edit Mode routes peer calls to the local mirror by construction.
147
- const postToPeer = (peer, routePath, body, timeoutMs = 30_000) =>
148
- new Promise((resolve, reject) => {
149
- const isLocal = peer.endsWith('.localhost') || peer.startsWith('localhost')
150
- const mod = isLocal ? http : https
151
- const payload = JSON.stringify(body)
152
- const opts = {
153
- hostname: peer.split(':')[0],
154
- port: peer.includes(':') ? peer.split(':')[1] : (isLocal ? 80 : 443),
155
- path: routePath,
156
- method: 'POST',
157
- headers: {
158
- 'Content-Type': 'application/json',
159
- 'Content-Length': Buffer.byteLength(payload),
160
- },
161
- timeout: timeoutMs,
162
- }
163
- if (isLocal) opts.lookup = (h, o, cb) => cb(null, [{ address: '127.0.0.1', family: 4 }])
164
- const req = mod.request(opts, res => {
165
- let data = ''
166
- res.on('data', chunk => { data += chunk })
167
- res.on('end', () => {
168
- try { resolve({ status: res.statusCode, body: JSON.parse(data) }) }
169
- catch {
170
- resolve({ status: res.statusCode,
171
- body: { error: `no similarity server answered (${res.statusCode})` } })
172
- }
173
- })
174
- })
175
- req.on('timeout', () => { req.destroy(new Error('peer timeout')) })
176
- req.on('error', reject)
177
- req.write(payload)
178
- req.end()
179
- })
180
-
181
- // Append peer farms' results to a locally-built report page. Each peer gets
182
- // its own section; provenance rides on the reference items' site field.
183
- // Scores merge visually only when the peer declares the same embedding model.
184
- const appendPeerSections = (page, peerOutcomes) => {
185
- const mk = () => crypto.randomBytes(8).toString('hex')
186
- for (const { peer, status, body } of peerOutcomes) {
187
- if (status === 200 && body.page) {
188
- const sameModel = body.meta && body.meta.model === MODEL_META.model
189
- page.story.push({
190
- type: 'markdown', id: mk(),
191
- text: `# From ${peer}\n\n<small>${body.meta?.count ?? '?'} results — ` +
192
- `model ${body.meta?.model || 'undeclared'}${sameModel ? '' :
193
- ' (different model: scores not comparable with local results)'}</small>`,
194
- })
195
- for (const item of body.page.story || []) {
196
- if (item.type === 'reference') page.story.push({ ...item, id: mk() })
197
- }
198
- } else {
199
- page.story.push({
200
- type: 'markdown', id: mk(),
201
- text: `<small>Peer ${peer}: ${body?.error || `failed (${status})`}</small>`,
202
- })
203
- }
204
- }
205
- return page
206
- }
207
145
 
208
146
  // ── startServer — called by wiki-server/lib/plugins.js ────────────────────────
209
147
 
148
+ setModelMeta(MODEL_META)
149
+
210
150
  const startServer = ({ argv, app }) => {
211
151
  // Farm root: argv.status = {farm}/{thisDomain}/status → go up two levels
212
152
  const farmRoot = path.dirname(path.dirname(argv.status))
@@ -248,78 +188,10 @@ const startServer = ({ argv, app }) => {
248
188
  app.options(route, (req, res) => { cors(res); res.sendStatus(204) })
249
189
  }
250
190
 
251
- // Peer federation guards (shared across peer requests). Rate limits are
252
- // keyed by remote IP — never by the asserted origin — plus a global cap.
253
- const isDuplicate = makeDedup()
254
- const takeIpToken = makeBucket()
255
- const takeGlobalToken = makeBucket({ capacity: 120, refillPerSec: 2, maxKeys: 1 })
256
-
257
- const requestIp = req =>
258
- (req.headers['x-forwarded-for'] || '').split(',')[0].trim() ||
259
- req.socket.remoteAddress || 'unknown'
260
-
261
- // GET JSON from a peer (hello probe) — same transport rules as postToPeer.
262
- const getFromPeer = (peer, routePath, timeoutMs = 10_000) =>
263
- new Promise((resolve, reject) => {
264
- const isLocal = peer.endsWith('.localhost') || peer.startsWith('localhost')
265
- const mod = isLocal ? http : https
266
- const opts = {
267
- hostname: peer.split(':')[0],
268
- port: peer.includes(':') ? peer.split(':')[1] : (isLocal ? 80 : 443),
269
- path: routePath,
270
- timeout: timeoutMs,
271
- }
272
- if (isLocal) opts.lookup = (h, o, cb) => cb(null, [{ address: '127.0.0.1', family: 4 }])
273
- const req = mod.get(opts, res => {
274
- let data = ''
275
- res.on('data', chunk => { data += chunk })
276
- res.on('end', () => {
277
- try { resolve({ status: res.statusCode, body: JSON.parse(data) }) }
278
- catch { resolve({ status: res.statusCode, body: null }) }
279
- })
280
- })
281
- req.on('timeout', () => { req.destroy(new Error('peer timeout')) })
282
- req.on('error', reject)
283
- })
284
-
285
- // Hello probe cache: peer → {at, ok, body}. TTL 1h, small LRU.
286
- const helloCache = new Map()
287
- const HELLO_TTL = 60 * 60_000
288
-
289
- const probePeer = async peer => {
290
- const hit = helloCache.get(peer)
291
- if (hit && Date.now() - hit.at < HELLO_TTL) return hit
292
- let entry
293
- try {
294
- const { status, body } = await getFromPeer(peer, '/system/peer-hello.json')
295
- entry = { at: Date.now(), ok: status === 200 && body && body.plugin, body }
296
- } catch {
297
- entry = { at: Date.now(), ok: false, body: null }
298
- }
299
- helloCache.set(peer, entry)
300
- if (helloCache.size > 200) helloCache.delete(helloCache.keys().next().value)
301
- return entry
302
- }
303
-
304
- const askPeers = async (peers, envelope) => {
305
- const outcomes = await Promise.all(peers.map(async peer => {
306
- const hello = await probePeer(peer)
307
- if (!hello.ok) {
308
- return { peer, status: 0,
309
- body: { error: 'peer does not answer hello — no similarity federation there' } }
310
- }
311
- if (hello.body.federation && hello.body.federation.enabled === false) {
312
- return { peer, status: 0, body: { error: 'peer has federation switched off' } }
313
- }
314
- try {
315
- const { status, body } = await postToPeer(peer, '/system/peer-search.json', envelope)
316
- return { peer, status, body }
317
- } catch (e) {
318
- return { peer, status: 0, body: { error: e.message } }
319
- }
320
- }))
321
- return outcomes
322
- }
191
+ // Peer federation transport, probes and rate limits live in ./peer.js —
192
+ // one desk per server, built once at startup.
193
+ const { requestIp, getFromPeer, probePeer, askPeers,
194
+ isDuplicate, takeIpToken, takeGlobalToken } = makePeerDesk()
323
195
 
324
196
  // ── GET /system/indexed-domains.json?pattern=glob1,glob2 ──────────────────
325
197
  app.get('/system/indexed-domains.json', (req, res) => {
@@ -433,14 +305,44 @@ const startServer = ({ argv, app }) => {
433
305
  try {
434
306
  const body = await readBody(req)
435
307
  if (!body.query) return res.status(400).json({ error: 'query required' })
308
+
309
+ // body.author narrows to the sites that person owns here. It is applied
310
+ // locally AND forwarded to peers, who resolve the same plain name against
311
+ // their own records — an endpoint that accepted the field but only
312
+ // honoured it remotely would be a trap.
313
+ let domains = body.domains || ['*']
314
+ if (body.author) {
315
+ const res_ = resolveAuthor(farms, String(body.author).slice(0, 100))
316
+ if (res_.ambiguous) {
317
+ return res.status(409).json({
318
+ error: `"${body.author}" matches more than one account here`,
319
+ accounts: res_.ambiguous,
320
+ hint: 'ask again with a username',
321
+ })
322
+ }
323
+ if (res_.available !== false) {
324
+ // Author narrows on a different axis from scope, so the two INTERSECT
325
+ // — replacing the caller's scope would silently widen a search that
326
+ // asked to be narrow. Expand their patterns first, then keep only
327
+ // what this author owns.
328
+ const owned = new Set(res_.sites)
329
+ domains = listDomains(farms, domains, restricted)
330
+ .map(d => d.domain)
331
+ .filter(d => owned.has(d))
332
+ if (!domains.length) domains = ['none'] // match nothing, not everything
333
+ }
334
+ }
335
+
436
336
  const page = await buildReport(
437
- body.query, body.domains || ['*'], body.limit || 10, ctx,
337
+ body.query, domains, body.limit || 10, ctx,
438
338
  body.threshold ?? null, !!body.live, seedOptsFrom(body))
439
339
  if (Array.isArray(body.farms) && body.farms.length) {
440
340
  const envelope = {
441
341
  query: body.query, kind: 'report', limit: body.limit || 10,
442
342
  hops: 0, requestId: crypto.randomBytes(8).toString('hex'),
443
343
  origin: req.hostname,
344
+ // the name as typed, never our resolved username
345
+ ...(body.author ? { author: String(body.author).slice(0, 100) } : {}),
444
346
  }
445
347
  appendPeerSections(page, await askPeers(body.farms.slice(0, 8), envelope))
446
348
  }
@@ -600,7 +502,35 @@ const startServer = ({ argv, app }) => {
600
502
  hint: 'a site opts in with FROM lines on its Federated Farm Search page',
601
503
  })
602
504
  }
603
- const grantedNames = granted.map(d => d.domain)
505
+ let grantedNames = granted.map(d => d.domain)
506
+
507
+ // An author filter is one more field on the envelope, and it travels as
508
+ // the plain name the asker typed — never a username resolved against
509
+ // their map. We answer from OUR OWN ownership records: we cannot resolve
510
+ // another farm's pseudonyms, only ours. A farm that keeps no records
511
+ // ignores the field and answers unfiltered rather than failing.
512
+ let authorFilter = 'none'
513
+ if (envelope.author) {
514
+ const who = String(envelope.author).slice(0, 100)
515
+ const res = resolveAuthor(farms, who)
516
+ if (res.available === false) {
517
+ authorFilter = 'ignored' // no ownership records on this farm
518
+ } else if (res.ambiguous) {
519
+ authorFilter = 'ambiguous' // shared display name — never guess
520
+ grantedNames = []
521
+ } else {
522
+ authorFilter = 'applied'
523
+ const owned = new Set(res.sites)
524
+ grantedNames = grantedNames.filter(d => owned.has(d))
525
+ }
526
+ }
527
+ if (!grantedNames.length) {
528
+ return res.json({
529
+ page: { title: `${envelope.query} Report`, story: [] },
530
+ meta: { ...MODEL_META, version: PLUGIN_VERSION, farm: req.hostname,
531
+ sites: 0, count: 0, authorFilter },
532
+ })
533
+ }
604
534
 
605
535
  const limit = envelope.limit || 10
606
536
  let page
@@ -614,7 +544,7 @@ const startServer = ({ argv, app }) => {
614
544
  }
615
545
  const count = page.story.filter(i => i.type === 'reference').length
616
546
  res.json({ page, meta: { ...MODEL_META, version: PLUGIN_VERSION,
617
- farm: req.hostname, sites: grantedNames.length, count } })
547
+ farm: req.hostname, sites: grantedNames.length, count, authorFilter } })
618
548
  } catch (e) {
619
549
  console.error('[wiki-plugin-similarity] peer-search error:', e.message)
620
550
  res.status(e.code === 'EMBEDDER_DOWN' ? 503 : 500)