@mneme-ai/xray 2.173.0 → 2.174.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": "2.173.0",
3
+ "version": "2.174.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",
package/public/index.html CHANGED
@@ -146,6 +146,8 @@
146
146
  .listfoot{display:flex;justify-content:space-between;align-items:center;padding:14px 4px 0;font-size:13px}
147
147
  .moreb{height:38px;padding:0 18px;border:1px solid var(--line);background:#fff;border-radius:10px;cursor:pointer;font-size:13px;font-weight:540;transition:border-color .15s}
148
148
  .moreb:hover{border-color:var(--ink)}
149
+ .moreb:disabled{opacity:.4;cursor:not-allowed;border-color:var(--line)}
150
+ .pagenav{display:inline-flex;gap:8px}
149
151
  /* share + buttons */
150
152
  .share{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:18px 30px;border-top:1px solid var(--line2)}
151
153
  .badgeimg{height:20px}
@@ -182,7 +184,9 @@
182
184
  .bridgebox{margin-top:14px;border-top:1px solid var(--line);padding-top:12px}
183
185
  .bridgebox summary{cursor:pointer;font-size:12.5px;color:var(--a);font-weight:560;list-style:none}
184
186
  .lscard{margin-top:12px;border:1px solid var(--line);border-radius:12px;background:#fff;padding:14px 16px;text-align:left}
185
- .lscard .lsh{font-size:14px;color:var(--ink);margin-bottom:8px}
187
+ .lscard .lstop{display:flex;align-items:center;gap:14px;margin-bottom:8px}
188
+ .lscard .lsgrade{width:46px;height:46px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:23px;font-weight:700;color:#fff;flex-shrink:0;letter-spacing:-.02em}
189
+ .lscard .lsh{font-size:15px;color:var(--ink)}
186
190
  .lscard .lsrow{display:flex;gap:12px;padding:7px 0;border-top:1px solid var(--line2);font-size:13px}
187
191
  .lscard .lsk{width:110px;color:var(--sub);font-weight:560;flex-shrink:0}
188
192
  .lscard .lsv{color:var(--ink2)} .lscard .lsv.bad{color:var(--red)} .lscard .lsv.ok{color:var(--green)}
@@ -190,19 +194,24 @@
190
194
  .lscard .lsok{margin:6px 0;font-size:12.5px;color:var(--green)}
191
195
  .lscard .pill{display:inline-block;font-size:11px;background:var(--soft);border:1px solid var(--line);border-radius:20px;padding:1px 8px;color:var(--ink2)}
192
196
  .lscard .lsnote{margin-top:10px;font-size:11.5px;color:var(--sub);line-height:1.5;border-top:1px solid var(--line2);padding-top:9px}
193
- /* SHOWCASE — selling points + grand graphic at the bottom */
194
- .showcase{max-width:980px;margin:64px auto 0;text-align:center;padding:0 20px}
195
- .showcase .sctitle{font-size:30px;font-weight:700;letter-spacing:-.02em;color:var(--ink);margin:0 0 10px;line-height:1.2}
197
+ /* SHOWCASE — selling points; the graphic sits to the SIDE (grid areas, no markup move) */
198
+ .showcase{max-width:1060px;margin:64px auto 0;padding:0 20px;text-align:left;
199
+ display:grid;column-gap:36px;row-gap:18px;align-items:center;
200
+ grid-template-columns:1fr 360px;grid-template-areas:"title art" "sub art" "cards cards"}
201
+ .showcase .sctitle{grid-area:title;font-size:30px;font-weight:700;letter-spacing:-.02em;color:var(--ink);margin:0;line-height:1.2}
196
202
  .showcase .sctitle .hl{color:var(--a)}
197
- .showcase .scsub{font-size:15px;color:var(--sub);max-width:640px;margin:0 auto 30px;line-height:1.6}
198
- .showcase .sc3{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;text-align:left}
199
- @media(max-width:760px){.showcase .sc3{grid-template-columns:1fr}}
203
+ .showcase .scsub{grid-area:sub;font-size:15px;color:var(--sub);margin:0;line-height:1.6}
204
+ .showcase .bigart{grid-area:art;display:flex;justify-content:center;align-self:center;opacity:.95}
205
+ .showcase .sc3{grid-area:cards;display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:14px}
200
206
  .showcase .sccard{background:var(--soft);border:1px solid var(--line);border-radius:16px;padding:20px}
201
207
  .showcase .scico{font-size:26px;margin-bottom:8px}
202
208
  .showcase .sccard b{display:block;font-size:15px;color:var(--ink);margin-bottom:6px}
203
209
  .showcase .sccard span{font-size:13px;color:var(--sub);line-height:1.55}
204
- .showcase .bigart{margin-top:8px;display:flex;justify-content:center;opacity:.92}
205
- @media(max-width:760px){.showcase .bigart svg{width:300px;height:auto}}
210
+ @media(max-width:820px){
211
+ .showcase{grid-template-columns:1fr;grid-template-areas:"title" "sub" "art" "cards"}
212
+ .showcase .sc3{grid-template-columns:1fr}
213
+ .showcase .bigart svg{width:300px;height:auto}
214
+ }
206
215
  .picker{display:none;margin-top:12px;border:1px solid var(--line);border-radius:10px;background:#fff;overflow:hidden}
207
216
  .picker.on{display:block}
208
217
  .pkbar{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--line2);font-size:12.5px}
@@ -434,12 +443,7 @@ function mountTracking(r){
434
443
  const gitUrl = r.subject.ref;
435
444
  panel.style.display="block";
436
445
  panel.innerHTML = `<div class="thead"><span class="ticon">🛰</span><b>Live tracking</b><span class="tnew">new</span></div>
437
- <div class="thelp">Turn this one-shot X-Ray into an <b>AI Auditor for your team</b>. Pick a branch and press Track live — when you, a teammate, or an AI pushes, this report <b>re-scans itself</b> and shows what changed. No re-click.</div>
438
- <div class="tfeat">
439
- <div class="tf"><b>🛡 AI Code Drift</b><span>a new pushed secret, a destructive CI command, a grade drop — flagged the moment it lands</span></div>
440
- <div class="tf"><b>🔁 Continuous audit</b><span>re-checked on every push (or every 30s) — 24/7, no one has to click</span></div>
441
- <div class="tf"><b>🕰 Time-Machine</b><span>a timeline of how code-health drifted, commit by commit</span></div>
442
- </div>
446
+ <div class="thelp">Pick a branch and press <b>Track live</b> — when you, a teammate, or an AI pushes, this report <b>re-scans itself</b> and shows what changed. No re-click. <span class="muted">(What you get → see “AI Auditor for your team” below.)</span></div>
443
447
  <div class="trow">
444
448
  <span class="tlabel">Branch</span>
445
449
  <select id="tbranch"><option value="">loading branches…</option></select>
@@ -461,12 +465,13 @@ function mountTracking(r){
461
465
  const toggle=document.getElementById("ttoggle");
462
466
  const stateEl=()=>document.getElementById("tstate");
463
467
  const noteEl=()=>document.getElementById("tnote");
464
- function stop(){ if(es){es.close();es=null;} toggle.className="tbtn off"; toggle.textContent="● Track live"; stateEl().textContent="stopped"; const n=noteEl(); if(n) n.style.display="none"; }
468
+ function stop(){ if(es){es.close();es=null;} toggle.className="tbtn off"; toggle.textContent="● Track live"; stateEl().textContent="stopped"; const n=noteEl(); if(n) n.style.display="none"; const sel=document.getElementById("tbranch"); if(sel) sel.disabled=false; }
465
469
  function showDrift(d){ const el=document.getElementById("tdrift"); if(!el||!d) return; el.className="drift "+d.drift; el.style.display="block"; el.textContent=((d.highlights&&d.highlights[0])||("changed → grade "+d.gradeTo))+" · just now"; }
466
470
  function pushTL(d){ history.push(d); const el=document.getElementById("ttl"); if(!el) return; el.innerHTML='<span class="tlabel">Timeline</span> '+history.slice(-14).map(h=>{const c=h.drift==="degraded"?"deg":h.drift==="improved"?"imp":""; return '<span class="pill '+c+'">'+esc(h.gradeTo)+(h.newSecretLeaks>0?" 🔴":"")+'</span>';}).join(" "); }
467
471
  function startSSE(id, branch){
468
472
  es=new EventSource("/api/track/"+id+"/stream");
469
473
  toggle.className="tbtn stop"; toggle.textContent="Stop"; toggle.disabled=false;
474
+ const sel=document.getElementById("tbranch"); if(sel) sel.disabled=true; // lock branch while LIVE
470
475
  stateEl().innerHTML='<span class="live"><span class="dot"></span>LIVE · '+esc(branch||"default")+'</span>';
471
476
  const n=noteEl(); if(n){ n.style.display="block"; n.innerHTML='✓ <b>Now monitoring '+esc(branch||"the default branch")+'.</b> Leave this tab open to watch changes appear here live. The monitor keeps running on the server even if you close the tab — come back and X-Ray this same repo to reconnect. <span class="muted">Switching repo or branch starts a separate monitor; a refresh reconnects this view.</span>'; }
472
477
  es.addEventListener("update", ev=>{ try{ const p=JSON.parse(ev.data); renderCard(p.signed); showDrift(p.delta); pushTL(p.delta);}catch{} });
@@ -548,7 +553,7 @@ document.getElementById("savekey").addEventListener("click", ()=>{
548
553
  });
549
554
 
550
555
  let tab="board", offset=0, loaded=[];
551
- const PAGE=20;
556
+ const PAGE=5;
552
557
  function fmtDate(s){ if(!s) return "—"; const d=new Date(s); return isNaN(d)?"—":d.toLocaleDateString(undefined,{year:"numeric",month:"short",day:"numeric"}); }
553
558
  function rowHTML(b){
554
559
  const lock = b.visibility==="private" ? '<span class="lock" title="private — only you can open this">🔒</span>' : "";
@@ -578,16 +583,18 @@ async function loadList(reset=true){
578
583
  }
579
584
  try{
580
585
  const data=await fetchPage(offset);
581
- loaded=loaded.concat(data.items||[]);
582
- if(loaded.length===0){
583
- el.innerHTML=`<div class="bitem muted">${tab==="mine"?"Nothing here yet — run an <b>X-Ray</b> or <b>📦 AI Pack</b> above and it appears here automatically. (Private folder? use the local Browse panel.)":"No reports yet — be the first."}</div>`;
586
+ const items=data.items||[];
587
+ if((data.total||0)===0){
588
+ el.innerHTML=`<div class="bitem muted">${tab==="mine"?"Nothing here yet — run an <b>X-Ray</b> or <b>📦 AI Pack</b> above and it appears here automatically. (Private folder? use the Choose-a-folder panel.)":"No reports yet — be the first."}</div>`;
584
589
  return;
585
590
  }
586
- const more = (data.offset+data.limit) < data.total;
587
- el.innerHTML = `<div class="listbox">${loaded.map(rowHTML).join("")}</div>`
588
- + `<div class="listfoot"><span class="muted">${loaded.length} of ${data.total} repos</span>${more?'<button class="moreb" id="moreb">Load more</button>':""}</div>`;
591
+ const start=data.offset+1, end=data.offset+items.length;
592
+ const hasPrev=data.offset>0, hasNext=(data.offset+data.limit)<data.total;
593
+ el.innerHTML = `<div class="listbox">${items.map(rowHTML).join("")}</div>`
594
+ + `<div class="listfoot"><span class="muted">${start}–${end} of ${data.total} repos</span><span class="pagenav"><button class="moreb" id="prevb"${hasPrev?"":" disabled"}>← Prev</button><button class="moreb" id="nextb"${hasNext?"":" disabled"}>Next →</button></span></div>`;
589
595
  el.querySelectorAll(".bitem[data-fp]").forEach(n=>n.addEventListener("click",()=>openReport(n.dataset.fp, n.dataset.priv==="1")));
590
- const mb=document.getElementById("moreb"); if(mb) mb.addEventListener("click",()=>{ offset+=PAGE; loadList(false); });
596
+ const pb=document.getElementById("prevb"); if(pb&&hasPrev) pb.addEventListener("click",()=>{ offset=Math.max(0,offset-PAGE); loadList(false); window.scrollTo({top:document.getElementById("board").offsetTop-80,behavior:"smooth"}); });
597
+ const nb=document.getElementById("nextb"); if(nb&&hasNext) nb.addEventListener("click",()=>{ offset+=PAGE; loadList(false); window.scrollTo({top:document.getElementById("board").offsetTop-80,behavior:"smooth"}); });
591
598
  }catch{ el.innerHTML='<div class="bitem muted">Could not load.</div>'; }
592
599
  }
593
600
  async function openReport(fp, isPrivate){
@@ -648,14 +655,21 @@ function renderLocalScan(r){
648
655
  const s=r.summary, out=document.getElementById("localout"); if(!out) return;
649
656
  const sec=s.secrets, secCls=sec.totalFindings>0?"bad":"ok";
650
657
  const langs=s.langs.map(([e,n])=>`<span class="pill">${esc(e)} ${n}</span>`).join(" ");
658
+ const g=s.git, vit = !g.has ? "" : (s.dormantDays>365?"dormant":s.dormantDays>120?"slowing":"active");
659
+ const gitRows = g.has ? `
660
+ <div class="lsrow"><span class="lsk">Bus factor</span><span class="lsv ${g.authors===1?"bad":""}"><b>${g.authors}</b> author${g.authors===1?" — single point of failure":"s"} · top author ${Math.round(g.topShare*100)}% of ${g.commits} commits</span></div>
661
+ <div class="lsrow"><span class="lsk">Vitality</span><span class="lsv ${vit==="dormant"?"bad":""}"><b>${vit}</b> · ${s.ageDays}d span · last activity ${s.dormantDays}d ago</span></div>`
662
+ : `<div class="lsrow"><span class="lsk">Git history</span><span class="lsv muted">— no .git reflog in this folder (bus factor / vitality unavailable)</span></div>`;
651
663
  out.innerHTML=`<div class="lscard">
652
- <div class="lsh"><b>📂 ${esc(r.folder)}</b> <span class="muted">— scanned in your browser · ${r.files} files · nothing uploaded</span></div>
664
+ <div class="lstop"><div class="lsgrade g-${("ABCDEF".includes(s.grade)?s.grade:"C")}">${esc(s.grade)}</div>
665
+ <div><div class="lsh"><b>📂 ${esc(r.folder)}</b></div><div class="muted" style="font-size:12px">scanned in your browser · ${r.files} files · nothing uploaded</div></div></div>
653
666
  <div class="lsrow"><span class="lsk">Secrets</span><span class="lsv ${secCls}"><b>${sec.totalFindings}</b> in production code${sec.excludedTestHits?` · ${sec.excludedTestHits} in tests (excluded)`:""}</span></div>
654
667
  ${sec.hits.length?`<div class="lshits">${sec.hits.slice(0,6).map(h=>`<div>🔴 ${esc(h.kind)} — ${esc(h.file)}:${h.line}</div>`).join("")}</div>`:`<div class="lsok">✓ no leaked credentials in production code</div>`}
668
+ ${gitRows}
655
669
  <div class="lsrow"><span class="lsk">Dependencies</span><span class="lsv"><b>${s.deps.total}</b> total · ${s.deps.deps} deps · ${s.deps.devDeps} dev</span></div>
656
- <div class="lsrow"><span class="lsk">Size</span><span class="lsv"><b>${s.filesScanned}</b> source files · ${s.loc.toLocaleString()} lines</span></div>
670
+ <div class="lsrow"><span class="lsk">Complexity</span><span class="lsv"><b>${(s.symbols||0).toLocaleString()}</b> symbols · ${s.filesScanned} files · ${s.loc.toLocaleString()} lines</span></div>
657
671
  <div class="lsrow"><span class="lsk">Languages</span><span class="lsv">${langs||"—"}</span></div>
658
- <div class="lsnote">Quick local scan — secrets · dependencies · size, computed <b>in your browser</b> (unsigned, no upload). The full <b>signed</b> report with git history, bus factor &amp; vitality needs a public URL above or the bridge.</div>
672
+ <div class="lsnote">Computed <b>in your browser</b> (unsigned, nothing uploaded). Secrets · dependencies · complexity from your files; bus factor &amp; vitality from <code>.git</code> reflog. The full <b>signed</b> report (dep mortality, hotspots, coupling) needs a public URL above or the bridge.</div>
659
673
  </div>`;
660
674
  }
661
675
  const pick=document.getElementById("pickfolder");
@@ -51,10 +51,44 @@
51
51
  } catch { return { total: 0, deps: 0, devDeps: 0, names: [] }; }
52
52
  }
53
53
 
54
- /** Compose a deterministic local report from already-read files. Pure. */
55
- function summarize(files) {
54
+ // structural complexity count declared symbols (deterministic, regex-based)
55
+ const SYMBOL_RE = /\b(function\s+\w+|class\s+\w+|interface\s+\w+|def\s+\w+|func\s+\w+|fn\s+\w+|export\s+(?:const|function|class|default)|=>\s*[{(])/g;
56
+ function countSymbols(text) { const m = String(text).match(SYMBOL_RE); return m ? m.length : 0; }
57
+
58
+ // REAL git signals from .git/logs/HEAD (the reflog — plain text the browser can read).
59
+ // Each line: <old> <new> <Name> <email> <unixTs> <tz>\t<message>. Honest: this is
60
+ // HEAD-movement history (commits/resets/merges), a sound approximation of authorship
61
+ // + activity window without running git or parsing packfiles.
62
+ function parseGitLog(text) {
63
+ const authors = {}; let commits = 0, firstTs = 0, lastTs = 0;
64
+ for (const line of String(text || "").split("\n")) {
65
+ const m = line.match(/^[0-9a-f]+\s+[0-9a-f]+\s+(.+?)\s+<[^>]*>\s+(\d+)\s/);
66
+ if (!m) continue;
67
+ commits++;
68
+ authors[m[1]] = (authors[m[1]] || 0) + 1;
69
+ const ts = parseInt(m[2], 10) * 1000;
70
+ if (!firstTs || ts < firstTs) firstTs = ts;
71
+ if (ts > lastTs) lastTs = ts;
72
+ }
73
+ const names = Object.keys(authors);
74
+ const top = names.length ? Math.max(...names.map((n) => authors[n])) : 0;
75
+ return { commits, authors: names.length, topShare: commits ? top / commits : 0, firstTs, lastTs, has: commits > 0 };
76
+ }
77
+
78
+ const GRADES = ["A", "B", "C", "D", "F"];
79
+ /** A deterministic grade from the signals available IN-BROWSER. Honest: lighter
80
+ * than the full server grade (no dep-mortality / hotspots), but real. */
81
+ function gradeLocal(s) {
82
+ let pen = 0;
83
+ if (s.secrets.totalFindings > 0) pen += s.secrets.totalFindings >= 3 ? 4 : s.secrets.totalFindings >= 1 ? 3 : 0; // a leaked secret is severe
84
+ if (s.git.has) { if (s.git.authors === 1) pen += 1; if (s.git.topShare > 0.8) pen += 1; if (s.ageDays > 0 && s.dormantDays > 365) pen += 1; }
85
+ return GRADES[Math.min(pen, 4)];
86
+ }
87
+
88
+ /** Compose a deterministic local report from already-read files + .git reflog. Pure. */
89
+ function summarize(files, gitLog, nowMs) {
56
90
  // files: [{ rel, text }]
57
- let loc = 0, scanned = 0, testHits = 0;
91
+ let loc = 0, scanned = 0, testHits = 0, symbols = 0;
58
92
  const prodHits = [];
59
93
  const langs = {};
60
94
  let deps = { total: 0, deps: 0, devDeps: 0, names: [] };
@@ -64,18 +98,25 @@
64
98
  if (!TEXT_EXT.test(f.rel)) continue;
65
99
  scanned++;
66
100
  loc += f.text.split("\n").length;
101
+ symbols += countSymbols(f.text);
67
102
  const ext = (f.rel.match(/\.([a-z0-9]+)$/i) || [, "?"])[1].toLowerCase();
68
103
  langs[ext] = (langs[ext] || 0) + 1;
69
104
  for (const h of scanSecretsText(f.text, f.rel)) { if (h.isTest) testHits++; else prodHits.push(h); }
70
105
  }
71
- return {
72
- filesScanned: scanned, loc, deps,
106
+ const git = parseGitLog(gitLog);
107
+ const now = nowMs || (git.lastTs || 0);
108
+ const ageDays = git.has && git.firstTs ? Math.max(0, Math.round((git.lastTs - git.firstTs) / 86400000)) : 0;
109
+ const dormantDays = git.has && git.lastTs && now ? Math.max(0, Math.round((now - git.lastTs) / 86400000)) : 0;
110
+ const s = {
111
+ filesScanned: scanned, loc, symbols, deps, git, ageDays, dormantDays,
73
112
  secrets: { totalFindings: prodHits.length, excludedTestHits: testHits, hits: prodHits.slice(0, 20) },
74
113
  langs: Object.entries(langs).sort((a, b) => b[1] - a[1]).slice(0, 8),
75
114
  };
115
+ s.grade = gradeLocal(s);
116
+ return s;
76
117
  }
77
118
 
78
- g.MnemeLocalScan = { scanSecretsText, parseDeps, summarize, _patterns: SECRET_PATTERNS };
119
+ g.MnemeLocalScan = { scanSecretsText, parseDeps, countSymbols, parseGitLog, gradeLocal, summarize, _patterns: SECRET_PATTERNS };
79
120
 
80
121
  // ── File System Access glue (browser-only; needs a user gesture + HTTPS) ────
81
122
  g.MnemeLocalScan.supported = typeof g.showDirectoryPicker === "function";
@@ -99,6 +140,14 @@
99
140
  const dir = await g.showDirectoryPicker(); // native picker — user grants ONE folder
100
141
  const files = [];
101
142
  await readDir(dir, "", files, cap);
102
- return { folder: dir.name || "folder", files: files.length, summary: summarize(files) };
143
+ // REAL git signals: read .git/logs/HEAD (plain text) for authors / commits / age
144
+ let gitLog = "";
145
+ try {
146
+ const gitDir = await dir.getDirectoryHandle(".git");
147
+ const logsDir = await gitDir.getDirectoryHandle("logs");
148
+ const headFile = await logsDir.getFileHandle("HEAD");
149
+ gitLog = await (await headFile.getFile()).text();
150
+ } catch { /* not a git repo, or no reflog — file signals only */ }
151
+ return { folder: dir.name || "folder", files: files.length, summary: summarize(files, gitLog, Date.now()) };
103
152
  };
104
153
  })(typeof window !== "undefined" ? window : globalThis);