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,338 @@
1
+ /**
2
+ * cascade.mjs — SigRank yield cascade facade.
3
+ *
4
+ * The canonical cascade math (Υ Yield, SNR, Leverage, Velocity, 10xDEV, and the
5
+ * 24-stage RS05 class taxonomy) now lives in the `@sigrank/cascade` package.
6
+ * This file re-exports the canonical functions and adds local-only helpers that
7
+ * are presentation/MCP-specific and intentionally NOT part of the canonical
8
+ * math package:
9
+ * - CLASS_TIERS, SIGNAL_CLASSES, UNCLASSED (display taxonomy)
10
+ * - tierOf(), stageOf() (display helpers)
11
+ * - detectMode(), MODE_EXPECTED_YIELD, qualityScore() (mode/quality layer)
12
+ * - parsePillars() (text → 4 pillars extractor)
13
+ *
14
+ * The cascade() wrapper preserves the object-signature
15
+ * ({ input, output, cacheCreate, cacheRead }) used throughout this repo and
16
+ * re-attaches the `mode` field (detectMode) that the canonical package
17
+ * intentionally omits (mode is a presentation-layer concern, not canonical
18
+ * math).
19
+ *
20
+ * Canonical reference: MO§ES Υ 18436.98 from (1251211, 11296121, 128196310, 2555179769).
21
+ *
22
+ * Degenerate-input policy (inherited from the canonical package):
23
+ * - Any pillar that collapses a denominator (i=0, o=0, cw=0, cr=0) returns null for
24
+ * the affected metrics rather than Infinity/NaN.
25
+ * - A `warnings[]` array is attached when any metric is null so callers can surface the
26
+ * reason without silently corrupting downstream calculations.
27
+ * - The cascade is NEVER thrown away — even partial results are useful for review/storage.
28
+ * Callers that require a fully-formed result should check `warnings.length === 0`.
29
+ */
30
+
31
+ // ─── Canonical math (from @sigrank/cascade) ─────────────────────────────────
32
+ import {
33
+ cascade as cascadeCanonical,
34
+ round,
35
+ classify,
36
+ RS05_CLASS_THRESHOLDS,
37
+ fieldStats,
38
+ percentileOf,
39
+ rankOf,
40
+ operatorSignature,
41
+ evaluateOperator,
42
+ } from "@sigrank/cascade";
43
+
44
+ // Re-export the canonical functions so existing import paths keep working.
45
+ export {
46
+ round,
47
+ classify,
48
+ RS05_CLASS_THRESHOLDS,
49
+ fieldStats,
50
+ percentileOf,
51
+ rankOf,
52
+ operatorSignature,
53
+ evaluateOperator,
54
+ };
55
+
56
+ /**
57
+ * The four raw token pillars → the cascade.
58
+ *
59
+ * Object-signature wrapper around the canonical `@sigrank/cascade` cascade()
60
+ * (which takes positional args). Re-attaches the `mode` field via detectMode()
61
+ * so the 20+ call sites in this repo that read `result.mode` keep working.
62
+ */
63
+ export function cascade({ input, output, cacheCreate, cacheRead }) {
64
+ const result = cascadeCanonical(input, output, cacheCreate, cacheRead);
65
+ // Re-attach mode — the canonical package intentionally omits it (mode is a
66
+ // presentation-layer concern, not canonical math).
67
+ result.mode = detectMode({ input, output, cacheCreate, cacheRead });
68
+ return result;
69
+ }
70
+
71
+ // ─── Local-only display taxonomy (not in @sigrank/cascade) ───────────────────
72
+
73
+ /**
74
+ * CLASS_TIERS — the 8 base tier names (K.01–K.08) for display: glyph, color,
75
+ * meaning. SINGLE SOURCE OF TRUTH for tier-level display info.
76
+ *
77
+ * The permanent class is an EXPERIENCE ladder keyed on TOTAL TOKENS. Each tier
78
+ * has 3 sub-stages (I/II/III) — 24 stages total. The 24 thresholds live in
79
+ * RS05_CLASS_THRESHOLDS (re-exported from @sigrank/cascade above). classify()
80
+ * returns the full sub-stage string (e.g. "REFINER II"). Use tierOf() to
81
+ * extract the base tier name.
82
+ *
83
+ * TRANSMITTER is NOT on this ladder — it is a temporary peak badge (RS.08).
84
+ * The client does not evaluate the badge (owner is still calibrating it);
85
+ * the server does it on read.
86
+ *
87
+ * Mirrors the server's canon-ids.ts CLASS_TIERS (display). Order is descending
88
+ * (ARCH+ → IGNITER).
89
+ */
90
+ export const CLASS_TIERS = [
91
+ "ARCH+",
92
+ "ARCH",
93
+ "POWER",
94
+ "BASE",
95
+ "SEEKER",
96
+ "REFINER",
97
+ "BEARER",
98
+ "IGNITER",
99
+ ];
100
+
101
+ /**
102
+ * SIGNAL_CLASSES — the full 24 sub-stage names (8 tiers × 3 sub-stages I/II/III).
103
+ * Mirrors the server's SIGNAL_CLASSES set in lib/board/mappers.ts. The API
104
+ * returns class_tier as one of these 24 values. classify() returns one of these
105
+ * (or UNCLASSED). Use this for schema enums and validation.
106
+ */
107
+ export const SIGNAL_CLASSES = [
108
+ "ARCH+ I", "ARCH+ II", "ARCH+ III",
109
+ "ARCH I", "ARCH II", "ARCH III",
110
+ "POWER I", "POWER II", "POWER III",
111
+ "BASE I", "BASE II", "BASE III",
112
+ "SEEKER I", "SEEKER II", "SEEKER III",
113
+ "REFINER I", "REFINER II", "REFINER III",
114
+ "BEARER I", "BEARER II", "BEARER III",
115
+ "IGNITER I", "IGNITER II", "IGNITER III",
116
+ ];
117
+
118
+ /** The degenerate-case class returned when totalTokens is null/undefined
119
+ * (all-zero / empty session). Distinct from IGNITER III (which is a real
120
+ * low-experience tier) so callers can tell "no data" from "bottom tier". */
121
+ export const UNCLASSED = "UNCLASSED";
122
+
123
+ /** Extract the base tier name from a sub-stage string (e.g. "ARCH+ I" → "ARCH+").
124
+ * Mirrors the server's tierOf() in components/sigrank/types.ts. Returns the
125
+ * input as-is if it's not a sub-stage string (e.g. UNCLASSED). */
126
+ export function tierOf(cls) {
127
+ if (cls === UNCLASSED || cls == null) return cls;
128
+ const parts = String(cls).split(" ");
129
+ if (parts.length >= 2 && ["I", "II", "III"].includes(parts[parts.length - 1])) {
130
+ return parts.slice(0, -1).join(" ");
131
+ }
132
+ return cls;
133
+ }
134
+
135
+ /** Extract the sub-stage from a sub-stage string (e.g. "ARCH+ I" → "I").
136
+ * Mirrors the server's stageOf(). Returns null for non-sub-stage strings. */
137
+ export function stageOf(cls) {
138
+ if (cls === UNCLASSED || cls == null) return null;
139
+ const parts = String(cls).split(" ");
140
+ const stage = parts[parts.length - 1];
141
+ return stage === "I" || stage === "II" || stage === "III" ? stage : null;
142
+ }
143
+
144
+ // ─── Mode / quality layer (not in @sigrank/cascade) ──────────────────────────
145
+
146
+ /**
147
+ * detectMode — classify an operator's current working mode from 4 token pillars.
148
+ *
149
+ * Pure ratio math, first-match-wins (same pattern as classify() — descending
150
+ * cuts preserve edge semantics). MAINTAIN checked first (high leverage is
151
+ * strongest signal), then DEBUG (low velocity is distinctive), then EDIT,
152
+ * then BUILD as fallback.
153
+ *
154
+ * Modes:
155
+ * BUILD — high input, low/zero cacheRead, output rising (greenfield)
156
+ * EDIT — high input, low cacheRead, high output (polishing)
157
+ * DEBUG — high input, low output, low cacheRead (investigating)
158
+ * MAINTAIN — low input, high cacheRead, high output (compounding)
159
+ * IDLE — near-zero tokens (not working)
160
+ *
161
+ * Returns { mode, confidence }.
162
+ */
163
+ export function detectMode({ input, output, cacheCreate, cacheRead }) {
164
+ const i = Number(input),
165
+ o = Number(output),
166
+ cw = Number(cacheCreate),
167
+ cr = Number(cacheRead);
168
+ const total = i + o + cw + cr;
169
+
170
+ // IDLE: near-zero tokens
171
+ if (total < 1000) return { mode: "IDLE", confidence: 1.0 };
172
+
173
+ const leverage = i > 0 ? cr / i : 0;
174
+ const velocity = i > 0 ? o / i : 0;
175
+ const input_share = total > 0 ? i / total : 0;
176
+
177
+ // MAINTAIN: high leverage + high velocity (the cascade is compounding)
178
+ if (leverage > 10 && velocity > 1)
179
+ return { mode: "MAINTAIN", confidence: 0.9 };
180
+ if (leverage > 3 && velocity > 0.5)
181
+ return { mode: "MAINTAIN", confidence: 0.7 };
182
+
183
+ // DEBUG: low velocity + high input share (reading, not producing)
184
+ if (velocity < 0.3 && input_share > 0.5)
185
+ return { mode: "DEBUG", confidence: 0.8 };
186
+
187
+ // EDIT: high input share + high velocity (fresh input but producing)
188
+ if (input_share > 0.4 && velocity > 0.5)
189
+ return { mode: "EDIT", confidence: 0.7 };
190
+
191
+ // DEBUG (secondary): high input share + low velocity
192
+ if (input_share > 0.4 && velocity < 0.5)
193
+ return { mode: "DEBUG", confidence: 0.6 };
194
+
195
+ // BUILD: fallback (high input, no cache reuse yet)
196
+ return { mode: "BUILD", confidence: 0.6 };
197
+ }
198
+
199
+ /**
200
+ * Expected yield per mode — global defaults used before personal baselines
201
+ * exist (needs 7+ days of history). Used by the quality score computation.
202
+ */
203
+ export const MODE_EXPECTED_YIELD = {
204
+ BUILD: 15,
205
+ EDIT: 45,
206
+ DEBUG: 10,
207
+ MAINTAIN: 5000,
208
+ IDLE: 0,
209
+ };
210
+
211
+ /**
212
+ * qualityScore — actual yield relative to mode expectation.
213
+ * Fixes the "debug is bad" problem: a DEBUG session at 80% quality is good.
214
+ * Returns a number 0+ (can exceed 1.0 if outperforming the expected yield).
215
+ */
216
+ export function qualityScore(actualYield, mode) {
217
+ const expected = MODE_EXPECTED_YIELD[mode] ?? 1;
218
+ if (expected === 0) return actualYield === 0 ? 1.0 : 0;
219
+ return actualYield / expected;
220
+ }
221
+
222
+ // ─── Text parsing (not in @sigrank/cascade) ──────────────────────────────────
223
+
224
+ /**
225
+ * Extract the 4 pillars from pasted text: JSON object OR 4 whitespace numbers.
226
+ *
227
+ * Hardened parse policy:
228
+ * - JSON path: requires named keys and numeric values. Rejects strings/null.
229
+ * - Positional path: requires the input to be ONLY numeric tokens (whitespace/commas
230
+ * allowed as separators). If the text contains non-numeric words the positional
231
+ * extractor attaches a `_parseWarnings` flag so downstream can route it to a review
232
+ * channel instead of treating it as authoritative data.
233
+ * - Negative values are accepted (not thrown away) but flagged — could be valid in
234
+ * some edge-case accounting or a data error; the server is the authority on validity.
235
+ * - We NEVER silently corrupt: if we can't parse 4 pillars we throw. If we parse but
236
+ * something looks suspicious we surface it in `_parseWarnings` on the returned object.
237
+ */
238
+ export function parsePillars(text) {
239
+ const t = String(text || "").trim();
240
+ const pw = []; // parse warnings to attach
241
+
242
+ // ── JSON path ──────────────────────────────────────────────────────────────
243
+ try {
244
+ const j = JSON.parse(t);
245
+ if (j && typeof j === "object" && !Array.isArray(j)) {
246
+ const g = (...keys) => {
247
+ for (const k of keys) if (j[k] != null) return j[k];
248
+ return null;
249
+ };
250
+ const input = g(
251
+ "input",
252
+ "tokens_input_fresh",
253
+ "inputTokens",
254
+ "input_tokens",
255
+ );
256
+ const output = g(
257
+ "output",
258
+ "tokens_output",
259
+ "outputTokens",
260
+ "output_tokens",
261
+ );
262
+ const cacheCreate = g(
263
+ "cacheCreate",
264
+ "tokens_cache_creation",
265
+ "cache_creation_tokens",
266
+ );
267
+ const cacheRead = g(
268
+ "cacheRead",
269
+ "tokens_cache_read",
270
+ "cache_read_tokens",
271
+ );
272
+ if ([input, output, cacheCreate, cacheRead].every((v) => v != null)) {
273
+ const pillars = {
274
+ input: Number(input),
275
+ output: Number(output),
276
+ cacheCreate: Number(cacheCreate),
277
+ cacheRead: Number(cacheRead),
278
+ };
279
+ if (
280
+ [
281
+ pillars.input,
282
+ pillars.output,
283
+ pillars.cacheCreate,
284
+ pillars.cacheRead,
285
+ ].some((v) => !Number.isFinite(v))
286
+ )
287
+ throw new Error(
288
+ "Non-numeric pillar value in JSON (got string or non-finite number).",
289
+ );
290
+ if (
291
+ [
292
+ pillars.input,
293
+ pillars.output,
294
+ pillars.cacheCreate,
295
+ pillars.cacheRead,
296
+ ].some((v) => v < 0)
297
+ )
298
+ pw.push(
299
+ "negative_pillar: one or more pillars is negative — may be a data error",
300
+ );
301
+ if (pw.length > 0) pillars._parseWarnings = pw;
302
+ return pillars;
303
+ }
304
+ }
305
+ } catch (e) {
306
+ // JSON.parse syntax error — fall through to positional. Re-throw parse errors we raised ourselves.
307
+ if (e.message.startsWith("Non-numeric")) throw e;
308
+ }
309
+
310
+ // ── Positional path ────────────────────────────────────────────────────────
311
+ // Guard: if the text contains alphabetic words, the numeric extraction is unreliable.
312
+ // We still attempt it (don't throw away the data) but flag it for review.
313
+ if (/[a-zA-Z]/.test(t))
314
+ pw.push(
315
+ "positional_from_mixed_text: extracted numbers from text that contains alphabetic characters — verify these are the correct 4 pillars",
316
+ );
317
+
318
+ const nums = (t.match(/-?\d[\d,]*\.?\d*/g) || []).map((s) =>
319
+ Number(s.replace(/,/g, "")),
320
+ );
321
+ if (nums.length >= 4) {
322
+ const [input, output, cacheCreate, cacheRead] = nums;
323
+ if (nums.length > 4)
324
+ pw.push(
325
+ `positional_extra_numbers: found ${nums.length} numbers, using first 4 — inspect for positional order error`,
326
+ );
327
+ const pillars = { input, output, cacheCreate, cacheRead };
328
+ if ([input, output, cacheCreate, cacheRead].some((v) => v < 0))
329
+ pw.push(
330
+ "negative_pillar: one or more pillars is negative — may be a data error",
331
+ );
332
+ if (pw.length > 0) pillars._parseWarnings = pw;
333
+ return pillars;
334
+ }
335
+ throw new Error(
336
+ "Could not parse 4 token pillars (input, output, cacheCreate, cacheRead) from the input.",
337
+ );
338
+ }
package/cascade.mjs CHANGED
@@ -1,274 +1 @@
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
- }
1
+ export * from './analytics/cascade.mjs';