wiki-plugin-similarity 0.15.0 → 0.16.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 +43 -26
- package/client/similarity.js.map +3 -3
- package/factory.json +1 -0
- package/package.json +1 -1
- package/pages/about-similarity-plugin +7 -2
- package/pages/search-tool +168 -0
- package/server/server.js +22 -6
package/client/similarity.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/* wiki-plugin-similarity - 0.
|
|
2
|
-
(()=>{var _={high:.78,medium:.68,low:.58},q=_.medium,Y=10,B=t=>{let e=[],s=[],
|
|
3
|
-
`)){let
|
|
4
|
-
`).slice(0,2e3);return{slug:
|
|
1
|
+
/* wiki-plugin-similarity - 0.16.0 - Thu, 03 Sep 2026 22:00:17 GMT */
|
|
2
|
+
(()=>{var _={high:.78,medium:.68,low:.58},q=_.medium,Y=10,B=t=>{let e=[],s=[],i=[],h=null,b=null,m="search",x=!1,C=!1,v=!1,T=null,R=null,S=(a,c)=>a===c||a.startsWith(c)&&/^[\s:]/.test(a.slice(c.length)),w=(a,c)=>a.slice(c.length).replace(/^\s*:?\s*/,"").trim();for(let a of t.split(`
|
|
3
|
+
`)){let c=a.trim();if(!c||c.startsWith("#"))continue;let g=c.toUpperCase();if(S(g,"LIVE")){x=!0;continue}if(S(g,"SUBJECT")){C=!0;continue}if(S(g,"AUTHOR")){!e.length&&m==="search"&&(m="author");continue}if(S(g,"REPORT")){m==="search"&&(m="report");continue}if(S(g,"KEYWORD")){m==="search"&&(m="keyword");continue}if(S(g,"SITES")){m==="search"&&(m="sites");continue}if(S(g,"BUILD")){m==="search"&&(m="build");continue}if(S(g,"FORCE")){v=!0;continue}if(S(g,"GHOST")){T=w(c,"GHOST"),m==="search"&&(m="ghost");continue}if(S(g,"BUTTON")){R=w(c,"BUTTON");continue}if(S(g,"ROSTER")){let k=w(c,"ROSTER");k&&s.push(k);continue}if(S(g,"FARM")){let k=w(c,"FARM");k&&i.push(k);continue}if(S(g,"LIST")){!e.length&&m==="search"&&(m="list");continue}if(S(g,"STATUS")){m==="search"&&(m="status");continue}if(S(g,"SIMILAR")){let k=w(g,"SIMILAR").toLowerCase();h=_[k]||q,!e.length&&m==="search"&&(m="similar");continue}if(S(g,"THRESHOLD")){let k=w(c,"THRESHOLD");h=_[k.toLowerCase()]??(parseFloat(k)||q);continue}if(S(g,"LIMIT")){b=parseInt(w(c,"LIMIT"))||Y;continue}e.push(["PUBLIC","LOCAL","PRIVATE","GALAXY","SITE"].includes(g)?g:c)}return{mode:m,specs:e,rosterRefs:s,farms:i,threshold:h??q,limit:b??Y,live:x,subject:C,force:v,ghostUrl:T,label:R,thresholdSet:h!==null}},Z=t=>t.includes("*")||t.includes("?"),X=t=>t==="PUBLIC"||t==="LOCAL"||t==="PRIVATE"||t==="GALAXY";var tt=t=>t.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,""),G=new Map,ht=async(t,e)=>{if(G.has(t))return G.get(t);let s=`${e}/system/indexed-domains.json?pattern=${encodeURIComponent(t)}`,i=await fetch(s);if(!i.ok)throw new Error(`indexed-domains failed: ${i.status}`);let h=await i.json();return G.set(t,h),h},A=async(t,e)=>{t.length||(t=[window.location.hostname]);let s=new Set,i=[];for(let h of t)if(h==="*"||Z(h)||X(h))for(let b of await ht(h,e))s.has(b.domain)||(s.add(b.domain),i.push(b));else s.has(h)||(s.add(h),i.push({domain:h,page_count:null}));return i},pt=/^([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+|localhost)(:\d+)?$/,gt=/^ROSTER ([A-Za-z0-9.\-:]+\/[a-z0-9-]+)$/,wt=/^REFERENCES ([A-Za-z0-9.\-:]+\/[a-z0-9-]+)$/,Q=async t=>{let e=t.indexOf("/"),s=await fetch(`//${t.slice(0,e)}/${t.slice(e+1)}.json`);if(!s.ok)throw new Error(`roster page ${t} failed: ${s.status}`);return s.json()},et=async(t,e,s)=>{if(s.has(t))return;s.add(t);let i;try{i=await Q(t)}catch(h){console.warn(h.message);return}for(let h of i.story||[])if(h.type==="roster")for(let b of(h.text||"").split(/\r?\n/)){let m=b.trim();if(!m)continue;let x=m.match(pt);if(x){e.add(x[0]);continue}let C=m.match(gt);if(C){await et(C[1],e,s);continue}let v=m.match(wt);if(v)try{let T=await Q(v[1]);for(let R of T.story||[])R.type==="reference"&&R.site&&e.add(R.site)}catch(T){console.warn(T.message)}}},yt=async t=>{let e=new Set,s=new Set;for(let i of t)await et(i,e,s);return[...e]},st=async(t,e)=>{let s=typeof window<"u"&&window.location?window.location.hostname:null,i=t.map(h=>h.toUpperCase()==="SITE"&&s?s:h);return e.length?[...i,...await yt(e)]:i};var V=new Map,$t=t=>`${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(t)}`,nt=async t=>{if(V.has(t))return V.get(t);let e=await fetch($t(t));if(!e.ok)return[];let s=await e.json();return V.set(t,s),s},H=async(t,e)=>{let s=await fetch(`${e}/system/embed.json`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text:t})});if(!s.ok)throw new Error(`embed failed: ${s.status}`);return(await s.json()).vector},it=async(t,e)=>{let i=(await nt(e)).find(h=>h.slug===t);return i?i.vector:null},N=(t,e,{threshold:s,limit:i,excludeSlug:h,excludeDomain:b})=>{let m=[];for(let{domain:x,pages:C}of e)for(let{slug:v,title:T,vector:R}of C){if(v===h&&x===b)continue;let S=0;for(let w=0;w<t.length;w++)S+=t[w]*R[w];S>=s&&m.push({domain:x,slug:v,title:T,score:S})}return m.sort((x,C)=>C.score-x.score),m.slice(0,i)},z=async(t,e)=>{let s=await A(t,e);return(await Promise.all(s.map(async({domain:h})=>({domain:h,pages:await nt(h)})))).filter(h=>h.pages.length>0)};var ot=t=>{let e=t.parents(".page:first"),s=e.prev(".page"),i=s.length?s:e,h=(i.attr("id")||"").split("_rev")[0],b=i.data("site")||window.location.hostname,m=i.data("data")||{},x=m.title||i.find(".title").text().trim()||h,C=[x,...(m.story||[]).filter(v=>v.type==="markdown"||v.type==="paragraph").map(v=>(v.text||"").trim()).filter(Boolean)].join(`
|
|
4
|
+
`).slice(0,2e3);return{slug:h,site:b,title:x,text:C,isSelf:!s.length}},D=t=>`Subject: ${t.title}${t.isSelf?" (this page)":` @ ${t.site}`}`;var at=t=>`sim-cache-${t}`,rt=t=>{try{let e=JSON.parse(localStorage.getItem(at(t.id))||"null");return e?.text===(t.text||"")?e:null}catch{return null}},U=(t,e)=>{try{localStorage.setItem(at(t.id),JSON.stringify({text:t.text||"",ts:Date.now(),...e}))}catch{}},lt=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 O=`
|
|
5
5
|
.sim-form { display:flex; gap:6px; margin-bottom:8px; }
|
|
6
6
|
.sim-input { flex:1; padding:6px 8px; font-size:14px; border:1px solid #ccc; border-radius:3px; }
|
|
7
7
|
.sim-btn { padding:6px 14px; background:#c4561d; color:white; border:none;
|
|
@@ -27,61 +27,78 @@
|
|
|
27
27
|
.sim-list .sim-flag { margin-right:6px; }
|
|
28
28
|
`,ct=(t,e)=>`<img class="sim-flag remote" src="${window.wiki.site(t).flag()}"
|
|
29
29
|
title="${t} \u2014 score ${e.toFixed(3)}"
|
|
30
|
-
data-site="${t}">`;var K=(t,e,s,
|
|
30
|
+
data-site="${t}">`;var K=(t,e,s,i)=>`<a class="sim-link" data-title="${s}" data-slug="${e}" data-site="${t}" href="#">${ct(t,i)} ${s}</a>`,xt=(t,e)=>{let{mode:s,specs:i,threshold:h,limit:b,force:m,ghostUrl:x,label:C}=B(e?.text||"");if(s==="ghost")t.html(`
|
|
31
31
|
<style>${O}</style>
|
|
32
32
|
<div class="similarity" data-id="${e.id}">
|
|
33
33
|
<div class="sim-form">
|
|
34
|
-
<button class="sim-btn">${
|
|
34
|
+
<button class="sim-btn">${C||"Open"}</button>
|
|
35
35
|
</div>
|
|
36
36
|
<div class="sim-status"></div>
|
|
37
37
|
</div>`);else if(s==="build")t.html(`
|
|
38
38
|
<style>${O}</style>
|
|
39
39
|
<div class="similarity" data-id="${e.id}">
|
|
40
40
|
<div class="sim-form">
|
|
41
|
-
<button class="sim-btn">${
|
|
41
|
+
<button class="sim-btn">${C||`Index ${i.length?i.join(", "):"*"}${m?" (force)":""}`}</button>
|
|
42
42
|
</div>
|
|
43
43
|
<div class="sim-status"></div>
|
|
44
|
-
</div>`);else if(s==="list"){let
|
|
44
|
+
</div>`);else if(s==="list"){let v=i.length?i.join(", "):"*";t.html(`
|
|
45
45
|
<style>${O}</style>
|
|
46
46
|
<div class="similarity" data-id="${e.id}">
|
|
47
|
-
<div class="sim-status">Loading indexed domains (${
|
|
47
|
+
<div class="sim-status">Loading indexed domains (${v})\u2026</div>
|
|
48
48
|
<div class="sim-list"></div>
|
|
49
|
-
</div>`)}else if(s==="
|
|
49
|
+
</div>`)}else if(s==="status")t.html(`
|
|
50
50
|
<style>${O}</style>
|
|
51
51
|
<div class="similarity" data-id="${e.id}">
|
|
52
|
-
<div class="sim-status">
|
|
52
|
+
<div class="sim-status">Reading the state of the index\u2026</div>
|
|
53
|
+
<div class="sim-list"></div>
|
|
54
|
+
</div>`);else if(s==="similar"){let v=i.length?i.join(", "):"current domain";t.html(`
|
|
55
|
+
<style>${O}</style>
|
|
56
|
+
<div class="similarity" data-id="${e.id}">
|
|
57
|
+
<div class="sim-status">Finding similar pages across ${v}\u2026</div>
|
|
53
58
|
<div class="sim-results"></div>
|
|
54
|
-
</div>`)}else if(s==="author"||s==="report"||s==="keyword"||s==="sites"){let
|
|
59
|
+
</div>`)}else if(s==="author"||s==="report"||s==="keyword"||s==="sites"){let v=i.length?i.join(", "):"(current domain)",T=s==="report"?"Report":s==="keyword"?"Keyword":s==="sites"?"Sites":"Author",R=s==="sites"?"Where should this page go? Title + first paragraph\u2026":"Search wiki pages\u2026";t.html(`
|
|
55
60
|
<style>${O}</style>
|
|
56
61
|
<div class="similarity" data-id="${e.id}">
|
|
57
62
|
<div class="sim-form">
|
|
58
|
-
<input class="sim-input" type="text" placeholder="${
|
|
59
|
-
<button class="sim-btn">${
|
|
63
|
+
<input class="sim-input" type="text" placeholder="${R}" />
|
|
64
|
+
<button class="sim-btn">${T}</button>
|
|
60
65
|
</div>
|
|
61
|
-
<div class="sim-status">Domains: ${
|
|
66
|
+
<div class="sim-status">Domains: ${v}</div>
|
|
62
67
|
<div class="sim-results"></div>
|
|
63
|
-
</div>`)}else{let
|
|
68
|
+
</div>`)}else{let v=i.length?i.join(", "):"(current domain)";t.html(`
|
|
64
69
|
<style>${O}</style>
|
|
65
70
|
<div class="similarity" data-id="${e.id}">
|
|
66
71
|
<div class="sim-form">
|
|
67
72
|
<input class="sim-input" type="text" placeholder="Search wiki pages\u2026" />
|
|
68
73
|
<button class="sim-btn">Search</button>
|
|
69
74
|
</div>
|
|
70
|
-
<div class="sim-status">Domains: ${
|
|
75
|
+
<div class="sim-status">Domains: ${v}</div>
|
|
71
76
|
<div class="sim-results"></div>
|
|
72
|
-
</div>`)}},bt=(t,e)=>{let{mode:s,specs:
|
|
77
|
+
</div>`)}},bt=(t,e)=>{let{mode:s,specs:i,rosterRefs:h,farms:b,threshold:m,limit:x,live:C,subject:v,force:T,ghostUrl:R,thresholdSet:S}=B(e?.text||""),w=window.location.origin,a=t.find(".sim-status")[0],c=v?ot(t):null,g=C||c?null:rt(e),k=st(i,h),J=()=>c?{seed:{site:c.site,slug:c.slug},text:c.text,excludePage:{site:c.site,slug:c.slug}}:{},M=(d,p,r)=>{let n=[`${d} \u2014 ${p.toLocaleString()} pages across ${r} domains`];return S&&n.push(`threshold ${m}`),n.push(`limit ${x}`),C&&n.push("LIVE"),n.join(" \xB7 ")};t.on("dblclick",d=>{$(d.target).closest(".sim-input").length||window.wiki.textEditor(t,e)}),t.on("click",".sim-link",function(d){d.preventDefault();let p=$(this),r=d.shiftKey?null:t.parents(".page");window.wiki.doInternalLink(p.data("title"),r,p.data("site"))});let dt=!i.length||i.length===1&&i[0]==="*"?"on farm":i.length===1?`on ${i[0]}`:"in domains",F=d=>d?` \xB7 cached ${lt(d)}`:"";if(s==="status"){let d=t.find(".sim-list")[0],p=n=>{if(!n)return"unknown";let u=(Date.now()-new Date(n))/864e5;return u<1?"today":u<2?"yesterday":`${Math.floor(u)} days ago`},r=n=>n?new Date(n).toISOString().slice(0,10):"\u2014";(async()=>{try{let[n,u]=await Promise.all([fetch(`${w}/system/similarity-health.json`).then(j=>j.json()),fetch(`${w}/system/indexed-domains.json`).then(j=>j.ok?j.json():[])]),o=n.embedder||{},l=u.map(j=>j.built).filter(Boolean).sort(),f=u.reduce((j,P)=>j+(P.page_count||0),0),y=o.state==="down",L=y?"<strong>Semantic search is down</strong> \u2014 the embedder is unavailable, so searches will answer by keyword instead.":"Semantic search is <strong>up</strong>.",E=o.via==="url"?`delegated to <code>${o.url}</code> (from ${o.source||"config"})`:o.via==="semindex"?"in this process, via the SemIndex plugin":"in this process, in a supervised child";a.style.display="none",d.innerHTML=`<h3>The Index Behind These Answers</h3>
|
|
78
|
+
<p>${L}</p>
|
|
79
|
+
<table>
|
|
80
|
+
<tr><th>Model</th><td>${n.model||"\u2014"} \xB7 ${n.dim||"\u2014"} dimensions</td></tr>
|
|
81
|
+
<tr><th>Embedder</th><td>${E}${y?" \u2014 <strong>currently down</strong>":""}</td></tr>
|
|
82
|
+
<tr><th>Indexed</th><td>${u.length.toLocaleString()} domains \xB7 ${f.toLocaleString()} pages</td></tr>
|
|
83
|
+
<tr><th>Newest page vectors</th><td>${r(l[l.length-1])} (${p(l[l.length-1])})</td></tr>
|
|
84
|
+
<tr><th>Oldest page vectors</th><td>${r(l[0])} (${p(l[0])})</td></tr>
|
|
85
|
+
<tr><th>Plugin</th><td>wiki-plugin-similarity ${n.version||"\u2014"}</td></tr>
|
|
86
|
+
</table>
|
|
87
|
+
<p class="sim-count">A domain's vectors are only rebuilt when its pages change, so an old
|
|
88
|
+
date means a quiet site, not a broken one. Pages saved since the last build are findable
|
|
89
|
+
by keyword but not yet by meaning.</p>`}catch(n){a.textContent=`Index state unavailable: ${n.message}`}})()}else if(s==="list"){let d=t.find(".sim-list")[0],p=(r,n)=>{let u=r.reduce((o,l)=>o+(l.page_count||0),0);a.style.display="none",d.innerHTML=`<h3>Indexed Farm Domains</h3>
|
|
73
90
|
<table>
|
|
74
91
|
<tr><th>Domain</th><th>Pages</th></tr>
|
|
75
|
-
${
|
|
92
|
+
${r.map(({domain:o,page_count:l})=>`
|
|
76
93
|
<tr>
|
|
77
|
-
<td><img class="sim-flag remote" src="${window.wiki.site(
|
|
78
|
-
title="${
|
|
79
|
-
<td>${
|
|
94
|
+
<td><img class="sim-flag remote" src="${window.wiki.site(o).flag()}"
|
|
95
|
+
title="${o}" data-site="${o}"> ${o}</td>
|
|
96
|
+
<td>${l!=null?l.toLocaleString():"\u2014"}</td>
|
|
80
97
|
</tr>`).join("")}
|
|
81
98
|
</table>
|
|
82
|
-
<p class="sim-count">${
|
|
83
|
-
`);
|
|
84
|
-
`),
|
|
99
|
+
<p class="sim-count">${r.length} domains \u2014 ${u.toLocaleString()} pages${F(n)}</p>`};g?.domains?p(g.domains,g.ts):(async()=>{try{let r=await k,n=r.length?r.join(","):"*",u=`${w}/system/indexed-domains.json?pattern=${encodeURIComponent(n)}&limit=${x}`,o=await fetch(u);if(!o.ok)throw new Error(`indexed-domains failed: ${o.status}`);let l=await o.json();if(!l.length){a.textContent="No indexed domains found";return}p(l,null),U(e,{domains:l})}catch(r){a.textContent=`Error: ${r.message}`}})()}else if(s==="similar"){let d=t.find(".sim-results")[0],p=(r,n)=>{if(!r.length){a.textContent=`No similar pages found above threshold ${m}`;return}d.innerHTML=`<h3>Similar Pages</h3><ul>${r.map(({domain:u,slug:o,title:l,score:f})=>`<li>${K(u,o,l,f)}</li>`).join("")}</ul><p class="sim-count">${r.length} found ${dt}${F(n)}</p>`,a.style.display="none"};g?.scored?p(g.scored,g.ts):(async()=>{try{let r=t.parents(".page"),n=c||(()=>{let y=r.find(".title").text().trim()||document.title;return{slug:tt(y),site:window.location.hostname,title:y,text:null,isSelf:!0}})(),u=await z(await k,w),o=u.reduce((y,L)=>y+L.pages.length,0);a.textContent=(c?`${D(n)} \xB7 `:"")+`Searching ${o.toLocaleString()} pages\u2026`;let l=await it(n.slug,n.site);if(!l){a.textContent="Embedding page (not yet indexed)\u2026";let y=n.text||r.find(".item").map((L,E)=>$(E).text().trim()).get().filter(Boolean).join(`
|
|
100
|
+
`);l=await H(y||n.title,w)}let f=N(l,u,{threshold:m,limit:x,excludeSlug:n.slug,excludeDomain:n.site});p(f,null),c&&(a.textContent=D(n),a.style.display=""),f.length&&!c&&U(e,{scored:f})}catch(r){a.textContent=`Error: ${r.message}`}})()}else if(s==="ghost"){let d=t.find(".sim-btn")[0],p=async()=>{if(!R){a.textContent="No URL \u2014 GHOST needs a page-json URL";return}d.disabled=!0,a.textContent="Fetching\u2026";try{let r=await fetch(R);if(!r.ok)throw new Error(`fetch failed: ${r.status}`);let n=await r.json();window.wiki.showResult(window.wiki.newPage(n),{$page:t.parents(".page")}),a.textContent=""}catch(r){a.textContent=`Error: ${r.message}`}finally{d.disabled=!1}};d.addEventListener("click",p)}else if(s==="build"){let d=t.find(".sim-btn")[0],p=async()=>{d.disabled=!0,a.textContent="Building index\u2026 (may take a while for large scopes)";try{let r=encodeURIComponent((i.length?i:["*"]).join(",")),n=await fetch(`${w}/system/build-index.json?domains=${r}&force=${T?1:0}`);if(n.status===501){let o=await n.json().catch(()=>({}));throw new Error(o.hint||"indexing runs on the farm indexer, not this server")}if(!n.ok)throw new Error(`build-index failed: ${n.status}`);let u=await n.json();window.wiki.showResult(window.wiki.newPage(u),{$page:t.parents(".page")}),a.textContent=""}catch(r){a.textContent=`Error: ${r.message}`}finally{d.disabled=!1}};d.addEventListener("click",p)}else if(s==="report"){let d=t.find(".sim-input")[0],p=t.find(".sim-btn")[0],r=`Domains: ${i.length?i.join(", "):"*"}`;c&&!d.value&&(d.value=c.title),(async()=>{try{let l=await k,f=await A(l.length?l:["*"],w),y=f.reduce((L,E)=>L+(E.page_count||0),0);r=M("Report ready",y,f.length),b.length&&(r+=` \xB7 +${b.length} peer farm${b.length>1?"s":""}`),c&&(r=`${D(c)} \xB7 ${r}`),a.textContent=r}catch(l){a.textContent=`Domain listing unavailable: ${l.message}`}})();let n=new Set([502,503]),u=async(l,f)=>{let y=encodeURIComponent((f.length?f:["*"]).join(",")),L=b.length?`&farms=${encodeURIComponent(b.join(","))}`:"",E=await fetch(`${w}/system/farm-search.json?q=${encodeURIComponent(l)}&pattern=${y}&limit=${x}${L}`);if(!E.ok)throw new Error(`keyword search also failed: ${E.status}`);let j=await E.json();window.wiki.showResult(window.wiki.newPage(j),{$page:t.parents(".page")})},o=async()=>{let l=d.value.trim();if(l){p.disabled=!0,a.textContent=b.length?"Generating report (asking peer farms)\u2026":"Generating report\u2026";try{let f=await k,y=c&&l===c.title?J():{},L={query:l,domains:f.length?f:["*"],limit:x,live:C,...y};b.length&&(L.farms=b),S&&(L.threshold=m);let E=await fetch(`${w}/system/search-report.json`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(L)});if(n.has(E.status)){let I=((await E.json().catch(()=>({}))).error||`status ${E.status}`).replace(/^embedding unavailable: /,"");a.textContent="Semantic search unavailable \u2014 searching by keyword instead\u2026",await u(l,f),a.textContent=`Semantic search unavailable (${I}) \u2014 these are keyword matches, ranked by word not meaning.`;return}if(!E.ok)throw new Error(`search-report failed: ${E.status}`);let j=await E.json(),P=window.wiki.newPage(j);window.wiki.showResult(P,{$page:t.parents(".page")}),a.textContent=r}catch(f){a.textContent=`Error: ${f.message}`}finally{p.disabled=!1}}};p.addEventListener("click",o),d.addEventListener("keydown",l=>{l.key==="Enter"&&o()})}else if(s==="sites"){let d=t.find(".sim-input")[0],p=t.find(".sim-btn")[0],r=`Domains: ${i.length?i.join(", "):"*"}`;c&&!d.value&&(d.value=c.title),(async()=>{try{let u=await k,o=await A(u.length?u:["*"],w),l=o.reduce((f,y)=>f+(y.page_count||0),0);r=M("Site report ready",l,o.length),c&&(r=`${D(c)} \xB7 ${r}`),a.textContent=r}catch(u){a.textContent=`Domain listing unavailable: ${u.message}`}})();let n=async()=>{let u=d.value.trim();if(u){p.disabled=!0,a.textContent="Ranking sites\u2026";try{let o=await k,l=c&&u===c.title?J():{},f=await fetch(`${w}/system/site-report.json`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:u,domains:o.length?o:["*"],limit:x,...l})});if(!f.ok)throw new Error(`site-report failed: ${f.status}`);let y=await f.json();window.wiki.showResult(window.wiki.newPage(y),{$page:t.parents(".page")}),a.textContent=r}catch(o){a.textContent=`Error: ${o.message}`}finally{p.disabled=!1}}};p.addEventListener("click",n),d.addEventListener("keydown",u=>{u.key==="Enter"&&n()})}else if(s==="keyword"){let d=t.find(".sim-input")[0],p=t.find(".sim-btn")[0];c&&!d.value&&(d.value=c.title),a.textContent=(c?`${D(c)} \xB7 `:"")+`Keyword search ready \u2014 domains: ${i.length?i.join(", "):"*"} \xB7 limit ${x}`;let r=async()=>{let n=d.value.trim();if(n){p.disabled=!0,a.textContent="Searching live site indexes\u2026";try{let u=await k,o=encodeURIComponent((u.length?u:["*"]).join(",")),l=b.length?`&farms=${encodeURIComponent(b.join(","))}`:"",f=await fetch(`${w}/system/farm-search.json?q=${encodeURIComponent(n)}&pattern=${o}&limit=${x}${l}`);if(!f.ok)throw new Error(`farm-search failed: ${f.status}`);let y=await f.json();window.wiki.showResult(window.wiki.newPage(y),{$page:t.parents(".page")}),a.textContent=`Keyword search ready \u2014 domains: ${i.length?i.join(", "):"*"} \xB7 limit ${x}`}catch(u){a.textContent=`Error: ${u.message}`}finally{p.disabled=!1}}};p.addEventListener("click",r),d.addEventListener("keydown",n=>{n.key==="Enter"&&r()})}else if(s==="author"){let d=t.find(".sim-input")[0],p=t.find(".sim-btn")[0],r=t.find(".sim-results")[0],n=null;c&&!d.value&&(d.value=c.title),(async()=>{try{g||(a.textContent="Resolving domains\u2026"),n=await z(await k,w);let o=n.reduce((l,f)=>l+f.pages.length,0);a.textContent=(c?`${D(c)} \xB7 `:"")+M("Author ready",o,n.length)}catch(o){a.textContent=`Load error: ${o.message}`}})();let u=async()=>{let o=d.value.trim();if(!(!o||!n)){p.disabled=!0,a.textContent="Embedding query\u2026",r.innerHTML="";try{let l=await H(o,w),f=N(l,n,{threshold:m,limit:x,excludeSlug:null,excludeDomain:null}),y=new Set,L=[];for(let{title:I}of f)y.has(I)||(y.add(I),L.push(I));let E=()=>Math.floor(Math.random()*18446744073709552e3).toString(16).padStart(16,"0"),j=L.map(I=>`- [[${I}]]`).join(`
|
|
101
|
+
`),P=[{type:"markdown",id:E(),text:`# Similar Pages
|
|
85
102
|
|
|
86
|
-
${
|
|
103
|
+
${j}`},{type:"markdown",id:E(),text:"# Reference Links"},...f.map(({domain:I,slug:ft,title:ut,score:mt})=>({type:"reference",id:E(),site:I,slug:ft,title:ut,text:`score ${mt.toFixed(3)}`}))],W=window.wiki.newPage({title:`${o} Results`,story:P,journal:[]});window.wiki.showResult(W,{$page:t.parents(".page")}),a.textContent=`${f.length} pages found`,U(e,{scored:f,query:o})}catch(l){a.textContent=`Error: ${l.message}`}finally{p.disabled=!1}}};p.addEventListener("click",u),d.addEventListener("keydown",o=>{o.key==="Enter"&&u()})}else{let d=t.find(".sim-input")[0],p=t.find(".sim-btn")[0],r=t.find(".sim-results")[0],n=null;g?.scored&&(d.value=g.query||"",r.innerHTML=g.scored.map(({domain:o,slug:l,title:f,score:y})=>`<div class="sim-result">${K(o,l,f,y)}</div>`).join("")+`<p class="sim-count">Top ${g.scored.length} for "${g.query||""}"${F(g.ts)}</p>`,a.textContent=""),c&&!d.value&&(d.value=c.title),(async()=>{try{g||(a.textContent="Resolving domains\u2026"),n=await z(await k,w);let o=n.reduce((l,f)=>l+f.pages.length,0);a.textContent=(c?`${D(c)} \xB7 `:"")+M("Search ready",o,n.length)}catch(o){a.textContent=`Load error: ${o.message}`}})();let u=async()=>{let o=d.value.trim();if(!(!o||!n)){p.disabled=!0,a.textContent="Embedding query\u2026",r.innerHTML="";try{let l=await H(o,w),f=N(l,n,{threshold:m,limit:x,excludeSlug:null,excludeDomain:null});r.innerHTML=f.map(({domain:y,slug:L,title:E,score:j})=>`<div class="sim-result">${K(y,L,E,j)}</div>`).join("")+`<p class="sim-count">Top ${f.length} for "${o}"</p>`,a.textContent="",U(e,{scored:f,query:o})}catch(l){a.textContent=`Error: ${l.message}`}finally{p.disabled=!1}}};p.addEventListener("click",u),d.addEventListener("keydown",o=>{o.key==="Enter"&&u()})}};typeof window<"u"&&(window.plugins=window.plugins||{},window.plugins.similarity={emit:xt,bind:bt});})();
|
|
87
104
|
//# sourceMappingURL=similarity.js.map
|
package/client/similarity.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/client/dsl.js", "../src/client/scope.js", "../src/client/vectors.js", "../src/client/subject.js", "../src/client/cache.js", "../src/client/styles.js", "../src/client/similarity.js"],
|
|
4
|
-
"sourcesContent": ["// wiki-plugin-similarity \u2014 the item's line-oriented DSL: parse it, and classify a spec\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\n// \u2500\u2500 DSL Parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst SIMILAR_THRESHOLDS = { high: 0.78, medium: 0.68, low: 0.58 }\nconst DEFAULT_THRESHOLD = SIMILAR_THRESHOLDS.medium\nconst DEFAULT_LIMIT = 10\n\n// parseDSL returns { mode, specs, threshold, limit }\n//\n// mode: 'similar' if SIMILAR: is the first meaningful line (ambient auto-run),\n// 'search' otherwise (interactive search form).\n//\n// Ward's convention: ALL-CAPS keyword as first word signals a mode switch.\n// Placing SIMILAR: first makes the item ambient; placing domain specs first\n// (or leaving text empty) keeps it as an interactive search form.\n\nconst parseDSL = text => {\n const specs = []\n const rosterRefs = [] // ROSTER site/slug \u2014 roster pages whose sites join the scope\n const farms = [] // FARM domain \u2014 peer farms asked to continue the search\n let threshold = null\n let limit = null\n let mode = 'search' // default: interactive search form\n let live = false // default: cache results in localStorage\n let subject = false // SUBJECT modifier: act on the previous lineup page\n let force = false // BUILD mode: re-embed even when index is fresh\n let ghostUrl = null // GHOST mode: page-json URL to open as a ghost page\n let label = null // BUTTON: custom button caption (GHOST / BUILD modes)\n\n // Match a keyword at the start of a line (case-insensitive), requiring it to\n // be followed by end-of-string, whitespace, or colon \u2014 not by more word chars.\n // This prevents domain specs like \"similarity.example.com\" matching SIMILAR.\n const isCmd = (upper, kw) => upper === kw || (upper.startsWith(kw) && /^[\\s:]/.test(upper.slice(kw.length)))\n // Extract the value after the keyword, tolerating optional colon and whitespace.\n // Returns '' for bare commands (e.g. \"SIMILAR\" alone), callers use their default.\n const val = (line, kw) => line.slice(kw.length).replace(/^\\s*:?\\s*/, '').trim()\n\n for (const raw of text.split('\\n')) {\n const line = raw.trim()\n if (!line || line.startsWith('#')) continue\n\n const upper = line.toUpperCase()\n if (isCmd(upper, 'LIVE')) { live = true; continue }\n if (isCmd(upper, 'SUBJECT')) { subject = true; continue }\n if (isCmd(upper, 'AUTHOR')) {\n if (!specs.length && mode === 'search') mode = 'author'\n continue\n }\n if (isCmd(upper, 'REPORT')) {\n if (mode === 'search') mode = 'report'\n continue\n }\n if (isCmd(upper, 'KEYWORD')) {\n if (mode === 'search') mode = 'keyword'\n continue\n }\n if (isCmd(upper, 'SITES')) {\n if (mode === 'search') mode = 'sites'\n continue\n }\n if (isCmd(upper, 'BUILD')) {\n if (mode === 'search') mode = 'build'\n continue\n }\n if (isCmd(upper, 'FORCE')) { force = true; continue }\n if (isCmd(upper, 'GHOST')) {\n ghostUrl = val(line, 'GHOST')\n if (mode === 'search') mode = 'ghost'\n continue\n }\n if (isCmd(upper, 'BUTTON')) { label = val(line, 'BUTTON'); continue }\n if (isCmd(upper, 'ROSTER')) {\n const ref = val(line, 'ROSTER')\n if (ref) rosterRefs.push(ref)\n continue\n }\n if (isCmd(upper, 'FARM')) {\n const peer = val(line, 'FARM')\n if (peer) farms.push(peer)\n continue\n }\n if (isCmd(upper, 'LIST')) {\n if (!specs.length && mode === 'search') mode = 'list'\n continue\n }\n if (isCmd(upper, 'SIMILAR')) {\n const level = val(upper, 'SIMILAR').toLowerCase()\n threshold = SIMILAR_THRESHOLDS[level] || DEFAULT_THRESHOLD // '' \u2192 medium\n if (!specs.length && mode === 'search') mode = 'similar'\n continue\n }\n if (isCmd(upper, 'THRESHOLD')) {\n const tv = val(line, 'THRESHOLD')\n threshold = SIMILAR_THRESHOLDS[tv.toLowerCase()] ?? (parseFloat(tv) || DEFAULT_THRESHOLD)\n continue\n }\n if (isCmd(upper, 'LIMIT')) {\n limit = parseInt(val(line, 'LIMIT')) || DEFAULT_LIMIT // '' \u2192 10\n continue\n }\n // Anything else is a domain spec (glob, explicit domain, or scope keyword)\n specs.push(['PUBLIC', 'LOCAL', 'PRIVATE', 'GALAXY'].includes(upper) ? upper : line)\n }\n\n return {\n mode,\n specs,\n rosterRefs,\n farms,\n threshold: threshold ?? DEFAULT_THRESHOLD,\n limit: limit ?? DEFAULT_LIMIT,\n live,\n subject,\n force,\n ghostUrl,\n label,\n thresholdSet: threshold !== null, // explicit THRESHOLD/SIMILAR in the DSL\n }\n}\n\nconst isGlob = spec => spec.includes('*') || spec.includes('?')\n\n// Scope keywords expand server-side: PUBLIC (Nextcloud mirror farms),\n// LOCAL (primary farm), PRIVATE (public domains with restricted: true),\n// GALAXY (off-farm federation sites indexed into the galaxy tree)\nconst isScope = spec => spec === 'PUBLIC' || spec === 'LOCAL' || spec === 'PRIVATE' ||\n spec === 'GALAXY'\n\nexport { parseDSL, isGlob, isScope, SIMILAR_THRESHOLDS, DEFAULT_THRESHOLD, DEFAULT_LIMIT }\n", "// wiki-plugin-similarity \u2014 turning DSL specs into a concrete list of domains\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\nimport { isGlob, isScope } from './dsl.js'\n\n// \u2500\u2500 Slug \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst slugify = title => title.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '')\n\n// \u2500\u2500 Domain resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst domainCache = new Map()\n\nconst resolveDomainsForSpec = async (spec, origin) => {\n if (domainCache.has(spec)) return domainCache.get(spec)\n const url = `${origin}/system/indexed-domains.json?pattern=${encodeURIComponent(spec)}`\n const res = await fetch(url)\n if (!res.ok) throw new Error(`indexed-domains failed: ${res.status}`)\n const list = await res.json()\n domainCache.set(spec, list)\n return list\n}\n\nconst resolveDomains = async (specs, origin) => {\n if (!specs.length) specs = [window.location.hostname]\n const seen = new Set()\n const result = []\n for (const spec of specs) {\n if (spec === '*' || isGlob(spec) || isScope(spec)) {\n for (const item of await resolveDomainsForSpec(spec, origin)) {\n if (!seen.has(item.domain)) { seen.add(item.domain); result.push(item) }\n }\n } else if (!seen.has(spec)) {\n seen.add(spec); result.push({ domain: spec, page_count: null })\n }\n }\n return result\n}\n\n// \u2500\u2500 Roster resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// ROSTER site/slug resolves a roster page into domain names, following the\n// roster plugin's own line semantics: bare domains are sites; category and\n// blank lines are skipped; nested `ROSTER site/slug` includes another roster\n// page; `REFERENCES site/slug` collects the .site of a page's reference items.\n// Includes are followed transitively with a visited-set loop guard.\n\nconst SITE_LINE = /^([a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)+|localhost)(:\\d+)?$/\nconst ROSTER_LINE = /^ROSTER ([A-Za-z0-9.\\-:]+\\/[a-z0-9-]+)$/\nconst REFS_LINE = /^REFERENCES ([A-Za-z0-9.\\-:]+\\/[a-z0-9-]+)$/\n\nconst fetchPage = async ref => { // ref = site/slug\n const i = ref.indexOf('/')\n const res = await fetch(`//${ref.slice(0, i)}/${ref.slice(i + 1)}.json`)\n if (!res.ok) throw new Error(`roster page ${ref} failed: ${res.status}`)\n return res.json()\n}\n\nconst resolveRoster = async (ref, out, visited) => {\n if (visited.has(ref)) return\n visited.add(ref)\n let page\n try { page = await fetchPage(ref) } catch (e) { console.warn(e.message); return }\n for (const item of page.story || []) {\n if (item.type !== 'roster') continue\n for (const raw of (item.text || '').split(/\\r?\\n/)) {\n const line = raw.trim()\n if (!line) continue\n const site = line.match(SITE_LINE)\n if (site) { out.add(site[0]); continue }\n const nested = line.match(ROSTER_LINE)\n if (nested) { await resolveRoster(nested[1], out, visited); continue }\n const refs = line.match(REFS_LINE)\n if (refs) {\n try {\n const refPage = await fetchPage(refs[1])\n for (const it of refPage.story || []) {\n if (it.type === 'reference' && it.site) out.add(it.site)\n }\n } catch (e) { console.warn(e.message) }\n }\n // anything else is a category name \u2014 skipped\n }\n }\n}\n\nconst resolveRosters = async rosterRefs => {\n const out = new Set()\n const visited = new Set()\n for (const ref of rosterRefs) await resolveRoster(ref, out, visited)\n return [...out]\n}\n\n// Effective specs: DSL specs plus any domains contributed by ROSTER lines.\nconst effectiveSpecs = async (specs, rosterRefs) =>\n rosterRefs.length ? [...specs, ...await resolveRosters(rosterRefs)] : specs\n\nexport { slugify, resolveDomains, resolveRosters, effectiveSpecs, SITE_LINE, ROSTER_LINE, REFS_LINE }\n", "// wiki-plugin-similarity \u2014 page vectors, embedding, and the cosine scan over them\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\nimport { resolveDomains } from './scope.js'\n\n// \u2500\u2500 Vector loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst vectorCache = new Map()\n\n// Route vector requests through the current wiki's plugin server. This keeps\n// public HTTPS pages same-origin, avoids CORS failures, and lets the server read\n// indices from its configured farm roots.\nconst vectorUrl = domain =>\n `${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(domain)}`\n\nconst loadVectors = async domain => {\n if (vectorCache.has(domain)) return vectorCache.get(domain)\n const res = await fetch(vectorUrl(domain))\n if (!res.ok) return []\n const data = await res.json()\n vectorCache.set(domain, data)\n return data\n}\n\n// \u2500\u2500 Embedding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// POST, never GET: page-length text in a query string overflows Node's header\n// limit and dies with 431 before any handler runs (server \u2265 0.11.0).\nconst getEmbedding = async (text, origin) => {\n const res = await fetch(`${origin}/system/embed.json`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ text }),\n })\n if (!res.ok) throw new Error(`embed failed: ${res.status}`)\n return (await res.json()).vector\n}\n\n// Look up an existing page vector from the current domain's cached index.\n// Returns the float[] vector if found, null otherwise.\nconst lookupPageVector = async (slug, domain) => {\n const pages = await loadVectors(domain)\n const entry = pages.find(p => p.slug === slug)\n return entry ? entry.vector : null\n}\n// \u2500\u2500 Cosine search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst cosineScan = (queryVec, domainEntries, { threshold, limit, excludeSlug, excludeDomain }) => {\n const results = []\n for (const { domain, pages } of domainEntries) {\n for (const { slug, title, vector } of pages) {\n if (slug === excludeSlug && domain === excludeDomain) continue\n let dot = 0\n for (let i = 0; i < queryVec.length; i++) dot += queryVec[i] * vector[i]\n if (dot >= threshold) results.push({ domain, slug, title, score: dot })\n }\n }\n results.sort((a, b) => b.score - a.score)\n return results.slice(0, limit)\n}\n\n// \u2500\u2500 Shared domain loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst loadDomainEntries = async (specs, origin) => {\n const domains = await resolveDomains(specs, origin)\n const entries = await Promise.all(\n domains.map(async ({ domain }) => ({ domain, pages: await loadVectors(domain) }))\n )\n return entries.filter(e => e.pages.length > 0)\n}\n\nexport { loadVectors, vectorUrl, getEmbedding, lookupPageVector, cosineScan, loadDomainEntries }\n", "// wiki-plugin-similarity \u2014 SUBJECT \u2014 the page an item acts on\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\n// \u2500\u2500 Subject resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// SUBJECT makes an item act on the page BEFORE its own in the lineup \u2014 the\n// host a tool page was opened beside \u2014 falling back to the containing page so\n// a tool page opened alone still works (and announces the fallback).\n// The page div's id IS the slug (possibly _rev-suffixed on a historical view);\n// data('data') carries the full raw page JSON; data('site') is set only on\n// remote pages. `text` is title + opening prose, capped: the BGE-small\n// embedder truncates around 512 tokens, so more would be discarded anyway.\n\nconst resolveSubject = div => {\n const $self = div.parents('.page:first')\n const $host = $self.prev('.page')\n const $page = $host.length ? $host : $self\n const slug = ($page.attr('id') || '').split('_rev')[0]\n const site = $page.data('site') || window.location.hostname\n const data = $page.data('data') || {}\n const title = data.title || $page.find('.title').text().trim() || slug\n const text = [title, ...(data.story || [])\n .filter(i => i.type === 'markdown' || i.type === 'paragraph')\n .map(i => (i.text || '').trim()).filter(Boolean)].join('\\n').slice(0, 2000)\n return { slug, site, title, text, isSelf: !$host.length }\n}\n\nconst subjectNote = s =>\n `Subject: ${s.title}${s.isSelf ? ' (this page)' : ` @ ${s.site}`}`\n\nexport { resolveSubject, subjectNote }\n", "// wiki-plugin-similarity \u2014 per-item result cache in localStorage\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\n// \u2500\u2500 Result cache (localStorage) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Cache is keyed by item id. Invalidated when item.text changes (DSL edited).\n// LIVE mode bypasses the cache entirely.\n\nconst cacheKey = id => `sim-cache-${id}`\n\nconst readCache = item => {\n try {\n const c = JSON.parse(localStorage.getItem(cacheKey(item.id)) || 'null')\n return c?.text === (item.text || '') ? c : null\n } catch { return null }\n}\n\nconst writeCache = (item, data) => {\n try {\n localStorage.setItem(cacheKey(item.id), JSON.stringify({\n text: item.text || '',\n ts: Date.now(),\n ...data,\n }))\n } catch { /* storage unavailable or full */ }\n}\n\nconst cacheAge = ts => {\n const s = Math.floor((Date.now() - ts) / 1000)\n if (s < 60) return `${s}s ago`\n if (s < 3600) return `${Math.floor(s / 60)}m ago`\n if (s < 86400) return `${Math.floor(s / 3600)}h ago`\n return `${Math.floor(s / 86400)}d ago`\n}\n\nexport { readCache, writeCache, cacheAge }\n", "// wiki-plugin-similarity \u2014 item styles and the site flag\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\n// \u2500\u2500 Styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst STYLES = `\n .sim-form { display:flex; gap:6px; margin-bottom:8px; }\n .sim-input { flex:1; padding:6px 8px; font-size:14px; border:1px solid #ccc; border-radius:3px; }\n .sim-btn { padding:6px 14px; background:#c4561d; color:white; border:none;\n border-radius:3px; cursor:pointer; font-size:14px; }\n .sim-btn:disabled { opacity:0.5; cursor:not-allowed; }\n .sim-status { font-size:12px; color:#888; margin-bottom:6px; min-height:16px; }\n .sim-results { margin-top:4px; }\n .sim-result { display:flex; align-items:center; gap:8px; padding:3px 0;\n border-bottom:1px solid #f0f0f0; }\n .sim-flag { width:16px; height:16px; vertical-align:middle; margin-right:4px; }\n .sim-link { font-size:14px; color:#406; flex:1; }\n .sim-domain { font-size:11px; color:#999; }\n .similar-results h3, .sim-list h3 { margin:4px 0 6px; font-size:14px; color:#555; }\n .similar-results ul { margin:0; padding-left:18px; }\n .similar-results li { font-size:14px; padding:2px 0; }\n .similar-results .sim-domain { margin-left:6px; }\n .sim-count { font-size:12px; color:#888; margin:4px 0 0; }\n .sim-list table { border-collapse:collapse; width:100%; font-size:13px; }\n .sim-list th { text-align:left; font-size:11px; color:#888; padding:2px 8px 4px 0;\n border-bottom:1px solid #ddd; }\n .sim-list td { padding:3px 8px 3px 0; border-bottom:1px solid #f0f0f0; vertical-align:middle; }\n .sim-list td:last-child { text-align:right; color:#999; font-size:11px; }\n .sim-list .sim-flag { margin-right:6px; }\n`\n\nconst siteFlag = (domain, score) =>\n `<img class=\"sim-flag remote\" src=\"${window.wiki.site(domain).flag()}\"\n title=\"${domain} \u2014 score ${score.toFixed(3)}\"\n data-site=\"${domain}\">`\n\nexport { STYLES, siteFlag }\n", "// wiki-plugin-similarity\n//\n// One item type: similarity\n//\n// DSL in item.text (newline-separated lines):\n// * all indexed domains on this server\n// david.* glob pattern\n// *.fish glob pattern\n// david.hitchhikers.earth explicit domain name\n// # comment ignored\n// LIST list indexed domains (mode directive)\n// SIMILAR: high threshold preset AND ambient mode trigger\n// SUBJECT modifier: act on the PREVIOUS page in the lineup\n// (the host a tool page was opened beside; falls\n// back to the containing page when none)\n// THRESHOLD: 0.72 exact cosine threshold (overrides SIMILAR:)\n// LIMIT: 8 max results shown (default 10)\n// ROSTER site/slug add the sites of a roster page to the scope\n// FARM other.farm.tld ask a peer farm to continue the search (experimental)\n//\n// Mode is determined by the FIRST meaningful line (Ward's ALL-CAPS convention):\n// LIST \u2192 show a table of all indexed domains and their page counts;\n// optional glob patterns on subsequent lines filter the list\n// SIMILAR: \u2192 ambient mode \u2014 automatically find pages similar to this page\n// REPORT \u2192 server-side ranked/bundled semantic report (ghost page)\n// KEYWORD \u2192 galactic MiniSearch over live site-index.json files (ghost page)\n// SITES \u2192 which site should this page go on? \u2014 per-domain aggregation of\n// the page-vector scan (ghost page)\n// (other) \u2192 search form mode \u2014 user types a query, results appear\n//\n// Server endpoints required (all same-origin, served by this plugin's server\n// component \u2014 works on any host, including the public farm):\n// GET /system/indexed-domains.json?pattern=glob1,glob2\n// GET /system/semantic-vectors.json?domain=\n// POST /system/embed.json {text}\n// POST /system/search-report.json\n// POST /system/site-report.json\n// GET /system/farm-search.json?q=&pattern=&limit=\n// GET /system/build-index.json?domains=&force=\n\nimport { parseDSL, DEFAULT_LIMIT } from './dsl.js'\nimport { slugify, effectiveSpecs, resolveDomains } from './scope.js'\nimport { loadVectors, getEmbedding, lookupPageVector, cosineScan, loadDomainEntries } from './vectors.js'\nimport { resolveSubject, subjectNote } from './subject.js'\nimport { readCache, writeCache, cacheAge } from './cache.js'\nimport { STYLES, siteFlag } from './styles.js'\n\n// \u2500\u2500 similarity item \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst simLink = (domain, slug, title, score) =>\n `<a class=\"sim-link\" data-title=\"${title}\" data-slug=\"${slug}\" data-site=\"${domain}\" href=\"#\">` +\n `${siteFlag(domain, score)} ${title}</a>`\n\nexport const emit = (div, item) => {\n const { mode, specs, threshold, limit, force, ghostUrl, label } = parseDSL(item?.text || '')\n if (mode === 'ghost') {\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-form\">\n <button class=\"sim-btn\">${label || 'Open'}</button>\n </div>\n <div class=\"sim-status\"></div>\n </div>`)\n } else if (mode === 'build') {\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-form\">\n <button class=\"sim-btn\">${label || `Index ${specs.length ? specs.join(', ') : '*'}${force ? ' (force)' : ''}`}</button>\n </div>\n <div class=\"sim-status\"></div>\n </div>`)\n } else if (mode === 'list') {\n const label = specs.length ? specs.join(', ') : '*'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-status\">Loading indexed domains (${label})\u2026</div>\n <div class=\"sim-list\"></div>\n </div>`)\n } else if (mode === 'similar') {\n const label = specs.length ? specs.join(', ') : 'current domain'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-status\">Finding similar pages across ${label}\u2026</div>\n <div class=\"sim-results\"></div>\n </div>`)\n } else if (mode === 'author' || mode === 'report' || mode === 'keyword' || mode === 'sites') {\n const label = specs.length ? specs.join(', ') : '(current domain)'\n const btnLabel = mode === 'report' ? 'Report' : mode === 'keyword' ? 'Keyword'\n : mode === 'sites' ? 'Sites' : 'Author'\n const hint = mode === 'sites' ? 'Where should this page go? Title + first paragraph\u2026'\n : 'Search wiki pages\u2026'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-form\">\n <input class=\"sim-input\" type=\"text\" placeholder=\"${hint}\" />\n <button class=\"sim-btn\">${btnLabel}</button>\n </div>\n <div class=\"sim-status\">Domains: ${label}</div>\n <div class=\"sim-results\"></div>\n </div>`)\n } else {\n const label = specs.length ? specs.join(', ') : '(current domain)'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-form\">\n <input class=\"sim-input\" type=\"text\" placeholder=\"Search wiki pages\u2026\" />\n <button class=\"sim-btn\">Search</button>\n </div>\n <div class=\"sim-status\">Domains: ${label}</div>\n <div class=\"sim-results\"></div>\n </div>`)\n }\n}\n\nexport const bind = (div, item) => {\n const { mode, specs, rosterRefs, farms, threshold, limit, live, subject: subjectFlag,\n force, ghostUrl, thresholdSet } = parseDSL(item?.text || '')\n const origin = window.location.origin\n const status = div.find('.sim-status')[0]\n // The subject depends on where in the lineup this page was opened, so\n // cached results from one host must never replay beside another.\n const subject = subjectFlag ? resolveSubject(div) : null\n const cache = (live || subject) ? null : readCache(item)\n // Scope = DSL specs plus roster-page domains (resolved once, shared by modes)\n const specsP = effectiveSpecs(specs, rosterRefs)\n // Seed params for the server report routes \u2014 the stored page vector wins\n // server-side; text is the fallback for remote or unindexed subjects.\n const seedParams = () => subject ? {\n seed: { site: subject.site, slug: subject.slug },\n text: subject.text,\n excludePage: { site: subject.site, slug: subject.slug },\n } : {}\n\n // Standardised pre-search status: what will run, over how much, with what config.\n // e.g. \"Report ready \u2014 18,583 pages across 267 domains \u00B7 threshold 0.68 \u00B7 limit 20 \u00B7 LIVE\"\n const configSummary = (verb, pages, nDomains) => {\n const parts = [`${verb} \u2014 ${pages.toLocaleString()} pages across ${nDomains} domains`]\n if (thresholdSet) parts.push(`threshold ${threshold}`)\n parts.push(`limit ${limit}`)\n if (live) parts.push('LIVE')\n return parts.join(' \u00B7 ')\n }\n\n div.on('dblclick', e => {\n if ($(e.target).closest('.sim-input').length) return\n window.wiki.textEditor(div, item)\n })\n div.on('click', '.sim-link', function (e) {\n e.preventDefault()\n const $a = $(this)\n // shift-click appends at the end of the lineup instead of truncating after this page\n const $page = e.shiftKey ? null : div.parents('.page')\n window.wiki.doInternalLink($a.data('title'), $page, $a.data('site'))\n })\n\n const scopeLabel = !specs.length || (specs.length === 1 && specs[0] === '*')\n ? 'on farm'\n : specs.length === 1 ? `on ${specs[0]}` : 'in domains'\n\n const cacheNote = ts => ts ? ` \u00B7 cached ${cacheAge(ts)}` : ''\n\n if (mode === 'list') {\n const listDiv = div.find('.sim-list')[0]\n\n const renderList = (domains, ts) => {\n const totalPages = domains.reduce((n, d) => n + (d.page_count || 0), 0)\n status.style.display = 'none'\n listDiv.innerHTML = `<h3>Indexed Farm Domains</h3>\n <table>\n <tr><th>Domain</th><th>Pages</th></tr>\n ${domains.map(({ domain, page_count }) => `\n <tr>\n <td><img class=\"sim-flag remote\" src=\"${window.wiki.site(domain).flag()}\"\n title=\"${domain}\" data-site=\"${domain}\"> ${domain}</td>\n <td>${page_count != null ? page_count.toLocaleString() : '\u2014'}</td>\n </tr>`).join('')}\n </table>\n <p class=\"sim-count\">${domains.length} domains \u2014 ${totalPages.toLocaleString()} pages${cacheNote(ts)}</p>`\n }\n\n if (cache?.domains) {\n renderList(cache.domains, cache.ts)\n } else {\n ;(async () => {\n try {\n const eff = await specsP\n const patterns = eff.length ? eff.join(',') : '*'\n const url = `${origin}/system/indexed-domains.json?pattern=${encodeURIComponent(patterns)}&limit=${limit}`\n const res = await fetch(url)\n if (!res.ok) throw new Error(`indexed-domains failed: ${res.status}`)\n const domains = await res.json()\n if (!domains.length) { status.textContent = 'No indexed domains found'; return }\n renderList(domains, null)\n writeCache(item, { domains })\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n }\n })()\n }\n\n } else if (mode === 'similar') {\n const results = div.find('.sim-results')[0]\n\n const renderScored = (scored, ts) => {\n if (!scored.length) {\n status.textContent = `No similar pages found above threshold ${threshold}`\n return\n }\n results.innerHTML = `<h3>Similar Pages</h3><ul>${\n scored.map(({ domain, slug, title, score }) =>\n `<li>${simLink(domain, slug, title, score)}</li>`).join('')\n }</ul><p class=\"sim-count\">${scored.length} found ${scopeLabel}${cacheNote(ts)}</p>`\n status.style.display = 'none'\n }\n\n if (cache?.scored) {\n renderScored(cache.scored, cache.ts)\n } else {\n ;(async () => {\n try {\n // SUBJECT: act on the previous lineup page; otherwise this page.\n const $page = div.parents('.page')\n const s = subject || (() => {\n const pageTitle = $page.find('.title').text().trim() || document.title\n return { slug: slugify(pageTitle), site: window.location.hostname,\n title: pageTitle, text: null, isSelf: true }\n })()\n\n const domainEntries = await loadDomainEntries(await specsP, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = (subject ? `${subjectNote(s)} \u00B7 ` : '') +\n `Searching ${total.toLocaleString()} pages\u2026`\n\n let qVec = await lookupPageVector(s.slug, s.site)\n if (!qVec) {\n status.textContent = 'Embedding page (not yet indexed)\u2026'\n const pageText = s.text || $page.find('.item')\n .map((_, el) => $(el).text().trim()).get().filter(Boolean).join('\\n')\n qVec = await getEmbedding(pageText || s.title, origin)\n }\n\n const scored = cosineScan(qVec, domainEntries, {\n threshold, limit, excludeSlug: s.slug, excludeDomain: s.site,\n })\n renderScored(scored, null)\n if (subject) { // keep the subject visible above the results\n status.textContent = subjectNote(s)\n status.style.display = ''\n }\n if (scored.length && !subject) writeCache(item, { scored })\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n }\n })()\n }\n\n } else if (mode === 'ghost') {\n // Ghost mode \u2014 fetch page-json from any URL and open it as a ghost page\n const btn = div.find('.sim-btn')[0]\n\n const doGhost = async () => {\n if (!ghostUrl) { status.textContent = 'No URL \u2014 GHOST needs a page-json URL'; return }\n btn.disabled = true\n status.textContent = 'Fetching\u2026'\n try {\n const res = await fetch(ghostUrl)\n if (!res.ok) throw new Error(`fetch failed: ${res.status}`)\n const page = await res.json()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n status.textContent = ''\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doGhost)\n\n } else if (mode === 'build') {\n // Build mode \u2014 trigger a semantic index build; result opens as a ghost page\n const btn = div.find('.sim-btn')[0]\n\n const doBuild = async () => {\n btn.disabled = true\n status.textContent = 'Building index\u2026 (may take a while for large scopes)'\n try {\n const domains = encodeURIComponent((specs.length ? specs : ['*']).join(','))\n const res = await fetch(\n `${origin}/system/build-index.json?domains=${domains}&force=${force ? 1 : 0}`)\n if (res.status === 501) {\n const body = await res.json().catch(() => ({}))\n throw new Error(body.hint || 'indexing runs on the farm indexer, not this server')\n }\n if (!res.ok) throw new Error(`build-index failed: ${res.status}`)\n const page = await res.json()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n status.textContent = ''\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doBuild)\n\n } else if (mode === 'report') {\n // Report mode \u2014 server-side ranked/bundled search, opens result as ghost page\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n let readyLine = `Domains: ${specs.length ? specs.join(', ') : '*'}`\n\n // SUBJECT: the host page IS the query \u2014 prefill its title (editable) and\n // seed the server search with its stored vector.\n if (subject && !input.value) input.value = subject.title\n\n // Preload the lightweight domain listing (counts only, no vectors) so the\n // status line shows scope and config before any search is issued.\n ;(async () => {\n try {\n const eff = await specsP\n const domains = await resolveDomains(eff.length ? eff : ['*'], origin)\n const pages = domains.reduce((n, d) => n + (d.page_count || 0), 0)\n readyLine = configSummary('Report ready', pages, domains.length)\n if (farms.length) readyLine += ` \u00B7 +${farms.length} peer farm${farms.length > 1 ? 's' : ''}`\n if (subject) readyLine = `${subjectNote(subject)} \u00B7 ${readyLine}`\n status.textContent = readyLine\n } catch (e) {\n status.textContent = `Domain listing unavailable: ${e.message}`\n }\n })()\n\n // 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 const doReport = async () => {\n const query = input.value.trim()\n if (!query) return\n btn.disabled = true\n status.textContent = farms.length ? 'Generating report (asking peer farms)\u2026' : 'Generating report\u2026'\n try {\n const eff = await specsP\n // A hand-edited query is a new question \u2014 drop the page seed then.\n const seeded = subject && query === subject.title ? seedParams() : {}\n const body = { query, domains: eff.length ? eff : ['*'], limit, live, ...seeded }\n if (farms.length) body.farms = farms\n if (thresholdSet) body.threshold = threshold\n const res = await fetch(`${origin}/system/search-report.json`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (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 } else if (mode === 'sites') {\n // Sites mode \u2014 which site should this page go on? Server aggregates the\n // page-vector scan per domain; result opens as a ghost page.\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n let readyLine = `Domains: ${specs.length ? specs.join(', ') : '*'}`\n\n if (subject && !input.value) input.value = subject.title\n\n ;(async () => {\n try {\n const eff = await specsP\n const domains = await resolveDomains(eff.length ? eff : ['*'], origin)\n const pages = domains.reduce((n, d) => n + (d.page_count || 0), 0)\n readyLine = configSummary('Site report ready', pages, domains.length)\n if (subject) readyLine = `${subjectNote(subject)} \u00B7 ${readyLine}`\n status.textContent = readyLine\n } catch (e) {\n status.textContent = `Domain listing unavailable: ${e.message}`\n }\n })()\n\n const doSites = async () => {\n const query = input.value.trim()\n if (!query) return\n btn.disabled = true\n status.textContent = 'Ranking sites\u2026'\n try {\n const eff = await specsP\n const seeded = subject && query === subject.title ? seedParams() : {}\n const res = await fetch(`${origin}/system/site-report.json`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ query, domains: eff.length ? eff : ['*'], limit, ...seeded }),\n })\n if (!res.ok) throw new Error(`site-report failed: ${res.status}`)\n const page = await res.json()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n status.textContent = readyLine\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doSites)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doSites() })\n\n } else if (mode === 'keyword') {\n // Keyword mode \u2014 galactic MiniSearch: the server reads each site's own\n // per-edit site-index.json; result opens as a ghost page.\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n\n // SUBJECT: keyword search wants words, not vectors \u2014 the host title is\n // the prewired query (editable), finding forks and namesakes alike.\n if (subject && !input.value) input.value = subject.title\n\n status.textContent = (subject ? `${subjectNote(subject)} \u00B7 ` : '') +\n `Keyword search ready \u2014 domains: ${specs.length ? specs.join(', ') : '*'} \u00B7 limit ${limit}`\n\n const doKeyword = async () => {\n const query = input.value.trim()\n if (!query) return\n btn.disabled = true\n status.textContent = 'Searching live site indexes\u2026'\n try {\n const eff = await specsP\n const pattern = encodeURIComponent((eff.length ? eff : ['*']).join(','))\n const farmsParam = farms.length ? `&farms=${encodeURIComponent(farms.join(','))}` : ''\n const res = await fetch(\n `${origin}/system/farm-search.json?q=${encodeURIComponent(query)}&pattern=${pattern}&limit=${limit}${farmsParam}`)\n if (!res.ok) throw new Error(`farm-search failed: ${res.status}`)\n const page = await res.json()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n status.textContent = `Keyword search ready \u2014 domains: ${specs.length ? specs.join(', ') : '*'} \u00B7 limit ${limit}`\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doKeyword)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doKeyword() })\n\n } else if (mode === 'author') {\n // Author mode \u2014 same search form but creates a ghost page from results\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n const results = div.find('.sim-results')[0]\n let domainEntries = null\n\n if (subject && !input.value) input.value = subject.title\n\n ;(async () => {\n try {\n if (!cache) status.textContent = 'Resolving domains\u2026'\n domainEntries = await loadDomainEntries(await specsP, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = (subject ? `${subjectNote(subject)} \u00B7 ` : '') +\n configSummary('Author ready', total, domainEntries.length)\n } catch (e) {\n status.textContent = `Load error: ${e.message}`\n }\n })()\n\n const doAuthor = async () => {\n const query = input.value.trim()\n if (!query || !domainEntries) return\n btn.disabled = true\n status.textContent = 'Embedding query\u2026'\n results.innerHTML = ''\n try {\n const qVec = await getEmbedding(query, origin)\n const scored = cosineScan(qVec, domainEntries,\n { threshold, limit, excludeSlug: null, excludeDomain: null })\n\n // Unique titles for wiki-link list\n const seenTitles = new Set()\n const uniqueTitles = []\n for (const { title } of scored) {\n if (!seenTitles.has(title)) { seenTitles.add(title); uniqueTitles.push(title) }\n }\n\n const hexId = () => Math.floor(Math.random() * 0xffffffffffffffff).toString(16).padStart(16, '0')\n const primaryLines = uniqueTitles.map(t => `- [[${t}]]`).join('\\n')\n\n const story = [\n { type: 'markdown', id: hexId(), text: `# Similar Pages\\n\\n${primaryLines}` },\n { type: 'markdown', id: hexId(), text: '# Reference Links' },\n ...scored.map(({ domain, slug, title, score }) => ({\n type: 'reference', id: hexId(), site: domain, slug, title,\n text: `score ${score.toFixed(3)}`,\n })),\n ]\n\n const pageObj = window.wiki.newPage({ title: `${query} Results`, story, journal: [] })\n window.wiki.showResult(pageObj, { $page: div.parents('.page') })\n\n status.textContent = `${scored.length} pages found`\n writeCache(item, { scored, query })\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doAuthor)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doAuthor() })\n\n } else {\n // Search form mode\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n const results = div.find('.sim-results')[0]\n let domainEntries = null\n\n // Show cached results immediately while domains preload in background\n if (cache?.scored) {\n input.value = cache.query || ''\n results.innerHTML = cache.scored.map(({ domain, slug, title, score }) =>\n `<div class=\"sim-result\">${simLink(domain, slug, title, score)}</div>`).join('') +\n `<p class=\"sim-count\">Top ${cache.scored.length} for \"${cache.query || ''}\"${cacheNote(cache.ts)}</p>`\n status.textContent = ''\n }\n\n if (subject && !input.value) input.value = subject.title\n\n ;(async () => {\n try {\n if (!cache) status.textContent = 'Resolving domains\u2026'\n domainEntries = await loadDomainEntries(await specsP, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = (subject ? `${subjectNote(subject)} \u00B7 ` : '') +\n configSummary('Search ready', total, domainEntries.length)\n } catch (e) {\n status.textContent = `Load error: ${e.message}`\n }\n })()\n\n const doSearch = async () => {\n const query = input.value.trim()\n if (!query || !domainEntries) return\n btn.disabled = true\n status.textContent = 'Embedding query\u2026'\n results.innerHTML = ''\n try {\n const qVec = await getEmbedding(query, origin)\n const scored = cosineScan(qVec, domainEntries,\n { threshold, limit, excludeSlug: null, excludeDomain: null })\n results.innerHTML = scored.map(({ domain, slug, title, score }) =>\n `<div class=\"sim-result\">${simLink(domain, slug, title, score)}</div>`).join('') +\n `<p class=\"sim-count\">Top ${scored.length} for \"${query}\"</p>`\n status.textContent = ''\n writeCache(item, { scored, query })\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doSearch)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch() })\n }\n}\n\n// \u2500\u2500 Register item type with the wiki \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nif (typeof window !== 'undefined') {\n window.plugins = window.plugins || {}\n window.plugins.similarity = { emit, bind }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["SIMILAR_THRESHOLDS", "DEFAULT_THRESHOLD", "DEFAULT_LIMIT", "parseDSL", "text", "specs", "rosterRefs", "farms", "threshold", "limit", "mode", "live", "subject", "force", "ghostUrl", "label", "isCmd", "upper", "kw", "val", "line", "raw", "ref", "peer", "level", "tv", "isGlob", "spec", "isScope", "slugify", "title", "domainCache", "resolveDomainsForSpec", "spec", "origin", "url", "res", "list", "resolveDomains", "specs", "seen", "result", "isGlob", "isScope", "item", "SITE_LINE", "ROSTER_LINE", "REFS_LINE", "fetchPage", "ref", "i", "resolveRoster", "out", "visited", "page", "e", "raw", "line", "site", "nested", "refs", "refPage", "it", "resolveRosters", "rosterRefs", "effectiveSpecs", "vectorCache", "vectorUrl", "domain", "loadVectors", "res", "data", "getEmbedding", "text", "origin", "lookupPageVector", "slug", "entry", "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", "STYLES", "siteFlag", "domain", "score", "simLink", "domain", "slug", "title", "score", "siteFlag", "emit", "div", "item", "mode", "specs", "threshold", "limit", "force", "ghostUrl", "label", "parseDSL", "STYLES", "btnLabel", "hint", "bind", "rosterRefs", "farms", "live", "subjectFlag", "thresholdSet", "origin", "status", "subject", "resolveSubject", "cache", "readCache", "specsP", "effectiveSpecs", "seedParams", "configSummary", "verb", "pages", "nDomains", "parts", "e", "$a", "$page", "scopeLabel", "cacheNote", "ts", "cacheAge", "listDiv", "
|
|
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: '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\n // Match a keyword at the start of a line (case-insensitive), requiring it to\n // be followed by end-of-string, whitespace, or colon \u2014 not by more word chars.\n // This prevents domain specs like \"similarity.example.com\" matching SIMILAR.\n const isCmd = (upper, kw) => upper === kw || (upper.startsWith(kw) && /^[\\s:]/.test(upper.slice(kw.length)))\n // Extract the value after the keyword, tolerating optional colon and whitespace.\n // Returns '' for bare commands (e.g. \"SIMILAR\" alone), callers use their default.\n const val = (line, kw) => line.slice(kw.length).replace(/^\\s*:?\\s*/, '').trim()\n\n for (const raw of text.split('\\n')) {\n const line = raw.trim()\n if (!line || line.startsWith('#')) continue\n\n const upper = line.toUpperCase()\n if (isCmd(upper, 'LIVE')) { live = true; continue }\n if (isCmd(upper, 'SUBJECT')) { subject = true; continue }\n if (isCmd(upper, 'AUTHOR')) {\n if (!specs.length && mode === 'search') mode = 'author'\n continue\n }\n if (isCmd(upper, 'REPORT')) {\n if (mode === 'search') mode = 'report'\n continue\n }\n if (isCmd(upper, 'KEYWORD')) {\n if (mode === 'search') mode = 'keyword'\n continue\n }\n if (isCmd(upper, 'SITES')) {\n if (mode === 'search') mode = 'sites'\n continue\n }\n if (isCmd(upper, 'BUILD')) {\n if (mode === 'search') mode = 'build'\n continue\n }\n if (isCmd(upper, 'FORCE')) { force = true; continue }\n if (isCmd(upper, 'GHOST')) {\n ghostUrl = val(line, 'GHOST')\n if (mode === 'search') mode = 'ghost'\n continue\n }\n if (isCmd(upper, 'BUTTON')) { label = val(line, 'BUTTON'); continue }\n if (isCmd(upper, 'ROSTER')) {\n const ref = val(line, 'ROSTER')\n if (ref) rosterRefs.push(ref)\n continue\n }\n if (isCmd(upper, 'FARM')) {\n const peer = val(line, 'FARM')\n if (peer) farms.push(peer)\n continue\n }\n if (isCmd(upper, 'LIST')) {\n if (!specs.length && mode === 'search') mode = 'list'\n continue\n }\n if (isCmd(upper, '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 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 item styles and the site flag\n// Split out of similarity.js; see the Splitting the Server phase of the plan.\n\n// \u2500\u2500 Styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst STYLES = `\n .sim-form { display:flex; gap:6px; margin-bottom:8px; }\n .sim-input { flex:1; padding:6px 8px; font-size:14px; border:1px solid #ccc; border-radius:3px; }\n .sim-btn { padding:6px 14px; background:#c4561d; color:white; border:none;\n border-radius:3px; cursor:pointer; font-size:14px; }\n .sim-btn:disabled { opacity:0.5; cursor:not-allowed; }\n .sim-status { font-size:12px; color:#888; margin-bottom:6px; min-height:16px; }\n .sim-results { margin-top:4px; }\n .sim-result { display:flex; align-items:center; gap:8px; padding:3px 0;\n border-bottom:1px solid #f0f0f0; }\n .sim-flag { width:16px; height:16px; vertical-align:middle; margin-right:4px; }\n .sim-link { font-size:14px; color:#406; flex:1; }\n .sim-domain { font-size:11px; color:#999; }\n .similar-results h3, .sim-list h3 { margin:4px 0 6px; font-size:14px; color:#555; }\n .similar-results ul { margin:0; padding-left:18px; }\n .similar-results li { font-size:14px; padding:2px 0; }\n .similar-results .sim-domain { margin-left:6px; }\n .sim-count { font-size:12px; color:#888; margin:4px 0 0; }\n .sim-list table { border-collapse:collapse; width:100%; font-size:13px; }\n .sim-list th { text-align:left; font-size:11px; color:#888; padding:2px 8px 4px 0;\n border-bottom:1px solid #ddd; }\n .sim-list td { padding:3px 8px 3px 0; border-bottom:1px solid #f0f0f0; vertical-align:middle; }\n .sim-list td:last-child { text-align:right; color:#999; font-size:11px; }\n .sim-list .sim-flag { margin-right:6px; }\n`\n\nconst siteFlag = (domain, score) =>\n `<img class=\"sim-flag remote\" src=\"${window.wiki.site(domain).flag()}\"\n title=\"${domain} \u2014 score ${score.toFixed(3)}\"\n data-site=\"${domain}\">`\n\nexport { STYLES, siteFlag }\n", "// wiki-plugin-similarity\n//\n// One item type: similarity\n//\n// DSL in item.text (newline-separated lines):\n// * all indexed domains on this server\n// david.* glob pattern\n// *.fish glob pattern\n// david.hitchhikers.earth explicit domain name\n// # comment ignored\n// LIST list indexed domains (mode directive)\n// SIMILAR: high threshold preset AND ambient mode trigger\n// SUBJECT modifier: act on the PREVIOUS page in the lineup\n// (the host a tool page was opened beside; falls\n// back to the containing page when none)\n// THRESHOLD: 0.72 exact cosine threshold (overrides SIMILAR:)\n// LIMIT: 8 max results shown (default 10)\n// ROSTER site/slug add the sites of a roster page to the scope\n// FARM other.farm.tld ask a peer farm to continue the search (experimental)\n//\n// Mode is determined by the FIRST meaningful line (Ward's ALL-CAPS convention):\n// LIST \u2192 show a table of all indexed domains and their page counts;\n// optional glob patterns on subsequent lines filter the list\n// SIMILAR: \u2192 ambient mode \u2014 automatically find pages similar to this page\n// REPORT \u2192 server-side ranked/bundled semantic report (ghost page)\n// KEYWORD \u2192 galactic MiniSearch over live site-index.json files (ghost page)\n// SITES \u2192 which site should this page go on? \u2014 per-domain aggregation of\n// the page-vector scan (ghost page)\n// (other) \u2192 search form mode \u2014 user types a query, results appear\n//\n// Server endpoints required (all same-origin, served by this plugin's server\n// component \u2014 works on any host, including the public farm):\n// GET /system/indexed-domains.json?pattern=glob1,glob2\n// GET /system/semantic-vectors.json?domain=\n// POST /system/embed.json {text}\n// POST /system/search-report.json\n// POST /system/site-report.json\n// GET /system/farm-search.json?q=&pattern=&limit=\n// GET /system/build-index.json?domains=&force=\n\nimport { parseDSL, DEFAULT_LIMIT } from './dsl.js'\nimport { slugify, effectiveSpecs, resolveDomains } from './scope.js'\nimport { loadVectors, getEmbedding, lookupPageVector, cosineScan, loadDomainEntries } from './vectors.js'\nimport { resolveSubject, subjectNote } from './subject.js'\nimport { readCache, writeCache, cacheAge } from './cache.js'\nimport { STYLES, siteFlag } from './styles.js'\n\n// \u2500\u2500 similarity item \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst simLink = (domain, slug, title, score) =>\n `<a class=\"sim-link\" data-title=\"${title}\" data-slug=\"${slug}\" data-site=\"${domain}\" href=\"#\">` +\n `${siteFlag(domain, score)} ${title}</a>`\n\nexport const emit = (div, item) => {\n const { mode, specs, threshold, limit, force, ghostUrl, label } = parseDSL(item?.text || '')\n if (mode === 'ghost') {\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-form\">\n <button class=\"sim-btn\">${label || 'Open'}</button>\n </div>\n <div class=\"sim-status\"></div>\n </div>`)\n } else if (mode === 'build') {\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-form\">\n <button class=\"sim-btn\">${label || `Index ${specs.length ? specs.join(', ') : '*'}${force ? ' (force)' : ''}`}</button>\n </div>\n <div class=\"sim-status\"></div>\n </div>`)\n } else if (mode === 'list') {\n const label = specs.length ? specs.join(', ') : '*'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-status\">Loading indexed domains (${label})\u2026</div>\n <div class=\"sim-list\"></div>\n </div>`)\n } else if (mode === 'status') {\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\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}\">\n <div class=\"sim-status\">Finding similar pages across ${label}\u2026</div>\n <div class=\"sim-results\"></div>\n </div>`)\n } else if (mode === 'author' || mode === 'report' || mode === 'keyword' || mode === 'sites') {\n const label = specs.length ? specs.join(', ') : '(current domain)'\n const btnLabel = mode === 'report' ? 'Report' : mode === 'keyword' ? 'Keyword'\n : mode === 'sites' ? 'Sites' : 'Author'\n const hint = mode === 'sites' ? 'Where should this page go? Title + first paragraph\u2026'\n : 'Search wiki pages\u2026'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-form\">\n <input class=\"sim-input\" type=\"text\" placeholder=\"${hint}\" />\n <button class=\"sim-btn\">${btnLabel}</button>\n </div>\n <div class=\"sim-status\">Domains: ${label}</div>\n <div class=\"sim-results\"></div>\n </div>`)\n } else {\n const label = specs.length ? specs.join(', ') : '(current domain)'\n div.html(`\n <style>${STYLES}</style>\n <div class=\"similarity\" data-id=\"${item.id}\">\n <div class=\"sim-form\">\n <input class=\"sim-input\" type=\"text\" placeholder=\"Search wiki pages\u2026\" />\n <button class=\"sim-btn\">Search</button>\n </div>\n <div class=\"sim-status\">Domains: ${label}</div>\n <div class=\"sim-results\"></div>\n </div>`)\n }\n}\n\nexport const bind = (div, item) => {\n const { mode, specs, rosterRefs, farms, threshold, limit, live, subject: subjectFlag,\n force, ghostUrl, thresholdSet } = parseDSL(item?.text || '')\n const origin = window.location.origin\n const status = div.find('.sim-status')[0]\n // The subject depends on where in the lineup this page was opened, so\n // cached results from one host must never replay beside another.\n const subject = subjectFlag ? resolveSubject(div) : null\n const cache = (live || subject) ? null : readCache(item)\n // Scope = DSL specs plus roster-page domains (resolved once, shared by modes)\n const specsP = effectiveSpecs(specs, rosterRefs)\n // Seed params for the server report routes \u2014 the stored page vector wins\n // server-side; text is the fallback for remote or unindexed subjects.\n const seedParams = () => subject ? {\n seed: { site: subject.site, slug: subject.slug },\n text: subject.text,\n excludePage: { site: subject.site, slug: subject.slug },\n } : {}\n\n // Standardised pre-search status: what will run, over how much, with what config.\n // e.g. \"Report ready \u2014 18,583 pages across 267 domains \u00B7 threshold 0.68 \u00B7 limit 20 \u00B7 LIVE\"\n const configSummary = (verb, pages, nDomains) => {\n const parts = [`${verb} \u2014 ${pages.toLocaleString()} pages across ${nDomains} domains`]\n if (thresholdSet) parts.push(`threshold ${threshold}`)\n parts.push(`limit ${limit}`)\n if (live) parts.push('LIVE')\n return parts.join(' \u00B7 ')\n }\n\n div.on('dblclick', e => {\n if ($(e.target).closest('.sim-input').length) return\n window.wiki.textEditor(div, item)\n })\n div.on('click', '.sim-link', function (e) {\n e.preventDefault()\n const $a = $(this)\n // shift-click appends at the end of the lineup instead of truncating after this page\n const $page = e.shiftKey ? null : div.parents('.page')\n window.wiki.doInternalLink($a.data('title'), $page, $a.data('site'))\n })\n\n const scopeLabel = !specs.length || (specs.length === 1 && specs[0] === '*')\n ? 'on farm'\n : specs.length === 1 ? `on ${specs[0]}` : 'in domains'\n\n const cacheNote = ts => ts ? ` \u00B7 cached ${cacheAge(ts)}` : ''\n\n if (mode === '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\n ;(async () => {\n try {\n const [health, domains] = 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 ])\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. A breaker that has\n // tripped reads as down, because for the next few minutes it is.\n const down = emb.state === 'down'\n const upLine = down\n ? `<strong>Semantic search is down</strong> \u2014 the embedder is unavailable, so searches will answer by keyword instead.`\n : `Semantic search is <strong>up</strong>.`\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 </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 === 'list') {\n const listDiv = div.find('.sim-list')[0]\n\n const renderList = (domains, ts) => {\n const totalPages = domains.reduce((n, d) => n + (d.page_count || 0), 0)\n status.style.display = 'none'\n listDiv.innerHTML = `<h3>Indexed Farm Domains</h3>\n <table>\n <tr><th>Domain</th><th>Pages</th></tr>\n ${domains.map(({ domain, page_count }) => `\n <tr>\n <td><img class=\"sim-flag remote\" src=\"${window.wiki.site(domain).flag()}\"\n title=\"${domain}\" data-site=\"${domain}\"> ${domain}</td>\n <td>${page_count != null ? page_count.toLocaleString() : '\u2014'}</td>\n </tr>`).join('')}\n </table>\n <p class=\"sim-count\">${domains.length} domains \u2014 ${totalPages.toLocaleString()} pages${cacheNote(ts)}</p>`\n }\n\n if (cache?.domains) {\n renderList(cache.domains, cache.ts)\n } else {\n ;(async () => {\n try {\n const eff = await specsP\n const patterns = eff.length ? eff.join(',') : '*'\n const url = `${origin}/system/indexed-domains.json?pattern=${encodeURIComponent(patterns)}&limit=${limit}`\n const res = await fetch(url)\n if (!res.ok) throw new Error(`indexed-domains failed: ${res.status}`)\n const domains = await res.json()\n if (!domains.length) { status.textContent = 'No indexed domains found'; return }\n renderList(domains, null)\n writeCache(item, { domains })\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n }\n })()\n }\n\n } else if (mode === 'similar') {\n const results = div.find('.sim-results')[0]\n\n const renderScored = (scored, ts) => {\n if (!scored.length) {\n status.textContent = `No similar pages found above threshold ${threshold}`\n return\n }\n results.innerHTML = `<h3>Similar Pages</h3><ul>${\n scored.map(({ domain, slug, title, score }) =>\n `<li>${simLink(domain, slug, title, score)}</li>`).join('')\n }</ul><p class=\"sim-count\">${scored.length} found ${scopeLabel}${cacheNote(ts)}</p>`\n status.style.display = 'none'\n }\n\n if (cache?.scored) {\n renderScored(cache.scored, cache.ts)\n } else {\n ;(async () => {\n try {\n // SUBJECT: act on the previous lineup page; otherwise this page.\n const $page = div.parents('.page')\n const s = subject || (() => {\n const pageTitle = $page.find('.title').text().trim() || document.title\n return { slug: slugify(pageTitle), site: window.location.hostname,\n title: pageTitle, text: null, isSelf: true }\n })()\n\n const domainEntries = await loadDomainEntries(await specsP, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = (subject ? `${subjectNote(s)} \u00B7 ` : '') +\n `Searching ${total.toLocaleString()} pages\u2026`\n\n let qVec = await lookupPageVector(s.slug, s.site)\n if (!qVec) {\n status.textContent = 'Embedding page (not yet indexed)\u2026'\n const pageText = s.text || $page.find('.item')\n .map((_, el) => $(el).text().trim()).get().filter(Boolean).join('\\n')\n qVec = await getEmbedding(pageText || s.title, origin)\n }\n\n const scored = cosineScan(qVec, domainEntries, {\n threshold, limit, excludeSlug: s.slug, excludeDomain: s.site,\n })\n renderScored(scored, null)\n if (subject) { // keep the subject visible above the results\n status.textContent = subjectNote(s)\n status.style.display = ''\n }\n if (scored.length && !subject) writeCache(item, { scored })\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n }\n })()\n }\n\n } else if (mode === 'ghost') {\n // Ghost mode \u2014 fetch page-json from any URL and open it as a ghost page\n const btn = div.find('.sim-btn')[0]\n\n const doGhost = async () => {\n if (!ghostUrl) { status.textContent = 'No URL \u2014 GHOST needs a page-json URL'; return }\n btn.disabled = true\n status.textContent = 'Fetching\u2026'\n try {\n const res = await fetch(ghostUrl)\n if (!res.ok) throw new Error(`fetch failed: ${res.status}`)\n const page = await res.json()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n status.textContent = ''\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doGhost)\n\n } else if (mode === 'build') {\n // Build mode \u2014 trigger a semantic index build; result opens as a ghost page\n const btn = div.find('.sim-btn')[0]\n\n const doBuild = async () => {\n btn.disabled = true\n status.textContent = 'Building index\u2026 (may take a while for large scopes)'\n try {\n const domains = encodeURIComponent((specs.length ? specs : ['*']).join(','))\n const res = await fetch(\n `${origin}/system/build-index.json?domains=${domains}&force=${force ? 1 : 0}`)\n if (res.status === 501) {\n const body = await res.json().catch(() => ({}))\n throw new Error(body.hint || 'indexing runs on the farm indexer, not this server')\n }\n if (!res.ok) throw new Error(`build-index failed: ${res.status}`)\n const page = await res.json()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n status.textContent = ''\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doBuild)\n\n } else if (mode === 'report') {\n // Report mode \u2014 server-side ranked/bundled search, opens result as ghost page\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n let readyLine = `Domains: ${specs.length ? specs.join(', ') : '*'}`\n\n // SUBJECT: the host page IS the query \u2014 prefill its title (editable) and\n // seed the server search with its stored vector.\n if (subject && !input.value) input.value = subject.title\n\n // Preload the lightweight domain listing (counts only, no vectors) so the\n // status line shows scope and config before any search is issued.\n ;(async () => {\n try {\n const eff = await specsP\n const domains = await resolveDomains(eff.length ? eff : ['*'], origin)\n const pages = domains.reduce((n, d) => n + (d.page_count || 0), 0)\n readyLine = configSummary('Report ready', pages, domains.length)\n if (farms.length) readyLine += ` \u00B7 +${farms.length} peer farm${farms.length > 1 ? 's' : ''}`\n if (subject) readyLine = `${subjectNote(subject)} \u00B7 ${readyLine}`\n status.textContent = readyLine\n } catch (e) {\n status.textContent = `Domain listing unavailable: ${e.message}`\n }\n })()\n\n // 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 const doReport = async () => {\n const query = input.value.trim()\n if (!query) return\n btn.disabled = true\n status.textContent = farms.length ? 'Generating report (asking peer farms)\u2026' : 'Generating report\u2026'\n try {\n const eff = await specsP\n // A hand-edited query is a new question \u2014 drop the page seed then.\n const seeded = subject && query === subject.title ? seedParams() : {}\n const body = { query, domains: eff.length ? eff : ['*'], limit, live, ...seeded }\n if (farms.length) body.farms = farms\n if (thresholdSet) body.threshold = threshold\n const res = await fetch(`${origin}/system/search-report.json`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (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 } else if (mode === 'sites') {\n // Sites mode \u2014 which site should this page go on? Server aggregates the\n // page-vector scan per domain; result opens as a ghost page.\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n let readyLine = `Domains: ${specs.length ? specs.join(', ') : '*'}`\n\n if (subject && !input.value) input.value = subject.title\n\n ;(async () => {\n try {\n const eff = await specsP\n const domains = await resolveDomains(eff.length ? eff : ['*'], origin)\n const pages = domains.reduce((n, d) => n + (d.page_count || 0), 0)\n readyLine = configSummary('Site report ready', pages, domains.length)\n if (subject) readyLine = `${subjectNote(subject)} \u00B7 ${readyLine}`\n status.textContent = readyLine\n } catch (e) {\n status.textContent = `Domain listing unavailable: ${e.message}`\n }\n })()\n\n const doSites = async () => {\n const query = input.value.trim()\n if (!query) return\n btn.disabled = true\n status.textContent = 'Ranking sites\u2026'\n try {\n const eff = await specsP\n const seeded = subject && query === subject.title ? seedParams() : {}\n const res = await fetch(`${origin}/system/site-report.json`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ query, domains: eff.length ? eff : ['*'], limit, ...seeded }),\n })\n if (!res.ok) throw new Error(`site-report failed: ${res.status}`)\n const page = await res.json()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n status.textContent = readyLine\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doSites)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doSites() })\n\n } else if (mode === 'keyword') {\n // Keyword mode \u2014 galactic MiniSearch: the server reads each site's own\n // per-edit site-index.json; result opens as a ghost page.\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n\n // SUBJECT: keyword search wants words, not vectors \u2014 the host title is\n // the prewired query (editable), finding forks and namesakes alike.\n if (subject && !input.value) input.value = subject.title\n\n status.textContent = (subject ? `${subjectNote(subject)} \u00B7 ` : '') +\n `Keyword search ready \u2014 domains: ${specs.length ? specs.join(', ') : '*'} \u00B7 limit ${limit}`\n\n const doKeyword = async () => {\n const query = input.value.trim()\n if (!query) return\n btn.disabled = true\n status.textContent = 'Searching live site indexes\u2026'\n try {\n const eff = await specsP\n const pattern = encodeURIComponent((eff.length ? eff : ['*']).join(','))\n const farmsParam = farms.length ? `&farms=${encodeURIComponent(farms.join(','))}` : ''\n const res = await fetch(\n `${origin}/system/farm-search.json?q=${encodeURIComponent(query)}&pattern=${pattern}&limit=${limit}${farmsParam}`)\n if (!res.ok) throw new Error(`farm-search failed: ${res.status}`)\n const page = await res.json()\n window.wiki.showResult(window.wiki.newPage(page), { $page: div.parents('.page') })\n status.textContent = `Keyword search ready \u2014 domains: ${specs.length ? specs.join(', ') : '*'} \u00B7 limit ${limit}`\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doKeyword)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doKeyword() })\n\n } else if (mode === 'author') {\n // Author mode \u2014 same search form but creates a ghost page from results\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n const results = div.find('.sim-results')[0]\n let domainEntries = null\n\n if (subject && !input.value) input.value = subject.title\n\n ;(async () => {\n try {\n if (!cache) status.textContent = 'Resolving domains\u2026'\n domainEntries = await loadDomainEntries(await specsP, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = (subject ? `${subjectNote(subject)} \u00B7 ` : '') +\n configSummary('Author ready', total, domainEntries.length)\n } catch (e) {\n status.textContent = `Load error: ${e.message}`\n }\n })()\n\n const doAuthor = async () => {\n const query = input.value.trim()\n if (!query || !domainEntries) return\n btn.disabled = true\n status.textContent = 'Embedding query\u2026'\n results.innerHTML = ''\n try {\n const qVec = await getEmbedding(query, origin)\n const scored = cosineScan(qVec, domainEntries,\n { threshold, limit, excludeSlug: null, excludeDomain: null })\n\n // Unique titles for wiki-link list\n const seenTitles = new Set()\n const uniqueTitles = []\n for (const { title } of scored) {\n if (!seenTitles.has(title)) { seenTitles.add(title); uniqueTitles.push(title) }\n }\n\n const hexId = () => Math.floor(Math.random() * 0xffffffffffffffff).toString(16).padStart(16, '0')\n const primaryLines = uniqueTitles.map(t => `- [[${t}]]`).join('\\n')\n\n const story = [\n { type: 'markdown', id: hexId(), text: `# Similar Pages\\n\\n${primaryLines}` },\n { type: 'markdown', id: hexId(), text: '# Reference Links' },\n ...scored.map(({ domain, slug, title, score }) => ({\n type: 'reference', id: hexId(), site: domain, slug, title,\n text: `score ${score.toFixed(3)}`,\n })),\n ]\n\n const pageObj = window.wiki.newPage({ title: `${query} Results`, story, journal: [] })\n window.wiki.showResult(pageObj, { $page: div.parents('.page') })\n\n status.textContent = `${scored.length} pages found`\n writeCache(item, { scored, query })\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doAuthor)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doAuthor() })\n\n } else {\n // Search form mode\n const input = div.find('.sim-input')[0]\n const btn = div.find('.sim-btn')[0]\n const results = div.find('.sim-results')[0]\n let domainEntries = null\n\n // Show cached results immediately while domains preload in background\n if (cache?.scored) {\n input.value = cache.query || ''\n results.innerHTML = cache.scored.map(({ domain, slug, title, score }) =>\n `<div class=\"sim-result\">${simLink(domain, slug, title, score)}</div>`).join('') +\n `<p class=\"sim-count\">Top ${cache.scored.length} for \"${cache.query || ''}\"${cacheNote(cache.ts)}</p>`\n status.textContent = ''\n }\n\n if (subject && !input.value) input.value = subject.title\n\n ;(async () => {\n try {\n if (!cache) status.textContent = 'Resolving domains\u2026'\n domainEntries = await loadDomainEntries(await specsP, origin)\n const total = domainEntries.reduce((n, e) => n + e.pages.length, 0)\n status.textContent = (subject ? `${subjectNote(subject)} \u00B7 ` : '') +\n configSummary('Search ready', total, domainEntries.length)\n } catch (e) {\n status.textContent = `Load error: ${e.message}`\n }\n })()\n\n const doSearch = async () => {\n const query = input.value.trim()\n if (!query || !domainEntries) return\n btn.disabled = true\n status.textContent = 'Embedding query\u2026'\n results.innerHTML = ''\n try {\n const qVec = await getEmbedding(query, origin)\n const scored = cosineScan(qVec, domainEntries,\n { threshold, limit, excludeSlug: null, excludeDomain: null })\n results.innerHTML = scored.map(({ domain, slug, title, score }) =>\n `<div class=\"sim-result\">${simLink(domain, slug, title, score)}</div>`).join('') +\n `<p class=\"sim-count\">Top ${scored.length} for \"${query}\"</p>`\n status.textContent = ''\n writeCache(item, { scored, query })\n } catch (e) {\n status.textContent = `Error: ${e.message}`\n } finally {\n btn.disabled = false\n }\n }\n\n btn.addEventListener('click', doSearch)\n input.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch() })\n }\n}\n\n// \u2500\u2500 Register item type with the wiki \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nif (typeof window !== 'undefined') {\n window.plugins = window.plugins || {}\n window.plugins.similarity = { emit, bind }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["SIMILAR_THRESHOLDS", "DEFAULT_THRESHOLD", "DEFAULT_LIMIT", "parseDSL", "text", "specs", "rosterRefs", "farms", "threshold", "limit", "mode", "live", "subject", "force", "ghostUrl", "label", "isCmd", "upper", "kw", "val", "line", "raw", "ref", "peer", "level", "tv", "isGlob", "spec", "isScope", "slugify", "title", "domainCache", "resolveDomainsForSpec", "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", "STYLES", "siteFlag", "domain", "score", "simLink", "domain", "slug", "title", "score", "siteFlag", "emit", "div", "item", "mode", "specs", "threshold", "limit", "force", "ghostUrl", "label", "parseDSL", "STYLES", "btnLabel", "hint", "bind", "rosterRefs", "farms", "live", "subjectFlag", "thresholdSet", "origin", "status", "subject", "resolveSubject", "cache", "readCache", "specsP", "effectiveSpecs", "seedParams", "configSummary", "verb", "pages", "nDomains", "parts", "e", "$a", "$page", "scopeLabel", "cacheNote", "ts", "cacheAge", "listDiv", "ago", "iso", "days", "day", "health", "domains", "r", "emb", "built", "d", "n", "down", "upLine", "via", "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", "EMBEDDER_DOWN", "keywordInstead", "query", "pattern", "farmsParam", "doReport", "seeded", "why", "pageObj", "doSites", "doKeyword", "doAuthor", "seenTitles", "uniqueTitles", "hexId", "primaryLines", "t", "story", "doSearch"]
|
|
7
7
|
}
|
package/factory.json
CHANGED
package/package.json
CHANGED
|
@@ -74,12 +74,17 @@
|
|
|
74
74
|
{
|
|
75
75
|
"type": "markdown",
|
|
76
76
|
"id": "c72e1a4f08b5d693",
|
|
77
|
-
"text": "0.15.0 makes a dead embedder degrade honestly. The embedder and the keyword index share nothing, so one can be down while the other is fine \u2014 but a report that could not embed its query used to leave a bare status code and no results. It now says semantic search is unavailable, gives the reason, and answers the same query by keyword instead. The result page is titled \u201cKeyword Search\u201d by the server, so the reader can see which tier answered."
|
|
77
|
+
"text": "0.15.0 makes a dead embedder degrade honestly. The embedder and the keyword index share nothing, so one can be down while the other is fine \u2014 but a report that could not embed its query used to leave a bare status code and no results. It now says semantic search is unavailable, gives the reason, and answers the same query by keyword instead. The result page is titled \u201cKeyword Search\u201d by the server, so the reader can see which tier answered. 0.15.1 makes that fallback actually fire: an unreachable remote embedder arrived as a bare 500, indistinguishable from a bad query, so the client had nothing to trigger on. A proxy that cannot be reached is now the same outage as a local embedder that will not start, and answers 503."
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"type": "markdown",
|
|
81
|
+
"id": "e91b3c7d25a4f608",
|
|
82
|
+
"text": "0.16.0 ships a **Search Tool** page and the two DSL words it needed. `STATUS` renders the state of the index behind the answers \u2014 which model, how many domains and pages, when the vectors were newest and oldest built, and whether a query can be embedded at all. `SITE` means the domain the page is served from, so a page shipped to every site in a farm can say \"search here\" without naming anywhere; leaving the scope empty means the whole farm in the report modes, which is not the same thing. `indexed-domains.json` gained a `built` date per domain to feed the status panel."
|
|
78
83
|
},
|
|
79
84
|
{
|
|
80
85
|
"type": "markdown",
|
|
81
86
|
"id": "21eb5354d2adb29e",
|
|
82
|
-
"text": "**Version:** 0.
|
|
87
|
+
"text": "**Version:** 0.16.0\n\n# Links\n\n- [https://github.com/Hitchhikers-Guide-to-the-Galaxy/wiki-plugin-similarity GitHub repository]\n- [https://www.npmjs.com/package/wiki-plugin-similarity npm package]\n- [https://plugin.fedwiki.club/similarity-plugin.html Plugin documentation]\n- [https://search.fedwiki.club/ Search hub] \u2014 architecture, indexing, freshness."
|
|
83
88
|
}
|
|
84
89
|
],
|
|
85
90
|
"plugin": "similarity",
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Search Tool",
|
|
3
|
+
"story": [
|
|
4
|
+
{
|
|
5
|
+
"type": "markdown",
|
|
6
|
+
"id": "e13d10c38c88ff88",
|
|
7
|
+
"text": "Search this wiki, and the wikis around it. Ask for pages like the one you were just reading, or search this site, the whole farm, or the wider federation \u2014 and see the state of the index the answers come from."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"type": "markdown",
|
|
11
|
+
"id": "eb958d43d4264fd5",
|
|
12
|
+
"text": "## Like the page you came from\n\nOpened beside another page, this searches for pages close in meaning to that one \u2014 no typing. Opened on its own, it works from this page instead."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"type": "similarity",
|
|
16
|
+
"id": "ac3777b8ddf08f4a",
|
|
17
|
+
"text": "SIMILAR: medium\nSUBJECT\n*\nLIMIT: 10"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"type": "markdown",
|
|
21
|
+
"id": "ecb88f9a4ca03e53",
|
|
22
|
+
"text": "## Search this site"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"type": "similarity",
|
|
26
|
+
"id": "240ca379bbbf95ff",
|
|
27
|
+
"text": "SITE\nREPORT\nLIMIT: 10"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"type": "markdown",
|
|
31
|
+
"id": "99d53ddabf48c89e",
|
|
32
|
+
"text": "## Search the whole farm"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"type": "similarity",
|
|
36
|
+
"id": "bde4ee0cb2cbc014",
|
|
37
|
+
"text": "*\nREPORT\nLIMIT: 10"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"type": "markdown",
|
|
41
|
+
"id": "12487c2e344fde5e",
|
|
42
|
+
"text": "## Search the federation\n\nSites beyond this farm, indexed separately. Slower, and deliberately opt-in \u2014 the wildcard scope never reaches outside on its own."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"type": "similarity",
|
|
46
|
+
"id": "e0c23ec26680bb1d",
|
|
47
|
+
"text": "GALAXY\nREPORT\nLIMIT: 10"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"type": "markdown",
|
|
51
|
+
"id": "4093f4900370bf1e",
|
|
52
|
+
"text": "## Where should a page like this live?\n\nPaste a title and opening paragraph; the farm votes on which site it belongs to."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"type": "similarity",
|
|
56
|
+
"id": "3427aaebe0af9cf0",
|
|
57
|
+
"text": "*\nSITES\nLIMIT: 5"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"type": "markdown",
|
|
61
|
+
"id": "1917c2f8c6bf118b",
|
|
62
|
+
"text": "# The Index"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"type": "similarity",
|
|
66
|
+
"id": "cb738ee74c6228ab",
|
|
67
|
+
"text": "STATUS"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"type": "markdown",
|
|
71
|
+
"id": "cf2ec2e014ea8ded",
|
|
72
|
+
"text": "# Make This Page Your Own\n\nThis page is shipped by the [[Similarity Plugin]], which is why it is here on a site that never created it. Fork it \u2014 the \u2630 menu, then Fork \u2014 and your copy wins from then on: the server looks in this site's own pages first and only falls back to the plugin's copy when there is nothing there.\n\nSo change it. Narrow the farm search to the sites you actually read, drop the federation search if it is never what you want, add a search for a subject you return to. The searches are written as plain lines in each item \u2014 open one and read it. [[About Similarity Plugin]] explains the words."
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"type": "markdown",
|
|
76
|
+
"id": "7f64f839442f38eb",
|
|
77
|
+
"text": "# See\n\n- [[Semantic Search]] \u2014 the same searches, without the framing\n- [[About Similarity Plugin]] \u2014 the DSL these items are written in\n- [[SemIndex Plugin]] \u2014 what builds the index this page reports on"
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"journal": [
|
|
81
|
+
{
|
|
82
|
+
"type": "create",
|
|
83
|
+
"item": {
|
|
84
|
+
"title": "Search Tool",
|
|
85
|
+
"story": [
|
|
86
|
+
{
|
|
87
|
+
"type": "markdown",
|
|
88
|
+
"id": "e13d10c38c88ff88",
|
|
89
|
+
"text": "Search this wiki, and the wikis around it. Ask for pages like the one you were just reading, or search this site, the whole farm, or the wider federation \u2014 and see the state of the index the answers come from."
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"type": "markdown",
|
|
93
|
+
"id": "eb958d43d4264fd5",
|
|
94
|
+
"text": "## Like the page you came from\n\nOpened beside another page, this searches for pages close in meaning to that one \u2014 no typing. Opened on its own, it works from this page instead."
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"type": "similarity",
|
|
98
|
+
"id": "ac3777b8ddf08f4a",
|
|
99
|
+
"text": "SIMILAR: medium\nSUBJECT\n*\nLIMIT: 10"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"type": "markdown",
|
|
103
|
+
"id": "ecb88f9a4ca03e53",
|
|
104
|
+
"text": "## Search this site"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"type": "similarity",
|
|
108
|
+
"id": "240ca379bbbf95ff",
|
|
109
|
+
"text": "SITE\nREPORT\nLIMIT: 10"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"type": "markdown",
|
|
113
|
+
"id": "99d53ddabf48c89e",
|
|
114
|
+
"text": "## Search the whole farm"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"type": "similarity",
|
|
118
|
+
"id": "bde4ee0cb2cbc014",
|
|
119
|
+
"text": "*\nREPORT\nLIMIT: 10"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"type": "markdown",
|
|
123
|
+
"id": "12487c2e344fde5e",
|
|
124
|
+
"text": "## Search the federation\n\nSites beyond this farm, indexed separately. Slower, and deliberately opt-in \u2014 the wildcard scope never reaches outside on its own."
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"type": "similarity",
|
|
128
|
+
"id": "e0c23ec26680bb1d",
|
|
129
|
+
"text": "GALAXY\nREPORT\nLIMIT: 10"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"type": "markdown",
|
|
133
|
+
"id": "4093f4900370bf1e",
|
|
134
|
+
"text": "## Where should a page like this live?\n\nPaste a title and opening paragraph; the farm votes on which site it belongs to."
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"type": "similarity",
|
|
138
|
+
"id": "3427aaebe0af9cf0",
|
|
139
|
+
"text": "*\nSITES\nLIMIT: 5"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"type": "markdown",
|
|
143
|
+
"id": "1917c2f8c6bf118b",
|
|
144
|
+
"text": "# The Index"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"type": "similarity",
|
|
148
|
+
"id": "cb738ee74c6228ab",
|
|
149
|
+
"text": "STATUS"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"type": "markdown",
|
|
153
|
+
"id": "cf2ec2e014ea8ded",
|
|
154
|
+
"text": "# Make This Page Your Own\n\nThis page is shipped by the [[Similarity Plugin]], which is why it is here on a site that never created it. Fork it \u2014 the \u2630 menu, then Fork \u2014 and your copy wins from then on: the server looks in this site's own pages first and only falls back to the plugin's copy when there is nothing there.\n\nSo change it. Narrow the farm search to the sites you actually read, drop the federation search if it is never what you want, add a search for a subject you return to. The searches are written as plain lines in each item \u2014 open one and read it. [[About Similarity Plugin]] explains the words."
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"type": "markdown",
|
|
158
|
+
"id": "7f64f839442f38eb",
|
|
159
|
+
"text": "# See\n\n- [[Semantic Search]] \u2014 the same searches, without the framing\n- [[About Similarity Plugin]] \u2014 the DSL these items are written in\n- [[SemIndex Plugin]] \u2014 what builds the index this page reports on"
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
},
|
|
163
|
+
"date": 1788434000000,
|
|
164
|
+
"provenance": "Claude Code, 3 September 2026, through a dialogue with David Bovill"
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
"plugin": "similarity"
|
|
168
|
+
}
|
package/server/server.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Routes:
|
|
9
9
|
// GET /system/indexed-domains.json?pattern=glob1,glob2
|
|
10
|
-
// [{domain, page_count}] for domains with a semantic-vectors.json
|
|
10
|
+
// [{domain, page_count, built}] for domains with a semantic-vectors.json;
|
|
11
|
+
// built is when those vectors were last written (ISO, since 0.16.0).
|
|
11
12
|
// GET /system/semantic-vectors.json[?domain=]
|
|
12
13
|
// Serves {farm}/{domain}/status/semantic-vectors.json.
|
|
13
14
|
// GET|POST /system/embed.json ?text=… | {text}
|
|
@@ -267,8 +268,18 @@ const startServer = ({ argv, app }) => {
|
|
|
267
268
|
|
|
268
269
|
async function embedText(text) {
|
|
269
270
|
const url = embedUrl()
|
|
270
|
-
if (url) return (
|
|
271
|
-
|
|
271
|
+
if (!url) return embedder.embed(text)
|
|
272
|
+
try {
|
|
273
|
+
return (await postJson(url, { text })).vector
|
|
274
|
+
} catch (e) {
|
|
275
|
+
// A proxy we cannot reach is the same outage as a local embedder that
|
|
276
|
+
// will not start, and callers must be able to tell "no vector to be had"
|
|
277
|
+
// from "bad query". Without this the failure arrived as a bare 500 and
|
|
278
|
+
// the client could not know to fall back to keyword search.
|
|
279
|
+
const err = new Error(`remote embedder at ${url}: ${e.message}`)
|
|
280
|
+
err.code = 'EMBEDDER_DOWN'
|
|
281
|
+
throw err
|
|
282
|
+
}
|
|
272
283
|
}
|
|
273
284
|
|
|
274
285
|
const cors = res => {
|
|
@@ -302,13 +313,18 @@ const startServer = ({ argv, app }) => {
|
|
|
302
313
|
let results = listDomains(farms, patterns, restricted, 'status/semantic-vectors.json')
|
|
303
314
|
.filter(({ domain }) => visible(req, domain))
|
|
304
315
|
.map(({ farm, domain }) => {
|
|
316
|
+
const file = path.join(farm, domain, 'status', 'semantic-vectors.json')
|
|
305
317
|
let pageCount = null
|
|
318
|
+
let built = null
|
|
306
319
|
try {
|
|
307
|
-
|
|
308
|
-
|
|
320
|
+
// When the vectors were last written. One stat on top of a read we
|
|
321
|
+
// are already paying for, and it is what lets a reader see whether
|
|
322
|
+
// the index behind an answer is current or months old.
|
|
323
|
+
built = new Date(fs.statSync(file).mtimeMs).toISOString()
|
|
324
|
+
const pages = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
309
325
|
pageCount = Array.isArray(pages) ? pages.length : null
|
|
310
326
|
} catch { /* ignore */ }
|
|
311
|
-
return { domain, page_count: pageCount }
|
|
327
|
+
return { domain, page_count: pageCount, built }
|
|
312
328
|
})
|
|
313
329
|
if (limit) results = results.slice(0, limit)
|
|
314
330
|
res.json(results)
|