sigrank 0.0.181 → 0.0.183

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.
@@ -15,7 +15,7 @@
15
15
  * npx sigrank watch --window 7d watch one window only (optional filter)
16
16
  *
17
17
  * Color palette mirrors the SigRank web dark theme:
18
- * gold = class TRANSMITTER headline + rank #1
18
+ * gold = rank #1 headline
19
19
  * cyan = active metrics / your row highlight
20
20
  * dim = secondary data, separators
21
21
  * red = negative movement / delta
@@ -23,9 +23,11 @@
23
23
  */
24
24
 
25
25
  import { callTool, DEFAULT_API_BASE, pullActivePlatforms } from "../tools.mjs";
26
- import { classify } from "../cascade.mjs";
26
+ import { cascade, classify, CLASS_TIERS, UNCLASSED, tierOf } from "../cascade.mjs";
27
+ import { ALL_PLATFORMS } from "../adapters.mjs";
27
28
  import { ensureIdentity, keystorePath } from "../keystore.mjs";
28
29
  import { submitSignedWindow } from "../submit.mjs";
30
+ import { LEADERBOARD_METRIC } from "../lib/constants.mjs";
29
31
  import { execFile } from "child_process";
30
32
  import { existsSync, readFileSync } from "fs";
31
33
  import os from "os";
@@ -88,8 +90,10 @@ const green = (str) => paint(c.green, str);
88
90
  const red = (str) => paint(c.red, str);
89
91
 
90
92
  // ── Class tier → color ─────────────────────────────────────────────────────
93
+ // Color is keyed on the BASE TIER (8 entries in CLASS_TIERS). classify() returns
94
+ // a full sub-stage string (e.g. "REFINER II") — tierOf() extracts the base tier
95
+ // for color lookup. TRANSMITTER is not a class. UNCLASSED for no-data.
91
96
  const CLASS_COLOR = {
92
- TRANSMITTER: (s) => paint(c.boldGold, s),
93
97
  "ARCH+": (s) => paint(c.boldCyan, s),
94
98
  ARCH: (s) => paint(c.cyan, s),
95
99
  POWER: (s) => paint(c.boldWhite, s),
@@ -98,8 +102,16 @@ const CLASS_COLOR = {
98
102
  REFINER: (s) => paint(c.blue, s),
99
103
  BEARER: (s) => paint(c.dim, s),
100
104
  IGNITER: (s) => paint(c.dim, s),
105
+ [UNCLASSED]: (s) => paint(c.dim, s),
101
106
  };
102
- const colorClass = (cls) => (CLASS_COLOR[cls] ?? ((s) => s))(cls);
107
+ const colorClass = (cls) => (CLASS_COLOR[tierOf(cls)] ?? CLASS_COLOR[UNCLASSED] ?? ((s) => s))(cls);
108
+ // Guard: every canonical tier must have a color here.
109
+ for (const t of CLASS_TIERS) {
110
+ if (!CLASS_COLOR[t]) {
111
+ // eslint-disable-next-line no-console
112
+ console.warn(`[cli] CLASS_COLOR map missing color for tier "${t}"`);
113
+ }
114
+ }
103
115
 
104
116
  // ── Terminal utils ──────────────────────────────────────────────────────────
105
117
  const CLEAR_SCREEN = `${ESC}2J${ESC}H`;
@@ -234,7 +246,7 @@ function renderBoardHeader(window = "30d") {
234
246
 
235
247
  async function fetchBoard(window = "30d") {
236
248
  const res = await fetch(
237
- `${DEFAULT_API_BASE}/api/v1/leaderboard?window=${window}&metric=yield_`,
249
+ `${DEFAULT_API_BASE}/api/v1/leaderboard?window=${window}&metric=${LEADERBOARD_METRIC}`,
238
250
  {
239
251
  headers: {
240
252
  accept: "application/json",
@@ -469,8 +481,10 @@ async function tokscalePillars(platform = "claude") {
469
481
  }
470
482
 
471
483
  function appPillars() {
472
- // App numbers from screenshots — all-time, per model (no cache fields)
473
- // Hard-coded from 2026-06-23 screenshot capture (update when re-screenshotted)
484
+ // ⚠ STALE HARDCODED DATA — App numbers from screenshots, all-time, per model
485
+ // (no cache fields). Captured 2026-06-23. NOT refreshed automatically. The
486
+ // compare view uses this only as a static reference point; it does NOT feed
487
+ // the ranked submit path. Update manually when a fresh screenshot is taken.
474
488
  return {
475
489
  all: {
476
490
  input: 6_378_000, // sum of all models: 5.6M + 102.1K + 92.9K + 130.3K + 418.9K + 33.5K
@@ -544,39 +558,32 @@ function fmtDelta(a, b) {
544
558
  return d > 0 ? green(label) : red(label);
545
559
  }
546
560
 
547
- // Compute cascade metrics from raw pillars (mirrors bridge.ts computeCascadeMetrics)
561
+ // Compute cascade metrics from raw pillars. Fix 3: this was a third copy of
562
+ // the cascade math with its own divide-by-zero policy (safeI = max(i,1) clamp),
563
+ // which let the CLI's Υ drift from the MCP tools' Υ for the same tokens. It now
564
+ // forwards to the single shared cascade() from analytics/cascade.mjs (null-guard
565
+ // policy, identical to rank_paste / submit_verified) and only adds the
566
+ // CLI-display-only `efficiency` + `total` fields the compare view renders.
548
567
  function cascadeFromPillars(p) {
549
568
  if (!p) return null;
569
+ const c = cascade(p);
550
570
  const i = p.input ?? 0;
551
571
  const o = p.output ?? 0;
552
572
  const cw = p.cacheCreate ?? 0;
553
573
  const cr = p.cacheRead ?? 0;
554
- if (i === 0 && o === 0) return null;
555
- const safeI = Math.max(i, 1);
556
574
  const total = i + o + cw + cr;
557
- const velocity = o / safeI;
558
- const leverage = cr / safeI;
559
- const yield_ = leverage * velocity;
560
- const snr = i + o > 0 ? o / (i + o) : 0;
561
- // dev10x = log10(T × C × R) — only when all four pillars present
562
- let dev10x = null;
563
- if (cw > 0 && o > 0 && i > 0 && cr > 0) {
564
- const T = o / i,
565
- C = cw / o,
566
- R = cr / cw;
567
- dev10x = Math.log10(T * C * R);
568
- }
569
- // efficiency = ((cr+cw+o)/i) / 4.0
570
- const efficiency = (cr + cw + o) / safeI / 4.0;
571
- const cls = classify(yield_, dev10x);
575
+ // efficiency = ((cr+cw+o)/i)/4.0 — display-only derived metric (not in the
576
+ // canonical cascade). Null when input is 0 (matches the old safeI→0 behavior
577
+ // would have been div-by-zero; the shared cascade already nulls yield there).
578
+ const efficiency = i > 0 ? (cr + cw + o) / i / 4.0 : null;
572
579
  return {
573
- yield: yield_,
574
- velocity,
575
- leverage,
576
- snr,
577
- dev10x,
580
+ yield: c.yield,
581
+ velocity: c.velocity,
582
+ leverage: c.leverage,
583
+ snr: c.snr,
584
+ dev10x: c.dev10x,
578
585
  efficiency,
579
- class: cls,
586
+ class: c.class,
580
587
  total,
581
588
  };
582
589
  }
@@ -868,11 +875,18 @@ async function runWatch({
868
875
  id,
869
876
  { platform: plat },
870
877
  );
878
+ // Pass 6: gate the ✓ on r.ranked (verified + persisted), NOT on
879
+ // r.status === "received". An unenrolled/revoked device gets HTTP
880
+ // 202 "received" but is NEVER ranked — gating on "received" would
881
+ // show a green ✓ to an operator who didn't actually make the board.
882
+ // Mirrors the isRankedAck() predicate used by every submit path.
871
883
  lastSubmitMsg.set(
872
884
  key,
873
- r.status === "received"
874
- ? green(`✓ tier=${r.verification_tier || "—"}`)
875
- : red(`✗ ${r.reason || r.status}`),
885
+ r.ranked
886
+ ? green(`✓ tier=${r.verification_tier || "—"} (ranked)`)
887
+ : red(
888
+ `✗ ${r.reason || r.status || "not ranked"}${r.verification_tier ? ` (tier=${r.verification_tier})` : ""}`,
889
+ ),
876
890
  );
877
891
  } catch (e) {
878
892
  lastSubmitMsg.set(key, red(`✗ ${e.message}`));
@@ -934,25 +948,9 @@ async function runWatch({
934
948
  // - live board position
935
949
  // - [S] submit [B] board [Q] quit
936
950
 
937
- const ALL_PLATFORMS = [
938
- "claude",
939
- "codex",
940
- "devin",
941
- "amp",
942
- "gemini",
943
- "kimi",
944
- "qwen",
945
- "goose",
946
- "kilo",
947
- "hermes",
948
- "droid",
949
- "codebuff",
950
- "copilot",
951
- "opencode",
952
- "openclaw",
953
- "pi",
954
- "other",
955
- ];
951
+ // ALL_PLATFORMS is imported from ../adapters.mjs (single source of truth —
952
+ // was redefined inline here, which drifted from the adapter registry whenever
953
+ // a platform was added there but not in this list).
956
954
 
957
955
  async function runSigRank() {
958
956
  write(HIDE_CURSOR);
@@ -14,6 +14,7 @@
14
14
 
15
15
  import { callTool, DEFAULT_API_BASE, pullActivePlatforms } from "../tools.mjs";
16
16
  import { ALL_PLATFORMS } from "../adapters.mjs";
17
+ import { cascade, CLASS_TIERS, UNCLASSED, tierOf } from "../cascade.mjs";
17
18
  import { freshVerifierPillars } from "../tokenpull.mjs";
18
19
  import { isSignedIn, isCodeChar } from "../connect.mjs";
19
20
  import { loadIdentity, clearIdentity } from "../keystore.mjs";
@@ -21,17 +22,12 @@ import { execFile } from "child_process";
21
22
  import { existsSync, readFileSync } from "fs";
22
23
  import os from "os";
23
24
  import path from "path";
24
- import { createRequire } from "module";
25
+ import { pkgVersion } from "../lib/pkg-version.mjs";
26
+ import { LEADERBOARD_METRIC } from "../lib/constants.mjs";
25
27
 
26
- // Version read from package.json (single source of truth — matches cli.mjs;
27
- // never hardcode, that's what caused the version drift).
28
- const VERSION = (() => {
29
- try {
30
- return createRequire(import.meta.url)("./package.json").version;
31
- } catch {
32
- return "?";
33
- }
34
- })();
28
+ // Version read from the shared pkg-version helper (single source of truth —
29
+ // matches cli.mjs; never hardcode, that's what caused the version drift).
30
+ const VERSION = pkgVersion();
35
31
 
36
32
  // ── ANSI ───────────────────────────────────────────────────────────────────
37
33
  const ESC = "\x1b[";
@@ -205,8 +201,10 @@ const fmtMov = (n) =>
205
201
  n == null || n === 0 ? dim("—") : n > 0 ? green(`+${n}`) : red(`${n}`);
206
202
 
207
203
  // ── Class tier colors ────────────────────────────────────────────────────────
204
+ // Color is keyed on the BASE TIER (8 entries in CLASS_TIERS). classify() returns
205
+ // a full sub-stage string (e.g. "REFINER II") — tierOf() extracts the base tier
206
+ // for color lookup. TRANSMITTER is not a class — it's a temporary peak badge.
208
207
  const CLS = {
209
- TRANSMITTER: (s) => paint(c.boldGold, s),
210
208
  "ARCH+": (s) => paint(c.boldCyan, s),
211
209
  ARCH: (s) => paint(c.cyan, s),
212
210
  POWER: (s) => paint(c.boldWhite, s),
@@ -215,35 +213,29 @@ const CLS = {
215
213
  REFINER: (s) => paint(c.blue, s),
216
214
  BEARER: (s) => paint(c.dim, s),
217
215
  IGNITER: (s) => paint(c.dim, s),
216
+ [UNCLASSED]: (s) => paint(c.dim, s),
218
217
  };
219
- const colorCls = (cls) => (CLS[cls] ?? ((s) => s))(cls);
218
+ const colorCls = (cls) => (CLS[tierOf(cls)] ?? CLS[UNCLASSED] ?? ((s) => s))(cls);
219
+
220
+ // Guard: every canonical tier must have a color, so a new tier added in
221
+ // analytics/cascade.mjs can't silently render uncolored here.
222
+ for (const t of CLASS_TIERS) {
223
+ if (!CLS[t]) {
224
+ // eslint-disable-next-line no-console
225
+ console.warn(`[tui] CLS map missing color for tier "${t}"`);
226
+ }
227
+ }
220
228
 
221
- // ── Cascade math (inline, no dep) ───────────────────────────────────────────
229
+ // ── Cascade math ───────────────────────────────────────────────────────────
230
+ // Fix 3: the inline cascadeFrom() (a third copy of the cascade math with its
231
+ // own null-guard policy) is replaced by the single shared cascade() from
232
+ // analytics/cascade.mjs. CLI, TUI, and MCP tools now report identical Υ for
233
+ // identical tokens. cascadeFrom() keeps the old name as a thin alias so the
234
+ // many call sites below don't need rewriting; it forwards to the shared
235
+ // implementation and returns null only when there are no pillars at all.
222
236
  function cascadeFrom(p) {
223
237
  if (!p) return null;
224
- const i = p.input ?? 0,
225
- o = p.output ?? 0,
226
- cr = p.cacheRead ?? 0;
227
- if (i === 0 || o === 0) return null;
228
- const leverage = cr / i;
229
- const velocity = o / i;
230
- const yld = leverage * velocity;
231
- const dev10x =
232
- i > 0 && o > 0 && (p.cacheCreate ?? 0) > 0 && cr > 0
233
- ? Math.log10(cr / i)
234
- : null;
235
- const snr = o / (i + o);
236
-
237
- let cls = "IGNITER";
238
- if (yld >= 1000 || dev10x >= 3) cls = "TRANSMITTER";
239
- else if (dev10x != null && dev10x >= 1.45) cls = "ARCH+";
240
- else if (dev10x != null && dev10x >= 1.35) cls = "ARCH";
241
- else if (dev10x != null && dev10x >= 1.2) cls = "POWER";
242
- else if (dev10x != null && dev10x >= 1.0) cls = "BASE";
243
- else if (dev10x != null && dev10x >= 0) cls = "SEEKER";
244
- else if (dev10x != null && dev10x >= -0.3) cls = "REFINER";
245
-
246
- return { yield: yld, snr, leverage, velocity, dev10x, class: cls };
238
+ return cascade(p);
247
239
  }
248
240
 
249
241
  // ── Unicode bar chart (no dep) ───────────────────────────────────────────────
@@ -309,7 +301,7 @@ function sparkline(values) {
309
301
 
310
302
  async function loadDashboardData() {
311
303
  const boardPromise = Promise.race([
312
- fetch(`${DEFAULT_API_BASE}/api/v1/leaderboard?window=30d&metric=yield_`, {
304
+ fetch(`${DEFAULT_API_BASE}/api/v1/leaderboard?window=30d&metric=${LEADERBOARD_METRIC}`, {
313
305
  headers: { accept: "application/json" },
314
306
  })
315
307
  .then((r) => (r.ok ? r.json() : null))
@@ -386,7 +378,7 @@ async function loadBoardData(window = "30d") {
386
378
  // Primary: submissions endpoint (ranked submission rows).
387
379
  try {
388
380
  const res = await fetch(
389
- `${DEFAULT_API_BASE}/api/v1/submissions?window=${window}&metric=yield_`,
381
+ `${DEFAULT_API_BASE}/api/v1/submissions?window=${window}&metric=${LEADERBOARD_METRIC}`,
390
382
  {
391
383
  headers: { accept: "application/json" },
392
384
  },
@@ -401,7 +393,7 @@ async function loadBoardData(window = "30d") {
401
393
  // Fallback: existing leaderboard endpoint + current render (pre-deploy safety).
402
394
  try {
403
395
  const res = await fetch(
404
- `${DEFAULT_API_BASE}/api/v1/leaderboard?window=${window}&metric=yield_`,
396
+ `${DEFAULT_API_BASE}/api/v1/leaderboard?window=${window}&metric=${LEADERBOARD_METRIC}`,
405
397
  {
406
398
  headers: { accept: "application/json" },
407
399
  },
@@ -796,7 +788,12 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
796
788
  const wk = firstWin[d.platform];
797
789
  const wd = d.windows?.find((x) => x.window === wk);
798
790
  const cas = wd && cascadeFrom(wd.pillars);
799
- if (cas) return cas; // cascadeFrom already returns null for non-compounding rows
791
+ // cascadeFrom returns null only when wd.pillars is falsy; otherwise it
792
+ // forwards to cascade() which always returns an object (degenerate input
793
+ // surfaces as null fields + warnings[], not a null return). The guard
794
+ // below skips rows with no pillars at all; downstream `chk(val == null)`
795
+ // guards handle degenerate-but-present pillars.
796
+ if (cas) return cas;
800
797
  }
801
798
  return null;
802
799
  })();
@@ -1,23 +1,81 @@
1
1
  # SigRank Class Tiers
2
2
 
3
- Every operator is classified into one of three tiers based on their Yield (Υ = Cache Reads × Output / Input²).
4
-
5
- ## Burner (Υ < 1.0)
6
- - **Profile:** Raw volume operators. High input tokens, low cache reuse.
7
- - **Behavior:** Brute-force — lots of context fed in, relatively little output back.
8
- - **Typical:** New AI users, verbose prompters, no session continuity.
9
- - **Fix:** Build cache across sessions. Stop re-explaining context. Use --continue.
10
-
11
- ## Builder (1.0 ≤ Υ < 10.0)
12
- - **Profile:** Balanced operators. Moderate cache, decent output efficiency.
13
- - **Behavior:** Productive — reasonable input-to-output ratio, some cache leverage.
14
- - **Typical:** Experienced AI coders who use CLAUDE.md, project context, and session continuity.
15
- - **Fix:** Increase cache reads by reusing sessions. Reduce input by trimming unnecessary context.
16
-
17
- ## 10xer (Υ ≥ 10.0)
18
- - **Profile:** Cascade-optimized operators. High cache reads, minimal input, efficient output.
19
- - **Behavior:** Surgical — minimal new input, maximum cache reuse, high-yield output.
20
- - **Typical:** Power users with long-running sessions, tight context windows, and aggressive cache strategies.
21
- - **Maintain:** Keep cache hit rate high. Avoid context bloat. Monitor with watch_tokenpull.
22
-
23
- Tiers are recalculated on every submission. Your tier can change between windows (7d, 30d, 90d, all-time).
3
+ Every operator is classified into one of **24 experience stages** — 8 tiers ×
4
+ 3 sub-stages (I/II/III) — based on their **total tokens** (input + output +
5
+ cacheCreate + cacheRead). The ladder mirrors the server's
6
+ `RS05_CLASS_THRESHOLDS` (`lib/analytics/ruleset.ts` in sigrank-app) — the
7
+ client's `classify()` in `analytics/cascade.mjs` is the canonical local source
8
+ of truth. The TUI, CLI, MCP output schemas, and this doc all read from that
9
+ one list.
10
+
11
+ The class is an **experience** axis — it measures how much volume the operator
12
+ has accumulated. It is separate from the **cascade archetype** (the operator's
13
+ token-flow shape: kinetic loop, archival sponge, volatile ingestor, etc.) and
14
+ from the **TRANSMITTER peak badge** (a temporary state — see below).
15
+
16
+ A 25th value, **UNCLASSED**, is returned when total tokens are null or
17
+ non-finite (empty / all-zero session). It is distinct from IGNITER III so
18
+ callers can tell "no data" from "bottom tier".
19
+
20
+ ## The 8 base tiers (K.01–K.08)
21
+
22
+ Each tier has 3 sub-stages (I/II/III) split by total-token thresholds. The
23
+ sub-stage is the granular experience level; the base tier is the display
24
+ grouping (color, glyph, meaning).
25
+
26
+ ### ARCH+ (K.01) — total ≥ 7.07T
27
+ - **Sub-stages:** ARCH+ I (≥ 7.07T), ARCH+ II (≥ 7.07T), ARCH+ III (≥ 7.07T)
28
+ - **Profile:** Deepest field experience. Volume that became architecture.
29
+ - **Note:** Aspirational — currently 1 operator in the static board.
30
+
31
+ ### ARCH (K.02) — total ≥ 68.8B
32
+ - **Sub-stages:** ARCH I (≥ 186B), ARCH II (≥ 98.5B), ARCH III (≥ 68.8B)
33
+ - **Profile:** System builders. Sustained volume, coherent output.
34
+
35
+ ### POWER (K.03) — total ≥ 19.1B
36
+ - **Sub-stages:** POWER I (≥ 40.0B), POWER II (≥ 27.0B), POWER III (≥ 19.1B)
37
+ - **Profile:** Above the center. Volume compounding.
38
+
39
+ ### BASE (K.04) — total ≥ 7.7B
40
+ - **Sub-stages:** BASE I (≥ 14.0B), BASE II (≥ 10.2B), BASE III (≥ 7.7B)
41
+ - **Profile:** The center of the field. The average operator's experience.
42
+
43
+ ### SEEKER (K.05) — total ≥ 3.0B
44
+ - **Sub-stages:** SEEKER I (≥ 5.4B), SEEKER II (≥ 4.0B), SEEKER III (≥ 3.0B)
45
+ - **Profile:** Approaching the center. Experience accumulating.
46
+
47
+ ### REFINER (K.06) — total ≥ 1.3B
48
+ - **Sub-stages:** REFINER I (≥ 2.4B), REFINER II (≥ 1.8B), REFINER III (≥ 1.3B)
49
+ - **Profile:** Practicing with purpose. Early sustained volume.
50
+
51
+ ### BEARER (K.07) — total ≥ 432M
52
+ - **Sub-stages:** BEARER I (≥ 984M), BEARER II (≥ 715M), BEARER III (≥ 432M)
53
+ - **Profile:** Quiet accumulation. The first real volume.
54
+
55
+ ### IGNITER (K.08) — total ≥ 0
56
+ - **Sub-stages:** IGNITER I (≥ 216M), IGNITER II (≥ 89.0M), IGNITER III (≥ 0)
57
+ - **Profile:** Dormant potential. The still soul. Waiting.
58
+
59
+ ## UNCLASSED (no data)
60
+ - **Profile:** Empty session — all four pillars are zero.
61
+ - **Fix:** Run some sessions; the stage will resolve once there is data.
62
+
63
+ ## TRANSMITTER (peak badge, not a class)
64
+
65
+ TRANSMITTER is **not** on the experience ladder. It is a temporary peak badge
66
+ (RS.08) that any experience tier can earn during a high-frequency,
67
+ high-resonance window. An operator "transmits" when they hit both:
68
+
69
+ - **High frequency:** token throughput (total tokens in window) ≥ 1B
70
+ - **High resonance:** SIGNA RATE (composite signal quality) ≥ 85
71
+
72
+ The badge is per-window (daily/weekly) and lapses when frequency or resonance
73
+ drops. The server evaluates it via `evaluateTransmitterBadge()` in
74
+ `lib/analytics/transmitter-badge.ts`; the client does not evaluate the badge
75
+ locally (the owner is still calibrating the thresholds). The server is the
76
+ authority.
77
+
78
+ ---
79
+
80
+ Tiers are recalculated on every submission. Your stage can change between
81
+ windows (7d, 30d, 90d, all-time) as total tokens accumulate.
@@ -2,20 +2,23 @@
2
2
 
3
3
  ## What leaves your machine
4
4
 
5
- **Only four numbers per submission:**
5
+ **Every submission path sends only four numbers:**
6
6
  - Input tokens
7
7
  - Output tokens
8
8
  - Cache create tokens
9
9
  - Cache read tokens
10
10
 
11
- **That's it.** No prompts, no code, no file contents, no conversation text.
11
+ That's the entire payload — no prompts, no code, no file contents, no conversation text. The MCP parses your input locally and sends only the four token counts to the server, which re-parses and re-scores them authoritatively.
12
+
13
+ - **Signed path** (`submit_verified`, `watch_tokenpull` with `submit:true`): the four numbers travel with the device's public key, codename, window, and an ed25519 signature. The board verifies the signature without seeing your data.
14
+ - **Paste path** (`submit_paste`, `tokenpull_submit`): the MCP parses your pasted token counts locally, then sends only the four canonical numbers to the server's web-paste endpoint. Even if you paste mixed text (prose + numbers), only the extracted token counts are transmitted — the raw text never leaves your machine.
12
15
 
13
16
  ## How it works
14
17
 
15
18
  1. **Local-first:** All token pulling happens on your machine. SigRank reads session logs from ~/.claude, ~/.codex, ~/.local/share/amp, etc.
16
- 2. **Token counts only:** The MCP tools extract integer counts from log metadata. The actual content of your conversations is never read, parsed, or transmitted.
17
- 3. **Signed submission:** Submissions are ed25519-signed. The board verifies authenticity without seeing your data.
18
- 4. **No auth required:** No API keys, no OAuth, no account needed to read the leaderboard. Enrollment only requires a codename.
19
+ 2. **Token counts only:** The MCP tools extract integer counts from log metadata. The actual content of your conversations is never read, parsed, or transmitted. On the paste path, the MCP parses the paste locally and sends only the four extracted numbers — the raw paste text stays on your machine.
20
+ 3. **Signed submission:** Ranked submissions (`submit_verified`, `watch_tokenpull` with `submit:true`) are ed25519-signed with a device-bound key generated locally at enrollment. The board verifies the signature without seeing your data. Paste submissions (`submit_paste`, `tokenpull_submit`) are unsigned and go through the web-paste endpoint with a codename only — but still send only the four token counts, not the raw paste.
21
+ 4. **Read tools need no auth:** No API keys, no OAuth, no account needed to read the leaderboard or operator profiles. **Enrollment requires a connect code** from signalaf.com → Settings → New key (the code binds your device's public key to your operator server-side); a codename alone is not enough for the signed/ranked path.
19
22
 
20
23
  ## What SigRank can NOT see
21
24
 
package/submit/index.mjs CHANGED
@@ -11,22 +11,15 @@
11
11
 
12
12
  import { snapshotHash, signPayload } from "../identity/sign.mjs";
13
13
  import { preflight } from "./preflight.mjs";
14
- import { readFileSync } from "node:fs";
14
+ import { pkgVersion } from "../lib/pkg-version.mjs";
15
+ import {
16
+ TERMS_VERSION,
17
+ PRIVACY_VERSION,
18
+ isRankedAck,
19
+ } from "../lib/constants.mjs";
15
20
 
16
21
  const WINDOW_TYPE = { "7d": "7d", "30d": "30d", "90d": "90d", all: "all_time" };
17
22
  const WINDOW_SPAN_DAYS = { "7d": 7, "30d": 30, "90d": 90, all_time: 3650 };
18
-
19
- /** Resolve this package's version for the User-Agent stamp (best-effort). */
20
- function _pkgVersion() {
21
- try {
22
- const pkg = JSON.parse(
23
- readFileSync(new URL("./package.json", import.meta.url), "utf-8"),
24
- );
25
- return pkg.version;
26
- } catch {
27
- return "unknown";
28
- }
29
- }
30
23
  const PLATFORM_ENUM = new Set([
31
24
  "claude",
32
25
  "chatgpt",
@@ -38,8 +31,6 @@ const PLATFORM_ENUM = new Set([
38
31
  ]);
39
32
  // Provenance tag only — the board ranks by Υ (no RS.xx weights, §0.1).
40
33
  const RULESET_VERSION = "sigrank-token-1";
41
- const TERMS_VERSION = "2026-07-21";
42
- const PRIVACY_VERSION = "2026-07-21";
43
34
  const DAY_MS = 86_400_000;
44
35
 
45
36
  const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
@@ -200,7 +191,7 @@ export async function submitSignedWindow(
200
191
  const ctrl = new AbortController();
201
192
  const timer = setTimeout(() => ctrl.abort(), 15_000);
202
193
  const headers = {
203
- "user-agent": `node/${process.version} sigrank-mcp/${_pkgVersion()}`,
194
+ "user-agent": `node/${process.version} sigrank-mcp/${pkgVersion()}`,
204
195
  ...(init.headers || {}),
205
196
  };
206
197
  return fetch(url, {
@@ -296,11 +287,7 @@ export async function submitSignedWindow(
296
287
  persisted: ack.persisted ?? null,
297
288
  // ranked = actually on the board (verified + written), not just "received".
298
289
  // An unenrolled/revoked device gets HTTP 202 received but is NEVER ranked.
299
- ranked: !!(
300
- res.ok &&
301
- ack.verification_tier === "verified" &&
302
- ack.persisted === true
303
- ),
290
+ ranked: isRankedAck(res, ack),
304
291
  snapshot_hash: payload.agent.snapshot_hash,
305
292
  reason: res.ok ? null : ack.reason || ack.status || `http_${res.status}`,
306
293
  detail: ack.detail ?? null,
@@ -33,6 +33,7 @@ import { existsSync, readFileSync } from "node:fs";
33
33
  import os from "node:os";
34
34
  import path from "node:path";
35
35
  import { fileURLToPath } from "node:url";
36
+ import { TOKSCALE_CLIENT_MAP } from "./lib/constants.mjs";
36
37
 
37
38
  // Resolve local node_modules/.bin for the bundled tokscale binary.
38
39
  const _pkgRoot = path.dirname(fileURLToPath(import.meta.url));
@@ -85,23 +86,11 @@ export function num(x) {
85
86
  return Number.isFinite(n) ? n : 0;
86
87
  }
87
88
 
88
- // Map tokscale's client slugs to canonical platform names (mirrors tools.mjs).
89
+ // Map tokscale's client slugs to canonical platform names.
90
+ // TOKSCALE_CLIENT_MAP is imported from lib/constants.mjs (single source of
91
+ // truth — was duplicated here and in tools/index.mjs +
92
+ // tools/tokscale-breakdown.mjs, and the copies had started to drift).
89
93
  // `synthetic` → null (filtered). unmapped slugs → "other".
90
- const TOKSCALE_CLIENT_MAP = {
91
- claude: "claude", codex: "codex", "devin-cli": "devin",
92
- "devin-desktop": "devin", gemini: "gemini", amp: "amp",
93
- kimi: "kimi", qwen: "qwen", goose: "goose", kilo: "kilo",
94
- kilocode: "kilo", hermes: "hermes", droid: "droid",
95
- codebuff: "codebuff", copilot: "copilot", opencode: "opencode",
96
- openclaw: "openclaw", pi: "pi",
97
- cursor: "other", roocode: "other", mux: "other", crush: "other",
98
- antigravity: "other", "antigravity-cli": "other", zed: "other",
99
- kiro: "other", trae: "other", warp: "other", cline: "other",
100
- gjc: "other", grok: "other", jcode: "other", commandcode: "other",
101
- micode: "other", junie: "other", zcode: "other",
102
- opencodereview: "other", codebuddy: "other", workbuddy: "other",
103
- synthetic: null,
104
- };
105
94
 
106
95
  /** Human-readable label for a tokscale client slug. */
107
96
  const CLIENT_LABELS = {