sigrank 0.15.5 → 0.16.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.
package/badges.mjs ADDED
@@ -0,0 +1,264 @@
1
+ /**
2
+ * badges.mjs — pure badge computation for the Cascade Report System.
3
+ *
4
+ * 8 launch badges. Pure functions — take history + current pillars, return
5
+ * badge collection + progress. No deps, no transport, no network.
6
+ *
7
+ * Badge types:
8
+ * Permanent — once earned, always earned (First Spark, Cascade Engine, etc.)
9
+ * Current — must be maintained (Cascade Streak, Top 10)
10
+ *
11
+ * Badge states:
12
+ * earned — condition met, badge is in the collection
13
+ * in_progress — condition not yet met, but progress is trackable
14
+ * locked — condition not met, no progress to show (not returned)
15
+ */
16
+
17
+ /**
18
+ * Compute all 8 launch badges.
19
+ *
20
+ * @param {object} params
21
+ * @param {object} params.pillars — current { input, output, cacheCreate, cacheRead }
22
+ * @param {object} params.cascade — current cascade result (from cascade())
23
+ * @param {array} params.history — array of daily snapshots { date, mode, yield, pillars }
24
+ * @param {boolean} params.isVerified — whether the operator submitted via signed agent
25
+ * @param {number|null} params.rank — current leaderboard rank (null if not ranked)
26
+ * @returns {object} { earned_this_week, in_progress, collection }
27
+ */
28
+ export function computeBadges({
29
+ pillars,
30
+ cascade: cas,
31
+ history = [],
32
+ isVerified = false,
33
+ rank = null,
34
+ }) {
35
+ const cr = Number(pillars?.cacheRead || 0);
36
+ const i = Number(pillars?.input || 0);
37
+ const leverage = cas?.leverage ?? (i > 0 ? cr / i : 0);
38
+
39
+ const earned = [];
40
+ const inProgress = [];
41
+ const collection = [];
42
+
43
+ // 1. First Spark — cacheRead > input (1× leverage)
44
+ if (leverage > 1) {
45
+ earned.push({
46
+ id: "first_spark",
47
+ label: "First Spark",
48
+ icon: "◈",
49
+ condition: "1× cache leverage",
50
+ date_earned: _latestDate(history),
51
+ });
52
+ collection.push("first_spark");
53
+ } else if (cr > 0) {
54
+ inProgress.push({
55
+ id: "first_spark",
56
+ label: "First Spark",
57
+ icon: "◈",
58
+ progress: Math.min(leverage, 1),
59
+ target: 1,
60
+ display: `${leverage.toFixed(1)}× / 1×`,
61
+ });
62
+ }
63
+
64
+ // 2. Cascade Engine — cacheRead > 10× input
65
+ if (leverage > 10) {
66
+ earned.push({
67
+ id: "cascade_engine",
68
+ label: "Cascade Engine",
69
+ icon: "⚡",
70
+ condition: "10× cache leverage",
71
+ date_earned: _latestDate(history),
72
+ });
73
+ collection.push("cascade_engine");
74
+ } else if (leverage > 1) {
75
+ inProgress.push({
76
+ id: "cascade_engine",
77
+ label: "Cascade Engine",
78
+ icon: "⚡",
79
+ progress: Math.min(leverage, 10),
80
+ target: 10,
81
+ display: `${leverage.toFixed(1)}× / 10×`,
82
+ });
83
+ }
84
+
85
+ // 3. Chain Reaction — cacheRead > 100× input
86
+ if (leverage > 100) {
87
+ earned.push({
88
+ id: "chain_reaction",
89
+ label: "Chain Reaction",
90
+ icon: "⚡",
91
+ condition: "100× cache leverage",
92
+ date_earned: _latestDate(history),
93
+ });
94
+ collection.push("chain_reaction");
95
+ } else if (leverage > 10) {
96
+ inProgress.push({
97
+ id: "chain_reaction",
98
+ label: "Chain Reaction",
99
+ icon: "⚡",
100
+ progress: Math.min(leverage, 100),
101
+ target: 100,
102
+ display: `${leverage.toFixed(1)}× / 100×`,
103
+ });
104
+ }
105
+
106
+ // 4. Foundation — 3 completed BUILD→MAINTAIN arcs
107
+ const arcs = _countBuildMaintainArcs(history);
108
+ if (arcs >= 3) {
109
+ earned.push({
110
+ id: "foundation",
111
+ label: "Foundation",
112
+ icon: "🏗️",
113
+ condition: "3 BUILD→MAINTAIN arcs",
114
+ date_earned: _latestDate(history),
115
+ });
116
+ collection.push("foundation");
117
+ } else {
118
+ inProgress.push({
119
+ id: "foundation",
120
+ label: "Foundation",
121
+ icon: "🏗️",
122
+ progress: arcs,
123
+ target: 3,
124
+ display: `${arcs}/3 arcs`,
125
+ });
126
+ }
127
+
128
+ // 5. Phoenix — returned to MAINTAIN within 1 day of disruption, 5×
129
+ const phoenixCount = _countPhoenixReturns(history);
130
+ if (phoenixCount >= 5) {
131
+ earned.push({
132
+ id: "phoenix",
133
+ label: "Phoenix",
134
+ icon: "🔥",
135
+ condition: "5× MAINTAIN recovery within 1 day",
136
+ date_earned: _latestDate(history),
137
+ });
138
+ collection.push("phoenix");
139
+ } else {
140
+ inProgress.push({
141
+ id: "phoenix",
142
+ label: "Phoenix",
143
+ icon: "🔥",
144
+ progress: phoenixCount,
145
+ target: 5,
146
+ display: `${phoenixCount}/5 recoveries`,
147
+ });
148
+ }
149
+
150
+ // 6. Verified — submitted via signed agent
151
+ if (isVerified) {
152
+ earned.push({
153
+ id: "verified",
154
+ label: "Verified",
155
+ icon: "✓",
156
+ condition: "Submitted via signed agent",
157
+ date_earned: _latestDate(history),
158
+ });
159
+ collection.push("verified");
160
+ }
161
+ // No in-progress for verified — it's binary
162
+
163
+ // 7. Cascade Streak 🔥 — 7 consecutive days MAINTAIN (current badge)
164
+ const streak = _countMaintainStreak(history);
165
+ if (streak >= 7) {
166
+ earned.push({
167
+ id: "cascade_streak",
168
+ label: "Cascade Streak",
169
+ icon: "🔥",
170
+ condition: "7 consecutive days MAINTAIN",
171
+ date_earned: _latestDate(history),
172
+ current: true,
173
+ });
174
+ collection.push("cascade_streak");
175
+ } else {
176
+ inProgress.push({
177
+ id: "cascade_streak",
178
+ label: "Cascade Streak",
179
+ icon: "🔥",
180
+ progress: streak,
181
+ target: 7,
182
+ display: `${streak}/7 days`,
183
+ });
184
+ }
185
+
186
+ // 8. Top 10 🏆 — currently in top 10 (server-side, but we include it if rank is provided)
187
+ if (rank !== null && rank <= 10) {
188
+ earned.push({
189
+ id: "top_10",
190
+ label: "Top 10",
191
+ icon: "🏆",
192
+ condition: `Currently #${rank}`,
193
+ date_earned: _latestDate(history),
194
+ current: true,
195
+ });
196
+ collection.push("top_10");
197
+ }
198
+ // No in-progress for top 10 — it's a live position, not a progress badge
199
+
200
+ return {
201
+ earned_this_week: earned.map((b) => b.id),
202
+ in_progress: inProgress,
203
+ collection,
204
+ };
205
+ }
206
+
207
+ // ── Helpers ─────────────────────────────────────────────────────────────────
208
+
209
+ function _latestDate(history) {
210
+ if (!history || history.length === 0) return null;
211
+ return history[history.length - 1]?.date || null;
212
+ }
213
+
214
+ /**
215
+ * Count completed BUILD→MAINTAIN arcs in history.
216
+ * An arc = a BUILD phase followed by a MAINTAIN phase.
217
+ */
218
+ function _countBuildMaintainArcs(history) {
219
+ if (!history || history.length < 2) return 0;
220
+ let arcs = 0;
221
+ let wasBuild = false;
222
+ for (const h of history) {
223
+ if (h.mode === "BUILD") {
224
+ wasBuild = true;
225
+ } else if (h.mode === "MAINTAIN" && wasBuild) {
226
+ arcs++;
227
+ wasBuild = false;
228
+ }
229
+ }
230
+ return arcs;
231
+ }
232
+
233
+ /**
234
+ * Count Phoenix returns — MAINTAIN within 1 day of a non-MAINTAIN day.
235
+ */
236
+ function _countPhoenixReturns(history) {
237
+ if (!history || history.length < 2) return 0;
238
+ let count = 0;
239
+ for (let idx = 1; idx < history.length; idx++) {
240
+ if (
241
+ history[idx].mode === "MAINTAIN" &&
242
+ history[idx - 1].mode !== "MAINTAIN"
243
+ ) {
244
+ count++;
245
+ }
246
+ }
247
+ return count;
248
+ }
249
+
250
+ /**
251
+ * Count current consecutive MAINTAIN days (ending at the last entry).
252
+ */
253
+ function _countMaintainStreak(history) {
254
+ if (!history || history.length === 0) return 0;
255
+ let streak = 0;
256
+ for (let idx = history.length - 1; idx >= 0; idx--) {
257
+ if (history[idx].mode === "MAINTAIN") {
258
+ streak++;
259
+ } else {
260
+ break;
261
+ }
262
+ }
263
+ return streak;
264
+ }
package/cascade.mjs CHANGED
@@ -13,40 +13,45 @@
13
13
  * Callers that require a fully-formed result should check `warnings.length === 0`.
14
14
  */
15
15
 
16
- export const round = (n, d) => (Number.isFinite(n) ? Number(n.toFixed(d)) : null)
16
+ export const round = (n, d) =>
17
+ Number.isFinite(n) ? Number(n.toFixed(d)) : null;
17
18
 
18
19
  /** The four raw token pillars → the cascade. */
19
20
  export function cascade({ input, output, cacheCreate, cacheRead }) {
20
- const i = Number(input), o = Number(output), cw = Number(cacheCreate), cr = Number(cacheRead)
21
- const total = i + o + cw + cr
22
- const warnings = []
21
+ const i = Number(input),
22
+ o = Number(output),
23
+ cw = Number(cacheCreate),
24
+ cr = Number(cacheRead);
25
+ const total = i + o + cw + cr;
26
+ const warnings = [];
23
27
 
24
28
  // SNR: undefined when both i and o are 0 (empty session)
25
- const snrDenom = i + o
26
- const snr = snrDenom > 0 ? o / snrDenom : null
27
- if (snr === null) warnings.push('snr_undefined: input+output=0')
29
+ const snrDenom = i + o;
30
+ const snr = snrDenom > 0 ? o / snrDenom : null;
31
+ if (snr === null) warnings.push("snr_undefined: input+output=0");
28
32
 
29
33
  // velocity: undefined when i=0 (no fresh input — pure cache-only session)
30
- const velocity = i > 0 ? o / i : null
31
- if (velocity === null) warnings.push('velocity_undefined: input=0')
34
+ const velocity = i > 0 ? o / i : null;
35
+ if (velocity === null) warnings.push("velocity_undefined: input=0");
32
36
 
33
37
  // leverage: undefined when i=0
34
- const leverage = i > 0 ? cr / i : null
35
- if (leverage === null) warnings.push('leverage_undefined: input=0')
38
+ const leverage = i > 0 ? cr / i : null;
39
+ if (leverage === null) warnings.push("leverage_undefined: input=0");
36
40
 
37
41
  // Υ = leverage × velocity = (Cr·O)/I² — null when either component is null
38
- const yield_ = leverage !== null && velocity !== null ? leverage * velocity : null
39
- if (yield_ === null && !warnings.some((w) => w.startsWith('yield')))
40
- warnings.push('yield_undefined: requires input>0')
42
+ const yield_ =
43
+ leverage !== null && velocity !== null ? leverage * velocity : null;
44
+ if (yield_ === null && !warnings.some((w) => w.startsWith("yield")))
45
+ warnings.push("yield_undefined: requires input>0");
41
46
 
42
47
  // dev10x = log10(Cr/I) — collapses when cw=0 (no cache commits) or i=0 or cr=0.
43
48
  // narrate.mjs already has a nonCompounding branch for this case; we return null
44
49
  // explicitly so the branch triggers correctly (vs -Infinity on log10(0)).
45
- let dev10x = null
50
+ let dev10x = null;
46
51
  if (i > 0 && o > 0 && cw > 0 && cr > 0) {
47
- dev10x = Math.log10((o / i) * (cw / o) * (cr / cw)) // = log10(Cr/I)
52
+ dev10x = Math.log10((o / i) * (cw / o) * (cr / cw)); // = log10(Cr/I)
48
53
  } else {
49
- warnings.push('dev10x_undefined: requires all four pillars > 0')
54
+ warnings.push("dev10x_undefined: requires all four pillars > 0");
50
55
  }
51
56
 
52
57
  const result = {
@@ -57,22 +62,99 @@ export function cascade({ input, output, cacheCreate, cacheRead }) {
57
62
  velocity: round(velocity, 3),
58
63
  dev10x: round(dev10x, 2),
59
64
  class: classify(yield_, dev10x),
60
- }
61
- if (warnings.length > 0) result.warnings = warnings
62
- return result
65
+ mode: detectMode({ input: i, output: o, cacheCreate: cw, cacheRead: cr }),
66
+ };
67
+ if (warnings.length > 0) result.warnings = warnings;
68
+ return result;
63
69
  }
64
70
 
65
71
  /** MVP class tiering from Υ + 10xDEV (canon assigns from cascade SNR + 10xDEV; this
66
72
  * is the open-MVP approximation — proprietary threshold cuts stay server-side). */
67
73
  export function classify(yieldVal, dev10x) {
68
- if (yieldVal >= 1000 || dev10x >= 3) return 'TRANSMITTER'
69
- if (dev10x >= 1.45) return 'ARCH+'
70
- if (dev10x >= 1.35) return 'ARCH'
71
- if (dev10x >= 1.2) return 'POWER'
72
- if (dev10x >= 1.0) return 'BASE'
73
- if (dev10x >= 0) return 'SEEKER'
74
- if (dev10x >= -0.3) return 'REFINER'
75
- return 'IGNITER'
74
+ if (yieldVal >= 1000 || dev10x >= 3) return "TRANSMITTER";
75
+ if (dev10x >= 1.45) return "ARCH+";
76
+ if (dev10x >= 1.35) return "ARCH";
77
+ if (dev10x >= 1.2) return "POWER";
78
+ if (dev10x >= 1.0) return "BASE";
79
+ if (dev10x >= 0) return "SEEKER";
80
+ if (dev10x >= -0.3) return "REFINER";
81
+ return "IGNITER";
82
+ }
83
+
84
+ /**
85
+ * detectMode — classify an operator's current working mode from 4 token pillars.
86
+ *
87
+ * Pure ratio math, first-match-wins (same pattern as classify() — descending
88
+ * cuts preserve edge semantics). MAINTAIN checked first (high leverage is
89
+ * strongest signal), then DEBUG (low velocity is distinctive), then EDIT,
90
+ * then BUILD as fallback.
91
+ *
92
+ * Modes:
93
+ * BUILD — high input, low/zero cacheRead, output rising (greenfield)
94
+ * EDIT — high input, low cacheRead, high output (polishing)
95
+ * DEBUG — high input, low output, low cacheRead (investigating)
96
+ * MAINTAIN — low input, high cacheRead, high output (compounding)
97
+ * IDLE — near-zero tokens (not working)
98
+ *
99
+ * Returns { mode, confidence }.
100
+ */
101
+ export function detectMode({ input, output, cacheCreate, cacheRead }) {
102
+ const i = Number(input),
103
+ o = Number(output),
104
+ cw = Number(cacheCreate),
105
+ cr = Number(cacheRead);
106
+ const total = i + o + cw + cr;
107
+
108
+ // IDLE: near-zero tokens
109
+ if (total < 1000) return { mode: "IDLE", confidence: 1.0 };
110
+
111
+ const leverage = i > 0 ? cr / i : 0;
112
+ const velocity = i > 0 ? o / i : 0;
113
+ const input_share = total > 0 ? i / total : 0;
114
+
115
+ // MAINTAIN: high leverage + high velocity (the cascade is compounding)
116
+ if (leverage > 10 && velocity > 1)
117
+ return { mode: "MAINTAIN", confidence: 0.9 };
118
+ if (leverage > 3 && velocity > 0.5)
119
+ return { mode: "MAINTAIN", confidence: 0.7 };
120
+
121
+ // DEBUG: low velocity + high input share (reading, not producing)
122
+ if (velocity < 0.3 && input_share > 0.5)
123
+ return { mode: "DEBUG", confidence: 0.8 };
124
+
125
+ // EDIT: high input share + high velocity (fresh input but producing)
126
+ if (input_share > 0.4 && velocity > 0.5)
127
+ return { mode: "EDIT", confidence: 0.7 };
128
+
129
+ // DEBUG (secondary): high input share + low velocity
130
+ if (input_share > 0.4 && velocity < 0.5)
131
+ return { mode: "DEBUG", confidence: 0.6 };
132
+
133
+ // BUILD: fallback (high input, no cache reuse yet)
134
+ return { mode: "BUILD", confidence: 0.6 };
135
+ }
136
+
137
+ /**
138
+ * Expected yield per mode — global defaults used before personal baselines
139
+ * exist (needs 7+ days of history). Used by the quality score computation.
140
+ */
141
+ export const MODE_EXPECTED_YIELD = {
142
+ BUILD: 15,
143
+ EDIT: 45,
144
+ DEBUG: 10,
145
+ MAINTAIN: 5000,
146
+ IDLE: 0,
147
+ };
148
+
149
+ /**
150
+ * qualityScore — actual yield relative to mode expectation.
151
+ * Fixes the "debug is bad" problem: a DEBUG session at 80% quality is good.
152
+ * Returns a number 0+ (can exceed 1.0 if outperforming the expected yield).
153
+ */
154
+ export function qualityScore(actualYield, mode) {
155
+ const expected = MODE_EXPECTED_YIELD[mode] ?? 1;
156
+ if (expected === 0) return actualYield === 0 ? 1.0 : 0;
157
+ return actualYield / expected;
76
158
  }
77
159
 
78
160
  /**
@@ -90,54 +172,103 @@ export function classify(yieldVal, dev10x) {
90
172
  * something looks suspicious we surface it in `_parseWarnings` on the returned object.
91
173
  */
92
174
  export function parsePillars(text) {
93
- const t = String(text || '').trim()
94
- const pw = [] // parse warnings to attach
175
+ const t = String(text || "").trim();
176
+ const pw = []; // parse warnings to attach
95
177
 
96
178
  // ── JSON path ──────────────────────────────────────────────────────────────
97
179
  try {
98
- const j = JSON.parse(t)
99
- if (j && typeof j === 'object' && !Array.isArray(j)) {
100
- const g = (...keys) => { for (const k of keys) if (j[k] != null) return j[k]; return null }
101
- const input = g('input', 'tokens_input_fresh', 'inputTokens', 'input_tokens')
102
- const output = g('output', 'tokens_output', 'outputTokens', 'output_tokens')
103
- const cacheCreate = g('cacheCreate', 'tokens_cache_creation', 'cache_creation_tokens')
104
- const cacheRead = g('cacheRead', 'tokens_cache_read', 'cache_read_tokens')
180
+ const j = JSON.parse(t);
181
+ if (j && typeof j === "object" && !Array.isArray(j)) {
182
+ const g = (...keys) => {
183
+ for (const k of keys) if (j[k] != null) return j[k];
184
+ return null;
185
+ };
186
+ const input = g(
187
+ "input",
188
+ "tokens_input_fresh",
189
+ "inputTokens",
190
+ "input_tokens",
191
+ );
192
+ const output = g(
193
+ "output",
194
+ "tokens_output",
195
+ "outputTokens",
196
+ "output_tokens",
197
+ );
198
+ const cacheCreate = g(
199
+ "cacheCreate",
200
+ "tokens_cache_creation",
201
+ "cache_creation_tokens",
202
+ );
203
+ const cacheRead = g(
204
+ "cacheRead",
205
+ "tokens_cache_read",
206
+ "cache_read_tokens",
207
+ );
105
208
  if ([input, output, cacheCreate, cacheRead].every((v) => v != null)) {
106
209
  const pillars = {
107
210
  input: Number(input),
108
211
  output: Number(output),
109
212
  cacheCreate: Number(cacheCreate),
110
213
  cacheRead: Number(cacheRead),
111
- }
112
- if ([pillars.input, pillars.output, pillars.cacheCreate, pillars.cacheRead].some((v) => !Number.isFinite(v)))
113
- throw new Error('Non-numeric pillar value in JSON (got string or non-finite number).')
114
- if ([pillars.input, pillars.output, pillars.cacheCreate, pillars.cacheRead].some((v) => v < 0))
115
- pw.push('negative_pillar: one or more pillars is negative — may be a data error')
116
- if (pw.length > 0) pillars._parseWarnings = pw
117
- return pillars
214
+ };
215
+ if (
216
+ [
217
+ pillars.input,
218
+ pillars.output,
219
+ pillars.cacheCreate,
220
+ pillars.cacheRead,
221
+ ].some((v) => !Number.isFinite(v))
222
+ )
223
+ throw new Error(
224
+ "Non-numeric pillar value in JSON (got string or non-finite number).",
225
+ );
226
+ if (
227
+ [
228
+ pillars.input,
229
+ pillars.output,
230
+ pillars.cacheCreate,
231
+ pillars.cacheRead,
232
+ ].some((v) => v < 0)
233
+ )
234
+ pw.push(
235
+ "negative_pillar: one or more pillars is negative — may be a data error",
236
+ );
237
+ if (pw.length > 0) pillars._parseWarnings = pw;
238
+ return pillars;
118
239
  }
119
240
  }
120
241
  } catch (e) {
121
242
  // JSON.parse syntax error — fall through to positional. Re-throw parse errors we raised ourselves.
122
- if (e.message.startsWith('Non-numeric')) throw e
243
+ if (e.message.startsWith("Non-numeric")) throw e;
123
244
  }
124
245
 
125
246
  // ── Positional path ────────────────────────────────────────────────────────
126
247
  // Guard: if the text contains alphabetic words, the numeric extraction is unreliable.
127
248
  // We still attempt it (don't throw away the data) but flag it for review.
128
249
  if (/[a-zA-Z]/.test(t))
129
- pw.push('positional_from_mixed_text: extracted numbers from text that contains alphabetic characters — verify these are the correct 4 pillars')
250
+ pw.push(
251
+ "positional_from_mixed_text: extracted numbers from text that contains alphabetic characters — verify these are the correct 4 pillars",
252
+ );
130
253
 
131
- const nums = (t.match(/-?\d[\d,]*\.?\d*/g) || []).map((s) => Number(s.replace(/,/g, '')))
254
+ const nums = (t.match(/-?\d[\d,]*\.?\d*/g) || []).map((s) =>
255
+ Number(s.replace(/,/g, "")),
256
+ );
132
257
  if (nums.length >= 4) {
133
- const [input, output, cacheCreate, cacheRead] = nums
258
+ const [input, output, cacheCreate, cacheRead] = nums;
134
259
  if (nums.length > 4)
135
- pw.push(`positional_extra_numbers: found ${nums.length} numbers, using first 4 — inspect for positional order error`)
136
- const pillars = { input, output, cacheCreate, cacheRead }
260
+ pw.push(
261
+ `positional_extra_numbers: found ${nums.length} numbers, using first 4 — inspect for positional order error`,
262
+ );
263
+ const pillars = { input, output, cacheCreate, cacheRead };
137
264
  if ([input, output, cacheCreate, cacheRead].some((v) => v < 0))
138
- pw.push('negative_pillar: one or more pillars is negative — may be a data error')
139
- if (pw.length > 0) pillars._parseWarnings = pw
140
- return pillars
265
+ pw.push(
266
+ "negative_pillar: one or more pillars is negative — may be a data error",
267
+ );
268
+ if (pw.length > 0) pillars._parseWarnings = pw;
269
+ return pillars;
141
270
  }
142
- throw new Error('Could not parse 4 token pillars (input, output, cacheCreate, cacheRead) from the input.')
271
+ throw new Error(
272
+ "Could not parse 4 token pillars (input, output, cacheCreate, cacheRead) from the input.",
273
+ );
143
274
  }