wiki-plugin-similarity 0.13.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- /* wiki-plugin-similarity - 0.13.0 - Tue, 18 Aug 2026 13:37:47 GMT */
1
+ /* wiki-plugin-similarity - 0.13.1 - Sun, 30 Aug 2026 12:37:30 GMT */
2
2
  (()=>{var q={high:.78,medium:.68,low:.58},N=q.medium,J=10,_=t=>{let e=[],s=[],i=[],p=null,E=null,f="search",y=!1,L=!1,b=!1,k=null,C=null,S=(a,r)=>a===r||a.startsWith(r)&&/^[\s:]/.test(a.slice(r.length)),w=(a,r)=>a.slice(r.length).replace(/^\s*:?\s*/,"").trim();for(let a of t.split(`
3
3
  `)){let r=a.trim();if(!r||r.startsWith("#"))continue;let g=r.toUpperCase();if(S(g,"LIVE")){y=!0;continue}if(S(g,"SUBJECT")){L=!0;continue}if(S(g,"AUTHOR")){!e.length&&f==="search"&&(f="author");continue}if(S(g,"REPORT")){f==="search"&&(f="report");continue}if(S(g,"KEYWORD")){f==="search"&&(f="keyword");continue}if(S(g,"SITES")){f==="search"&&(f="sites");continue}if(S(g,"BUILD")){f==="search"&&(f="build");continue}if(S(g,"FORCE")){b=!0;continue}if(S(g,"GHOST")){k=w(r,"GHOST"),f==="search"&&(f="ghost");continue}if(S(g,"BUTTON")){C=w(r,"BUTTON");continue}if(S(g,"ROSTER")){let v=w(r,"ROSTER");v&&s.push(v);continue}if(S(g,"FARM")){let v=w(r,"FARM");v&&i.push(v);continue}if(S(g,"LIST")){!e.length&&f==="search"&&(f="list");continue}if(S(g,"SIMILAR")){let v=w(g,"SIMILAR").toLowerCase();p=q[v]||N,!e.length&&f==="search"&&(f="similar");continue}if(S(g,"THRESHOLD")){let v=w(r,"THRESHOLD");p=q[v.toLowerCase()]??(parseFloat(v)||N);continue}if(S(g,"LIMIT")){E=parseInt(w(r,"LIMIT"))||J;continue}e.push(["PUBLIC","LOCAL","PRIVATE","GALAXY"].includes(g)?g:r)}return{mode:f,specs:e,rosterRefs:s,farms:i,threshold:p??N,limit:E??J,live:y,subject:L,force:b,ghostUrl:k,label:C,thresholdSet:p!==null}},Y=t=>t.includes("*")||t.includes("?"),W=t=>t==="PUBLIC"||t==="LOCAL"||t==="PRIVATE"||t==="GALAXY";var X=t=>t.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,""),B=new Map,pt=async(t,e)=>{if(B.has(t))return B.get(t);let s=`${e}/system/indexed-domains.json?pattern=${encodeURIComponent(t)}`,i=await fetch(s);if(!i.ok)throw new Error(`indexed-domains failed: ${i.status}`);let p=await i.json();return B.set(t,p),p},O=async(t,e)=>{t.length||(t=[window.location.hostname]);let s=new Set,i=[];for(let p of t)if(p==="*"||Y(p)||W(p))for(let E of await pt(p,e))s.has(E.domain)||(s.add(E.domain),i.push(E));else s.has(p)||(s.add(p),i.push({domain:p,page_count:null}));return i},gt=/^([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+|localhost)(:\d+)?$/,ht=/^ROSTER ([A-Za-z0-9.\-:]+\/[a-z0-9-]+)$/,wt=/^REFERENCES ([A-Za-z0-9.\-:]+\/[a-z0-9-]+)$/,Z=async t=>{let e=t.indexOf("/"),s=await fetch(`//${t.slice(0,e)}/${t.slice(e+1)}.json`);if(!s.ok)throw new Error(`roster page ${t} failed: ${s.status}`);return s.json()},Q=async(t,e,s)=>{if(s.has(t))return;s.add(t);let i;try{i=await Z(t)}catch(p){console.warn(p.message);return}for(let p of i.story||[])if(p.type==="roster")for(let E of(p.text||"").split(/\r?\n/)){let f=E.trim();if(!f)continue;let y=f.match(gt);if(y){e.add(y[0]);continue}let L=f.match(ht);if(L){await Q(L[1],e,s);continue}let b=f.match(wt);if(b)try{let k=await Z(b[1]);for(let C of k.story||[])C.type==="reference"&&C.site&&e.add(C.site)}catch(k){console.warn(k.message)}}},yt=async t=>{let e=new Set,s=new Set;for(let i of t)await Q(i,e,s);return[...e]},tt=async(t,e)=>e.length?[...t,...await yt(e)]:t;var G=new Map,$t=t=>`${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(t)}`,et=async t=>{if(G.has(t))return G.get(t);let e=await fetch($t(t));if(!e.ok)return[];let s=await e.json();return G.set(t,s),s},U=async(t,e)=>{let s=await fetch(`${e}/system/embed.json`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text:t})});if(!s.ok)throw new Error(`embed failed: ${s.status}`);return(await s.json()).vector},st=async(t,e)=>{let i=(await et(e)).find(p=>p.slug===t);return i?i.vector:null},M=(t,e,{threshold:s,limit:i,excludeSlug:p,excludeDomain:E})=>{let f=[];for(let{domain:y,pages:L}of e)for(let{slug:b,title:k,vector:C}of L){if(b===p&&y===E)continue;let S=0;for(let w=0;w<t.length;w++)S+=t[w]*C[w];S>=s&&f.push({domain:y,slug:b,title:k,score:S})}return f.sort((y,L)=>L.score-y.score),f.slice(0,i)},z=async(t,e)=>{let s=await O(t,e);return(await Promise.all(s.map(async({domain:p})=>({domain:p,pages:await et(p)})))).filter(p=>p.pages.length>0)};var nt=t=>{let e=t.parents(".page:first"),s=e.prev(".page"),i=s.length?s:e,p=(i.attr("id")||"").split("_rev")[0],E=i.data("site")||window.location.hostname,f=i.data("data")||{},y=f.title||i.find(".title").text().trim()||p,L=[y,...(f.story||[]).filter(b=>b.type==="markdown"||b.type==="paragraph").map(b=>(b.text||"").trim()).filter(Boolean)].join(`
4
4
  `).slice(0,2e3);return{slug:p,site:E,title:y,text:L,isSelf:!s.length}},T=t=>`Subject: ${t.title}${t.isSelf?" (this page)":` @ ${t.site}`}`;var it=t=>`sim-cache-${t}`,ot=t=>{try{let e=JSON.parse(localStorage.getItem(it(t.id))||"null");return e?.text===(t.text||"")?e:null}catch{return null}},P=(t,e)=>{try{localStorage.setItem(it(t.id),JSON.stringify({text:t.text||"",ts:Date.now(),...e}))}catch{}},at=t=>{let e=Math.floor((Date.now()-t)/1e3);return e<60?`${e}s ago`:e<3600?`${Math.floor(e/60)}m ago`:e<86400?`${Math.floor(e/3600)}h ago`:`${Math.floor(e/86400)}d ago`};var D=`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-plugin-similarity",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Federated Wiki plugin — semantic similarity search across wiki domains using prebuilt vector indexes",
5
5
  "keywords": [
6
6
  "wiki",
@@ -10,10 +10,12 @@
10
10
  // at index time because off-farm pages have no local page JSON for
11
11
  // search-report's enrich() to read.
12
12
  //
13
- // Farm vectors (149 MB across the whole farm) are cheap enough to re-read per
14
- // query; galaxy scale is not. This module holds deserialized per-site entries
15
- // in a byte-capped LRU keyed by file mtime, so a repeat query over a followed
16
- // roster costs no JSON parsing at all.
13
+ // This module holds deserialized per-site entries in a byte-capped LRU keyed
14
+ // by file path + mtime, so a repeat query costs no JSON parsing at all. Since
15
+ // 0.13.1 the FARM vectors ride the same cache: "cheap enough to re-read per
16
+ // query" proved false at 400 sites / 150 MB — the synchronous re-parse
17
+ // blocked the event loop for tens of seconds and 502'd concurrent page saves
18
+ // (Semantic ReIndex Plan, Phase 1; the yellow-halo bug, 2026-08-30).
17
19
 
18
20
  const fs = require('node:fs')
19
21
  const path = require('node:path')
@@ -28,29 +30,28 @@ const galaxyRoot = () => {
28
30
  return path.join(process.env.HOME || '/tmp', '.cache', 'wiki-similarity', 'galaxy-vectors')
29
31
  }
30
32
 
31
- // domain → {mtimeMs, bytes, pages}
33
+ // file path → {mtimeMs, bytes, pages} — keyed by path, not domain, so the
34
+ // same domain name in different farms (or farm vs galaxy) can never collide.
32
35
  const cache = new Map()
33
36
  let cachedBytes = 0
34
37
 
35
38
  const evictUntil = budget => {
36
- for (const [domain, entry] of cache) {
39
+ for (const [file, entry] of cache) {
37
40
  if (cachedBytes <= budget) break
38
- cache.delete(domain)
41
+ cache.delete(file)
39
42
  cachedBytes -= entry.bytes
40
43
  }
41
44
  }
42
45
 
43
- // Same contract as search-report's loadVectors(farm, domain) — returns the
44
- // entry array (with enrichment fields) or [].
45
- const loadGalaxyVectors = (farm, domain) => {
46
- const file = path.join(farm, domain, 'status', 'semantic-vectors.json')
46
+ // The cache core: one vectors FILE → its entry array (or []), mtime-fresh.
47
+ const loadVectorsFile = file => {
47
48
  let stat
48
49
  try { stat = fs.statSync(file) } catch { return [] }
49
50
 
50
- const hit = cache.get(domain)
51
+ const hit = cache.get(file)
51
52
  if (hit && hit.mtimeMs === stat.mtimeMs) {
52
- cache.delete(domain) // refresh LRU position
53
- cache.set(domain, hit)
53
+ cache.delete(file) // refresh LRU position
54
+ cache.set(file, hit)
54
55
  return hit.pages
55
56
  }
56
57
 
@@ -58,17 +59,23 @@ const loadGalaxyVectors = (farm, domain) => {
58
59
  try { pages = JSON.parse(fs.readFileSync(file, 'utf8')) } catch { return [] }
59
60
  if (!Array.isArray(pages)) return []
60
61
 
61
- if (hit) { cache.delete(domain); cachedBytes -= hit.bytes }
62
+ if (hit) { cache.delete(file); cachedBytes -= hit.bytes }
62
63
  cachedBytes += stat.size
63
- cache.set(domain, { mtimeMs: stat.mtimeMs, bytes: stat.size, pages })
64
+ cache.set(file, { mtimeMs: stat.mtimeMs, bytes: stat.size, pages })
64
65
  evictUntil(CACHE_BYTES)
65
66
  return pages
66
67
  }
67
68
 
69
+ // Same contract as the old loadVectors(farm, domain) — farm or galaxy tree.
70
+ const loadVectorsCached = (farm, domain) =>
71
+ loadVectorsFile(path.join(farm, domain, 'status', 'semantic-vectors.json'))
72
+
73
+ const loadGalaxyVectors = loadVectorsCached
74
+
68
75
  const galaxyCacheStats = () => ({
69
76
  sites: cache.size,
70
77
  bytes: cachedBytes,
71
78
  capBytes: CACHE_BYTES,
72
79
  })
73
80
 
74
- module.exports = { galaxyRoot, loadGalaxyVectors, galaxyCacheStats }
81
+ module.exports = { galaxyRoot, loadGalaxyVectors, loadVectorsCached, loadVectorsFile, galaxyCacheStats }
Binary file