sigrank 0.15.4 → 0.16.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/README.md +54 -7
- package/adapters.mjs +24 -11
- package/badges.mjs +163 -0
- package/cascade.mjs +69 -0
- package/package.json +4 -2
- package/preflight.mjs +119 -0
- package/submit.mjs +38 -0
- package/tokenpull.mjs +13 -1
- package/tools.mjs +1070 -2
- package/tui.mjs +2 -2
package/README.md
CHANGED
|
@@ -12,10 +12,14 @@ For all builders, burners and 10xers.
|
|
|
12
12
|
|
|
13
13
|
[](https://www.npmjs.com/package/sigrank)
|
|
14
14
|
[](https://github.com/SunrisesIllNeverSee/sigrank-mcp/actions/workflows/ci.yml)
|
|
15
|
+
[](https://github.com/SunrisesIllNeverSee/sigrank-mcp/actions/workflows/codeql.yml)
|
|
16
|
+
[](https://github.com/SunrisesIllNeverSee/sigrank-mcp/actions/workflows/dependency-audit.yml)
|
|
17
|
+
[](https://github.com/SunrisesIllNeverSee/sigrank-mcp/network/dependencies)
|
|
15
18
|
[](./LICENSE)
|
|
16
19
|
[](https://nodejs.org)
|
|
17
20
|
[](https://signalaf.com)
|
|
18
21
|
[](https://glama.ai/mcp/servers/SunrisesIllNeverSee/sigrank-mcp)
|
|
22
|
+
[](https://smithery.ai/servers/burnmydays/sigrank-mcp)
|
|
19
23
|
|
|
20
24
|
</div>
|
|
21
25
|
|
|
@@ -72,13 +76,44 @@ sigrank
|
|
|
72
76
|
**Repo:** [`SunrisesIllNeverSee/sigrank-mcp`](https://github.com/SunrisesIllNeverSee/sigrank-mcp)
|
|
73
77
|
**Site:** [signalaf.com](https://signalaf.com)
|
|
74
78
|
**npm:** [sigrank](https://www.npmjs.com/package/sigrank)
|
|
79
|
+
**Smithery:** [smithery.ai/servers/burnmydays/sigrank-mcp](https://smithery.ai/servers/burnmydays/sigrank-mcp)
|
|
80
|
+
**Glama:** [glama.ai/mcp/servers/SunrisesIllNeverSee/sigrank-mcp](https://glama.ai/mcp/servers/SunrisesIllNeverSee/sigrank-mcp)
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Install via Smithery
|
|
85
|
+
|
|
86
|
+
SigRank is available on [Smithery](https://smithery.ai/servers/burnmydays/sigrank-mcp) as a stdio MCP bundle — one-click install for Claude Desktop, Cursor, and other MCP clients.
|
|
87
|
+
|
|
88
|
+
### Smithery CLI
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Install Smithery CLI
|
|
92
|
+
npm install -g smithery
|
|
93
|
+
|
|
94
|
+
# Connect to SigRank (downloads the MCPB bundle locally)
|
|
95
|
+
smithery mcp add burnmydays/sigrank-mcp --id sigrank
|
|
96
|
+
|
|
97
|
+
# List available tools
|
|
98
|
+
smithery tool list sigrank
|
|
99
|
+
|
|
100
|
+
# Call a tool
|
|
101
|
+
smithery tool call sigrank get_leaderboard '{}'
|
|
102
|
+
smithery tool call sigrank rank_paste '{"text": "1000000 500000 50000 800000"}'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Claude Desktop (via Smithery)
|
|
106
|
+
|
|
107
|
+
1. Go to [smithery.ai/servers/burnmydays/sigrank-mcp](https://smithery.ai/servers/burnmydays/sigrank-mcp)
|
|
108
|
+
2. Click **Install**
|
|
109
|
+
3. Smithery handles the rest — no manual config editing
|
|
75
110
|
|
|
76
111
|
---
|
|
77
112
|
|
|
78
113
|
## Commands
|
|
79
114
|
|
|
80
115
|
```
|
|
81
|
-
⊙ SigRank CLI v0.
|
|
116
|
+
⊙ SigRank CLI v0.15.5
|
|
82
117
|
|
|
83
118
|
Default (no args)
|
|
84
119
|
sigrank unified dashboard: cascade + token pillars + board
|
|
@@ -93,11 +128,12 @@ Commands
|
|
|
93
128
|
compare --platform codex compare for a specific platform
|
|
94
129
|
tui full tabbed TUI: Dashboard / Trends / Compare / Board / Watch / Connect
|
|
95
130
|
tui --platform codex TUI with a different default platform
|
|
96
|
-
watch live tune meter —
|
|
97
|
-
watch --
|
|
131
|
+
watch live tune meter — ALL active platforms × all windows, every 30s
|
|
132
|
+
watch --platform codex watch only one platform (optional filter)
|
|
133
|
+
watch --window 7d watch only one window (optional filter)
|
|
98
134
|
|
|
99
135
|
Options
|
|
100
|
-
--window 7d · 30d · 90d · all (default: 30d for board
|
|
136
|
+
--window 7d · 30d · 90d · all (default: 30d for board; all windows for watch)
|
|
101
137
|
--platform claude · codex · amp · gemini · opencode · goose · …
|
|
102
138
|
--refresh poll interval in seconds (default: 30)
|
|
103
139
|
--once print once and exit (board only)
|
|
@@ -188,6 +224,10 @@ Or if installed globally:
|
|
|
188
224
|
| `watch_tokenpull` | `{platform?, interval_s?}` | One cascade snapshot per call (interval_s advisory) |
|
|
189
225
|
| `submit_verified` | `{window?, platform?, dry_run?}` | THE ranked path: builds + ed25519-signs Schema 1.0 snapshots and POSTs them. `platform:'multi'` sums all active platforms. `dry_run:true` returns the exact payload unsent |
|
|
190
226
|
| `enroll` | `{code, device_label?}` | Bind this device with a connect code from signalaf.com → Settings |
|
|
227
|
+
| `diagnose_cascade` | `{text?}` | Diagnoses where your token cascade is leaking efficiency — ranked findings with severity + estimated Υ impact |
|
|
228
|
+
| `simulate_change` | `{text?, changes}` | Prescriptive "what if" — test proposed pillar changes and see the exact Υ delta + class change before committing |
|
|
229
|
+
| `suggest_improvements` | `{text?}` | Generates ranked, simulated improvement suggestions — tests strategies and returns them sorted by Υ yield impact |
|
|
230
|
+
| `self_improve` | `{text?}` | One-click optimize: diagnoses, suggests, and simulates the best change in a single call |
|
|
191
231
|
|
|
192
232
|
---
|
|
193
233
|
|
|
@@ -277,11 +317,12 @@ All adapters are token-only (no message content, no cost fields, no credentials)
|
|
|
277
317
|
## Dev / test
|
|
278
318
|
|
|
279
319
|
```bash
|
|
280
|
-
node test.mjs #
|
|
320
|
+
node test.mjs # 13 test groups, 200 assertions (no network, no fs writes)
|
|
321
|
+
node sign.test.mjs # ed25519 signing + canon parity
|
|
281
322
|
node index.mjs # stdio MCP server directly (pipe to MCP client)
|
|
282
323
|
```
|
|
283
324
|
|
|
284
|
-
Tests verify (
|
|
325
|
+
Tests verify (13 groups, 200 assertions):
|
|
285
326
|
- `rank_paste` canon: MO§ES `(1251211, 11296121, 128196310, 2555179769)` → Υ 18436.98 · TRANSMITTER
|
|
286
327
|
- `submit_paste` preview (no codename) + POST shape (injected fetch, no live writes)
|
|
287
328
|
- `tokenpull` dedup, window slicing, 4-window pillars (mock adapter)
|
|
@@ -289,8 +330,12 @@ Tests verify (29 assertions):
|
|
|
289
330
|
- `tokenpullCodex` io_ratio conversion per-window
|
|
290
331
|
- Adapter registry (15 platforms) + per-adapter shape contracts
|
|
291
332
|
- `rank_windows` 4-window paste scoring, partial input, no-network
|
|
292
|
-
- `watch_tokenpull` cascade snapshot, interval_s,
|
|
333
|
+
- `watch_tokenpull` cascade snapshot, interval_s, submit path
|
|
334
|
+
- `enroll` posts identity (public key only), maps 201 enrolled + 410 code_invalid
|
|
335
|
+
- `submit_verified` signs Schema 1.0, server-verifiable
|
|
336
|
+
- `simulate_change` relative + absolute deltas, quadratic penalty, JSON input
|
|
293
337
|
- Hardening: div-by-zero guards, parsePillars warnings, fetch timeout, EXCLUDE_TOOLING regex, narrate safety
|
|
338
|
+
- `sign.test.mjs` ed25519 round-trip + canonical 926-byte payload parity
|
|
294
339
|
|
|
295
340
|
---
|
|
296
341
|
|
|
@@ -310,4 +355,6 @@ Tests verify (29 assertions):
|
|
|
310
355
|
| `submit.mjs` | Verified submit flow (signs + POSTs to board) |
|
|
311
356
|
| `sign.mjs` | Schema 1.0 signing (X-Agent-Signature) |
|
|
312
357
|
| `narrate.mjs` | Deterministic prose narration card |
|
|
358
|
+
| `preflight.mjs` | Plausibility checks (Benford, bounds, anomaly detection) |
|
|
313
359
|
| `test.mjs` | Unit tests (no external deps) |
|
|
360
|
+
| `sign.test.mjs` | ed25519 signing + canon parity test |
|
package/adapters.mjs
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
import { readdir, readFile } from 'node:fs/promises'
|
|
36
|
-
import { join,
|
|
36
|
+
import { join, dirname } from 'node:path'
|
|
37
37
|
import { homedir } from 'node:os'
|
|
38
38
|
import { execFile as execFileCb } from 'node:child_process'
|
|
39
39
|
import { promisify } from 'node:util'
|
|
@@ -212,10 +212,6 @@ export const openclawAdapter = {
|
|
|
212
212
|
defaultRoot: () => {
|
|
213
213
|
const env = process.env.OPENCLAW_DIR
|
|
214
214
|
if (env) return env.split(',')[0].trim()
|
|
215
|
-
for (const d of ['.openclaw', '.clawdbot', '.moltbot', '.moldbot']) {
|
|
216
|
-
const p = join(homedir(), d)
|
|
217
|
-
return p // return first; walkFiles silently skips missing
|
|
218
|
-
}
|
|
219
215
|
return join(homedir(), '.openclaw')
|
|
220
216
|
},
|
|
221
217
|
async *messages(root) {
|
|
@@ -245,6 +241,9 @@ export const openclawAdapter = {
|
|
|
245
241
|
// ~/.factory/sessions/**/*.settings.json
|
|
246
242
|
// Fields: input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, thinking_tokens
|
|
247
243
|
// Thinking tokens → output. Per-settings-file (session-level granularity).
|
|
244
|
+
// CUMULATIVE-COLUMN GUARD: settings files contain session-level totals. If the same session
|
|
245
|
+
// appears in multiple files, summing double-counts. We set both id and sid to the session id
|
|
246
|
+
// so tokenpull()'s (sid|id) keep-last dedup collapses duplicates to the latest values.
|
|
248
247
|
export const droidAdapter = {
|
|
249
248
|
platform: 'droid',
|
|
250
249
|
defaultRoot: () => join(homedir(), '.factory', 'sessions'),
|
|
@@ -260,7 +259,8 @@ export const droidAdapter = {
|
|
|
260
259
|
const cacheCreate = Number(s.cache_creation_tokens || 0)
|
|
261
260
|
const cacheRead = Number(s.cache_read_tokens || 0)
|
|
262
261
|
if (input + output + cacheCreate + cacheRead === 0) continue
|
|
263
|
-
|
|
262
|
+
const sessionId = String(s.session_id || s.id || '')
|
|
263
|
+
yield { id: sessionId, sid: sessionId, ts: s.updated_at || s.created_at || null, input, output, cacheCreate, cacheRead, file: path }
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
},
|
|
@@ -345,7 +345,7 @@ export const copilotAdapter = {
|
|
|
345
345
|
defaultRoot: () => join(homedir(), '.copilot', 'otel'),
|
|
346
346
|
async *messages(root) {
|
|
347
347
|
const dir = process.env.COPILOT_OTEL_FILE_EXPORTER_PATH
|
|
348
|
-
?
|
|
348
|
+
? dirname(process.env.COPILOT_OTEL_FILE_EXPORTER_PATH)
|
|
349
349
|
: root
|
|
350
350
|
for await (const path of walkFiles(dir, isJsonl)) {
|
|
351
351
|
const text = await readUtf8(path)
|
|
@@ -387,6 +387,10 @@ export const opencodeAdapter = {
|
|
|
387
387
|
// SQLite: sessions.db at standard Goose data roots or $GOOSE_PATH_ROOT/data/sessions/sessions.db
|
|
388
388
|
// Columns: accumulated_input_tokens (or input_tokens), accumulated_output_tokens (or output_tokens),
|
|
389
389
|
// accumulated_total_tokens (or total_tokens). NO cache fields. Reasoning = total-input-output.
|
|
390
|
+
// CUMULATIVE-COLUMN GUARD: accumulated_* columns are per-session running totals. If the sessions
|
|
391
|
+
// table has multiple rows per session id, summing them downstream double-counts. We set both sid
|
|
392
|
+
// and id to the session id and ORDER BY updated_at so tokenpull()'s (sid|id) keep-last dedup
|
|
393
|
+
// collapses duplicate session rows to the latest (max accumulated) values.
|
|
390
394
|
export const gooseAdapter = {
|
|
391
395
|
platform: 'goose',
|
|
392
396
|
estimated: true, // no cacheCreate or cacheRead
|
|
@@ -405,14 +409,17 @@ export const gooseAdapter = {
|
|
|
405
409
|
join(homedir(), '.local', 'share', 'Block', 'goose', 'sessions', 'sessions.db'),
|
|
406
410
|
]
|
|
407
411
|
for (const db of dbCandidates) {
|
|
408
|
-
|
|
412
|
+
// ORDER BY updated_at so the latest cumulative row per session is yielded last
|
|
413
|
+
// (tokenpull keep-last dedup picks the final row = max accumulated values).
|
|
414
|
+
const rows = await sqliteJson(db, 'SELECT * FROM sessions ORDER BY updated_at')
|
|
409
415
|
for (const row of rows) {
|
|
410
416
|
const input = Number(row.accumulated_input_tokens || row.input_tokens || 0)
|
|
411
417
|
const output = Number(row.accumulated_output_tokens || row.output_tokens || 0)
|
|
412
418
|
const total = Number(row.accumulated_total_tokens || row.total_tokens || 0)
|
|
413
419
|
const reasoning = Math.max(0, total - input - output) // folded into output
|
|
414
420
|
if (input + output === 0) continue
|
|
415
|
-
|
|
421
|
+
const sessionId = String(row.id || row.session_id || '')
|
|
422
|
+
yield { id: sessionId, sid: sessionId, ts: row.created_at || row.updated_at || null, input, output: output + reasoning, cacheCreate: 0, cacheRead: 0, file: db }
|
|
416
423
|
}
|
|
417
424
|
}
|
|
418
425
|
},
|
|
@@ -441,13 +448,18 @@ export const kiloAdapter = {
|
|
|
441
448
|
// ── 13. Hermes Agent ─────────────────────────────────────────────────────────
|
|
442
449
|
// SQLite: ~/.hermes/state.db
|
|
443
450
|
// Per-session rows: input, output, cache_read, cache_write (=cacheCreate), reasoning_tokens → output
|
|
451
|
+
// CUMULATIVE-COLUMN GUARD: session rows may contain cumulative totals. If the same session
|
|
452
|
+
// appears in multiple rows, summing double-counts. We set both id and sid to the session id
|
|
453
|
+
// and ORDER BY updated_at so tokenpull()'s (sid|id) keep-last dedup collapses duplicates
|
|
454
|
+
// to the latest values.
|
|
444
455
|
export const hermesAdapter = {
|
|
445
456
|
platform: 'hermes',
|
|
446
457
|
defaultRoot: () => join(homedir(), '.hermes'),
|
|
447
458
|
async *messages(root) {
|
|
448
459
|
for (const r of roots('HERMES_HOME', root)) {
|
|
449
460
|
const dbPath = join(r, 'state.db')
|
|
450
|
-
|
|
461
|
+
// ORDER BY updated_at so the latest cumulative row per session is yielded last.
|
|
462
|
+
const rows = await sqliteJson(dbPath, 'SELECT * FROM sessions ORDER BY updated_at')
|
|
451
463
|
for (const row of rows) {
|
|
452
464
|
const input = Number(row.input || 0)
|
|
453
465
|
const reasoning = Number(row.reasoning_tokens || 0)
|
|
@@ -455,7 +467,8 @@ export const hermesAdapter = {
|
|
|
455
467
|
const cacheCreate = Number(row.cache_write || row.cache_creation || 0)
|
|
456
468
|
const cacheRead = Number(row.cache_read || 0)
|
|
457
469
|
if (input + output + cacheCreate + cacheRead === 0) continue
|
|
458
|
-
|
|
470
|
+
const sessionId = String(row.id || row.session_id || '')
|
|
471
|
+
yield { id: sessionId, sid: sessionId, ts: row.created_at || row.updated_at || null, input, output, cacheCreate, cacheRead, file: dbPath }
|
|
459
472
|
}
|
|
460
473
|
}
|
|
461
474
|
},
|
package/badges.mjs
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* badges.mjs — pure badge computation for the Cascade Report System.
|
|
3
|
+
*
|
|
4
|
+
* 8 launch badges. Pure functions — take history + current pillars, return
|
|
5
|
+
* badge collection + progress. No deps, no transport, no network.
|
|
6
|
+
*
|
|
7
|
+
* Badge types:
|
|
8
|
+
* Permanent — once earned, always earned (First Spark, Cascade Engine, etc.)
|
|
9
|
+
* Current — must be maintained (Cascade Streak, Top 10)
|
|
10
|
+
*
|
|
11
|
+
* Badge states:
|
|
12
|
+
* earned — condition met, badge is in the collection
|
|
13
|
+
* in_progress — condition not yet met, but progress is trackable
|
|
14
|
+
* locked — condition not met, no progress to show (not returned)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compute all 8 launch badges.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} params
|
|
21
|
+
* @param {object} params.pillars — current { input, output, cacheCreate, cacheRead }
|
|
22
|
+
* @param {object} params.cascade — current cascade result (from cascade())
|
|
23
|
+
* @param {array} params.history — array of daily snapshots { date, mode, yield, pillars }
|
|
24
|
+
* @param {boolean} params.isVerified — whether the operator submitted via signed agent
|
|
25
|
+
* @param {number|null} params.rank — current leaderboard rank (null if not ranked)
|
|
26
|
+
* @returns {object} { earned_this_week, in_progress, collection }
|
|
27
|
+
*/
|
|
28
|
+
export function computeBadges({ pillars, cascade: cas, history = [], isVerified = false, rank = null }) {
|
|
29
|
+
const cr = Number(pillars?.cacheRead || 0)
|
|
30
|
+
const i = Number(pillars?.input || 0)
|
|
31
|
+
const leverage = cas?.leverage ?? (i > 0 ? cr / i : 0)
|
|
32
|
+
|
|
33
|
+
const earned = []
|
|
34
|
+
const inProgress = []
|
|
35
|
+
const collection = []
|
|
36
|
+
|
|
37
|
+
// 1. First Spark — cacheRead > input (1× leverage)
|
|
38
|
+
if (leverage > 1) {
|
|
39
|
+
earned.push({ id: 'first_spark', label: 'First Spark', icon: '◈', condition: '1× cache leverage', date_earned: _latestDate(history) })
|
|
40
|
+
collection.push('first_spark')
|
|
41
|
+
} else if (cr > 0) {
|
|
42
|
+
inProgress.push({ id: 'first_spark', label: 'First Spark', icon: '◈', progress: Math.min(leverage, 1), target: 1, display: `${leverage.toFixed(1)}× / 1×` })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Cascade Engine — cacheRead > 10× input
|
|
46
|
+
if (leverage > 10) {
|
|
47
|
+
earned.push({ id: 'cascade_engine', label: 'Cascade Engine', icon: '⚡', condition: '10× cache leverage', date_earned: _latestDate(history) })
|
|
48
|
+
collection.push('cascade_engine')
|
|
49
|
+
} else if (leverage > 1) {
|
|
50
|
+
inProgress.push({ id: 'cascade_engine', label: 'Cascade Engine', icon: '⚡', progress: Math.min(leverage, 10), target: 10, display: `${leverage.toFixed(1)}× / 10×` })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. Chain Reaction — cacheRead > 100× input
|
|
54
|
+
if (leverage > 100) {
|
|
55
|
+
earned.push({ id: 'chain_reaction', label: 'Chain Reaction', icon: '⚡', condition: '100× cache leverage', date_earned: _latestDate(history) })
|
|
56
|
+
collection.push('chain_reaction')
|
|
57
|
+
} else if (leverage > 10) {
|
|
58
|
+
inProgress.push({ id: 'chain_reaction', label: 'Chain Reaction', icon: '⚡', progress: Math.min(leverage, 100), target: 100, display: `${leverage.toFixed(1)}× / 100×` })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 4. Foundation — 3 completed BUILD→MAINTAIN arcs
|
|
62
|
+
const arcs = _countBuildMaintainArcs(history)
|
|
63
|
+
if (arcs >= 3) {
|
|
64
|
+
earned.push({ id: 'foundation', label: 'Foundation', icon: '🏗️', condition: '3 BUILD→MAINTAIN arcs', date_earned: _latestDate(history) })
|
|
65
|
+
collection.push('foundation')
|
|
66
|
+
} else {
|
|
67
|
+
inProgress.push({ id: 'foundation', label: 'Foundation', icon: '🏗️', progress: arcs, target: 3, display: `${arcs}/3 arcs` })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 5. Phoenix — returned to MAINTAIN within 1 day of disruption, 5×
|
|
71
|
+
const phoenixCount = _countPhoenixReturns(history)
|
|
72
|
+
if (phoenixCount >= 5) {
|
|
73
|
+
earned.push({ id: 'phoenix', label: 'Phoenix', icon: '🔥', condition: '5× MAINTAIN recovery within 1 day', date_earned: _latestDate(history) })
|
|
74
|
+
collection.push('phoenix')
|
|
75
|
+
} else {
|
|
76
|
+
inProgress.push({ id: 'phoenix', label: 'Phoenix', icon: '🔥', progress: phoenixCount, target: 5, display: `${phoenixCount}/5 recoveries` })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 6. Verified — submitted via signed agent
|
|
80
|
+
if (isVerified) {
|
|
81
|
+
earned.push({ id: 'verified', label: 'Verified', icon: '✓', condition: 'Submitted via signed agent', date_earned: _latestDate(history) })
|
|
82
|
+
collection.push('verified')
|
|
83
|
+
}
|
|
84
|
+
// No in-progress for verified — it's binary
|
|
85
|
+
|
|
86
|
+
// 7. Cascade Streak 🔥 — 7 consecutive days MAINTAIN (current badge)
|
|
87
|
+
const streak = _countMaintainStreak(history)
|
|
88
|
+
if (streak >= 7) {
|
|
89
|
+
earned.push({ id: 'cascade_streak', label: 'Cascade Streak', icon: '🔥', condition: '7 consecutive days MAINTAIN', date_earned: _latestDate(history), current: true })
|
|
90
|
+
collection.push('cascade_streak')
|
|
91
|
+
} else {
|
|
92
|
+
inProgress.push({ id: 'cascade_streak', label: 'Cascade Streak', icon: '🔥', progress: streak, target: 7, display: `${streak}/7 days` })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 8. Top 10 🏆 — currently in top 10 (server-side, but we include it if rank is provided)
|
|
96
|
+
if (rank !== null && rank <= 10) {
|
|
97
|
+
earned.push({ id: 'top_10', label: 'Top 10', icon: '🏆', condition: `Currently #${rank}`, date_earned: _latestDate(history), current: true })
|
|
98
|
+
collection.push('top_10')
|
|
99
|
+
}
|
|
100
|
+
// No in-progress for top 10 — it's a live position, not a progress badge
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
earned_this_week: earned.map(b => b.id),
|
|
104
|
+
in_progress: inProgress,
|
|
105
|
+
collection,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
function _latestDate(history) {
|
|
112
|
+
if (!history || history.length === 0) return null
|
|
113
|
+
return history[history.length - 1]?.date || null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Count completed BUILD→MAINTAIN arcs in history.
|
|
118
|
+
* An arc = a BUILD phase followed by a MAINTAIN phase.
|
|
119
|
+
*/
|
|
120
|
+
function _countBuildMaintainArcs(history) {
|
|
121
|
+
if (!history || history.length < 2) return 0
|
|
122
|
+
let arcs = 0
|
|
123
|
+
let wasBuild = false
|
|
124
|
+
for (const h of history) {
|
|
125
|
+
if (h.mode === 'BUILD') {
|
|
126
|
+
wasBuild = true
|
|
127
|
+
} else if (h.mode === 'MAINTAIN' && wasBuild) {
|
|
128
|
+
arcs++
|
|
129
|
+
wasBuild = false
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return arcs
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Count Phoenix returns — MAINTAIN within 1 day of a non-MAINTAIN day.
|
|
137
|
+
*/
|
|
138
|
+
function _countPhoenixReturns(history) {
|
|
139
|
+
if (!history || history.length < 2) return 0
|
|
140
|
+
let count = 0
|
|
141
|
+
for (let idx = 1; idx < history.length; idx++) {
|
|
142
|
+
if (history[idx].mode === 'MAINTAIN' && history[idx - 1].mode !== 'MAINTAIN') {
|
|
143
|
+
count++
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return count
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Count current consecutive MAINTAIN days (ending at the last entry).
|
|
151
|
+
*/
|
|
152
|
+
function _countMaintainStreak(history) {
|
|
153
|
+
if (!history || history.length === 0) return 0
|
|
154
|
+
let streak = 0
|
|
155
|
+
for (let idx = history.length - 1; idx >= 0; idx--) {
|
|
156
|
+
if (history[idx].mode === 'MAINTAIN') {
|
|
157
|
+
streak++
|
|
158
|
+
} else {
|
|
159
|
+
break
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return streak
|
|
163
|
+
}
|
package/cascade.mjs
CHANGED
|
@@ -57,6 +57,7 @@ export function cascade({ input, output, cacheCreate, cacheRead }) {
|
|
|
57
57
|
velocity: round(velocity, 3),
|
|
58
58
|
dev10x: round(dev10x, 2),
|
|
59
59
|
class: classify(yield_, dev10x),
|
|
60
|
+
mode: detectMode({ input: i, output: o, cacheCreate: cw, cacheRead: cr }),
|
|
60
61
|
}
|
|
61
62
|
if (warnings.length > 0) result.warnings = warnings
|
|
62
63
|
return result
|
|
@@ -75,6 +76,74 @@ export function classify(yieldVal, dev10x) {
|
|
|
75
76
|
return 'IGNITER'
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
* detectMode — classify an operator's current working mode from 4 token pillars.
|
|
81
|
+
*
|
|
82
|
+
* Pure ratio math, first-match-wins (same pattern as classify() — descending
|
|
83
|
+
* cuts preserve edge semantics). MAINTAIN checked first (high leverage is
|
|
84
|
+
* strongest signal), then DEBUG (low velocity is distinctive), then EDIT,
|
|
85
|
+
* then BUILD as fallback.
|
|
86
|
+
*
|
|
87
|
+
* Modes:
|
|
88
|
+
* BUILD — high input, low/zero cacheRead, output rising (greenfield)
|
|
89
|
+
* EDIT — high input, low cacheRead, high output (polishing)
|
|
90
|
+
* DEBUG — high input, low output, low cacheRead (investigating)
|
|
91
|
+
* MAINTAIN — low input, high cacheRead, high output (compounding)
|
|
92
|
+
* IDLE — near-zero tokens (not working)
|
|
93
|
+
*
|
|
94
|
+
* Returns { mode, confidence }.
|
|
95
|
+
*/
|
|
96
|
+
export function detectMode({ input, output, cacheCreate, cacheRead }) {
|
|
97
|
+
const i = Number(input), o = Number(output), cw = Number(cacheCreate), cr = Number(cacheRead)
|
|
98
|
+
const total = i + o + cw + cr
|
|
99
|
+
|
|
100
|
+
// IDLE: near-zero tokens
|
|
101
|
+
if (total < 1000) return { mode: 'IDLE', confidence: 1.0 }
|
|
102
|
+
|
|
103
|
+
const leverage = i > 0 ? cr / i : 0
|
|
104
|
+
const velocity = i > 0 ? o / i : 0
|
|
105
|
+
const input_share = total > 0 ? i / total : 0
|
|
106
|
+
|
|
107
|
+
// MAINTAIN: high leverage + high velocity (the cascade is compounding)
|
|
108
|
+
if (leverage > 10 && velocity > 1) return { mode: 'MAINTAIN', confidence: 0.9 }
|
|
109
|
+
if (leverage > 3 && velocity > 0.5) return { mode: 'MAINTAIN', confidence: 0.7 }
|
|
110
|
+
|
|
111
|
+
// DEBUG: low velocity + high input share (reading, not producing)
|
|
112
|
+
if (velocity < 0.3 && input_share > 0.5) return { mode: 'DEBUG', confidence: 0.8 }
|
|
113
|
+
|
|
114
|
+
// EDIT: high input share + high velocity (fresh input but producing)
|
|
115
|
+
if (input_share > 0.4 && velocity > 0.5) return { mode: 'EDIT', confidence: 0.7 }
|
|
116
|
+
|
|
117
|
+
// DEBUG (secondary): high input share + low velocity
|
|
118
|
+
if (input_share > 0.4 && velocity < 0.5) return { mode: 'DEBUG', confidence: 0.6 }
|
|
119
|
+
|
|
120
|
+
// BUILD: fallback (high input, no cache reuse yet)
|
|
121
|
+
return { mode: 'BUILD', confidence: 0.6 }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Expected yield per mode — global defaults used before personal baselines
|
|
126
|
+
* exist (needs 7+ days of history). Used by the quality score computation.
|
|
127
|
+
*/
|
|
128
|
+
export const MODE_EXPECTED_YIELD = {
|
|
129
|
+
BUILD: 15,
|
|
130
|
+
EDIT: 45,
|
|
131
|
+
DEBUG: 10,
|
|
132
|
+
MAINTAIN: 5000,
|
|
133
|
+
IDLE: 0,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* qualityScore — actual yield relative to mode expectation.
|
|
138
|
+
* Fixes the "debug is bad" problem: a DEBUG session at 80% quality is good.
|
|
139
|
+
* Returns a number 0+ (can exceed 1.0 if outperforming the expected yield).
|
|
140
|
+
*/
|
|
141
|
+
export function qualityScore(actualYield, mode) {
|
|
142
|
+
const expected = MODE_EXPECTED_YIELD[mode] ?? 1
|
|
143
|
+
if (expected === 0) return actualYield === 0 ? 1.0 : 0
|
|
144
|
+
return actualYield / expected
|
|
145
|
+
}
|
|
146
|
+
|
|
78
147
|
/**
|
|
79
148
|
* Extract the 4 pillars from pasted text: JSON object OR 4 whitespace numbers.
|
|
80
149
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigrank",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "SigRank MCP server — the yield cascade + live leaderboard as MCP tools any agent can call",
|
|
5
5
|
"mcpName": "io.github.SunrisesIllNeverSee/sigrank-mcp",
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"start": "node index.mjs",
|
|
26
|
-
"test": "node test.mjs && node sign.test.mjs"
|
|
26
|
+
"test": "node test.mjs && node sign.test.mjs && node --test __tests__/cascade.test.mjs __tests__/badges.test.mjs"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"index.mjs",
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"keystore.mjs",
|
|
39
39
|
"submit.mjs",
|
|
40
40
|
"connect.mjs",
|
|
41
|
+
"badges.mjs",
|
|
42
|
+
"preflight.mjs",
|
|
41
43
|
"banner.svg",
|
|
42
44
|
"README.md",
|
|
43
45
|
"LICENSE"
|
package/preflight.mjs
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preflight.mjs — local plausibility pre-checks for the MCP submit path.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors ONLY the server's public plausibility gate (gates.ts:86-137) —
|
|
5
|
+
* totals consistency, turns/sessions ratios, output rate, cache ratio,
|
|
6
|
+
* cadence. These are integrity guards labeled "NOT the proprietary RS.xx"
|
|
7
|
+
* and are safe to replicate in an open agent.
|
|
8
|
+
*
|
|
9
|
+
* The proprietary battery (Benford / cadence / contamination) is SERVER-ONLY
|
|
10
|
+
* and must NOT be shipped in the agent. The server always runs its own
|
|
11
|
+
* battery after the POST — this preflight is a client-side preview of the
|
|
12
|
+
* public plausibility checks only, not a replacement for the full gate chain.
|
|
13
|
+
*
|
|
14
|
+
* If preflight flags, the operator sees WHY their submission would be
|
|
15
|
+
* rejected or flagged, and can fix the issue or submit anyway.
|
|
16
|
+
*
|
|
17
|
+
* Source of truth: lib/ingest/gates.ts (plausibilityGate). This port must
|
|
18
|
+
* stay in sync with the plausibility gate ONLY.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// ── plausibility checks (from gates.ts:86-137) ──────────────────────────────
|
|
22
|
+
|
|
23
|
+
const GATE_LIMITS = {
|
|
24
|
+
TOTALS_TOLERANCE_FRAC: 0.01,
|
|
25
|
+
MAX_OUTPUT_TOKENS_PER_MIN: 20_000,
|
|
26
|
+
// Tightened range-plausibility bounds (deviewreview3)
|
|
27
|
+
MAX_CACHE_REUSE_RATIO: 35,
|
|
28
|
+
MIN_CACHE_WRITE_RATIO: 0.5,
|
|
29
|
+
MIN_INPUT_SHARE_FRAC: 0.0003,
|
|
30
|
+
MAX_CADENCE_PER_MIN: 15,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Run plausibility checks against a payload's raw_telemetry.
|
|
35
|
+
* Returns array of issues: { severity, code, detail }
|
|
36
|
+
*/
|
|
37
|
+
export function plausibilityCheck(rt, window) {
|
|
38
|
+
const out = []
|
|
39
|
+
const pillars = rt.tokens_input_fresh + rt.tokens_output + rt.tokens_cache_read + rt.tokens_cache_creation
|
|
40
|
+
|
|
41
|
+
if (rt.tokens_total > 0) {
|
|
42
|
+
const tol = Math.max(1, rt.tokens_total * GATE_LIMITS.TOTALS_TOLERANCE_FRAC)
|
|
43
|
+
if (Math.abs(pillars - rt.tokens_total) > tol) {
|
|
44
|
+
out.push({ severity: 'reject', code: 'totals_inconsistent', detail: `Σ4 pillars (${pillars}) ≠ tokens_total (${rt.tokens_total})` })
|
|
45
|
+
}
|
|
46
|
+
if (rt.sessions_count === 0) {
|
|
47
|
+
out.push({ severity: 'reject', code: 'tokens_without_sessions', detail: `tokens_total ${rt.tokens_total} with sessions_count 0` })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (rt.turns_total < rt.sessions_count) {
|
|
51
|
+
out.push({ severity: 'reject', code: 'turns_lt_sessions', detail: `turns_total ${rt.turns_total} < sessions_count ${rt.sessions_count}` })
|
|
52
|
+
}
|
|
53
|
+
if (rt.tokens_output > 0 && rt.turns_total === 0) {
|
|
54
|
+
out.push({ severity: 'reject', code: 'output_without_turns', detail: `tokens_output ${rt.tokens_output} with turns_total 0` })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (window) {
|
|
58
|
+
const spanMin = (Date.parse(window.end) - Date.parse(window.start)) / 60_000
|
|
59
|
+
if (Number.isFinite(spanMin) && rt.active_minutes_est > spanMin + 1) {
|
|
60
|
+
out.push({ severity: 'flag', code: 'active_exceeds_window', detail: `active_minutes_est ${rt.active_minutes_est} > window span ${Math.round(spanMin)}m` })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const outPerMin = rt.tokens_output / Math.max(rt.active_minutes_est, 1)
|
|
65
|
+
if (outPerMin > GATE_LIMITS.MAX_OUTPUT_TOKENS_PER_MIN) {
|
|
66
|
+
out.push({ severity: 'flag', code: 'implausible_output_rate', detail: `${Math.round(outPerMin)} output tok/min > ${GATE_LIMITS.MAX_OUTPUT_TOKENS_PER_MIN}` })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Cross-field ratio checks (defense-in-depth — also in the plausibility gate)
|
|
70
|
+
// Bounds tightened (deviewreview3): original 100:1 + 50/min were too loose.
|
|
71
|
+
if (rt.tokens_cache_read > 1_000 && rt.tokens_cache_creation === 0) {
|
|
72
|
+
out.push({ severity: 'flag', code: 'cache_without_creation', detail: `${rt.tokens_cache_read} cache_read with 0 cache_creation (impossible cascade)` })
|
|
73
|
+
}
|
|
74
|
+
if (rt.tokens_cache_creation > 0 && rt.tokens_cache_read / rt.tokens_cache_creation > GATE_LIMITS.MAX_CACHE_REUSE_RATIO) {
|
|
75
|
+
out.push({ severity: 'flag', code: 'extreme_cache_ratio', detail: `cache_read/cache_creation = ${(rt.tokens_cache_read / rt.tokens_cache_creation).toFixed(1)}:1 (real max ~30:1)` })
|
|
76
|
+
}
|
|
77
|
+
if (rt.tokens_output > 1_000 && rt.tokens_cache_creation / rt.tokens_output < GATE_LIMITS.MIN_CACHE_WRITE_RATIO) {
|
|
78
|
+
out.push({ severity: 'flag', code: 'low_cache_write_ratio', detail: `cache_creation/output = ${(rt.tokens_cache_creation / rt.tokens_output).toFixed(2)}:1 (real min ~1.5:1)` })
|
|
79
|
+
}
|
|
80
|
+
if (pillars > 10_000 && rt.tokens_input_fresh / pillars < GATE_LIMITS.MIN_INPUT_SHARE_FRAC) {
|
|
81
|
+
out.push({ severity: 'flag', code: 'implausible_input_share', detail: `input is ${(rt.tokens_input_fresh / pillars * 100).toFixed(3)}% of total (real min ~0.3%)` })
|
|
82
|
+
}
|
|
83
|
+
if (rt.active_minutes_est > 0 && rt.turns_total / rt.active_minutes_est > GATE_LIMITS.MAX_CADENCE_PER_MIN) {
|
|
84
|
+
out.push({ severity: 'flag', code: 'implausible_cadence', detail: `${(rt.turns_total / rt.active_minutes_est).toFixed(1)} turns/min (real: 0.5-10)` })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return out
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── full preflight (plausibility only — NO battery) ─────────────────────────
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Run preflight plausibility checks against a payload. Returns:
|
|
94
|
+
* { pass: true, issues: [] } — clean submission
|
|
95
|
+
* { pass: false, issues: [...], wouldDowngrade } — would be flagged/rejected
|
|
96
|
+
*
|
|
97
|
+
* NOTE: this only mirrors the PUBLIC plausibility gate. The server runs
|
|
98
|
+
* additional proprietary checks (the battery) that this preflight does NOT
|
|
99
|
+
* replicate. A "pass" here means "passes the plausibility gate," not
|
|
100
|
+
* "guaranteed to pass all server gates."
|
|
101
|
+
*/
|
|
102
|
+
export function preflight(payload) {
|
|
103
|
+
const issues = plausibilityCheck(payload.raw_telemetry, payload.window)
|
|
104
|
+
|
|
105
|
+
const hasReject = issues.some((i) => i.severity === 'reject')
|
|
106
|
+
const hasFlag = issues.some((i) => i.severity === 'flag')
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
pass: issues.length === 0,
|
|
110
|
+
wouldReject: hasReject,
|
|
111
|
+
wouldDowngrade: hasFlag,
|
|
112
|
+
issues,
|
|
113
|
+
summary: hasReject
|
|
114
|
+
? `REJECT: ${issues.filter((i) => i.severity === 'reject').map((i) => i.code).join(', ')}`
|
|
115
|
+
: hasFlag
|
|
116
|
+
? `FLAG (verified → flagged, not ranked): ${issues.filter((i) => i.severity === 'flag').map((i) => i.code).join(', ')}`
|
|
117
|
+
: 'clean — passes plausibility gate (server runs additional proprietary checks)',
|
|
118
|
+
}
|
|
119
|
+
}
|