wiki-plugin-similarity 0.5.0 → 0.5.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.5.0 - Sat, 11 Jul 2026 12:35:40 GMT */
1
+ /* wiki-plugin-similarity - 0.5.1 - Sat, 11 Jul 2026 15:13:27 GMT */
2
2
  (()=>{var z={high:.78,medium:.68,low:.58},O=z.medium,F=10,N=t=>{let n=[],a=null,e=null,r="search",x=!1,y=!1,C=null,b=null,h=(s,d)=>s===d||s.startsWith(d)&&/^[\s:]/.test(s.slice(d.length)),w=(s,d)=>s.slice(d.length).replace(/^\s*:?\s*/,"").trim();for(let s of t.split(`
3
3
  `)){let d=s.trim();if(!d||d.startsWith("#"))continue;let g=d.toUpperCase();if(h(g,"LIVE")){x=!0;continue}if(h(g,"AUTHOR")){!n.length&&r==="search"&&(r="author");continue}if(h(g,"REPORT")){r==="search"&&(r="report");continue}if(h(g,"KEYWORD")){r==="search"&&(r="keyword");continue}if(h(g,"BUILD")){r==="search"&&(r="build");continue}if(h(g,"FORCE")){y=!0;continue}if(h(g,"GHOST")){C=w(d,"GHOST"),r==="search"&&(r="ghost");continue}if(h(g,"BUTTON")){b=w(d,"BUTTON");continue}if(h(g,"LIST")){!n.length&&r==="search"&&(r="list");continue}if(h(g,"SIMILAR")){let S=w(g,"SIMILAR").toLowerCase();a=z[S]||O,!n.length&&r==="search"&&(r="similar");continue}if(h(g,"THRESHOLD")){let S=w(d,"THRESHOLD");a=z[S.toLowerCase()]??(parseFloat(S)||O);continue}if(h(g,"LIMIT")){e=parseInt(w(d,"LIMIT"))||F;continue}n.push(["PUBLIC","LOCAL","PRIVATE"].includes(g)?g:d)}return{mode:r,specs:n,threshold:a??O,limit:e??F,live:x,force:y,ghostUrl:C,label:b,thresholdSet:a!==null}},Y=t=>t.includes("*")||t.includes("?"),Q=t=>t==="PUBLIC"||t==="LOCAL"||t==="PRIVATE",X=t=>t.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,""),U=new Map,Z=async(t,n)=>{if(U.has(t))return U.get(t);let a=`${n}/system/indexed-domains.json?pattern=${encodeURIComponent(t)}`,e=await fetch(a);if(!e.ok)throw new Error(`indexed-domains failed: ${e.status}`);let r=await e.json();return U.set(t,r),r},B=async(t,n)=>{t.length||(t=[window.location.hostname]);let a=new Set,e=[];for(let r of t)if(r==="*"||Y(r)||Q(r))for(let x of await Z(r,n))a.has(x.domain)||(a.add(x.domain),e.push(x));else a.has(r)||(a.add(r),e.push({domain:r,page_count:null}));return e},M=new Map,tt=t=>`${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(t)}`,V=async t=>{if(M.has(t))return M.get(t);let n=await fetch(tt(t));if(!n.ok)return[];let a=await n.json();return M.set(t,a),a},H=async(t,n)=>{let a=await fetch(`${n}/system/embed.json?text=${encodeURIComponent(t)}`);if(!a.ok)throw new Error(`embed failed: ${a.status}`);return(await a.json()).vector},et=async(t,n)=>{let e=(await V(n)).find(r=>r.slug===t);return e?e.vector:null},P=(t,n,{threshold:a,limit:e,excludeSlug:r,excludeDomain:x})=>{let y=[];for(let{domain:C,pages:b}of n)for(let{slug:h,title:w,vector:s}of b){if(h===r&&C===x)continue;let d=0;for(let g=0;g<t.length;g++)d+=t[g]*s[g];d>=a&&y.push({domain:C,slug:h,title:w,score:d})}return y.sort((C,b)=>b.score-C.score),y.slice(0,e)},A=async(t,n)=>{let a=await B(t,n);return(await Promise.all(a.map(async({domain:r})=>({domain:r,pages:await V(r)})))).filter(r=>r.pages.length>0)},K=t=>`sim-cache-${t}`,nt=t=>{try{let n=JSON.parse(localStorage.getItem(K(t.id))||"null");return n?.text===(t.text||"")?n:null}catch{return null}},R=(t,n)=>{try{localStorage.setItem(K(t.id),JSON.stringify({text:t.text||"",ts:Date.now(),...n}))}catch{}},st=t=>{let n=Math.floor((Date.now()-t)/1e3);return n<60?`${n}s ago`:n<3600?`${Math.floor(n/60)}m ago`:n<86400?`${Math.floor(n/3600)}h ago`:`${Math.floor(n/86400)}d ago`},T=`
4
4
  .sim-form { display:flex; gap:6px; margin-bottom:8px; }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "wiki-plugin-similarity",
3
- "version": "0.5.0",
4
- "description": "Federated Wiki plugin semantic similarity search across wiki domains using prebuilt vector indexes",
3
+ "version": "0.5.1",
4
+ "description": "Federated Wiki plugin \u2014 semantic similarity search across wiki domains using prebuilt vector indexes",
5
5
  "keywords": [
6
6
  "wiki",
7
7
  "federated wiki",
@@ -43,9 +43,10 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@xenova/transformers": "^2.17.2",
46
- "minisearch": "^7.2.0"
46
+ "minisearch": "^7.2.0",
47
+ "onnxruntime-web": "1.14.0"
47
48
  },
48
49
  "devDependencies": {
49
50
  "esbuild": "^0.25.0"
50
51
  }
51
- }
52
+ }
@@ -3,70 +3,66 @@
3
3
  "story": [
4
4
  {
5
5
  "type": "markdown",
6
- "id": "61ccb2d2cdb60c71",
7
- "text": "The Similarity plugin adds semantic similarity search to a Federated Wiki farm. One item type — **similarity** — with behaviour controlled by a DSL: interactive search, ambient similar-pages panel, domain listing, ranked search reports, ghost-page authoring, index building, and generic report buttons."
6
+ "id": "aec1dfcd5f2bc42b",
7
+ "text": "The Similarity plugin adds search to a Federated Wiki farm — semantic similarity over prebuilt vector indexes, ranked search reports, and galactic keyword search over each site's own MiniSearch index. One item type — **similarity** — with a line-oriented DSL, plus a server component that serves every search same-origin. Works on any host: embedding runs in-process (native ONNX where available, WASM everywhere else)."
8
8
  },
9
9
  {
10
10
  "type": "markdown",
11
- "id": "a3f7c8d2e1b94056",
12
- "text": "**Version:** 0.5.0 — server-side search: everything runs same-origin on the wiki host, no external API needed."
13
- },
14
- {
15
- "type": "similarity",
16
- "id": "ad3c41667c285efd",
17
- "text": "SIMILAR: medium\nLIMIT: 5"
11
+ "id": "28edff984cc5334d",
12
+ "text": "## The DSL\n\nThe first ALL-CAPS keyword sets the mode; other lines are domain specs (globs, explicit domains, or the scope keywords PUBLIC / LOCAL / PRIVATE):\n\n- (no keyword) interactive semantic search form\n- `SIMILAR: high|medium|low` — ambient: pages similar to this page\n- `REPORT` — server-side ranked, stub-filtered, fork-bundled report\n- `KEYWORD` — galactic MiniSearch over live site-index.json files\n- `LIST` — table of indexed domains\n- `AUTHOR` — search results as a draggable ghost page\n- `BUILD` / `FORCE` — proxy to the farm indexer, where configured\n- `GHOST url` / `BUTTON label` — open any page-json as a ghost page\n- `THRESHOLD: 0.72` · `LIMIT: 8` · `LIVE` — tuning"
18
13
  },
19
14
  {
20
15
  "type": "markdown",
21
- "id": "bdf93e3860d64ebb",
22
- "text": "## The DSL"
16
+ "id": "251793ca0dae4810",
17
+ "text": "## Try It\n\nSemantic search across the farm:"
23
18
  },
24
19
  {
25
- "type": "markdown",
26
- "id": "0d628d43edf9f8e8",
27
- "text": "The item's `text` field configures what the item does. Mode commands come first; the rest are options and domain specs:\n\n```\nSIMILAR ambient mode — similar pages to this one\nSIMILAR high threshold preset (high=0.78 / medium=0.68 / low=0.58)\nLIST table of indexed domains with page counts\nAUTHOR search → ghost page of [[wiki links]] + references\nREPORT server-side ranked report (stubs dropped, forks bundled)\nBUILD button that builds semantic indexes\nGHOST <url> button that opens any page-json URL as a ghost page\n\nTHRESHOLD 0.72 exact cosine threshold\nLIMIT 8 max results (default 10)\nLIVE bypass the localStorage cache\nFORCE BUILD: re-embed even when the index is fresh\nBUTTON <text> custom button caption (BUILD / GHOST)\n```\n\nColons and extra whitespace are optional — `LIMIT: 8`, `LIMIT 8`, and `LIMIT:8` all work. Leave `text` empty for an interactive search form on the current domain."
20
+ "type": "similarity",
21
+ "id": "86aeab8be16dd8e1",
22
+ "text": "*"
28
23
  },
29
24
  {
30
25
  "type": "markdown",
31
- "id": "1e2f3a4b5c6d7e8f",
32
- "text": "## Domain Specs and Scopes"
26
+ "id": "b6a9be0145bae9bd",
27
+ "text": "Pages similar to this one:"
33
28
  },
34
29
  {
35
- "type": "markdown",
36
- "id": "2f3a4b5c6d7e8f90",
37
- "text": "Any other line is a domain spec. Scope keywords expand server-side:\n\n```\n* all indexed domains, all farms\nPUBLIC the Nextcloud mirror farm\nLOCAL the localhost farm\nPRIVATE public wikis with restricted: true in the farm config\ndavid.* glob pattern\ndavid.hitchhikers.earth explicit domain\n# comment ignored\n```\n\nSpecs combine — `LOCAL` and `david.*` on separate lines searches both."
30
+ "type": "similarity",
31
+ "id": "cf558a16395d7d8c",
32
+ "text": "SIMILAR: medium\nLIMIT: 5"
38
33
  },
39
34
  {
40
35
  "type": "markdown",
41
- "id": "c0775c94a895e9d2",
42
- "text": "## Modes"
36
+ "id": "1aceb7035c7537b2",
37
+ "text": "A ranked report (opens as a ghost page):"
43
38
  },
44
39
  {
45
- "type": "markdown",
46
- "id": "4f91a3b2d8e05c11",
47
- "text": "**Search** (default) — interactive form; results appear inline as scored links. Shift-click a result to append it at the end of the lineup instead of truncating.\n\n**Similar** — runs on page load using the current page's pre-computed vector (no embed call if already indexed). Ranked *Similar Pages* list.\n\n**List** — table of indexed domains with page counts; supports scopes, globs, and `LIMIT`.\n\n**Author** — same search, but opens a ghost page with `# Similar Pages` ([[wiki links]], deduplicated) and `# Reference Links` (all results as reference items with scores).\n\n**Report** — posts to the plugin's own `/system/search-report.json`: candidates from semantic scan ∪ title matching, stubs dropped, forks and near-duplicates bundled under the strongest copy with a one-click lineup link, ranked by semantic × richness × recency + title boost. Opens as a ghost page.\n\n**Keyword** — galactic MiniSearch via `/system/farm-search.json`: the server reads each site's own per-edit `site-index.json` — the indexes fedwiki already maintains — plus sitemap titles. No separate index is built. Opens as a ghost page.\n\n**Build** — a button that asks the farm indexer to rebuild semantic indexes. The wiki server never embeds page corpora itself; it proxies to `WIKI_INDEXER_URL` when configured and otherwise explains that indexing runs on the farm indexer. Add `FORCE` to re-embed fresh domains.\n\n**Ghost** — a generic button: `GHOST <url>` fetches any endpoint returning fedwiki page JSON and opens it as a ghost page. Caption set with `BUTTON <text>`."
40
+ "type": "similarity",
41
+ "id": "59bfe1e3968743a1",
42
+ "text": "REPORT\nLIMIT: 5"
48
43
  },
49
44
  {
50
45
  "type": "markdown",
51
- "id": "c0775c94a895e9d3",
52
- "text": "## Caching"
46
+ "id": "31b81a31f675c4e1",
47
+ "text": "Keyword search over the sites' own indexes:"
53
48
  },
54
49
  {
55
- "type": "markdown",
56
- "id": "9a1b2c3d4e5f6001",
57
- "text": "Results are cached in `localStorage` keyed by item id and invalidated whenever the item's `text` changes. Add `LIVE` anywhere in the DSL to bypass the cache."
50
+ "type": "similarity",
51
+ "id": "0c3fb9298ba8fd23",
52
+ "text": "KEYWORD\nLIMIT: 5"
58
53
  },
59
54
  {
60
55
  "type": "markdown",
61
- "id": "c0775c94a895e9d4",
62
- "text": "## How It Works"
56
+ "id": "d9a96dfebb9d1692",
57
+ "text": "## Server Routes\n\nAll same-origin, registered by the plugin's server component:\n\n- `GET /system/indexed-domains.json?pattern=`\n- `GET /system/semantic-vectors.json?domain=`\n- `GET /system/embed.json?text=`\n- `POST /system/search-report.json`\n- `GET /system/farm-search.json?q=&pattern=&limit=`\n- `GET /system/build-index.json?domains=` (proxied to the farm indexer)\n\nIndexes are `status/semantic-vectors.json` per domain — built by the farm indexer, distributed by sync, read from farm disk. Keyword search reads each site's per-edit `status/site-index.json`; no separate index is built. Query embedding is BAAI bge-small-en-v1.5, 384-dim, CLS-pooled, unit-normalised — byte-compatible with the index builder."
63
58
  },
64
59
  {
65
60
  "type": "markdown",
66
- "id": "4b1bbe95544548eb",
67
- "text": "Pages are pre-indexed as 384-dimensional embedding vectors (BAAI/bge-small-en-v1.5, unit-normalised) by the farm indexer, which writes `semantic-vectors.json` into each domain's `status/` folder. The plugin loads these indexes and scores them with a dot product — no remote search engine required.\n\nThe server component registers same-origin `/system` routes: `indexed-domains.json`, `semantic-vectors.json`, `embed.json` (query embedding **in-process** via transformers.js — the same model the indexes were built with, so scores match), `search-report.json` (the full ranked report pipeline), `farm-search.json` (keyword search over the sites' own per-edit MiniSearch indexes), and `build-index.json` (indexer proxy). Because every route runs on the wiki host itself, search works for any visitor — no HomeLab services needed. See [[Similarity Indexing]] for building indices and [[Build Semantic Index]] for triggering builds from the wiki."
61
+ "id": "21eb5354d2adb29e",
62
+ "text": "**Version:** 0.5.1\n\n# Links\n\n- [https://github.com/Hitchhikers-Guide-to-the-Galaxy/wiki-plugin-similarity GitHub repository]\n- [https://www.npmjs.com/package/wiki-plugin-similarity npm package]\n- [https://plugin.fedwiki.club/similarity-plugin.html Plugin documentation]\n- [https://search.fedwiki.club/ Search hub] architecture, indexing, freshness."
68
63
  }
69
64
  ],
65
+ "plugin": "similarity",
70
66
  "journal": [
71
67
  {
72
68
  "type": "create",
@@ -75,44 +71,68 @@
75
71
  "story": [
76
72
  {
77
73
  "type": "markdown",
78
- "id": "61ccb2d2cdb60c71",
79
- "text": "The Similarity plugin adds semantic similarity search to a Federated Wiki farm."
74
+ "id": "aec1dfcd5f2bc42b",
75
+ "text": "The Similarity plugin adds search to a Federated Wiki farm — semantic similarity over prebuilt vector indexes, ranked search reports, and galactic keyword search over each site's own MiniSearch index. One item type — **similarity** — with a line-oriented DSL, plus a server component that serves every search same-origin. Works on any host: embedding runs in-process (native ONNX where available, WASM everywhere else)."
76
+ },
77
+ {
78
+ "type": "markdown",
79
+ "id": "28edff984cc5334d",
80
+ "text": "## The DSL\n\nThe first ALL-CAPS keyword sets the mode; other lines are domain specs (globs, explicit domains, or the scope keywords PUBLIC / LOCAL / PRIVATE):\n\n- (no keyword) — interactive semantic search form\n- `SIMILAR: high|medium|low` — ambient: pages similar to this page\n- `REPORT` — server-side ranked, stub-filtered, fork-bundled report\n- `KEYWORD` — galactic MiniSearch over live site-index.json files\n- `LIST` — table of indexed domains\n- `AUTHOR` — search results as a draggable ghost page\n- `BUILD` / `FORCE` — proxy to the farm indexer, where configured\n- `GHOST url` / `BUTTON label` — open any page-json as a ghost page\n- `THRESHOLD: 0.72` · `LIMIT: 8` · `LIVE` — tuning"
81
+ },
82
+ {
83
+ "type": "markdown",
84
+ "id": "251793ca0dae4810",
85
+ "text": "## Try It\n\nSemantic search across the farm:"
86
+ },
87
+ {
88
+ "type": "similarity",
89
+ "id": "86aeab8be16dd8e1",
90
+ "text": "*"
91
+ },
92
+ {
93
+ "type": "markdown",
94
+ "id": "b6a9be0145bae9bd",
95
+ "text": "Pages similar to this one:"
96
+ },
97
+ {
98
+ "type": "similarity",
99
+ "id": "cf558a16395d7d8c",
100
+ "text": "SIMILAR: medium\nLIMIT: 5"
101
+ },
102
+ {
103
+ "type": "markdown",
104
+ "id": "1aceb7035c7537b2",
105
+ "text": "A ranked report (opens as a ghost page):"
106
+ },
107
+ {
108
+ "type": "similarity",
109
+ "id": "59bfe1e3968743a1",
110
+ "text": "REPORT\nLIMIT: 5"
111
+ },
112
+ {
113
+ "type": "markdown",
114
+ "id": "31b81a31f675c4e1",
115
+ "text": "Keyword search over the sites' own indexes:"
116
+ },
117
+ {
118
+ "type": "similarity",
119
+ "id": "0c3fb9298ba8fd23",
120
+ "text": "KEYWORD\nLIMIT: 5"
121
+ },
122
+ {
123
+ "type": "markdown",
124
+ "id": "d9a96dfebb9d1692",
125
+ "text": "## Server Routes\n\nAll same-origin, registered by the plugin's server component:\n\n- `GET /system/indexed-domains.json?pattern=`\n- `GET /system/semantic-vectors.json?domain=`\n- `GET /system/embed.json?text=`\n- `POST /system/search-report.json`\n- `GET /system/farm-search.json?q=&pattern=&limit=`\n- `GET /system/build-index.json?domains=` (proxied to the farm indexer)\n\nIndexes are `status/semantic-vectors.json` per domain — built by the farm indexer, distributed by sync, read from farm disk. Keyword search reads each site's per-edit `status/site-index.json`; no separate index is built. Query embedding is BAAI bge-small-en-v1.5, 384-dim, CLS-pooled, unit-normalised — byte-compatible with the index builder."
126
+ },
127
+ {
128
+ "type": "markdown",
129
+ "id": "21eb5354d2adb29e",
130
+ "text": "**Version:** 0.5.1\n\n# Links\n\n- [https://github.com/Hitchhikers-Guide-to-the-Galaxy/wiki-plugin-similarity GitHub repository]\n- [https://www.npmjs.com/package/wiki-plugin-similarity npm package]\n- [https://plugin.fedwiki.club/similarity-plugin.html Plugin documentation]\n- [https://search.fedwiki.club/ Search hub] — architecture, indexing, freshness."
80
131
  }
81
132
  ]
82
133
  },
83
- "date": 1781133629000,
84
- "certificate": "from marvin"
85
- },
86
- {
87
- "type": "edit",
88
- "id": "a3f7c8d2e1b94056",
89
- "item": {
90
- "type": "markdown",
91
- "id": "a3f7c8d2e1b94056",
92
- "text": "**Version:** 0.5.0 — server-side search: everything runs same-origin on the wiki host, no external API needed."
93
- },
94
- "date": 1783773290699
95
- },
96
- {
97
- "type": "edit",
98
- "id": "4f91a3b2d8e05c11",
99
- "item": {
100
- "type": "markdown",
101
- "id": "4f91a3b2d8e05c11",
102
- "text": "**Search** (default) — interactive form; results appear inline as scored links. Shift-click a result to append it at the end of the lineup instead of truncating.\n\n**Similar** — runs on page load using the current page's pre-computed vector (no embed call if already indexed). Ranked *Similar Pages* list.\n\n**List** — table of indexed domains with page counts; supports scopes, globs, and `LIMIT`.\n\n**Author** — same search, but opens a ghost page with `# Similar Pages` ([[wiki links]], deduplicated) and `# Reference Links` (all results as reference items with scores).\n\n**Report** — posts to the plugin's own `/system/search-report.json`: candidates from semantic scan ∪ title matching, stubs dropped, forks and near-duplicates bundled under the strongest copy with a one-click lineup link, ranked by semantic × richness × recency + title boost. Opens as a ghost page.\n\n**Keyword** — galactic MiniSearch via `/system/farm-search.json`: the server reads each site's own per-edit `site-index.json` — the indexes fedwiki already maintains — plus sitemap titles. No separate index is built. Opens as a ghost page.\n\n**Build** — a button that asks the farm indexer to rebuild semantic indexes. The wiki server never embeds page corpora itself; it proxies to `WIKI_INDEXER_URL` when configured and otherwise explains that indexing runs on the farm indexer. Add `FORCE` to re-embed fresh domains.\n\n**Ghost** — a generic button: `GHOST <url>` fetches any endpoint returning fedwiki page JSON and opens it as a ghost page. Caption set with `BUTTON <text>`."
103
- },
104
- "date": 1783773290699
105
- },
106
- {
107
- "type": "edit",
108
- "id": "4b1bbe95544548eb",
109
- "item": {
110
- "type": "markdown",
111
- "id": "4b1bbe95544548eb",
112
- "text": "Pages are pre-indexed as 384-dimensional embedding vectors (BAAI/bge-small-en-v1.5, unit-normalised) by the farm indexer, which writes `semantic-vectors.json` into each domain's `status/` folder. The plugin loads these indexes and scores them with a dot product — no remote search engine required.\n\nThe server component registers same-origin `/system` routes: `indexed-domains.json`, `semantic-vectors.json`, `embed.json` (query embedding **in-process** via transformers.js — the same model the indexes were built with, so scores match), `search-report.json` (the full ranked report pipeline), `farm-search.json` (keyword search over the sites' own per-edit MiniSearch indexes), and `build-index.json` (indexer proxy). Because every route runs on the wiki host itself, search works for any visitor — no HomeLab services needed. See [[Similarity Indexing]] for building indices and [[Build Semantic Index]] for triggering builds from the wiki."
113
- },
114
- "date": 1783773290699
134
+ "date": 1783782734556,
135
+ "provenance": "Authored by an AI agent — Claude Fable 5 via the [[fedwiki-page]] skill. [https://claude.com/claude-code Claude Code]"
115
136
  }
116
- ],
117
- "plugin": "similarity"
137
+ ]
118
138
  }
@@ -19,14 +19,37 @@ const MODEL = process.env.WIKI_EMBED_MODEL || 'Xenova/bge-small-en-v1.5'
19
19
  const POOLING = process.env.WIKI_EMBED_POOLING || 'cls'
20
20
  const QUANTIZED = process.env.WIKI_EMBED_QUANTIZED === '1'
21
21
 
22
+ // transformers.js statically imports onnxruntime-node, whose native binary
23
+ // needs glibc. On musl containers (node:alpine — the public farm) that import
24
+ // throws "Error loading shared library ld-linux-x86-64.so.2". Probe the
25
+ // native runtime with a CJS require first; if it can't load, register a
26
+ // module hook (Node >= 18.19) that redirects the onnxruntime-node import to
27
+ // onnxruntime-web — the WASM backend runs everywhere and benchmarks ~25ms a
28
+ // query. WIKI_EMBED_FORCE_WASM=1 forces the WASM path for testing.
29
+
30
+ const nativeOrtLoads = () => {
31
+ if (process.env.WIKI_EMBED_FORCE_WASM === '1') return false
32
+ try { require('onnxruntime-node'); return true } catch { return false }
33
+ }
34
+
22
35
  let extractorPromise = null
23
36
 
24
37
  const getExtractor = () => {
25
38
  if (!extractorPromise) {
26
39
  extractorPromise = (async () => {
40
+ if (!nativeOrtLoads()) {
41
+ const { register } = require('node:module')
42
+ if (typeof register !== 'function') {
43
+ throw new Error('native onnxruntime unavailable and Node lacks ' +
44
+ 'module.register (need >= 18.19) for the WASM fallback')
45
+ }
46
+ register('./ort-hooks.mjs', require('node:url').pathToFileURL(__filename))
47
+ console.log('[wiki-plugin-similarity] native onnxruntime unavailable — using WASM backend')
48
+ }
27
49
  // Dynamic import: @xenova/transformers is ESM-only; this file must stay CJS.
28
50
  const { pipeline, env } = await import('@xenova/transformers')
29
51
  if (process.env.WIKI_MODEL_CACHE) env.cacheDir = process.env.WIKI_MODEL_CACHE
52
+ if (env.backends?.onnx?.wasm) env.backends.onnx.wasm.numThreads = 1
30
53
  return pipeline('feature-extraction', MODEL, { quantized: QUANTIZED })
31
54
  })()
32
55
  extractorPromise.catch(() => { extractorPromise = null }) // allow retry
@@ -0,0 +1,6 @@
1
+ export async function resolve(specifier, context, nextResolve) {
2
+ if (specifier === 'onnxruntime-node') {
3
+ return nextResolve(new URL('./ort-shim.mjs', import.meta.url).href, context)
4
+ }
5
+ return nextResolve(specifier, context)
6
+ }
@@ -0,0 +1,2 @@
1
+ import ort from 'onnxruntime-web'
2
+ export default ort