sigrank 0.0.211 → 0.0.213

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/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://raw.githubusercontent.com/anthropics/mcpb/main/schemas/mcpb-manifest-v0.4.schema.json",
3
3
  "manifest_version": "0.4",
4
4
  "name": "sigrank",
5
- "version": "0.0.210",
5
+ "version": "0.0.212",
6
6
  "description": "Ranks AI coding operators by token cascade efficiency (Yield = cache_read \u00d7 output / input\u00b2). 23 tools for yield calculation, local token usage pulls, signed submission, and tokscale-powered analytics (market share, developer profiles, model trends, cost analysis, device profiles, MCP usage, competitive intelligence). Privacy-first: runs locally, only four token counts leave the machine.",
7
7
  "author": {
8
8
  "name": "SunrisesIllNeverSee",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigrank",
3
- "version": "0.0.211",
3
+ "version": "0.0.213",
4
4
  "author": "SunrisesIllNeverSee (https://github.com/SunrisesIllNeverSee)",
5
5
  "description": "SigRank MCP server — AI operator evaluation and token cascade efficiency tools. The yield cascade + live leaderboard as MCP tools any agent can call.",
6
6
  "mcpName": "io.github.SunrisesIllNeverSee/sigrank-mcp",
@@ -260,27 +260,12 @@ function cascadeFrom(p) {
260
260
  // ── Unicode bar chart (no dep) ───────────────────────────────────────────────
261
261
  const BLOCKS = " ▏▎▍▌▋▊▉█";
262
262
 
263
- // Linear bar — use when all values are the same order of magnitude.
264
- function barChart(values, labels, opts = {}) {
265
- const { width = 30, colorFn = (s) => s, maxVal } = opts;
266
- const max = maxVal ?? Math.max(...values.filter(Number.isFinite), 1);
267
- const lines = [];
268
- for (let i = 0; i < values.length; i++) {
269
- const v = values[i] ?? 0;
270
- const pct = Math.min(v / max, 1);
271
- const full = Math.floor(pct * width);
272
- const frac = Math.floor((pct * width - full) * 8);
273
- const bar = colorFn("█".repeat(full) + (frac > 0 ? BLOCKS[frac] : ""));
274
- const lbl = padEnd(dim(labels[i] ?? ""), 10);
275
- const val = padStart(fmtTok(v), 8);
276
- lines.push(` ${lbl} ${padEnd(bar, width)} ${val}`);
277
- }
278
- return lines;
279
- }
280
-
281
263
  // Log-scale bar — use when values span multiple orders of magnitude (e.g. token pillars
282
264
  // where cacheRead >> input). Maps log10(v) to bar width so each 10x = same visual step.
283
- // minLog floor prevents zero/tiny values from going negative.
265
+ // minLog floor prevents zero/tiny values from going negative. WIRED into the Dashboard
266
+ // Token Composition section (logTokenBar) so cache-heavy operators like MOSES (1000×
267
+ // scale differences between pillars) render every pillar visibly instead of one pillar
268
+ // erasing the other three.
284
269
  function logBar(v, maxLog, width = 40, colorCode = c.cyan) {
285
270
  if (!v || v <= 0) return { bar: dim("·".repeat(width)), pct: 0 };
286
271
  const log = Math.log10(v);
@@ -297,14 +282,23 @@ function logBar(v, maxLog, width = 40, colorCode = c.cyan) {
297
282
  // ── Sparkline (no dep) ────────────────────────────────────────────────────────
298
283
  const SPARK = "▁▂▃▄▅▆▇█";
299
284
  function sparkline(values) {
300
- const valid = values.filter(Number.isFinite);
285
+ const valid = values.filter((v) => v != null && Number.isFinite(v));
301
286
  if (valid.length === 0) return dim("no data");
287
+ // With only 1 valid point there's no trend to show — render a flat marker
288
+ // at the mid-height so it doesn't look like a rising/falling edge.
289
+ if (valid.length === 1) {
290
+ return values
291
+ .map((v) =>
292
+ v != null && Number.isFinite(v) ? SPARK[4] : dim(" "),
293
+ )
294
+ .join("");
295
+ }
302
296
  const min = Math.min(...valid),
303
297
  max = Math.max(...valid);
304
298
  return values
305
299
  .map((v) => {
306
- if (!Number.isFinite(v)) return dim("·");
307
- const idx = max === min ? 7 : Math.round(((v - min) / (max - min)) * 7);
300
+ if (v == null || !Number.isFinite(v)) return dim(" "); // gap, not a fake low point
301
+ const idx = max === min ? 4 : Math.round(((v - min) / (max - min)) * 7);
308
302
  return SPARK[idx];
309
303
  })
310
304
  .join("");
@@ -524,11 +518,34 @@ function tokenBar(p, width = 40) {
524
518
  return bar;
525
519
  }
526
520
 
521
+ // Log-scale per-pillar breakdown — 4 separate log10 bars (input / cacheW / cacheR / output)
522
+ // so every pillar stays visible even when one dominates by 1000× (e.g. MOSES cacheRead
523
+ // 2.5B vs input 1.25M). The linear tokenBar() above erases small pillars in that case;
524
+ // this companion view shows the true shape. Returns an array of 4 lines:
525
+ // [" I ▏▎▍▌ 1.25M", " W ▏▎▍▌▋ 128M", " R ▏▎▍▌▋▊▉█ 2.5B", " O ▏▎▍ 11.3M"]
526
+ // maxLog is derived from the largest pillar so the biggest pillar fills the bar.
527
+ function logTokenBar(p, width = 36) {
528
+ if (!p) return [dim(" no data")];
529
+ const vals = [
530
+ { lbl: "I", v: p.input ?? 0, color: c.cyan },
531
+ { lbl: "W", v: p.cacheCreate ?? 0, color: c.blue },
532
+ { lbl: "R", v: p.cacheRead ?? 0, color: c.boldGold },
533
+ { lbl: "O", v: p.output ?? 0, color: c.green },
534
+ ];
535
+ const maxV = Math.max(...vals.map((x) => x.v), 1);
536
+ const maxLog = Math.log10(maxV);
537
+ if (maxLog <= 0) return [dim(" no data")]; // all pillars ≤ 1 → nothing to show
538
+ return vals.map(({ lbl, v, color }) => {
539
+ const { bar } = logBar(v, maxLog, width, color);
540
+ return ` ${paint(color, lbl)} ${bar} ${dim(fmtTok(v))}`;
541
+ });
542
+ }
543
+
527
544
  // Generalized cascade-metric sparkline across windows (7d→30d→90d→all).
528
545
  // pick: (cas) => number · fmt: (v) => string. Used by the Trends tab for every metric.
529
546
  // FIX G1: window headers are rendered once at the top of each metric block (mirror
530
- // renderCompare's column layout). metricSpark/yieldSpark now return sparkline + RIGHT-
531
- // ALIGNED values in fixed COL_W columns — NO per-value `w:` prefix (the header row
547
+ // renderCompare's column layout). metricSpark returns sparkline + RIGHT-ALIGNED
548
+ // values in fixed COL_W columns — NO per-value `w:` prefix (the header row
532
549
  // carries the window labels). Null windows render a dim `—` (columns stay aligned).
533
550
  const TREND_COL_W = 10;
534
551
  const TREND_WINS = ["7d", "30d", "90d", "all"];
@@ -550,23 +567,14 @@ function metricSpark(d, pick, fmt) {
550
567
  const cols = vals.map((v) =>
551
568
  v != null ? padStart(fmt(v), TREND_COL_W) : padStart(dim("—"), TREND_COL_W),
552
569
  );
553
- return sparkline(vals) + ` ${cols.join(" ")}`;
554
- }
555
-
556
- // Yield sparkline across windows
557
- function yieldSpark(d) {
558
- const vals = TREND_WINS.map((w) => {
559
- const wd = d.windows?.find((x) => x.window === w);
560
- if (!wd) return null;
561
- const cas = cascadeFrom(wd.pillars);
562
- return cas?.yield ?? null;
563
- });
564
- const cols = vals.map((v) =>
565
- v != null
566
- ? padStart(fmtY(v), TREND_COL_W)
567
- : padStart(dim("—"), TREND_COL_W),
570
+ // Non-numeric pick values (e.g. Op Ratio returns an object) can't sparkline —
571
+ // show just the formatted columns. Numeric values get the sparkline prefix.
572
+ const numericVals = vals.map((v) =>
573
+ v != null && Number.isFinite(v) ? v : null,
568
574
  );
569
- return sparkline(vals) + ` ${cols.join(" ")}`;
575
+ const hasNumeric = numericVals.some((v) => v != null);
576
+ if (!hasNumeric) return ` ${cols.join(" ")}`;
577
+ return sparkline(numericVals) + ` ${cols.join(" ")}`;
570
578
  }
571
579
 
572
580
  // ── TAB 1: DASHBOARD ─────────────────────────────────────────────────────────
@@ -828,6 +836,30 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
828
836
  ` ${padEnd(bold(active.map((d) => d.platform).join("+")), 10)} ${tokenBar(cp, 50)} ${dim(fmtTok(cp.input + cp.output + cp.cacheCreate + cp.cacheRead))}`,
829
837
  );
830
838
  }
839
+ // Log-scale per-pillar breakdown — companion to the linear stacked bar above.
840
+ // The linear bar erases small pillars when one dominates by 1000× (MOSES-class:
841
+ // cacheRead 2.5B vs input 1.25M → input gets 0 width). The log view shows every
842
+ // pillar's true shape so the operator can see the full composition at a glance.
843
+ // Only renders for the combined row (or the single platform when there's one) to
844
+ // avoid blowing the height budget on multi-platform setups.
845
+ if (used < budget - boardLines - 5) {
846
+ const cp = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
847
+ for (const d of active) {
848
+ const all = d.windows?.find((w) => w.window === "all");
849
+ if (!all) continue;
850
+ cp.input += all.pillars.input ?? 0;
851
+ cp.output += all.pillars.output ?? 0;
852
+ cp.cacheCreate += all.pillars.cacheCreate ?? 0;
853
+ cp.cacheRead += all.pillars.cacheRead ?? 0;
854
+ }
855
+ const total = cp.input + cp.output + cp.cacheCreate + cp.cacheRead;
856
+ if (total > 0) {
857
+ emit(
858
+ ` ${dim("log-scale (each 10× = same step — every pillar visible)")}`,
859
+ );
860
+ for (const line of logTokenBar(cp, 36)) emit(line);
861
+ }
862
+ }
831
863
  }
832
864
 
833
865
  // ── Your insights + the Three Degrees panel (replaces the old top-3 mini board)
@@ -1042,11 +1074,43 @@ function renderTrends(data, subIdx = 0) {
1042
1074
  emit();
1043
1075
 
1044
1076
  if (sub === "You" || sub === "Platform") {
1077
+ // FIX (2026-08-18): "You" previously took only active[0]?.windows (the first
1078
+ // platform by sort order — usually claude). If that platform had no timestamps
1079
+ // (NULL-TIMESTAMP WINDOW ASYMMETRY → 7d/30d/90d empty) or no local data, "You"
1080
+ // showed nothing even when other platforms had data. Now aggregate ALL active
1081
+ // platforms' windows into one combined "you" row, summing pillars per window
1082
+ // so every platform contributes to every window it has data for.
1045
1083
  const rows =
1046
1084
  sub === "You"
1047
1085
  ? active?.length
1048
- ? [{ platform: "you", windows: active[0]?.windows }]
1049
- : [] // primary cascade
1086
+ ? [
1087
+ {
1088
+ platform: "you",
1089
+ windows: TREND_WINS.map((wk) => {
1090
+ let input = 0,
1091
+ output = 0,
1092
+ cacheCreate = 0,
1093
+ cacheRead = 0,
1094
+ messages = 0;
1095
+ for (const d of active) {
1096
+ const wd = d.windows?.find((x) => x.window === wk);
1097
+ if (wd) {
1098
+ input += wd.pillars.input ?? 0;
1099
+ output += wd.pillars.output ?? 0;
1100
+ cacheCreate += wd.pillars.cacheCreate ?? 0;
1101
+ cacheRead += wd.pillars.cacheRead ?? 0;
1102
+ messages += wd.messages ?? 0;
1103
+ }
1104
+ }
1105
+ return {
1106
+ window: wk,
1107
+ pillars: { input, output, cacheCreate, cacheRead },
1108
+ messages,
1109
+ };
1110
+ }),
1111
+ },
1112
+ ]
1113
+ : []
1050
1114
  : (active ?? []);
1051
1115
  if (!rows.length) {
1052
1116
  emit(` ${dim(" reading your cascade… (press [R] to refresh)")}`);
@@ -1071,7 +1135,7 @@ function renderTrends(data, subIdx = 0) {
1071
1135
  if (used >= budget - 2) break;
1072
1136
  if (m.stub) {
1073
1137
  emit(
1074
- ` ${padEnd(dim(m.label), 10)} ${dim("▁▁▁▁ calculating… (" + m.stub + ")")}`,
1138
+ ` ${padEnd(dim(m.label), 10)} ${dim(" calculating… (" + m.stub + ")")}`,
1075
1139
  );
1076
1140
  } else {
1077
1141
  emit(
@@ -1083,7 +1147,7 @@ function renderTrends(data, subIdx = 0) {
1083
1147
  // rank trend — wired later; honest placeholder for now
1084
1148
  if (used < budget - 1)
1085
1149
  emit(
1086
- ` ${padEnd(dim("Rank"), 10)} ${dim("▁▁▁▁ calculating… (rank history wires post-ingest)")}`,
1150
+ ` ${padEnd(dim("Rank"), 10)} ${dim(" calculating… (rank history wires post-ingest)")}`,
1087
1151
  );
1088
1152
  else trendsDropped += 1;
1089
1153
  emit();
@@ -1104,7 +1168,11 @@ function renderTrends(data, subIdx = 0) {
1104
1168
 
1105
1169
  // ── TAB 3: COMPARE ───────────────────────────────────────────────────────────
1106
1170
  function renderCompare(data) {
1107
- const { tpData, cc, ts, td, platform } = data;
1171
+ // FIX (2026-08-18): guard against null data — the debug renderOnce path
1172
+ // (node tui.mjs --render 1) passes null when loadCompareData fails, and
1173
+ // destructure of null throws. Coalesce to a safe empty shape.
1174
+ const { tpData, cc, ts, td } = data ?? {};
1175
+ const platform = data?.platform ?? "claude";
1108
1176
  const WINS = ["7d", "30d", "90d", "all"];
1109
1177
  const w = W();
1110
1178
  const budget = H() - 4;
@@ -1163,8 +1231,27 @@ function renderCompare(data) {
1163
1231
  );
1164
1232
  }
1165
1233
  }
1234
+ // FIX (2026-08-18): surface WHY external verifiers are absent so the user knows
1235
+ // it's a missing-tool issue, not a bug. Each verifier is null when its CLI/DB
1236
+ // isn't installed or returned no data for this platform.
1237
+ const missing = [];
1238
+ if (!cc) missing.push("ccusage (npm i -g ccusage)");
1239
+ if (!td) missing.push("token-dashboard (~/.claude/token-dashboard.db)");
1240
+ if (!ts) missing.push("tokscale (npm i -g tokscale)");
1241
+ if (missing.length)
1242
+ emit(` ${dim(` no data from: ${missing.join(" · ")}`)}`);
1166
1243
  emit();
1167
1244
 
1245
+ // FIX (2026-08-18): if ALL sources are empty (tokenpull has no windows AND all
1246
+ // verifiers are null), show a clear empty-state message instead of bare headers
1247
+ // with zero data rows.
1248
+ if (SOURCES.length === 0) {
1249
+ emit(
1250
+ ` ${dim(" no source data available for this platform — install a verifier or check ~/.claude/projects/")}`,
1251
+ );
1252
+ return;
1253
+ }
1254
+
1168
1255
  const COL_W = 11;
1169
1256
  const hcols = [
1170
1257
  padEnd(dim("Source"), 12),
@@ -1789,20 +1876,23 @@ function renderSubmitPreview(dashData, id) {
1789
1876
  }
1790
1877
 
1791
1878
  // ── DEBUG: render a tab once (no TTY/alt-screen) + audit each line's visible
1792
- // width vs terminal columns. Usage: `node tui.mjs --render [0|1|2]`. Prints a
1793
- // width report (>w = would overflow/wrap) then the raw frame. For diagnosing
1794
- // layout overflow without an interactive session.
1879
+ // width vs terminal columns. Usage: `node tui.mjs --render [0-5]`. Tab indices
1880
+ // match the live TABS array: 0=Dashboard, 1=Trends, 2=Compare, 3=Board,
1881
+ // 4=Watch, 5=Connect. Prints a width report (>w = would overflow/wrap) then
1882
+ // the raw frame. For diagnosing layout overflow without an interactive session.
1795
1883
  async function renderOnce(tabIdx = 0) {
1796
1884
  const w = W();
1797
1885
  const data = {
1798
1886
  0: await loadDashboardData().catch((e) => ({ error: e.message })),
1799
- 1: await loadCompareData("claude").catch(() => null),
1800
- 2: await loadBoardData("30d").catch(() => null),
1887
+ 1: await loadDashboardData().catch((e) => ({ error: e.message })),
1888
+ 2: await loadCompareData("claude").catch(() => null),
1889
+ 3: await loadBoardData("30d").catch(() => null),
1801
1890
  }[tabIdx];
1802
1891
  startBuffer();
1803
1892
  if (tabIdx === 0) renderDashboard(data, "debug");
1804
- else if (tabIdx === 1) renderCompare(data);
1805
- else if (tabIdx === 2) renderBoard(data, "30d");
1893
+ else if (tabIdx === 1) renderTrends(data, 0);
1894
+ else if (tabIdx === 2) renderCompare(data);
1895
+ else if (tabIdx === 3) renderBoard(data, "30d");
1806
1896
  else if (tabIdx === 4) await renderWatch("all", "all-windows");
1807
1897
  else if (tabIdx === 5) renderConnect(loadIdentity(), "", "");
1808
1898
  const lines = _screenBuf || [];