sigrank 0.0.175
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/LICENSE +21 -0
- package/README.md +420 -0
- package/adapters.mjs +750 -0
- package/badges.mjs +264 -0
- package/banner.svg +7 -0
- package/cascade.mjs +274 -0
- package/cli.mjs +1846 -0
- package/connect.mjs +13 -0
- package/index.mjs +390 -0
- package/keystore.mjs +242 -0
- package/narrate.mjs +77 -0
- package/package.json +71 -0
- package/preflight.mjs +191 -0
- package/sign.mjs +106 -0
- package/submit.mjs +285 -0
- package/tokenpull.mjs +584 -0
- package/tools.mjs +3303 -0
- package/tui.mjs +2656 -0
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/banner.svg
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="200" viewBox="0 0 800 200">
|
|
2
|
+
<rect width="800" height="200" rx="12" fill="#0a0805"/>
|
|
3
|
+
<rect x="0" y="0" width="800" height="3" rx="12" fill="#c4923a"/>
|
|
4
|
+
<text x="60" y="110" font-family="Georgia, serif" font-size="56" font-weight="700" fill="#e8b955">§ SigRank</text>
|
|
5
|
+
<text x="60" y="150" font-family="Helvetica, Arial, sans-serif" font-size="18" fill="#a89a7a">The AI operator leaderboard</text>
|
|
6
|
+
<text x="60" y="178" font-family="Helvetica, Arial, sans-serif" font-size="14" fill="#6b6354">signalaf.com</text>
|
|
7
|
+
</svg>
|
package/cascade.mjs
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cascade.mjs — pure SigRank yield cascade (no deps, no transport).
|
|
3
|
+
* Mirrors sigrank-app/lib/ingest/bridge.ts computeCascadeMetrics() so rank_paste
|
|
4
|
+
* reproduces the canonical MO§ES Υ 18436.98 from its 4 raw token pillars.
|
|
5
|
+
* Paper-and-pencil math, open by design; proprietary threshold cuts stay server-side.
|
|
6
|
+
*
|
|
7
|
+
* Degenerate-input policy (hardened):
|
|
8
|
+
* - Any pillar that collapses a denominator (i=0, o=0, cw=0, cr=0) returns null for
|
|
9
|
+
* the affected metrics rather than Infinity/NaN.
|
|
10
|
+
* - A `warnings[]` array is attached when any metric is null so callers can surface the
|
|
11
|
+
* reason without silently corrupting downstream calculations.
|
|
12
|
+
* - The cascade is NEVER thrown away — even partial results are useful for review/storage.
|
|
13
|
+
* Callers that require a fully-formed result should check `warnings.length === 0`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const round = (n, d) =>
|
|
17
|
+
Number.isFinite(n) ? Number(n.toFixed(d)) : null;
|
|
18
|
+
|
|
19
|
+
/** The four raw token pillars → the cascade. */
|
|
20
|
+
export function cascade({ input, output, cacheCreate, cacheRead }) {
|
|
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 = [];
|
|
27
|
+
|
|
28
|
+
// SNR: undefined when both i and o are 0 (empty session)
|
|
29
|
+
const snrDenom = i + o;
|
|
30
|
+
const snr = snrDenom > 0 ? o / snrDenom : null;
|
|
31
|
+
if (snr === null) warnings.push("snr_undefined: input+output=0");
|
|
32
|
+
|
|
33
|
+
// velocity: undefined when i=0 (no fresh input — pure cache-only session)
|
|
34
|
+
const velocity = i > 0 ? o / i : null;
|
|
35
|
+
if (velocity === null) warnings.push("velocity_undefined: input=0");
|
|
36
|
+
|
|
37
|
+
// leverage: undefined when i=0
|
|
38
|
+
const leverage = i > 0 ? cr / i : null;
|
|
39
|
+
if (leverage === null) warnings.push("leverage_undefined: input=0");
|
|
40
|
+
|
|
41
|
+
// Υ = leverage × velocity = (Cr·O)/I² — null when either component is null
|
|
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");
|
|
46
|
+
|
|
47
|
+
// dev10x = log10(Cr/I) — collapses when cw=0 (no cache commits) or i=0 or cr=0.
|
|
48
|
+
// narrate.mjs already has a nonCompounding branch for this case; we return null
|
|
49
|
+
// explicitly so the branch triggers correctly (vs -Infinity on log10(0)).
|
|
50
|
+
let dev10x = null;
|
|
51
|
+
if (i > 0 && o > 0 && cw > 0 && cr > 0) {
|
|
52
|
+
dev10x = Math.log10((o / i) * (cw / o) * (cr / cw)); // = log10(Cr/I)
|
|
53
|
+
} else {
|
|
54
|
+
warnings.push("dev10x_undefined: requires all four pillars > 0");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const result = {
|
|
58
|
+
pillars: { input: i, output: o, cacheCreate: cw, cacheRead: cr, total },
|
|
59
|
+
yield: round(yield_, 2),
|
|
60
|
+
snr: round(snr, 4),
|
|
61
|
+
leverage: round(leverage, 1),
|
|
62
|
+
velocity: round(velocity, 3),
|
|
63
|
+
dev10x: round(dev10x, 2),
|
|
64
|
+
class: classify(yield_, dev10x),
|
|
65
|
+
mode: detectMode({ input: i, output: o, cacheCreate: cw, cacheRead: cr }),
|
|
66
|
+
};
|
|
67
|
+
if (warnings.length > 0) result.warnings = warnings;
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** MVP class tiering from Υ + 10xDEV (canon assigns from cascade SNR + 10xDEV; this
|
|
72
|
+
* is the open-MVP approximation — proprietary threshold cuts stay server-side). */
|
|
73
|
+
export function classify(yieldVal, dev10x) {
|
|
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;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Extract the 4 pillars from pasted text: JSON object OR 4 whitespace numbers.
|
|
162
|
+
*
|
|
163
|
+
* Hardened parse policy:
|
|
164
|
+
* - JSON path: requires named keys and numeric values. Rejects strings/null.
|
|
165
|
+
* - Positional path: requires the input to be ONLY numeric tokens (whitespace/commas
|
|
166
|
+
* allowed as separators). If the text contains non-numeric words the positional
|
|
167
|
+
* extractor attaches a `_parseWarnings` flag so downstream can route it to a review
|
|
168
|
+
* channel instead of treating it as authoritative data.
|
|
169
|
+
* - Negative values are accepted (not thrown away) but flagged — could be valid in
|
|
170
|
+
* some edge-case accounting or a data error; the server is the authority on validity.
|
|
171
|
+
* - We NEVER silently corrupt: if we can't parse 4 pillars we throw. If we parse but
|
|
172
|
+
* something looks suspicious we surface it in `_parseWarnings` on the returned object.
|
|
173
|
+
*/
|
|
174
|
+
export function parsePillars(text) {
|
|
175
|
+
const t = String(text || "").trim();
|
|
176
|
+
const pw = []; // parse warnings to attach
|
|
177
|
+
|
|
178
|
+
// ── JSON path ──────────────────────────────────────────────────────────────
|
|
179
|
+
try {
|
|
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
|
+
);
|
|
208
|
+
if ([input, output, cacheCreate, cacheRead].every((v) => v != null)) {
|
|
209
|
+
const pillars = {
|
|
210
|
+
input: Number(input),
|
|
211
|
+
output: Number(output),
|
|
212
|
+
cacheCreate: Number(cacheCreate),
|
|
213
|
+
cacheRead: Number(cacheRead),
|
|
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;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} catch (e) {
|
|
242
|
+
// JSON.parse syntax error — fall through to positional. Re-throw parse errors we raised ourselves.
|
|
243
|
+
if (e.message.startsWith("Non-numeric")) throw e;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Positional path ────────────────────────────────────────────────────────
|
|
247
|
+
// Guard: if the text contains alphabetic words, the numeric extraction is unreliable.
|
|
248
|
+
// We still attempt it (don't throw away the data) but flag it for review.
|
|
249
|
+
if (/[a-zA-Z]/.test(t))
|
|
250
|
+
pw.push(
|
|
251
|
+
"positional_from_mixed_text: extracted numbers from text that contains alphabetic characters — verify these are the correct 4 pillars",
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const nums = (t.match(/-?\d[\d,]*\.?\d*/g) || []).map((s) =>
|
|
255
|
+
Number(s.replace(/,/g, "")),
|
|
256
|
+
);
|
|
257
|
+
if (nums.length >= 4) {
|
|
258
|
+
const [input, output, cacheCreate, cacheRead] = nums;
|
|
259
|
+
if (nums.length > 4)
|
|
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 };
|
|
264
|
+
if ([input, output, cacheCreate, cacheRead].some((v) => v < 0))
|
|
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;
|
|
270
|
+
}
|
|
271
|
+
throw new Error(
|
|
272
|
+
"Could not parse 4 token pillars (input, output, cacheCreate, cacheRead) from the input.",
|
|
273
|
+
);
|
|
274
|
+
}
|