sigrank 0.0.178 → 0.0.179
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/CHANGELOG.md +100 -0
- package/MCP_SERVER_DESCRIPTION.md +141 -0
- package/README.md +8 -0
- package/adapters/index.mjs +865 -0
- package/adapters/tokenpull.mjs +576 -0
- package/adapters.mjs +1 -865
- package/analytics/cascade.mjs +274 -0
- package/cascade.mjs +1 -274
- package/cli.mjs +1 -1853
- package/connect.mjs +1 -13
- package/identity/connect.mjs +13 -0
- package/identity/keystore.mjs +242 -0
- package/identity/sign.mjs +106 -0
- package/index.mjs +29 -153
- package/keystore.mjs +1 -242
- package/narrate.mjs +1 -77
- package/package.json +29 -13
- package/preflight.mjs +1 -191
- package/presentation/cli.mjs +1874 -0
- package/presentation/narrate.mjs +77 -0
- package/presentation/tui.mjs +2668 -0
- package/prompts/check-my-efficiency.md +1 -0
- package/prompts/compare-with-leader.md +1 -0
- package/prompts/simulate-improvement.md +1 -0
- package/resources/class-tiers.md +23 -0
- package/resources/data-policy.md +27 -0
- package/resources/install-guide.md +59 -0
- package/resources/privacy-model.md +30 -0
- package/resources/scoring-formula.md +28 -0
- package/review.mjs +263 -0
- package/sign.mjs +1 -106
- package/submit/index.mjs +309 -0
- package/submit/preflight.mjs +191 -0
- package/submit.mjs +1 -303
- package/tokenpull.mjs +1 -576
- package/tools/_framing.mjs +139 -0
- package/tools/_helpers.mjs +258 -0
- package/tools/_report.mjs +262 -0
- package/tools/_schemas.mjs +394 -0
- package/tools/_verifiers.mjs +148 -0
- package/tools/compare-operators.mjs +79 -0
- package/tools/compare-self.mjs +122 -0
- package/tools/describe-power-user.mjs +68 -0
- package/tools/diagnose-cascade.mjs +192 -0
- package/tools/enroll.mjs +121 -0
- package/tools/get-best-operator.mjs +58 -0
- package/tools/get-leaderboard.mjs +26 -0
- package/tools/get-operator.mjs +33 -0
- package/tools/index.mjs +257 -0
- package/tools/optimize-efficiency.mjs +160 -0
- package/tools/rank-paste.mjs +51 -0
- package/tools/rank-windows.mjs +120 -0
- package/tools/self-improve.mjs +423 -0
- package/tools/simulate-change.mjs +207 -0
- package/tools/submit-paste.mjs +118 -0
- package/tools/submit-verified.mjs +153 -0
- package/tools/suggest-improvements.mjs +168 -0
- package/tools/tokenpull-compare.mjs +123 -0
- package/tools/tokenpull-submit.mjs +234 -0
- package/tools/tokenpull.mjs +113 -0
- package/tools/tokscale-breakdown.mjs +159 -0
- package/tools/tokscale-competitive-intel.mjs +45 -0
- package/tools/tokscale-cost-analysis.mjs +31 -0
- package/tools/tokscale-developer-profile.mjs +36 -0
- package/tools/tokscale-device-profile.mjs +35 -0
- package/tools/tokscale-market-share.mjs +38 -0
- package/tools/tokscale-mcp-usage.mjs +33 -0
- package/tools/tokscale-model-trends.mjs +32 -0
- package/tools/watch-tokenpull.mjs +134 -0
- package/tools.mjs +1 -3897
- package/tui.mjs +4 -2650
- package/banner.svg +0 -7
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Pull my local token usage with tokenpull, compute my yield, and then fetch the leaderboard so I can see where I rank. Show me my class tier and my key metrics (Yield, Leverage, Velocity).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Fetch the SigRank leaderboard with get_leaderboard, then pull my local token usage with tokenpull. Compare my yield, leverage, and velocity against the top operator. What's the gap and what would I need to change to close it?
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
First pull my local token usage with tokenpull to get my current pillars. Then use simulate_change to answer: "what if I {{change}}?" Show me the yield delta and whether my class tier would change.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# SigRank Class Tiers
|
|
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).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# SigRank Data Policy
|
|
2
|
+
|
|
3
|
+
SigRank is a public AI-operator leaderboard. This policy summarizes what data we collect, how we use it, and how you can delete it.
|
|
4
|
+
|
|
5
|
+
## What we collect
|
|
6
|
+
|
|
7
|
+
- **Token telemetry** from your local agent: token counts, content lengths, model identifiers, and platform/source tags. We use these to compute your score on the leaderboard.
|
|
8
|
+
- **Account identifiers** when you sign in (email from your OAuth provider) so we can link your operator profile to your login.
|
|
9
|
+
- **Optional profile fields** you choose to add: display name, handle, bio, location, links, avatar.
|
|
10
|
+
|
|
11
|
+
## What we never collect
|
|
12
|
+
|
|
13
|
+
We do **not** read or transmit the content of your prompts or AI conversations. The local agent extracts only numeric token counts and metadata; transcripts never leave your device.
|
|
14
|
+
|
|
15
|
+
## Consent
|
|
16
|
+
|
|
17
|
+
Before you connect a device, you must agree to the current Terms of Service and Privacy Policy. We record the timestamp and version of your consent.
|
|
18
|
+
|
|
19
|
+
## Your choices
|
|
20
|
+
|
|
21
|
+
- **Pause data collection** at any time from [signalaf.com/settings](https://signalaf.com/settings). While paused, your agent submissions are rejected and no new telemetry is stored.
|
|
22
|
+
- **Delete your data** from Settings to erase all snapshots, scored metrics, ranks, devices, and enrollment codes while keeping your account and profile.
|
|
23
|
+
- **Delete your account** from Settings to fully sever your identity. This runs Delete your data first, then anonymizes your board row (kept as aggregate history), removes your login, and cancels billing.
|
|
24
|
+
|
|
25
|
+
## Contact
|
|
26
|
+
|
|
27
|
+
Questions: hello@signalaf.com
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# SigRank Quick Start
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx sigrank
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
No global install needed. The MCP server starts automatically when an AI client (Claude, Cursor, Cline) connects.
|
|
10
|
+
|
|
11
|
+
## First Submission
|
|
12
|
+
|
|
13
|
+
1. **Enroll** — register your operator codename:
|
|
14
|
+
```bash
|
|
15
|
+
npx sigrank enroll
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
2. **Pull + Submit** — scan your local AI session logs and publish:
|
|
19
|
+
```bash
|
|
20
|
+
npx sigrank submit
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
3. **Dry Run** — see exactly what would be sent before publishing:
|
|
24
|
+
```bash
|
|
25
|
+
npx sigrank submit --dry-run
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Token Pull Sources
|
|
29
|
+
|
|
30
|
+
SigRank bundles two token readers as npm dependencies:
|
|
31
|
+
- **ccusage** — Claude Code session logs
|
|
32
|
+
- **tokscale** — multi-platform token telemetry
|
|
33
|
+
|
|
34
|
+
A third source, **token-dashboard**, is read from `~/.claude/token-dashboard.db` (SQLite).
|
|
35
|
+
That DB is created by [Nate's token-dashboard](https://github.com/nateherkai/token-dashboard) (Python):
|
|
36
|
+
```bash
|
|
37
|
+
git clone https://github.com/nateherkai/token-dashboard.git
|
|
38
|
+
cd token-dashboard
|
|
39
|
+
python3 cli.py dashboard
|
|
40
|
+
```
|
|
41
|
+
Run the scan once to create the DB; SigRank reads it directly via sqlite3 after that.
|
|
42
|
+
|
|
43
|
+
## MCP Client Setup
|
|
44
|
+
|
|
45
|
+
Add to your MCP client config:
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"sigrank": {
|
|
50
|
+
"command": "npx",
|
|
51
|
+
"args": ["sigrank"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Live Board
|
|
58
|
+
|
|
59
|
+
Visit https://signalaf.com to see the global leaderboard.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# SigRank Privacy Model
|
|
2
|
+
|
|
3
|
+
## What leaves your machine
|
|
4
|
+
|
|
5
|
+
**Only four numbers per submission:**
|
|
6
|
+
- Input tokens
|
|
7
|
+
- Output tokens
|
|
8
|
+
- Cache create tokens
|
|
9
|
+
- Cache read tokens
|
|
10
|
+
|
|
11
|
+
**That's it.** No prompts, no code, no file contents, no conversation text.
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
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
|
+
|
|
20
|
+
## What SigRank can NOT see
|
|
21
|
+
|
|
22
|
+
- Your prompts or messages
|
|
23
|
+
- Your code or file contents
|
|
24
|
+
- Your tool calls or their results
|
|
25
|
+
- Which AI platform you use (beyond token counts)
|
|
26
|
+
- Your identity (only your chosen codename)
|
|
27
|
+
|
|
28
|
+
## Verification
|
|
29
|
+
|
|
30
|
+
The submit_verified tool uses ed25519 signing. The board's source_attestations table records the signature for audit. You can verify your own submissions via get_operator.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# SigRank Scoring Formula
|
|
2
|
+
|
|
3
|
+
## Yield (Υ) — the headline metric
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Υ = Cache Reads × Output / Input²
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Yield rewards operators who maximize output while minimizing input — and who build cache to amortize that input across sessions. A high yield means you're getting more done with less, efficiently.
|
|
10
|
+
|
|
11
|
+
## Derived Metrics
|
|
12
|
+
|
|
13
|
+
| Metric | Formula | Meaning |
|
|
14
|
+
|--------|---------|---------|
|
|
15
|
+
| **SNR** | Output / (Input + CacheCreate) | Signal-to-noise: how much of your token spend is productive output vs. overhead |
|
|
16
|
+
| **Leverage** | Cache Reads / Input | How well you reuse cached context — higher = better cache utilization |
|
|
17
|
+
| **Velocity** | Output / Input | Raw output efficiency — how much output you generate per token of input |
|
|
18
|
+
| **10xDEV** | Composite score | Weighted blend of yield, leverage, and velocity for cross-platform comparison |
|
|
19
|
+
|
|
20
|
+
## Class Tier Thresholds
|
|
21
|
+
|
|
22
|
+
| Tier | Yield Range | Profile |
|
|
23
|
+
|------|-------------|---------|
|
|
24
|
+
| **Burner** | Υ < 1.0 | Raw volume — high input, low cache reuse, brute-force output |
|
|
25
|
+
| **Builder** | 1.0 ≤ Υ < 10.0 | Balanced — moderate cache, decent output efficiency |
|
|
26
|
+
| **10xer** | Υ ≥ 10.0 | Cascade-optimized — high cache reads, minimal input, efficient output |
|
|
27
|
+
|
|
28
|
+
The formula is deterministic and computed locally. No network calls needed for scoring.
|
package/review.mjs
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review.mjs — `sigrank review` subcommand.
|
|
3
|
+
*
|
|
4
|
+
* Runs an operator's numbers through all three reference operating ratios
|
|
5
|
+
* (AA avg, HCM, Codex PU) and validates results to identify correct placement.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx sigrank review <handle> # pull from raw data (if available)
|
|
9
|
+
* npx sigrank review --output N --cache-read N --combined-input N
|
|
10
|
+
*
|
|
11
|
+
* The cache_write convergence test:
|
|
12
|
+
* When we split combined_input into input + cache_write using each ratio's
|
|
13
|
+
* velocity, the cache_write values should converge. If one ratio produces
|
|
14
|
+
* a cache_write far below the others, that ratio is broken for this operator.
|
|
15
|
+
*
|
|
16
|
+
* Transcribed by Devin from owner (djm) analysis and direction.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { cascade, classify, round } from "./cascade.mjs";
|
|
20
|
+
|
|
21
|
+
// Reference operating ratios (cache_read : input : output, input=1)
|
|
22
|
+
const RATIOS = {
|
|
23
|
+
"AA avg": {
|
|
24
|
+
cache: 3.5,
|
|
25
|
+
velocity: 0.5,
|
|
26
|
+
label: "3.5:1:0.5",
|
|
27
|
+
description: "all-users average",
|
|
28
|
+
},
|
|
29
|
+
HCM: {
|
|
30
|
+
cache: 20,
|
|
31
|
+
velocity: 0.1,
|
|
32
|
+
label: "20:1:0.1",
|
|
33
|
+
description: "human center of mass",
|
|
34
|
+
},
|
|
35
|
+
"Codex PU": {
|
|
36
|
+
cache: 243,
|
|
37
|
+
velocity: 1.03,
|
|
38
|
+
label: "243:1:1.03",
|
|
39
|
+
description: "Codex power-user",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function fmtTokens(n) {
|
|
44
|
+
if (n >= 1e12) return `${(n / 1e12).toFixed(2)}T`;
|
|
45
|
+
if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
|
|
46
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
47
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
48
|
+
return String(n);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function runRatios(output, cacheRead, combinedInput) {
|
|
52
|
+
const results = {};
|
|
53
|
+
|
|
54
|
+
for (const [name, r] of Object.entries(RATIOS)) {
|
|
55
|
+
const v = r.velocity;
|
|
56
|
+
const cRef = r.cache;
|
|
57
|
+
|
|
58
|
+
const inputEst = Math.round(output / v);
|
|
59
|
+
const cacheWriteEst = combinedInput - inputEst;
|
|
60
|
+
|
|
61
|
+
const leverage = cacheRead / inputEst;
|
|
62
|
+
const velocity = v;
|
|
63
|
+
const yieldVal = leverage * velocity;
|
|
64
|
+
|
|
65
|
+
results[name] = {
|
|
66
|
+
label: r.label,
|
|
67
|
+
description: r.description,
|
|
68
|
+
refCache: cRef,
|
|
69
|
+
refVelocity: v,
|
|
70
|
+
input: inputEst,
|
|
71
|
+
output,
|
|
72
|
+
cacheWrite: cacheWriteEst,
|
|
73
|
+
cacheRead,
|
|
74
|
+
leverage,
|
|
75
|
+
velocity,
|
|
76
|
+
yield: yieldVal,
|
|
77
|
+
levMatch: leverage / cRef,
|
|
78
|
+
cwValid: cacheWriteEst > 0,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Convergence test
|
|
83
|
+
const cwValues = Object.values(results)
|
|
84
|
+
.filter((r) => r.cwValid)
|
|
85
|
+
.map((r) => r.cacheWrite);
|
|
86
|
+
|
|
87
|
+
for (const [name, r] of Object.entries(results)) {
|
|
88
|
+
if (!r.cwValid) {
|
|
89
|
+
r.cwStatus = "INVALID (negative)";
|
|
90
|
+
r.cwConverges = false;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const others = Object.entries(results)
|
|
94
|
+
.filter(([n]) => n !== name && results[n].cwValid)
|
|
95
|
+
.map(([, v]) => v.cacheWrite);
|
|
96
|
+
if (others.length > 0) {
|
|
97
|
+
const otherMedian = [...others].sort((a, b) => a - b)[
|
|
98
|
+
Math.floor(others.length / 2)
|
|
99
|
+
];
|
|
100
|
+
if (r.cacheWrite < otherMedian * 0.5) {
|
|
101
|
+
r.cwStatus = `OUTLIER (cache_write ${fmtTokens(r.cacheWrite)} is >50% below peers)`;
|
|
102
|
+
r.cwConverges = false;
|
|
103
|
+
} else if (r.cacheWrite < otherMedian * 0.7) {
|
|
104
|
+
r.cwStatus = `LOW (cache_write ${fmtTokens(r.cacheWrite)} is below peers)`;
|
|
105
|
+
r.cwConverges = false;
|
|
106
|
+
} else {
|
|
107
|
+
r.cwStatus = "VALID (converges with peers)";
|
|
108
|
+
r.cwConverges = true;
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
r.cwStatus = "VALID";
|
|
112
|
+
r.cwConverges = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function classifyYield(y) {
|
|
120
|
+
if (y >= 100) return "POWER";
|
|
121
|
+
if (y >= 10) return "SEEKER";
|
|
122
|
+
if (y >= 1) return "IGNITER";
|
|
123
|
+
if (y > 0) return "BURNER";
|
|
124
|
+
return "BASE";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ANSI colors
|
|
128
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
129
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
130
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
131
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
132
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
133
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
134
|
+
|
|
135
|
+
export async function runReview(opts) {
|
|
136
|
+
const { handle, output, cacheRead, combinedInput } = opts;
|
|
137
|
+
|
|
138
|
+
if (!output || !cacheRead || !combinedInput) {
|
|
139
|
+
console.error(
|
|
140
|
+
red(
|
|
141
|
+
"\n Missing required values. Usage:\n" +
|
|
142
|
+
" npx sigrank review --output N --cache-read N --combined-input N\n",
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const results = runRatios(output, cacheRead, combinedInput);
|
|
149
|
+
|
|
150
|
+
console.log();
|
|
151
|
+
console.log(` ${"=".repeat(80)}`);
|
|
152
|
+
console.log(` RATIO REVIEW${handle ? `: ${handle}` : " (custom)"}`);
|
|
153
|
+
console.log(` ${"=".repeat(80)}`);
|
|
154
|
+
console.log();
|
|
155
|
+
|
|
156
|
+
// Real telemetry
|
|
157
|
+
console.log(` ${bold("REAL TELEMETRY (constants):")}`);
|
|
158
|
+
console.log(` Output: ${output.toLocaleString()} (${fmtTokens(output)})`);
|
|
159
|
+
console.log(` Cache read: ${cacheRead.toLocaleString()} (${fmtTokens(cacheRead)})`);
|
|
160
|
+
console.log(` Combined input: ${combinedInput.toLocaleString()} (${fmtTokens(combinedInput)})`);
|
|
161
|
+
console.log();
|
|
162
|
+
console.log(` ${bold("REFERENCE RATIOS (cache_read : input : output, input=1):")}`);
|
|
163
|
+
for (const [name, r] of Object.entries(RATIOS)) {
|
|
164
|
+
console.log(` ${name.padEnd(10)} ${r.label.padEnd(12)} (${r.description})`);
|
|
165
|
+
}
|
|
166
|
+
console.log();
|
|
167
|
+
|
|
168
|
+
// Side-by-side table
|
|
169
|
+
const names = ["AA avg", "HCM", "Codex PU"];
|
|
170
|
+
const headerRow = ` ${"".padEnd(14)} ${"AA avg".padStart(14)} ${"HCM".padStart(14)} ${"Codex PU".padStart(14)}`;
|
|
171
|
+
const subHeaderRow = ` ${"".padEnd(14)} ${"3.5:1:0.5".padStart(14)} ${"20:1:0.1".padStart(14)} ${"243:1:1.03".padStart(14)}`;
|
|
172
|
+
const divider = ` ${"-".repeat(14)} ${"-".repeat(14)} ${"-".repeat(14)} ${"-".repeat(14)}`;
|
|
173
|
+
|
|
174
|
+
console.log(headerRow);
|
|
175
|
+
console.log(subHeaderRow);
|
|
176
|
+
console.log(divider);
|
|
177
|
+
|
|
178
|
+
const dataRows = [
|
|
179
|
+
["Input", "input", "int"],
|
|
180
|
+
["Output", "output", "int"],
|
|
181
|
+
["Cache write", "cacheWrite", "int"],
|
|
182
|
+
["Cache read", "cacheRead", "int"],
|
|
183
|
+
["Velocity", "velocity", "float"],
|
|
184
|
+
["Leverage", "leverage", "lev"],
|
|
185
|
+
["Yield", "yield", "float"],
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
for (const [label, key, type] of dataRows) {
|
|
189
|
+
const vals = names.map((n) => {
|
|
190
|
+
const v = results[n][key];
|
|
191
|
+
if (type === "float") return v.toFixed(2).padStart(14);
|
|
192
|
+
if (type === "lev") return `${v.toFixed(1)}:1`.padStart(14);
|
|
193
|
+
return v.toLocaleString().padStart(14);
|
|
194
|
+
});
|
|
195
|
+
console.log(` ${label.padEnd(14)} ${vals[0]} ${vals[1]} ${vals[2]}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log(divider);
|
|
199
|
+
const classes = names.map((n) => classifyYield(results[n].yield).padStart(14));
|
|
200
|
+
console.log(` ${"Class".padEnd(14)} ${classes[0]} ${classes[1]} ${classes[2]}`);
|
|
201
|
+
console.log();
|
|
202
|
+
|
|
203
|
+
// Cache write validation
|
|
204
|
+
console.log(` ${bold("CACHE WRITE VALIDATION:")}`);
|
|
205
|
+
console.log();
|
|
206
|
+
for (const name of names) {
|
|
207
|
+
const r = results[name];
|
|
208
|
+
const marker = r.cwConverges ? green("PASS") : red("FAIL");
|
|
209
|
+
console.log(` ${name.padEnd(10)} cache_write = ${fmtTokens(r.cacheWrite).padStart(10)} [${marker}] ${r.cwStatus}`);
|
|
210
|
+
}
|
|
211
|
+
console.log();
|
|
212
|
+
|
|
213
|
+
// Leverage match
|
|
214
|
+
console.log(` ${bold("LEVERAGE MATCH (actual cache_read/input vs reference cache term):")}`);
|
|
215
|
+
console.log();
|
|
216
|
+
for (const name of names) {
|
|
217
|
+
const r = results[name];
|
|
218
|
+
let matchStatus;
|
|
219
|
+
if (r.levMatch < 0.5) matchStatus = "POOR (ratio does not fit this operator)";
|
|
220
|
+
else if (r.levMatch < 0.8 || r.levMatch > 2.0) matchStatus = "WEAK (leverage far from reference)";
|
|
221
|
+
else if (r.levMatch >= 0.8 && r.levMatch <= 1.3) matchStatus = "STRONG (leverage matches reference)";
|
|
222
|
+
else matchStatus = "MODERATE";
|
|
223
|
+
console.log(
|
|
224
|
+
` ${name.padEnd(10)} actual ${r.leverage.toFixed(1)}:1 vs ref ${r.refCache.toFixed(1)}:1 = ${r.levMatch.toFixed(2)}x [${matchStatus}]`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
console.log();
|
|
228
|
+
|
|
229
|
+
// Recommendation
|
|
230
|
+
const valid = Object.entries(results).filter(([, r]) => r.cwConverges);
|
|
231
|
+
const validNames = valid.map(([n]) => n);
|
|
232
|
+
|
|
233
|
+
console.log(` ${bold("RECOMMENDATION:")}`);
|
|
234
|
+
console.log();
|
|
235
|
+
|
|
236
|
+
if (valid.length === 0) {
|
|
237
|
+
console.log(` ${red("No ratio passes cache_write validation. Manual review needed.")}`);
|
|
238
|
+
} else if (valid.length === 1) {
|
|
239
|
+
const [name, r] = valid[0];
|
|
240
|
+
console.log(` ${green(name)} (${r.label}) is the only ratio that passes cache_write validation.`);
|
|
241
|
+
console.log(` Yield: ${r.yield.toFixed(2)} Class: ${classifyYield(r.yield)}`);
|
|
242
|
+
} else {
|
|
243
|
+
const eliminated = names.filter((n) => !validNames.includes(n));
|
|
244
|
+
if (eliminated.length > 0) {
|
|
245
|
+
console.log(` ${dim("Eliminated:")} ${eliminated.join(", ")} (cache_write outlier)`);
|
|
246
|
+
}
|
|
247
|
+
console.log(` ${dim("Candidates:")} ${validNames.join(", ")}`);
|
|
248
|
+
console.log();
|
|
249
|
+
|
|
250
|
+
// Best leverage match
|
|
251
|
+
const [bestName, best] = valid.reduce((best, cur) =>
|
|
252
|
+
Math.abs(cur[1].levMatch - 1.0) < Math.abs(best[1].levMatch - 1.0) ? cur : best
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
console.log(` ${green("Best fit:")} ${bestName} (${best.label})`);
|
|
256
|
+
console.log(` Leverage match: ${best.levMatch.toFixed(2)}x (actual ${best.leverage.toFixed(1)}:1 vs ref ${best.refCache.toFixed(1)}:1)`);
|
|
257
|
+
console.log(` Yield: ${best.yield.toFixed(2)} Class: ${classifyYield(best.yield)}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.log();
|
|
261
|
+
console.log(` Run ${cyan("`npx sigrank`")} to get a real signed snapshot with proper cache_write.`);
|
|
262
|
+
console.log();
|
|
263
|
+
}
|
package/sign.mjs
CHANGED
|
@@ -1,106 +1 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
1
|
+
export * from './identity/sign.mjs';
|