sigrank 0.0.178 → 0.0.180

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 (73) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/MCP_SERVER_DESCRIPTION.md +141 -0
  3. package/README.md +8 -0
  4. package/adapters/index.mjs +865 -0
  5. package/adapters/tokenpull.mjs +576 -0
  6. package/adapters.mjs +1 -865
  7. package/analytics/cascade.mjs +274 -0
  8. package/cascade.mjs +1 -274
  9. package/cli.mjs +1 -1853
  10. package/connect.mjs +1 -13
  11. package/identity/connect.mjs +13 -0
  12. package/identity/keystore.mjs +242 -0
  13. package/identity/sign.mjs +106 -0
  14. package/index.mjs +43 -159
  15. package/keystore.mjs +1 -242
  16. package/narrate.mjs +1 -77
  17. package/package.json +32 -15
  18. package/preflight.mjs +1 -191
  19. package/presentation/cli.mjs +1874 -0
  20. package/presentation/narrate.mjs +77 -0
  21. package/presentation/tui.mjs +2668 -0
  22. package/prompts/check-my-efficiency.md +1 -0
  23. package/prompts/compare-with-leader.md +1 -0
  24. package/prompts/simulate-improvement.md +1 -0
  25. package/resources/class-tiers.md +23 -0
  26. package/resources/data-policy.md +27 -0
  27. package/resources/install-guide.md +59 -0
  28. package/resources/privacy-model.md +30 -0
  29. package/resources/scoring-formula.md +28 -0
  30. package/review.mjs +263 -0
  31. package/sign.mjs +1 -106
  32. package/submit/index.mjs +309 -0
  33. package/submit/preflight.mjs +191 -0
  34. package/submit.mjs +1 -303
  35. package/tokenpull.mjs +1 -576
  36. package/tokscale_analytics.mjs +875 -0
  37. package/tools/_framing.mjs +139 -0
  38. package/tools/_helpers.mjs +258 -0
  39. package/tools/_report.mjs +262 -0
  40. package/tools/_schemas.mjs +394 -0
  41. package/tools/_verifiers.mjs +148 -0
  42. package/tools/compare-operators.mjs +79 -0
  43. package/tools/compare-self.mjs +122 -0
  44. package/tools/describe-power-user.mjs +68 -0
  45. package/tools/diagnose-cascade.mjs +192 -0
  46. package/tools/enroll.mjs +121 -0
  47. package/tools/get-best-operator.mjs +58 -0
  48. package/tools/get-leaderboard.mjs +26 -0
  49. package/tools/get-operator.mjs +33 -0
  50. package/tools/index.mjs +246 -0
  51. package/tools/optimize-efficiency.mjs +160 -0
  52. package/tools/rank-paste.mjs +51 -0
  53. package/tools/rank-windows.mjs +120 -0
  54. package/tools/self-improve.mjs +423 -0
  55. package/tools/simulate-change.mjs +207 -0
  56. package/tools/submit-paste.mjs +118 -0
  57. package/tools/submit-verified.mjs +153 -0
  58. package/tools/suggest-improvements.mjs +168 -0
  59. package/tools/tokenpull-compare.mjs +123 -0
  60. package/tools/tokenpull-submit.mjs +234 -0
  61. package/tools/tokenpull.mjs +113 -0
  62. package/tools/tokscale-breakdown.mjs +159 -0
  63. package/tools/tokscale-competitive-intel.mjs +45 -0
  64. package/tools/tokscale-cost-analysis.mjs +31 -0
  65. package/tools/tokscale-developer-profile.mjs +36 -0
  66. package/tools/tokscale-device-profile.mjs +35 -0
  67. package/tools/tokscale-market-share.mjs +38 -0
  68. package/tools/tokscale-mcp-usage.mjs +33 -0
  69. package/tools/tokscale-model-trends.mjs +32 -0
  70. package/tools/watch-tokenpull.mjs +134 -0
  71. package/tools.mjs +1 -3897
  72. package/tui.mjs +4 -2650
  73. package/banner.svg +0 -7
@@ -0,0 +1,423 @@
1
+ /**
2
+ * tools/self-improve.mjs — self_improve tool.
3
+ */
4
+
5
+ import { cascade, parsePillars, detectMode, qualityScore, MODE_EXPECTED_YIELD } from "../analytics/cascade.mjs";
6
+ import { computeBadges } from "../badges.mjs";
7
+ import { CASCADE_OUTPUT, ANNOTATIONS } from "./_schemas.mjs";
8
+ import { MAX_INPUT, withParseWarnings } from "./_helpers.mjs";
9
+ import {
10
+ _pullDailyRows,
11
+ _compoundWeekly,
12
+ _modeDistribution,
13
+ _modeWeightedYield,
14
+ _peakYield,
15
+ _healthScore,
16
+ _yieldForDays,
17
+ _trajectory,
18
+ _phasePattern,
19
+ _dailyAssessment,
20
+ _dailyAdvice,
21
+ } from "./_report.mjs";
22
+
23
+ export const TOOL_DEF = {
24
+ name: "self_improve",
25
+ description:
26
+ "Runs the full self-improvement cycle in one call: (1) gets your current token pillars — either from the provided text or by running tokenpull on your local logs, (2) diagnoses where you're leaking efficiency (diagnose_cascade), (3) generates ranked improvement suggestions (suggest_improvements), (4) simulates the top suggestion (simulate_change), and (5) returns the complete cycle: diagnosis + suggestions + the simulated impact of the best change. This is the 'one-click optimize' tool — call it at the end of a session to see what to improve next time. If you provide pillars in `text`, it skips the tokenpull step. If you omit `text`, it runs tokenpull first (requires local ccusage logs). Pure local math — no network, no submission. The `scope` parameter adds mode detection (BUILD/EDIT/DEBUG/MAINTAIN/IDLE) and scoped analysis: 'daily' (default — current behavior + mode), 'weekly' (compound into weekly snapshots + report artifact), 'trend' (30d/90d trajectory analysis).",
27
+ annotations: {
28
+ title: "Self-improve plan",
29
+ ...ANNOTATIONS.readOnlyHint,
30
+ ...ANNOTATIONS.idempotentHint,
31
+ ...ANNOTATIONS.openWorldHint,
32
+ },
33
+ inputSchema: {
34
+ type: "object",
35
+ properties: {
36
+ text: {
37
+ type: "string",
38
+ description:
39
+ 'Optional: token pillars — ccusage JSON or "input output cacheCreate cacheRead". If omitted, runs tokenpull to get current pillars from local logs.',
40
+ },
41
+ window: {
42
+ type: "string",
43
+ enum: ["7d", "30d", "90d", "all"],
44
+ description:
45
+ "Which time window to pull when running tokenpull (default: 30d). Ignored if `text` is provided.",
46
+ },
47
+ scope: {
48
+ type: "string",
49
+ enum: ["daily", "weekly", "trend"],
50
+ description:
51
+ 'Analysis scope: "daily" (default — current behavior + mode detection), "weekly" (compound daily rows into weekly snapshots + report artifact with badges), "trend" (30d/90d trajectory + phase patterns). Daily modes never leave the machine — only weekly distribution goes in submitted reports.',
52
+ },
53
+ },
54
+ },
55
+ outputSchema: {
56
+ type: "object",
57
+ properties: {
58
+ pillars: {
59
+ type: "object",
60
+ description: "The 4 raw token pillars used",
61
+ },
62
+ current_cascade: CASCADE_OUTPUT,
63
+ diagnosis: {
64
+ type: "array",
65
+ description: "Efficiency leaks found (from diagnose_cascade)",
66
+ },
67
+ suggestions: {
68
+ type: "array",
69
+ description: "Ranked improvements (from suggest_improvements)",
70
+ },
71
+ best_simulation: {
72
+ type: "object",
73
+ description: "Simulated result of the top suggestion",
74
+ },
75
+ cycle_summary: {
76
+ type: "string",
77
+ description: "One-line summary of the full cycle",
78
+ },
79
+ // Scope-specific fields
80
+ mode: {
81
+ type: "object",
82
+ description:
83
+ "Detected mode { mode, confidence } — present when scope is daily/weekly/trend",
84
+ },
85
+ quality_score: {
86
+ type: "number",
87
+ description: "Yield relative to mode expectation (daily scope)",
88
+ },
89
+ assessment: {
90
+ type: "string",
91
+ description: "One-line assessment for daily scope",
92
+ },
93
+ advice: {
94
+ type: "string",
95
+ description: "Advice for next session (daily scope)",
96
+ },
97
+ report: {
98
+ type: "object",
99
+ description: "Weekly report artifact (weekly scope)",
100
+ },
101
+ trend: { type: "object", description: "Trend analysis (trend scope)" },
102
+ },
103
+ },
104
+ };
105
+
106
+ export async function handleSelfImprove(args, ctx) {
107
+ // The full self-improvement cycle: pull → diagnose → suggest → simulate.
108
+ // If text is provided, use it as pillars. If not, run tokenpull first.
109
+ let pillars;
110
+ let pulledFrom = "provided";
111
+
112
+ if (args?.text) {
113
+ if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
114
+ return {
115
+ status: "error",
116
+ reason: "input_too_large",
117
+ detail: `text exceeds ${MAX_INPUT} chars.`,
118
+ };
119
+ }
120
+ pillars = parsePillars(args.text);
121
+ } else {
122
+ // Run tokenpull to get current pillars from local logs
123
+ const windowType = args?.window || "30d";
124
+ try {
125
+ const pullResult = await ctx.callTool(
126
+ "tokenpull",
127
+ { window: windowType },
128
+ );
129
+ if (pullResult?.status === "error" || !pullResult?.windows?.length) {
130
+ return {
131
+ status: "error",
132
+ reason: "tokenpull_failed",
133
+ detail:
134
+ "Could not pull pillars from local logs. Provide pillars via `text` argument instead.",
135
+ pull_result: pullResult,
136
+ };
137
+ }
138
+ // Use the first window's pillars
139
+ const w = pullResult.windows[0];
140
+ pillars = w.pillars;
141
+ pulledFrom = `tokenpull ${w.window || windowType}`;
142
+ } catch (e) {
143
+ return {
144
+ status: "error",
145
+ reason: "tokenpull_error",
146
+ detail: String(e.message || e),
147
+ hint: "Provide pillars via `text` argument instead.",
148
+ };
149
+ }
150
+ }
151
+
152
+ const currentResult = withParseWarnings(pillars, cascade(pillars));
153
+ const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = pillars;
154
+
155
+ // ── Step 1: Diagnose ──────────────────────────────────────────────
156
+ const diagnosis = [];
157
+ const leverage = currentResult.leverage;
158
+ if (leverage !== null && leverage < 10) {
159
+ diagnosis.push({
160
+ metric: "cache_leverage",
161
+ severity: "critical",
162
+ finding: `Cache leverage is ${leverage}× — TRANSMITTER-class operators hit 200×+.`,
163
+ recommendation:
164
+ "Increase context reuse: load prior session context, use longer threads.",
165
+ });
166
+ } else if (leverage !== null && leverage < 50) {
167
+ diagnosis.push({
168
+ metric: "cache_leverage",
169
+ severity: "warning",
170
+ finding: `Cache leverage is ${leverage}× — below ARCH+ threshold (~100×+).`,
171
+ recommendation: "Push cache reads higher by reusing prior context.",
172
+ });
173
+ }
174
+
175
+ const velocity = currentResult.velocity;
176
+ if (velocity !== null && velocity < 0.5) {
177
+ diagnosis.push({
178
+ metric: "velocity",
179
+ severity: "critical",
180
+ finding: `Velocity is ${velocity} — generating only ${velocity}× input as output.`,
181
+ recommendation: "Increase output per turn, reduce over-reading.",
182
+ });
183
+ } else if (velocity !== null && velocity < 1.0) {
184
+ diagnosis.push({
185
+ metric: "velocity",
186
+ severity: "warning",
187
+ finding: `Velocity is ${velocity} — below 1.0. Healthy operators hit 1.5×+.`,
188
+ recommendation: "Generate more output per input token.",
189
+ });
190
+ }
191
+
192
+ if (currentResult.dev10x === null && cw === 0) {
193
+ diagnosis.push({
194
+ metric: "10xdev",
195
+ severity: "critical",
196
+ finding: "No cache creation — cascade cannot compound.",
197
+ recommendation:
198
+ "Switch to a platform with prompt caching (Claude Code).",
199
+ });
200
+ }
201
+
202
+ const total = i + o + cw + cr;
203
+ if (total > 0 && (i / total) * 100 > 10) {
204
+ diagnosis.push({
205
+ metric: "input_bloat",
206
+ severity: "warning",
207
+ finding: `Fresh input is ${((i / total) * 100).toFixed(1)}% of total flow — efficient operators keep it under 1%.`,
208
+ recommendation: "Reduce fresh input by reusing prior context.",
209
+ });
210
+ }
211
+
212
+ // ── Step 2: Suggest (generate + simulate candidates) ──────────────
213
+ const candidates = [];
214
+ // If cacheRead is 0, suggest absolute amounts instead of percentage boosts
215
+ const crBoosts = cr > 0 ? [1.5, 2, 3, 5] : [];
216
+ for (const mult of crBoosts) {
217
+ const sim = cascade({ ...pillars, cacheRead: Math.round(cr * mult) });
218
+ if (sim.yield !== null)
219
+ candidates.push({
220
+ action: `Increase cache reads by ${Math.round((mult - 1) * 100)}%`,
221
+ pillar: "cacheRead",
222
+ delta: `+${Math.round(cr * (mult - 1)).toLocaleString()}`,
223
+ simulated_yield: sim.yield,
224
+ yield_delta: Number(
225
+ (sim.yield - (currentResult.yield ?? 0)).toFixed(2),
226
+ ),
227
+ class_after: sim.class,
228
+ rationale: "Cache reads are the strongest Υ multiplier.",
229
+ });
230
+ }
231
+ // For zero-cache operators, suggest enabling caching with a starter amount
232
+ if (cr === 0 && cw === 0) {
233
+ const starterAmounts = [
234
+ Math.round(i * 10),
235
+ Math.round(i * 50),
236
+ Math.round(i * 100),
237
+ ];
238
+ for (const amt of starterAmounts) {
239
+ const sim = cascade({
240
+ ...pillars,
241
+ cacheCreate: Math.round(amt * 0.5),
242
+ cacheRead: amt,
243
+ });
244
+ if (sim.yield !== null && sim.yield > 0)
245
+ candidates.push({
246
+ action: `Enable caching with ${amt.toLocaleString()} cache reads`,
247
+ pillar: "cacheRead",
248
+ delta: `+${amt.toLocaleString()}`,
249
+ simulated_yield: sim.yield,
250
+ yield_delta: Number(
251
+ (sim.yield - (currentResult.yield ?? 0)).toFixed(2),
252
+ ),
253
+ class_after: sim.class,
254
+ rationale:
255
+ "You have no cache — enabling it unlocks the cascade. Start by reusing prior context.",
256
+ });
257
+ }
258
+ }
259
+ for (const mult of [0.9, 0.75, 0.5]) {
260
+ const newInput = Math.round(i * mult);
261
+ if (newInput < 1) continue;
262
+ const sim = cascade({ ...pillars, input: newInput });
263
+ if (sim.yield !== null)
264
+ candidates.push({
265
+ action: `Reduce fresh input by ${Math.round((1 - mult) * 100)}%`,
266
+ pillar: "input",
267
+ delta: `-${Math.round(i * (1 - mult)).toLocaleString()}`,
268
+ simulated_yield: sim.yield,
269
+ yield_delta: Number(
270
+ (sim.yield - (currentResult.yield ?? 0)).toFixed(2),
271
+ ),
272
+ class_after: sim.class,
273
+ rationale: "Input is squared in Υ denominator — quadratic payoff.",
274
+ });
275
+ }
276
+ for (const mult of [1.25, 1.5, 2]) {
277
+ const sim = cascade({ ...pillars, output: Math.round(o * mult) });
278
+ if (sim.yield !== null)
279
+ candidates.push({
280
+ action: `Increase output by ${Math.round((mult - 1) * 100)}%`,
281
+ pillar: "output",
282
+ delta: `+${Math.round(o * (mult - 1)).toLocaleString()}`,
283
+ simulated_yield: sim.yield,
284
+ yield_delta: Number(
285
+ (sim.yield - (currentResult.yield ?? 0)).toFixed(2),
286
+ ),
287
+ class_after: sim.class,
288
+ rationale: "Output is a linear multiplier in Υ.",
289
+ });
290
+ }
291
+ candidates.sort((a, b) => b.yield_delta - a.yield_delta);
292
+ const suggestions = candidates
293
+ .slice(0, 8)
294
+ .map((c, idx) => ({ rank: idx + 1, ...c }));
295
+
296
+ // ── Step 3: Simulate the top suggestion ───────────────────────────
297
+ const best = suggestions[0];
298
+ let bestSimulation = null;
299
+ if (best) {
300
+ const simPillars = { ...pillars };
301
+ const delta = parseInt(best.delta.replace(/[+\-,]/g, ""), 10);
302
+ if (best.pillar === "cacheRead") simPillars.cacheRead = cr + delta;
303
+ else if (best.pillar === "input")
304
+ simPillars.input = Math.max(i - delta, 1);
305
+ else if (best.pillar === "output") simPillars.output = o + delta;
306
+ const simResult = cascade(simPillars);
307
+ bestSimulation = {
308
+ action: best.action,
309
+ current_yield: currentResult.yield,
310
+ simulated_yield: simResult.yield,
311
+ yield_delta: best.yield_delta,
312
+ current_class: currentResult.class,
313
+ simulated_class: simResult.class,
314
+ class_changed: currentResult.class !== simResult.class,
315
+ };
316
+ }
317
+
318
+ // ── Step 4: Cycle summary ─────────────────────────────────────────
319
+ const criticalCount = diagnosis.filter(
320
+ (d) => d.severity === "critical",
321
+ ).length;
322
+ const cycleSummary = best
323
+ ? `Pulled from ${pulledFrom}. Υ ${currentResult.yield} (${currentResult.class}). ${criticalCount} critical, ${diagnosis.length - criticalCount} other findings. Best: ${best.action} → Υ ${best.simulated_yield} (+${best.yield_delta}).`
324
+ : `Pulled from ${pulledFrom}. Υ ${currentResult.yield} (${currentResult.class}). ${diagnosis.length} findings. No improvements suggested — cascade is optimized.`;
325
+
326
+ // ── Step 5: Scope-specific analysis ───────────────────────────────
327
+ const scope = args?.scope || "daily";
328
+ const modeInfo = currentResult.mode; // { mode, confidence }
329
+ const scopeResult = {};
330
+
331
+ if (scope === "daily") {
332
+ // Daily: mode + quality score + assessment + advice
333
+ const qs = qualityScore(currentResult.yield ?? 0, modeInfo.mode);
334
+ const expected = MODE_EXPECTED_YIELD[modeInfo.mode] ?? 0;
335
+ const assessment = _dailyAssessment(
336
+ modeInfo.mode,
337
+ currentResult.yield,
338
+ qs,
339
+ expected,
340
+ );
341
+ const advice = _dailyAdvice(modeInfo.mode, currentResult.yield, qs);
342
+ scopeResult.mode = modeInfo;
343
+ scopeResult.quality_score = Math.round(qs * 100) / 100;
344
+ scopeResult.assessment = assessment;
345
+ scopeResult.advice = advice;
346
+ }
347
+
348
+ if (scope === "weekly" || scope === "trend") {
349
+ // Pull daily rows from ccusage to build weekly snapshots
350
+ const dailyRows = await _pullDailyRows(ctx.opts);
351
+ const weeklySnapshots = _compoundWeekly(dailyRows);
352
+ const modeDistribution = _modeDistribution(weeklySnapshots);
353
+ const modeWeightedYield = _modeWeightedYield(weeklySnapshots);
354
+
355
+ // Compute badges
356
+ const historyForBadges = weeklySnapshots.map((w) => ({
357
+ date: w.weekStart,
358
+ mode: w.mode,
359
+ yield: w.yield,
360
+ pillars: w.pillars,
361
+ }));
362
+ const badges = computeBadges({
363
+ pillars,
364
+ cascade: currentResult,
365
+ history: historyForBadges,
366
+ isVerified: false, // set by submit_verified
367
+ rank: null, // server-side
368
+ });
369
+
370
+ if (scope === "weekly") {
371
+ scopeResult.mode = modeInfo;
372
+ // Strip dailyModes from weekly_snapshots — privacy boundary (daily modes never leave the machine)
373
+ const safeSnapshots = weeklySnapshots.map(
374
+ ({ dailyModes, ...rest }) => rest,
375
+ );
376
+ scopeResult.report = {
377
+ current_mode: modeInfo.mode,
378
+ mode_confidence: modeInfo.confidence,
379
+ mode_distribution: modeDistribution,
380
+ mode_weighted_yield: modeWeightedYield,
381
+ peak_yield: _peakYield(weeklySnapshots),
382
+ health_score: _healthScore(weeklySnapshots, modeWeightedYield),
383
+ weekly_snapshots: safeSnapshots,
384
+ badges,
385
+ };
386
+ }
387
+
388
+ if (scope === "trend") {
389
+ const yield7d = _yieldForDays(weeklySnapshots, 7);
390
+ const yield30d = _yieldForDays(weeklySnapshots, 30);
391
+ const yield90d = _yieldForDays(weeklySnapshots, 90);
392
+ const trajectory = _trajectory(yield7d, yield30d, yield90d);
393
+ scopeResult.mode = modeInfo;
394
+ scopeResult.trend = {
395
+ yield_7d: yield7d,
396
+ yield_30d: yield30d,
397
+ yield_90d: yield90d,
398
+ trajectory,
399
+ mode_distribution: modeDistribution,
400
+ phase_pattern: _phasePattern(weeklySnapshots),
401
+ };
402
+ }
403
+ }
404
+
405
+ return {
406
+ pillars,
407
+ current_cascade: {
408
+ yield_: currentResult.yield,
409
+ snr: currentResult.snr,
410
+ leverage: currentResult.leverage,
411
+ velocity: currentResult.velocity,
412
+ tenx_dev: currentResult.dev10x,
413
+ class: currentResult.class,
414
+ mode: modeInfo,
415
+ warnings: currentResult.warnings,
416
+ },
417
+ diagnosis,
418
+ suggestions,
419
+ best_simulation: bestSimulation,
420
+ cycle_summary: cycleSummary,
421
+ ...scopeResult,
422
+ };
423
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * tools/simulate-change.mjs — simulate_change tool.
3
+ */
4
+
5
+ import { cascade, parsePillars } from "../analytics/cascade.mjs";
6
+ import { SIMULATE_OUTPUT, ANNOTATIONS } from "./_schemas.mjs";
7
+ import { MAX_INPUT, withParseWarnings } from "./_helpers.mjs";
8
+
9
+ export const TOOL_DEF = {
10
+ name: "simulate_change",
11
+ description:
12
+ "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}).",
13
+ annotations: {
14
+ title: "Simulate metric change",
15
+ ...ANNOTATIONS.readOnlyHint,
16
+ ...ANNOTATIONS.idempotentHint,
17
+ ...ANNOTATIONS.openWorldHint,
18
+ },
19
+ inputSchema: {
20
+ type: "object",
21
+ properties: {
22
+ text: {
23
+ type: "string",
24
+ description:
25
+ 'Current token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
26
+ },
27
+ changes: {
28
+ type: "object",
29
+ description:
30
+ "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.",
31
+ properties: {
32
+ input: {
33
+ type: ["number", "string"],
34
+ description:
35
+ 'new input token count (absolute) or "+/-N" for a relative delta',
36
+ },
37
+ output: {
38
+ type: ["number", "string"],
39
+ description:
40
+ 'new output token count (absolute) or "+/-N" for a relative delta',
41
+ },
42
+ cacheCreate: {
43
+ type: ["number", "string"],
44
+ description:
45
+ 'new cache-create token count (absolute) or "+/-N" for a relative delta',
46
+ },
47
+ cacheRead: {
48
+ type: ["number", "string"],
49
+ description:
50
+ 'new cache-read token count (absolute) or "+/-N" for a relative delta',
51
+ },
52
+ },
53
+ },
54
+ },
55
+ required: ["text", "changes"],
56
+ },
57
+ outputSchema: SIMULATE_OUTPUT,
58
+ };
59
+
60
+ export async function handleSimulateChange(args) {
61
+ // The first prescriptive tool — "what if I changed my token mix?"
62
+ // Pure local math: parse current pillars, apply proposed changes, run the
63
+ // cascade on both, return the delta. No network, no submission.
64
+ if (!args?.text)
65
+ throw new Error(
66
+ "simulate_change requires a non-empty `text` argument (current pillars).",
67
+ );
68
+ if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
69
+ return {
70
+ status: "error",
71
+ reason: "input_too_large",
72
+ detail: `text exceeds ${MAX_INPUT} chars.`,
73
+ };
74
+ }
75
+ if (!args?.changes || typeof args.changes !== "object") {
76
+ throw new Error(
77
+ "simulate_change requires a `changes` object with at least one pillar change.",
78
+ );
79
+ }
80
+
81
+ const currentPillars = parsePillars(args.text);
82
+ const current = withParseWarnings(currentPillars, cascade(currentPillars));
83
+
84
+ // Apply changes: each pillar is either an absolute number (replace) or a
85
+ // string starting with +/- (relative delta). Omitted pillars are unchanged.
86
+ const PILLAR_KEYS = ["input", "output", "cacheCreate", "cacheRead"];
87
+ const simulated = { ...currentPillars };
88
+ const appliedChanges = {};
89
+
90
+ for (const key of PILLAR_KEYS) {
91
+ if (args.changes[key] == null) continue;
92
+ const raw = args.changes[key];
93
+ let newVal;
94
+
95
+ if (typeof raw === "number") {
96
+ newVal = raw;
97
+ } else if (typeof raw === "string") {
98
+ const trimmed = raw.trim();
99
+ if (trimmed.startsWith("+") || trimmed.startsWith("-")) {
100
+ // Relative delta: add/subtract from current value
101
+ const delta = Number(trimmed);
102
+ if (!Number.isFinite(delta)) {
103
+ return {
104
+ status: "error",
105
+ reason: "invalid_change",
106
+ detail: `changes.${key}: "${raw}" is not a valid relative delta.`,
107
+ };
108
+ }
109
+ newVal = currentPillars[key] + delta;
110
+ } else {
111
+ // Absolute value as a string
112
+ newVal = Number(trimmed);
113
+ }
114
+ } else {
115
+ return {
116
+ status: "error",
117
+ reason: "invalid_change",
118
+ detail: `changes.${key}: expected number or string, got ${typeof raw}.`,
119
+ };
120
+ }
121
+
122
+ if (!Number.isFinite(newVal)) {
123
+ return {
124
+ status: "error",
125
+ reason: "invalid_change",
126
+ detail: `changes.${key}: result is not a finite number.`,
127
+ };
128
+ }
129
+ // Clamp to non-negative — token counts can't be negative
130
+ if (newVal < 0) {
131
+ return {
132
+ status: "error",
133
+ reason: "invalid_change",
134
+ detail: `changes.${key}: result ${newVal} is negative — token counts must be >= 0.`,
135
+ };
136
+ }
137
+
138
+ simulated[key] = newVal;
139
+ appliedChanges[key] = {
140
+ from: currentPillars[key],
141
+ to: newVal,
142
+ delta: newVal - currentPillars[key],
143
+ };
144
+ }
145
+
146
+ if (Object.keys(appliedChanges).length === 0) {
147
+ return {
148
+ status: "error",
149
+ reason: "no_changes",
150
+ detail: "No pillar changes specified in the `changes` object.",
151
+ };
152
+ }
153
+
154
+ const simulatedResult = cascade(simulated);
155
+
156
+ // Compute deltas for every cascade metric
157
+ const metricDelta = (curr, sim) => {
158
+ if (curr == null && sim == null) return null;
159
+ if (curr == null) return { from: null, to: sim, delta: null };
160
+ if (sim == null) return { from: curr, to: null, delta: null };
161
+ return { from: curr, to: sim, delta: Number((sim - curr).toFixed(4)) };
162
+ };
163
+
164
+ const classChanged = current.class !== simulatedResult.class;
165
+
166
+ return {
167
+ current: {
168
+ pillars: currentPillars,
169
+ yield: current.yield,
170
+ snr: current.snr,
171
+ leverage: current.leverage,
172
+ velocity: current.velocity,
173
+ dev10x: current.dev10x,
174
+ class: current.class,
175
+ },
176
+ simulated: {
177
+ pillars: {
178
+ input: simulated.input,
179
+ output: simulated.output,
180
+ cacheCreate: simulated.cacheCreate,
181
+ cacheRead: simulated.cacheRead,
182
+ },
183
+ yield: simulatedResult.yield,
184
+ snr: simulatedResult.snr,
185
+ leverage: simulatedResult.leverage,
186
+ velocity: simulatedResult.velocity,
187
+ dev10x: simulatedResult.dev10x,
188
+ class: simulatedResult.class,
189
+ },
190
+ changes: appliedChanges,
191
+ deltas: {
192
+ yield: metricDelta(current.yield, simulatedResult.yield),
193
+ snr: metricDelta(current.snr, simulatedResult.snr),
194
+ leverage: metricDelta(current.leverage, simulatedResult.leverage),
195
+ velocity: metricDelta(current.velocity, simulatedResult.velocity),
196
+ dev10x: metricDelta(current.dev10x, simulatedResult.dev10x),
197
+ },
198
+ class_changed: classChanged,
199
+ ...(classChanged
200
+ ? { class_transition: `${current.class} → ${simulatedResult.class}` }
201
+ : {}),
202
+ ...(simulatedResult.warnings
203
+ ? { simulated_warnings: simulatedResult.warnings }
204
+ : {}),
205
+ note: "Local simulation only — no submission. The actual score depends on server-side RS.xx weights and class thresholds.",
206
+ };
207
+ }