sigrank 0.0.180 → 0.0.182

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.
@@ -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 } 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,6 +90,9 @@ const green = (str) => paint(c.green, str);
88
90
  const red = (str) => paint(c.red, str);
89
91
 
90
92
  // ── Class tier → color ─────────────────────────────────────────────────────
93
+ // Built from the canonical CLASS_TIERS list (single source of truth). The dead
94
+ // BEARER entry (no tier by that name exists) was removed; UNCLASSED added for
95
+ // the degenerate no-data case.
91
96
  const CLASS_COLOR = {
92
97
  TRANSMITTER: (s) => paint(c.boldGold, s),
93
98
  "ARCH+": (s) => paint(c.boldCyan, s),
@@ -96,10 +101,17 @@ const CLASS_COLOR = {
96
101
  BASE: (s) => paint(c.white, s),
97
102
  SEEKER: (s) => paint(c.magenta, s),
98
103
  REFINER: (s) => paint(c.blue, s),
99
- BEARER: (s) => paint(c.dim, s),
100
104
  IGNITER: (s) => paint(c.dim, s),
105
+ [UNCLASSED]: (s) => paint(c.dim, s),
101
106
  };
102
107
  const colorClass = (cls) => (CLASS_COLOR[cls] ?? ((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 } 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,6 +201,9 @@ const fmtMov = (n) =>
205
201
  n == null || n === 0 ? dim("—") : n > 0 ? green(`+${n}`) : red(`${n}`);
206
202
 
207
203
  // ── Class tier colors ────────────────────────────────────────────────────────
204
+ // Built from the canonical CLASS_TIERS list (single source of truth) so a new
205
+ // tier added in analytics/cascade.mjs can't silently lack a color here. The
206
+ // dead BEARER entry (no tier by that name exists) was removed.
208
207
  const CLS = {
209
208
  TRANSMITTER: (s) => paint(c.boldGold, s),
210
209
  "ARCH+": (s) => paint(c.boldCyan, s),
@@ -213,37 +212,30 @@ const CLS = {
213
212
  BASE: (s) => paint(c.white, s),
214
213
  SEEKER: (s) => paint(c.magenta, s),
215
214
  REFINER: (s) => paint(c.blue, s),
216
- BEARER: (s) => paint(c.dim, s),
217
215
  IGNITER: (s) => paint(c.dim, s),
216
+ [UNCLASSED]: (s) => paint(c.dim, s),
218
217
  };
219
218
  const colorCls = (cls) => (CLS[cls] ?? ((s) => s))(cls);
220
219
 
221
- // ── Cascade math (inline, no dep) ───────────────────────────────────────────
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
+ }
228
+
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,62 @@
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²).
3
+ Every operator is classified into one of eight tiers (the **dev10x** taxonomy)
4
+ based on their Yield (Υ = Cache Reads × Output / Input²) and 10xDEV score
5
+ (`log10(T × C × R)` where T = output/input, C = cacheCreate/output,
6
+ R = cacheRead/cacheCreate). The classifier is `classify()` in
7
+ `analytics/cascade.mjs` — the canonical source of truth. The TUI, CLI, MCP
8
+ output schemas, and this doc all read from that one list.
4
9
 
5
- ## Burner (Υ < 1.0)
10
+ A ninth value, **UNCLASSED**, is returned when both Υ and 10xDEV are null
11
+ (empty / all-zero session). It is distinct from IGNITER so callers can tell
12
+ "no data" from "bottom tier".
13
+
14
+ ## TRANSMITTER (Υ ≥ 1000 or 10xDEV ≥ 3)
15
+ - **Profile:** Cascade-optimized operators. High cache reads, minimal input, efficient output.
16
+ - **Behavior:** Surgical — minimal new input, maximum cache reuse, high-yield output.
17
+ - **Typical:** Power users with long-running sessions, tight context windows, and aggressive cache strategies.
18
+ - **Maintain:** Keep cache hit rate high. Avoid context bloat. Monitor with watch_tokenpull.
19
+
20
+ ## ARCH+ (10xDEV ≥ 1.45)
21
+ - **Profile:** High-leverage architect. Strong compounding loop.
22
+ - **Behavior:** Builds forward aggressively while reusing prior context.
23
+ - **Fix:** Push cache reads higher to cross into TRANSMITTER.
24
+
25
+ ## ARCH (1.35 ≤ 10xDEV < 1.45)
26
+ - **Profile:** Established architect. Solid compounding, room to tighten.
27
+ - **Behavior:** Reliable reuse + generation balance.
28
+ - **Fix:** Increase cacheCreate quality (better summaries, tighter prompts) to reach ARCH+.
29
+
30
+ ## POWER (1.2 ≤ 10xDEV < 1.35)
31
+ - **Profile:** Productive operator with a forming compounding loop.
32
+ - **Behavior:** Moderate cache leverage, decent output efficiency.
33
+ - **Fix:** Reuse sessions more aggressively to lift cache reads.
34
+
35
+ ## BASE (1.0 ≤ 10xDEV < 1.2)
36
+ - **Profile:** Balanced operator. Some cache, decent output efficiency.
37
+ - **Behavior:** Productive — reasonable input-to-output ratio, some cache leverage.
38
+ - **Typical:** Experienced AI coders who use CLAUDE.md, project context, and session continuity.
39
+ - **Fix:** Increase cache reads by reusing sessions. Reduce input by trimming unnecessary context.
40
+
41
+ ## SEEKER (0 ≤ 10xDEV < 1.0)
42
+ - **Profile:** Learning operator. Compounding loop not yet formed.
43
+ - **Behavior:** Exploring — input-heavy, building toward reuse.
44
+ - **Fix:** Commit context to cache (longer sessions, --continue) to start compounding.
45
+
46
+ ## REFINER (-0.3 ≤ 10xDEV < 0)
6
47
  - **Profile:** Raw volume operators. High input tokens, low cache reuse.
7
48
  - **Behavior:** Brute-force — lots of context fed in, relatively little output back.
8
49
  - **Typical:** New AI users, verbose prompters, no session continuity.
9
50
  - **Fix:** Build cache across sessions. Stop re-explaining context. Use --continue.
10
51
 
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.
52
+ ## IGNITER (10xDEV < -0.3)
53
+ - **Profile:** Sub-baseline. Negative compounding — context is being lost faster than it's built.
54
+ - **Behavior:** Each turn re-ingests more than it reuses; the loop runs in reverse.
55
+ - **Fix:** Stop starting fresh sessions. Commit context once and reuse it.
16
56
 
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.
57
+ ## UNCLASSED (no data)
58
+ - **Profile:** Empty session — all four pillars are zero.
59
+ - **Behavior:** No tokens recorded for this window.
60
+ - **Fix:** Run some sessions; the tier will resolve once there is data.
22
61
 
23
62
  Tiers are recalculated on every submission. Your tier can change between windows (7d, 30d, 90d, all-time).
@@ -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 = {