sigrank 0.16.0 → 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 CHANGED
@@ -25,92 +25,190 @@
25
25
  * @param {number|null} params.rank — current leaderboard rank (null if not ranked)
26
26
  * @returns {object} { earned_this_week, in_progress, collection }
27
27
  */
28
- export function computeBadges({ pillars, cascade: cas, history = [], isVerified = false, rank = null }) {
29
- const cr = Number(pillars?.cacheRead || 0)
30
- const i = Number(pillars?.input || 0)
31
- const leverage = cas?.leverage ?? (i > 0 ? cr / i : 0)
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);
32
38
 
33
- const earned = []
34
- const inProgress = []
35
- const collection = []
39
+ const earned = [];
40
+ const inProgress = [];
41
+ const collection = [];
36
42
 
37
43
  // 1. First Spark — cacheRead > input (1× leverage)
38
44
  if (leverage > 1) {
39
- earned.push({ id: 'first_spark', label: 'First Spark', icon: '◈', condition: '1× cache leverage', date_earned: _latestDate(history) })
40
- collection.push('first_spark')
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");
41
53
  } else if (cr > 0) {
42
- inProgress.push({ id: 'first_spark', label: 'First Spark', icon: '◈', progress: Math.min(leverage, 1), target: 1, display: `${leverage.toFixed(1)}× / 1×` })
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
+ });
43
62
  }
44
63
 
45
64
  // 2. Cascade Engine — cacheRead > 10× input
46
65
  if (leverage > 10) {
47
- earned.push({ id: 'cascade_engine', label: 'Cascade Engine', icon: '⚡', condition: '10× cache leverage', date_earned: _latestDate(history) })
48
- collection.push('cascade_engine')
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");
49
74
  } else if (leverage > 1) {
50
- inProgress.push({ id: 'cascade_engine', label: 'Cascade Engine', icon: '⚡', progress: Math.min(leverage, 10), target: 10, display: `${leverage.toFixed(1)}× / 10×` })
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
+ });
51
83
  }
52
84
 
53
85
  // 3. Chain Reaction — cacheRead > 100× input
54
86
  if (leverage > 100) {
55
- earned.push({ id: 'chain_reaction', label: 'Chain Reaction', icon: '⚡', condition: '100× cache leverage', date_earned: _latestDate(history) })
56
- collection.push('chain_reaction')
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");
57
95
  } else if (leverage > 10) {
58
- inProgress.push({ id: 'chain_reaction', label: 'Chain Reaction', icon: '⚡', progress: Math.min(leverage, 100), target: 100, display: `${leverage.toFixed(1)}× / 100×` })
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
+ });
59
104
  }
60
105
 
61
106
  // 4. Foundation — 3 completed BUILD→MAINTAIN arcs
62
- const arcs = _countBuildMaintainArcs(history)
107
+ const arcs = _countBuildMaintainArcs(history);
63
108
  if (arcs >= 3) {
64
- earned.push({ id: 'foundation', label: 'Foundation', icon: '🏗️', condition: '3 BUILD→MAINTAIN arcs', date_earned: _latestDate(history) })
65
- collection.push('foundation')
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");
66
117
  } else {
67
- inProgress.push({ id: 'foundation', label: 'Foundation', icon: '🏗️', progress: arcs, target: 3, display: `${arcs}/3 arcs` })
118
+ inProgress.push({
119
+ id: "foundation",
120
+ label: "Foundation",
121
+ icon: "🏗️",
122
+ progress: arcs,
123
+ target: 3,
124
+ display: `${arcs}/3 arcs`,
125
+ });
68
126
  }
69
127
 
70
128
  // 5. Phoenix — returned to MAINTAIN within 1 day of disruption, 5×
71
- const phoenixCount = _countPhoenixReturns(history)
129
+ const phoenixCount = _countPhoenixReturns(history);
72
130
  if (phoenixCount >= 5) {
73
- earned.push({ id: 'phoenix', label: 'Phoenix', icon: '🔥', condition: '5× MAINTAIN recovery within 1 day', date_earned: _latestDate(history) })
74
- collection.push('phoenix')
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");
75
139
  } else {
76
- inProgress.push({ id: 'phoenix', label: 'Phoenix', icon: '🔥', progress: phoenixCount, target: 5, display: `${phoenixCount}/5 recoveries` })
140
+ inProgress.push({
141
+ id: "phoenix",
142
+ label: "Phoenix",
143
+ icon: "🔥",
144
+ progress: phoenixCount,
145
+ target: 5,
146
+ display: `${phoenixCount}/5 recoveries`,
147
+ });
77
148
  }
78
149
 
79
150
  // 6. Verified — submitted via signed agent
80
151
  if (isVerified) {
81
- earned.push({ id: 'verified', label: 'Verified', icon: '✓', condition: 'Submitted via signed agent', date_earned: _latestDate(history) })
82
- collection.push('verified')
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");
83
160
  }
84
161
  // No in-progress for verified — it's binary
85
162
 
86
163
  // 7. Cascade Streak 🔥 — 7 consecutive days MAINTAIN (current badge)
87
- const streak = _countMaintainStreak(history)
164
+ const streak = _countMaintainStreak(history);
88
165
  if (streak >= 7) {
89
- earned.push({ id: 'cascade_streak', label: 'Cascade Streak', icon: '🔥', condition: '7 consecutive days MAINTAIN', date_earned: _latestDate(history), current: true })
90
- collection.push('cascade_streak')
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");
91
175
  } else {
92
- inProgress.push({ id: 'cascade_streak', label: 'Cascade Streak', icon: '🔥', progress: streak, target: 7, display: `${streak}/7 days` })
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
+ });
93
184
  }
94
185
 
95
186
  // 8. Top 10 🏆 — currently in top 10 (server-side, but we include it if rank is provided)
96
187
  if (rank !== null && rank <= 10) {
97
- earned.push({ id: 'top_10', label: 'Top 10', icon: '🏆', condition: `Currently #${rank}`, date_earned: _latestDate(history), current: true })
98
- collection.push('top_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");
99
197
  }
100
198
  // No in-progress for top 10 — it's a live position, not a progress badge
101
199
 
102
200
  return {
103
- earned_this_week: earned.map(b => b.id),
201
+ earned_this_week: earned.map((b) => b.id),
104
202
  in_progress: inProgress,
105
203
  collection,
106
- }
204
+ };
107
205
  }
108
206
 
109
207
  // ── Helpers ─────────────────────────────────────────────────────────────────
110
208
 
111
209
  function _latestDate(history) {
112
- if (!history || history.length === 0) return null
113
- return history[history.length - 1]?.date || null
210
+ if (!history || history.length === 0) return null;
211
+ return history[history.length - 1]?.date || null;
114
212
  }
115
213
 
116
214
  /**
@@ -118,46 +216,49 @@ function _latestDate(history) {
118
216
  * An arc = a BUILD phase followed by a MAINTAIN phase.
119
217
  */
120
218
  function _countBuildMaintainArcs(history) {
121
- if (!history || history.length < 2) return 0
122
- let arcs = 0
123
- let wasBuild = false
219
+ if (!history || history.length < 2) return 0;
220
+ let arcs = 0;
221
+ let wasBuild = false;
124
222
  for (const h of history) {
125
- if (h.mode === 'BUILD') {
126
- wasBuild = true
127
- } else if (h.mode === 'MAINTAIN' && wasBuild) {
128
- arcs++
129
- wasBuild = false
223
+ if (h.mode === "BUILD") {
224
+ wasBuild = true;
225
+ } else if (h.mode === "MAINTAIN" && wasBuild) {
226
+ arcs++;
227
+ wasBuild = false;
130
228
  }
131
229
  }
132
- return arcs
230
+ return arcs;
133
231
  }
134
232
 
135
233
  /**
136
234
  * Count Phoenix returns — MAINTAIN within 1 day of a non-MAINTAIN day.
137
235
  */
138
236
  function _countPhoenixReturns(history) {
139
- if (!history || history.length < 2) return 0
140
- let count = 0
237
+ if (!history || history.length < 2) return 0;
238
+ let count = 0;
141
239
  for (let idx = 1; idx < history.length; idx++) {
142
- if (history[idx].mode === 'MAINTAIN' && history[idx - 1].mode !== 'MAINTAIN') {
143
- count++
240
+ if (
241
+ history[idx].mode === "MAINTAIN" &&
242
+ history[idx - 1].mode !== "MAINTAIN"
243
+ ) {
244
+ count++;
144
245
  }
145
246
  }
146
- return count
247
+ return count;
147
248
  }
148
249
 
149
250
  /**
150
251
  * Count current consecutive MAINTAIN days (ending at the last entry).
151
252
  */
152
253
  function _countMaintainStreak(history) {
153
- if (!history || history.length === 0) return 0
154
- let streak = 0
254
+ if (!history || history.length === 0) return 0;
255
+ let streak = 0;
155
256
  for (let idx = history.length - 1; idx >= 0; idx--) {
156
- if (history[idx].mode === 'MAINTAIN') {
157
- streak++
257
+ if (history[idx].mode === "MAINTAIN") {
258
+ streak++;
158
259
  } else {
159
- break
260
+ break;
160
261
  }
161
262
  }
162
- return streak
263
+ return streak;
163
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 = {
@@ -58,22 +63,22 @@ export function cascade({ input, output, cacheCreate, cacheRead }) {
58
63
  dev10x: round(dev10x, 2),
59
64
  class: classify(yield_, dev10x),
60
65
  mode: detectMode({ input: i, output: o, cacheCreate: cw, cacheRead: cr }),
61
- }
62
- if (warnings.length > 0) result.warnings = warnings
63
- return result
66
+ };
67
+ if (warnings.length > 0) result.warnings = warnings;
68
+ return result;
64
69
  }
65
70
 
66
71
  /** MVP class tiering from Υ + 10xDEV (canon assigns from cascade SNR + 10xDEV; this
67
72
  * is the open-MVP approximation — proprietary threshold cuts stay server-side). */
68
73
  export function classify(yieldVal, dev10x) {
69
- if (yieldVal >= 1000 || dev10x >= 3) return 'TRANSMITTER'
70
- if (dev10x >= 1.45) return 'ARCH+'
71
- if (dev10x >= 1.35) return 'ARCH'
72
- if (dev10x >= 1.2) return 'POWER'
73
- if (dev10x >= 1.0) return 'BASE'
74
- if (dev10x >= 0) return 'SEEKER'
75
- if (dev10x >= -0.3) return 'REFINER'
76
- 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";
77
82
  }
78
83
 
79
84
  /**
@@ -94,31 +99,39 @@ export function classify(yieldVal, dev10x) {
94
99
  * Returns { mode, confidence }.
95
100
  */
96
101
  export function detectMode({ input, output, cacheCreate, cacheRead }) {
97
- const i = Number(input), o = Number(output), cw = Number(cacheCreate), cr = Number(cacheRead)
98
- const total = i + o + cw + cr
102
+ const i = Number(input),
103
+ o = Number(output),
104
+ cw = Number(cacheCreate),
105
+ cr = Number(cacheRead);
106
+ const total = i + o + cw + cr;
99
107
 
100
108
  // IDLE: near-zero tokens
101
- if (total < 1000) return { mode: 'IDLE', confidence: 1.0 }
109
+ if (total < 1000) return { mode: "IDLE", confidence: 1.0 };
102
110
 
103
- const leverage = i > 0 ? cr / i : 0
104
- const velocity = i > 0 ? o / i : 0
105
- const input_share = total > 0 ? i / total : 0
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;
106
114
 
107
115
  // MAINTAIN: high leverage + high velocity (the cascade is compounding)
108
- if (leverage > 10 && velocity > 1) return { mode: 'MAINTAIN', confidence: 0.9 }
109
- if (leverage > 3 && velocity > 0.5) return { mode: 'MAINTAIN', confidence: 0.7 }
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 };
110
120
 
111
121
  // DEBUG: low velocity + high input share (reading, not producing)
112
- if (velocity < 0.3 && input_share > 0.5) return { mode: 'DEBUG', confidence: 0.8 }
122
+ if (velocity < 0.3 && input_share > 0.5)
123
+ return { mode: "DEBUG", confidence: 0.8 };
113
124
 
114
125
  // EDIT: high input share + high velocity (fresh input but producing)
115
- if (input_share > 0.4 && velocity > 0.5) return { mode: 'EDIT', confidence: 0.7 }
126
+ if (input_share > 0.4 && velocity > 0.5)
127
+ return { mode: "EDIT", confidence: 0.7 };
116
128
 
117
129
  // DEBUG (secondary): high input share + low velocity
118
- if (input_share > 0.4 && velocity < 0.5) return { mode: 'DEBUG', confidence: 0.6 }
130
+ if (input_share > 0.4 && velocity < 0.5)
131
+ return { mode: "DEBUG", confidence: 0.6 };
119
132
 
120
133
  // BUILD: fallback (high input, no cache reuse yet)
121
- return { mode: 'BUILD', confidence: 0.6 }
134
+ return { mode: "BUILD", confidence: 0.6 };
122
135
  }
123
136
 
124
137
  /**
@@ -131,7 +144,7 @@ export const MODE_EXPECTED_YIELD = {
131
144
  DEBUG: 10,
132
145
  MAINTAIN: 5000,
133
146
  IDLE: 0,
134
- }
147
+ };
135
148
 
136
149
  /**
137
150
  * qualityScore — actual yield relative to mode expectation.
@@ -139,9 +152,9 @@ export const MODE_EXPECTED_YIELD = {
139
152
  * Returns a number 0+ (can exceed 1.0 if outperforming the expected yield).
140
153
  */
141
154
  export function qualityScore(actualYield, mode) {
142
- const expected = MODE_EXPECTED_YIELD[mode] ?? 1
143
- if (expected === 0) return actualYield === 0 ? 1.0 : 0
144
- return actualYield / expected
155
+ const expected = MODE_EXPECTED_YIELD[mode] ?? 1;
156
+ if (expected === 0) return actualYield === 0 ? 1.0 : 0;
157
+ return actualYield / expected;
145
158
  }
146
159
 
147
160
  /**
@@ -159,54 +172,103 @@ export function qualityScore(actualYield, mode) {
159
172
  * something looks suspicious we surface it in `_parseWarnings` on the returned object.
160
173
  */
161
174
  export function parsePillars(text) {
162
- const t = String(text || '').trim()
163
- const pw = [] // parse warnings to attach
175
+ const t = String(text || "").trim();
176
+ const pw = []; // parse warnings to attach
164
177
 
165
178
  // ── JSON path ──────────────────────────────────────────────────────────────
166
179
  try {
167
- const j = JSON.parse(t)
168
- if (j && typeof j === 'object' && !Array.isArray(j)) {
169
- const g = (...keys) => { for (const k of keys) if (j[k] != null) return j[k]; return null }
170
- const input = g('input', 'tokens_input_fresh', 'inputTokens', 'input_tokens')
171
- const output = g('output', 'tokens_output', 'outputTokens', 'output_tokens')
172
- const cacheCreate = g('cacheCreate', 'tokens_cache_creation', 'cache_creation_tokens')
173
- 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
+ );
174
208
  if ([input, output, cacheCreate, cacheRead].every((v) => v != null)) {
175
209
  const pillars = {
176
210
  input: Number(input),
177
211
  output: Number(output),
178
212
  cacheCreate: Number(cacheCreate),
179
213
  cacheRead: Number(cacheRead),
180
- }
181
- if ([pillars.input, pillars.output, pillars.cacheCreate, pillars.cacheRead].some((v) => !Number.isFinite(v)))
182
- throw new Error('Non-numeric pillar value in JSON (got string or non-finite number).')
183
- if ([pillars.input, pillars.output, pillars.cacheCreate, pillars.cacheRead].some((v) => v < 0))
184
- pw.push('negative_pillar: one or more pillars is negative — may be a data error')
185
- if (pw.length > 0) pillars._parseWarnings = pw
186
- 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;
187
239
  }
188
240
  }
189
241
  } catch (e) {
190
242
  // JSON.parse syntax error — fall through to positional. Re-throw parse errors we raised ourselves.
191
- if (e.message.startsWith('Non-numeric')) throw e
243
+ if (e.message.startsWith("Non-numeric")) throw e;
192
244
  }
193
245
 
194
246
  // ── Positional path ────────────────────────────────────────────────────────
195
247
  // Guard: if the text contains alphabetic words, the numeric extraction is unreliable.
196
248
  // We still attempt it (don't throw away the data) but flag it for review.
197
249
  if (/[a-zA-Z]/.test(t))
198
- 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
+ );
199
253
 
200
- 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
+ );
201
257
  if (nums.length >= 4) {
202
- const [input, output, cacheCreate, cacheRead] = nums
258
+ const [input, output, cacheCreate, cacheRead] = nums;
203
259
  if (nums.length > 4)
204
- pw.push(`positional_extra_numbers: found ${nums.length} numbers, using first 4 — inspect for positional order error`)
205
- 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 };
206
264
  if ([input, output, cacheCreate, cacheRead].some((v) => v < 0))
207
- pw.push('negative_pillar: one or more pillars is negative — may be a data error')
208
- if (pw.length > 0) pillars._parseWarnings = pw
209
- 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;
210
270
  }
211
- 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
+ );
212
274
  }