sigrank 0.14.3 → 0.15.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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/package.json +1 -1
  3. package/tools.mjs +136 -0
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ > **🏆 SigRank is live: [signalaf.com](https://signalaf.com)** — the leaderboard for how
2
+ > efficiently you use AI, not how much. See your projected rank in 60 seconds at
3
+ > [signalaf.com/score](https://signalaf.com/score). *Token counts only. Never your prompts.*
4
+
1
5
  <div align="center">
2
6
 
3
7
  <p><img src="./.github/assets/og-card.png" alt="SigRank — the new standard in AI evaluation &amp; benchmarks" width="800"></p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigrank",
3
- "version": "0.14.3",
3
+ "version": "0.15.0",
4
4
  "description": "SigRank MCP server — the yield cascade + live leaderboard as MCP tools any agent can call",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/tools.mjs CHANGED
@@ -295,6 +295,31 @@ export const TOOLS = [
295
295
  },
296
296
  },
297
297
  },
298
+ {
299
+ name: 'simulate_change',
300
+ description:
301
+ "The first PRESCRIPTIVE SigRank tool — 'what if I changed my token mix?' Takes your current 4 pillars (input/output/cacheCreate/cacheRead) and one or more proposed changes, runs the canonical cascade on BOTH the current and simulated values, and returns the exact Υ Yield delta, class change, and per-metric diffs. This is the 'show me the payoff before I do the work' primitive: no network, no submission, pure local math. Use it to answer 'would increasing my cache-read by 50k tokens actually move my class?' before you change your workflow. Accepts the current pillars as JSON or 4 numbers (same as rank_paste) plus a `changes` object with any of the 4 pillar names mapped to new absolute values OR relative deltas (e.g. {cacheRead: '+50000'} or {input: 800000}).",
302
+ inputSchema: {
303
+ type: 'object',
304
+ properties: {
305
+ text: {
306
+ type: 'string',
307
+ description: 'Current token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
308
+ },
309
+ changes: {
310
+ type: 'object',
311
+ description: 'Proposed changes to apply. Keys: input, output, cacheCreate, cacheRead. Values are either absolute numbers (replace) or strings starting with +/- for relative deltas (add/subtract). Omitted pillars are unchanged.',
312
+ properties: {
313
+ input: { type: ['number', 'string'], description: 'new input token count (absolute) or "+/-N" for a relative delta' },
314
+ output: { type: ['number', 'string'], description: 'new output token count (absolute) or "+/-N" for a relative delta' },
315
+ cacheCreate: { type: ['number', 'string'], description: 'new cache-create token count (absolute) or "+/-N" for a relative delta' },
316
+ cacheRead: { type: ['number', 'string'], description: 'new cache-read token count (absolute) or "+/-N" for a relative delta' },
317
+ },
318
+ },
319
+ },
320
+ required: ['text', 'changes'],
321
+ },
322
+ },
298
323
  ]
299
324
 
300
325
  // tokenpull window key → the board's window_type enum.
@@ -718,5 +743,116 @@ export async function callTool(name, args, opts = {}) {
718
743
  }
719
744
  }
720
745
 
746
+ if (name === 'simulate_change') {
747
+ // The first prescriptive tool — "what if I changed my token mix?"
748
+ // Pure local math: parse current pillars, apply proposed changes, run the
749
+ // cascade on both, return the delta. No network, no submission.
750
+ if (!args?.text) throw new Error('simulate_change requires a non-empty `text` argument (current pillars).')
751
+ if (typeof args.text === 'string' && args.text.length > MAX_INPUT) {
752
+ return { status: 'error', reason: 'input_too_large', detail: `text exceeds ${MAX_INPUT} chars.` }
753
+ }
754
+ if (!args?.changes || typeof args.changes !== 'object') {
755
+ throw new Error('simulate_change requires a `changes` object with at least one pillar change.')
756
+ }
757
+
758
+ const currentPillars = parsePillars(args.text)
759
+ const current = withParseWarnings(currentPillars, cascade(currentPillars))
760
+
761
+ // Apply changes: each pillar is either an absolute number (replace) or a
762
+ // string starting with +/- (relative delta). Omitted pillars are unchanged.
763
+ const PILLAR_KEYS = ['input', 'output', 'cacheCreate', 'cacheRead']
764
+ const simulated = { ...currentPillars }
765
+ const appliedChanges = {}
766
+
767
+ for (const key of PILLAR_KEYS) {
768
+ if (args.changes[key] == null) continue
769
+ const raw = args.changes[key]
770
+ let newVal
771
+
772
+ if (typeof raw === 'number') {
773
+ newVal = raw
774
+ } else if (typeof raw === 'string') {
775
+ const trimmed = raw.trim()
776
+ if (trimmed.startsWith('+') || trimmed.startsWith('-')) {
777
+ // Relative delta: add/subtract from current value
778
+ const delta = Number(trimmed)
779
+ if (!Number.isFinite(delta)) {
780
+ return { status: 'error', reason: 'invalid_change', detail: `changes.${key}: "${raw}" is not a valid relative delta.` }
781
+ }
782
+ newVal = currentPillars[key] + delta
783
+ } else {
784
+ // Absolute value as a string
785
+ newVal = Number(trimmed)
786
+ }
787
+ } else {
788
+ return { status: 'error', reason: 'invalid_change', detail: `changes.${key}: expected number or string, got ${typeof raw}.` }
789
+ }
790
+
791
+ if (!Number.isFinite(newVal)) {
792
+ return { status: 'error', reason: 'invalid_change', detail: `changes.${key}: result is not a finite number.` }
793
+ }
794
+ // Clamp to non-negative — token counts can't be negative
795
+ if (newVal < 0) {
796
+ return { status: 'error', reason: 'invalid_change', detail: `changes.${key}: result ${newVal} is negative — token counts must be >= 0.` }
797
+ }
798
+
799
+ simulated[key] = newVal
800
+ appliedChanges[key] = {
801
+ from: currentPillars[key],
802
+ to: newVal,
803
+ delta: newVal - currentPillars[key],
804
+ }
805
+ }
806
+
807
+ if (Object.keys(appliedChanges).length === 0) {
808
+ return { status: 'error', reason: 'no_changes', detail: 'No pillar changes specified in the `changes` object.' }
809
+ }
810
+
811
+ const simulatedResult = cascade(simulated)
812
+
813
+ // Compute deltas for every cascade metric
814
+ const metricDelta = (curr, sim) => {
815
+ if (curr == null && sim == null) return null
816
+ if (curr == null) return { from: null, to: sim, delta: null }
817
+ if (sim == null) return { from: curr, to: null, delta: null }
818
+ return { from: curr, to: sim, delta: Number((sim - curr).toFixed(4)) }
819
+ }
820
+
821
+ const classChanged = current.class !== simulatedResult.class
822
+
823
+ return {
824
+ current: {
825
+ pillars: currentPillars,
826
+ yield: current.yield,
827
+ snr: current.snr,
828
+ leverage: current.leverage,
829
+ velocity: current.velocity,
830
+ dev10x: current.dev10x,
831
+ class: current.class,
832
+ },
833
+ simulated: {
834
+ pillars: { input: simulated.input, output: simulated.output, cacheCreate: simulated.cacheCreate, cacheRead: simulated.cacheRead },
835
+ yield: simulatedResult.yield,
836
+ snr: simulatedResult.snr,
837
+ leverage: simulatedResult.leverage,
838
+ velocity: simulatedResult.velocity,
839
+ dev10x: simulatedResult.dev10x,
840
+ class: simulatedResult.class,
841
+ },
842
+ changes: appliedChanges,
843
+ deltas: {
844
+ yield: metricDelta(current.yield, simulatedResult.yield),
845
+ snr: metricDelta(current.snr, simulatedResult.snr),
846
+ leverage: metricDelta(current.leverage, simulatedResult.leverage),
847
+ velocity: metricDelta(current.velocity, simulatedResult.velocity),
848
+ dev10x: metricDelta(current.dev10x, simulatedResult.dev10x),
849
+ },
850
+ class_changed: classChanged,
851
+ ...(classChanged ? { class_transition: `${current.class} → ${simulatedResult.class}` } : {}),
852
+ ...(simulatedResult.warnings ? { simulated_warnings: simulatedResult.warnings } : {}),
853
+ note: 'Local simulation only — no submission. The actual score depends on server-side RS.xx weights and class thresholds.',
854
+ }
855
+ }
856
+
721
857
  throw new Error(`Unknown tool: ${name}`)
722
858
  }