@mneme-ai/xray 3.18.0 → 3.19.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mneme-ai/xray",
3
- "version": "3.18.0",
3
+ "version": "3.19.0",
4
4
  "description": "Mneme Repo X-Ray — a signed, raw-free, deterministic X-Ray of any repo. Every number is reproducible from git/AST/metadata and sealed with an offline-verifiable NOTARY receipt. No source code ever leaves the machine; no LLM guesses anything.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -47,7 +47,7 @@
47
47
  "mneme"
48
48
  ],
49
49
  "dependencies": {
50
- "@mneme-ai/core": "3.18.0"
50
+ "@mneme-ai/core": "3.19.0"
51
51
  },
52
52
  "optionalDependencies": {
53
53
  "@resvg/resvg-js": "^2.6.2"
package/public/index.html CHANGED
@@ -53,6 +53,9 @@
53
53
  button:hover{background:#22232c}
54
54
  button:active{transform:scale(.98)}
55
55
  button:disabled{opacity:.5;cursor:default}
56
+ .thymoslink{display:inline-flex;align-items:center;gap:8px;margin:16px auto 0;padding:9px 16px;border:1px solid var(--line);border-radius:999px;color:var(--ink2);text-decoration:none;font-size:13.5px;font-weight:560;background:var(--soft);transition:border-color .15s,transform .1s}
57
+ .thymoslink:hover{border-color:#d9b8c6;transform:translateY(-1px)}
58
+ .thymoswrap{text-align:center}
56
59
  .hint{color:var(--sub);font-size:13.5px;text-align:center;line-height:1.6}
57
60
  .hint b{color:var(--ink2);font-weight:560}
58
61
  .err{color:var(--red);text-align:center;margin-top:18px;font-size:14.5px}
@@ -477,6 +480,7 @@ document.querySelectorAll(".fontpick button").forEach(function(b){b.addEventList
477
480
  <span><b>X-Ray</b> — instant health report: dependencies, secrets, risk &amp; who-to-ask.</span>
478
481
  <span><b>📦 AI Pack</b> — bundle the whole repo into one text to paste into any AI chat.</span>
479
482
  </div>
483
+ <div class="thymoswrap"><a class="thymoslink" href="/thymos" title="THYMOS — a semantic attention filter that runs in your browser">💗 THYMOS — drowning in a pile? pull what matters to the top (real semantics, in your browser) &nbsp;→</a></div>
480
484
  <p class="hint">Every number is <b>reproducible</b> from git, code &amp; package metadata and sealed with an <b>offline-verifiable</b> signature. <span class="muted">Private repo or a local folder with no public URL? Run <code>npx @mneme-ai/xray bridge</code> on your machine — a <b>Scan a local folder</b> box appears here + your code never leaves your computer.</span></p>
481
485
  <details class="keybox">
482
486
  <summary>🔖 Optional — save your reports</summary>
@@ -3,130 +3,145 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>THYMOS — a heart you can measure · Mneme</title>
7
- <meta name="description" content="The first AI memory with a heart you can audit: affective salience decay + resonance. A measurable feeling, not a claim of sentience.">
6
+ <title>THYMOS — pull what matters out of a messy pile · Mneme</title>
7
+ <meta name="description" content="A filter for your attention. Tell THYMOS your goal, paste a messy pile of notes/tasks/links, and it pulls what matters to the top using a real sentence model that runs 100% in your browser. Nothing is uploaded.">
8
+ <link rel="icon" href="/favicon.svg">
8
9
  <style>
9
- :root { --bg:#0b0d12; --panel:#141821; --ink:#e8ecf4; --mut:#8b94a7; --line:#222838; --pink:#ff5d8f; --grn:#16c784; --blu:#4ea1ff; --gold:#f7b955; }
10
- * { box-sizing:border-box; }
11
- body { margin:0; background:radial-gradient(1200px 600px at 70% -10%, #15202e 0%, var(--bg) 60%); color:var(--ink); font:15px/1.6 system-ui,-apple-system,Segoe UI,Roboto,sans-serif; }
12
- .wrap { max-width:960px; margin:0 auto; padding:32px 20px 80px; }
13
- h1 { font-size:30px; margin:0 0 4px; letter-spacing:-.5px; }
14
- h1 .h { color:var(--pink); }
15
- .sub { color:var(--mut); margin:0 0 28px; font-size:16px; }
16
- .card { background:var(--panel); border:1px solid var(--line); border-radius:16px; padding:20px 22px; margin:18px 0; box-shadow:0 1px 0 rgba(255,255,255,.02) inset; }
17
- .card h2 { font-size:17px; margin:0 0 4px; }
18
- .card p.k { color:var(--mut); margin:0 0 14px; font-size:13.5px; }
19
- canvas { width:100%; display:block; border-radius:10px; }
20
- .row { display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin-top:10px; }
21
- input[type=text] { flex:1; min-width:180px; background:#0e121b; border:1px solid var(--line); color:var(--ink); border-radius:9px; padding:9px 12px; font:inherit; }
22
- .legend { display:flex; gap:16px; flex-wrap:wrap; color:var(--mut); font-size:12.5px; margin-top:8px; }
23
- .dot { display:inline-block; width:9px; height:9px; border-radius:50%; vertical-align:middle; margin-right:5px; }
24
- .pill { display:inline-block; padding:3px 9px; border-radius:999px; font-size:12px; font-weight:600; }
25
- .mono { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; }
26
- .honest { border-left:3px solid var(--gold); padding:10px 14px; background:rgba(247,185,85,.06); border-radius:0 10px 10px 0; color:#d7c39a; font-size:13.5px; }
27
- a { color:var(--blu); text-decoration:none; } a:hover { text-decoration:underline; }
28
- .foot { color:var(--mut); font-size:12.5px; margin-top:30px; text-align:center; }
29
- .big { font-size:26px; font-weight:700; }
10
+ :root{--bg:#ffffff;--soft:#f7f8fa;--ink:#111317;--ink2:#33353c;--sub:#6b7280;--line:#e7e9ee;--pink:#e0316f;--pinks:#fdeef4;--grn:#0a9d62;--blu:#2563eb;--gold:#b7791f;--r:16px;--rs:11px}
11
+ *{box-sizing:border-box}
12
+ html{-webkit-font-smoothing:antialiased}
13
+ body{margin:0;background:var(--bg);color:var(--ink);font:15.5px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
14
+ .wrap{max-width:760px;margin:0 auto;padding:26px 20px 90px}
15
+ .back{display:inline-block;color:var(--sub);text-decoration:none;font-size:13px;margin-bottom:18px}.back:hover{color:var(--ink)}
16
+ h1{font-size:30px;line-height:1.15;margin:0 0 6px;letter-spacing:-.6px}h1 .h{color:var(--pink)}
17
+ .tag{color:var(--sub);font-size:17px;margin:0 0 22px;max-width:560px}
18
+ /* explainer */
19
+ .explain{background:var(--soft);border:1px solid var(--line);border-radius:var(--r);padding:18px 20px;margin:0 0 22px}
20
+ .explain h2{font-size:14px;letter-spacing:.02em;text-transform:uppercase;color:var(--sub);margin:0 0 8px}
21
+ .explain p{margin:0 0 14px;color:var(--ink2)}
22
+ .steps{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}@media(max-width:620px){.steps{grid-template-columns:1fr}}
23
+ .step{display:flex;gap:10px;align-items:flex-start}
24
+ .num{flex:none;width:24px;height:24px;border-radius:50%;background:var(--pink);color:#fff;font-weight:700;font-size:13px;display:grid;place-items:center}
25
+ .step b{display:block;font-size:14px}.step span{color:var(--sub);font-size:13px}
26
+ /* engine */
27
+ .engine{display:inline-flex;align-items:center;gap:8px;font-size:12.5px;color:var(--sub);border:1px solid var(--line);border-radius:999px;padding:5px 12px;margin:0 0 18px;background:#fff}
28
+ .engine .led{width:8px;height:8px;border-radius:50%;background:var(--gold)}.engine.ready .led{background:var(--grn)}.engine.fb .led{background:var(--pink)}
29
+ /* form */
30
+ .grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}@media(max-width:620px){.grid{grid-template-columns:1fr}}
31
+ label{display:block;font-size:13px;color:var(--ink2);margin:0 0 6px;font-weight:600}
32
+ textarea{width:100%;background:#fff;border:1px solid var(--line);color:var(--ink);border-radius:var(--rs);padding:12px 14px;font:inherit;resize:vertical;transition:border-color .15s,box-shadow .15s}
33
+ textarea:focus{outline:0;border-color:var(--pink);box-shadow:0 0 0 3px var(--pinks)}
34
+ #goal{height:74px}#pile{height:210px}
35
+ .row{display:flex;gap:12px;align-items:center;margin:16px 0 4px;flex-wrap:wrap}
36
+ button{height:48px;padding:0 24px;border:0;border-radius:var(--rs);background:var(--pink);color:#fff;font-weight:700;font-size:15px;cursor:pointer;transition:transform .1s,filter .15s}
37
+ button:hover{filter:brightness(1.05)}button:active{transform:scale(.98)}button:disabled{opacity:.5;cursor:default}
38
+ .ghost{background:#fff;border:1px solid var(--line);color:var(--ink2);font-weight:600}
39
+ .thr{display:flex;align-items:center;gap:8px;color:var(--sub);font-size:13px;margin-left:auto}
40
+ input[type=range]{accent-color:var(--pink)}
41
+ .ex{color:var(--sub);font-size:13px;margin-top:8px}.ex a{color:var(--blu);cursor:pointer;text-decoration:none}.ex a:hover{text-decoration:underline}
42
+ .mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
43
+ /* results */
44
+ .summary{margin:24px 0 10px;font-size:15.5px;min-height:22px}.summary b{font-size:19px}
45
+ .item{display:flex;align-items:center;gap:13px;background:#fff;border:1px solid var(--line);border-radius:var(--rs);padding:12px 15px;margin:9px 0}
46
+ .item.keep{border-color:#cfead9;background:#fbfefc}.item.drop{opacity:.62}
47
+ .bar{flex:none;width:56px;height:6px;border-radius:99px;background:#eef0f4;overflow:hidden}
48
+ .bar i{display:block;height:100%;background:linear-gradient(90deg,var(--blu),var(--pink))}
49
+ .txt{flex:1;min-width:0;overflow-wrap:anywhere}
50
+ .verdict{flex:none;font-size:12px;font-weight:700;padding:3px 10px;border-radius:999px}
51
+ .keepv{background:#e6f6ee;color:var(--grn)}.letv{background:#f1f2f5;color:var(--sub)}
52
+ .sc{flex:none;color:var(--sub);font-size:12px;font-family:ui-monospace,monospace;width:32px;text-align:right}
53
+ .honest{border-left:3px solid var(--gold);padding:11px 15px;background:#fdf8ee;border-radius:0 10px 10px 0;color:#7a5b14;font-size:13px;margin-top:28px}
54
+ .foot{color:var(--sub);font-size:12.5px;margin-top:30px;text-align:center}.foot a{color:var(--blu);text-decoration:none}
55
+ .spin{width:13px;height:13px;border:2px solid var(--line);border-top-color:var(--blu);border-radius:50%;display:inline-block;animation:s .8s linear infinite;vertical-align:-2px}@keyframes s{to{transform:rotate(360deg)}}
30
56
  </style>
31
57
  </head>
32
58
  <body>
33
59
  <div class="wrap">
34
- <h1>💗 <span class="h">THYMOS</span> a heart you can <em>measure</em></h1>
35
- <p class="sub">The first AI memory that forgets like a mind and pulls in what matters — every bit of it a signed, deterministic number. <b>Not a claim of sentience.</b> The math below is exactly the math Mneme runs.</p>
60
+ <a class="back" href="/">← Mneme · Repo X-Ray</a>
61
+ <h1>💗 <span class="h">THYMOS</span></h1>
62
+ <p class="tag">A filter for your attention — it pulls what matters out of a messy pile.</p>
36
63
 
37
- <!-- 1. DECAY -->
38
- <div class="card">
39
- <h2>① Salience decaykeep what bonds, forget the noise</h2>
40
- <p class="k">Every memory carries a <b>salience</b> (0–1) from real signals — was it reused, did you react strongly (EN or Thai), was it consequential. Salience sets a <b>half-life</b>: the trivial fades within a day, what bonds survives for a year. This is the real curve <span class="mono">strength = 0.5^(elapsed / halfLife)</span>, <span class="mono">halfLife = (0.5 + salience²·547) days</span>.</p>
41
- <canvas id="decay" height="260"></canvas>
42
- <div class="legend">
43
- <span><span class="dot" style="background:var(--grn)"></span>salience 0.9 a decision you cared about</span>
44
- <span><span class="dot" style="background:var(--gold)"></span>salience 0.5 — a normal note</span>
45
- <span><span class="dot" style="background:var(--pink)"></span>salience 0.15 — a throwaway log line</span>
64
+ <div class="explain">
65
+ <h2>What it is &amp; what it's for</h2>
66
+ <p>Got a long, messy list notes, tasks, links, search results — and a goal? THYMOS reads the <b>meaning</b> of each line and surfaces the ones that actually matter for your goal, so you can ignore the rest. It’s a focus tool: less scrolling, less noise.</p>
67
+ <div class="steps">
68
+ <div class="step"><div class="num">1</div><div><b>Say your goal</b><span>what you’re trying to do right now</span></div></div>
69
+ <div class="step"><div class="num">2</div><div><b>Paste the pile</b><span>one item per line</span></div></div>
70
+ <div class="step"><div class="num">3</div><div><b>Hit Triage</b><span>keep the 🧲, drop the rest</span></div></div>
46
71
  </div>
47
72
  </div>
48
73
 
49
- <!-- 2. RESONANCE MAGNET -->
50
- <div class="card">
51
- <h2>② Resonance — the core attracts</h2>
52
- <p class="k">Type your <b>vision</b>, then inbound items. Each item is pulled toward the core by its <b>resonance</b> (cosine over words) — high resonance is magnetised in, noise is repelled. Same math as <span class="mono">mneme thymos resonate</span>.</p>
53
- <div class="row"><input id="core" type="text" value="local-first trust and memory for AI agents" placeholder="your core vision"></div>
54
- <div class="row"><input id="items" type="text" value="signed memory for agents, vendor-neutral trust, cheap flights to tokyo, offline governance proof" placeholder="comma-separated inbound items"></div>
55
- <canvas id="magnet" height="320"></canvas>
56
- <div class="legend"><span><span class="dot" style="background:var(--grn)"></span>pulled in (resonance ≥ 0.12)</span><span><span class="dot" style="background:var(--mut)"></span>repelled</span></div>
57
- </div>
74
+ <div class="engine" id="engine"><span class="led"></span><span id="engtext">semantic engine: loading… (≈25&nbsp;MB once, then cached)</span></div>
58
75
 
59
- <!-- 3. AFFECT -->
60
- <div class="card">
61
- <h2>③ Affect readhow strongly did you react? (EN + Thai)</h2>
62
- <p class="k">Strong feeling either way sticks — praise <em>and</em> correction. Type anything.</p>
63
- <div class="row"><input id="affect" type="text" value="งานนี้เยี่ยมมาก ขอบคุณ!" placeholder="type EN or Thai…"></div>
64
- <div class="row" style="margin-top:14px">
65
- <div>valence <span id="val" class="big mono">—</span></div>
66
- <div style="margin-left:24px">intensity <span id="inten" class="big mono">—</span></div>
67
- <div style="margin-left:24px">→ salience <span id="sal" class="pill" style="background:#1d2838;color:var(--blu)">—</span></div>
68
- </div>
76
+ <div class="grid">
77
+ <div><label>① Your goal / focus right now</label><textarea id="goal" placeholder="e.g. ship the local-first trust layer for AI agents this week"></textarea></div>
78
+ <div><label>② The pileone item per line</label><textarea id="pile" placeholder="refactor the auth module&#10;reply to the recruiter&#10;benchmark the signed-memory protocol&#10;buy cat food&#10;draft the launch post&#10;random cooking video"></textarea></div>
69
79
  </div>
80
+ <div class="row">
81
+ <button id="run">🧲 Triage</button>
82
+ <button id="copy" class="ghost">Copy the keepers</button>
83
+ <span class="thr">let-go below <input id="thr" type="range" min="10" max="50" value="22"> <span id="thrv" class="mono">0.22</span></span>
84
+ </div>
85
+ <p class="ex">no idea? <a id="demo">load an example</a> — then hit Triage. <span class="mono">⌘/Ctrl + Enter</span> in the pile also runs it.</p>
70
86
 
71
- <div class="honest">★ <b>Honest (DIAKRISIS):</b> “feeling” here is a measurable salience / valence / bond score derived from observable signals — <b>not</b> qualia or real emotion. The value is a memory that <em>behaves</em> like a mind and that you can <b>audit</b> (<span class="mono">thymosGauntlet = 100</span>). A heart you can measure beats a heart you must take on faith.</div>
87
+ <div id="summary" class="summary"></div>
88
+ <div id="out"></div>
72
89
 
73
- <p class="foot">Part of <a href="/">Mneme</a> — the local-first trust &amp; memory layer · <a href="https://www.npmjs.com/package/mneme-ai" target="_blank" rel="noopener">npm i -g mneme-ai</a> · <span class="mono">mneme thymos</span></p>
74
- </div>
90
+ <div class="honest" id="honest">★ <b>How it works, honestly:</b> THYMOS turns your goal and each line into a real 384-dimension embedding with <b>all-MiniLM-L6-v2</b> (a small sentence model) running <b>100% in your browser</b> only the model downloads once from a CDN; <b>your text is never uploaded</b>, there’s no server and no API key. It ranks by the similarity of meaning (it gets synonyms &amp; paraphrase, not just shared words). It surfaces what fits <em>your stated goal</em> it doesn’t read your mind, and it’s a small fast model, not a giant one.</div>
75
91
 
76
- <script>
77
- // ── the REAL thymos math, mirrored deterministically in the browser ──
78
- const clamp=(n,lo,hi)=>Math.max(lo,Math.min(hi,n||0));
79
- const DAY=86400000;
80
- function halfLifeMs(sal){ return (0.5 + clamp(sal,0,1)**2 * 547) * DAY; }
81
- function strengthAt(sal, elapsedMs){ return clamp(Math.pow(0.5, elapsedMs/halfLifeMs(sal)),0,1); }
82
- function bag(t){ const m={}; for(const w of (t||'').toLowerCase().split(/[^a-z0-9฀-๿]+/).filter(x=>x.length>1)) m[w]=(m[w]||0)+1; return m; }
83
- function resonance(core,item){ const a=bag(core),b=bag(item); let dot=0; for(const k in a) dot+=a[k]*(b[k]||0); const mag=o=>Math.sqrt(Object.values(o).reduce((s,v)=>s+v*v,0)); const d=mag(a)*mag(b); return d?clamp(dot/d,0,1):0; }
84
- const POS=["love","great","excellent","important","amazing","thank","perfect","brilliant","awesome","beautiful","ชอบ","รัก","เยี่ยม","สุดยอด","ดีมาก","สำคัญ","ประทับใจ","ขอบคุณ","เจ๋ง","ปลื้ม"];
85
- const NEG=["wrong","bad","hate","broken","bug","terrible","angry","sad","awful","fail","ผิด","แย่","เกลียด","ไม่ชอบ","เสียใจ","โกรธ","พัง","บั๊ก","ห่วย","เซ็ง"];
86
- const BOOST=["very","so","most","really","extremely","มาก","สุด","ที่สุด","โคตร","สุดๆ"];
87
- function readAffect(t){ t=(t||'').toLowerCase(); const c=w=>w.reduce((n,x)=>n+(t.includes(x)?1:0),0); const pos=c(POS),neg=c(NEG),boost=c(BOOST),bangs=(t.match(/!|ที่สุด/g)||[]).length; const hits=pos+neg; if(!hits&&!boost&&!bangs)return{valence:0,intensity:0}; const valence=hits?clamp((pos-neg)/hits,-1,1):0; const intensity=clamp(hits*0.34+boost*0.18+bangs*0.12,0,1); return{valence:Math.round(valence*100)/100,intensity:Math.round(intensity*100)/100}; }
88
- function salience(recalls,valence,consequence){ const reuse=recalls/(recalls+3); return Math.round(clamp(0.12+0.34*reuse+0.27*Math.abs(valence)+0.27*consequence,0,1)*1000)/1000; }
89
- const css=v=>getComputedStyle(document.documentElement).getPropertyValue(v).trim();
92
+ <p class="foot">Part of <a href="/">Mneme</a> · the local-first trust &amp; memory layer · <span class="mono">npm i -g mneme-ai</span></p>
93
+ </div>
90
94
 
91
- // ── ① decay curve ──
92
- function drawDecay(){ const cv=document.getElementById('decay'); const w=cv.clientWidth, h=260; cv.width=w*devicePixelRatio; cv.height=h*devicePixelRatio; const x=cv.getContext('2d'); x.scale(devicePixelRatio,devicePixelRatio); x.clearRect(0,0,w,h);
93
- const pad=36, days=400; const X=d=>pad+(d/days)*(w-pad-10), Y=s=>10+(1-s)*(h-30);
94
- x.strokeStyle=css('--line'); x.lineWidth=1; x.fillStyle=css('--mut'); x.font='11px system-ui';
95
- for(const s of [0,0.5,1]){ x.beginPath(); x.moveTo(pad,Y(s)); x.lineTo(w-10,Y(s)); x.stroke(); x.fillText(s.toFixed(1),6,Y(s)+3); }
96
- for(const d of [0,100,200,300,400]){ x.fillText(d+'d',X(d)-8,h-6); }
97
- const series=[[0.9,css('--grn')],[0.5,css('--gold')],[0.15,css('--pink')]];
98
- for(const [sal,col] of series){ x.strokeStyle=col; x.lineWidth=2.4; x.beginPath(); for(let d=0; d<=days; d+=2){ const s=strengthAt(sal, d*DAY); const px=X(d),py=Y(s); d===0?x.moveTo(px,py):x.lineTo(px,py);} x.stroke(); }
99
- // forget floor
100
- x.strokeStyle='rgba(255,93,143,.5)'; x.setLineDash([4,4]); x.beginPath(); x.moveTo(pad,Y(0.18)); x.lineTo(w-10,Y(0.18)); x.stroke(); x.setLineDash([]); x.fillStyle='rgba(255,93,143,.8)'; x.fillText('forget floor 0.18',w-130,Y(0.18)-5);
101
- }
95
+ <script type="module">
96
+ const $=id=>document.getElementById(id);
97
+ let THR=0.22, extractor=null, loading=null, MODE="semantic";
98
+ const eng=$('engine');
99
+ function setEng(t,cls){ $('engtext').innerHTML=t; eng.className="engine"+(cls?" "+cls:""); }
102
100
 
103
- // ── ② resonance magnet (animated, deterministic targets) ──
104
- let parts=[], raf=0;
105
- function buildMagnet(){ const core=document.getElementById('core').value; const items=document.getElementById('items').value.split(',').map(s=>s.trim()).filter(Boolean);
106
- parts=items.map((it,i)=>{ const r=resonance(core,it); const ang=(i/Math.max(1,items.length))*Math.PI*2 - Math.PI/2; const pulled=r>=0.12; return { label:it, r, ang, pulled, x:Math.cos(ang)*240, y:Math.sin(ang)*240 }; }); }
107
- function drawMagnet(t){ const cv=document.getElementById('magnet'); const w=cv.clientWidth,h=320; if(cv.width!==w*devicePixelRatio){cv.width=w*devicePixelRatio;cv.height=h*devicePixelRatio;} const x=cv.getContext('2d'); x.setTransform(devicePixelRatio,0,0,devicePixelRatio,0,0); x.clearRect(0,0,w,h); const cx=w/2, cy=h/2;
108
- // core
109
- const pulse=8+Math.sin(t/600)*3; x.fillStyle='rgba(78,161,255,.12)'; x.beginPath(); x.arc(cx,cy,46+pulse,0,7); x.fill();
110
- x.fillStyle=css('--blu'); x.beginPath(); x.arc(cx,cy,16,0,7); x.fill(); x.fillStyle='#fff'; x.font='600 11px system-ui'; x.textAlign='center'; x.fillText('CORE',cx,cy+3);
111
- for(const p of parts){ // target radius: high resonance close to core, low → far/repelled
112
- const targR = p.pulled ? (46 + (1-p.r)*120) : 150 + (1-p.r)*60;
113
- const tx=cx+Math.cos(p.ang)*targR, ty=cy+Math.sin(p.ang)*targR*0.62;
114
- p.cxp = (p.cxp==null?tx:p.cxp+(tx-p.cxp)*0.08); p.cyp=(p.cyp==null?ty:p.cyp+(ty-p.cyp)*0.08);
115
- x.strokeStyle = p.pulled ? 'rgba(22,199,132,.35)' : 'rgba(139,148,167,.18)'; x.lineWidth=p.pulled?1.6:1; x.beginPath(); x.moveTo(cx,cy); x.lineTo(p.cxp,p.cyp); x.stroke();
116
- x.fillStyle = p.pulled ? css('--grn') : css('--mut'); x.beginPath(); x.arc(p.cxp,p.cyp,p.pulled?7:5,0,7); x.fill();
117
- x.fillStyle=css('--ink'); x.font='12px system-ui'; x.textAlign='center'; const lbl=p.label.length>26?p.label.slice(0,24)+'…':p.label; x.fillText(lbl + ' ('+p.r.toFixed(2)+')', p.cxp, p.cyp-12);
118
- }
119
- raf=requestAnimationFrame(drawMagnet);
101
+ async function loadEngine(){
102
+ if(extractor) return extractor;
103
+ if(loading) return loading;
104
+ loading=(async()=>{
105
+ setEng('<span class="spin"></span> loading semantic engine (MiniLM, ≈25&nbsp;MB once)…');
106
+ const { pipeline, env } = await import('https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2');
107
+ env.allowLocalModels=false;
108
+ const ex = await pipeline('feature-extraction','Xenova/all-MiniLM-L6-v2',{ quantized:true,
109
+ progress_callback:(p)=>{ if(p&&p.status==='progress'&&p.file&&/onnx|model/.test(p.file)) setEng('<span class="spin"></span> downloading model… '+Math.round(p.progress||0)+'%'); } });
110
+ extractor=ex; MODE="semantic"; setEng('✓ semantic engine ready · all-MiniLM-L6-v2 · 100% in your browser','ready');
111
+ return ex;
112
+ })().catch(()=>{ extractor=null; MODE="fallback"; setEng('⚠ semantic engine unavailable — using an approximate keyword match','fb'); return null; });
113
+ return loading;
120
114
  }
115
+ function cos(a,b){ let d=0; for(let i=0;i<a.length;i++) d+=a[i]*b[i]; return d; }
116
+ function bag(t){const m={};for(const w of (t||'').toLowerCase().split(/[^a-z0-9฀-๿]+/).filter(x=>x.length>1))m[w]=(m[w]||0)+1;return m}
117
+ function tokRes(c,i){const a=bag(c),b=bag(i);let d=0;for(const k in a)d+=a[k]*(b[k]||0);const mg=o=>Math.sqrt(Object.values(o).reduce((s,v)=>s+v*v,0));const dn=mg(a)*mg(b);return dn?d/dn:0}
121
118
 
122
- // ── ③ affect ──
123
- function updAffect(){ const a=readAffect(document.getElementById('affect').value); document.getElementById('val').textContent=a.valence.toFixed(2); document.getElementById('inten').textContent=a.intensity.toFixed(2); const s=salience(1,a.valence,a.intensity*0.5); const el=document.getElementById('sal'); el.textContent=s.toFixed(3); el.style.color = a.valence>0.2?css('--grn'):a.valence<-0.2?css('--pink'):css('--mut'); document.getElementById('val').style.color = a.valence>0.2?css('--grn'):a.valence<-0.2?css('--pink'):css('--ink'); }
119
+ $('thr').addEventListener('input',()=>{THR=$('thr').value/100;$('thrv').textContent=THR.toFixed(2);if($('out').children.length)run()});
120
+ $('demo').addEventListener('click',()=>{$('goal').value="ship the local-first trust layer for AI agents this week";$('pile').value="harden the agent governance gate\nreply to the recruiter from xAI\nmeasure how fast the signed memory protocol verifies\nbuy cat food\nwrite the announcement for the offline-proof feature\nwatch a random cooking video\nfix the disconnected-ops charter bug\nrepaint the bedroom someday";run()});
124
121
 
125
- function boot(){ drawDecay(); buildMagnet(); cancelAnimationFrame(raf); drawMagnet(0); updAffect(); }
126
- ['core','items'].forEach(id=>document.getElementById(id).addEventListener('input',()=>buildMagnet()));
127
- document.getElementById('affect').addEventListener('input',updAffect);
128
- window.addEventListener('resize',()=>{drawDecay();});
129
- boot();
122
+ async function run(){
123
+ const goal=$('goal').value.trim();
124
+ const lines=$('pile').value.split('\n').map(s=>s.trim()).filter(Boolean);
125
+ if(!goal||!lines.length){$('summary').innerHTML='<span style="color:var(--sub)">enter a goal + a few lines, then hit Triage.</span>';$('out').innerHTML='';return}
126
+ $('run').disabled=true; $('summary').innerHTML='<span class="spin"></span> reading meaning + ranking…';
127
+ let scores;
128
+ try{
129
+ const ex=await loadEngine();
130
+ if(ex){ const t=await ex([goal,...lines],{pooling:'mean',normalize:true}); const v=t.tolist(); const g=v[0]; scores=lines.map((t,i)=>Math.max(0,cos(g,v[i+1]))); }
131
+ else { scores=lines.map(t=>tokRes(goal,t)); }
132
+ }catch(e){ MODE="fallback"; setEng('⚠ engine error — approximate keyword match','fb'); scores=lines.map(t=>tokRes(goal,t)); }
133
+ const eff = MODE==="semantic" ? THR : Math.min(THR,0.12);
134
+ const scored=lines.map((t,i)=>({t,score:Math.round(scores[i]*100)/100,keep:scores[i]>=eff})).sort((a,b)=>b.score-a.score);
135
+ const keep=scored.filter(s=>s.keep),drop=scored.filter(s=>!s.keep);
136
+ $('summary').innerHTML='🧲 Focus on <b style="color:var(--grn)">'+keep.length+'</b> · 〰 let go of <b>'+drop.length+'</b>'+(keep.length?' &nbsp;—&nbsp; <span style="color:var(--sub)">top: '+keep.slice(0,2).map(k=>'“'+esc(k.t.slice(0,46))+'”').join(', ')+'</span>':'')+(MODE==='fallback'?' <span style="color:var(--pink)">· approximate (keyword) mode</span>':'');
137
+ $('out').innerHTML=scored.map(s=>'<div class="item '+(s.keep?'keep':'drop')+'"><div class="bar"><i style="width:'+Math.round(s.score*100)+'%"></i></div><div class="txt">'+esc(s.t)+'</div><span class="sc">'+s.score.toFixed(2)+'</span><span class="verdict '+(s.keep?'keepv':'letv')+'">'+(s.keep?'🧲 KEEP':'〰 let go')+'</span></div>').join('');
138
+ window._keepers=keep.map(k=>k.t); $('run').disabled=false;
139
+ }
140
+ function esc(s){return String(s).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]))}
141
+ $('run').addEventListener('click',run);
142
+ $('copy').addEventListener('click',()=>{const k=(window._keepers||[]);if(!k.length)return;navigator.clipboard&&navigator.clipboard.writeText(k.join('\n'));$('copy').textContent='✓ copied '+k.length;setTimeout(()=>$('copy').textContent='Copy the keepers',1400)});
143
+ $('pile').addEventListener('keydown',e=>{if((e.metaKey||e.ctrlKey)&&e.key==='Enter')run()});
144
+ loadEngine();
130
145
  </script>
131
146
  </body>
132
147
  </html>