wiki-plugin-similarity 0.21.1 → 0.22.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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/* wiki-plugin-similarity - 0.
|
|
1
|
+
/* wiki-plugin-similarity - 0.22.0 - Fri, 04 Sep 2026 21:53:05 GMT */
|
|
2
2
|
(()=>{var dt={high:.78,medium:.68,low:.58},ct=dt.medium,yt=10,ht=t=>{let e=[],n=[],s=[],r=null,l=null,u="search",g=!1,k=!1,x=!1,P=null,D=null,R=null,M=null,j=(o,m)=>o===m||o.startsWith(m)&&/^[\s:]/.test(o.slice(m.length)),L=(o,m)=>o.slice(m.length).replace(/^\s*:?\s*/,"").trim();for(let o of t.split(`
|
|
3
3
|
`)){let m=o.trim();if(!m||m.startsWith("#"))continue;let w=m.toUpperCase();if(j(w,"LIVE")){g=!0;continue}if(j(w,"SUBJECT")){k=!0;continue}if(j(w,"AUTHOR")){!e.length&&u==="search"&&(u="author");continue}if(j(w,"REPORT")){u==="search"&&(u="report");continue}if(j(w,"KEYWORD")){u==="search"&&(u="keyword");continue}if(j(w,"SITES")){u==="search"&&(u="sites");continue}if(j(w,"BUILD")){u==="search"&&(u="build");continue}if(j(w,"FORCE")){x=!0;continue}if(j(w,"GHOST")){P=L(m,"GHOST"),u==="search"&&(u="ghost");continue}if(j(w,"BUTTON")){D=L(m,"BUTTON");continue}if(j(w,"BATCH")){let E=L(m,"BATCH").toLowerCase();R=E==="off"||E==="no"||E==="0"?0:parseInt(E)||50;continue}if(j(w,"ALGORITHM")){M=L(m,"ALGORITHM").replace(/^\[\[|\]\]$/g,"").trim()||"search-algorithm",u==="search"&&!e.length&&(u="algorithm");continue}if(j(w,"ROSTER")){let E=L(m,"ROSTER");E&&n.push(E);continue}if(j(w,"FARM")){let E=L(m,"FARM");E&&s.push(E);continue}if(j(w,"LIST")){!e.length&&u==="search"&&(u="list");continue}if(j(w,"STATUS")){u==="search"&&(u="status");continue}if(j(w,"SIMILAR")){let E=L(w,"SIMILAR").toLowerCase();r=dt[E]||ct,!e.length&&u==="search"&&(u="similar");continue}if(j(w,"THRESHOLD")){let E=L(m,"THRESHOLD");r=dt[E.toLowerCase()]??(parseFloat(E)||ct);continue}if(j(w,"LIMIT")){l=parseInt(L(m,"LIMIT"))||yt;continue}e.push(["PUBLIC","LOCAL","PRIVATE","GALAXY","SITE"].includes(w)?w:m)}return{mode:u,specs:e,rosterRefs:n,farms:s,threshold:r??ct,limit:l??yt,live:g,subject:k,force:x,ghostUrl:P,label:D,batch:R,algorithm:M,thresholdSet:r!==null}},X=t=>t.includes("*")||t.includes("?"),Z=t=>t==="PUBLIC"||t==="LOCAL"||t==="PRIVATE"||t==="GALAXY";var xt=t=>t.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,""),ut=new Map,Qt=async(t,e)=>{if(ut.has(t))return ut.get(t);let n=`${e}/system/indexed-domains.json?pattern=${encodeURIComponent(t)}`,s=await fetch(n);if(!s.ok)throw new Error(`indexed-domains failed: ${s.status}`);let r=await s.json();return ut.set(t,r),r},K=async(t,e)=>{t.length||(t=[window.location.hostname]);let n=new Set,s=[];for(let r of t)if(r==="*"||X(r)||Z(r))for(let l of await Qt(r,e))n.has(l.domain)||(n.add(l.domain),s.push(l));else n.has(r)||(n.add(r),s.push({domain:r,page_count:null}));return s},te=/^([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+|localhost)(:\d+)?$/,ee=/^ROSTER ([A-Za-z0-9.\-:]+\/[a-z0-9-]+)$/,ne=/^REFERENCES ([A-Za-z0-9.\-:]+\/[a-z0-9-]+)$/,bt=async t=>{let e=t.indexOf("/"),n=await fetch(`//${t.slice(0,e)}/${t.slice(e+1)}.json`);if(!n.ok)throw new Error(`roster page ${t} failed: ${n.status}`);return n.json()},St=async(t,e,n)=>{if(n.has(t))return;n.add(t);let s;try{s=await bt(t)}catch(r){console.warn(r.message);return}for(let r of s.story||[])if(r.type==="roster")for(let l of(r.text||"").split(/\r?\n/)){let u=l.trim();if(!u)continue;let g=u.match(te);if(g){e.add(g[0]);continue}let k=u.match(ee);if(k){await St(k[1],e,n);continue}let x=u.match(ne);if(x)try{let P=await bt(x[1]);for(let D of P.story||[])D.type==="reference"&&D.site&&e.add(D.site)}catch(P){console.warn(P.message)}}},se=async t=>{let e=new Set,n=new Set;for(let s of t)await St(s,e,n);return[...e]},vt=async(t,e)=>{let n=typeof window<"u"&&window.location?window.location.hostname:null,s=t.map(r=>r.toUpperCase()==="SITE"&&n?n:r);return e.length?[...s,...await se(e)]:s};var mt=new Map,oe=t=>`${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(t)}`,kt=async t=>{if(mt.has(t))return mt.get(t);let e=await fetch(oe(t));if(!e.ok)return[];let n=await e.json();return mt.set(t,n),n},Q=async(t,e)=>{let n=await fetch(`${e}/system/embed.json`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text:t})});if(!n.ok)throw new Error(`embed failed: ${n.status}`);return(await n.json()).vector},Lt=async(t,e)=>{let s=(await kt(e)).find(r=>r.slug===t);return s?s.vector:null},tt=(t,e,{threshold:n,limit:s,excludeSlug:r,excludeDomain:l})=>{let u=[];for(let{domain:g,pages:k}of e)for(let{slug:x,title:P,vector:D}of k){if(x===r&&g===l)continue;let R=0;for(let M=0;M<t.length;M++)R+=t[M]*D[M];R>=n&&u.push({domain:g,slug:x,title:P,score:R})}return u.sort((g,k)=>k.score-g.score),u.slice(0,s)},et=async(t,e)=>{let n=await K(t,e);return(await Promise.all(n.map(async({domain:r})=>({domain:r,pages:await kt(r)})))).filter(r=>r.pages.length>0)};var Et=t=>{let e=t.parents(".page:first"),n=e.prev(".page"),s=n.length?n:e,r=(s.attr("id")||"").split("_rev")[0],l=s.data("site")||window.location.hostname,u=s.data("data")||{},g=u.title||s.find(".title").text().trim()||r,k=[g,...(u.story||[]).filter(x=>x.type==="markdown"||x.type==="paragraph").map(x=>(x.text||"").trim()).filter(Boolean)].join(`
|
|
4
|
-
`).slice(0,2e3);return{slug:r,site:l,title:g,text:k,isSelf:!n.length}},G=t=>`Subject: ${t.title}${t.isSelf?" (this page)":` @ ${t.site}`}`;var Ct=t=>`sim-cache-${t}`,jt=t=>{try{let e=JSON.parse(localStorage.getItem(Ct(t.id))||"null");return e?.text===(t.text||"")?e:null}catch{return null}},W=(t,e)=>{try{localStorage.setItem(Ct(t.id),JSON.stringify({text:t.text||"",ts:Date.now(),...e}))}catch{}},Tt=t=>{let e=Math.floor((Date.now()-t)/1e3);return e<60?`${e}s ago`:e<3600?`${Math.floor(e/60)}m ago`:e<86400?`${Math.floor(e/3600)}h ago`:`${Math.floor(e/86400)}d ago`};var ie=()=>{let t=new Set;try{for(let e of Object.keys(window.wiki?.neighborhood||{}))t.add(e);document.querySelectorAll(".page").forEach(e=>{let n=window.$?window.$(e).data("site"):null;t.add(n||window.location.hostname)})}catch{}return[...t]},It=async(t,e)=>{let n=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok){let s=new Error(`${t.split("/").pop()} failed: ${n.status}`);throw s.status=n.status,s.body=await n.json().catch(()=>({})),s}return n.json()},ae=(t,e,n)=>{let s=t.slice(0,n).map(l=>`${l.site} ${l.slug}`).join("|"),r=e.slice(0,n).map(l=>`${l.site} ${l.slug}`).join("|");return s===r},At=async t=>{let{origin:e,query:n,seeded:s={},specs:r,limit:l,batch:u,algorithm:g,render:k}=t,x=a=>t.status&&t.status(a),P={roster:[...t.roster||[],...g&&g.liked||[]],neighborhood:ie(),followed:!0,...t.prefer||{}};x("Ranking sites\u2026");let D=await It(`${e}/system/site-rank.json`,{query:n,...s,domains:r,prefer:P,algorithm:g||{},batch:u||50}),R=D.batches||[],M=new Set(R.flat().map(a=>a.toLowerCase())),j=(r||[]).filter(a=>typeof a=="string"&&/\./.test(a)&&!/[*?]/.test(a)&&!["PUBLIC","LOCAL","PRIVATE","GALAXY"].includes(a.toUpperCase())&&!M.has(a.toLowerCase()));j.length&&R.push(j);let o={searched:0,total:(D.count||0)+j.length||R.reduce((a,p)=>a+p.length,0),batches:R.length,done:0,stopped:!1,converged:!1,running:!0,unindexed:D.unindexed||[],answered:{local:0},siteIndex:D.siteIndex||null,siteInfo:new Map((D.sites||[]).map(a=>[a.domain,a]))},m=new Map,w=[],E=0,J=()=>{o.stopped=!0};o.stop=J;let V=async a=>{let p={query:n,...s,domains:a,limit:Math.max(l*3,30),flat:!0};t.thresholdSet&&(p.threshold=t.threshold);let c=await It(`${e}/system/search-report.json`,p);for(let d of c.results||[]){let y=`${d.site} ${d.slug}`,v=m.get(y);(!v||v.score<d.score)&&m.set(y,d)}let f=0;for(let d of c.peers||[])o.answered[d.host]=(o.answered[d.host]||0)+(d.domains||0),f+=d.domains||0;o.answered.local+=Math.max(0,(c.stats&&c.stats.domains||0)-f);let h=[...m.values()].sort((d,y)=>y.score-d.score),i=ae(w,h,l);w=h,o.searched+=a.length,o.done+=1,E=i?E+1:0},lt=()=>R.slice(o.done),Y=async(a=!1)=>{for(o.running=!0,o.stopped=!1;o.done<R.length&&!o.stopped;)if(await V(R[o.done]),k(w,o),!a&&E>=3&&o.done<R.length){o.converged=!0;break}return o.running=!1,k(w,o),w};return o.continueAll=()=>(o.converged=!1,E=-1/0,Y(!0)),await Y(!1),{results:w,state:o,remaining:lt}};var st="similarity:learned",re=new Set(["liked","visited","neighbourhood","neighborhood","followed","centroid","fresh","reliable"]),nt=/^([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+|localhost)(:\d+)?$/,ft=t=>{let e={weights:{},always:[],never:[],batch:null,trust:null};for(let n of(t||"").split(`
|
|
4
|
+
`).slice(0,2e3);return{slug:r,site:l,title:g,text:k,isSelf:!n.length}},G=t=>`Subject: ${t.title}${t.isSelf?" (this page)":` @ ${t.site}`}`;var Ct=t=>`sim-cache-${t}`,jt=t=>{try{let e=JSON.parse(localStorage.getItem(Ct(t.id))||"null");return e?.text===(t.text||"")?e:null}catch{return null}},W=(t,e)=>{try{localStorage.setItem(Ct(t.id),JSON.stringify({text:t.text||"",ts:Date.now(),...e}))}catch{}},Tt=t=>{let e=Math.floor((Date.now()-t)/1e3);return e<60?`${e}s ago`:e<3600?`${Math.floor(e/60)}m ago`:e<86400?`${Math.floor(e/3600)}h ago`:`${Math.floor(e/86400)}d ago`};var ie=()=>{let t=new Set;try{for(let e of Object.keys(window.wiki?.neighborhood||{}))t.add(e);document.querySelectorAll(".page").forEach(e=>{let n=window.$?window.$(e).data("site"):null;t.add(n||window.location.hostname)})}catch{}return[...t]},It=async(t,e)=>{let n=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok){let s=new Error(`${t.split("/").pop()} failed: ${n.status}`);throw s.status=n.status,s.body=await n.json().catch(()=>({})),s}return n.json()},ae=(t,e,n)=>{let s=t.slice(0,n).map(l=>`${l.site} ${l.slug}`).join("|"),r=e.slice(0,n).map(l=>`${l.site} ${l.slug}`).join("|");return s===r},At=async t=>{let{origin:e,query:n,seeded:s={},specs:r,limit:l,batch:u,algorithm:g,render:k}=t,x=a=>t.status&&t.status(a),P={roster:[...t.roster||[],...g&&g.liked||[]],neighborhood:ie(),followed:!0,...t.prefer||{}};x("Ranking sites\u2026");let D=await It(`${e}/system/site-rank.json`,{query:n,...s,domains:r,prefer:P,algorithm:g||{},batch:u||50}),R=D.batches||[],M=new Set(R.flat().map(a=>a.toLowerCase())),j=(r||[]).filter(a=>typeof a=="string"&&/\./.test(a)&&!/[*?]/.test(a)&&!["PUBLIC","LOCAL","PRIVATE","GALAXY"].includes(a.toUpperCase())&&!M.has(a.toLowerCase()));j.length&&R.push(j);let o={searched:0,total:(D.count||0)+j.length||R.reduce((a,p)=>a+p.length,0),batches:R.length,done:0,stopped:!1,converged:!1,running:!0,unindexed:D.unindexed||[],answered:{local:0},siteIndex:D.siteIndex||null,siteInfo:new Map((D.sites||[]).map(a=>[a.domain,a]))},m=new Map,w=[],E=0,J=()=>{o.stopped=!0};o.stop=J;let V=async a=>{let p={query:n,...s,domains:a,limit:Math.max(l*3,30),flat:!0};t.thresholdSet&&(p.threshold=t.threshold);let c=await It(`${e}/system/search-report.json`,p);for(let d of c.results||[]){let y=`${d.site} ${d.slug}`,v=m.get(y);(!v||v.score<d.score)&&m.set(y,d)}let f=0;for(let d of c.peers||[])o.answered[d.host]=(o.answered[d.host]||0)+(d.domains||0),f+=d.domains||0;o.answered.local+=Math.max(0,(c.stats&&c.stats.domains||0)-f);let h=[...m.values()].sort((d,y)=>y.score-d.score),i=ae(w,h,l);w=h,o.searched+=a.length,o.done+=1,E=i?E+1:0},lt=()=>R.slice(o.done),Y=async(a=!1)=>{for(o.running=!0,o.stopped=!1;o.done<R.length&&!o.stopped;)if(await V(R[o.done]),k(w,o),!a&&E>=3&&o.done<R.length){o.converged=!0;break}return o.running=!1,k(w,o),w};return o.continueAll=()=>(o.converged=!1,E=-1/0,Y(!0)),await Y(!1),{results:w,state:o,remaining:lt}};var st="similarity:learned",re=new Set(["liked","visited","neighbourhood","neighborhood","followed","centroid","fresh","reliable","stale"]),nt=/^([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+|localhost)(:\d+)?$/,ft=t=>{let e={weights:{},always:[],never:[],batch:null,trust:null};for(let n of(t||"").split(`
|
|
5
5
|
`)){let s=n.trim();if(!s||s.startsWith("#"))continue;let[r,...l]=s.split(/\s+/),u=r.toUpperCase().replace(/:$/,"");if(u==="WEIGHT"&&l.length>=2){let g=l[0].toLowerCase().replace("neighborhood","neighbourhood"),k=parseFloat(l[1]);re.has(g)&&Number.isFinite(k)&&(e.weights[g]=k)}else u==="ALWAYS"&&l[0]&&nt.test(l[0])?e.always.push(l[0].toLowerCase()):u==="NEVER"&&l[0]&&nt.test(l[0])?e.never.push(l[0].toLowerCase()):u==="BATCH"&&l[0]?e.batch=parseInt(l[0])||null:u==="TRUST"&&l[0]&&["solid","flaky","any"].includes(l[0].toLowerCase())&&(e.trust=l[0].toLowerCase())}return e},pt=()=>{try{let t=JSON.parse(localStorage.getItem(st)||"{}"),e={};for(let[n,s]of Object.entries(t.sites||t)){let r=typeof s=="number"?s:s&&typeof s.score=="number"?s.score:0;r>0&&(e[n.toLowerCase()]=Math.min(1,r))}return e}catch{return{}}},le=(t,e)=>{if(!t)return`${e}/search-algorithm.json`;let n=t.indexOf("/");if(n>0&&nt.test(t.slice(0,n)))return`//${t.slice(0,n)}/${t.slice(n+1)}.json`;let s=t.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,"");return`${e}/${s}.json`},ce=t=>{let e=new Set;for(let n of t.story||[])if(n.type==="roster")for(let s of(n.text||"").split(/\r?\n/)){let r=s.trim();nt.test(r)&&e.add(r.toLowerCase())}return[...e]},Rt=new Map,Ot=async(t,e)=>{let n=le(t,e),s=Rt.get(n);if(!s){s={weights:{},always:[],never:[],liked:[],batch:null,trust:null,page:null};try{let r=await fetch(n);if(r.ok){let l=await r.json(),u=(l.story||[]).find(g=>g.type==="similarity"&&/^\s*ALGORITHM\b/im.test(g.text||""));u&&Object.assign(s,ft(u.text)),s.liked=ce(l),s.page={url:n,title:l.title}}}catch{}Rt.set(n,s)}return{...s,learned:pt()}};var de={neighbourhood:.1,visited:.2,clicked:.35},he=7*864e5,Dt=200,Ut=()=>{try{return JSON.parse(localStorage.getItem(st)||"{}").sites||{}}catch{return{}}},Ht=t=>{try{localStorage.setItem(st,JSON.stringify({sites:t,savedAt:Date.now()}))}catch{}},ot=(t,e)=>{let n=Math.max(0,(e-(t.at||e))/he);return(t.score||0)*Math.pow(.5,n)},it=(t,e)=>{if(!t||t==="localhost")return 0;let n=t.toLowerCase(),s=Date.now(),r=Ut(),l=r[n]?ot(r[n],s):0,u=Math.min(1,l+(de[e]||.1));r[n]={score:u,at:s};let g=Object.keys(r);if(g.length>Dt){g.sort((k,x)=>ot(r[x],s)-ot(r[k],s));for(let k of g.slice(Dt))delete r[k]}return Ht(r),u},ue=()=>{let t=Date.now();return Object.entries(Ut()).map(([e,n])=>[e,ot(n,t)]).filter(([,e])=>e>.01).sort((e,n)=>n[1]-e[1])},qt=()=>Ht({}),Mt=!1,Pt=t=>{try{return window.$&&window.$(t).data("site")||window.location.hostname}catch{return window.location.hostname}},Bt=()=>{if(Mt||typeof document>"u")return;Mt=!0;let t=new Set;document.querySelectorAll(".main > .page").forEach(n=>{let s=Pt(n);t.has(s)||(t.add(s),it(s,"neighbourhood"))});let e=document.querySelector(".main");!e||typeof MutationObserver>"u"||new MutationObserver(n=>{for(let s of n)for(let r of s.addedNodes)r.nodeType===1&&r.classList.contains("page")&&!r.classList.contains("ghost")&&it(Pt(r),"visited")}).observe(e,{childList:!0})},Ft=(t,e=20)=>new Promise((n,s)=>{let l=((t.data("data")||{}).story||[]).find(x=>x.type==="roster"&&/^\s*sites i visit\b/i.test(x.text||""));if(!l)return s(new Error('no "Sites I visit" roster on this page'));let u=ue().slice(0,e);if(!u.length)return s(new Error("nothing learned yet"));let g=["Sites I visit",...u.map(([x])=>x)].join(`
|
|
6
6
|
`),k={...l,text:g};try{window.wiki.pageHandler.put(t,{type:"edit",id:l.id,item:k});let x=t.find(`.item[data-id="${l.id}"]`);x.length&&(x.data("item",k),window.wiki.plugins?.roster?.emit?.(x,k)),n(u.length)}catch(x){s(x)}});var N=`
|
|
7
7
|
.sim-form { display:flex; gap:6px; margin-bottom:8px; }
|
package/client/similarity.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/client/dsl.js", "../src/client/scope.js", "../src/client/vectors.js", "../src/client/subject.js", "../src/client/cache.js", "../src/client/batch.js", "../src/client/algorithm.js", "../src/client/learn.js", "../src/client/styles.js", "../src/client/searchdoor.js", "../src/client/similarity.js"],
|
|
4
|
-
"sourcesContent": ["// wiki-plugin-similarity \u2014 the item's line-oriented DSL: parse it, and classify a spec\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\n// \u2500\u2500 DSL Parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst SIMILAR_THRESHOLDS = { high: 0.78, medium: 0.68, low: 0.58 }\nconst DEFAULT_THRESHOLD = SIMILAR_THRESHOLDS.medium\nconst DEFAULT_LIMIT = 10\n\n// parseDSL returns { mode, specs, threshold, limit, ... }\n//\n// mode: 'algorithm' if ALGORITHM is the first meaningful line \u2014 the item IS\n// the reader's search algorithm (weights and rules, see algorithm.js);\n// elsewhere in an item, ALGORITHM site/slug names the page to read one from;\n// 'status' if STATUS is present \u2014 the state of the index behind the answers;\n// '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 let batch = null // BATCH n: search sites n at a time, widening (0 = off)\n let algorithm = null // ALGORITHM site/slug or [[Page]]: the reader's Search Algorithm page\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, 'BATCH')) {\n const b = val(line, 'BATCH').toLowerCase()\n batch = (b === 'off' || b === 'no' || b === '0') ? 0 : (parseInt(b) || 50)\n continue\n }\n if (isCmd(upper, 'ALGORITHM')) {\n const ref = val(line, 'ALGORITHM').replace(/^\\[\\[|\\]\\]$/g, '').trim()\n algorithm = ref || 'search-algorithm'\n if (mode === 'search' && !specs.length) mode = 'algorithm'\n continue\n }\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, 'STATUS')) {\n if (mode === 'search') mode = 'status'\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', 'SITE'].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 batch,\n algorithm,\n thresholdSet: threshold !== null, // explicit THRESHOLD/SIMILAR in the DSL\n }\n}\n\nconst isGlob = spec => spec.includes('*') || spec.includes('?')\n\n// Scope keywords expand server-side: PUBLIC (Nextcloud mirror farms),\n// LOCAL (primary farm), PRIVATE (public domains with restricted: true),\n// GALAXY (off-farm federation sites indexed into the galaxy tree)\nconst isScope = spec => spec === 'PUBLIC' || spec === 'LOCAL' || spec === 'PRIVATE' ||\n spec === 'GALAXY'\n\nexport { parseDSL, isGlob, isScope, SIMILAR_THRESHOLDS, DEFAULT_THRESHOLD, DEFAULT_LIMIT }\n", "// wiki-plugin-similarity \u2014 turning DSL specs into a concrete list of domains\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\nimport { isGlob, isScope } from './dsl.js'\n\n// \u2500\u2500 Slug \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst slugify = title => title.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '')\n\n// \u2500\u2500 Domain resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst domainCache = new Map()\n\nconst resolveDomainsForSpec = async (spec, origin) => {\n if (domainCache.has(spec)) return domainCache.get(spec)\n const url = `${origin}/system/indexed-domains.json?pattern=${encodeURIComponent(spec)}`\n const res = await fetch(url)\n if (!res.ok) throw new Error(`indexed-domains failed: ${res.status}`)\n const list = await res.json()\n domainCache.set(spec, list)\n return list\n}\n\nconst resolveDomains = async (specs, origin) => {\n if (!specs.length) specs = [window.location.hostname]\n const seen = new Set()\n const result = []\n for (const spec of specs) {\n if (spec === '*' || isGlob(spec) || isScope(spec)) {\n for (const item of await resolveDomainsForSpec(spec, origin)) {\n if (!seen.has(item.domain)) { seen.add(item.domain); result.push(item) }\n }\n } else if (!seen.has(spec)) {\n seen.add(spec); result.push({ domain: spec, page_count: null })\n }\n }\n return result\n}\n\n// \u2500\u2500 Roster resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// ROSTER site/slug resolves a roster page into domain names, following the\n// roster plugin's own line semantics: bare domains are sites; category and\n// blank lines are skipped; nested `ROSTER site/slug` includes another roster\n// page; `REFERENCES site/slug` collects the .site of a page's reference items.\n// Includes are followed transitively with a visited-set loop guard.\n\nconst SITE_LINE = /^([a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)+|localhost)(:\\d+)?$/\nconst ROSTER_LINE = /^ROSTER ([A-Za-z0-9.\\-:]+\\/[a-z0-9-]+)$/\nconst REFS_LINE = /^REFERENCES ([A-Za-z0-9.\\-:]+\\/[a-z0-9-]+)$/\n\nconst fetchPage = async ref => { // ref = site/slug\n const i = ref.indexOf('/')\n const res = await fetch(`//${ref.slice(0, i)}/${ref.slice(i + 1)}.json`)\n if (!res.ok) throw new Error(`roster page ${ref} failed: ${res.status}`)\n return res.json()\n}\n\nconst resolveRoster = async (ref, out, visited) => {\n if (visited.has(ref)) return\n visited.add(ref)\n let page\n try { page = await fetchPage(ref) } catch (e) { console.warn(e.message); return }\n for (const item of page.story || []) {\n if (item.type !== 'roster') continue\n for (const raw of (item.text || '').split(/\\r?\\n/)) {\n const line = raw.trim()\n if (!line) continue\n const site = line.match(SITE_LINE)\n if (site) { out.add(site[0]); continue }\n const nested = line.match(ROSTER_LINE)\n if (nested) { await resolveRoster(nested[1], out, visited); continue }\n const refs = line.match(REFS_LINE)\n if (refs) {\n try {\n const refPage = await fetchPage(refs[1])\n for (const it of refPage.story || []) {\n if (it.type === 'reference' && it.site) out.add(it.site)\n }\n } catch (e) { console.warn(e.message) }\n }\n // anything else is a category name \u2014 skipped\n }\n }\n}\n\nconst resolveRosters = async rosterRefs => {\n const out = new Set()\n const visited = new Set()\n for (const ref of rosterRefs) await resolveRoster(ref, out, visited)\n return [...out]\n}\n\n// Effective specs: DSL specs plus any domains contributed by ROSTER lines.\n//\n// SITE expands to the domain the page is being served from. A page shipped to\n// every site in the farm \u2014 a plugin's utility page \u2014 cannot name its own home,\n// and leaving the scope empty means the whole farm in the report modes. SITE\n// is how such a page says \"search here\" and means it wherever it lands.\nconst effectiveSpecs = async (specs, rosterRefs) => {\n const here = (typeof window !== 'undefined' && window.location)\n ? window.location.hostname : null\n const expanded = specs.map(s => (s.toUpperCase() === 'SITE' && here ? here : s))\n return rosterRefs.length ? [...expanded, ...await resolveRosters(rosterRefs)] : expanded\n}\n\nexport { slugify, resolveDomains, resolveRosters, effectiveSpecs, SITE_LINE, ROSTER_LINE, REFS_LINE }\n", "// wiki-plugin-similarity \u2014 page vectors, embedding, and the cosine scan over them\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\nimport { resolveDomains } from './scope.js'\n\n// \u2500\u2500 Vector loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst vectorCache = new Map()\n\n// Route vector requests through the current wiki's plugin server. This keeps\n// public HTTPS pages same-origin, avoids CORS failures, and lets the server read\n// indices from its configured farm roots.\nconst vectorUrl = domain =>\n `${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(domain)}`\n\nconst loadVectors = async domain => {\n if (vectorCache.has(domain)) return vectorCache.get(domain)\n const res = await fetch(vectorUrl(domain))\n if (!res.ok) return []\n const data = await res.json()\n vectorCache.set(domain, data)\n return data\n}\n\n// \u2500\u2500 Embedding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// POST, never GET: page-length text in a query string overflows Node's header\n// limit and dies with 431 before any handler runs (server \u2265 0.11.0).\nconst getEmbedding = async (text, origin) => {\n const res = await fetch(`${origin}/system/embed.json`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ text }),\n })\n if (!res.ok) throw new Error(`embed failed: ${res.status}`)\n return (await res.json()).vector\n}\n\n// Look up an existing page vector from the current domain's cached index.\n// Returns the float[] vector if found, null otherwise.\nconst lookupPageVector = async (slug, domain) => {\n const pages = await loadVectors(domain)\n const entry = pages.find(p => p.slug === slug)\n return entry ? entry.vector : null\n}\n// \u2500\u2500 Cosine search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst cosineScan = (queryVec, domainEntries, { threshold, limit, excludeSlug, excludeDomain }) => {\n const results = []\n for (const { domain, pages } of domainEntries) {\n for (const { slug, title, vector } of pages) {\n if (slug === excludeSlug && domain === excludeDomain) continue\n let dot = 0\n for (let i = 0; i < queryVec.length; i++) dot += queryVec[i] * vector[i]\n if (dot >= threshold) results.push({ domain, slug, title, score: dot })\n }\n }\n results.sort((a, b) => b.score - a.score)\n return results.slice(0, limit)\n}\n\n// \u2500\u2500 Shared domain loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst loadDomainEntries = async (specs, origin) => {\n const domains = await resolveDomains(specs, origin)\n const entries = await Promise.all(\n domains.map(async ({ domain }) => ({ domain, pages: await loadVectors(domain) }))\n )\n return entries.filter(e => e.pages.length > 0)\n}\n\nexport { loadVectors, vectorUrl, getEmbedding, lookupPageVector, cosineScan, loadDomainEntries }\n", "// wiki-plugin-similarity \u2014 SUBJECT \u2014 the page an item acts on\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\n// \u2500\u2500 Subject resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// SUBJECT makes an item act on the page BEFORE its own in the lineup \u2014 the\n// host a tool page was opened beside \u2014 falling back to the containing page so\n// a tool page opened alone still works (and announces the fallback).\n// The page div's id IS the slug (possibly _rev-suffixed on a historical view);\n// data('data') carries the full raw page JSON; data('site') is set only on\n// remote pages. `text` is title + opening prose, capped: the BGE-small\n// embedder truncates around 512 tokens, so more would be discarded anyway.\n\nconst resolveSubject = div => {\n const $self = div.parents('.page:first')\n const $host = $self.prev('.page')\n const $page = $host.length ? $host : $self\n const slug = ($page.attr('id') || '').split('_rev')[0]\n const site = $page.data('site') || window.location.hostname\n const data = $page.data('data') || {}\n const title = data.title || $page.find('.title').text().trim() || slug\n const text = [title, ...(data.story || [])\n .filter(i => i.type === 'markdown' || i.type === 'paragraph')\n .map(i => (i.text || '').trim()).filter(Boolean)].join('\\n').slice(0, 2000)\n return { slug, site, title, text, isSelf: !$host.length }\n}\n\nconst subjectNote = s =>\n `Subject: ${s.title}${s.isSelf ? ' (this page)' : ` @ ${s.site}`}`\n\nexport { resolveSubject, subjectNote }\n", "// wiki-plugin-similarity \u2014 per-item result cache in localStorage\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\n// \u2500\u2500 Result cache (localStorage) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Cache is keyed by item id. Invalidated when item.text changes (DSL edited).\n// LIVE mode bypasses the cache entirely.\n\nconst cacheKey = id => `sim-cache-${id}`\n\nconst readCache = item => {\n try {\n const c = JSON.parse(localStorage.getItem(cacheKey(item.id)) || 'null')\n return c?.text === (item.text || '') ? c : null\n } catch { return null }\n}\n\nconst writeCache = (item, data) => {\n try {\n localStorage.setItem(cacheKey(item.id), JSON.stringify({\n text: item.text || '',\n ts: Date.now(),\n ...data,\n }))\n } catch { /* storage unavailable or full */ }\n}\n\nconst cacheAge = ts => {\n const s = Math.floor((Date.now() - ts) / 1000)\n if (s < 60) return `${s}s ago`\n if (s < 3600) return `${Math.floor(s / 60)}m ago`\n if (s < 86400) return `${Math.floor(s / 3600)}h ago`\n return `${Math.floor(s / 86400)}d ago`\n}\n\nexport { readCache, writeCache, cacheAge }\n", "// wiki-plugin-similarity \u2014 incremental federated search, client side\n//\n// Rank the sites first, then search them a batch at a time, merging results\n// in place until the answer stops changing (design: search.fedwiki.club\n// /incremental-federated-search).\n//\n// 1. POST site-rank.json \u2014 every site the farm knows, ordered for this\n// query: preferred sites (roster, neighbourhood, followed, the reader's\n// algorithm) first, then by centroid similarity. Returns the batches.\n// 2. POST search-report.json {flat: true, domains: batch} per batch \u2014 the\n// ordinary report pipeline over an explicit domain list; scores are\n// comparable across batches because every site shares one model.\n// 3. Merge by score, render in the item as each batch lands, stop when\n// QUIET consecutive batches change nothing in the top `limit`, and\n// offer the rest behind a button.\n\nconst QUIET = 3\n\nconst neighbourhoodDomains = () => {\n const out = new Set()\n try {\n for (const site of Object.keys(window.wiki?.neighborhood || {})) out.add(site)\n document.querySelectorAll('.page').forEach(p => {\n const site = window.$ ? window.$(p).data('site') : null\n out.add(site || window.location.hostname)\n })\n } catch { /* no lineup to read */ }\n return [...out]\n}\n\nconst post = async (url, body) => {\n const res = await fetch(url, {\n method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),\n })\n if (!res.ok) {\n const err = new Error(`${url.split('/').pop()} failed: ${res.status}`)\n err.status = res.status\n err.body = await res.json().catch(() => ({}))\n throw err\n }\n return res.json()\n}\n\nconst sameTop = (a, b, limit) => {\n const ka = a.slice(0, limit).map(r => `${r.site} ${r.slug}`).join('|')\n const kb = b.slice(0, limit).map(r => `${r.site} ${r.slug}`).join('|')\n return ka === kb\n}\n\n// opts: {origin, query, seeded, specs, limit, threshold, thresholdSet, batch,\n// roster, algorithm, prefer, render(results, state), status(text)}\n// Returns the final merged results; runs until done, stopped, or converged.\nconst runBatched = async opts => {\n const { origin, query, seeded = {}, specs, limit, batch, algorithm, render } = opts\n const say = t => opts.status && opts.status(t)\n const prefer = {\n roster: [...(opts.roster || []), ...((algorithm && algorithm.liked) || [])],\n neighborhood: neighbourhoodDomains(),\n followed: true,\n ...(opts.prefer || {}),\n }\n say('Ranking sites\u2026')\n const rank = await post(`${origin}/system/site-rank.json`, {\n query, ...seeded, domains: specs, prefer, algorithm: algorithm || {}, batch: batch || 50,\n })\n const batches = rank.batches || []\n // A domain named explicitly is searched whether or not this farm's Site\n // Index knows it: the cascade finds the peer that holds it. Unranked\n // explicit domains go last, as one more batch.\n const ranked = new Set(batches.flat().map(d => d.toLowerCase()))\n const explicit = (specs || []).filter(sp => typeof sp === 'string' && /\\./.test(sp) &&\n !/[*?]/.test(sp) && !['PUBLIC', 'LOCAL', 'PRIVATE', 'GALAXY'].includes(sp.toUpperCase()) &&\n !ranked.has(sp.toLowerCase()))\n if (explicit.length) batches.push(explicit)\n const total = (rank.count || 0) + explicit.length || batches.reduce((n, b) => n + b.length, 0)\n const state = { searched: 0, total, batches: batches.length, done: 0, stopped: false,\n converged: false, running: true, unindexed: rank.unindexed || [],\n // who answered: this farm and each peer, by sites searched\n answered: { local: 0 }, siteIndex: rank.siteIndex || null,\n // what the Site Index says of each site: when it was indexed, by whom\n siteInfo: new Map((rank.sites || []).map(r => [r.domain, r])) }\n\n const merged = new Map()\n let results = []\n let quiet = 0\n const stop = () => { state.stopped = true }\n state.stop = stop\n\n const runBatch = async domains => {\n const body = { query, ...seeded, domains, limit: Math.max(limit * 3, 30), flat: true }\n if (opts.thresholdSet) body.threshold = opts.threshold\n const flat = await post(`${origin}/system/search-report.json`, body)\n for (const r of flat.results || []) {\n const key = `${r.site} ${r.slug}`\n const prev = merged.get(key)\n if (!prev || prev.score < r.score) merged.set(key, r)\n }\n let peerDomains = 0\n for (const p of flat.peers || []) {\n state.answered[p.host] = (state.answered[p.host] || 0) + (p.domains || 0)\n peerDomains += p.domains || 0\n }\n state.answered.local += Math.max(0, ((flat.stats && flat.stats.domains) || 0) - peerDomains)\n const next = [...merged.values()].sort((a, b) => b.score - a.score)\n const unchanged = sameTop(results, next, limit)\n results = next\n state.searched += domains.length\n state.done += 1\n quiet = unchanged ? quiet + 1 : 0\n }\n\n const remaining = () => batches.slice(state.done)\n const loop = async (all = false) => {\n state.running = true\n state.stopped = false\n while (state.done < batches.length && !state.stopped) {\n await runBatch(batches[state.done])\n render(results, state)\n if (!all && quiet >= QUIET && state.done < batches.length) {\n state.converged = true\n break\n }\n }\n state.running = false\n render(results, state)\n return results\n }\n state.continueAll = () => { state.converged = false; quiet = -Infinity; return loop(true) }\n\n await loop(false)\n return { results, state, remaining }\n}\n\nexport { runBatched, neighbourhoodDomains, QUIET }\n", "// wiki-plugin-similarity \u2014 the reader's own search algorithm, as a page\n//\n// The ranking a federated search uses is a wiki page the reader owns (design:\n// search.fedwiki.club/incremental-federated-search, layer three). This module\n// reads it: the ALGORITHM item's weights and rules, the page's roster items\n// (sites liked, sites visited), and what this browser has learned so far\n// (learn.js writes localStorage['similarity:learned']). What comes back is the\n// `algorithm` object site-rank.json applies on top of the Site Index ranking.\n//\n// ALGORITHM\n// WEIGHT liked 1.0 how much a roster site is lifted\n// WEIGHT visited 0.6 \u2026 a site this browser learned\n// WEIGHT neighbourhood 0.4 \u2026 a site already in the lineup\n// WEIGHT followed 0.2 \u2026 a registry followed-tier site\n// WEIGHT centroid 1.0 \u2026 similarity of the site to the query\n// WEIGHT fresh 0.2 \u2026 freshness of the index that holds the site\n// ALWAYS ward.bay.wiki.org pinned into the first batch\n// NEVER spam.example pruned from every batch\n// BATCH 50 sites searched per request\n// WEIGHT reliable 0.3 how far an unreliable site sinks (Dead Sites Plan)\n// TRUST solid|flaky|any solid: skip shaky sites; flaky: search them later\n// (default); any: even dead and moved sites\n//\n// Unknown words are ignored, so the vocabulary can grow release by release.\n\nconst LEARNED_KEY = 'similarity:learned'\nconst WEIGHT_NAMES = new Set(['liked', 'visited', 'neighbourhood', 'neighborhood', 'followed', 'centroid', 'fresh', 'reliable'])\nconst SITE = /^([a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)+|localhost)(:\\d+)?$/\n\nconst parseAlgorithm = text => {\n const out = { weights: {}, always: [], never: [], batch: null, trust: null }\n for (const raw of (text || '').split('\\n')) {\n const line = raw.trim()\n if (!line || line.startsWith('#')) continue\n const [word, ...rest] = line.split(/\\s+/)\n const kw = word.toUpperCase().replace(/:$/, '')\n if (kw === 'WEIGHT' && rest.length >= 2) {\n const name = rest[0].toLowerCase().replace('neighborhood', 'neighbourhood')\n const v = parseFloat(rest[1])\n if (WEIGHT_NAMES.has(name) && Number.isFinite(v)) out.weights[name] = v\n } else if (kw === 'ALWAYS' && rest[0] && SITE.test(rest[0])) out.always.push(rest[0].toLowerCase())\n else if (kw === 'NEVER' && rest[0] && SITE.test(rest[0])) out.never.push(rest[0].toLowerCase())\n else if (kw === 'BATCH' && rest[0]) out.batch = parseInt(rest[0]) || null\n else if (kw === 'TRUST' && rest[0] && ['solid', 'flaky', 'any'].includes(rest[0].toLowerCase())) out.trust = rest[0].toLowerCase()\n }\n return out\n}\n\n// What this browser has learned: {domain: score 0..1}, decayed by learn.js.\nconst learnedSignals = () => {\n try {\n const raw = JSON.parse(localStorage.getItem(LEARNED_KEY) || '{}')\n const out = {}\n for (const [d, v] of Object.entries(raw.sites || raw)) {\n const score = typeof v === 'number' ? v : (v && typeof v.score === 'number' ? v.score : 0)\n if (score > 0) out[d.toLowerCase()] = Math.min(1, score)\n }\n return out\n } catch { return {} }\n}\n\n// ref: 'site/slug' | 'Page Title' | 'page-slug' | null (\u2192 this site's search-algorithm)\nconst refToUrl = (ref, origin) => {\n if (!ref) return `${origin}/search-algorithm.json`\n const i = ref.indexOf('/')\n if (i > 0 && SITE.test(ref.slice(0, i))) return `//${ref.slice(0, i)}/${ref.slice(i + 1)}.json`\n const slug = ref.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '')\n return `${origin}/${slug}.json`\n}\n\n// Roster items on the page: every bare site line, all of them \"liked\".\nconst rosterSites = page => {\n const out = new Set()\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 (SITE.test(line)) out.add(line.toLowerCase())\n }\n }\n return [...out]\n}\n\nconst cache = new Map()\nconst loadAlgorithm = async (ref, origin) => {\n const url = refToUrl(ref, origin)\n let algo = cache.get(url)\n if (!algo) {\n algo = { weights: {}, always: [], never: [], liked: [], batch: null, trust: null, page: null }\n try {\n const res = await fetch(url)\n if (res.ok) {\n const page = await res.json()\n const item = (page.story || []).find(i => i.type === 'similarity' &&\n /^\\s*ALGORITHM\\b/im.test(i.text || ''))\n if (item) Object.assign(algo, parseAlgorithm(item.text))\n algo.liked = rosterSites(page)\n algo.page = { url, title: page.title }\n }\n } catch { /* no algorithm page: defaults apply */ }\n cache.set(url, algo)\n }\n return { ...algo, learned: learnedSignals() }\n}\n\nexport { parseAlgorithm, learnedSignals, loadAlgorithm, rosterSites, refToUrl, LEARNED_KEY }\n", "// wiki-plugin-similarity \u2014 the learning loop, in the browser\n//\n// A site earns credit when it enters your neighbourhood, when you open a page\n// from it, and when you click one of its search results. Scores live in\n// localStorage['similarity:learned'] as {sites: {domain: {score, at}}} and\n// decay by half every week, so the list follows where you read now. Nothing\n// leaves the browser until the reader presses Remember on their Search\n// Algorithm page, which writes the top sites into its \"Sites I visit\" roster\n// through an ordinary page save \u2014 inspectable, forkable, deletable line by\n// line. (Design: search.fedwiki.club/incremental-federated-search, layer three.)\n\nimport { LEARNED_KEY } from './algorithm.js'\n\nconst CREDIT = { neighbourhood: 0.1, visited: 0.2, clicked: 0.35 }\nconst HALF_LIFE_MS = 7 * 86_400_000\nconst MAX_SITES = 200\n\nconst load = () => {\n try { return JSON.parse(localStorage.getItem(LEARNED_KEY) || '{}').sites || {} } catch { return {} }\n}\nconst save = sites => {\n try { localStorage.setItem(LEARNED_KEY, JSON.stringify({ sites, savedAt: Date.now() })) } catch { /* private mode */ }\n}\nconst decayed = (entry, now) => {\n const weeks = Math.max(0, (now - (entry.at || now)) / HALF_LIFE_MS)\n return (entry.score || 0) * Math.pow(0.5, weeks)\n}\n\n// Credit one site for one kind of signal; returns the new score.\nconst credit = (domain, kind) => {\n if (!domain || domain === 'localhost') return 0\n const d = domain.toLowerCase()\n const now = Date.now()\n const sites = load()\n const cur = sites[d] ? decayed(sites[d], now) : 0\n const score = Math.min(1, cur + (CREDIT[kind] || 0.1))\n sites[d] = { score, at: now }\n // keep the list bounded: drop the faintest\n const keys = Object.keys(sites)\n if (keys.length > MAX_SITES) {\n keys.sort((a, b) => decayed(sites[b], now) - decayed(sites[a], now))\n for (const k of keys.slice(MAX_SITES)) delete sites[k]\n }\n save(sites)\n return score\n}\n\n// Current scores, decayed, best first: [[domain, score], ...]\nconst learned = () => {\n const now = Date.now()\n return Object.entries(load())\n .map(([d, e]) => [d, decayed(e, now)])\n .filter(([, s]) => s > 0.01)\n .sort((a, b) => b[1] - a[1])\n}\n\nconst forget = () => save({})\n\n// \u2500\u2500 Ambient signals \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Installed once per page load by the first similarity item that binds:\n// every site already in the lineup gets neighbourhood credit, and every page\n// that later joins the lineup credits its site as visited. Wiki adds pages\n// as .page divs under .main, so a MutationObserver is the hook.\n\nlet installed = false\nconst siteOf = el => {\n try { return (window.$ && window.$(el).data('site')) || window.location.hostname } catch { return window.location.hostname }\n}\nconst installAmbient = () => {\n if (installed || typeof document === 'undefined') return\n installed = true\n const seen = new Set()\n document.querySelectorAll('.main > .page').forEach(p => {\n const site = siteOf(p)\n if (!seen.has(site)) { seen.add(site); credit(site, 'neighbourhood') }\n })\n const main = document.querySelector('.main')\n if (!main || typeof MutationObserver === 'undefined') return\n new MutationObserver(muts => {\n for (const m of muts) for (const node of m.addedNodes) {\n if (node.nodeType === 1 && node.classList.contains('page') && !node.classList.contains('ghost')) {\n credit(siteOf(node), 'visited')\n }\n }\n }).observe(main, { childList: true })\n}\n\n// \u2500\u2500 Remember: write the learned sites into the page's roster \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// $page is the jQuery page div holding the Search Algorithm; the roster whose\n// first line is \"Sites I visit\" is replaced in place through the standard\n// page action, so the change is journaled like any edit. Resolves to the\n// number of sites written, or throws when the page cannot be saved here.\n\nconst rememberInto = ($page, limit = 20) => new Promise((resolve, reject) => {\n const data = $page.data('data') || {}\n const item = (data.story || []).find(i => i.type === 'roster' && /^\\s*sites i visit\\b/i.test(i.text || ''))\n if (!item) return reject(new Error('no \"Sites I visit\" roster on this page'))\n const top = learned().slice(0, limit)\n if (!top.length) return reject(new Error('nothing learned yet'))\n const text = ['Sites I visit', ...top.map(([d]) => d)].join('\\n')\n const updated = { ...item, text }\n try {\n window.wiki.pageHandler.put($page, { type: 'edit', id: item.id, item: updated })\n // reflect the edit in the rendered item without a reload\n const $item = $page.find(`.item[data-id=\"${item.id}\"]`)\n if ($item.length) { $item.data('item', updated); window.wiki.plugins?.roster?.emit?.($item, updated) }\n resolve(top.length)\n } catch (e) { reject(e) }\n})\n\nexport { credit, learned, forget, installAmbient, rememberInto, CREDIT }\n", "// wiki-plugin-similarity \u2014 item styles and the site flag\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\n// \u2500\u2500 Styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst STYLES = `\n .sim-form { display:flex; gap:6px; margin-bottom:8px; }\n .sim-input { flex:1; padding:6px 8px; font-size:14px; border:1px solid #ccc; border-radius:3px; }\n .sim-btn { padding:6px 14px; background:#c4561d; color:white; border:none;\n border-radius:3px; cursor:pointer; font-size:14px; }\n .sim-btn:disabled { opacity:0.5; cursor:not-allowed; }\n .sim-status { font-size:12px; color:#888; margin-bottom:6px; min-height:16px; }\n .sim-results { margin-top:4px; }\n .sim-result { display:flex; align-items:center; gap:8px; padding:3px 0;\n border-bottom:1px solid #f0f0f0; }\n .sim-flag { width:16px; height:16px; vertical-align:middle; margin-right:4px; }\n .sim-link { font-size:14px; color:#406; flex:1; }\n .sim-domain { font-size:11px; color:#999; }\n .similar-results h3, .sim-list h3 { margin:4px 0 6px; font-size:14px; color:#555; }\n .similar-results ul { margin:0; padding-left:18px; }\n .similar-results li { font-size:14px; padding:2px 0; }\n .similar-results .sim-domain { margin-left:6px; }\n .sim-count { font-size:12px; color:#888; margin:4px 0 0; }\n .sim-list table { border-collapse:collapse; width:100%; font-size:13px; }\n .sim-list th { text-align:left; font-size:11px; color:#888; padding:2px 8px 4px 0;\n border-bottom:1px solid #ddd; }\n .sim-list td { padding:3px 8px 3px 0; border-bottom:1px solid #f0f0f0; vertical-align:middle; }\n .sim-list td:last-child { text-align:right; color:#999; font-size:11px; }\n .sim-list .sim-flag { margin-right:6px; }\n`\n\nconst siteFlag = (domain, score) =>\n `<img class=\"sim-flag remote\" src=\"${window.wiki.site(domain).flag()}\"\n title=\"${domain} \u2014 score ${score.toFixed(3)}\"\n data-site=\"${domain}\">`\n\nexport { STYLES, siteFlag }\n", "// wiki-plugin-similarity \u2014 the search door\n//\n// The `search` link at the foot of every page, and the search box beside it,\n// both build their result page in JavaScript: a fixed set of items, identical\n// on every site, that no owner can edit. This redirects both to the Search\n// Tool page, which is a real wiki page \u2014 shipped by this plugin, overridable\n// by forking it.\n//\n// Why this lives in a plugin at all. The honest home for a change to the\n// footer link is wiki-client, but wiki-client is bundled inside the wiki\n// package: on the public farm that comes from an image we do not build, and\n// the plugin door we do have installs wiki-plugin-* packages only. So a client\n// fork could never reach the sites that need this most.\n//\n// Plugin bundles load lazily, by item type, so a plugin is normally absent\n// from a page that does not use it. The one exception is preLoadEditors: a\n// plugin whose factory declares an editor is fetched on every page load. This\n// plugin declares one \u2014 delegating to the standard text editor, so editing\n// behaves exactly as before \u2014 and that is what gets this file onto the page.\n//\n// Capture phase, because wiki-client binds the same link by delegation on a\n// parent; stopping propagation there is what keeps the fabricated page from\n// being built underneath us.\n\nconst SEARCH_TOOL = 'Search Tool'\nconst SLUG = 'search-tool'\n\n// Only take the link over if the page is actually there. A site whose server\n// cannot serve it \u2014 an older plugin, or a server that answers no plugin\n// routes at all \u2014 keeps today's behaviour untouched, and nobody meets a dead\n// link. Checked once, at load, so the click itself stays synchronous.\nlet available = null\nconst checkAvailable = () =>\n fetch(`/${SLUG}.json`, { method: 'GET' })\n .then(r => { available = r.ok })\n .catch(() => { available = false })\n\n// The page the clicked link belongs to. doInternalLink opens the new page\n// straight after it, which is what SUBJECT reads: \"pages like the one I came\n// from\" needs no context passed, only the right position in the lineup.\nconst pageOf = target => {\n const $page = window.$(target).parents('.page')\n return $page.length ? $page : null\n}\n\nconst openSearchTool = ($page, keepLineup) => {\n // Shift-click keeps what is already open, matching the behaviour it replaces.\n window.wiki.doInternalLink(SEARCH_TOOL, keepLineup ? null : $page)\n}\n\nconst install = () => {\n if (typeof document === 'undefined' || !window.wiki || !window.$) return\n\n checkAvailable()\n\n document.addEventListener('click', e => {\n if (available !== true) return\n const link = e.target && e.target.closest && e.target.closest('a.search')\n if (!link) return\n const $page = pageOf(link)\n if (!$page) return\n e.preventDefault()\n e.stopPropagation()\n openSearchTool($page, e.shiftKey)\n }, true)\n\n // The search box: Enter builds the same fabricated page. Send it to the\n // Search Tool instead, carrying the query into the farm-wide search there\n // and running it, so a typed question still lands on results \u2014 on a page\n // the owner can change.\n document.addEventListener('keydown', e => {\n if (available !== true) return\n if (e.key !== 'Enter') return\n const input = e.target\n if (!input || !input.classList || !input.classList.contains('search')) return\n if (!input.closest('.searchbox')) return\n const query = (input.value || '').trim()\n if (!query) return\n e.preventDefault()\n e.stopPropagation()\n window.$('.incremental-search').remove()\n input.value = ''\n setPending(query)\n openSearchTool(window.$('.page').last(), false)\n }, true)\n}\n\n// Handing the query over, rather than racing the page.\n//\n// The first attempt polled for the search button and clicked it. The button\n// exists as soon as the item is drawn, but its handler is attached later, when\n// the item binds \u2014 so the click landed on nothing and the query sat in the box\n// unrun. Instead the door leaves the query here and the report item collects\n// it as it finishes wiring, which has no timing to get wrong.\nlet pending = null\nconst setPending = q => { pending = q }\nconst takePending = () => { const q = pending; pending = null; return q }\n\nexport { install, takePending }\n", "// wiki-plugin-similarity\n//\n// One item type: similarity\n//\n// DSL in item.text (newline-separated lines):\n// * all indexed domains on this server\n// david.* glob pattern\n// *.fish glob pattern\n// david.hitchhikers.earth explicit domain name\n// # comment ignored\n// LIST list indexed domains (mode directive)\n// SIMILAR: high threshold preset AND ambient mode trigger\n// SUBJECT modifier: act on the PREVIOUS page in the lineup\n// (the host a tool page was opened beside; falls\n// back to the containing page when none)\n// THRESHOLD: 0.72 exact cosine threshold (overrides SIMILAR:)\n// LIMIT: 8 max results shown (default 10)\n// ROSTER site/slug add the sites of a roster page to the scope\n// FARM other.farm.tld ask a peer farm to continue the search (experimental)\n//\n// Mode is determined by the FIRST meaningful line (Ward's ALL-CAPS convention):\n// LIST \u2192 show a table of all indexed domains and their page counts;\n// optional glob patterns on subsequent lines filter the list\n// SIMILAR: \u2192 ambient mode \u2014 automatically find pages similar to this page\n// REPORT \u2192 server-side ranked/bundled semantic report (ghost page)\n// KEYWORD \u2192 galactic MiniSearch over live site-index.json files (ghost page)\n// SITES \u2192 which site should this page go on? \u2014 per-domain aggregation of\n// the page-vector scan (ghost page)\n// (other) \u2192 search form mode \u2014 user types a query, results appear\n//\n// Server endpoints required (all same-origin, served by this plugin's server\n// component \u2014 works on any host, including the public farm):\n// GET /system/indexed-domains.json?pattern=glob1,glob2\n// GET /system/semantic-vectors.json?domain=\n// POST /system/embed.json {text}\n// POST /system/search-report.json\n// POST /system/site-report.json\n// GET /system/farm-search.json?q=&pattern=&limit=\n// GET /system/build-index.json?domains=&force=\n\nimport { parseDSL, DEFAULT_LIMIT, isScope, isGlob } from './dsl.js'\nimport { slugify, effectiveSpecs, resolveDomains } from './scope.js'\nimport { loadVectors, getEmbedding, lookupPageVector, cosineScan, loadDomainEntries } from './vectors.js'\nimport { resolveSubject, subjectNote } from './subject.js'\nimport { readCache, writeCache, cacheAge } from './cache.js'\nimport { runBatched } from './batch.js'\nimport { loadAlgorithm, parseAlgorithm, learnedSignals } from './algorithm.js'\nimport { credit, learned, forget, installAmbient, rememberInto } from './learn.js'\nimport { STYLES, siteFlag } from './styles.js'\nimport { install, takePending } from './searchdoor.js'\n\n// \u2500\u2500 similarity item \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst simLink = (domain, slug, title, score) =>\n `<a class=\"sim-link\" data-title=\"${title}\" data-slug=\"${slug}\" data-site=\"${domain}\" href=\"#\">` +\n `${siteFlag(domain, score)} ${title}</a>`\n\nconst shortDate = ms => {\n try { return new Date(ms).toLocaleDateString(undefined, { day: 'numeric', month: 'short' }) } catch { return '' }\n}\n\n// A merged batch result as a page, the same shape search-report renders.\nconst flatPage = (query, merged) => {\n const id = () => Math.random().toString(16).slice(2, 18).padEnd(16, '0')\n const story = [{ type: 'markdown', id: id(),\n text: `Federated search for **${query}** \u2014 ${merged.length} results, searched a batch at a time, nearest sites first.` }]\n const titles = [...new Set(merged.map(r => r.title))]\n if (titles.length) story.push({ type: 'markdown', id: id(), text: titles.map(t => `- [[${t}]]`).join('\\n') })\n story.push({ type: 'markdown', id: id(), text: '# Results' })\n for (const r of merged) {\n story.push({ type: 'reference', id: id(), site: r.site, slug: r.slug, title: r.title,\n text: (r.synopsis || `score ${r.score}`) + (r.via ? ` \u2014 via ${r.via}` : '') })\n }\n return { title: `${query} Federated Search`, story }\n}\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}\" data-mode=\"${mode}\" data-scope=\"${specs.join(',') || ''}\">\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}\" data-mode=\"${mode}\" data-scope=\"${specs.join(',') || ''}\">\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}\" data-mode=\"${mode}\" data-scope=\"${specs.join(',') || ''}\">\n <div class=\"sim-status\">Loading indexed domains (${label})\u2026</div>\n <div class=\"sim-list\"></div>\n </div>`)\n } else if (mode === 'algorithm') {\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\" data-mode=\"${mode}\" data-scope=\"\">\n <div class=\"sim-status\">Reading this search algorithm\u2026</div>\n <div class=\"sim-list\"></div>\n </div>`)\n } else if (mode === 'status') {\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\" data-mode=\"${mode}\" data-scope=\"${specs.join(',') || ''}\">\n <div class=\"sim-status\">Reading the state of the index\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}\" data-mode=\"${mode}\" data-scope=\"${specs.join(',') || ''}\">\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}\" data-mode=\"${mode}\" data-scope=\"${specs.join(',') || ''}\">\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}\" data-mode=\"${mode}\" data-scope=\"${specs.join(',') || ''}\">\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, batch, algorithm: algorithmRef } = parseDSL(item?.text || '')\n const origin = window.location.origin\n const status = div.find('.sim-status')[0]\n installAmbient() // learn.js: neighbourhood + visits, once per page load\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 // An explicit domain spec is taken at its word rather than looked up, so its\n // page count is unknown \u2014 not zero. Saying \"0 pages\" there told a reader\n // their site was unindexed when it was merely unqueried.\n const configSummary = (verb, pages, nDomains) => {\n const scale = pages > 0\n ? `${pages.toLocaleString()} pages across ${nDomains} domains`\n : `${nDomains} domain${nDomains === 1 ? '' : 's'}`\n const parts = [`${verb} \u2014 ${scale}`]\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 === 'status') {\n // The state of the index behind every other item on the page: which model,\n // how much is indexed, when it was last built, and \u2014 the one that decides\n // whether any of this works \u2014 whether a query can be turned into a vector\n // at all. A search page that cannot say this leaves the reader guessing\n // why an answer looks thin.\n const listDiv = div.find('.sim-list')[0]\n\n const ago = iso => {\n if (!iso) return 'unknown'\n const days = (Date.now() - new Date(iso)) / 86400000\n if (days < 1) return 'today'\n if (days < 2) return 'yesterday'\n return `${Math.floor(days)} days ago`\n }\n const day = iso => (iso ? new Date(iso).toISOString().slice(0, 10) : '\u2014')\n // The vector store (0.18.0): what is resident in memory right now, and\n // whether the warm-up has finished \u2014 a federation search costs\n // milliseconds once it has, and a cold parse until it has.\n const storeRow = st => {\n if (!st || !st.warm) return ''\n const w = st.warm\n const mb = Math.round((st.bytes || 0) / 1e6)\n const state = w.state === 'warm'\n ? `warm \u2014 ${w.done.toLocaleString()} sites resident in ${Math.round(w.ms / 1000)} s`\n : w.state === 'warming' ? `warming \u2014 ${w.done.toLocaleString()} of ${w.total.toLocaleString()} sites so far`\n : 'not warmed'\n const capped = w.capped ? ' \u00B7 <strong>cap reached</strong>, some sites re-parse per query' : ''\n return `<tr><th>Vector store</th><td>${(st.pages || 0).toLocaleString()} pages \u00B7 ${mb} MB of ${Math.round((st.capBytes || 0) / 1e6)} MB \u00B7 ${state}${capped}</td></tr>`\n }\n\n ;(async () => {\n try {\n // Whether search works is answered by trying it, not by reading a\n // field. A delegated embedder reports no supervisor state at all, so\n // a proxy pointed at a dead address looked healthy here \u2014 the exact\n // silent failure this panel exists to prevent.\n const probeEmbedder = () =>\n fetch(`${origin}/system/embed.json?text=ping`)\n .then(async r => r.ok\n ? { ok: true }\n : { ok: false, detail: ((await r.json().catch(() => ({}))).error || `status ${r.status}`)\n .replace(/^embedding unavailable: /, '') })\n .catch(e => ({ ok: false, detail: e.message }))\n\n const [health, domains, probe] = await Promise.all([\n fetch(`${origin}/system/similarity-health.json`).then(r => r.json()),\n fetch(`${origin}/system/indexed-domains.json`).then(r => r.ok ? r.json() : []),\n probeEmbedder(),\n ])\n const emb = health.embedder || {}\n const built = domains.map(d => d.built).filter(Boolean).sort()\n const pages = domains.reduce((n, d) => n + (d.page_count || 0), 0)\n\n // \"Up\" means a query can be embedded right now \u2014 proven a moment ago.\n const down = !probe.ok\n const upLine = down\n ? `<strong>Semantic search is down</strong> \u2014 a query cannot be turned into a vector right now (${probe.detail}), so searches on this page will answer by keyword instead.`\n : `Semantic search is <strong>up</strong> \u2014 checked just now by embedding a word.`\n const via = emb.via === 'url'\n ? `delegated to <code>${emb.url}</code> (from ${emb.source || 'config'})`\n : emb.via === 'semindex' ? 'in this process, via the SemIndex plugin'\n : 'in this process, in a supervised child'\n\n status.style.display = 'none'\n listDiv.innerHTML = `<h3>The Index Behind These Answers</h3>\n <p>${upLine}</p>\n <table>\n <tr><th>Model</th><td>${health.model || '\u2014'} \u00B7 ${health.dim || '\u2014'} dimensions</td></tr>\n <tr><th>Embedder</th><td>${via}${down ? ' \u2014 <strong>currently down</strong>' : ''}</td></tr>\n <tr><th>Indexed</th><td>${domains.length.toLocaleString()} domains \u00B7 ${pages.toLocaleString()} pages</td></tr>\n <tr><th>Newest page vectors</th><td>${day(built[built.length - 1])} (${ago(built[built.length - 1])})</td></tr>\n <tr><th>Oldest page vectors</th><td>${day(built[0])} (${ago(built[0])})</td></tr>\n <tr><th>Plugin</th><td>wiki-plugin-similarity ${health.version || '\u2014'}</td></tr>\n ${storeRow(health.store)}\n </table>\n <p class=\"sim-count\">A domain's vectors are only rebuilt when its pages change, so an old\n date means a quiet site, not a broken one. Pages saved since the last build are findable\n by keyword but not yet by meaning.</p>`\n } catch (e) {\n status.textContent = `Index state unavailable: ${e.message}`\n }\n })()\n\n } else if (mode === 'algorithm') {\n // The item IS the reader's search algorithm: show what the ranker will\n // read from it, and what the browser has learned so far (algorithm.js).\n // Rendered a tick later, like every other mode: a synchronous render\n // here was wiped by a second emit wiki-client runs on the item at load.\n setTimeout(() => {\n const listDiv = div.find('.sim-list')[0]\n try {\n const a = parseAlgorithm(item.text || '')\n const learned = learnedSignals()\n const top = Object.entries(learned).sort((x, y) => y[1] - x[1]).slice(0, 8)\n status.style.display = 'none'\n listDiv.innerHTML = `<h3>This Search Algorithm</h3>\n <table>\n ${Object.entries(a.weights).map(([k, v]) => `<tr><th>weight ${k}</th><td>${v}</td></tr>`).join('')}\n <tr><th>always</th><td>${a.always.length ? a.always.join(', ') : '\u2014'}</td></tr>\n <tr><th>never</th><td>${a.never.length ? a.never.join(', ') : '\u2014'}</td></tr>\n <tr><th>batch</th><td>${a.batch || 50}</td></tr>\n </table>\n <p class=\"sim-count\">Learned in this browser: ${top.length\n ? top.map(([d, v]) => `${d} (${v.toFixed(2)})`).join(', ')\n : 'nothing yet \u2014 sites you are near, open and click will appear here'}</p>\n <p class=\"sim-count\"><button class=\"sim-remember\">Remember</button> <button class=\"sim-forget\">Forget</button>\n <span class=\"sim-remember-note\"></span></p>`\n const note = listDiv.querySelector('.sim-remember-note')\n listDiv.querySelector('.sim-remember').addEventListener('click', async () => {\n try {\n const n = await rememberInto(div.parents('.page'))\n note.textContent = `${n} sites written into \"Sites I visit\" \u2014 read the roster above, delete any line.`\n } catch (e) { note.textContent = `Could not remember here: ${e.message}` }\n })\n listDiv.querySelector('.sim-forget').addEventListener('click', () => {\n forget(); note.textContent = 'Forgotten \u2014 the browser starts learning again from here.'\n })\n } catch (e) {\n status.textContent = `Algorithm unreadable: ${e.message}`\n }\n }, 0)\n\n } else 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 // A batched item searches by the Site Index, not this farm's vector\n // listing \u2014 so say how far the index reaches, not how many domains\n // this host happens to hold (0 on a farm with no galaxy tree).\n const isBatched = batch != null ? batch > 0 : eff.some(sp => sp.toUpperCase() === 'GALAXY')\n if (isBatched) {\n try {\n const health = await fetch(`${origin}/system/similarity-health.json`).then(r => r.json())\n const si = health.siteIndex || {}\n const named = eff.filter(sp => /\\./.test(sp) && !/[*?]/.test(sp) &&\n !['PUBLIC', 'LOCAL', 'PRIVATE', 'GALAXY'].includes(sp.toUpperCase()))\n const reach = si.count\n ? `ranks ${si.count.toLocaleString()} sites from the Site Index${si.via === 'peer' ? ' (a peer\\'s copy)' : ''}`\n : 'no Site Index on this farm \u2014 batches ask its peers'\n const parts = [`Ready \u2014 ${reach}`]\n if (named.length) parts.push(`${named.length} named site${named.length > 1 ? 's' : ''} searched wherever held`)\n if (thresholdSet) parts.push(`threshold ${threshold}`)\n parts.push(`limit ${limit}`)\n readyLine = parts.join(' \u00B7 ')\n } catch { /* keep the domain summary */ }\n }\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 // The embedder and the keyword index share nothing: one can be down while\n // the other is perfectly healthy. A report that cannot embed its query\n // should say so and answer with words, not leave an error and no results.\n // 502/503 are what the server sends when embedding is unavailable \u2014 a\n // missing local embedder, or the circuit breaker open after crashes.\n const EMBEDDER_DOWN = new Set([502, 503])\n\n const keywordInstead = async (query, eff) => {\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(`keyword search also failed: ${res.status}`)\n const page = await res.json()\n // The server titles this page \"\u2026 Keyword Search\", so the reader can see\n // which tier answered without being told twice.\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n }\n\n // Incremental federated search (batch.js): on by default for a scope\n // that reaches the galaxy, off for a farm-only scope, BATCH n/off decides.\n const batched = eff => batch != null ? batch > 0\n : eff.some(sp => sp.toUpperCase() === 'GALAXY')\n const results = div.find('.sim-results')[0]\n\n const doBatched = async (query, eff, seeded) => {\n const algorithm = await loadAlgorithm(algorithmRef, origin)\n const rosterDomains = eff.filter(sp => !isScope(sp) && sp !== '*' && !isGlob(sp))\n let ghost = null\n const openAsPage = merged => {\n const page = flatPage(query, merged)\n if (ghost) ghost.remove()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n ghost = div.parents('.page').next('.page')\n }\n const render = (merged, state) => {\n const shown = merged.slice(0, limit)\n const head = state.running\n ? `Searched ${state.searched.toLocaleString()} of ${state.total.toLocaleString()} sites \u2014 ` +\n `${merged.length} results \u2014 searching\u2026`\n : state.converged\n ? `Searched ${state.searched.toLocaleString()} of ${state.total.toLocaleString()} sites \u2014 ` +\n `${merged.length} results \u2014 the top ${limit} stopped changing`\n : `Searched ${state.searched.toLocaleString()} of ${state.total.toLocaleString()} sites \u2014 ` +\n `${merged.length} results`\n const rest = state.total - state.searched\n // Which indexer answered, and how old its index is (Mini Indexer\n // Plan, Phase 4): the batches say who held each site, the Site Index\n // says when each site was last indexed and which indexer placed it.\n const answered = Object.entries(state.answered || {})\n .filter(([, n]) => n > 0)\n .map(([h, n]) => `${h === 'local' ? 'this farm' : h} (${n.toLocaleString()} site${n === 1 ? '' : 's'})`)\n const built = state.siteIndex?.builtAt ? ` \u00B7 site index built ${shortDate(state.siteIndex.builtAt)}` : ''\n const tier = `<small class=\"sim-tier\">semantic \u00B7 nearest sites first${answered.length ? ` \u00B7 answered by ${answered.join(', ')}` : ''}${built}</small>`\n const provenance = r => {\n const info = state.siteInfo?.get(r.site)\n const bits = []\n if (r.via) bits.push(`via ${r.via}`)\n if (info?.indexedAt) bits.push(`indexed ${shortDate(info.indexedAt * 1000)}${info.placedBy ? ` by ${info.placedBy}` : ''}`)\n return bits.length ? ` <small class=\"sim-src\">${bits.join(' \u00B7 ')}</small>` : ''\n }\n results.innerHTML = `<h3>${head}</h3>${tier}<ul>${\n shown.map(r => `<li>${simLink(r.site, r.slug, r.title, r.semantic)}` + provenance(r) +\n (r.siblings?.length ? ` <small>+${r.siblings.length}</small>` : '') +\n (r.movedFrom ? ` <small>moved here from ${r.movedFrom}</small>` : '') +\n (r.gone ? ` <small>site ${r.gone}${r.movedTo ? `, probably now ${r.movedTo}` : ''}</small>` : '') + '</li>').join('')\n }</ul><p class=\"sim-count sim-batch-controls\">` +\n (state.running ? `<button class=\"sim-stop\">Stop</button>` : '') +\n (!state.running && rest > 0 ? `<button class=\"sim-more\">Search the remaining ${rest.toLocaleString()} sites</button>` : '') +\n (!state.running && merged.length ? `<button class=\"sim-open\">Open as page</button>` : '') +\n (state.unindexed?.length && !state.running\n ? `<br><small>Sites that look relevant but carry no page vectors yet: ${state.unindexed.slice(0, 8).join(', ')}</small>` : '') +\n `</p>`\n results.querySelector('.sim-stop')?.addEventListener('click', () => state.stop())\n results.querySelector('.sim-more')?.addEventListener('click', () => state.continueAll())\n results.querySelector('.sim-open')?.addEventListener('click', () => openAsPage(merged))\n results.querySelectorAll('.sim-link').forEach(a => a.addEventListener('click', e => {\n e.preventDefault()\n const { site, slug, title } = a.dataset\n credit(site, 'clicked')\n window.wiki.pageHandler.context = { site: window.location.hostname, slug: 'search-tool' }\n window.wiki.doInternalLink(title, div.parents('.page'), site)\n }))\n }\n status.textContent = ''\n const out = await runBatched({\n origin, query, seeded, specs: eff.length ? eff : ['GALAXY'], limit, threshold, thresholdSet,\n batch: batch || 50, roster: rosterDomains, algorithm, render,\n status: t => { status.textContent = t },\n })\n status.textContent = readyLine\n return out\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 if (batched(eff)) {\n const seeded = subject && query === subject.title ? seedParams() : {}\n await doBatched(query, eff, seeded)\n return\n }\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 (EMBEDDER_DOWN.has(res.status)) {\n const body = await res.json().catch(() => ({}))\n const why = (body.error || `status ${res.status}`).replace(/^embedding unavailable: /, '')\n status.textContent = 'Semantic search unavailable \u2014 searching by keyword instead\u2026'\n await keywordInstead(query, eff)\n status.textContent = `Semantic search unavailable (${why}) \u2014 these are keyword matches, ranked by word not meaning.`\n return\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 // A query typed into the wiki's search box and sent here by the search\n // door. Only the farm-wide report takes it: that is the scope the box\n // used to search, and picking any nearer one would quietly narrow it.\n if (mode === 'report' && specs.join(',') === '*') {\n const handed = takePending()\n if (handed) { input.value = handed; doReport() }\n }\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 // editor: declared so wiki-client's preLoadEditors fetches this plugin on\n // every page, which is what puts the search door (below) on pages that carry\n // no similarity item. It hands straight back to the standard text editor, so\n // creating and editing an item behaves exactly as it did before.\n window.plugins.similarity = {\n emit,\n bind,\n editor: ($item, item) => window.wiki.textEditor($item, item),\n }\n install()\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["// wiki-plugin-similarity \u2014 the item's line-oriented DSL: parse it, and classify a spec\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\n// \u2500\u2500 DSL Parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst SIMILAR_THRESHOLDS = { high: 0.78, medium: 0.68, low: 0.58 }\nconst DEFAULT_THRESHOLD = SIMILAR_THRESHOLDS.medium\nconst DEFAULT_LIMIT = 10\n\n// parseDSL returns { mode, specs, threshold, limit, ... }\n//\n// mode: 'algorithm' if ALGORITHM is the first meaningful line \u2014 the item IS\n// the reader's search algorithm (weights and rules, see algorithm.js);\n// elsewhere in an item, ALGORITHM site/slug names the page to read one from;\n// 'status' if STATUS is present \u2014 the state of the index behind the answers;\n// '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 let batch = null // BATCH n: search sites n at a time, widening (0 = off)\n let algorithm = null // ALGORITHM site/slug or [[Page]]: the reader's Search Algorithm page\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, 'BATCH')) {\n const b = val(line, 'BATCH').toLowerCase()\n batch = (b === 'off' || b === 'no' || b === '0') ? 0 : (parseInt(b) || 50)\n continue\n }\n if (isCmd(upper, 'ALGORITHM')) {\n const ref = val(line, 'ALGORITHM').replace(/^\\[\\[|\\]\\]$/g, '').trim()\n algorithm = ref || 'search-algorithm'\n if (mode === 'search' && !specs.length) mode = 'algorithm'\n continue\n }\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, 'STATUS')) {\n if (mode === 'search') mode = 'status'\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', 'SITE'].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 batch,\n algorithm,\n thresholdSet: threshold !== null, // explicit THRESHOLD/SIMILAR in the DSL\n }\n}\n\nconst isGlob = spec => spec.includes('*') || spec.includes('?')\n\n// Scope keywords expand server-side: PUBLIC (Nextcloud mirror farms),\n// LOCAL (primary farm), PRIVATE (public domains with restricted: true),\n// GALAXY (off-farm federation sites indexed into the galaxy tree)\nconst isScope = spec => spec === 'PUBLIC' || spec === 'LOCAL' || spec === 'PRIVATE' ||\n spec === 'GALAXY'\n\nexport { parseDSL, isGlob, isScope, SIMILAR_THRESHOLDS, DEFAULT_THRESHOLD, DEFAULT_LIMIT }\n", "// wiki-plugin-similarity \u2014 turning DSL specs into a concrete list of domains\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\nimport { isGlob, isScope } from './dsl.js'\n\n// \u2500\u2500 Slug \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst slugify = title => title.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '')\n\n// \u2500\u2500 Domain resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst domainCache = new Map()\n\nconst resolveDomainsForSpec = async (spec, origin) => {\n if (domainCache.has(spec)) return domainCache.get(spec)\n const url = `${origin}/system/indexed-domains.json?pattern=${encodeURIComponent(spec)}`\n const res = await fetch(url)\n if (!res.ok) throw new Error(`indexed-domains failed: ${res.status}`)\n const list = await res.json()\n domainCache.set(spec, list)\n return list\n}\n\nconst resolveDomains = async (specs, origin) => {\n if (!specs.length) specs = [window.location.hostname]\n const seen = new Set()\n const result = []\n for (const spec of specs) {\n if (spec === '*' || isGlob(spec) || isScope(spec)) {\n for (const item of await resolveDomainsForSpec(spec, origin)) {\n if (!seen.has(item.domain)) { seen.add(item.domain); result.push(item) }\n }\n } else if (!seen.has(spec)) {\n seen.add(spec); result.push({ domain: spec, page_count: null })\n }\n }\n return result\n}\n\n// \u2500\u2500 Roster resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// ROSTER site/slug resolves a roster page into domain names, following the\n// roster plugin's own line semantics: bare domains are sites; category and\n// blank lines are skipped; nested `ROSTER site/slug` includes another roster\n// page; `REFERENCES site/slug` collects the .site of a page's reference items.\n// Includes are followed transitively with a visited-set loop guard.\n\nconst SITE_LINE = /^([a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)+|localhost)(:\\d+)?$/\nconst ROSTER_LINE = /^ROSTER ([A-Za-z0-9.\\-:]+\\/[a-z0-9-]+)$/\nconst REFS_LINE = /^REFERENCES ([A-Za-z0-9.\\-:]+\\/[a-z0-9-]+)$/\n\nconst fetchPage = async ref => { // ref = site/slug\n const i = ref.indexOf('/')\n const res = await fetch(`//${ref.slice(0, i)}/${ref.slice(i + 1)}.json`)\n if (!res.ok) throw new Error(`roster page ${ref} failed: ${res.status}`)\n return res.json()\n}\n\nconst resolveRoster = async (ref, out, visited) => {\n if (visited.has(ref)) return\n visited.add(ref)\n let page\n try { page = await fetchPage(ref) } catch (e) { console.warn(e.message); return }\n for (const item of page.story || []) {\n if (item.type !== 'roster') continue\n for (const raw of (item.text || '').split(/\\r?\\n/)) {\n const line = raw.trim()\n if (!line) continue\n const site = line.match(SITE_LINE)\n if (site) { out.add(site[0]); continue }\n const nested = line.match(ROSTER_LINE)\n if (nested) { await resolveRoster(nested[1], out, visited); continue }\n const refs = line.match(REFS_LINE)\n if (refs) {\n try {\n const refPage = await fetchPage(refs[1])\n for (const it of refPage.story || []) {\n if (it.type === 'reference' && it.site) out.add(it.site)\n }\n } catch (e) { console.warn(e.message) }\n }\n // anything else is a category name \u2014 skipped\n }\n }\n}\n\nconst resolveRosters = async rosterRefs => {\n const out = new Set()\n const visited = new Set()\n for (const ref of rosterRefs) await resolveRoster(ref, out, visited)\n return [...out]\n}\n\n// Effective specs: DSL specs plus any domains contributed by ROSTER lines.\n//\n// SITE expands to the domain the page is being served from. A page shipped to\n// every site in the farm \u2014 a plugin's utility page \u2014 cannot name its own home,\n// and leaving the scope empty means the whole farm in the report modes. SITE\n// is how such a page says \"search here\" and means it wherever it lands.\nconst effectiveSpecs = async (specs, rosterRefs) => {\n const here = (typeof window !== 'undefined' && window.location)\n ? window.location.hostname : null\n const expanded = specs.map(s => (s.toUpperCase() === 'SITE' && here ? here : s))\n return rosterRefs.length ? [...expanded, ...await resolveRosters(rosterRefs)] : expanded\n}\n\nexport { slugify, resolveDomains, resolveRosters, effectiveSpecs, SITE_LINE, ROSTER_LINE, REFS_LINE }\n", "// wiki-plugin-similarity \u2014 page vectors, embedding, and the cosine scan over them\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\nimport { resolveDomains } from './scope.js'\n\n// \u2500\u2500 Vector loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst vectorCache = new Map()\n\n// Route vector requests through the current wiki's plugin server. This keeps\n// public HTTPS pages same-origin, avoids CORS failures, and lets the server read\n// indices from its configured farm roots.\nconst vectorUrl = domain =>\n `${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(domain)}`\n\nconst loadVectors = async domain => {\n if (vectorCache.has(domain)) return vectorCache.get(domain)\n const res = await fetch(vectorUrl(domain))\n if (!res.ok) return []\n const data = await res.json()\n vectorCache.set(domain, data)\n return data\n}\n\n// \u2500\u2500 Embedding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// POST, never GET: page-length text in a query string overflows Node's header\n// limit and dies with 431 before any handler runs (server \u2265 0.11.0).\nconst getEmbedding = async (text, origin) => {\n const res = await fetch(`${origin}/system/embed.json`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ text }),\n })\n if (!res.ok) throw new Error(`embed failed: ${res.status}`)\n return (await res.json()).vector\n}\n\n// Look up an existing page vector from the current domain's cached index.\n// Returns the float[] vector if found, null otherwise.\nconst lookupPageVector = async (slug, domain) => {\n const pages = await loadVectors(domain)\n const entry = pages.find(p => p.slug === slug)\n return entry ? entry.vector : null\n}\n// \u2500\u2500 Cosine search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst cosineScan = (queryVec, domainEntries, { threshold, limit, excludeSlug, excludeDomain }) => {\n const results = []\n for (const { domain, pages } of domainEntries) {\n for (const { slug, title, vector } of pages) {\n if (slug === excludeSlug && domain === excludeDomain) continue\n let dot = 0\n for (let i = 0; i < queryVec.length; i++) dot += queryVec[i] * vector[i]\n if (dot >= threshold) results.push({ domain, slug, title, score: dot })\n }\n }\n results.sort((a, b) => b.score - a.score)\n return results.slice(0, limit)\n}\n\n// \u2500\u2500 Shared domain loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst loadDomainEntries = async (specs, origin) => {\n const domains = await resolveDomains(specs, origin)\n const entries = await Promise.all(\n domains.map(async ({ domain }) => ({ domain, pages: await loadVectors(domain) }))\n )\n return entries.filter(e => e.pages.length > 0)\n}\n\nexport { loadVectors, vectorUrl, getEmbedding, lookupPageVector, cosineScan, loadDomainEntries }\n", "// wiki-plugin-similarity \u2014 SUBJECT \u2014 the page an item acts on\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\n// \u2500\u2500 Subject resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// SUBJECT makes an item act on the page BEFORE its own in the lineup \u2014 the\n// host a tool page was opened beside \u2014 falling back to the containing page so\n// a tool page opened alone still works (and announces the fallback).\n// The page div's id IS the slug (possibly _rev-suffixed on a historical view);\n// data('data') carries the full raw page JSON; data('site') is set only on\n// remote pages. `text` is title + opening prose, capped: the BGE-small\n// embedder truncates around 512 tokens, so more would be discarded anyway.\n\nconst resolveSubject = div => {\n const $self = div.parents('.page:first')\n const $host = $self.prev('.page')\n const $page = $host.length ? $host : $self\n const slug = ($page.attr('id') || '').split('_rev')[0]\n const site = $page.data('site') || window.location.hostname\n const data = $page.data('data') || {}\n const title = data.title || $page.find('.title').text().trim() || slug\n const text = [title, ...(data.story || [])\n .filter(i => i.type === 'markdown' || i.type === 'paragraph')\n .map(i => (i.text || '').trim()).filter(Boolean)].join('\\n').slice(0, 2000)\n return { slug, site, title, text, isSelf: !$host.length }\n}\n\nconst subjectNote = s =>\n `Subject: ${s.title}${s.isSelf ? ' (this page)' : ` @ ${s.site}`}`\n\nexport { resolveSubject, subjectNote }\n", "// wiki-plugin-similarity \u2014 per-item result cache in localStorage\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\n// \u2500\u2500 Result cache (localStorage) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Cache is keyed by item id. Invalidated when item.text changes (DSL edited).\n// LIVE mode bypasses the cache entirely.\n\nconst cacheKey = id => `sim-cache-${id}`\n\nconst readCache = item => {\n try {\n const c = JSON.parse(localStorage.getItem(cacheKey(item.id)) || 'null')\n return c?.text === (item.text || '') ? c : null\n } catch { return null }\n}\n\nconst writeCache = (item, data) => {\n try {\n localStorage.setItem(cacheKey(item.id), JSON.stringify({\n text: item.text || '',\n ts: Date.now(),\n ...data,\n }))\n } catch { /* storage unavailable or full */ }\n}\n\nconst cacheAge = ts => {\n const s = Math.floor((Date.now() - ts) / 1000)\n if (s < 60) return `${s}s ago`\n if (s < 3600) return `${Math.floor(s / 60)}m ago`\n if (s < 86400) return `${Math.floor(s / 3600)}h ago`\n return `${Math.floor(s / 86400)}d ago`\n}\n\nexport { readCache, writeCache, cacheAge }\n", "// wiki-plugin-similarity \u2014 incremental federated search, client side\n//\n// Rank the sites first, then search them a batch at a time, merging results\n// in place until the answer stops changing (design: search.fedwiki.club\n// /incremental-federated-search).\n//\n// 1. POST site-rank.json \u2014 every site the farm knows, ordered for this\n// query: preferred sites (roster, neighbourhood, followed, the reader's\n// algorithm) first, then by centroid similarity. Returns the batches.\n// 2. POST search-report.json {flat: true, domains: batch} per batch \u2014 the\n// ordinary report pipeline over an explicit domain list; scores are\n// comparable across batches because every site shares one model.\n// 3. Merge by score, render in the item as each batch lands, stop when\n// QUIET consecutive batches change nothing in the top `limit`, and\n// offer the rest behind a button.\n\nconst QUIET = 3\n\nconst neighbourhoodDomains = () => {\n const out = new Set()\n try {\n for (const site of Object.keys(window.wiki?.neighborhood || {})) out.add(site)\n document.querySelectorAll('.page').forEach(p => {\n const site = window.$ ? window.$(p).data('site') : null\n out.add(site || window.location.hostname)\n })\n } catch { /* no lineup to read */ }\n return [...out]\n}\n\nconst post = async (url, body) => {\n const res = await fetch(url, {\n method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),\n })\n if (!res.ok) {\n const err = new Error(`${url.split('/').pop()} failed: ${res.status}`)\n err.status = res.status\n err.body = await res.json().catch(() => ({}))\n throw err\n }\n return res.json()\n}\n\nconst sameTop = (a, b, limit) => {\n const ka = a.slice(0, limit).map(r => `${r.site} ${r.slug}`).join('|')\n const kb = b.slice(0, limit).map(r => `${r.site} ${r.slug}`).join('|')\n return ka === kb\n}\n\n// opts: {origin, query, seeded, specs, limit, threshold, thresholdSet, batch,\n// roster, algorithm, prefer, render(results, state), status(text)}\n// Returns the final merged results; runs until done, stopped, or converged.\nconst runBatched = async opts => {\n const { origin, query, seeded = {}, specs, limit, batch, algorithm, render } = opts\n const say = t => opts.status && opts.status(t)\n const prefer = {\n roster: [...(opts.roster || []), ...((algorithm && algorithm.liked) || [])],\n neighborhood: neighbourhoodDomains(),\n followed: true,\n ...(opts.prefer || {}),\n }\n say('Ranking sites\u2026')\n const rank = await post(`${origin}/system/site-rank.json`, {\n query, ...seeded, domains: specs, prefer, algorithm: algorithm || {}, batch: batch || 50,\n })\n const batches = rank.batches || []\n // A domain named explicitly is searched whether or not this farm's Site\n // Index knows it: the cascade finds the peer that holds it. Unranked\n // explicit domains go last, as one more batch.\n const ranked = new Set(batches.flat().map(d => d.toLowerCase()))\n const explicit = (specs || []).filter(sp => typeof sp === 'string' && /\\./.test(sp) &&\n !/[*?]/.test(sp) && !['PUBLIC', 'LOCAL', 'PRIVATE', 'GALAXY'].includes(sp.toUpperCase()) &&\n !ranked.has(sp.toLowerCase()))\n if (explicit.length) batches.push(explicit)\n const total = (rank.count || 0) + explicit.length || batches.reduce((n, b) => n + b.length, 0)\n const state = { searched: 0, total, batches: batches.length, done: 0, stopped: false,\n converged: false, running: true, unindexed: rank.unindexed || [],\n // who answered: this farm and each peer, by sites searched\n answered: { local: 0 }, siteIndex: rank.siteIndex || null,\n // what the Site Index says of each site: when it was indexed, by whom\n siteInfo: new Map((rank.sites || []).map(r => [r.domain, r])) }\n\n const merged = new Map()\n let results = []\n let quiet = 0\n const stop = () => { state.stopped = true }\n state.stop = stop\n\n const runBatch = async domains => {\n const body = { query, ...seeded, domains, limit: Math.max(limit * 3, 30), flat: true }\n if (opts.thresholdSet) body.threshold = opts.threshold\n const flat = await post(`${origin}/system/search-report.json`, body)\n for (const r of flat.results || []) {\n const key = `${r.site} ${r.slug}`\n const prev = merged.get(key)\n if (!prev || prev.score < r.score) merged.set(key, r)\n }\n let peerDomains = 0\n for (const p of flat.peers || []) {\n state.answered[p.host] = (state.answered[p.host] || 0) + (p.domains || 0)\n peerDomains += p.domains || 0\n }\n state.answered.local += Math.max(0, ((flat.stats && flat.stats.domains) || 0) - peerDomains)\n const next = [...merged.values()].sort((a, b) => b.score - a.score)\n const unchanged = sameTop(results, next, limit)\n results = next\n state.searched += domains.length\n state.done += 1\n quiet = unchanged ? quiet + 1 : 0\n }\n\n const remaining = () => batches.slice(state.done)\n const loop = async (all = false) => {\n state.running = true\n state.stopped = false\n while (state.done < batches.length && !state.stopped) {\n await runBatch(batches[state.done])\n render(results, state)\n if (!all && quiet >= QUIET && state.done < batches.length) {\n state.converged = true\n break\n }\n }\n state.running = false\n render(results, state)\n return results\n }\n state.continueAll = () => { state.converged = false; quiet = -Infinity; return loop(true) }\n\n await loop(false)\n return { results, state, remaining }\n}\n\nexport { runBatched, neighbourhoodDomains, QUIET }\n", "// wiki-plugin-similarity \u2014 the reader's own search algorithm, as a page\n//\n// The ranking a federated search uses is a wiki page the reader owns (design:\n// search.fedwiki.club/incremental-federated-search, layer three). This module\n// reads it: the ALGORITHM item's weights and rules, the page's roster items\n// (sites liked, sites visited), and what this browser has learned so far\n// (learn.js writes localStorage['similarity:learned']). What comes back is the\n// `algorithm` object site-rank.json applies on top of the Site Index ranking.\n//\n// ALGORITHM\n// WEIGHT liked 1.0 how much a roster site is lifted\n// WEIGHT visited 0.6 \u2026 a site this browser learned\n// WEIGHT neighbourhood 0.4 \u2026 a site already in the lineup\n// WEIGHT followed 0.2 \u2026 a registry followed-tier site\n// WEIGHT centroid 1.0 \u2026 similarity of the site to the query\n// WEIGHT fresh 0.2 \u2026 freshness of the index that holds the site\n// ALWAYS ward.bay.wiki.org pinned into the first batch\n// NEVER spam.example pruned from every batch\n// BATCH 50 sites searched per request\n// WEIGHT reliable 0.3 how far an unreliable site sinks (Dead Sites Plan)\n// WEIGHT stale 0.2 how far a stale or abandoned site sinks; 0 or less digs in the old ones\n// TRUST solid|flaky|any solid: skip shaky sites; flaky: search them later\n// (default); any: even dead and moved sites\n//\n// Unknown words are ignored, so the vocabulary can grow release by release.\n\nconst LEARNED_KEY = 'similarity:learned'\nconst WEIGHT_NAMES = new Set(['liked', 'visited', 'neighbourhood', 'neighborhood', 'followed', 'centroid', 'fresh', 'reliable', 'stale'])\nconst SITE = /^([a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)+|localhost)(:\\d+)?$/\n\nconst parseAlgorithm = text => {\n const out = { weights: {}, always: [], never: [], batch: null, trust: null }\n for (const raw of (text || '').split('\\n')) {\n const line = raw.trim()\n if (!line || line.startsWith('#')) continue\n const [word, ...rest] = line.split(/\\s+/)\n const kw = word.toUpperCase().replace(/:$/, '')\n if (kw === 'WEIGHT' && rest.length >= 2) {\n const name = rest[0].toLowerCase().replace('neighborhood', 'neighbourhood')\n const v = parseFloat(rest[1])\n if (WEIGHT_NAMES.has(name) && Number.isFinite(v)) out.weights[name] = v\n } else if (kw === 'ALWAYS' && rest[0] && SITE.test(rest[0])) out.always.push(rest[0].toLowerCase())\n else if (kw === 'NEVER' && rest[0] && SITE.test(rest[0])) out.never.push(rest[0].toLowerCase())\n else if (kw === 'BATCH' && rest[0]) out.batch = parseInt(rest[0]) || null\n else if (kw === 'TRUST' && rest[0] && ['solid', 'flaky', 'any'].includes(rest[0].toLowerCase())) out.trust = rest[0].toLowerCase()\n }\n return out\n}\n\n// What this browser has learned: {domain: score 0..1}, decayed by learn.js.\nconst learnedSignals = () => {\n try {\n const raw = JSON.parse(localStorage.getItem(LEARNED_KEY) || '{}')\n const out = {}\n for (const [d, v] of Object.entries(raw.sites || raw)) {\n const score = typeof v === 'number' ? v : (v && typeof v.score === 'number' ? v.score : 0)\n if (score > 0) out[d.toLowerCase()] = Math.min(1, score)\n }\n return out\n } catch { return {} }\n}\n\n// ref: 'site/slug' | 'Page Title' | 'page-slug' | null (\u2192 this site's search-algorithm)\nconst refToUrl = (ref, origin) => {\n if (!ref) return `${origin}/search-algorithm.json`\n const i = ref.indexOf('/')\n if (i > 0 && SITE.test(ref.slice(0, i))) return `//${ref.slice(0, i)}/${ref.slice(i + 1)}.json`\n const slug = ref.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '')\n return `${origin}/${slug}.json`\n}\n\n// Roster items on the page: every bare site line, all of them \"liked\".\nconst rosterSites = page => {\n const out = new Set()\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 (SITE.test(line)) out.add(line.toLowerCase())\n }\n }\n return [...out]\n}\n\nconst cache = new Map()\nconst loadAlgorithm = async (ref, origin) => {\n const url = refToUrl(ref, origin)\n let algo = cache.get(url)\n if (!algo) {\n algo = { weights: {}, always: [], never: [], liked: [], batch: null, trust: null, page: null }\n try {\n const res = await fetch(url)\n if (res.ok) {\n const page = await res.json()\n const item = (page.story || []).find(i => i.type === 'similarity' &&\n /^\\s*ALGORITHM\\b/im.test(i.text || ''))\n if (item) Object.assign(algo, parseAlgorithm(item.text))\n algo.liked = rosterSites(page)\n algo.page = { url, title: page.title }\n }\n } catch { /* no algorithm page: defaults apply */ }\n cache.set(url, algo)\n }\n return { ...algo, learned: learnedSignals() }\n}\n\nexport { parseAlgorithm, learnedSignals, loadAlgorithm, rosterSites, refToUrl, LEARNED_KEY }\n", "// wiki-plugin-similarity \u2014 the learning loop, in the browser\n//\n// A site earns credit when it enters your neighbourhood, when you open a page\n// from it, and when you click one of its search results. Scores live in\n// localStorage['similarity:learned'] as {sites: {domain: {score, at}}} and\n// decay by half every week, so the list follows where you read now. Nothing\n// leaves the browser until the reader presses Remember on their Search\n// Algorithm page, which writes the top sites into its \"Sites I visit\" roster\n// through an ordinary page save \u2014 inspectable, forkable, deletable line by\n// line. (Design: search.fedwiki.club/incremental-federated-search, layer three.)\n\nimport { LEARNED_KEY } from './algorithm.js'\n\nconst CREDIT = { neighbourhood: 0.1, visited: 0.2, clicked: 0.35 }\nconst HALF_LIFE_MS = 7 * 86_400_000\nconst MAX_SITES = 200\n\nconst load = () => {\n try { return JSON.parse(localStorage.getItem(LEARNED_KEY) || '{}').sites || {} } catch { return {} }\n}\nconst save = sites => {\n try { localStorage.setItem(LEARNED_KEY, JSON.stringify({ sites, savedAt: Date.now() })) } catch { /* private mode */ }\n}\nconst decayed = (entry, now) => {\n const weeks = Math.max(0, (now - (entry.at || now)) / HALF_LIFE_MS)\n return (entry.score || 0) * Math.pow(0.5, weeks)\n}\n\n// Credit one site for one kind of signal; returns the new score.\nconst credit = (domain, kind) => {\n if (!domain || domain === 'localhost') return 0\n const d = domain.toLowerCase()\n const now = Date.now()\n const sites = load()\n const cur = sites[d] ? decayed(sites[d], now) : 0\n const score = Math.min(1, cur + (CREDIT[kind] || 0.1))\n sites[d] = { score, at: now }\n // keep the list bounded: drop the faintest\n const keys = Object.keys(sites)\n if (keys.length > MAX_SITES) {\n keys.sort((a, b) => decayed(sites[b], now) - decayed(sites[a], now))\n for (const k of keys.slice(MAX_SITES)) delete sites[k]\n }\n save(sites)\n return score\n}\n\n// Current scores, decayed, best first: [[domain, score], ...]\nconst learned = () => {\n const now = Date.now()\n return Object.entries(load())\n .map(([d, e]) => [d, decayed(e, now)])\n .filter(([, s]) => s > 0.01)\n .sort((a, b) => b[1] - a[1])\n}\n\nconst forget = () => save({})\n\n// \u2500\u2500 Ambient signals \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Installed once per page load by the first similarity item that binds:\n// every site already in the lineup gets neighbourhood credit, and every page\n// that later joins the lineup credits its site as visited. Wiki adds pages\n// as .page divs under .main, so a MutationObserver is the hook.\n\nlet installed = false\nconst siteOf = el => {\n try { return (window.$ && window.$(el).data('site')) || window.location.hostname } catch { return window.location.hostname }\n}\nconst installAmbient = () => {\n if (installed || typeof document === 'undefined') return\n installed = true\n const seen = new Set()\n document.querySelectorAll('.main > .page').forEach(p => {\n const site = siteOf(p)\n if (!seen.has(site)) { seen.add(site); credit(site, 'neighbourhood') }\n })\n const main = document.querySelector('.main')\n if (!main || typeof MutationObserver === 'undefined') return\n new MutationObserver(muts => {\n for (const m of muts) for (const node of m.addedNodes) {\n if (node.nodeType === 1 && node.classList.contains('page') && !node.classList.contains('ghost')) {\n credit(siteOf(node), 'visited')\n }\n }\n }).observe(main, { childList: true })\n}\n\n// \u2500\u2500 Remember: write the learned sites into the page's roster \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// $page is the jQuery page div holding the Search Algorithm; the roster whose\n// first line is \"Sites I visit\" is replaced in place through the standard\n// page action, so the change is journaled like any edit. Resolves to the\n// number of sites written, or throws when the page cannot be saved here.\n\nconst rememberInto = ($page, limit = 20) => new Promise((resolve, reject) => {\n const data = $page.data('data') || {}\n const item = (data.story || []).find(i => i.type === 'roster' && /^\\s*sites i visit\\b/i.test(i.text || ''))\n if (!item) return reject(new Error('no \"Sites I visit\" roster on this page'))\n const top = learned().slice(0, limit)\n if (!top.length) return reject(new Error('nothing learned yet'))\n const text = ['Sites I visit', ...top.map(([d]) => d)].join('\\n')\n const updated = { ...item, text }\n try {\n window.wiki.pageHandler.put($page, { type: 'edit', id: item.id, item: updated })\n // reflect the edit in the rendered item without a reload\n const $item = $page.find(`.item[data-id=\"${item.id}\"]`)\n if ($item.length) { $item.data('item', updated); window.wiki.plugins?.roster?.emit?.($item, updated) }\n resolve(top.length)\n } catch (e) { reject(e) }\n})\n\nexport { credit, learned, forget, installAmbient, rememberInto, CREDIT }\n", "// wiki-plugin-similarity \u2014 item styles and the site flag\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\n// \u2500\u2500 Styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst STYLES = `\n .sim-form { display:flex; gap:6px; margin-bottom:8px; }\n .sim-input { flex:1; padding:6px 8px; font-size:14px; border:1px solid #ccc; border-radius:3px; }\n .sim-btn { padding:6px 14px; background:#c4561d; color:white; border:none;\n border-radius:3px; cursor:pointer; font-size:14px; }\n .sim-btn:disabled { opacity:0.5; cursor:not-allowed; }\n .sim-status { font-size:12px; color:#888; margin-bottom:6px; min-height:16px; }\n .sim-results { margin-top:4px; }\n .sim-result { display:flex; align-items:center; gap:8px; padding:3px 0;\n border-bottom:1px solid #f0f0f0; }\n .sim-flag { width:16px; height:16px; vertical-align:middle; margin-right:4px; }\n .sim-link { font-size:14px; color:#406; flex:1; }\n .sim-domain { font-size:11px; color:#999; }\n .similar-results h3, .sim-list h3 { margin:4px 0 6px; font-size:14px; color:#555; }\n .similar-results ul { margin:0; padding-left:18px; }\n .similar-results li { font-size:14px; padding:2px 0; }\n .similar-results .sim-domain { margin-left:6px; }\n .sim-count { font-size:12px; color:#888; margin:4px 0 0; }\n .sim-list table { border-collapse:collapse; width:100%; font-size:13px; }\n .sim-list th { text-align:left; font-size:11px; color:#888; padding:2px 8px 4px 0;\n border-bottom:1px solid #ddd; }\n .sim-list td { padding:3px 8px 3px 0; border-bottom:1px solid #f0f0f0; vertical-align:middle; }\n .sim-list td:last-child { text-align:right; color:#999; font-size:11px; }\n .sim-list .sim-flag { margin-right:6px; }\n`\n\nconst siteFlag = (domain, score) =>\n `<img class=\"sim-flag remote\" src=\"${window.wiki.site(domain).flag()}\"\n title=\"${domain} \u2014 score ${score.toFixed(3)}\"\n data-site=\"${domain}\">`\n\nexport { STYLES, siteFlag }\n", "// wiki-plugin-similarity \u2014 the search door\n//\n// The `search` link at the foot of every page, and the search box beside it,\n// both build their result page in JavaScript: a fixed set of items, identical\n// on every site, that no owner can edit. This redirects both to the Search\n// Tool page, which is a real wiki page \u2014 shipped by this plugin, overridable\n// by forking it.\n//\n// Why this lives in a plugin at all. The honest home for a change to the\n// footer link is wiki-client, but wiki-client is bundled inside the wiki\n// package: on the public farm that comes from an image we do not build, and\n// the plugin door we do have installs wiki-plugin-* packages only. So a client\n// fork could never reach the sites that need this most.\n//\n// Plugin bundles load lazily, by item type, so a plugin is normally absent\n// from a page that does not use it. The one exception is preLoadEditors: a\n// plugin whose factory declares an editor is fetched on every page load. This\n// plugin declares one \u2014 delegating to the standard text editor, so editing\n// behaves exactly as before \u2014 and that is what gets this file onto the page.\n//\n// Capture phase, because wiki-client binds the same link by delegation on a\n// parent; stopping propagation there is what keeps the fabricated page from\n// being built underneath us.\n\nconst SEARCH_TOOL = 'Search Tool'\nconst SLUG = 'search-tool'\n\n// Only take the link over if the page is actually there. A site whose server\n// cannot serve it \u2014 an older plugin, or a server that answers no plugin\n// routes at all \u2014 keeps today's behaviour untouched, and nobody meets a dead\n// link. Checked once, at load, so the click itself stays synchronous.\nlet available = null\nconst checkAvailable = () =>\n fetch(`/${SLUG}.json`, { method: 'GET' })\n .then(r => { available = r.ok })\n .catch(() => { available = false })\n\n// The page the clicked link belongs to. doInternalLink opens the new page\n// straight after it, which is what SUBJECT reads: \"pages like the one I came\n// from\" needs no context passed, only the right position in the lineup.\nconst pageOf = target => {\n const $page = window.$(target).parents('.page')\n return $page.length ? $page : null\n}\n\nconst openSearchTool = ($page, keepLineup) => {\n // Shift-click keeps what is already open, matching the behaviour it replaces.\n window.wiki.doInternalLink(SEARCH_TOOL, keepLineup ? null : $page)\n}\n\nconst install = () => {\n if (typeof document === 'undefined' || !window.wiki || !window.$) return\n\n checkAvailable()\n\n document.addEventListener('click', e => {\n if (available !== true) return\n const link = e.target && e.target.closest && e.target.closest('a.search')\n if (!link) return\n const $page = pageOf(link)\n if (!$page) return\n e.preventDefault()\n e.stopPropagation()\n openSearchTool($page, e.shiftKey)\n }, true)\n\n // The search box: Enter builds the same fabricated page. Send it to the\n // Search Tool instead, carrying the query into the farm-wide search there\n // and running it, so a typed question still lands on results \u2014 on a page\n // the owner can change.\n document.addEventListener('keydown', e => {\n if (available !== true) return\n if (e.key !== 'Enter') return\n const input = e.target\n if (!input || !input.classList || !input.classList.contains('search')) return\n if (!input.closest('.searchbox')) return\n const query = (input.value || '').trim()\n if (!query) return\n e.preventDefault()\n e.stopPropagation()\n window.$('.incremental-search').remove()\n input.value = ''\n setPending(query)\n openSearchTool(window.$('.page').last(), false)\n }, true)\n}\n\n// Handing the query over, rather than racing the page.\n//\n// The first attempt polled for the search button and clicked it. The button\n// exists as soon as the item is drawn, but its handler is attached later, when\n// the item binds \u2014 so the click landed on nothing and the query sat in the box\n// unrun. Instead the door leaves the query here and the report item collects\n// it as it finishes wiring, which has no timing to get wrong.\nlet pending = null\nconst setPending = q => { pending = q }\nconst takePending = () => { const q = pending; pending = null; return q }\n\nexport { install, takePending }\n", "// wiki-plugin-similarity\n//\n// One item type: similarity\n//\n// DSL in item.text (newline-separated lines):\n// * all indexed domains on this server\n// david.* glob pattern\n// *.fish glob pattern\n// david.hitchhikers.earth explicit domain name\n// # comment ignored\n// LIST list indexed domains (mode directive)\n// SIMILAR: high threshold preset AND ambient mode trigger\n// SUBJECT modifier: act on the PREVIOUS page in the lineup\n// (the host a tool page was opened beside; falls\n// back to the containing page when none)\n// THRESHOLD: 0.72 exact cosine threshold (overrides SIMILAR:)\n// LIMIT: 8 max results shown (default 10)\n// ROSTER site/slug add the sites of a roster page to the scope\n// FARM other.farm.tld ask a peer farm to continue the search (experimental)\n//\n// Mode is determined by the FIRST meaningful line (Ward's ALL-CAPS convention):\n// LIST \u2192 show a table of all indexed domains and their page counts;\n// optional glob patterns on subsequent lines filter the list\n// SIMILAR: \u2192 ambient mode \u2014 automatically find pages similar to this page\n// REPORT \u2192 server-side ranked/bundled semantic report (ghost page)\n// KEYWORD \u2192 galactic MiniSearch over live site-index.json files (ghost page)\n// SITES \u2192 which site should this page go on? \u2014 per-domain aggregation of\n// the page-vector scan (ghost page)\n// (other) \u2192 search form mode \u2014 user types a query, results appear\n//\n// Server endpoints required (all same-origin, served by this plugin's server\n// component \u2014 works on any host, including the public farm):\n// GET /system/indexed-domains.json?pattern=glob1,glob2\n// GET /system/semantic-vectors.json?domain=\n// POST /system/embed.json {text}\n// POST /system/search-report.json\n// POST /system/site-report.json\n// GET /system/farm-search.json?q=&pattern=&limit=\n// GET /system/build-index.json?domains=&force=\n\nimport { parseDSL, DEFAULT_LIMIT, isScope, isGlob } from './dsl.js'\nimport { slugify, effectiveSpecs, resolveDomains } from './scope.js'\nimport { loadVectors, getEmbedding, lookupPageVector, cosineScan, loadDomainEntries } from './vectors.js'\nimport { resolveSubject, subjectNote } from './subject.js'\nimport { readCache, writeCache, cacheAge } from './cache.js'\nimport { runBatched } from './batch.js'\nimport { loadAlgorithm, parseAlgorithm, learnedSignals } from './algorithm.js'\nimport { credit, learned, forget, installAmbient, rememberInto } from './learn.js'\nimport { STYLES, siteFlag } from './styles.js'\nimport { install, takePending } from './searchdoor.js'\n\n// \u2500\u2500 similarity item \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst simLink = (domain, slug, title, score) =>\n `<a class=\"sim-link\" data-title=\"${title}\" data-slug=\"${slug}\" data-site=\"${domain}\" href=\"#\">` +\n `${siteFlag(domain, score)} ${title}</a>`\n\nconst shortDate = ms => {\n try { return new Date(ms).toLocaleDateString(undefined, { day: 'numeric', month: 'short' }) } catch { return '' }\n}\n\n// A merged batch result as a page, the same shape search-report renders.\nconst flatPage = (query, merged) => {\n const id = () => Math.random().toString(16).slice(2, 18).padEnd(16, '0')\n const story = [{ type: 'markdown', id: id(),\n text: `Federated search for **${query}** \u2014 ${merged.length} results, searched a batch at a time, nearest sites first.` }]\n const titles = [...new Set(merged.map(r => r.title))]\n if (titles.length) story.push({ type: 'markdown', id: id(), text: titles.map(t => `- [[${t}]]`).join('\\n') })\n story.push({ type: 'markdown', id: id(), text: '# Results' })\n for (const r of merged) {\n story.push({ type: 'reference', id: id(), site: r.site, slug: r.slug, title: r.title,\n text: (r.synopsis || `score ${r.score}`) + (r.via ? ` \u2014 via ${r.via}` : '') })\n }\n return { title: `${query} Federated Search`, story }\n}\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}\" data-mode=\"${mode}\" data-scope=\"${specs.join(',') || ''}\">\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}\" data-mode=\"${mode}\" data-scope=\"${specs.join(',') || ''}\">\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}\" data-mode=\"${mode}\" data-scope=\"${specs.join(',') || ''}\">\n <div class=\"sim-status\">Loading indexed domains (${label})\u2026</div>\n <div class=\"sim-list\"></div>\n </div>`)\n } else if (mode === 'algorithm') {\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\" data-mode=\"${mode}\" data-scope=\"\">\n <div class=\"sim-status\">Reading this search algorithm\u2026</div>\n <div class=\"sim-list\"></div>\n </div>`)\n } else if (mode === 'status') {\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\" data-mode=\"${mode}\" data-scope=\"${specs.join(',') || ''}\">\n <div class=\"sim-status\">Reading the state of the index\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}\" data-mode=\"${mode}\" data-scope=\"${specs.join(',') || ''}\">\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}\" data-mode=\"${mode}\" data-scope=\"${specs.join(',') || ''}\">\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}\" data-mode=\"${mode}\" data-scope=\"${specs.join(',') || ''}\">\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, batch, algorithm: algorithmRef } = parseDSL(item?.text || '')\n const origin = window.location.origin\n const status = div.find('.sim-status')[0]\n installAmbient() // learn.js: neighbourhood + visits, once per page load\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 // An explicit domain spec is taken at its word rather than looked up, so its\n // page count is unknown \u2014 not zero. Saying \"0 pages\" there told a reader\n // their site was unindexed when it was merely unqueried.\n const configSummary = (verb, pages, nDomains) => {\n const scale = pages > 0\n ? `${pages.toLocaleString()} pages across ${nDomains} domains`\n : `${nDomains} domain${nDomains === 1 ? '' : 's'}`\n const parts = [`${verb} \u2014 ${scale}`]\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 === 'status') {\n // The state of the index behind every other item on the page: which model,\n // how much is indexed, when it was last built, and \u2014 the one that decides\n // whether any of this works \u2014 whether a query can be turned into a vector\n // at all. A search page that cannot say this leaves the reader guessing\n // why an answer looks thin.\n const listDiv = div.find('.sim-list')[0]\n\n const ago = iso => {\n if (!iso) return 'unknown'\n const days = (Date.now() - new Date(iso)) / 86400000\n if (days < 1) return 'today'\n if (days < 2) return 'yesterday'\n return `${Math.floor(days)} days ago`\n }\n const day = iso => (iso ? new Date(iso).toISOString().slice(0, 10) : '\u2014')\n // The vector store (0.18.0): what is resident in memory right now, and\n // whether the warm-up has finished \u2014 a federation search costs\n // milliseconds once it has, and a cold parse until it has.\n const storeRow = st => {\n if (!st || !st.warm) return ''\n const w = st.warm\n const mb = Math.round((st.bytes || 0) / 1e6)\n const state = w.state === 'warm'\n ? `warm \u2014 ${w.done.toLocaleString()} sites resident in ${Math.round(w.ms / 1000)} s`\n : w.state === 'warming' ? `warming \u2014 ${w.done.toLocaleString()} of ${w.total.toLocaleString()} sites so far`\n : 'not warmed'\n const capped = w.capped ? ' \u00B7 <strong>cap reached</strong>, some sites re-parse per query' : ''\n return `<tr><th>Vector store</th><td>${(st.pages || 0).toLocaleString()} pages \u00B7 ${mb} MB of ${Math.round((st.capBytes || 0) / 1e6)} MB \u00B7 ${state}${capped}</td></tr>`\n }\n\n ;(async () => {\n try {\n // Whether search works is answered by trying it, not by reading a\n // field. A delegated embedder reports no supervisor state at all, so\n // a proxy pointed at a dead address looked healthy here \u2014 the exact\n // silent failure this panel exists to prevent.\n const probeEmbedder = () =>\n fetch(`${origin}/system/embed.json?text=ping`)\n .then(async r => r.ok\n ? { ok: true }\n : { ok: false, detail: ((await r.json().catch(() => ({}))).error || `status ${r.status}`)\n .replace(/^embedding unavailable: /, '') })\n .catch(e => ({ ok: false, detail: e.message }))\n\n const [health, domains, probe] = await Promise.all([\n fetch(`${origin}/system/similarity-health.json`).then(r => r.json()),\n fetch(`${origin}/system/indexed-domains.json`).then(r => r.ok ? r.json() : []),\n probeEmbedder(),\n ])\n const emb = health.embedder || {}\n const built = domains.map(d => d.built).filter(Boolean).sort()\n const pages = domains.reduce((n, d) => n + (d.page_count || 0), 0)\n\n // \"Up\" means a query can be embedded right now \u2014 proven a moment ago.\n const down = !probe.ok\n const upLine = down\n ? `<strong>Semantic search is down</strong> \u2014 a query cannot be turned into a vector right now (${probe.detail}), so searches on this page will answer by keyword instead.`\n : `Semantic search is <strong>up</strong> \u2014 checked just now by embedding a word.`\n const via = emb.via === 'url'\n ? `delegated to <code>${emb.url}</code> (from ${emb.source || 'config'})`\n : emb.via === 'semindex' ? 'in this process, via the SemIndex plugin'\n : 'in this process, in a supervised child'\n\n status.style.display = 'none'\n listDiv.innerHTML = `<h3>The Index Behind These Answers</h3>\n <p>${upLine}</p>\n <table>\n <tr><th>Model</th><td>${health.model || '\u2014'} \u00B7 ${health.dim || '\u2014'} dimensions</td></tr>\n <tr><th>Embedder</th><td>${via}${down ? ' \u2014 <strong>currently down</strong>' : ''}</td></tr>\n <tr><th>Indexed</th><td>${domains.length.toLocaleString()} domains \u00B7 ${pages.toLocaleString()} pages</td></tr>\n <tr><th>Newest page vectors</th><td>${day(built[built.length - 1])} (${ago(built[built.length - 1])})</td></tr>\n <tr><th>Oldest page vectors</th><td>${day(built[0])} (${ago(built[0])})</td></tr>\n <tr><th>Plugin</th><td>wiki-plugin-similarity ${health.version || '\u2014'}</td></tr>\n ${storeRow(health.store)}\n </table>\n <p class=\"sim-count\">A domain's vectors are only rebuilt when its pages change, so an old\n date means a quiet site, not a broken one. Pages saved since the last build are findable\n by keyword but not yet by meaning.</p>`\n } catch (e) {\n status.textContent = `Index state unavailable: ${e.message}`\n }\n })()\n\n } else if (mode === 'algorithm') {\n // The item IS the reader's search algorithm: show what the ranker will\n // read from it, and what the browser has learned so far (algorithm.js).\n // Rendered a tick later, like every other mode: a synchronous render\n // here was wiped by a second emit wiki-client runs on the item at load.\n setTimeout(() => {\n const listDiv = div.find('.sim-list')[0]\n try {\n const a = parseAlgorithm(item.text || '')\n const learned = learnedSignals()\n const top = Object.entries(learned).sort((x, y) => y[1] - x[1]).slice(0, 8)\n status.style.display = 'none'\n listDiv.innerHTML = `<h3>This Search Algorithm</h3>\n <table>\n ${Object.entries(a.weights).map(([k, v]) => `<tr><th>weight ${k}</th><td>${v}</td></tr>`).join('')}\n <tr><th>always</th><td>${a.always.length ? a.always.join(', ') : '\u2014'}</td></tr>\n <tr><th>never</th><td>${a.never.length ? a.never.join(', ') : '\u2014'}</td></tr>\n <tr><th>batch</th><td>${a.batch || 50}</td></tr>\n </table>\n <p class=\"sim-count\">Learned in this browser: ${top.length\n ? top.map(([d, v]) => `${d} (${v.toFixed(2)})`).join(', ')\n : 'nothing yet \u2014 sites you are near, open and click will appear here'}</p>\n <p class=\"sim-count\"><button class=\"sim-remember\">Remember</button> <button class=\"sim-forget\">Forget</button>\n <span class=\"sim-remember-note\"></span></p>`\n const note = listDiv.querySelector('.sim-remember-note')\n listDiv.querySelector('.sim-remember').addEventListener('click', async () => {\n try {\n const n = await rememberInto(div.parents('.page'))\n note.textContent = `${n} sites written into \"Sites I visit\" \u2014 read the roster above, delete any line.`\n } catch (e) { note.textContent = `Could not remember here: ${e.message}` }\n })\n listDiv.querySelector('.sim-forget').addEventListener('click', () => {\n forget(); note.textContent = 'Forgotten \u2014 the browser starts learning again from here.'\n })\n } catch (e) {\n status.textContent = `Algorithm unreadable: ${e.message}`\n }\n }, 0)\n\n } else 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 // A batched item searches by the Site Index, not this farm's vector\n // listing \u2014 so say how far the index reaches, not how many domains\n // this host happens to hold (0 on a farm with no galaxy tree).\n const isBatched = batch != null ? batch > 0 : eff.some(sp => sp.toUpperCase() === 'GALAXY')\n if (isBatched) {\n try {\n const health = await fetch(`${origin}/system/similarity-health.json`).then(r => r.json())\n const si = health.siteIndex || {}\n const named = eff.filter(sp => /\\./.test(sp) && !/[*?]/.test(sp) &&\n !['PUBLIC', 'LOCAL', 'PRIVATE', 'GALAXY'].includes(sp.toUpperCase()))\n const reach = si.count\n ? `ranks ${si.count.toLocaleString()} sites from the Site Index${si.via === 'peer' ? ' (a peer\\'s copy)' : ''}`\n : 'no Site Index on this farm \u2014 batches ask its peers'\n const parts = [`Ready \u2014 ${reach}`]\n if (named.length) parts.push(`${named.length} named site${named.length > 1 ? 's' : ''} searched wherever held`)\n if (thresholdSet) parts.push(`threshold ${threshold}`)\n parts.push(`limit ${limit}`)\n readyLine = parts.join(' \u00B7 ')\n } catch { /* keep the domain summary */ }\n }\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 // The embedder and the keyword index share nothing: one can be down while\n // the other is perfectly healthy. A report that cannot embed its query\n // should say so and answer with words, not leave an error and no results.\n // 502/503 are what the server sends when embedding is unavailable \u2014 a\n // missing local embedder, or the circuit breaker open after crashes.\n const EMBEDDER_DOWN = new Set([502, 503])\n\n const keywordInstead = async (query, eff) => {\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(`keyword search also failed: ${res.status}`)\n const page = await res.json()\n // The server titles this page \"\u2026 Keyword Search\", so the reader can see\n // which tier answered without being told twice.\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n }\n\n // Incremental federated search (batch.js): on by default for a scope\n // that reaches the galaxy, off for a farm-only scope, BATCH n/off decides.\n const batched = eff => batch != null ? batch > 0\n : eff.some(sp => sp.toUpperCase() === 'GALAXY')\n const results = div.find('.sim-results')[0]\n\n const doBatched = async (query, eff, seeded) => {\n const algorithm = await loadAlgorithm(algorithmRef, origin)\n const rosterDomains = eff.filter(sp => !isScope(sp) && sp !== '*' && !isGlob(sp))\n let ghost = null\n const openAsPage = merged => {\n const page = flatPage(query, merged)\n if (ghost) ghost.remove()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n ghost = div.parents('.page').next('.page')\n }\n const render = (merged, state) => {\n const shown = merged.slice(0, limit)\n const head = state.running\n ? `Searched ${state.searched.toLocaleString()} of ${state.total.toLocaleString()} sites \u2014 ` +\n `${merged.length} results \u2014 searching\u2026`\n : state.converged\n ? `Searched ${state.searched.toLocaleString()} of ${state.total.toLocaleString()} sites \u2014 ` +\n `${merged.length} results \u2014 the top ${limit} stopped changing`\n : `Searched ${state.searched.toLocaleString()} of ${state.total.toLocaleString()} sites \u2014 ` +\n `${merged.length} results`\n const rest = state.total - state.searched\n // Which indexer answered, and how old its index is (Mini Indexer\n // Plan, Phase 4): the batches say who held each site, the Site Index\n // says when each site was last indexed and which indexer placed it.\n const answered = Object.entries(state.answered || {})\n .filter(([, n]) => n > 0)\n .map(([h, n]) => `${h === 'local' ? 'this farm' : h} (${n.toLocaleString()} site${n === 1 ? '' : 's'})`)\n const built = state.siteIndex?.builtAt ? ` \u00B7 site index built ${shortDate(state.siteIndex.builtAt)}` : ''\n const tier = `<small class=\"sim-tier\">semantic \u00B7 nearest sites first${answered.length ? ` \u00B7 answered by ${answered.join(', ')}` : ''}${built}</small>`\n const provenance = r => {\n const info = state.siteInfo?.get(r.site)\n const bits = []\n if (r.via) bits.push(`via ${r.via}`)\n if (info?.indexedAt) bits.push(`indexed ${shortDate(info.indexedAt * 1000)}${info.placedBy ? ` by ${info.placedBy}` : ''}`)\n return bits.length ? ` <small class=\"sim-src\">${bits.join(' \u00B7 ')}</small>` : ''\n }\n results.innerHTML = `<h3>${head}</h3>${tier}<ul>${\n shown.map(r => `<li>${simLink(r.site, r.slug, r.title, r.semantic)}` + provenance(r) +\n (r.siblings?.length ? ` <small>+${r.siblings.length}</small>` : '') +\n (r.movedFrom ? ` <small>moved here from ${r.movedFrom}</small>` : '') +\n (r.gone ? ` <small>site ${r.gone}${r.movedTo ? `, probably now ${r.movedTo}` : ''}</small>` : '') + '</li>').join('')\n }</ul><p class=\"sim-count sim-batch-controls\">` +\n (state.running ? `<button class=\"sim-stop\">Stop</button>` : '') +\n (!state.running && rest > 0 ? `<button class=\"sim-more\">Search the remaining ${rest.toLocaleString()} sites</button>` : '') +\n (!state.running && merged.length ? `<button class=\"sim-open\">Open as page</button>` : '') +\n (state.unindexed?.length && !state.running\n ? `<br><small>Sites that look relevant but carry no page vectors yet: ${state.unindexed.slice(0, 8).join(', ')}</small>` : '') +\n `</p>`\n results.querySelector('.sim-stop')?.addEventListener('click', () => state.stop())\n results.querySelector('.sim-more')?.addEventListener('click', () => state.continueAll())\n results.querySelector('.sim-open')?.addEventListener('click', () => openAsPage(merged))\n results.querySelectorAll('.sim-link').forEach(a => a.addEventListener('click', e => {\n e.preventDefault()\n const { site, slug, title } = a.dataset\n credit(site, 'clicked')\n window.wiki.pageHandler.context = { site: window.location.hostname, slug: 'search-tool' }\n window.wiki.doInternalLink(title, div.parents('.page'), site)\n }))\n }\n status.textContent = ''\n const out = await runBatched({\n origin, query, seeded, specs: eff.length ? eff : ['GALAXY'], limit, threshold, thresholdSet,\n batch: batch || 50, roster: rosterDomains, algorithm, render,\n status: t => { status.textContent = t },\n })\n status.textContent = readyLine\n return out\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 if (batched(eff)) {\n const seeded = subject && query === subject.title ? seedParams() : {}\n await doBatched(query, eff, seeded)\n return\n }\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 (EMBEDDER_DOWN.has(res.status)) {\n const body = await res.json().catch(() => ({}))\n const why = (body.error || `status ${res.status}`).replace(/^embedding unavailable: /, '')\n status.textContent = 'Semantic search unavailable \u2014 searching by keyword instead\u2026'\n await keywordInstead(query, eff)\n status.textContent = `Semantic search unavailable (${why}) \u2014 these are keyword matches, ranked by word not meaning.`\n return\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 // A query typed into the wiki's search box and sent here by the search\n // door. Only the farm-wide report takes it: that is the scope the box\n // used to search, and picking any nearer one would quietly narrow it.\n if (mode === 'report' && specs.join(',') === '*') {\n const handed = takePending()\n if (handed) { input.value = handed; doReport() }\n }\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 // editor: declared so wiki-client's preLoadEditors fetches this plugin on\n // every page, which is what puts the search door (below) on pages that carry\n // no similarity item. It hands straight back to the standard text editor, so\n // creating and editing an item behaves exactly as it did before.\n window.plugins.similarity = {\n emit,\n bind,\n editor: ($item, item) => window.wiki.textEditor($item, item),\n }\n install()\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["SIMILAR_THRESHOLDS", "DEFAULT_THRESHOLD", "DEFAULT_LIMIT", "parseDSL", "text", "specs", "rosterRefs", "farms", "threshold", "limit", "mode", "live", "subject", "force", "ghostUrl", "label", "batch", "algorithm", "isCmd", "upper", "kw", "val", "line", "raw", "b", "ref", "peer", "level", "tv", "isGlob", "spec", "isScope", "slugify", "title", "domainCache", "resolveDomainsForSpec", "spec", "origin", "url", "res", "list", "resolveDomains", "specs", "seen", "result", "isGlob", "isScope", "item", "SITE_LINE", "ROSTER_LINE", "REFS_LINE", "fetchPage", "ref", "i", "resolveRoster", "out", "visited", "page", "e", "raw", "line", "site", "nested", "refs", "refPage", "it", "resolveRosters", "rosterRefs", "effectiveSpecs", "here", "expanded", "s", "vectorCache", "vectorUrl", "domain", "loadVectors", "res", "data", "getEmbedding", "text", "origin", "lookupPageVector", "slug", "entry", "p", "cosineScan", "queryVec", "domainEntries", "threshold", "limit", "excludeSlug", "excludeDomain", "results", "pages", "title", "vector", "dot", "i", "a", "b", "loadDomainEntries", "specs", "domains", "resolveDomains", "e", "resolveSubject", "div", "$self", "$host", "$page", "slug", "site", "data", "title", "text", "i", "subjectNote", "s", "cacheKey", "id", "readCache", "item", "c", "writeCache", "data", "cacheAge", "ts", "s", "neighbourhoodDomains", "out", "site", "p", "post", "url", "body", "res", "err", "sameTop", "a", "b", "limit", "ka", "r", "kb", "runBatched", "opts", "origin", "query", "seeded", "specs", "batch", "algorithm", "render", "say", "t", "prefer", "rank", "batches", "ranked", "d", "explicit", "sp", "state", "n", "merged", "results", "quiet", "stop", "runBatch", "domains", "flat", "key", "prev", "peerDomains", "next", "unchanged", "remaining", "loop", "all", "LEARNED_KEY", "WEIGHT_NAMES", "SITE", "parseAlgorithm", "text", "out", "raw", "line", "word", "rest", "kw", "name", "v", "learnedSignals", "d", "score", "refToUrl", "ref", "origin", "i", "slug", "rosterSites", "page", "item", "cache", "loadAlgorithm", "url", "algo", "res", "CREDIT", "HALF_LIFE_MS", "MAX_SITES", "load", "LEARNED_KEY", "save", "sites", "decayed", "entry", "now", "weeks", "credit", "domain", "kind", "d", "cur", "score", "keys", "a", "b", "learned", "e", "s", "forget", "installed", "siteOf", "el", "installAmbient", "seen", "p", "site", "main", "muts", "m", "node", "rememberInto", "$page", "limit", "resolve", "reject", "item", "i", "top", "text", "updated", "$item", "STYLES", "siteFlag", "domain", "score", "SEARCH_TOOL", "SLUG", "available", "checkAvailable", "r", "pageOf", "target", "$page", "openSearchTool", "keepLineup", "install", "e", "link", "input", "query", "setPending", "pending", "q", "takePending", "simLink", "domain", "slug", "title", "score", "siteFlag", "shortDate", "ms", "flatPage", "query", "merged", "id", "story", "titles", "r", "t", "emit", "div", "item", "mode", "specs", "threshold", "limit", "force", "ghostUrl", "label", "parseDSL", "STYLES", "btnLabel", "hint", "bind", "rosterRefs", "farms", "live", "subjectFlag", "thresholdSet", "batch", "algorithmRef", "origin", "status", "installAmbient", "subject", "resolveSubject", "cache", "readCache", "specsP", "effectiveSpecs", "seedParams", "configSummary", "verb", "pages", "nDomains", "scale", "parts", "e", "$a", "$page", "scopeLabel", "cacheNote", "ts", "cacheAge", "listDiv", "ago", "iso", "days", "day", "storeRow", "st", "w", "mb", "state", "capped", "probeEmbedder", "health", "domains", "probe", "emb", "built", "d", "n", "down", "upLine", "via", "a", "parseAlgorithm", "learned", "learnedSignals", "top", "x", "y", "k", "v", "note", "rememberInto", "forget", "renderList", "totalPages", "page_count", "eff", "patterns", "url", "res", "writeCache", "results", "renderScored", "scored", "s", "pageTitle", "slugify", "domainEntries", "loadDomainEntries", "total", "subjectNote", "qVec", "lookupPageVector", "pageText", "_", "el", "getEmbedding", "cosineScan", "btn", "doGhost", "page", "doBuild", "body", "input", "readyLine", "resolveDomains", "sp", "si", "named", "EMBEDDER_DOWN", "keywordInstead", "pattern", "farmsParam", "batched", "doBatched", "seeded", "algorithm", "loadAlgorithm", "rosterDomains", "isScope", "isGlob", "ghost", "openAsPage", "render", "shown", "head", "rest", "answered", "h", "tier", "provenance", "info", "bits", "site", "credit", "out", "runBatched", "doReport", "why", "pageObj", "handed", "takePending", "doSites", "doKeyword", "doAuthor", "seenTitles", "uniqueTitles", "hexId", "primaryLines", "doSearch", "$item", "install"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -126,10 +126,20 @@
|
|
|
126
126
|
"id": "e2cde2572a99224e",
|
|
127
127
|
"text": "**0.21.0** \u2014 the cascade beyond the first peer (Mini Indexer Plan, Phase 4). A flat report's missing domains walk the farm's peers **in order**, each asked only for what is still missing; a peer's answer says which domains it `held`, so the walk moves on with the rest, two hops at most and never back to a host already in the chain. The Cafe names only the Pi5; the Pi5 names the mini after itself; a page only the mini indexed is answered from any Cafe site, marked `via` the host that had it. The result page names who answered \u2014 this farm and each peer, by sites \u2014 when each result's site was last indexed and which indexer placed it, and how old the Site Index is."
|
|
128
128
|
},
|
|
129
|
+
{
|
|
130
|
+
"type": "markdown",
|
|
131
|
+
"id": "6c51f2131b0e7623",
|
|
132
|
+
"text": "**0.22.0** \u2014 staleness (Dead Sites Plan, Phase 6). The graveyard's verdicts now include *stale* (a year without an edit, or a third of a site's references dead) and *abandoned* (three years), read from the same sitemap. `site-rank` sinks them by `WEIGHT stale` (default 0.2, abandoned twice as far as stale); a [[Search Algorithm]] with `WEIGHT stale 0` searches old sites on equal terms, and below 0 prefers them. Neither class is gone: the sites are searched, only later."
|
|
133
|
+
},
|
|
129
134
|
{
|
|
130
135
|
"type": "markdown",
|
|
131
136
|
"id": "ac44fae5937e27eb",
|
|
132
137
|
"text": "**0.21.1** \u2014 three fixes from the first day of the second peer. A farm's copy of a peer's Site Index is checked **hourly** by conditional GET, and always once at startup, so a restart never serves a copy the peer has since rebuilt \u2014 it was six hours, and a 304 reset the clock, so a new indexer's sites could take most of a day to reach the Cafe. A domain named explicitly in a batched item is **searched whether or not the Site Index knows it**, as one more batch, since the cascade finds the peer that holds it. And a batched item's ready line reports how far the Site Index reaches rather than how many domains this host holds, which on a farm with no galaxy tree read as a misleading zero."
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"type": "markdown",
|
|
141
|
+
"id": "99d1f2910f05b38d",
|
|
142
|
+
"text": "**0.21.2** \u2014 a site answered two hops away was counted twice in the *answered by* line, once under the peer that relayed it and once under the peer that held it. The relay's count now excludes what its own peers answered."
|
|
133
143
|
}
|
|
134
144
|
],
|
|
135
145
|
"plugin": "similarity",
|
|
@@ -490,6 +500,28 @@
|
|
|
490
500
|
},
|
|
491
501
|
"after": "e2cde2572a99224e",
|
|
492
502
|
"date": 1788551290021
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
"type": "add",
|
|
506
|
+
"id": "99d1f2910f05b38d",
|
|
507
|
+
"item": {
|
|
508
|
+
"type": "markdown",
|
|
509
|
+
"id": "99d1f2910f05b38d",
|
|
510
|
+
"text": "**0.21.2** \u2014 a site answered two hops away was counted twice in the *answered by* line, once under the peer that relayed it and once under the peer that held it. The relay's count now excludes what its own peers answered."
|
|
511
|
+
},
|
|
512
|
+
"after": "ac44fae5937e27eb",
|
|
513
|
+
"date": 1788552261437
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
"type": "add",
|
|
517
|
+
"id": "6c51f2131b0e7623",
|
|
518
|
+
"item": {
|
|
519
|
+
"type": "markdown",
|
|
520
|
+
"id": "6c51f2131b0e7623",
|
|
521
|
+
"text": "**0.22.0** \u2014 staleness (Dead Sites Plan, Phase 6). The graveyard's verdicts now include *stale* (a year without an edit, or a third of a site's references dead) and *abandoned* (three years), read from the same sitemap. `site-rank` sinks them by `WEIGHT stale` (default 0.2, abandoned twice as far as stale); a [[Search Algorithm]] with `WEIGHT stale 0` searches old sites on equal terms, and below 0 prefers them. Neither class is gone: the sites are searched, only later."
|
|
522
|
+
},
|
|
523
|
+
"after": "e2cde2572a99224e",
|
|
524
|
+
"date": 1788558698739
|
|
493
525
|
}
|
|
494
526
|
]
|
|
495
527
|
}
|
package/pages/search-algorithm
CHANGED
|
@@ -44,12 +44,12 @@
|
|
|
44
44
|
{
|
|
45
45
|
"type": "similarity",
|
|
46
46
|
"id": "8b616e19c4ff3973",
|
|
47
|
-
"text": "ALGORITHM\nWEIGHT liked 1.0\nWEIGHT visited 0.6\nWEIGHT neighbourhood 0.4\nWEIGHT followed 0.2\nWEIGHT centroid 1.0\nWEIGHT fresh 0.2\nWEIGHT reliable 0.3\nTRUST flaky\nBATCH 50"
|
|
47
|
+
"text": "ALGORITHM\nWEIGHT liked 1.0\nWEIGHT visited 0.6\nWEIGHT neighbourhood 0.4\nWEIGHT followed 0.2\nWEIGHT centroid 1.0\nWEIGHT fresh 0.2\nWEIGHT reliable 0.3\nWEIGHT stale 0.2\nTRUST flaky\nBATCH 50"
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"type": "markdown",
|
|
51
51
|
"id": "c7a6381efe13e028",
|
|
52
|
-
"text": "`WEIGHT` says how much each signal moves a site: **liked** for the roster above, **visited** for what the browser learned, **neighbourhood** for sites already in your lineup, **followed** for the federation's followed tier, **centroid** for how close a site is to the query, **fresh** for how recent the index holding it is. **reliable** for how far a site the [https://dead.fedwiki.club/view/welcome-visitors graveyard] calls unreliable or flaky sinks. `TRUST solid` skips shaky sites, `TRUST flaky` searches them later (the default), `TRUST any` searches even the dead and the moved. `ALWAYS site` pins a site into the first batch; `NEVER site` prunes it from every batch; `BATCH n` sets how many sites each request searches. Words the ranker does not know are ignored, so this vocabulary can grow."
|
|
52
|
+
"text": "`WEIGHT` says how much each signal moves a site: **liked** for the roster above, **visited** for what the browser learned, **neighbourhood** for sites already in your lineup, **followed** for the federation's followed tier, **centroid** for how close a site is to the query, **fresh** for how recent the index holding it is. **reliable** for how far a site the [https://dead.fedwiki.club/view/welcome-visitors graveyard] calls unreliable or flaky sinks, **stale** for how far a live site nobody tends sinks — the graveyard calls a site stale after a year without an edit, or when a third of its references are dead, and abandoned after three years; set `WEIGHT stale 0` to dig in the old ones on equal terms, or below 0 to prefer them. `TRUST solid` skips shaky sites, `TRUST flaky` searches them later (the default), `TRUST any` searches even the dead and the moved. `ALWAYS site` pins a site into the first batch; `NEVER site` prunes it from every batch; `BATCH n` sets how many sites each request searches. Words the ranker does not know are ignored, so this vocabulary can grow."
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"type": "markdown",
|
|
@@ -149,7 +149,7 @@
|
|
|
149
149
|
"item": {
|
|
150
150
|
"type": "similarity",
|
|
151
151
|
"id": "8b616e19c4ff3973",
|
|
152
|
-
"text": "ALGORITHM\nWEIGHT liked 1.0\nWEIGHT visited 0.6\nWEIGHT neighbourhood 0.4\nWEIGHT followed 0.2\nWEIGHT centroid 1.0\nWEIGHT fresh 0.2\nWEIGHT reliable 0.3\nTRUST flaky\nBATCH 50"
|
|
152
|
+
"text": "ALGORITHM\nWEIGHT liked 1.0\nWEIGHT visited 0.6\nWEIGHT neighbourhood 0.4\nWEIGHT followed 0.2\nWEIGHT centroid 1.0\nWEIGHT fresh 0.2\nWEIGHT reliable 0.3\nWEIGHT stale 0.2\nTRUST flaky\nBATCH 50"
|
|
153
153
|
},
|
|
154
154
|
"date": 1788517953986
|
|
155
155
|
},
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
"item": {
|
|
160
160
|
"type": "markdown",
|
|
161
161
|
"id": "c7a6381efe13e028",
|
|
162
|
-
"text": "`WEIGHT` says how much each signal moves a site: **liked** for the roster above, **visited** for what the browser learned, **neighbourhood** for sites already in your lineup, **followed** for the federation's followed tier, **centroid** for how close a site is to the query, **fresh** for how recent the index holding it is. **reliable** for how far a site the [https://dead.fedwiki.club/view/welcome-visitors graveyard] calls unreliable or flaky sinks. `TRUST solid` skips shaky sites, `TRUST flaky` searches them later (the default), `TRUST any` searches even the dead and the moved. `ALWAYS site` pins a site into the first batch; `NEVER site` prunes it from every batch; `BATCH n` sets how many sites each request searches. Words the ranker does not know are ignored, so this vocabulary can grow."
|
|
162
|
+
"text": "`WEIGHT` says how much each signal moves a site: **liked** for the roster above, **visited** for what the browser learned, **neighbourhood** for sites already in your lineup, **followed** for the federation's followed tier, **centroid** for how close a site is to the query, **fresh** for how recent the index holding it is. **reliable** for how far a site the [https://dead.fedwiki.club/view/welcome-visitors graveyard] calls unreliable or flaky sinks, **stale** for how far a live site nobody tends sinks — the graveyard calls a site stale after a year without an edit, or when a third of its references are dead, and abandoned after three years; set `WEIGHT stale 0` to dig in the old ones on equal terms, or below 0 to prefer them. `TRUST solid` skips shaky sites, `TRUST flaky` searches them later (the default), `TRUST any` searches even the dead and the moved. `ALWAYS site` pins a site into the first batch; `NEVER site` prunes it from every batch; `BATCH n` sets how many sites each request searches. Words the ranker does not know are ignored, so this vocabulary can grow."
|
|
163
163
|
},
|
|
164
164
|
"date": 1788517953986
|
|
165
165
|
}
|
package/server/cascade.js
CHANGED
|
@@ -42,9 +42,15 @@ const askCascade = async ({ peers, missing, body, hops = 0, via = [], post, time
|
|
|
42
42
|
continue
|
|
43
43
|
}
|
|
44
44
|
for (const r of ans.body.results) out.results.push({ ...r, via: r.via || host })
|
|
45
|
+
// A peer's stats include what its own peers answered; count those
|
|
46
|
+
// under the host that really held them, not twice.
|
|
45
47
|
const s = ans.body.stats || {}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
const nested = ans.body.peers || []
|
|
49
|
+
const nestedDomains = nested.reduce((n, p) => n + (p.domains || 0), 0)
|
|
50
|
+
const nestedPages = nested.reduce((n, p) => n + (p.pages || 0), 0)
|
|
51
|
+
out.peers.push({ host, domains: Math.max(0, (s.domains || 0) - nestedDomains),
|
|
52
|
+
pages: Math.max(0, (s.pages || 0) - nestedPages) })
|
|
53
|
+
for (const p of nested) out.peers.push(p) // what it asked in turn
|
|
48
54
|
// A peer that says what it held lets the walk go on with the rest; an
|
|
49
55
|
// older peer that does not is taken to have answered for everything.
|
|
50
56
|
const held = Array.isArray(ans.body.held) ? new Set(ans.body.held) : null
|
package/server/site-index.js
CHANGED
|
@@ -158,7 +158,7 @@ const localSites = (farms, restricted, exclude) => {
|
|
|
158
158
|
|
|
159
159
|
const VERDICT_TTL_MS = parseInt(process.env.WIKI_VERDICT_TTL_MS || String(24 * 3600 * 1000), 10)
|
|
160
160
|
const verdictCopy = () => path.join(cacheDir(), 'verdicts-sitemap.json')
|
|
161
|
-
const VERDICT_RE = /^(\S+) (?:is|has) (dead|moved|lapsed|unreliable|flaky)\b(.*)$/i
|
|
161
|
+
const VERDICT_RE = /^(\S+) (?:is|has) (dead|moved|lapsed|unreliable|flaky|stale|abandoned)\b(.*)$/i
|
|
162
162
|
const TARGET_RE = /(?:probably moved to|now answers from) ([a-z0-9.-]+(?::\d+)?)/i
|
|
163
163
|
|
|
164
164
|
const parseVerdicts = sitemap => {
|
package/server/site-rank.js
CHANGED
|
@@ -20,13 +20,16 @@
|
|
|
20
20
|
|
|
21
21
|
const DEFAULT_WEIGHTS = {
|
|
22
22
|
liked: 1.0, visited: 0.6, neighbourhood: 0.4, followed: 0.2,
|
|
23
|
-
centroid: 1.0, fresh: 0.2, reliable: 0.3,
|
|
23
|
+
centroid: 1.0, fresh: 0.2, reliable: 0.3, stale: 0.2,
|
|
24
24
|
}
|
|
25
25
|
// How a verdict from the Semantic Site Graveyard moves a site (Dead Sites
|
|
26
26
|
// Plan): dead, lapsed and moved sites leave the list — a moved site is
|
|
27
27
|
// replaced by where it went when that is known — unless the reader's
|
|
28
28
|
// algorithm says TRUST any; unreliable and flaky sink by `reliable`.
|
|
29
29
|
const VERDICT_PENALTY = { unreliable: 1.0, flaky: 0.5 }
|
|
30
|
+
// Staleness (Dead Sites Plan, Phase 6): a live site nobody tends sinks by
|
|
31
|
+
// WEIGHT stale; a reader who wants the old ones sets it to 0 or below.
|
|
32
|
+
const STALE_PENALTY = { stale: 0.5, abandoned: 1.0 }
|
|
30
33
|
const KIND_FRESHNESS = { local: 1.0, public: 1.0, own: 1.0, farm: 0.8, peer: 0.6, galaxy: 0.4 }
|
|
31
34
|
|
|
32
35
|
const dot = (a, b) => {
|
|
@@ -97,8 +100,9 @@ const rankSites = (qvec, index, local, prefer = {}, algorithm = {}, verdicts = {
|
|
|
97
100
|
if (learned[d] > 0) { prefScore += w.visited * Math.min(1, learned[d]); reasons.push('learned') }
|
|
98
101
|
const v = verdicts[d]
|
|
99
102
|
const penalty = v ? (VERDICT_PENALTY[v.class] || 0) : 0
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
const stale = v ? (STALE_PENALTY[v.class] || 0) : 0
|
|
104
|
+
if (penalty || stale) reasons.push(v.class)
|
|
105
|
+
const score = prefScore + w.centroid * centroid + w.fresh * r.fresh - w.reliable * penalty - w.stale * stale
|
|
102
106
|
out.push({
|
|
103
107
|
domain: r.domain, kind: r.kind, method: m.method || 'pages', tier: m.tier || '',
|
|
104
108
|
pages: m.pages || 0, indexedAt: m.indexedAt || 0, source: m.source || r.kind,
|
|
@@ -130,4 +134,4 @@ const batches = (ranked, size = 50) => {
|
|
|
130
134
|
return out
|
|
131
135
|
}
|
|
132
136
|
|
|
133
|
-
module.exports = { rankSites, batches, DEFAULT_WEIGHTS, KIND_FRESHNESS, VERDICT_PENALTY }
|
|
137
|
+
module.exports = { rankSites, batches, DEFAULT_WEIGHTS, KIND_FRESHNESS, VERDICT_PENALTY, STALE_PENALTY }
|