wiki-plugin-similarity 0.10.0 → 0.11.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.
- package/client/similarity.js +30 -29
- package/client/similarity.js.map +3 -3
- package/package.json +1 -1
- package/pages/about-similarity-plugin +57 -17
- package/server/farm-lib.js +5 -2
- package/server/farm-search.js +41 -1
- package/server/galaxy-vectors.js +74 -0
- package/server/search-report.js +0 -0
- package/server/server.js +94 -13
- package/server/site-report.js +12 -4
package/client/similarity.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
/* wiki-plugin-similarity - 0.
|
|
2
|
-
(()=>{var
|
|
3
|
-
`)){let
|
|
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=`
|
|
4
5
|
.sim-form { display:flex; gap:6px; margin-bottom:8px; }
|
|
5
6
|
.sim-input { flex:1; padding:6px 8px; font-size:14px; border:1px solid #ccc; border-radius:3px; }
|
|
6
7
|
.sim-btn { padding:6px 14px; background:#c4561d; color:white; border:none;
|
|
@@ -24,63 +25,63 @@
|
|
|
24
25
|
.sim-list td { padding:3px 8px 3px 0; border-bottom:1px solid #f0f0f0; vertical-align:middle; }
|
|
25
26
|
.sim-list td:last-child { text-align:right; color:#999; font-size:11px; }
|
|
26
27
|
.sim-list .sim-flag { margin-right:6px; }
|
|
27
|
-
|
|
28
|
+
`,$t=(t,e)=>`<img class="sim-flag remote" src="${window.wiki.site(t).flag()}"
|
|
28
29
|
title="${t} \u2014 score ${e.toFixed(3)}"
|
|
29
|
-
data-site="${t}">`,
|
|
30
|
-
<style>${
|
|
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>
|
|
31
32
|
<div class="similarity" data-id="${e.id}">
|
|
32
33
|
<div class="sim-form">
|
|
33
|
-
<button class="sim-btn">${
|
|
34
|
+
<button class="sim-btn">${L||"Open"}</button>
|
|
34
35
|
</div>
|
|
35
36
|
<div class="sim-status"></div>
|
|
36
37
|
</div>`);else if(s==="build")t.html(`
|
|
37
|
-
<style>${
|
|
38
|
+
<style>${O}</style>
|
|
38
39
|
<div class="similarity" data-id="${e.id}">
|
|
39
40
|
<div class="sim-form">
|
|
40
|
-
<button class="sim-btn">${
|
|
41
|
+
<button class="sim-btn">${L||`Index ${i.length?i.join(", "):"*"}${f?" (force)":""}`}</button>
|
|
41
42
|
</div>
|
|
42
43
|
<div class="sim-status"></div>
|
|
43
|
-
</div>`);else if(s==="list"){let
|
|
44
|
-
<style>${
|
|
44
|
+
</div>`);else if(s==="list"){let b=i.length?i.join(", "):"*";t.html(`
|
|
45
|
+
<style>${O}</style>
|
|
45
46
|
<div class="similarity" data-id="${e.id}">
|
|
46
|
-
<div class="sim-status">Loading indexed domains (${
|
|
47
|
+
<div class="sim-status">Loading indexed domains (${b})\u2026</div>
|
|
47
48
|
<div class="sim-list"></div>
|
|
48
|
-
</div>`)}else if(s==="similar"){let
|
|
49
|
-
<style>${
|
|
49
|
+
</div>`)}else if(s==="similar"){let b=i.length?i.join(", "):"current domain";t.html(`
|
|
50
|
+
<style>${O}</style>
|
|
50
51
|
<div class="similarity" data-id="${e.id}">
|
|
51
|
-
<div class="sim-status">Finding similar pages across ${
|
|
52
|
+
<div class="sim-status">Finding similar pages across ${b}\u2026</div>
|
|
52
53
|
<div class="sim-results"></div>
|
|
53
|
-
</div>`)}else if(s==="author"||s==="report"||s==="keyword"||s==="sites"){let
|
|
54
|
-
<style>${
|
|
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
56
|
<div class="similarity" data-id="${e.id}">
|
|
56
57
|
<div class="sim-form">
|
|
57
|
-
<input class="sim-input" type="text" placeholder="${
|
|
58
|
+
<input class="sim-input" type="text" placeholder="${C}" />
|
|
58
59
|
<button class="sim-btn">${k}</button>
|
|
59
60
|
</div>
|
|
60
|
-
<div class="sim-status">Domains: ${
|
|
61
|
+
<div class="sim-status">Domains: ${b}</div>
|
|
61
62
|
<div class="sim-results"></div>
|
|
62
|
-
</div>`)}else{let
|
|
63
|
-
<style>${
|
|
63
|
+
</div>`)}else{let b=i.length?i.join(", "):"(current domain)";t.html(`
|
|
64
|
+
<style>${O}</style>
|
|
64
65
|
<div class="similarity" data-id="${e.id}">
|
|
65
66
|
<div class="sim-form">
|
|
66
67
|
<input class="sim-input" type="text" placeholder="Search wiki pages\u2026" />
|
|
67
68
|
<button class="sim-btn">Search</button>
|
|
68
69
|
</div>
|
|
69
|
-
<div class="sim-status">Domains: ${
|
|
70
|
+
<div class="sim-status">Domains: ${b}</div>
|
|
70
71
|
<div class="sim-results"></div>
|
|
71
|
-
</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
73
|
<table>
|
|
73
74
|
<tr><th>Domain</th><th>Pages</th></tr>
|
|
74
|
-
${
|
|
75
|
+
${o.map(({domain:n,page_count:u})=>`
|
|
75
76
|
<tr>
|
|
76
77
|
<td><img class="sim-flag remote" src="${window.wiki.site(n).flag()}"
|
|
77
78
|
title="${n}" data-site="${n}"> ${n}</td>
|
|
78
|
-
<td>${
|
|
79
|
+
<td>${u!=null?u.toLocaleString():"\u2014"}</td>
|
|
79
80
|
</tr>`).join("")}
|
|
80
81
|
</table>
|
|
81
|
-
<p class="sim-count">${
|
|
82
|
-
`);
|
|
83
|
-
`),
|
|
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
|
|
84
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
87
|
//# sourceMappingURL=similarity.js.map
|
package/client/similarity.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/client/similarity.js"],
|
|
4
|
-
"sourcesContent": ["// wiki-plugin-similarity\n//\n// One item type: similarity\n//\n// DSL in item.text (newline-separated lines):\n// * all indexed domains on this server\n// david.* glob pattern\n// *.fish glob pattern\n// david.hitchhikers.earth explicit domain name\n// # comment ignored\n// LIST list indexed domains (mode directive)\n// SIMILAR: high threshold preset AND ambient mode trigger\n// THRESHOLD: 0.72 exact cosine threshold (overrides SIMILAR:)\n// LIMIT: 8 max results shown (default 10)\n// 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// GET /system/embed.json?text=query\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 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, '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'].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 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)\nconst isScope = spec => spec === 'PUBLIC' || spec === 'LOCAL' || spec === 'PRIVATE'\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\nconst getEmbedding = async (text, origin) => {\n const res = await fetch(`${origin}/system/embed.json?text=${encodeURIComponent(text)}`)\n if (!res.ok) throw new Error(`embed failed: ${res.status}`)\n return (await res.json()).vector\n}\n\n// Look up an existing page vector from the current domain's cached index.\n// Returns the float[] vector if found, null otherwise.\nconst lookupPageVector = async (slug, domain) => {\n const pages = await loadVectors(domain)\n const entry = pages.find(p => p.slug === slug)\n return entry ? entry.vector : null\n}\n\n// \u2500\u2500 Cosine search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst cosineScan = (queryVec, domainEntries, { threshold, limit, excludeSlug, excludeDomain }) => {\n const results = []\n for (const { domain, pages } of domainEntries) {\n for (const { slug, title, vector } of pages) {\n if (slug === excludeSlug && domain === excludeDomain) continue\n let dot = 0\n for (let i = 0; i < queryVec.length; i++) dot += queryVec[i] * vector[i]\n if (dot >= threshold) results.push({ domain, slug, title, score: dot })\n }\n }\n results.sort((a, b) => b.score - a.score)\n return results.slice(0, limit)\n}\n\n// \u2500\u2500 Shared domain loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst loadDomainEntries = async (specs, origin) => {\n const domains = await resolveDomains(specs, origin)\n const entries = await Promise.all(\n domains.map(async ({ domain }) => ({ domain, pages: await loadVectors(domain) }))\n )\n return entries.filter(e => e.pages.length > 0)\n}\n\n// \u2500\u2500 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, force, ghostUrl, thresholdSet } =\n parseDSL(item?.text || '')\n const origin = window.location.origin\n const status = div.find('.sim-status')[0]\n const cache = live ? null : readCache(item)\n // Scope = DSL specs plus roster-page domains (resolved once, shared by modes)\n const specsP = effectiveSpecs(specs, rosterRefs)\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 const $page = div.parents('.page')\n const pageTitle = $page.find('.title').text().trim() || document.title\n const currentSlug = slugify(pageTitle)\n const currentDomain = window.location.hostname\n\n const domainEntries = await loadDomainEntries(await specsP, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = `Searching ${total.toLocaleString()} pages\u2026`\n\n let qVec = await lookupPageVector(currentSlug, currentDomain)\n if (!qVec) {\n status.textContent = 'Embedding page (not yet indexed)\u2026'\n const pageText = $page.find('.item').map((_, el) => $(el).text().trim()).get().filter(Boolean).join('\\n')\n qVec = await getEmbedding(pageText || pageTitle, origin)\n }\n\n const scored = cosineScan(qVec, domainEntries, {\n threshold, limit, excludeSlug: currentSlug, excludeDomain: currentDomain,\n })\n renderScored(scored, null)\n if (scored.length) 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 // 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 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 const body = { query, domains: eff.length ? eff : ['*'], limit, live }\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 ;(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 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 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 }),\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 status.textContent = `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 ;(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 = 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 ;(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 = 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", "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", "cosineScan", "queryVec", "domainEntries", "excludeSlug", "excludeDomain", "results", "pages", "vector", "dot", "a", "b", "loadDomainEntries", "domains", "cacheKey", "id", "readCache", "c", "writeCache", "cacheAge", "ts", "
|
|
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"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
{
|
|
5
5
|
"type": "markdown",
|
|
6
6
|
"id": "aec1dfcd5f2bc42b",
|
|
7
|
-
"text": "The Similarity plugin adds search to a Federated Wiki farm
|
|
7
|
+
"text": "The Similarity plugin adds search to a Federated Wiki farm \u2014 semantic similarity over prebuilt vector indexes, ranked search reports, galactic keyword search over each site's own MiniSearch index, and semantic search across the wider federation. One item type \u2014 **similarity** \u2014 with a line-oriented DSL, plus a server component that serves every search same-origin. Works on any host: embedding runs in-process (native ONNX where available, WASM everywhere else)."
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
10
|
"type": "markdown",
|
|
11
11
|
"id": "28edff984cc5334d",
|
|
12
|
-
"text": "## The DSL\n\nThe first ALL-CAPS keyword sets the mode; other lines are domain specs (globs, explicit domains, or the scope keywords PUBLIC / LOCAL / PRIVATE):\n\n- (no keyword)
|
|
12
|
+
"text": "## The DSL\n\nThe first ALL-CAPS keyword sets the mode; other lines are domain specs (globs, explicit domains, or the scope keywords PUBLIC / LOCAL / PRIVATE / GALAXY):\n\n- (no keyword) \u2014 interactive semantic search form\n- `SIMILAR: high|medium|low` \u2014 ambient: pages similar to this page\n- `REPORT` \u2014 server-side ranked, stub-filtered, fork-bundled report\n- `KEYWORD` \u2014 galactic MiniSearch over live site-index.json files\n- `SITES` \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- `LIST` \u2014 table of indexed domains\n- `AUTHOR` \u2014 search results as a draggable ghost page\n- `BUILD` / `FORCE` \u2014 proxy to the farm indexer, where configured\n- `GHOST url` / `BUTTON label` \u2014 open any page-json as a ghost page\n- `ROSTER site/slug` \u2014 add a roster page's sites to the scope (off-farm sites searched via the galaxy cache)\n- `FARM domain` \u2014 ask a peer farm to continue the search. The peer answers from exactly the sites whose own [[Federated Farm Search]] page grants your origin (`FROM` lines; `FROM *` needs the peer admin's `WIKI_PEER_FEDERATION=open`)\n- `GALAXY` \u2014 search the off-farm federation sites indexed into this host's galaxy tree. A scope, not a mode: `GALAXY` alone answers only from the federation (the farm's own pages are never swept in, and `*` never includes galaxy sites); `*` and `GALAXY` together search farm and federation as one\n- `SUBJECT` \u2014 act on the previous page in the lineup, the host a tool page was opened beside\n- `THRESHOLD: 0.72` \u00b7 `LIMIT: 8` \u00b7 `LIVE` \u2014 tuning"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"type": "markdown",
|
|
@@ -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`
|
|
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."
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"type": "markdown",
|
|
71
71
|
"id": "21eb5354d2adb29e",
|
|
72
|
-
"text": "**Version:** 0.
|
|
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."
|
|
73
73
|
}
|
|
74
74
|
],
|
|
75
75
|
"plugin": "similarity",
|
|
@@ -82,12 +82,12 @@
|
|
|
82
82
|
{
|
|
83
83
|
"type": "markdown",
|
|
84
84
|
"id": "aec1dfcd5f2bc42b",
|
|
85
|
-
"text": "The Similarity plugin adds search to a Federated Wiki farm
|
|
85
|
+
"text": "The Similarity plugin adds search to a Federated Wiki farm \u2014 semantic similarity over prebuilt vector indexes, ranked search reports, and galactic keyword search over each site's own MiniSearch index. One item type \u2014 **similarity** \u2014 with a line-oriented DSL, plus a server component that serves every search same-origin. Works on any host: embedding runs in-process (native ONNX where available, WASM everywhere else)."
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
88
|
"type": "markdown",
|
|
89
89
|
"id": "28edff984cc5334d",
|
|
90
|
-
"text": "## The DSL\n\nThe first ALL-CAPS keyword sets the mode; other lines are domain specs (globs, explicit domains, or the scope keywords PUBLIC / LOCAL / PRIVATE):\n\n- (no keyword)
|
|
90
|
+
"text": "## The DSL\n\nThe first ALL-CAPS keyword sets the mode; other lines are domain specs (globs, explicit domains, or the scope keywords PUBLIC / LOCAL / PRIVATE):\n\n- (no keyword) \u2014 interactive semantic search form\n- `SIMILAR: high|medium|low` \u2014 ambient: pages similar to this page\n- `REPORT` \u2014 server-side ranked, stub-filtered, fork-bundled report\n- `KEYWORD` \u2014 galactic MiniSearch over live site-index.json files\n- `LIST` \u2014 table of indexed domains\n- `AUTHOR` \u2014 search results as a draggable ghost page\n- `BUILD` / `FORCE` \u2014 proxy to the farm indexer, where configured\n- `GHOST url` / `BUTTON label` \u2014 open any page-json as a ghost page\n- `THRESHOLD: 0.72` \u00b7 `LIMIT: 8` \u00b7 `LIVE` \u2014 tuning"
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
"type": "markdown",
|
|
@@ -132,17 +132,17 @@
|
|
|
132
132
|
{
|
|
133
133
|
"type": "markdown",
|
|
134
134
|
"id": "d9a96dfebb9d1692",
|
|
135
|
-
"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- `GET /system/farm-search.json?q=&pattern=&limit=`\n- `GET /system/build-index.json?domains=` (proxied to the farm indexer)\n\nIndexes are `status/semantic-vectors.json` per domain
|
|
135
|
+
"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- `GET /system/farm-search.json?q=&pattern=&limit=`\n- `GET /system/build-index.json?domains=` (proxied to the farm indexer)\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."
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
138
|
"type": "markdown",
|
|
139
139
|
"id": "21eb5354d2adb29e",
|
|
140
|
-
"text": "**Version:** 0.5.1\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]
|
|
140
|
+
"text": "**Version:** 0.5.1\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."
|
|
141
141
|
}
|
|
142
142
|
]
|
|
143
143
|
},
|
|
144
144
|
"date": 1783782734556,
|
|
145
|
-
"provenance": "Authored by an AI agent
|
|
145
|
+
"provenance": "Authored by an AI agent \u2014 Claude Fable 5 via the [[fedwiki-page]] skill. [https://claude.com/claude-code Claude Code]"
|
|
146
146
|
},
|
|
147
147
|
{
|
|
148
148
|
"type": "edit",
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
"item": {
|
|
151
151
|
"type": "markdown",
|
|
152
152
|
"id": "28edff984cc5334d",
|
|
153
|
-
"text": "## The DSL\n\nThe first ALL-CAPS keyword sets the mode; other lines are domain specs (globs, explicit domains, or the scope keywords PUBLIC / LOCAL / PRIVATE):\n\n- (no keyword)
|
|
153
|
+
"text": "## The DSL\n\nThe first ALL-CAPS keyword sets the mode; other lines are domain specs (globs, explicit domains, or the scope keywords PUBLIC / LOCAL / PRIVATE):\n\n- (no keyword) \u2014 interactive semantic search form\n- `SIMILAR: high|medium|low` \u2014 ambient: pages similar to this page\n- `REPORT` \u2014 server-side ranked, stub-filtered, fork-bundled report\n- `KEYWORD` \u2014 galactic MiniSearch over live site-index.json files\n- `LIST` \u2014 table of indexed domains\n- `AUTHOR` \u2014 search results as a draggable ghost page\n- `BUILD` / `FORCE` \u2014 proxy to the farm indexer, where configured\n- `GHOST url` / `BUTTON label` \u2014 open any page-json as a ghost page\n- `ROSTER site/slug` \u2014 add a roster page's sites to the scope (off-farm sites searched via the galaxy cache)\n- `FARM domain` \u2014 ask a peer farm to continue the search (experimental; requires the peer's `search-peers` roster to allow you)\n- `THRESHOLD: 0.72` \u00b7 `LIMIT: 8` \u00b7 `LIVE` \u2014 tuning"
|
|
154
154
|
},
|
|
155
155
|
"date": 1783800718188
|
|
156
156
|
},
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
"item": {
|
|
161
161
|
"type": "markdown",
|
|
162
162
|
"id": "d9a96dfebb9d1692",
|
|
163
|
-
"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- `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=`
|
|
163
|
+
"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- `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- `POST /system/peer-search.json` \u2014 answer another farm's search (guarded: allowlist roster, hop limit, replay dedup, rate limit; 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."
|
|
164
164
|
},
|
|
165
165
|
"date": 1783800718188
|
|
166
166
|
},
|
|
@@ -170,7 +170,7 @@
|
|
|
170
170
|
"item": {
|
|
171
171
|
"type": "markdown",
|
|
172
172
|
"id": "21eb5354d2adb29e",
|
|
173
|
-
"text": "**Version:** 0.6.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]
|
|
173
|
+
"text": "**Version:** 0.6.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."
|
|
174
174
|
},
|
|
175
175
|
"date": 1783800718188
|
|
176
176
|
},
|
|
@@ -180,7 +180,7 @@
|
|
|
180
180
|
"item": {
|
|
181
181
|
"type": "markdown",
|
|
182
182
|
"id": "28edff984cc5334d",
|
|
183
|
-
"text": "## The DSL\n\nThe first ALL-CAPS keyword sets the mode; other lines are domain specs (globs, explicit domains, or the scope keywords PUBLIC / LOCAL / PRIVATE):\n\n- (no keyword)
|
|
183
|
+
"text": "## The DSL\n\nThe first ALL-CAPS keyword sets the mode; other lines are domain specs (globs, explicit domains, or the scope keywords PUBLIC / LOCAL / PRIVATE):\n\n- (no keyword) \u2014 interactive semantic search form\n- `SIMILAR: high|medium|low` \u2014 ambient: pages similar to this page\n- `REPORT` \u2014 server-side ranked, stub-filtered, fork-bundled report\n- `KEYWORD` \u2014 galactic MiniSearch over live site-index.json files\n- `LIST` \u2014 table of indexed domains\n- `AUTHOR` \u2014 search results as a draggable ghost page\n- `BUILD` / `FORCE` \u2014 proxy to the farm indexer, where configured\n- `GHOST url` / `BUTTON label` \u2014 open any page-json as a ghost page\n- `ROSTER site/slug` \u2014 add a roster page's sites to the scope (off-farm sites searched via the galaxy cache)\n- `FARM domain` \u2014 ask a peer farm to continue the search. The peer answers from exactly the sites whose own [[Federated Farm Search]] page grants your origin (`FROM` lines; `FROM *` needs the peer admin's `WIKI_PEER_FEDERATION=open`)\n- `THRESHOLD: 0.72` \u00b7 `LIMIT: 8` \u00b7 `LIVE` \u2014 tuning"
|
|
184
184
|
},
|
|
185
185
|
"date": 1784591495272
|
|
186
186
|
},
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
"item": {
|
|
191
191
|
"type": "markdown",
|
|
192
192
|
"id": "d9a96dfebb9d1692",
|
|
193
|
-
"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- `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=`
|
|
193
|
+
"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- `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/peer-hello.json` \u2014 capability probe: version, model, federation status (404 = plugin absent)\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."
|
|
194
194
|
},
|
|
195
195
|
"date": 1784591495272
|
|
196
196
|
},
|
|
@@ -200,7 +200,7 @@
|
|
|
200
200
|
"item": {
|
|
201
201
|
"type": "markdown",
|
|
202
202
|
"id": "21eb5354d2adb29e",
|
|
203
|
-
"text": "**Version:** 0.7.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]
|
|
203
|
+
"text": "**Version:** 0.7.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."
|
|
204
204
|
},
|
|
205
205
|
"date": 1784591495272
|
|
206
206
|
},
|
|
@@ -210,7 +210,7 @@
|
|
|
210
210
|
"item": {
|
|
211
211
|
"type": "markdown",
|
|
212
212
|
"id": "d9a96dfebb9d1692",
|
|
213
|
-
"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- `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=`
|
|
213
|
+
"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- `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/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."
|
|
214
214
|
},
|
|
215
215
|
"date": 1784706029671
|
|
216
216
|
},
|
|
@@ -220,9 +220,49 @@
|
|
|
220
220
|
"item": {
|
|
221
221
|
"type": "markdown",
|
|
222
222
|
"id": "21eb5354d2adb29e",
|
|
223
|
-
"text": "**Version:** 0.9.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]
|
|
223
|
+
"text": "**Version:** 0.9.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."
|
|
224
224
|
},
|
|
225
225
|
"date": 1784706029671
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"type": "edit",
|
|
229
|
+
"id": "aec1dfcd5f2bc42b",
|
|
230
|
+
"item": {
|
|
231
|
+
"type": "markdown",
|
|
232
|
+
"id": "aec1dfcd5f2bc42b",
|
|
233
|
+
"text": "The Similarity plugin adds search to a Federated Wiki farm \u2014 semantic similarity over prebuilt vector indexes, ranked search reports, galactic keyword search over each site's own MiniSearch index, and semantic search across the wider federation. One item type \u2014 **similarity** \u2014 with a line-oriented DSL, plus a server component that serves every search same-origin. Works on any host: embedding runs in-process (native ONNX where available, WASM everywhere else)."
|
|
234
|
+
},
|
|
235
|
+
"date": 1785280898931
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"type": "edit",
|
|
239
|
+
"id": "28edff984cc5334d",
|
|
240
|
+
"item": {
|
|
241
|
+
"type": "markdown",
|
|
242
|
+
"id": "28edff984cc5334d",
|
|
243
|
+
"text": "## The DSL\n\nThe first ALL-CAPS keyword sets the mode; other lines are domain specs (globs, explicit domains, or the scope keywords PUBLIC / LOCAL / PRIVATE / GALAXY):\n\n- (no keyword) \u2014 interactive semantic search form\n- `SIMILAR: high|medium|low` \u2014 ambient: pages similar to this page\n- `REPORT` \u2014 server-side ranked, stub-filtered, fork-bundled report\n- `KEYWORD` \u2014 galactic MiniSearch over live site-index.json files\n- `SITES` \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- `LIST` \u2014 table of indexed domains\n- `AUTHOR` \u2014 search results as a draggable ghost page\n- `BUILD` / `FORCE` \u2014 proxy to the farm indexer, where configured\n- `GHOST url` / `BUTTON label` \u2014 open any page-json as a ghost page\n- `ROSTER site/slug` \u2014 add a roster page's sites to the scope (off-farm sites searched via the galaxy cache)\n- `FARM domain` \u2014 ask a peer farm to continue the search. The peer answers from exactly the sites whose own [[Federated Farm Search]] page grants your origin (`FROM` lines; `FROM *` needs the peer admin's `WIKI_PEER_FEDERATION=open`)\n- `GALAXY` \u2014 search the off-farm federation sites indexed into this host's galaxy tree. A scope, not a mode: `GALAXY` alone answers only from the federation (the farm's own pages are never swept in, and `*` never includes galaxy sites); `*` and `GALAXY` together search farm and federation as one\n- `SUBJECT` \u2014 act on the previous page in the lineup, the host a tool page was opened beside\n- `THRESHOLD: 0.72` \u00b7 `LIMIT: 8` \u00b7 `LIVE` \u2014 tuning"
|
|
244
|
+
},
|
|
245
|
+
"date": 1785280898931
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"type": "edit",
|
|
249
|
+
"id": "d9a96dfebb9d1692",
|
|
250
|
+
"item": {
|
|
251
|
+
"type": "markdown",
|
|
252
|
+
"id": "d9a96dfebb9d1692",
|
|
253
|
+
"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."
|
|
254
|
+
},
|
|
255
|
+
"date": 1785280898931
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"type": "edit",
|
|
259
|
+
"id": "21eb5354d2adb29e",
|
|
260
|
+
"item": {
|
|
261
|
+
"type": "markdown",
|
|
262
|
+
"id": "21eb5354d2adb29e",
|
|
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
|
+
},
|
|
265
|
+
"date": 1785280898931
|
|
226
266
|
}
|
|
227
267
|
]
|
|
228
268
|
}
|
package/server/farm-lib.js
CHANGED
|
@@ -30,10 +30,12 @@ const globMatch = (pattern, str) => {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// ── Scope keywords (exact uppercase, per DSL convention) ──────────────────────
|
|
33
|
-
// * all farms
|
|
33
|
+
// * all farms — EXCEPT galaxy: off-farm sites join a search only by
|
|
34
|
+
// explicit opt-in (GALAXY, an explicit domain, or a roster)
|
|
34
35
|
// PUBLIC domains in 'public' farms (Nextcloud mirror)
|
|
35
36
|
// LOCAL domains in the primary ('local') farm
|
|
36
37
|
// PRIVATE public domains marked "restricted": true in a farm config
|
|
38
|
+
// GALAXY off-farm federation sites in the 'galaxy' tree (see galaxy-vectors.js)
|
|
37
39
|
|
|
38
40
|
const loadRestricted = publicFarms => {
|
|
39
41
|
const restricted = new Set()
|
|
@@ -55,10 +57,11 @@ const loadRestricted = publicFarms => {
|
|
|
55
57
|
|
|
56
58
|
const matchesAny = (domain, kind, patterns, restricted) =>
|
|
57
59
|
patterns.some(p => {
|
|
58
|
-
if (p === '*') return
|
|
60
|
+
if (p === '*') return kind !== 'galaxy'
|
|
59
61
|
if (p === 'PUBLIC') return kind === 'public'
|
|
60
62
|
if (p === 'LOCAL') return kind === 'local'
|
|
61
63
|
if (p === 'PRIVATE') return kind === 'public' && restricted.has(domain)
|
|
64
|
+
if (p === 'GALAXY') return kind === 'galaxy'
|
|
62
65
|
return globMatch(p, domain)
|
|
63
66
|
})
|
|
64
67
|
|
package/server/farm-search.js
CHANGED
|
@@ -85,6 +85,46 @@ const searchFarm = (farms, patterns, restricted, query, limit, exclude = null) =
|
|
|
85
85
|
return { results: results.slice(0, limit), searched, matched: results.length }
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// ── Same-title scan ───────────────────────────────────────────────────────────
|
|
89
|
+
// A fork carries the slug, so slug presence in a site's page list IS "this
|
|
90
|
+
// page lives there". An existence question, not a search — keyword scoring is
|
|
91
|
+
// the wrong tool here: a twin's score can rank below fifty noisy word-matches
|
|
92
|
+
// on a large farm. Two sources per domain, freshest first: the per-edit
|
|
93
|
+
// sitemap (via the site-index cache above), else semantic-meta.json — the
|
|
94
|
+
// tiny slug+title list the indexer writes beside the vectors, which synced
|
|
95
|
+
// mirror sites carry even when their sitemap is not synced.
|
|
96
|
+
|
|
97
|
+
const metaCache = new Map() // domain → {mtime, titles}
|
|
98
|
+
|
|
99
|
+
const loadMetaTitles = (farm, domain) => {
|
|
100
|
+
const p = path.join(farm, domain, 'status', 'semantic-meta.json')
|
|
101
|
+
let mtime
|
|
102
|
+
try { mtime = fs.statSync(p).mtimeMs } catch { return null }
|
|
103
|
+
const hit = metaCache.get(domain)
|
|
104
|
+
if (hit && hit.mtime === mtime) return hit.titles
|
|
105
|
+
try {
|
|
106
|
+
const titles = new Map(
|
|
107
|
+
JSON.parse(fs.readFileSync(p, 'utf8')).map(e => [e.slug, e.title]))
|
|
108
|
+
metaCache.set(domain, { mtime, titles })
|
|
109
|
+
return titles
|
|
110
|
+
} catch { return null }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const findTwins = (farms, patterns, restricted, slug, limit = 25) => {
|
|
114
|
+
const domains = listDomains(farms, patterns, restricted)
|
|
115
|
+
const twins = []
|
|
116
|
+
for (const { farm, domain } of domains) {
|
|
117
|
+
const entry = loadDomainIndex(farm, domain)
|
|
118
|
+
const titles = entry ? entry.titles : loadMetaTitles(farm, domain)
|
|
119
|
+
if (!titles) continue
|
|
120
|
+
const title = titles.get(slug)
|
|
121
|
+
if (title === undefined) continue
|
|
122
|
+
twins.push({ domain, slug, title: title || slug })
|
|
123
|
+
if (twins.length >= limit) break
|
|
124
|
+
}
|
|
125
|
+
return twins
|
|
126
|
+
}
|
|
127
|
+
|
|
88
128
|
// ── Report page JSON (ghost-page ready, mirrors search-report style) ──────────
|
|
89
129
|
|
|
90
130
|
const keywordReportPage = (query, { results, searched, matched }, limit, specs) => {
|
|
@@ -121,4 +161,4 @@ const keywordReportPage = (query, { results, searched, matched }, limit, specs)
|
|
|
121
161
|
return { title: `${query} Keyword Search`, story }
|
|
122
162
|
}
|
|
123
163
|
|
|
124
|
-
module.exports = { searchFarm, keywordReportPage }
|
|
164
|
+
module.exports = { searchFarm, keywordReportPage, findTwins }
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// wiki-plugin-similarity — galaxy vector store (CommonJS)
|
|
2
|
+
//
|
|
3
|
+
// The galaxy tree is a farm-shaped directory of OFF-FARM federation sites,
|
|
4
|
+
// written by the galaxy indexer (~/src/fedwiki-galaxy on the writer host):
|
|
5
|
+
//
|
|
6
|
+
// {GALAXY_ROOT}/{domain}/status/semantic-vectors.json
|
|
7
|
+
// [{slug, title, vector, chars, items, date, synopsis}]
|
|
8
|
+
//
|
|
9
|
+
// Entries are a superset of the farm shape: enrichment fields are precomputed
|
|
10
|
+
// at index time because off-farm pages have no local page JSON for
|
|
11
|
+
// search-report's enrich() to read.
|
|
12
|
+
//
|
|
13
|
+
// Farm vectors (149 MB across the whole farm) are cheap enough to re-read per
|
|
14
|
+
// query; galaxy scale is not. This module holds deserialized per-site entries
|
|
15
|
+
// in a byte-capped LRU keyed by file mtime, so a repeat query over a followed
|
|
16
|
+
// roster costs no JSON parsing at all.
|
|
17
|
+
|
|
18
|
+
const fs = require('node:fs')
|
|
19
|
+
const path = require('node:path')
|
|
20
|
+
|
|
21
|
+
const CACHE_BYTES = parseInt(process.env.WIKI_GALAXY_VECTOR_CACHE_MB || '512', 10) * 1024 * 1024
|
|
22
|
+
|
|
23
|
+
// Galaxy root: env override, else the Pi5 tree, else the Mac staging tree.
|
|
24
|
+
const galaxyRoot = () => {
|
|
25
|
+
if (process.env.WIKI_GALAXY_VECTORS) return process.env.WIKI_GALAXY_VECTORS
|
|
26
|
+
const pi5 = '/mnt/wikimedia/galaxy'
|
|
27
|
+
try { fs.accessSync(pi5, fs.constants.F_OK); return pi5 } catch { /* not the Pi5 */ }
|
|
28
|
+
return path.join(process.env.HOME || '/tmp', '.cache', 'wiki-similarity', 'galaxy-vectors')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// domain → {mtimeMs, bytes, pages}
|
|
32
|
+
const cache = new Map()
|
|
33
|
+
let cachedBytes = 0
|
|
34
|
+
|
|
35
|
+
const evictUntil = budget => {
|
|
36
|
+
for (const [domain, entry] of cache) {
|
|
37
|
+
if (cachedBytes <= budget) break
|
|
38
|
+
cache.delete(domain)
|
|
39
|
+
cachedBytes -= entry.bytes
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Same contract as search-report's loadVectors(farm, domain) — returns the
|
|
44
|
+
// entry array (with enrichment fields) or [].
|
|
45
|
+
const loadGalaxyVectors = (farm, domain) => {
|
|
46
|
+
const file = path.join(farm, domain, 'status', 'semantic-vectors.json')
|
|
47
|
+
let stat
|
|
48
|
+
try { stat = fs.statSync(file) } catch { return [] }
|
|
49
|
+
|
|
50
|
+
const hit = cache.get(domain)
|
|
51
|
+
if (hit && hit.mtimeMs === stat.mtimeMs) {
|
|
52
|
+
cache.delete(domain) // refresh LRU position
|
|
53
|
+
cache.set(domain, hit)
|
|
54
|
+
return hit.pages
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let pages
|
|
58
|
+
try { pages = JSON.parse(fs.readFileSync(file, 'utf8')) } catch { return [] }
|
|
59
|
+
if (!Array.isArray(pages)) return []
|
|
60
|
+
|
|
61
|
+
if (hit) { cache.delete(domain); cachedBytes -= hit.bytes }
|
|
62
|
+
cachedBytes += stat.size
|
|
63
|
+
cache.set(domain, { mtimeMs: stat.mtimeMs, bytes: stat.size, pages })
|
|
64
|
+
evictUntil(CACHE_BYTES)
|
|
65
|
+
return pages
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const galaxyCacheStats = () => ({
|
|
69
|
+
sites: cache.size,
|
|
70
|
+
bytes: cachedBytes,
|
|
71
|
+
capBytes: CACHE_BYTES,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
module.exports = { galaxyRoot, loadGalaxyVectors, galaxyCacheStats }
|
package/server/search-report.js
CHANGED
|
Binary file
|
package/server/server.js
CHANGED
|
@@ -10,21 +10,33 @@
|
|
|
10
10
|
// [{domain, page_count}] for domains with a semantic-vectors.json.
|
|
11
11
|
// GET /system/semantic-vectors.json[?domain=]
|
|
12
12
|
// Serves {farm}/{domain}/status/semantic-vectors.json.
|
|
13
|
-
// GET
|
|
13
|
+
// GET|POST /system/embed.json ?text=… | {text}
|
|
14
14
|
// 384-dim unit vector via the crash-isolated child-process embedder
|
|
15
15
|
// (BAAI/bge-small-en-v1.5 — same model the indexes were built with).
|
|
16
|
+
// POST for whole-page prose (long GET query strings die with 431).
|
|
16
17
|
// Set WIKI_EMBED_URL to proxy to an external embedder instead.
|
|
17
18
|
// GET /system/similarity-health.json
|
|
18
19
|
// Embedder supervisor state (child-process | semindex | url, breaker,
|
|
19
20
|
// recent crashes) — 200 always; diagnosable from outside.
|
|
20
|
-
// POST /system/search-report.json {query, domains, limit, threshold, live
|
|
21
|
+
// POST /system/search-report.json {query, domains, limit, threshold, live,
|
|
22
|
+
// vector?, seed?, text?, excludePage?}
|
|
21
23
|
// Ranked, stub-filtered, fork-bundled semantic report (page JSON).
|
|
22
|
-
//
|
|
24
|
+
// Optionally seeded by an existing page: vector (precomputed embedding) >
|
|
25
|
+
// seed {site, slug} (stored page vector from farm disk) > text (embedded
|
|
26
|
+
// instead of query). excludePage {site, slug} drops the host page and its
|
|
27
|
+
// slug-fork family from the results; defaults to seed.
|
|
28
|
+
// POST /system/site-report.json {query, domains, limit, format,
|
|
29
|
+
// vector?, seed?, text?, excludePage?}
|
|
23
30
|
// Which site should this page go on? Per-domain aggregation of the
|
|
24
31
|
// page-vector scan (page JSON, or flat JSON with format: 'flat').
|
|
32
|
+
// Same seed params; excludePage keeps an existing page from voting for
|
|
33
|
+
// its own home site.
|
|
25
34
|
// GET /system/farm-search.json?q=…&pattern=…&limit=…
|
|
26
35
|
// Galactic keyword search — reads each site's own per-edit MiniSearch
|
|
27
36
|
// index (status/site-index.json). No index building.
|
|
37
|
+
// GET /system/title-twins.json?slug=…&pattern=…&limit=…
|
|
38
|
+
// Which sites carry a page with this slug? Existence scan over each
|
|
39
|
+
// site's sitemap (forks share the slug) — [{domain, slug, title}].
|
|
28
40
|
// GET /system/build-index.json?domains=…&force=…
|
|
29
41
|
// Proxy to the farm indexer (WIKI_INDEXER_URL) when configured; heavy
|
|
30
42
|
// embedding is the indexer's job (Pi5 on the Hitchhikers farm), never
|
|
@@ -56,8 +68,9 @@ const { loadRestricted, matchesAny, listDomains, findInFarms } = require('./farm
|
|
|
56
68
|
const embedder = require('./embedder')
|
|
57
69
|
const { buildReport } = require('./search-report')
|
|
58
70
|
const { buildSiteReport } = require('./site-report')
|
|
59
|
-
const { searchFarm, keywordReportPage } = require('./farm-search')
|
|
71
|
+
const { searchFarm, keywordReportPage, findTwins } = require('./farm-search')
|
|
60
72
|
const { searchGalaxy } = require('./galaxy-search')
|
|
73
|
+
const { galaxyRoot, galaxyCacheStats } = require('./galaxy-vectors')
|
|
61
74
|
const { ceiling, grantingDomains, makeDedup, makeBucket, guardEnvelope } = require('./peer-guard')
|
|
62
75
|
|
|
63
76
|
const MODEL_META = { model: 'BAAI/bge-small-en-v1.5', dim: 384 }
|
|
@@ -197,8 +210,13 @@ const appendPeerSections = (page, peerOutcomes) => {
|
|
|
197
210
|
const startServer = ({ argv, app }) => {
|
|
198
211
|
// Farm root: argv.status = {farm}/{thisDomain}/status → go up two levels
|
|
199
212
|
const farmRoot = path.dirname(path.dirname(argv.status))
|
|
200
|
-
// primary farm is 'local'; extra farms (Nextcloud mirror) are 'public'
|
|
213
|
+
// primary farm is 'local'; extra farms (Nextcloud mirror) are 'public'.
|
|
214
|
+
// The galaxy tree (off-farm federation sites, written by the galaxy
|
|
215
|
+
// indexer) joins as kind 'galaxy' when present — never matched by '*',
|
|
216
|
+
// only by GALAXY, an explicit domain, or a roster (see farm-lib.js).
|
|
201
217
|
const farms = [[farmRoot, 'local'], ...EXTRA_FARMS.map(f => [f, 'public'])]
|
|
218
|
+
const galaxyDir = galaxyRoot()
|
|
219
|
+
if (fs.existsSync(galaxyDir)) farms.push([galaxyDir, 'galaxy'])
|
|
202
220
|
const restricted = loadRestricted(EXTRA_FARMS)
|
|
203
221
|
const ctx = { farms, restricted, embed: embedText }
|
|
204
222
|
|
|
@@ -222,9 +240,11 @@ const startServer = ({ argv, app }) => {
|
|
|
222
240
|
for (const route of ['/system/indexed-domains.json', '/system/semantic-vectors.json',
|
|
223
241
|
'/system/embed.json', '/system/search-report.json',
|
|
224
242
|
'/system/site-report.json',
|
|
225
|
-
'/system/farm-search.json', '/system/
|
|
243
|
+
'/system/farm-search.json', '/system/title-twins.json',
|
|
244
|
+
'/system/build-index.json',
|
|
226
245
|
'/system/galaxy-search.json', '/system/peer-search.json',
|
|
227
|
-
'/system/peer-hello.json', '/system/similarity-health.json'
|
|
246
|
+
'/system/peer-hello.json', '/system/similarity-health.json',
|
|
247
|
+
'/system/galaxy-registry.json']) {
|
|
228
248
|
app.options(route, (req, res) => { cors(res); res.sendStatus(204) })
|
|
229
249
|
}
|
|
230
250
|
|
|
@@ -321,6 +341,17 @@ const startServer = ({ argv, app }) => {
|
|
|
321
341
|
res.json(results)
|
|
322
342
|
})
|
|
323
343
|
|
|
344
|
+
// ── GET /system/galaxy-registry.json ──────────────────────────────────────
|
|
345
|
+
// Read-only view of the galaxy site registry (written by the galaxy
|
|
346
|
+
// indexer host — the similarity server never writes it).
|
|
347
|
+
app.get('/system/galaxy-registry.json', (req, res) => {
|
|
348
|
+
cors(res)
|
|
349
|
+
fs.readFile(path.join(galaxyDir, 'registry.json'), 'utf8', (err, data) => {
|
|
350
|
+
if (err) return res.status(404).json({ error: 'no galaxy registry on this host' })
|
|
351
|
+
res.type('application/json').send(data)
|
|
352
|
+
})
|
|
353
|
+
})
|
|
354
|
+
|
|
324
355
|
// ── GET /system/semantic-vectors.json[?domain=] ────────────────────────────
|
|
325
356
|
app.get('/system/semantic-vectors.json', (req, res) => {
|
|
326
357
|
cors(res)
|
|
@@ -337,10 +368,17 @@ const startServer = ({ argv, app }) => {
|
|
|
337
368
|
})
|
|
338
369
|
})
|
|
339
370
|
|
|
340
|
-
// ── GET /system/embed.json
|
|
341
|
-
|
|
371
|
+
// ── GET|POST /system/embed.json ────────────────────────────────────────────
|
|
372
|
+
// GET ?text=… suits short queries; POST {text} carries whole-page prose —
|
|
373
|
+
// long text in a GET query string overflows Node's header limit and dies
|
|
374
|
+
// with 431 before any handler runs. One handler, both verbs (the semindex
|
|
375
|
+
// dual-verb house pattern).
|
|
376
|
+
const embedHandler = async (req, res) => {
|
|
342
377
|
cors(res)
|
|
343
|
-
|
|
378
|
+
let text = req.query.text
|
|
379
|
+
if (!text && req.method === 'POST') {
|
|
380
|
+
try { text = (await readBody(req)).text } catch { /* fall through to 400 */ }
|
|
381
|
+
}
|
|
344
382
|
if (!text) return res.status(400).json({ error: 'text parameter required' })
|
|
345
383
|
try {
|
|
346
384
|
res.json({ vector: await embedText(text) })
|
|
@@ -349,7 +387,28 @@ const startServer = ({ argv, app }) => {
|
|
|
349
387
|
res.status(e.code === 'EMBEDDER_DOWN' ? 503 : 502)
|
|
350
388
|
.json({ error: `embedding unavailable: ${e.message}` })
|
|
351
389
|
}
|
|
352
|
-
}
|
|
390
|
+
}
|
|
391
|
+
app.get('/system/embed.json', embedHandler)
|
|
392
|
+
app.post('/system/embed.json', embedHandler)
|
|
393
|
+
|
|
394
|
+
// Seed params shared by the report routes: a report can be seeded by an
|
|
395
|
+
// existing page rather than typed text. Validated here so the pipelines can
|
|
396
|
+
// trust shapes: vector must be a full-dimension number array; seed and
|
|
397
|
+
// excludePage need site+slug strings; excludePage defaults to seed (a
|
|
398
|
+
// report about a page should not lead with the page itself).
|
|
399
|
+
const seedOptsFrom = body => {
|
|
400
|
+
const vec = Array.isArray(body.vector) && body.vector.length === MODEL_META.dim &&
|
|
401
|
+
body.vector.every(x => typeof x === 'number') ? body.vector : null
|
|
402
|
+
const ref = v => (v && typeof v.site === 'string' && typeof v.slug === 'string')
|
|
403
|
+
? { site: v.site, slug: v.slug } : null
|
|
404
|
+
const seed = ref(body.seed)
|
|
405
|
+
return {
|
|
406
|
+
vector: vec,
|
|
407
|
+
seed,
|
|
408
|
+
text: typeof body.text === 'string' && body.text.trim() ? body.text : null,
|
|
409
|
+
excludePage: ref(body.excludePage) || seed,
|
|
410
|
+
}
|
|
411
|
+
}
|
|
353
412
|
|
|
354
413
|
// ── GET /system/similarity-health.json — embedder supervisor state ────────
|
|
355
414
|
app.get('/system/similarity-health.json', (req, res) => {
|
|
@@ -359,11 +418,16 @@ const startServer = ({ argv, app }) => {
|
|
|
359
418
|
version: PLUGIN_VERSION,
|
|
360
419
|
...MODEL_META,
|
|
361
420
|
embedder: EMBED_URL ? { via: 'url', url: EMBED_URL } : embedder.status(),
|
|
421
|
+
galaxy: fs.existsSync(galaxyDir)
|
|
422
|
+
? { root: galaxyDir, cache: galaxyCacheStats() }
|
|
423
|
+
: null,
|
|
362
424
|
})
|
|
363
425
|
})
|
|
364
426
|
|
|
365
427
|
// ── POST /system/search-report.json ────────────────────────────────────────
|
|
366
428
|
// body.farms: peer farms asked to continue the search (FARM prototype).
|
|
429
|
+
// Optional page seed: {vector | seed: {site, slug} | text} + excludePage —
|
|
430
|
+
// query stays required (report title + title-term boost). See seedOptsFrom.
|
|
367
431
|
app.post('/system/search-report.json', async (req, res) => {
|
|
368
432
|
cors(res)
|
|
369
433
|
try {
|
|
@@ -371,7 +435,7 @@ const startServer = ({ argv, app }) => {
|
|
|
371
435
|
if (!body.query) return res.status(400).json({ error: 'query required' })
|
|
372
436
|
const page = await buildReport(
|
|
373
437
|
body.query, body.domains || ['*'], body.limit || 10, ctx,
|
|
374
|
-
body.threshold ?? null, !!body.live)
|
|
438
|
+
body.threshold ?? null, !!body.live, seedOptsFrom(body))
|
|
375
439
|
if (Array.isArray(body.farms) && body.farms.length) {
|
|
376
440
|
const envelope = {
|
|
377
441
|
query: body.query, kind: 'report', limit: body.limit || 10,
|
|
@@ -399,7 +463,7 @@ const startServer = ({ argv, app }) => {
|
|
|
399
463
|
if (!body.query) return res.status(400).json({ error: 'query required' })
|
|
400
464
|
res.json(await buildSiteReport(
|
|
401
465
|
body.query, body.domains || ['*'], body.limit || 10, ctx,
|
|
402
|
-
body.format || null))
|
|
466
|
+
body.format || null, seedOptsFrom(body)))
|
|
403
467
|
} catch (e) {
|
|
404
468
|
console.error('[wiki-plugin-similarity] site-report error:', e.message)
|
|
405
469
|
res.status(e.code === 'EMBEDDER_DOWN' ? 503 : 500)
|
|
@@ -407,6 +471,23 @@ const startServer = ({ argv, app }) => {
|
|
|
407
471
|
}
|
|
408
472
|
})
|
|
409
473
|
|
|
474
|
+
// ── GET /system/title-twins.json?slug=…&pattern=…&limit=… ────────────────
|
|
475
|
+
// Which sites carry a page with this slug? An existence scan over each
|
|
476
|
+
// site's own sitemap (mtime-cached), not a ranked search — forks share the
|
|
477
|
+
// slug by definition. Answers [{domain, slug, title}].
|
|
478
|
+
app.get('/system/title-twins.json', (req, res) => {
|
|
479
|
+
cors(res)
|
|
480
|
+
const slug = (req.query.slug || '').trim()
|
|
481
|
+
if (!slug) return res.status(400).json({ error: 'slug parameter required' })
|
|
482
|
+
const patterns = (req.query.pattern || '*').split(',').map(p => p.trim()).filter(Boolean)
|
|
483
|
+
const limit = parseInt(req.query.limit) || 25
|
|
484
|
+
try {
|
|
485
|
+
res.json(findTwins(farms, patterns, restricted, slug, limit))
|
|
486
|
+
} catch (e) {
|
|
487
|
+
res.status(500).json({ error: `title-twins failed: ${e.message}` })
|
|
488
|
+
}
|
|
489
|
+
})
|
|
490
|
+
|
|
410
491
|
// ── GET /system/farm-search.json?q=…&pattern=…&limit=…&farms=… ────────────
|
|
411
492
|
// Explicit sites absent from farm disk are searched over HTTP via the
|
|
412
493
|
// galaxy cache; ?farms= asks peer farms to continue the search.
|
package/server/site-report.js
CHANGED
|
@@ -24,7 +24,7 @@ const path = require('node:path')
|
|
|
24
24
|
const crypto = require('node:crypto')
|
|
25
25
|
|
|
26
26
|
const { listDomains } = require('./farm-lib')
|
|
27
|
-
const { loadVectors, dot } = require('./search-report')
|
|
27
|
+
const { loadVectors, dot, seedVector } = require('./search-report')
|
|
28
28
|
|
|
29
29
|
const SITE_TOP_K = 5 // neighbourhood size
|
|
30
30
|
const SITE_TOPK_WEIGHT = 0.7
|
|
@@ -134,20 +134,28 @@ const siteReportPage = (query, sites, stats, limit, specs) => {
|
|
|
134
134
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
135
135
|
// Same context shape as buildReport: farms [[root, kind], ...], restricted
|
|
136
136
|
// Set, embed async text → number[], optional exclude Set.
|
|
137
|
+
// seedOpts: {vector, seed: {site, slug}, text, excludePage: {site, slug}} —
|
|
138
|
+
// same precedence as buildReport (see search-report.js seedVector). For a
|
|
139
|
+
// placement question about an EXISTING page, excludePage drops that page's
|
|
140
|
+
// own vector from its home site's scoring — otherwise the site it already
|
|
141
|
+
// sits on wins on the strength of the page itself.
|
|
137
142
|
|
|
138
143
|
const buildSiteReport = async (query, specs, limit,
|
|
139
144
|
{ farms, restricted, embed, exclude },
|
|
140
|
-
format = null) => {
|
|
145
|
+
format = null, seedOpts = {}) => {
|
|
141
146
|
const useSpecs = specs && specs.length ? specs : ['*']
|
|
142
147
|
const patterns = useSpecs
|
|
143
148
|
.map(s => ['PUBLIC', 'LOCAL', 'PRIVATE'].includes(s.toUpperCase()) ? s.toUpperCase() : s)
|
|
144
149
|
let domains = listDomains(farms, patterns, restricted, 'status/semantic-vectors.json')
|
|
145
150
|
if (exclude) domains = domains.filter(d => !exclude.has(d.domain))
|
|
146
|
-
const qvec = await
|
|
151
|
+
const qvec = await seedVector(seedOpts, query, farms, embed)
|
|
147
152
|
|
|
153
|
+
const ex = seedOpts.excludePage
|
|
148
154
|
let totalPages = 0
|
|
149
155
|
const entries = domains.map(({ farm, domain }) => {
|
|
150
|
-
|
|
156
|
+
let pages = loadVectors(farm, domain)
|
|
157
|
+
if (ex && ex.slug && domain === ex.site)
|
|
158
|
+
pages = pages.filter(p => p.slug !== ex.slug)
|
|
151
159
|
totalPages += pages.length
|
|
152
160
|
return { farm, domain, page_count: pages.length, pages }
|
|
153
161
|
})
|