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/README.md +132 -77
- package/adapters.mjs +508 -240
- package/badges.mjs +264 -0
- package/cascade.mjs +185 -54
- package/cli.mjs +1311 -893
- package/connect.mjs +2 -2
- package/index.mjs +78 -59
- package/keystore.mjs +64 -44
- package/narrate.mjs +34 -22
- package/package.json +4 -2
- package/preflight.mjs +191 -0
- package/sign.mjs +46 -27
- package/submit.mjs +166 -62
- package/tokenpull.mjs +337 -188
- package/tools.mjs +2154 -534
- package/tui.mjs +1860 -1081
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) =>
|
|
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),
|
|
21
|
-
|
|
22
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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_ =
|
|
39
|
-
|
|
40
|
-
|
|
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(
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|
69
|
-
if (dev10x >= 1.45) return
|
|
70
|
-
if (dev10x >= 1.35) return
|
|
71
|
-
if (dev10x >= 1.2) return
|
|
72
|
-
if (dev10x >= 1.0) return
|
|
73
|
-
if (dev10x >= 0) return
|
|
74
|
-
if (dev10x >= -0.3) return
|
|
75
|
-
return
|
|
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 ||
|
|
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 ===
|
|
100
|
-
const g = (...keys) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
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 (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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(
|
|
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(
|
|
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) =>
|
|
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(
|
|
136
|
-
|
|
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(
|
|
139
|
-
|
|
140
|
-
|
|
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(
|
|
271
|
+
throw new Error(
|
|
272
|
+
"Could not parse 4 token pillars (input, output, cacheCreate, cacheRead) from the input.",
|
|
273
|
+
);
|
|
143
274
|
}
|