@mneme-ai/xray 2.173.0 → 2.175.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 +2 -2
- package/public/card.js +51 -0
- package/public/index.html +52 -27
- package/public/local-scan.js +56 -7
- package/public/report.html +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mneme-ai/xray",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.175.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": "2.
|
|
50
|
+
"@mneme-ai/core": "2.175.0",
|
|
51
51
|
"@resvg/resvg-js": "^2.6.2"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
package/public/card.js
CHANGED
|
@@ -52,12 +52,58 @@
|
|
|
52
52
|
return A;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// VERDICT — turn the accurate metrics into a DECISION a human can act on:
|
|
56
|
+
// "should I trust / adopt / inherit this repo, and what do I do about it?"
|
|
57
|
+
// Every line is derived 100% from a signed metric (no new data, no AI guess).
|
|
58
|
+
function synthesizeVerdict(r) {
|
|
59
|
+
const num = (x) => (Number.isFinite(Number(x)) ? Number(x) : 0);
|
|
60
|
+
const dep = r.deps || {}, sec = r.secrets || {}, bf = r.busFactor || {}, age = r.age || {}, cx = r.complexity || {}, su = r.security || {}, hs = r.hotspots || {};
|
|
61
|
+
const dying = num((dep.byBand || {}).dead) + num((dep.byBand || {}).moribund);
|
|
62
|
+
const copyleft = num((dep.licenses || {})["strong-copyleft"]) + num((dep.licenses || {})["weak-copyleft"]);
|
|
63
|
+
const secrets = num(sec.totalFindings), destructive = (su.destructive || []).length;
|
|
64
|
+
const soloPct = num(bf.singleOwnerFilePct), topShare = num(bf.topContributorShare), busF = num(bf.busFactor);
|
|
65
|
+
const symbols = num(cx.totalSymbols), codeFiles = num(cx.filesAnalysed);
|
|
66
|
+
const isDocs = symbols < 30 && (codeFiles === 0 || symbols / Math.max(1, codeFiles) < 1.5);
|
|
67
|
+
const kind = isDocs ? "Docs / content repo" : "Code project";
|
|
68
|
+
|
|
69
|
+
// the takeaways — what it means + what to DO, severity-ranked, action-first
|
|
70
|
+
const T = [];
|
|
71
|
+
if (sec.worstVerdict === "BLOCK" && secrets > 0) T.push({ t: "bad", x: `🔴 ${secrets} live secret${secrets > 1 ? "s" : ""} in production code — rotate them now and add a pre-commit secret scan${sec.hits && sec.hits[0] ? ` (e.g. ${esc(sec.hits[0].file)}:${sec.hits[0].line})` : ""}.` });
|
|
72
|
+
else if (secrets > 0) T.push({ t: "warn", x: `🟠 ${secrets} credential-pattern match${secrets > 1 ? "es" : ""} to review in production code.` });
|
|
73
|
+
if (destructive > 0) T.push({ t: "bad", x: `🔴 ${destructive} destructive command${destructive > 1 ? "s" : ""} in build/CI (${esc((su.destructive[0] || {}).where || "ci")}) — audit before trusting this pipeline.` });
|
|
74
|
+
else if (num(su.injectionFindings) > 0) T.push({ t: "warn", x: `🟠 ${num(su.injectionFindings)} possible prompt-injection in docs — sanitize before feeding to an AI.` });
|
|
75
|
+
if (dying > 0) { const a = (dep.atRisk || [])[0]; T.push({ t: "warn", x: `🟠 ${dying} dependency${dying > 1 ? "ies" : "y"} dying/abandoned — plan a migration${a && a.successor ? ` (e.g. ${esc(a.name)} → ${esc(a.successor)})` : ""}.` }); }
|
|
76
|
+
if (copyleft > 0) T.push({ t: "warn", x: `🟠 ${copyleft} dependency${copyleft > 1 ? "ies" : "y"} with copyleft/unknown license — check before commercial use.` });
|
|
77
|
+
if (age.vitality === "archived" || age.dormant) T.push({ t: "warn", x: `🟠 No recent activity (${esc(age.vitality || "stalled")}) — may be unmaintained; pin a version if you depend on it.` });
|
|
78
|
+
else if (age.vitality === "active") T.push({ t: "ok", x: `✅ Actively maintained — ${num(age.totalCommits).toLocaleString()} commits over ${esc(age.lifespan || "its life")}, ${num(age.totalAuthors)} contributors. Low abandonment risk.` });
|
|
79
|
+
if (busF <= 1 && num(bf.authors) > 0) T.push({ t: "warn", x: `🟠 Key-person risk: one author owns ${topShare}% of commits${soloPct ? ` and ${soloPct}% of files have a single owner` : ""}. If they leave, those areas stall — spread reviews + document.` });
|
|
80
|
+
else if (soloPct >= 40) { const ff = (bf.fragileFiles || []).slice(0, 3).map((x) => esc(x.file)).join(", "); T.push({ t: "warn", x: `🟠 ${soloPct}% of files have a single owner — pair-review the fragile ones${ff ? `: ${ff}` : ""}.` }); }
|
|
81
|
+
if (secrets === 0 && destructive === 0 && dying === 0 && copyleft === 0) T.push({ t: "ok", x: `✅ Clean to adopt — no leaked secrets, no dying/risky-licensed deps, no destructive CI.` });
|
|
82
|
+
if ((hs.hotspots || [])[0] && !isDocs) { const h = hs.hotspots[0]; T.push({ t: "info", x: `ℹ️ If you change one thing first, it's <b>${esc(h.file)}</b> (highest churn×size)${h.expert ? ` — ${esc(h.expert)} knows it best` : ""}.` }); }
|
|
83
|
+
|
|
84
|
+
// the headline DECISION (worst-signal-wins)
|
|
85
|
+
const hasRed = secrets > 0 || destructive > 0;
|
|
86
|
+
const stale = age.vitality === "archived" || age.dormant;
|
|
87
|
+
const keyrisk = busF <= 1 || soloPct >= 60;
|
|
88
|
+
let tone, head;
|
|
89
|
+
if (hasRed) { tone = "bad"; head = "⚠️ Exposed risk — fix the red items before you trust this repo"; }
|
|
90
|
+
else if (stale) { tone = "warn"; head = "🪦 Looks unmaintained — risky to depend on without pinning"; }
|
|
91
|
+
else if (dying > 0) { tone = "warn"; head = "Adopt with care — aging dependencies need a migration plan"; }
|
|
92
|
+
else if (keyrisk) { tone = "warn"; head = "✅ Maintained — but ⚠️ concentrated in one person (key-person risk)"; }
|
|
93
|
+
else if (age.vitality === "active") { tone = "ok"; head = `✅ Healthy & actively maintained — safe to build on`; }
|
|
94
|
+
else { tone = "neutral"; head = `Reviewed — ${T.length} thing${T.length === 1 ? "" : "s"} to know below`; }
|
|
95
|
+
|
|
96
|
+
const top = T.slice(0, 5);
|
|
97
|
+
return { tone, head, kind, takeaways: top };
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
function xrayCardHTML(signed, opts) {
|
|
56
101
|
opts = opts || {};
|
|
57
102
|
const r = signed.report, s = r.summary;
|
|
58
103
|
const dep = r.deps, sec = r.secrets, bf = r.busFactor, age = r.age, cx = r.complexity;
|
|
59
104
|
const verified = signed.receipt ? '<span class="verified"><span class="dot"></span>Ed25519 — verifies offline</span>' : "unsigned";
|
|
60
105
|
const tri = triageOf(r);
|
|
106
|
+
const vd = synthesizeVerdict(r);
|
|
61
107
|
|
|
62
108
|
const depChips = (dep.atRisk || []).slice(0, 6).map((d) =>
|
|
63
109
|
`<span class="chip ${d.band === "dead" ? "bad" : "warn"}">${esc(d.name)} · ${d.band}${d.successor ? ` → ${esc(d.successor)}` : ""}</span>`).join("") || `<span class="chip">none dying</span>`;
|
|
@@ -86,6 +132,11 @@
|
|
|
86
132
|
${r.subject.kind === "git-url" ? `<a class="repourl" href="${esc(r.subject.ref)}" target="_blank" rel="noopener">${esc(r.subject.ref)} ↗</a>` : `<div class="repourl">${esc(r.subject.ref)}</div>`}
|
|
87
133
|
<div class="head">${esc(s.headline)} · ${s.signalsRun} signals · @ ${esc(String(r.subject.commitHash).slice(0, 10))}</div></div>
|
|
88
134
|
</div>
|
|
135
|
+
<div class="verdict v-${vd.tone}">
|
|
136
|
+
<div class="vhead">${esc(vd.head)}</div>
|
|
137
|
+
<div class="vkind">${esc(vd.kind)} · what this means for you ↓</div>
|
|
138
|
+
<ul class="vlist">${vd.takeaways.map((t) => `<li class="vt-${t.t}">${t.x}</li>`).join("")}</ul>
|
|
139
|
+
</div>
|
|
89
140
|
<div class="membrane">
|
|
90
141
|
<div class="mp"><span class="mpk">① CAPABILITY</span><span class="mpv">${s.signalsRun} deterministic signals · ${(sec.filesScanned || 0).toLocaleString()} files scanned</span></div>
|
|
91
142
|
<div class="mp"><span class="mpk">② ATTENTION</span><span class="mpv">${tri.length ? `${tri.length} signal(s) need attention` : `all signals clear`}</span></div>
|
package/public/index.html
CHANGED
|
@@ -70,6 +70,15 @@
|
|
|
70
70
|
.top .repourl{display:inline-block;font-size:12.5px;color:var(--a);text-decoration:none;margin-top:3px;word-break:break-all;font-family:ui-monospace,Menlo,monospace}
|
|
71
71
|
.top .repourl:hover{text-decoration:underline}
|
|
72
72
|
.top .head{color:var(--sub);font-size:14px;margin-top:4px}
|
|
73
|
+
.verdict{padding:18px 30px;border-bottom:1px solid #eef0f2}
|
|
74
|
+
.verdict .vhead{font-size:17px;font-weight:680;letter-spacing:-.01em;line-height:1.3;color:#0b0b0f}
|
|
75
|
+
.verdict .vkind{font-size:12px;color:#8b8f98;margin:3px 0 12px;font-weight:560}
|
|
76
|
+
.verdict .vlist{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:8px}
|
|
77
|
+
.verdict .vlist li{font-size:13.5px;line-height:1.5;color:#33333b}
|
|
78
|
+
.v-bad{background:#fff5f6} .v-bad .vhead{color:#be123c}
|
|
79
|
+
.v-warn{background:#fffaf0} .v-warn .vhead{color:#b45309}
|
|
80
|
+
.v-ok{background:#f0fdf4} .v-ok .vhead{color:#15803d}
|
|
81
|
+
.v-neutral{background:#fafafb}
|
|
73
82
|
.trustbar{display:flex;align-items:center;gap:14px;padding:13px 30px;background:#f0fdf4;border-bottom:1px solid #dcfce7;flex-wrap:wrap}
|
|
74
83
|
.hgauge{display:inline-flex;align-items:center;gap:8px;font-weight:680;color:#15803d;font-size:14px;white-space:nowrap}
|
|
75
84
|
.hdot{width:10px;height:10px;border-radius:50%;background:#16a34a;box-shadow:0 0 0 4px rgba(22,163,74,.16)}
|
|
@@ -146,6 +155,10 @@
|
|
|
146
155
|
.listfoot{display:flex;justify-content:space-between;align-items:center;padding:14px 4px 0;font-size:13px}
|
|
147
156
|
.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
157
|
.moreb:hover{border-color:var(--ink)}
|
|
158
|
+
.pagenav{display:inline-flex;gap:8px}
|
|
159
|
+
.pagenav .moreb{background:var(--ink);color:#fff;border-color:var(--ink)}
|
|
160
|
+
.pagenav .moreb:hover{opacity:.88}
|
|
161
|
+
.pagenav .moreb:disabled{background:#fff;color:#c8ccd2;border-color:var(--line);opacity:1;cursor:not-allowed}
|
|
149
162
|
/* share + buttons */
|
|
150
163
|
.share{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:18px 30px;border-top:1px solid var(--line2)}
|
|
151
164
|
.badgeimg{height:20px}
|
|
@@ -182,7 +195,9 @@
|
|
|
182
195
|
.bridgebox{margin-top:14px;border-top:1px solid var(--line);padding-top:12px}
|
|
183
196
|
.bridgebox summary{cursor:pointer;font-size:12.5px;color:var(--a);font-weight:560;list-style:none}
|
|
184
197
|
.lscard{margin-top:12px;border:1px solid var(--line);border-radius:12px;background:#fff;padding:14px 16px;text-align:left}
|
|
185
|
-
.lscard .
|
|
198
|
+
.lscard .lstop{display:flex;align-items:center;gap:14px;margin-bottom:8px}
|
|
199
|
+
.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}
|
|
200
|
+
.lscard .lsh{font-size:15px;color:var(--ink)}
|
|
186
201
|
.lscard .lsrow{display:flex;gap:12px;padding:7px 0;border-top:1px solid var(--line2);font-size:13px}
|
|
187
202
|
.lscard .lsk{width:110px;color:var(--sub);font-weight:560;flex-shrink:0}
|
|
188
203
|
.lscard .lsv{color:var(--ink2)} .lscard .lsv.bad{color:var(--red)} .lscard .lsv.ok{color:var(--green)}
|
|
@@ -190,19 +205,24 @@
|
|
|
190
205
|
.lscard .lsok{margin:6px 0;font-size:12.5px;color:var(--green)}
|
|
191
206
|
.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
207
|
.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
|
|
194
|
-
.showcase{max-width:
|
|
195
|
-
|
|
208
|
+
/* SHOWCASE — selling points; the graphic sits to the SIDE (grid areas, no markup move) */
|
|
209
|
+
.showcase{max-width:1060px;margin:64px auto 0;padding:0 20px;text-align:left;
|
|
210
|
+
display:grid;column-gap:36px;row-gap:10px;align-items:start;align-content:start;
|
|
211
|
+
grid-template-columns:1fr 380px;grid-template-areas:"title art" "sub art" "cards cards"}
|
|
212
|
+
.showcase .sctitle{grid-area:title;font-size:30px;font-weight:700;letter-spacing:-.02em;color:var(--ink);margin:0;line-height:1.2}
|
|
196
213
|
.showcase .sctitle .hl{color:var(--a)}
|
|
197
|
-
.showcase .scsub{font-size:15px;color:var(--sub);
|
|
198
|
-
.showcase .
|
|
199
|
-
|
|
214
|
+
.showcase .scsub{grid-area:sub;font-size:15px;color:var(--sub);margin:0;line-height:1.6}
|
|
215
|
+
.showcase .bigart{grid-area:art;display:flex;justify-content:center;align-self:center;opacity:.95}
|
|
216
|
+
.showcase .sc3{grid-area:cards;display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:14px}
|
|
200
217
|
.showcase .sccard{background:var(--soft);border:1px solid var(--line);border-radius:16px;padding:20px}
|
|
201
218
|
.showcase .scico{font-size:26px;margin-bottom:8px}
|
|
202
219
|
.showcase .sccard b{display:block;font-size:15px;color:var(--ink);margin-bottom:6px}
|
|
203
220
|
.showcase .sccard span{font-size:13px;color:var(--sub);line-height:1.55}
|
|
204
|
-
|
|
205
|
-
|
|
221
|
+
@media(max-width:820px){
|
|
222
|
+
.showcase{grid-template-columns:1fr;grid-template-areas:"title" "sub" "art" "cards"}
|
|
223
|
+
.showcase .sc3{grid-template-columns:1fr}
|
|
224
|
+
.showcase .bigart svg{width:300px;height:auto}
|
|
225
|
+
}
|
|
206
226
|
.picker{display:none;margin-top:12px;border:1px solid var(--line);border-radius:10px;background:#fff;overflow:hidden}
|
|
207
227
|
.picker.on{display:block}
|
|
208
228
|
.pkbar{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--line2);font-size:12.5px}
|
|
@@ -434,12 +454,7 @@ function mountTracking(r){
|
|
|
434
454
|
const gitUrl = r.subject.ref;
|
|
435
455
|
panel.style.display="block";
|
|
436
456
|
panel.innerHTML = `<div class="thead"><span class="ticon">🛰</span><b>Live tracking</b><span class="tnew">new</span></div>
|
|
437
|
-
<div class="thelp">
|
|
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>
|
|
457
|
+
<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
458
|
<div class="trow">
|
|
444
459
|
<span class="tlabel">Branch</span>
|
|
445
460
|
<select id="tbranch"><option value="">loading branches…</option></select>
|
|
@@ -461,12 +476,13 @@ function mountTracking(r){
|
|
|
461
476
|
const toggle=document.getElementById("ttoggle");
|
|
462
477
|
const stateEl=()=>document.getElementById("tstate");
|
|
463
478
|
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"; }
|
|
479
|
+
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
480
|
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
481
|
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
482
|
function startSSE(id, branch){
|
|
468
483
|
es=new EventSource("/api/track/"+id+"/stream");
|
|
469
484
|
toggle.className="tbtn stop"; toggle.textContent="Stop"; toggle.disabled=false;
|
|
485
|
+
const sel=document.getElementById("tbranch"); if(sel) sel.disabled=true; // lock branch while LIVE
|
|
470
486
|
stateEl().innerHTML='<span class="live"><span class="dot"></span>LIVE · '+esc(branch||"default")+'</span>';
|
|
471
487
|
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
488
|
es.addEventListener("update", ev=>{ try{ const p=JSON.parse(ev.data); renderCard(p.signed); showDrift(p.delta); pushTL(p.delta);}catch{} });
|
|
@@ -548,7 +564,7 @@ document.getElementById("savekey").addEventListener("click", ()=>{
|
|
|
548
564
|
});
|
|
549
565
|
|
|
550
566
|
let tab="board", offset=0, loaded=[];
|
|
551
|
-
const PAGE=
|
|
567
|
+
const PAGE=5;
|
|
552
568
|
function fmtDate(s){ if(!s) return "—"; const d=new Date(s); return isNaN(d)?"—":d.toLocaleDateString(undefined,{year:"numeric",month:"short",day:"numeric"}); }
|
|
553
569
|
function rowHTML(b){
|
|
554
570
|
const lock = b.visibility==="private" ? '<span class="lock" title="private — only you can open this">🔒</span>' : "";
|
|
@@ -578,16 +594,18 @@ async function loadList(reset=true){
|
|
|
578
594
|
}
|
|
579
595
|
try{
|
|
580
596
|
const data=await fetchPage(offset);
|
|
581
|
-
|
|
582
|
-
if(
|
|
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
|
|
597
|
+
const items=data.items||[];
|
|
598
|
+
if((data.total||0)===0){
|
|
599
|
+
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
600
|
return;
|
|
585
601
|
}
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
602
|
+
const start=data.offset+1, end=data.offset+items.length;
|
|
603
|
+
const hasPrev=data.offset>0, hasNext=(data.offset+data.limit)<data.total;
|
|
604
|
+
el.innerHTML = `<div class="listbox">${items.map(rowHTML).join("")}</div>`
|
|
605
|
+
+ `<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
606
|
el.querySelectorAll(".bitem[data-fp]").forEach(n=>n.addEventListener("click",()=>openReport(n.dataset.fp, n.dataset.priv==="1")));
|
|
590
|
-
const
|
|
607
|
+
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"}); });
|
|
608
|
+
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
609
|
}catch{ el.innerHTML='<div class="bitem muted">Could not load.</div>'; }
|
|
592
610
|
}
|
|
593
611
|
async function openReport(fp, isPrivate){
|
|
@@ -648,14 +666,21 @@ function renderLocalScan(r){
|
|
|
648
666
|
const s=r.summary, out=document.getElementById("localout"); if(!out) return;
|
|
649
667
|
const sec=s.secrets, secCls=sec.totalFindings>0?"bad":"ok";
|
|
650
668
|
const langs=s.langs.map(([e,n])=>`<span class="pill">${esc(e)} ${n}</span>`).join(" ");
|
|
669
|
+
const g=s.git, vit = !g.has ? "" : (s.dormantDays>365?"dormant":s.dormantDays>120?"slowing":"active");
|
|
670
|
+
const gitRows = g.has ? `
|
|
671
|
+
<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>
|
|
672
|
+
<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>`
|
|
673
|
+
: `<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
674
|
out.innerHTML=`<div class="lscard">
|
|
652
|
-
<div class="
|
|
675
|
+
<div class="lstop"><div class="lsgrade g-${("ABCDEF".includes(s.grade)?s.grade:"C")}">${esc(s.grade)}</div>
|
|
676
|
+
<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
677
|
<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
678
|
${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>`}
|
|
679
|
+
${gitRows}
|
|
655
680
|
<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">
|
|
681
|
+
<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
682
|
<div class="lsrow"><span class="lsk">Languages</span><span class="lsv">${langs||"—"}</span></div>
|
|
658
|
-
<div class="lsnote">
|
|
683
|
+
<div class="lsnote">Computed <b>in your browser</b> (unsigned, nothing uploaded). Secrets · dependencies · complexity from your files; bus factor & 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
684
|
</div>`;
|
|
660
685
|
}
|
|
661
686
|
const pick=document.getElementById("pickfolder");
|
package/public/local-scan.js
CHANGED
|
@@ -51,10 +51,44 @@
|
|
|
51
51
|
} catch { return { total: 0, deps: 0, devDeps: 0, names: [] }; }
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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);
|
package/public/report.html
CHANGED
|
@@ -23,6 +23,15 @@
|
|
|
23
23
|
.g-A{background:linear-gradient(135deg,#1fb255,#15863f)}.g-B{background:linear-gradient(135deg,#7bb736,#5c8f1e)}
|
|
24
24
|
.g-C{background:linear-gradient(135deg,#eaa83a,#c9821a)}.g-D{background:linear-gradient(135deg,#f0742e,#d4571a)}.g-F{background:linear-gradient(135deg,#f43f5e,#be123c)}
|
|
25
25
|
.top .repo{font-size:22px;font-weight:640;letter-spacing:-.02em;color:var(--ink);word-break:break-all;line-height:1.2}.top .repobr{font-size:13px;font-weight:600;color:var(--a);background:var(--a-soft);border-radius:6px;padding:1px 7px;vertical-align:middle}.top .repourl{display:inline-block;font-size:12.5px;color:var(--a);text-decoration:none;margin-top:3px;word-break:break-all;font-family:ui-monospace,Menlo,monospace}.top .repourl:hover{text-decoration:underline}.top .head{color:var(--sub);font-size:14px;margin-top:4px}
|
|
26
|
+
.verdict{padding:18px 30px;border-bottom:1px solid #eef0f2}
|
|
27
|
+
.verdict .vhead{font-size:17px;font-weight:680;letter-spacing:-.01em;line-height:1.3;color:#0b0b0f}
|
|
28
|
+
.verdict .vkind{font-size:12px;color:#8b8f98;margin:3px 0 12px;font-weight:560}
|
|
29
|
+
.verdict .vlist{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:8px}
|
|
30
|
+
.verdict .vlist li{font-size:13.5px;line-height:1.5;color:#33333b}
|
|
31
|
+
.v-bad{background:#fff5f6} .v-bad .vhead{color:#be123c}
|
|
32
|
+
.v-warn{background:#fffaf0} .v-warn .vhead{color:#b45309}
|
|
33
|
+
.v-ok{background:#f0fdf4} .v-ok .vhead{color:#15803d}
|
|
34
|
+
.v-neutral{background:#fafafb}
|
|
26
35
|
.trustbar{display:flex;align-items:center;gap:14px;padding:13px 30px;background:#f0fdf4;border-bottom:1px solid #dcfce7;flex-wrap:wrap}
|
|
27
36
|
.hgauge{display:inline-flex;align-items:center;gap:8px;font-weight:680;color:#15803d;font-size:14px}
|
|
28
37
|
.hdot{width:10px;height:10px;border-radius:50%;background:#16a34a;box-shadow:0 0 0 4px rgba(22,163,74,.16)}
|