sigrank 0.0.175

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/narrate.mjs ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * narrate.mjs — deterministic prose "card" for a cascade result.
3
+ *
4
+ * Port of _template() from ~/Desktop/moses-sigrank/narrate.py. The model path
5
+ * (MiniCPM4-0.5B) is intentionally SKIPPED: the template is the trustworthy,
6
+ * instant, auditable fallback — same numbers in → same card out, and it can never
7
+ * emit a metric the cascade didn't produce. A model hook can layer behind this same
8
+ * narrate() interface later without a rewrite.
9
+ *
10
+ * Token-only. No network, no randomness.
11
+ */
12
+
13
+ // Safe formatters: never emit NaN/Infinity/undefined into the card.
14
+ const safeNum = (n) => (Number.isFinite(Number(n)) ? Number(n) : null);
15
+ const comma = (n, dec) => {
16
+ const v = safeNum(n);
17
+ return v !== null
18
+ ? v.toLocaleString("en-US", {
19
+ minimumFractionDigits: dec,
20
+ maximumFractionDigits: dec,
21
+ })
22
+ : "—";
23
+ };
24
+ const plain = (n, dec) => {
25
+ const v = safeNum(n);
26
+ return v !== null ? v.toFixed(dec) : "—";
27
+ };
28
+
29
+ /**
30
+ * Given a cascade result ({ velocity, leverage, dev10x, pillars, class }) and an
31
+ * optional subject name, return "**CLASS.** <one or two sentences>". Deterministic.
32
+ */
33
+ export function narrate(cascade, name = "This operator") {
34
+ const klass = cascade.class || cascade.klass || "UNCLASSED";
35
+ const v = safeNum(cascade.velocity);
36
+ const l = safeNum(cascade.leverage);
37
+
38
+ // "non-compounding" = a stateless pipe: no cache commits, so the cascade can't
39
+ // form. cascade.mjs leaves dev10x null when cacheCreate is 0 (the cw/o term
40
+ // collapses), which is exactly metrics.py's non_compounding flag.
41
+ // Also catches zero-input sessions where velocity/leverage are null.
42
+ const cw = cascade.pillars ? Number(cascade.pillars.cacheCreate) : NaN;
43
+ const nonCompounding =
44
+ cascade.dev10x == null || !(cw > 0) || v === null || l === null;
45
+
46
+ let body;
47
+ if (nonCompounding) {
48
+ const leverageStr =
49
+ l !== null
50
+ ? `Leverage ${comma(l, 1)}x comes from reuse alone.`
51
+ : "Leverage is undefined (no fresh input recorded).";
52
+ const dev10xNote =
53
+ cascade.dev10x == null
54
+ ? " 10xDEV is undefined — the compounding loop has not formed yet."
55
+ : "";
56
+ body =
57
+ `${name} runs a stateless pipe — no cache commits, so the cascade can't form. ` +
58
+ `High read volume, but nothing is being built forward. ${leverageStr}${dev10xNote}`;
59
+ } else if (v >= 1 && l >= 100) {
60
+ body =
61
+ `${name} holds both axes at once: ${plain(v, 1)}x generation AND ${comma(l, 0)}x memory leverage. ` +
62
+ `A closed kinetic loop — the rare operator the leverage/generation tradeoff says shouldn't exist.`;
63
+ } else if (l >= 10 && v < 1) {
64
+ body =
65
+ `${name} is an archival sponge — ${comma(l, 0)}x reuse but only ${plain(v, 2)}x generation. ` +
66
+ `Holds context beautifully, executes little with it. The reuse number is inflated by a weak commitment stage.`;
67
+ } else if (v >= 0.8 && l < 2) {
68
+ body =
69
+ `${name} is a volatile ingestor — ${plain(v, 2)}x generation but ${plain(l, 1)}x leverage. ` +
70
+ `Fast on single shots, resets between turns. Memory doesn't persist into a compounding loop.`;
71
+ } else {
72
+ body =
73
+ `${name} sits low on both axes: ${plain(v, 2)}x generation, ${plain(l, 1)}x leverage. ` +
74
+ `A transient profile — neither building state nor converting input to output efficiently.`;
75
+ }
76
+ return `**${klass}.** ${body}`;
77
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "sigrank",
3
+ "version": "0.0.175",
4
+ "author": "SunrisesIllNeverSee (https://github.com/SunrisesIllNeverSee)",
5
+ "description": "SigRank MCP server — the yield cascade + live leaderboard as MCP tools any agent can call",
6
+ "mcpName": "io.github.SunrisesIllNeverSee/sigrank-mcp",
7
+ "type": "module",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/SunrisesIllNeverSee/sigrank-mcp.git"
12
+ },
13
+ "homepage": "https://signalaf.com",
14
+ "bugs": {
15
+ "url": "https://github.com/SunrisesIllNeverSee/sigrank-mcp/issues"
16
+ },
17
+ "bin": {
18
+ "sigrank": "index.mjs",
19
+ "sigrank-mcp": "index.mjs"
20
+ },
21
+ "main": "index.mjs",
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "scripts": {
26
+ "start": "node index.mjs",
27
+ "test": "node test.mjs && node sign.test.mjs && node --test __tests__/cascade.test.mjs __tests__/badges.test.mjs"
28
+ },
29
+ "files": [
30
+ "index.mjs",
31
+ "cli.mjs",
32
+ "tui.mjs",
33
+ "tools.mjs",
34
+ "cascade.mjs",
35
+ "narrate.mjs",
36
+ "tokenpull.mjs",
37
+ "adapters.mjs",
38
+ "sign.mjs",
39
+ "keystore.mjs",
40
+ "submit.mjs",
41
+ "connect.mjs",
42
+ "badges.mjs",
43
+ "preflight.mjs",
44
+ "banner.svg",
45
+ "README.md",
46
+ "LICENSE"
47
+ ],
48
+ "keywords": [
49
+ "mcp",
50
+ "model-context-protocol",
51
+ "ai-agents",
52
+ "claude",
53
+ "anthropic",
54
+ "llm",
55
+ "token-telemetry",
56
+ "token-usage",
57
+ "leaderboard",
58
+ "cli",
59
+ "tui",
60
+ "yield-cascade",
61
+ "sigrank",
62
+ "agent-tools",
63
+ "on-device"
64
+ ],
65
+ "dependencies": {
66
+ "@modelcontextprotocol/sdk": "^1.0.0",
67
+ "@zhangferry-dev/tokendash": "^1.7.5",
68
+ "ccusage": "^20.0.14",
69
+ "tokscale": "^4.0.4"
70
+ }
71
+ }
package/preflight.mjs ADDED
@@ -0,0 +1,191 @@
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 =
40
+ rt.tokens_input_fresh +
41
+ rt.tokens_output +
42
+ rt.tokens_cache_read +
43
+ rt.tokens_cache_creation;
44
+
45
+ if (rt.tokens_total > 0) {
46
+ const tol = Math.max(
47
+ 1,
48
+ rt.tokens_total * GATE_LIMITS.TOTALS_TOLERANCE_FRAC,
49
+ );
50
+ if (Math.abs(pillars - rt.tokens_total) > tol) {
51
+ out.push({
52
+ severity: "reject",
53
+ code: "totals_inconsistent",
54
+ detail: `Σ4 pillars (${pillars}) ≠ tokens_total (${rt.tokens_total})`,
55
+ });
56
+ }
57
+ if (rt.sessions_count === 0) {
58
+ out.push({
59
+ severity: "reject",
60
+ code: "tokens_without_sessions",
61
+ detail: `tokens_total ${rt.tokens_total} with sessions_count 0`,
62
+ });
63
+ }
64
+ }
65
+ if (rt.turns_total < rt.sessions_count) {
66
+ out.push({
67
+ severity: "reject",
68
+ code: "turns_lt_sessions",
69
+ detail: `turns_total ${rt.turns_total} < sessions_count ${rt.sessions_count}`,
70
+ });
71
+ }
72
+ if (rt.tokens_output > 0 && rt.turns_total === 0) {
73
+ out.push({
74
+ severity: "reject",
75
+ code: "output_without_turns",
76
+ detail: `tokens_output ${rt.tokens_output} with turns_total 0`,
77
+ });
78
+ }
79
+
80
+ if (window) {
81
+ const spanMin =
82
+ (Date.parse(window.end) - Date.parse(window.start)) / 60_000;
83
+ if (Number.isFinite(spanMin) && rt.active_minutes_est > spanMin + 1) {
84
+ out.push({
85
+ severity: "flag",
86
+ code: "active_exceeds_window",
87
+ detail: `active_minutes_est ${rt.active_minutes_est} > window span ${Math.round(spanMin)}m`,
88
+ });
89
+ }
90
+ }
91
+
92
+ const outPerMin = rt.tokens_output / Math.max(rt.active_minutes_est, 1);
93
+ if (outPerMin > GATE_LIMITS.MAX_OUTPUT_TOKENS_PER_MIN) {
94
+ out.push({
95
+ severity: "flag",
96
+ code: "implausible_output_rate",
97
+ detail: `${Math.round(outPerMin)} output tok/min > ${GATE_LIMITS.MAX_OUTPUT_TOKENS_PER_MIN}`,
98
+ });
99
+ }
100
+
101
+ // Cross-field ratio checks (defense-in-depth — also in the plausibility gate)
102
+ // Bounds tightened (deviewreview3): original 100:1 + 50/min were too loose.
103
+ if (rt.tokens_cache_read > 1_000 && rt.tokens_cache_creation === 0) {
104
+ out.push({
105
+ severity: "flag",
106
+ code: "cache_without_creation",
107
+ detail: `${rt.tokens_cache_read} cache_read with 0 cache_creation (impossible cascade)`,
108
+ });
109
+ }
110
+ if (
111
+ rt.tokens_cache_creation > 0 &&
112
+ rt.tokens_cache_read / rt.tokens_cache_creation >
113
+ GATE_LIMITS.MAX_CACHE_REUSE_RATIO
114
+ ) {
115
+ out.push({
116
+ severity: "flag",
117
+ code: "extreme_cache_ratio",
118
+ detail: `cache_read/cache_creation = ${(rt.tokens_cache_read / rt.tokens_cache_creation).toFixed(1)}:1 (real max ~30:1)`,
119
+ });
120
+ }
121
+ if (
122
+ rt.tokens_output > 1_000 &&
123
+ rt.tokens_cache_creation / rt.tokens_output <
124
+ GATE_LIMITS.MIN_CACHE_WRITE_RATIO
125
+ ) {
126
+ out.push({
127
+ severity: "flag",
128
+ code: "low_cache_write_ratio",
129
+ detail: `cache_creation/output = ${(rt.tokens_cache_creation / rt.tokens_output).toFixed(2)}:1 (real min ~1.5:1)`,
130
+ });
131
+ }
132
+ if (
133
+ pillars > 10_000 &&
134
+ rt.tokens_input_fresh / pillars < GATE_LIMITS.MIN_INPUT_SHARE_FRAC
135
+ ) {
136
+ out.push({
137
+ severity: "flag",
138
+ code: "implausible_input_share",
139
+ detail: `input is ${((rt.tokens_input_fresh / pillars) * 100).toFixed(3)}% of total (real min ~0.3%)`,
140
+ });
141
+ }
142
+ if (
143
+ rt.active_minutes_est > 0 &&
144
+ rt.turns_total / rt.active_minutes_est > GATE_LIMITS.MAX_CADENCE_PER_MIN
145
+ ) {
146
+ out.push({
147
+ severity: "flag",
148
+ code: "implausible_cadence",
149
+ detail: `${(rt.turns_total / rt.active_minutes_est).toFixed(1)} turns/min (real: 0.5-10)`,
150
+ });
151
+ }
152
+
153
+ return out;
154
+ }
155
+
156
+ // ── full preflight (plausibility only — NO battery) ─────────────────────────
157
+
158
+ /**
159
+ * Run preflight plausibility checks against a payload. Returns:
160
+ * { pass: true, issues: [] } — clean submission
161
+ * { pass: false, issues: [...], wouldDowngrade } — would be flagged/rejected
162
+ *
163
+ * NOTE: this only mirrors the PUBLIC plausibility gate. The server runs
164
+ * additional proprietary checks (the battery) that this preflight does NOT
165
+ * replicate. A "pass" here means "passes the plausibility gate," not
166
+ * "guaranteed to pass all server gates."
167
+ */
168
+ export function preflight(payload) {
169
+ const issues = plausibilityCheck(payload.raw_telemetry, payload.window);
170
+
171
+ const hasReject = issues.some((i) => i.severity === "reject");
172
+ const hasFlag = issues.some((i) => i.severity === "flag");
173
+
174
+ return {
175
+ pass: issues.length === 0,
176
+ wouldReject: hasReject,
177
+ wouldDowngrade: hasFlag,
178
+ issues,
179
+ summary: hasReject
180
+ ? `REJECT: ${issues
181
+ .filter((i) => i.severity === "reject")
182
+ .map((i) => i.code)
183
+ .join(", ")}`
184
+ : hasFlag
185
+ ? `FLAG (verified → flagged, not ranked): ${issues
186
+ .filter((i) => i.severity === "flag")
187
+ .map((i) => i.code)
188
+ .join(", ")}`
189
+ : "clean — passes plausibility gate (server runs additional proprietary checks)",
190
+ };
191
+ }
package/sign.mjs ADDED
@@ -0,0 +1,106 @@
1
+ // sign.mjs — canonical JSON + ed25519 signing for SigRank signed snapshot submit.
2
+ //
3
+ // BYTE-COMPATIBLE PORT of sigrank-app lib/ingest/signature.ts. This is the highest-
4
+ // risk file in the client: the server VERIFIES the signature over ITS recomputation
5
+ // of these exact canonical bytes, so a single divergent byte hard-rejects (422) every
6
+ // verified submission. Keep this a line-for-line mirror of the server canonicalizer:
7
+ // canonical JSON = recursively sorted keys, compact separators, UTF-8, with the
8
+ // derived agent.signature + agent.snapshot_hash stripped before serialization.
9
+ // snapshot_hash = "sha256:" + hex(sha256(canonical_bytes)).
10
+ // public key = "ed25519:<base64 of the 32 raw verify-key bytes>".
11
+ // signature = base64 of the 64-byte ed25519 signature over canonical_bytes.
12
+ // The canon-parity fixture (tests/fixtures/canon_parity.json) + sign.test.mjs + the
13
+ // sigrank-app canon_parity gate together lock this byte-for-byte.
14
+
15
+ import {
16
+ createHash,
17
+ createPrivateKey,
18
+ createPublicKey,
19
+ sign as edSign,
20
+ verify as edVerify,
21
+ } from "node:crypto";
22
+
23
+ /** Agent fields excluded from the canonical (signed/hashed) body — they derive from it. */
24
+ const DERIVED_AGENT_FIELDS = ["signature", "snapshot_hash"];
25
+
26
+ /** Recursively sort object keys (matches the server's Object.keys(...).sort()); arrays keep order. */
27
+ function sortDeep(v) {
28
+ if (Array.isArray(v)) return v.map(sortDeep);
29
+ if (v && typeof v === "object") {
30
+ const out = {};
31
+ for (const k of Object.keys(v).sort()) out[k] = sortDeep(v[k]);
32
+ return out;
33
+ }
34
+ return v;
35
+ }
36
+
37
+ /** Canonical JSON string the agent signs/hashes (derived agent fields stripped). */
38
+ export function canonicalJson(payload) {
39
+ const clone = JSON.parse(JSON.stringify(payload));
40
+ const agent = clone.agent;
41
+ if (agent && typeof agent === "object") {
42
+ for (const f of DERIVED_AGENT_FIELDS) delete agent[f];
43
+ }
44
+ return JSON.stringify(sortDeep(clone));
45
+ }
46
+
47
+ /** UTF-8 bytes of the canonical JSON — the exact bytes that get signed. */
48
+ export function canonicalBytes(payload) {
49
+ return Buffer.from(canonicalJson(payload), "utf-8");
50
+ }
51
+
52
+ /** "sha256:<hex>" over the canonical bytes (matches the server snapshotHash). */
53
+ export function snapshotHash(payload) {
54
+ return `sha256:${createHash("sha256").update(canonicalBytes(payload)).digest("hex")}`;
55
+ }
56
+
57
+ /** SPKI DER prefix for a raw 32-byte ed25519 public key. */
58
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
59
+
60
+ /** Build a node KeyObject from an "ed25519:<base64>" (or bare base64) public key, or null. */
61
+ function publicKeyFrom(pk) {
62
+ try {
63
+ const body = pk.startsWith("ed25519:") ? pk.slice("ed25519:".length) : pk;
64
+ const raw = Buffer.from(body, "base64");
65
+ if (raw.length !== 32) return null;
66
+ return createPublicKey({
67
+ key: Buffer.concat([ED25519_SPKI_PREFIX, raw]),
68
+ format: "der",
69
+ type: "spki",
70
+ });
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Sign the payload's canonical bytes with the PKCS8-DER-base64 private key → base64 sig.
78
+ * The returned string is the X-Agent-Signature header value.
79
+ */
80
+ export function signPayload(payload, privateKeyPkcs8B64) {
81
+ const key = createPrivateKey({
82
+ key: Buffer.from(privateKeyPkcs8B64, "base64"),
83
+ format: "der",
84
+ type: "pkcs8",
85
+ });
86
+ return edSign(null, canonicalBytes(payload), key).toString("base64");
87
+ }
88
+
89
+ /**
90
+ * Local verify (parity with the server's verifySignature) — used by the self-test
91
+ * and as a pre-send sanity check. Returns false on any malformation, never throws.
92
+ */
93
+ export function verifyPayload(payload, signatureB64, publicKey) {
94
+ const key = publicKeyFrom(publicKey);
95
+ if (!key) return false;
96
+ try {
97
+ return edVerify(
98
+ null,
99
+ canonicalBytes(payload),
100
+ key,
101
+ Buffer.from(signatureB64, "base64"),
102
+ );
103
+ } catch {
104
+ return false;
105
+ }
106
+ }