sigrank 0.18.5 → 1.0.1

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 (80) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/MCP_SERVER_DESCRIPTION.md +172 -0
  3. package/README.md +94 -41
  4. package/adapters/index.mjs +1100 -0
  5. package/adapters/tokenpull.mjs +568 -0
  6. package/adapters.mjs +1 -857
  7. package/analytics/cascade.mjs +338 -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 +231 -0
  13. package/identity/sign.mjs +106 -0
  14. package/index.mjs +47 -162
  15. package/keystore.mjs +1 -242
  16. package/lib/constants.mjs +137 -0
  17. package/lib/pkg-version.mjs +59 -0
  18. package/manifest.json +46 -0
  19. package/narrate.mjs +1 -77
  20. package/package.json +41 -16
  21. package/preflight.mjs +1 -191
  22. package/presentation/cli.mjs +1926 -0
  23. package/presentation/narrate.mjs +219 -0
  24. package/presentation/tui-themes.mjs +189 -0
  25. package/presentation/tui.mjs +3316 -0
  26. package/prompts/check-my-efficiency.md +1 -0
  27. package/prompts/compare-with-leader.md +1 -0
  28. package/prompts/simulate-improvement.md +1 -0
  29. package/proxy.mjs +492 -0
  30. package/resources/class-tiers.md +81 -0
  31. package/resources/data-policy.md +31 -0
  32. package/resources/install-guide.md +59 -0
  33. package/resources/privacy-model.md +44 -0
  34. package/resources/scoring-formula.md +24 -0
  35. package/review.mjs +263 -0
  36. package/sign.mjs +1 -106
  37. package/submit/index.mjs +297 -0
  38. package/submit/preflight.mjs +191 -0
  39. package/submit.mjs +1 -303
  40. package/tokenpull.mjs +1 -576
  41. package/tokscale_analytics.mjs +864 -0
  42. package/tools/_framing.mjs +217 -0
  43. package/tools/_helpers.mjs +253 -0
  44. package/tools/_peer-matching.mjs +361 -0
  45. package/tools/_report.mjs +262 -0
  46. package/tools/_schemas.mjs +492 -0
  47. package/tools/_verifiers.mjs +148 -0
  48. package/tools/compare-operators.mjs +80 -0
  49. package/tools/compare-self.mjs +124 -0
  50. package/tools/describe-power-user.mjs +78 -0
  51. package/tools/diagnose-cascade.mjs +192 -0
  52. package/tools/discover-peers.mjs +198 -0
  53. package/tools/enroll.mjs +119 -0
  54. package/tools/get-best-operator.mjs +59 -0
  55. package/tools/get-leaderboard.mjs +27 -0
  56. package/tools/get-operator.mjs +33 -0
  57. package/tools/index.mjs +229 -0
  58. package/tools/optimize-efficiency.mjs +170 -0
  59. package/tools/rank-paste.mjs +51 -0
  60. package/tools/rank-windows.mjs +120 -0
  61. package/tools/self-improve.mjs +423 -0
  62. package/tools/simulate-change.mjs +207 -0
  63. package/tools/submit-paste.mjs +121 -0
  64. package/tools/submit-verified.mjs +153 -0
  65. package/tools/suggest-improvements.mjs +168 -0
  66. package/tools/tokenpull-compare.mjs +123 -0
  67. package/tools/tokenpull-submit.mjs +235 -0
  68. package/tools/tokenpull.mjs +113 -0
  69. package/tools/tokscale-breakdown.mjs +119 -0
  70. package/tools/tokscale-competitive-intel.mjs +45 -0
  71. package/tools/tokscale-cost-analysis.mjs +31 -0
  72. package/tools/tokscale-developer-profile.mjs +36 -0
  73. package/tools/tokscale-device-profile.mjs +35 -0
  74. package/tools/tokscale-market-share.mjs +38 -0
  75. package/tools/tokscale-mcp-usage.mjs +33 -0
  76. package/tools/tokscale-model-trends.mjs +32 -0
  77. package/tools/watch-tokenpull.mjs +134 -0
  78. package/tools.mjs +1 -3897
  79. package/tui.mjs +4 -2650
  80. package/banner.svg +0 -7
@@ -0,0 +1,361 @@
1
+ /**
2
+ * tools/_peer-matching.mjs — pure peer-discovery logic for discover_peers.
3
+ *
4
+ * Shared between _02_sigrank-mcp (operator's agent, uses enrolled identity)
5
+ * and _04_bestuser-router-mcp (marketing layer, takes codename arg). The
6
+ * canonical version lives here; _04 vendors a copy.
7
+ *
8
+ * All functions are pure — no network, no side effects. They operate on
9
+ * leaderboard entry objects as returned by the signalaf.com API:
10
+ * { codename, class_tier, platform, yield_, leverage, velocity, snr,
11
+ * input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
12
+ * rank, ... }
13
+ *
14
+ * Three categorization functions:
15
+ * findMentors — operators 1-2 class tiers above, ranked by cascade shape similarity
16
+ * findPeers — operators in the same class tier, ranked by yield proximity
17
+ * findComplementary — operators whose strength maps to the operator's weakness
18
+ *
19
+ * Plus pillar delta computation: the specific token difference that explains
20
+ * the yield gap between an operator and a mentor.
21
+ */
22
+
23
+ import { CLASS_TIERS, UNCLASSED, tierOf } from "../analytics/cascade.mjs";
24
+
25
+ /**
26
+ * Class tier rank index (0 = ARCH+ top, 7 = IGNITER bottom, -1 = UNCLASSED).
27
+ * Used to determine who is "above" an operator in the experience ladder.
28
+ */
29
+ export function tierRank(klass) {
30
+ const base = tierOf(klass);
31
+ if (base === UNCLASSED || base == null) return -1;
32
+ return CLASS_TIERS.indexOf(base);
33
+ }
34
+
35
+ /**
36
+ * Normalize a cascade shape into a 3D vector for similarity comparison.
37
+ * The three dimensions are the cascade ratios that define an operator's
38
+ * "shape" independent of scale:
39
+ * - leverage (Cr/I): how much they reuse prior context
40
+ * - velocity (O/I): how much output they get per input token
41
+ * - snr (O/(I+O)): signal cleanliness
42
+ *
43
+ * Each is log-scaled and normalized to [0,1] so operators at different scales
44
+ * can be compared by shape, not absolute magnitude. An operator with 100x
45
+ * leverage and one with 1000x leverage have similar *shapes* — both are
46
+ * high-leverage operators, just at different scales.
47
+ */
48
+ export function cascadeShape(op) {
49
+ const l = Number(op.leverage) || 0;
50
+ const v = Number(op.velocity) || 0;
51
+ const s = Number(op.snr) || 0;
52
+ // Log-scale, clamp to [0,1]. Log thresholds chosen so typical ranges map well:
53
+ // leverage: 0-1000x → log1p(0)-log1p(1000) ≈ 0-6.9
54
+ // velocity: 0-10x → log1p(0)-log1p(10) ≈ 0-2.4
55
+ // snr: 0-1 → log1p(0)-log1p(1) ≈ 0-0.69
56
+ const norm = (val, max) => Math.min(1, Math.log1p(Math.max(0, val)) / Math.log1p(max));
57
+ return [
58
+ norm(l, 1000),
59
+ norm(v, 10),
60
+ norm(s, 1),
61
+ ];
62
+ }
63
+
64
+ /**
65
+ * Euclidean distance between two cascade shapes, normalized to [0,1].
66
+ * 0 = identical shape, 1 = maximally different. Used to rank mentors:
67
+ * closer shape = "they were where you are but scaled up."
68
+ */
69
+ export function shapeDistance(opA, opB) {
70
+ const a = cascadeShape(opA);
71
+ const b = cascadeShape(opB);
72
+ let sum = 0;
73
+ for (let i = 0; i < a.length; i++) {
74
+ const d = a[i] - b[i];
75
+ sum += d * d;
76
+ }
77
+ return Math.sqrt(sum / a.length);
78
+ }
79
+
80
+ /**
81
+ * Similarity score = 1 - distance. Higher = more similar cascade shape.
82
+ */
83
+ export function shapeSimilarity(opA, opB) {
84
+ return 1 - shapeDistance(opA, opB);
85
+ }
86
+
87
+ /**
88
+ * Compute the pillar delta between an operator and a mentor.
89
+ * Returns the specific token differences that explain the yield gap,
90
+ * expressed as both absolute tokens and multipliers.
91
+ *
92
+ * { pillar, mentor_value, operator_value, multiplier, absolute_delta, explanation }
93
+ *
94
+ * The pillar with the largest yield impact is listed first. Since yield =
95
+ * (cacheRead × output) / input², the impact ranking is:
96
+ * 1. cache_read (linear multiplier in yield)
97
+ * 2. input (quadratic denominator — reducing input has squared impact)
98
+ * 3. output (linear multiplier in yield)
99
+ * 4. cache_create (enables future cache reads — indirect)
100
+ */
101
+ export function pillarDelta(operator, mentor) {
102
+ const opI = Number(operator.input_tokens) || 0;
103
+ const opO = Number(operator.output_tokens) || 0;
104
+ const opCr = Number(operator.cache_read_tokens) || 0;
105
+ const opCw = Number(operator.cache_creation_tokens) || 0;
106
+
107
+ const mtI = Number(mentor.input_tokens) || 0;
108
+ const mtO = Number(mentor.output_tokens) || 0;
109
+ const mtCr = Number(mentor.cache_read_tokens) || 0;
110
+ const mtCw = Number(mentor.cache_creation_tokens) || 0;
111
+
112
+ const safeMult = (mt, op) => (op > 0 ? mt / op : mt > 0 ? Infinity : 1);
113
+ const fmtMult = (m) => (m === Infinity ? "∞" : `${m.toFixed(1)}×`);
114
+
115
+ const deltas = [
116
+ {
117
+ pillar: "cache_read",
118
+ mentor_value: mtCr,
119
+ operator_value: opCr,
120
+ multiplier: safeMult(mtCr, opCr),
121
+ absolute_delta: mtCr - opCr,
122
+ explanation:
123
+ opCr > 0
124
+ ? `${fmtMult(safeMult(mtCr, opCr))} your cache reads — the strongest yield multiplier`
125
+ : `You have no cache reads; mentor has ${mtCr.toLocaleString()} — this is the entire yield gap`,
126
+ },
127
+ {
128
+ pillar: "input",
129
+ mentor_value: mtI,
130
+ operator_value: opI,
131
+ multiplier: safeMult(mtI, opI),
132
+ absolute_delta: mtI - opI,
133
+ explanation:
134
+ mtI < opI
135
+ ? `${fmtMult(opI / mtI)} less input than you — input² is in the denominator, so leaner input has quadratic payoff`
136
+ : `${fmtMult(safeMult(mtI, opI))} your input — they use more fresh tokens but compensate with cache reuse`,
137
+ },
138
+ {
139
+ pillar: "output",
140
+ mentor_value: mtO,
141
+ operator_value: opO,
142
+ multiplier: safeMult(mtO, opO),
143
+ absolute_delta: mtO - opO,
144
+ explanation: `${fmtMult(safeMult(mtO, opO))} your output — linear yield multiplier`,
145
+ },
146
+ {
147
+ pillar: "cache_create",
148
+ mentor_value: mtCw,
149
+ operator_value: opCw,
150
+ multiplier: safeMult(mtCw, opCw),
151
+ absolute_delta: mtCw - opCw,
152
+ explanation:
153
+ opCw > 0
154
+ ? `${fmtMult(safeMult(mtCw, opCw))} your cache writes — enables future cache reads`
155
+ : `You have no cache writes; mentor has ${mtCw.toLocaleString()} — they're investing in future cache`,
156
+ },
157
+ ];
158
+
159
+ // Sort by yield impact: cache_read first (strongest), then input (quadratic),
160
+ // then output (linear), then cache_create (indirect).
161
+ const order = { cache_read: 0, input: 1, output: 2, cache_create: 3 };
162
+ deltas.sort((a, b) => order[a.pillar] - order[b.pillar]);
163
+
164
+ return deltas;
165
+ }
166
+
167
+ /**
168
+ * Find mentors: operators 1-2 class tiers above, ranked by cascade shape
169
+ * similarity. These are operators who were where you are and broke through.
170
+ *
171
+ * @param {object} operator — the operator's leaderboard entry
172
+ * @param {array} board — all leaderboard entries
173
+ * @param {object} opts — { platform: string|null, n: number }
174
+ * @returns {array} mentors sorted by similarity (descending)
175
+ */
176
+ export function findMentors(operator, board, opts = {}) {
177
+ const n = Math.min(20, Math.max(1, opts.n ?? 5));
178
+ const myTier = tierRank(operator.class_tier || operator.class);
179
+ if (myTier < 0) return []; // UNCLASSED — no mentors
180
+
181
+ const myPlatform = operator.platform;
182
+
183
+ return board
184
+ .filter((op) => op.codename !== operator.codename)
185
+ .filter((op) => {
186
+ if (opts.platform && op.platform !== opts.platform) return false;
187
+ if (!opts.platform && myPlatform && op.platform !== myPlatform) return false;
188
+ return true;
189
+ })
190
+ .filter((op) => {
191
+ const theirTier = tierRank(op.class_tier || op.class);
192
+ // 1-2 tiers above (lower index = higher tier)
193
+ return theirTier >= 0 && theirTier < myTier && theirTier >= myTier - 2;
194
+ })
195
+ .map((op) => ({
196
+ ...op,
197
+ similarity_score: Math.round(shapeSimilarity(operator, op) * 100) / 100,
198
+ pillar_delta: pillarDelta(operator, op),
199
+ }))
200
+ .sort((a, b) => b.similarity_score - a.similarity_score)
201
+ .slice(0, n);
202
+ }
203
+
204
+ /**
205
+ * Find peers: operators in the same class tier (same base tier, any sub-stage),
206
+ * ranked by yield proximity. These are your direct competitors.
207
+ *
208
+ * @param {object} operator
209
+ * @param {array} board
210
+ * @param {object} opts
211
+ * @returns {array} peers sorted by yield proximity (ascending delta)
212
+ */
213
+ export function findPeers(operator, board, opts = {}) {
214
+ const n = Math.min(20, Math.max(1, opts.n ?? 5));
215
+ const myTier = tierOf(operator.class_tier || operator.class);
216
+ if (myTier === UNCLASSED || myTier == null) return [];
217
+
218
+ const myPlatform = operator.platform;
219
+ const myYield = Number(operator.yield_) || 0;
220
+
221
+ return board
222
+ .filter((op) => op.codename !== operator.codename)
223
+ .filter((op) => {
224
+ if (opts.platform && op.platform !== opts.platform) return false;
225
+ if (!opts.platform && myPlatform && op.platform !== myPlatform) return false;
226
+ return true;
227
+ })
228
+ .filter((op) => tierOf(op.class_tier || op.class) === myTier)
229
+ .map((op) => ({
230
+ codename: op.codename,
231
+ class_tier: op.class_tier || op.class,
232
+ yield_: op.yield_,
233
+ leverage: op.leverage,
234
+ velocity: op.velocity,
235
+ platform: op.platform,
236
+ rank: op.rank,
237
+ yield_delta_from_you: Math.round((Number(op.yield_) || 0) - myYield),
238
+ shareable_url: op.codename
239
+ ? `${opts.apiBase || "https://signalaf.com"}/operator/${encodeURIComponent(op.codename)}`
240
+ : null,
241
+ }))
242
+ .sort((a, b) => Math.abs(a.yield_delta_from_you) - Math.abs(b.yield_delta_from_you))
243
+ .slice(0, n);
244
+ }
245
+
246
+ /**
247
+ * Find complementary operators: those whose strongest dimension is the
248
+ * operator's weakest. Surfaces operators whose strengths map to your gaps.
249
+ *
250
+ * Determines the operator's weakest cascade dimension (leverage, velocity, or
251
+ * snr) relative to the board, then finds operators who are strongest in that
252
+ * dimension.
253
+ *
254
+ * @param {object} operator
255
+ * @param {array} board
256
+ * @param {object} opts
257
+ * @returns {array} complementary operators with strength/weakness annotation
258
+ */
259
+ export function findComplementary(operator, board, opts = {}) {
260
+ const n = Math.min(20, Math.max(1, opts.n ?? 5));
261
+ const myPlatform = operator.platform;
262
+
263
+ const filtered = board
264
+ .filter((op) => op.codename !== operator.codename)
265
+ .filter((op) => {
266
+ if (opts.platform && op.platform !== opts.platform) return false;
267
+ if (!opts.platform && myPlatform && op.platform !== myPlatform) return false;
268
+ return true;
269
+ });
270
+
271
+ if (filtered.length === 0) return [];
272
+
273
+ // Compute board averages for each dimension
274
+ const avg = (key) =>
275
+ filtered.reduce((s, op) => s + (Number(op[key]) || 0), 0) / filtered.length;
276
+
277
+ const avgLeverage = avg("leverage");
278
+ const avgVelocity = avg("velocity");
279
+ const avgSnr = avg("snr");
280
+
281
+ // Find the operator's weakest dimension (furthest below average)
282
+ const myLeverage = Number(operator.leverage) || 0;
283
+ const myVelocity = Number(operator.velocity) || 0;
284
+ const mySnr = Number(operator.snr) || 0;
285
+
286
+ const weaknesses = [
287
+ { dimension: "leverage", value: myLeverage, avg: avgLeverage, gap: avgLeverage - myLeverage },
288
+ { dimension: "velocity", value: myVelocity, avg: avgVelocity, gap: avgVelocity - myVelocity },
289
+ { dimension: "snr", value: mySnr, avg: avgSnr, gap: avgSnr - mySnr },
290
+ ].sort((a, b) => b.gap - a.gap); // largest gap first
291
+
292
+ const weakest = weaknesses[0];
293
+
294
+ // If the operator is above average in all dimensions, find the dimension
295
+ // where they're closest to average (their "relatively weakest")
296
+ const targetDimension = weakest.gap > 0 ? weakest.dimension : weaknesses[0].dimension;
297
+
298
+ // Find operators strongest in that dimension
299
+ return filtered
300
+ .map((op) => ({
301
+ codename: op.codename,
302
+ class_tier: op.class_tier || op.class,
303
+ yield_: op.yield_,
304
+ platform: op.platform,
305
+ rank: op.rank,
306
+ strength_dimension: targetDimension,
307
+ strength_value: Number(op[targetDimension]) || 0,
308
+ your_weakness: {
309
+ dimension: targetDimension,
310
+ your_value: Number(operator[targetDimension]) || 0,
311
+ board_average: Math.round(weakest.avg * 100) / 100,
312
+ },
313
+ shareable_url: op.codename
314
+ ? `${opts.apiBase || "https://signalaf.com"}/operator/${encodeURIComponent(op.codename)}`
315
+ : null,
316
+ }))
317
+ .sort((a, b) => b.strength_value - a.strength_value)
318
+ .slice(0, n);
319
+ }
320
+
321
+ /**
322
+ * Generate the summary line for discover_peers output.
323
+ */
324
+ export function peerSummary(operator, mentors, peers, complementary) {
325
+ const klass = operator.class_tier || operator.class || UNCLASSED;
326
+ const platform = operator.platform || "all";
327
+ const parts = [];
328
+
329
+ parts.push(`You're ${klass} on ${platform}.`);
330
+
331
+ if (mentors.length > 0) {
332
+ const topMentor = mentors[0];
333
+ const topDelta = topMentor.pillar_delta[0];
334
+ parts.push(
335
+ `${mentors.length} mentor${mentors.length > 1 ? "s" : ""} found — operators whose cascade shape is closest to yours but 1-2 tiers higher.`,
336
+ );
337
+ if (topDelta) {
338
+ parts.push(`Key gap: ${topDelta.explanation.toLowerCase()}.`);
339
+ }
340
+ } else {
341
+ const myTier = tierRank(klass);
342
+ if (myTier === 0) {
343
+ parts.push("You're at the top tier (ARCH+) — you are the mentor.");
344
+ } else {
345
+ parts.push("No mentors found in the 1-2 tier range above you.");
346
+ }
347
+ }
348
+
349
+ if (peers.length > 0) {
350
+ parts.push(`${peers.length} direct peer${peers.length > 1 ? "s" : ""} in the ${tierOf(klass)} tier.`);
351
+ }
352
+
353
+ if (complementary.length > 0) {
354
+ const comp = complementary[0];
355
+ parts.push(
356
+ `Study ${comp.codename} for ${comp.strength_dimension} (${comp.strength_value.toFixed(1)} vs your ${(Number(operator[comp.strength_dimension]) || 0).toFixed(1)}).`,
357
+ );
358
+ }
359
+
360
+ return parts.join(" ");
361
+ }
@@ -0,0 +1,262 @@
1
+ /**
2
+ * tools/_report.mjs — cascade report blocks + self-improvement scope helpers.
3
+ *
4
+ * Used by submit_verified (per-window report payload) and self_improve
5
+ * (weekly/trend scope analysis).
6
+ */
7
+
8
+ import { cascade, detectMode, qualityScore, MODE_EXPECTED_YIELD } from "../analytics/cascade.mjs";
9
+ import { computeBadges } from "../badges.mjs";
10
+ import { execFileAsync } from "./_helpers.mjs";
11
+
12
+ /**
13
+ * Compute the cascade report block for a submission payload.
14
+ * Pure math — mode detection + badges + health score from the current pillars.
15
+ * The server stores this as-is (does NOT recompute modes).
16
+ * Weekly granularity is the privacy boundary — no daily modes in the report.
17
+ */
18
+ export function _computeReportBlock(pillars) {
19
+ const cas = cascade(pillars);
20
+ const modeInfo = detectMode(pillars);
21
+ const badges = computeBadges({
22
+ pillars,
23
+ cascade: cas,
24
+ history: [], // no history available at submit time — badges computed from current pillars only
25
+ isVerified: true, // submit_verified is the signed agent path
26
+ rank: null,
27
+ });
28
+ return {
29
+ current_mode: modeInfo.mode,
30
+ mode_confidence: modeInfo.confidence,
31
+ mode_distribution: { [modeInfo.mode]: 1.0 }, // single-window submission
32
+ mode_weighted_yield: cas.yield ?? 0,
33
+ peak_yield: cas.yield ?? 0,
34
+ health_score: _healthScore(
35
+ [{ mode: modeInfo.mode, yield: cas.yield ?? 0 }],
36
+ cas.yield ?? 0,
37
+ ),
38
+ badges,
39
+ };
40
+ }
41
+
42
+ /** Pull daily rows from ccusage to build mode history. Returns array of { date, pillars, yield, mode }. */
43
+ export async function _pullDailyRows(opts = {}) {
44
+ try {
45
+ const raw = await execFileAsync(
46
+ "ccusage",
47
+ ["claude", "daily", "--json"],
48
+ 15000,
49
+ );
50
+ const rows = JSON.parse(raw)?.daily ?? JSON.parse(raw);
51
+ if (!Array.isArray(rows)) return [];
52
+ return rows
53
+ .map((r) => {
54
+ const pillars = {
55
+ input: r.inputTokens ?? r.input_tokens ?? 0,
56
+ output: r.outputTokens ?? r.output_tokens ?? 0,
57
+ cacheCreate: r.cacheCreationTokens ?? r.cache_create_tokens ?? 0,
58
+ cacheRead: r.cacheReadTokens ?? r.cache_read_tokens ?? 0,
59
+ };
60
+ const cas = cascade(pillars);
61
+ const mode = detectMode(pillars);
62
+ return {
63
+ date: r.date ?? r.day ?? null,
64
+ pillars,
65
+ yield: cas.yield ?? 0,
66
+ mode: mode.mode,
67
+ mode_confidence: mode.confidence,
68
+ };
69
+ })
70
+ .filter((r) => r.date);
71
+ } catch {
72
+ return [];
73
+ }
74
+ }
75
+
76
+ /** Compound daily rows into weekly snapshots. Each week = 7 days, starting Monday. */
77
+ export function _compoundWeekly(dailyRows) {
78
+ if (!dailyRows || dailyRows.length === 0) return [];
79
+ // Group by ISO week (week starts Monday)
80
+ const weeks = new Map();
81
+ for (const row of dailyRows) {
82
+ const d = new Date(row.date);
83
+ const day = d.getDay() || 7; // Sunday=0 → 7
84
+ const monday = new Date(d);
85
+ monday.setDate(d.getDate() - day + 1);
86
+ const weekKey = monday.toISOString().slice(0, 10);
87
+ if (!weeks.has(weekKey)) {
88
+ weeks.set(weekKey, { weekStart: weekKey, days: [] });
89
+ }
90
+ weeks.get(weekKey).days.push(row);
91
+ }
92
+ // Compound each week
93
+ return Array.from(weeks.values())
94
+ .map((w) => {
95
+ const pillars = w.days.reduce(
96
+ (acc, d) => ({
97
+ input: acc.input + d.pillars.input,
98
+ output: acc.output + d.pillars.output,
99
+ cacheCreate: acc.cacheCreate + d.pillars.cacheCreate,
100
+ cacheRead: acc.cacheRead + d.pillars.cacheRead,
101
+ }),
102
+ { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 },
103
+ );
104
+ const cas = cascade(pillars);
105
+ const mode = detectMode(pillars);
106
+ return {
107
+ weekStart: w.weekStart,
108
+ pillars,
109
+ yield: cas.yield ?? 0,
110
+ mode: mode.mode,
111
+ mode_confidence: mode.confidence,
112
+ dayCount: w.days.length,
113
+ dailyModes: w.days.map((d) => d.mode), // stays local — not in submitted report
114
+ };
115
+ })
116
+ .sort((a, b) => a.weekStart.localeCompare(b.weekStart));
117
+ }
118
+
119
+ /** Compute mode distribution from weekly snapshots (weekly granularity = privacy boundary). */
120
+ export function _modeDistribution(weeklySnapshots) {
121
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return {};
122
+ const counts = { BUILD: 0, EDIT: 0, DEBUG: 0, MAINTAIN: 0, IDLE: 0 };
123
+ for (const w of weeklySnapshots) {
124
+ counts[w.mode] = (counts[w.mode] || 0) + 1;
125
+ }
126
+ const total = weeklySnapshots.length;
127
+ const dist = {};
128
+ for (const [mode, count] of Object.entries(counts)) {
129
+ if (count > 0) dist[mode] = Math.round((count / total) * 100) / 100;
130
+ }
131
+ return dist;
132
+ }
133
+
134
+ /** Mode-weighted yield — average yield weighted by mode distribution. */
135
+ export function _modeWeightedYield(weeklySnapshots) {
136
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
137
+ const total = weeklySnapshots.length;
138
+ const sum = weeklySnapshots.reduce((acc, w) => acc + (w.yield || 0), 0);
139
+ return Math.round(sum / total);
140
+ }
141
+
142
+ /** Peak yield across all weekly snapshots. */
143
+ export function _peakYield(weeklySnapshots) {
144
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
145
+ return Math.max(...weeklySnapshots.map((w) => w.yield || 0));
146
+ }
147
+
148
+ /** Health score (0-100) — weighted composite of consistency, momentum, quality. */
149
+ export function _healthScore(weeklySnapshots, modeWeightedYield) {
150
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
151
+ // Simplified Phase 1 formula:
152
+ // 40% — consistency (how often in MAINTAIN)
153
+ // 30% — momentum (recent yield vs older yield)
154
+ // 30% — quality (mode-weighted yield relative to MAINTAIN expected)
155
+ const dist = _modeDistribution(weeklySnapshots);
156
+ const maintainShare = dist.MAINTAIN || 0;
157
+ const consistency = Math.min(maintainShare, 1) * 40;
158
+
159
+ // Momentum: compare last 3 weeks to first 3 weeks
160
+ const recent = weeklySnapshots.slice(-3);
161
+ const older = weeklySnapshots.slice(0, 3);
162
+ const recentAvg =
163
+ recent.reduce((a, w) => a + (w.yield || 0), 0) / (recent.length || 1);
164
+ const olderAvg =
165
+ older.reduce((a, w) => a + (w.yield || 0), 0) / (older.length || 1);
166
+ const momentumRatio = olderAvg > 0 ? recentAvg / olderAvg : 1;
167
+ const momentum = (Math.min(momentumRatio, 2) / 2) * 30;
168
+
169
+ // Quality: mode-weighted yield vs MAINTAIN expected (5000)
170
+ const quality = Math.min(modeWeightedYield / 5000, 1) * 30;
171
+
172
+ return Math.round(consistency + momentum + quality);
173
+ }
174
+
175
+ /** Yield for the last N days (from weekly snapshots — approximates by summing recent weeks). */
176
+ export function _yieldForDays(weeklySnapshots, days) {
177
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
178
+ const weeksNeeded = Math.ceil(days / 7);
179
+ const recent = weeklySnapshots.slice(-weeksNeeded);
180
+ if (recent.length === 0) return 0;
181
+ const sum = recent.reduce((a, w) => a + (w.yield || 0), 0);
182
+ return Math.round(sum / recent.length); // average weekly yield
183
+ }
184
+
185
+ /** Trajectory description from 7d/30d/90d yields. */
186
+ export function _trajectory(y7, y30, y90) {
187
+ if (y90 === 0) return "insufficient_data";
188
+ const r7v30 = y30 > 0 ? y7 / y30 : 1;
189
+ const r30v90 = y90 > 0 ? y30 / y90 : 1;
190
+ if (r7v30 > 1.2 && r30v90 > 1.0) return "accelerating";
191
+ if (r7v30 > 1.2) return "recent_surge";
192
+ if (r7v30 < 0.8 && r30v90 < 0.8) return "declining";
193
+ if (r7v30 < 0.8) return "recent_dip";
194
+ if (r30v90 > 1.1) return "steady_growth";
195
+ return "stable";
196
+ }
197
+
198
+ /** Phase pattern description from mode distribution over time. */
199
+ export function _phasePattern(weeklySnapshots) {
200
+ if (!weeklySnapshots || weeklySnapshots.length < 2)
201
+ return "insufficient_data";
202
+ const modes = weeklySnapshots.map((w) => w.mode);
203
+ const transitions = [];
204
+ for (let i = 1; i < modes.length; i++) {
205
+ if (modes[i] !== modes[i - 1]) {
206
+ transitions.push(`${modes[i - 1]}→${modes[i]}`);
207
+ }
208
+ }
209
+ if (transitions.length === 0) return `consistent_${modes[0].toLowerCase()}`;
210
+ // Check for smooth transitions (BUILD→MAINTAIN without DEBUG)
211
+ const smooth = transitions.every((t) => !t.includes("DEBUG"));
212
+ if (smooth && transitions.some((t) => t === "BUILD→MAINTAIN"))
213
+ return "smooth_ramp";
214
+ if (transitions.filter((t) => t.includes("DEBUG")).length > 2)
215
+ return "erratic";
216
+ return "cyclical";
217
+ }
218
+
219
+ /** Daily assessment string. */
220
+ export function _dailyAssessment(mode, yieldVal, qualityScoreVal, expectedYield) {
221
+ const qsPct = Math.round(qualityScoreVal * 100);
222
+ if (mode === "IDLE")
223
+ return "You're idle — no significant token activity today.";
224
+ if (mode === "MAINTAIN") {
225
+ if (qualityScoreVal >= 0.5)
226
+ return `You're in MAINTAIN mode. Yield ${yieldVal} is ${qsPct}% of MAINTAIN norm. The cascade is compounding.`;
227
+ return `You're in MAINTAIN mode but yield ${yieldVal} is only ${qsPct}% of expected (${expectedYield}). You may be coasting — push for more output.`;
228
+ }
229
+ if (mode === "BUILD") {
230
+ if (qualityScoreVal >= 0.5)
231
+ return `You're in BUILD mode. Yield ${yieldVal} is ${qsPct}% of BUILD norm. Greenfield work — expected to be low.`;
232
+ return `You're in BUILD mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Building is slow — keep going.`;
233
+ }
234
+ if (mode === "EDIT") {
235
+ if (qualityScoreVal >= 0.5)
236
+ return `You're in EDIT mode. Yield ${yieldVal} is ${qsPct}% of EDIT norm. Fresh input but producing — good.`;
237
+ return `You're in EDIT mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Push for more output per input.`;
238
+ }
239
+ if (mode === "DEBUG") {
240
+ if (qualityScoreVal >= 0.5)
241
+ return `You're in DEBUG mode. Yield ${yieldVal} is ${qsPct}% of DEBUG norm. Investigating — yield is expected to be low.`;
242
+ return `You're in DEBUG mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Debugging is slow — consider loading prior context.`;
243
+ }
244
+ return `Mode: ${mode}. Yield: ${yieldVal}. Quality: ${qsPct}%.`;
245
+ }
246
+
247
+ /** Daily advice string. */
248
+ export function _dailyAdvice(mode, yieldVal, qs) {
249
+ if (mode === "MAINTAIN" && qs >= 0.5)
250
+ return "Keep the cascade going. Don't reset context — let it compound.";
251
+ if (mode === "MAINTAIN")
252
+ return "You're in MAINTAIN but underperforming. Push the agent for more output per turn.";
253
+ if (mode === "BUILD")
254
+ return "When you're done building, load prior context to transition to MAINTAIN. The cascade rewards reuse.";
255
+ if (mode === "EDIT")
256
+ return "You're producing but using fresh input. Try to reuse prior context to boost leverage.";
257
+ if (mode === "DEBUG")
258
+ return "When you're done debugging, load prior context to return to MAINTAIN. Don't let the debug phase drag on.";
259
+ if (mode === "IDLE")
260
+ return "No activity detected. Start a session to build your cascade.";
261
+ return "Keep working — the cascade will compound as you build context.";
262
+ }