squeezr-ai 1.90.0 → 1.99.2

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 (43) hide show
  1. package/dist/__tests__/compressor.test.js +5 -1
  2. package/dist/__tests__/contentRouter.test.d.ts +1 -0
  3. package/dist/__tests__/contentRouter.test.js +76 -0
  4. package/dist/__tests__/controlEndpointGuard.test.d.ts +1 -0
  5. package/dist/__tests__/controlEndpointGuard.test.js +61 -0
  6. package/dist/__tests__/doctor.test.js +17 -1
  7. package/dist/__tests__/jsonCrush.test.js +59 -1
  8. package/dist/__tests__/outputSavings.test.d.ts +1 -0
  9. package/dist/__tests__/outputSavings.test.js +49 -0
  10. package/dist/__tests__/relevance.test.d.ts +1 -0
  11. package/dist/__tests__/relevance.test.js +66 -0
  12. package/dist/__tests__/secureKeyFile.test.d.ts +1 -0
  13. package/dist/__tests__/secureKeyFile.test.js +27 -0
  14. package/dist/__tests__/serverBinding.test.d.ts +1 -0
  15. package/dist/__tests__/serverBinding.test.js +18 -0
  16. package/dist/__tests__/statsOutput.test.d.ts +1 -0
  17. package/dist/__tests__/statsOutput.test.js +46 -0
  18. package/dist/__tests__/textCrusher.test.js +63 -1
  19. package/dist/codexMitm.d.ts +1 -0
  20. package/dist/codexMitm.js +26 -2
  21. package/dist/compressor.js +31 -3
  22. package/dist/contentRouter.d.ts +30 -0
  23. package/dist/contentRouter.js +112 -0
  24. package/dist/dashboard.d.ts +1 -1
  25. package/dist/dashboard.js +26 -7
  26. package/dist/deterministic.d.ts +1 -1
  27. package/dist/deterministic.js +14 -13
  28. package/dist/doctor.d.ts +5 -0
  29. package/dist/doctor.js +11 -0
  30. package/dist/index.js +4 -1
  31. package/dist/jsonCrush.d.ts +4 -0
  32. package/dist/jsonCrush.js +83 -18
  33. package/dist/outputSavings.d.ts +20 -0
  34. package/dist/outputSavings.js +52 -0
  35. package/dist/relevance.d.ts +27 -0
  36. package/dist/relevance.js +78 -0
  37. package/dist/server.js +84 -11
  38. package/dist/stats.d.ts +17 -0
  39. package/dist/stats.js +55 -0
  40. package/dist/systemPrompt.js +3 -2
  41. package/dist/textCrusher.d.ts +4 -0
  42. package/dist/textCrusher.js +69 -7
  43. package/package.json +69 -69
package/dist/dashboard.js CHANGED
@@ -430,7 +430,7 @@ code{font-family:'Cascadia Code','SF Mono',Consolas,monospace;font-size:.9em}
430
430
  <div class="hero-card">
431
431
  <div class="hc-label">Requests</div>
432
432
  <div class="hc-val" id="h-reqs">—</div>
433
- <div class="hc-sub"><span id="h-comp">—</span> AI-compressed · det. always on</div>
433
+ <div class="hc-sub"><span id="h-comp">—</span> AI-compressed · det. always on<span id="h-bypassed"></span></div>
434
434
  </div>
435
435
  </div>
436
436
 
@@ -498,16 +498,16 @@ code{font-family:'Cascadia Code','SF Mono',Consolas,monospace;font-size:.9em}
498
498
  <div style="margin-top:8px;font-size:11px;color:var(--text3);text-align:center">0 here is normal when AI compression is off</div>
499
499
  </div>
500
500
  </div>
501
- <!-- AI Compression -->
501
+ <!-- Output shaping (output-side token reduction) — pinned in the old AI-Compression slot -->
502
502
  <div class="section" style="margin:0">
503
- <div class="section-head"><span class="section-title">AI Compression</span><span style="font-size:11px;color:var(--text3)">today · persisted</span></div>
503
+ <div class="section-head"><span class="section-title">Output</span><span style="font-size:11px;color:var(--text3)" id="out-note">what the model writes back · needs [output] enabled</span></div>
504
504
  <div class="section-body">
505
505
  <div class="cache-row">
506
- <div class="cache-card"><div class="cache-label">Calls</div><div class="cache-val" id="ai-calls">—</div></div>
507
- <div class="cache-card"><div class="cache-label">Saved</div><div class="cache-val" id="ai-saved" style="color:var(--brand2)">—</div></div>
508
- <div class="cache-card"><div class="cache-label">Spent</div><div class="cache-val" id="ai-spent">—</div></div>
506
+ <div class="cache-card"><div class="cache-label">Echo (restated)</div><div class="cache-val" id="out-echo">—</div></div>
507
+ <div class="cache-card"><div class="cache-label">Steered</div><div class="cache-val" id="out-steered">—</div></div>
508
+ <div class="cache-card"><div class="cache-label">Effort lowered</div><div class="cache-val" id="out-effort">—</div></div>
509
509
  </div>
510
- <div id="ai-net" style="margin-top:8px;font-size:11px;color:var(--text3);text-align:center">—</div>
510
+ <div style="margin-top:8px;font-size:11px;color:var(--text3);text-align:center">Echo = % of the model's output that merely restated context it was already given (lower is better).</div>
511
511
  </div>
512
512
  </div>
513
513
  </div>
@@ -1022,6 +1022,9 @@ function render(d) {
1022
1022
  document.getElementById('h-cost').textContent = fmtUsd(tCost);
1023
1023
  document.getElementById('h-reqs').textContent = fmt(tReqs);
1024
1024
  document.getElementById('h-comp').textContent = fmt(tComps);
1025
+ var byp = (d.bypassed_requests != null) ? d.bypassed_requests : 0;
1026
+ var bypEl = document.getElementById('h-bypassed');
1027
+ if (bypEl) bypEl.textContent = byp > 0 ? ' · ' + fmt(byp) + ' bypassed (not counted)' : '';
1025
1028
  var perEl = document.getElementById('overview-period');
1026
1029
  if (perEl && today.date) perEl.textContent = 'today · ' + today.date;
1027
1030
  // (No quality banner in the Overview — only genuine quality issues, high expand
@@ -1118,6 +1121,22 @@ var eff = (today.efficiency_pct != null) ? today.efficiency_pct : null;
1118
1121
  }
1119
1122
  }
1120
1123
  }
1124
+ // Output shaping card (all-time; only meaningful when [output] enabled)
1125
+ var out = d.output || {};
1126
+ var setOut = function(id, v){ var e = document.getElementById(id); if(e) e.textContent = v; };
1127
+ var echoEl = document.getElementById('out-echo');
1128
+ if (echoEl) {
1129
+ if (out.echo_samples > 0) {
1130
+ echoEl.textContent = out.avg_echo_pct + '%';
1131
+ // lower echo is better: green under 15%, amber 15-35%, red above
1132
+ echoEl.style.color = out.avg_echo_pct <= 15 ? 'var(--brand2)' : out.avg_echo_pct <= 35 ? '#fbbf24' : 'var(--red, #e5484d)';
1133
+ } else {
1134
+ echoEl.textContent = '—';
1135
+ }
1136
+ }
1137
+ setOut('out-steered', out.steered > 0 ? fmt(out.steered) : '—');
1138
+ setOut('out-effort', out.effort_lowered > 0 ? fmt(out.effort_lowered) : '—');
1139
+
1121
1140
  // Cost Comparison is OVERVIEW = TODAY: use today's tokens (not all-time), so the
1122
1141
  // sub-lines match the today money. (modelCosts already comes from today's models.)
1123
1142
  var cmpIn = today.original_tokens || 0;
@@ -34,4 +34,4 @@ export declare function preprocessAssistant(text: string): string;
34
34
  export type CodeLang = 'ts' | 'py' | 'go' | 'rs' | 'java' | 'c' | 'cpp';
35
35
  export declare function detectCodeLanguage(text: string): CodeLang | null;
36
36
  export declare function extractCodeStructure(text: string, lang: CodeLang): string;
37
- export declare function preprocessForTool(text: string, toolName: string, pressure?: number): string;
37
+ export declare function preprocessForTool(text: string, toolName: string, pressure?: number, query?: string): string;
@@ -27,7 +27,7 @@
27
27
  * Glob — compact large file listings into directory summary
28
28
  */
29
29
  import { storeOriginal, storeSegments } from './expand.js';
30
- import { crushJsonArrays } from './jsonCrush.js';
30
+ import { crushEmbeddedJson } from './contentRouter.js';
31
31
  import { crushText } from './textCrusher.js';
32
32
  // ── Pattern hit tracking (for squeezr discover) ───────────────────────────────
33
33
  export const detPatternHits = {};
@@ -928,13 +928,13 @@ function extractGenericErrors(text) {
928
928
  // (collapse near-duplicate spam, keep head/tail + high-signal lines) over blind
929
929
  // tail-keep — deterministic and cache-safe. Falls back to tail-keep if the crusher
930
930
  // can't beat it (e.g. all lines unique and low-signal).
931
- function truncateLongOutput(text, pressure = 0) {
931
+ function truncateLongOutput(text, pressure = 0, query = '') {
932
932
  const threshold = pressure >= 0.9 ? 50 : 80;
933
933
  const keepLines = pressure >= 0.9 ? 30 : 50;
934
934
  const lines = text.split('\n');
935
935
  if (lines.length <= threshold)
936
936
  return text;
937
- const crushed = crushText(text, { maxLines: keepLines, headKeep: 5, tailKeep: 8 });
937
+ const crushed = crushText(text, { maxLines: keepLines, headKeep: 5, tailKeep: 8, query });
938
938
  if (crushed.dropped > 0 && crushed.text.length < text.length) {
939
939
  hit('textCrush');
940
940
  return crushed.text;
@@ -959,7 +959,7 @@ function compactFileListing(text) {
959
959
  const summary = Object.entries(byDir).sort(([, a], [, b]) => b - a).map(([d, n]) => `${d}/ (${n} files)`).join('\n');
960
960
  return `${lines.length} files total:\n${summary}`;
961
961
  }
962
- function applyBashPatterns(text, pressure = 0) {
962
+ function applyBashPatterns(text, pressure = 0, query = '') {
963
963
  if (looksLikeGitDiff(text)) {
964
964
  hit('gitDiff');
965
965
  return compactGitDiff(text, pressure);
@@ -1083,7 +1083,7 @@ function applyBashPatterns(text, pressure = 0) {
1083
1083
  return errExtracted;
1084
1084
  }
1085
1085
  hit('truncated');
1086
- return truncateLongOutput(text, pressure);
1086
+ return truncateLongOutput(text, pressure, query);
1087
1087
  }
1088
1088
  // ── Grep tool ─────────────────────────────────────────────────────────────────
1089
1089
  // Group matches by file, cap at MAX_PER_FILE lines each, truncate if too many files
@@ -1344,7 +1344,7 @@ function makeRecoverable(original, compacted) {
1344
1344
  const id = storeOriginal(original);
1345
1345
  return `${compacted}\n[squeezr_expand("${id}") — full untruncated output]`;
1346
1346
  }
1347
- export function preprocessForTool(text, toolName, pressure = 0) {
1347
+ export function preprocessForTool(text, toolName, pressure = 0, query = '') {
1348
1348
  const tool = toolName.toLowerCase();
1349
1349
  // NEVER compress the result of an expand call. The whole point of squeezr_expand
1350
1350
  // is to deliver the FULL original back to the model; re-compacting it here (or
@@ -1354,19 +1354,20 @@ export function preprocessForTool(text, toolName, pressure = 0) {
1354
1354
  if (tool === 'read') {
1355
1355
  return makeRecoverable(text, compactReadOutput(preprocessRead(text)));
1356
1356
  }
1357
- // JSON array crush (deterministic, cache-safe, reversible). Fires only when the whole
1358
- // result is an array of homogeneous objects (gh api / curl / MCP record lists / kubectl
1359
- // -o json…): the repeated per-row keys collapse to a single header. Runs BEFORE the
1360
- // base pipeline so line-dedup/JSON-minify never touch the structured data. Self-recoverable
1361
- // (its marker already carries squeezr_expand), so we return it directly.
1362
- const crushed = crushJsonArrays(text);
1357
+ // JSON array crush (deterministic, cache-safe, reversible). Now via the content router,
1358
+ // so it fires on arrays of homogeneous objects whether they are the WHOLE result or
1359
+ // EMBEDDED inside a larger one (gh api with a header, MCP envelopes, bash echo + json…).
1360
+ // Repeated per-row keys collapse to a single header; each crushed span is self-recoverable
1361
+ // (its marker carries squeezr_expand). Runs BEFORE the base pipeline so line-dedup/JSON-minify
1362
+ // never touch the structured data; returned directly.
1363
+ const crushed = crushEmbeddedJson(text);
1363
1364
  if (crushed.savedChars > 0) {
1364
1365
  hit('jsonTableCrush');
1365
1366
  return crushed.text;
1366
1367
  }
1367
1368
  let t = preprocess(text);
1368
1369
  if (tool === 'bash') {
1369
- t = applyBashPatterns(t, pressure);
1370
+ t = applyBashPatterns(t, pressure, query);
1370
1371
  }
1371
1372
  else if (tool === 'grep') {
1372
1373
  const before = t;
package/dist/doctor.d.ts CHANGED
@@ -18,11 +18,16 @@ type Health = {
18
18
  version?: string;
19
19
  bypassed?: boolean;
20
20
  port?: number;
21
+ compression?: {
22
+ requests?: number;
23
+ savings_pct?: number;
24
+ };
21
25
  } | null;
22
26
  export declare function checkProxy(health: Health): CheckResult;
23
27
  export declare function checkVersion(installed: string, running: string | null): CheckResult;
24
28
  export declare function checkEnv(baseUrl: string | undefined, port: number): CheckResult;
25
29
  export declare function checkBypass(bypassed: boolean | undefined): CheckResult;
30
+ export declare function checkSavings(requests: number | undefined, savingsPct: number | undefined): CheckResult;
26
31
  export declare function computeExitCode(checks: CheckResult[]): number;
27
32
  export declare function formatDoctor(checks: CheckResult[], exitCode: number): string;
28
33
  export declare function runDoctor(): Promise<{
package/dist/doctor.js CHANGED
@@ -42,6 +42,16 @@ export function checkBypass(bypassed) {
42
42
  return { name: 'Bypass', status: 'warn', detail: 'bypass is ON — compression is disabled (squeezr bypass --off to resume)' };
43
43
  return { name: 'Bypass', status: 'pass', detail: 'compression active' };
44
44
  }
45
+ export function checkSavings(requests, savingsPct) {
46
+ if (requests === undefined)
47
+ return { name: 'Savings', status: 'skip', detail: 'proxy not running' };
48
+ if (requests === 0)
49
+ return { name: 'Savings', status: 'skip', detail: 'no traffic yet' };
50
+ if (!savingsPct || savingsPct <= 0) {
51
+ return { name: 'Savings', status: 'warn', detail: `${requests} requests seen but ~0% saved — bypass on? stale build? all content incompressible?` };
52
+ }
53
+ return { name: 'Savings', status: 'pass', detail: `~${Math.round(savingsPct)}% saved over ${requests} requests` };
54
+ }
45
55
  export function computeExitCode(checks) {
46
56
  if (checks.some(c => c.status === 'fail'))
47
57
  return 2;
@@ -96,6 +106,7 @@ export async function runDoctor() {
96
106
  checkVersion(installedVersion(), runningVersion),
97
107
  checkEnv(process.env.ANTHROPIC_BASE_URL, health?.port ?? port),
98
108
  checkBypass(health?.bypassed),
109
+ checkSavings(health?.compression?.requests, health?.compression?.savings_pct),
99
110
  ];
100
111
  const exitCode = computeExitCode(checks);
101
112
  return { checks, exitCode, report: formatDoctor(checks, exitCode) };
package/dist/index.js CHANGED
@@ -46,7 +46,10 @@ if (PORT !== config.port) {
46
46
  const httpServer = createAdaptorServer({ fetch: app.fetch });
47
47
  // Persist caches every 60s so a crash doesn't lose more than a minute of work
48
48
  setInterval(() => { persistSessionCache(); persistExpandStore(); persistHistory(); }, 60_000).unref();
49
- httpServer.listen(PORT, () => {
49
+ // Bind to loopback only: the proxy holds the user's API keys and exposes
50
+ // state-changing control endpoints, so it must never be reachable from the LAN.
51
+ // Matches the desktop proxy / MITM listeners, which already bind 127.0.0.1.
52
+ httpServer.listen(PORT, '127.0.0.1', () => {
50
53
  console.log(`Squeezr v${VERSION} listening on http://localhost:${PORT}`);
51
54
  console.log(`Mode: ${config.dryRun ? 'dry-run' : 'active'}`);
52
55
  if (config.disabled)
@@ -20,6 +20,10 @@
20
20
  * (the lossy SmartCrusher path) is intentionally NOT done here.
21
21
  */
22
22
  export declare const TABLE_MARKER_RE: RegExp;
23
+ /** 32-bit SimHash fingerprint of a text (bit-voting over its word tokens). Two texts
24
+ * that share most tokens get fingerprints a small Hamming distance apart. */
25
+ export declare function simhash32(text: string): number;
26
+ export declare function hammingDistance(a: number, b: number): number;
23
27
  /**
24
28
  * If `text` is (entirely) a crushable JSON array of objects, return the table form plus
25
29
  * the char count saved. Otherwise return the input unchanged with savedChars = 0.
package/dist/jsonCrush.js CHANGED
@@ -23,11 +23,73 @@ import { storeOriginal } from './expand.js';
23
23
  const MIN_ITEMS = 5;
24
24
  const MIN_OBJECT_FRACTION = 0.8; // ≥80% of elements must be plain objects
25
25
  const MIN_SAVINGS_RATIO = 0.15; // only crush if it shaves ≥15% of the chars
26
- // Recognises the table header this module emits. Group 1 = the expand id.
27
- export const TABLE_MARKER_RE = /^\[squeezr:table ([0-9a-f]{6,}(?:~\d+)?) —/;
26
+ const LOSSY_ROW_THRESHOLD = 50; // above this many rows, drop near-duplicates (SimHash)
27
+ const HAMMING_NEAR_DUP = 3; // rows within this 32-bit Hamming distance are "the same"
28
+ const ROW_SIGNAL_RE = /\b(error|err|fail|failed|failure|exception|fatal|panic|denied|refused|timeout|critical|crash|oom|unhealthy|degraded)\b/i;
29
+ // Recognises the table header this module emits (anywhere in the text, since the table
30
+ // may be embedded in a larger tool result). Group 1 = the expand id.
31
+ export const TABLE_MARKER_RE = /\[squeezr:table ([0-9a-f]{6,}(?:~\d+)?) —/;
28
32
  function isPlainObject(v) {
29
33
  return typeof v === 'object' && v !== null && !Array.isArray(v);
30
34
  }
35
+ // ── SimHash near-duplicate detection (deterministic, dependency-free) ─────────
36
+ /** FNV-1a 32-bit hash of a token. */
37
+ function fnv1a32(s) {
38
+ let h = 0x811c9dc5;
39
+ for (let i = 0; i < s.length; i++) {
40
+ h ^= s.charCodeAt(i);
41
+ h = Math.imul(h, 0x01000193);
42
+ }
43
+ return h >>> 0;
44
+ }
45
+ /** 32-bit SimHash fingerprint of a text (bit-voting over its word tokens). Two texts
46
+ * that share most tokens get fingerprints a small Hamming distance apart. */
47
+ export function simhash32(text) {
48
+ const tokens = text.toLowerCase().match(/[a-z0-9_]+/g) ?? [];
49
+ if (tokens.length === 0)
50
+ return 0;
51
+ const bits = new Array(32).fill(0);
52
+ for (const t of tokens) {
53
+ const h = fnv1a32(t);
54
+ for (let b = 0; b < 32; b++)
55
+ bits[b] += ((h >>> b) & 1) ? 1 : -1;
56
+ }
57
+ let f = 0;
58
+ for (let b = 0; b < 32; b++)
59
+ if (bits[b] > 0)
60
+ f |= (1 << b);
61
+ return f >>> 0;
62
+ }
63
+ export function hammingDistance(a, b) {
64
+ let x = (a ^ b) >>> 0;
65
+ let c = 0;
66
+ while (x) {
67
+ c += x & 1;
68
+ x >>>= 1;
69
+ }
70
+ return c;
71
+ }
72
+ /**
73
+ * Given the per-row rendered strings, return the indices to KEEP: one representative per
74
+ * SimHash cluster (near-duplicates dropped), plus every row that carries an error/anomaly
75
+ * signal (always kept). Order preserved. Deterministic.
76
+ */
77
+ function selectRepresentativeRows(rowStrings) {
78
+ const reps = []; // simhash fingerprints of chosen representatives
79
+ const kept = [];
80
+ for (let i = 0; i < rowStrings.length; i++) {
81
+ if (ROW_SIGNAL_RE.test(rowStrings[i])) {
82
+ kept.push(i);
83
+ continue;
84
+ } // anomaly → always keep
85
+ const sig = simhash32(rowStrings[i]);
86
+ if (reps.some(r => hammingDistance(r, sig) <= HAMMING_NEAR_DUP))
87
+ continue; // near-dup → drop
88
+ reps.push(sig);
89
+ kept.push(i);
90
+ }
91
+ return kept;
92
+ }
31
93
  /** Union of keys across rows, in first-seen order (deterministic). */
32
94
  function collectColumns(rows) {
33
95
  const cols = [];
@@ -86,27 +148,30 @@ export function crushJsonArrays(text) {
86
148
  if (cols.length === 0)
87
149
  return { text, savedChars: 0 };
88
150
  const header = cols.join(CELL_DELIM);
89
- const bodyLines = [];
90
- for (const row of rows) {
91
- if (isPlainObject(row)) {
92
- bodyLines.push(cols.map(c => cell(row[c], c in row)).join(CELL_DELIM));
93
- }
94
- else {
95
- // Non-object element inside a mostly-object array: keep it verbatim as JSON on
96
- // its own line so nothing is lost (recoverable in full via expand anyway).
97
- bodyLines.push(JSON.stringify(row));
151
+ const rowLines = rows.map(row => isPlainObject(row)
152
+ ? cols.map(c => cell(row[c], c in row)).join(CELL_DELIM)
153
+ // Non-object element inside a mostly-object array: keep it verbatim as JSON so
154
+ // nothing is lost (recoverable in full via expand anyway).
155
+ : JSON.stringify(row));
156
+ // Lossy row-drop for LARGE arrays: collapse near-duplicate rows (SimHash), always
157
+ // keeping anomaly/error rows. Deterministic → cache-safe. The full original is in the
158
+ // expand store, so dropped rows are recoverable. Small arrays keep every row.
159
+ let bodyLines = rowLines;
160
+ let omitted = 0;
161
+ if (rows.length > LOSSY_ROW_THRESHOLD) {
162
+ const kept = selectRepresentativeRows(rowLines);
163
+ if (kept.length < rowLines.length) {
164
+ bodyLines = kept.map(i => rowLines[i]);
165
+ omitted = rowLines.length - kept.length;
98
166
  }
99
167
  }
100
- // Build the (id-less) body first to measure real savings before we pay for an id.
101
- const provisional = `${header}\n${bodyLines.join('\n')}`;
102
- const saved = text.length - provisional.length;
103
- if (saved <= 0 || saved / text.length < MIN_SAVINGS_RATIO)
104
- return { text, savedChars: 0 };
105
168
  const id = storeOriginal(text);
106
- const marker = `[squeezr:table ${id} — ${rows.length} rows × ${cols.length} cols; squeezr_expand("${id}") for original JSON]`;
169
+ const marker = omitted > 0
170
+ ? `[squeezr:table ${id} — showing ${bodyLines.length} of ${rows.length} rows × ${cols.length} cols; ${omitted} near-duplicate rows omitted; squeezr_expand("${id}") for original JSON]`
171
+ : `[squeezr:table ${id} — ${rows.length} rows × ${cols.length} cols; squeezr_expand("${id}") for original JSON]`;
107
172
  const out = `${marker}\n${header}\n${bodyLines.join('\n')}`;
108
173
  const savedChars = text.length - out.length;
109
- if (savedChars <= 0)
174
+ if (savedChars <= 0 || savedChars / text.length < MIN_SAVINGS_RATIO)
110
175
  return { text, savedChars: 0 };
111
176
  return { text: out, savedChars };
112
177
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Output-savings signals — Squeezr's honest, no-counterfactual take on headroom's output
3
+ * measurement. The fully honest number (headroom's holdout A/B) needs a control arm; that
4
+ * is deferred. This ships the tier that needs NO counterfactual: the ECHO RATIO — how much
5
+ * of what the model just wrote merely restated context it was already given.
6
+ *
7
+ * High echo = wasted output tokens = exactly what verbosity steering (1.83) targets. Reading
8
+ * the response to measure this never changes the bytes we forward, so it's cache-safe.
9
+ */
10
+ /** Word n-grams (set) of a text. */
11
+ export declare function wordNgrams(text: string, n?: number): Set<string>;
12
+ /**
13
+ * Fraction of the OUTPUT's n-grams that also appear in the CONTEXT (0..1). 1 = the model
14
+ * only restated what it was already shown; 0 = fully novel output.
15
+ */
16
+ export declare function echoRatio(output: string, context: string, n?: number): number;
17
+ /** Pull the assistant's text out of an Anthropic SSE stream (concatenated text_delta). */
18
+ export declare function extractAssistantTextFromSse(sse: string): string;
19
+ /** Assistant text from a non-streaming Anthropic response body's content blocks. */
20
+ export declare function extractAssistantTextFromContent(content: unknown): string;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Output-savings signals — Squeezr's honest, no-counterfactual take on headroom's output
3
+ * measurement. The fully honest number (headroom's holdout A/B) needs a control arm; that
4
+ * is deferred. This ships the tier that needs NO counterfactual: the ECHO RATIO — how much
5
+ * of what the model just wrote merely restated context it was already given.
6
+ *
7
+ * High echo = wasted output tokens = exactly what verbosity steering (1.83) targets. Reading
8
+ * the response to measure this never changes the bytes we forward, so it's cache-safe.
9
+ */
10
+ /** Word n-grams (set) of a text. */
11
+ export function wordNgrams(text, n = 3) {
12
+ const words = text.toLowerCase().match(/[a-z0-9_]+/g) ?? [];
13
+ const set = new Set();
14
+ for (let i = 0; i + n <= words.length; i++)
15
+ set.add(words.slice(i, i + n).join(' '));
16
+ return set;
17
+ }
18
+ /**
19
+ * Fraction of the OUTPUT's n-grams that also appear in the CONTEXT (0..1). 1 = the model
20
+ * only restated what it was already shown; 0 = fully novel output.
21
+ */
22
+ export function echoRatio(output, context, n = 3) {
23
+ const out = wordNgrams(output, n);
24
+ if (out.size === 0)
25
+ return 0;
26
+ const ctx = wordNgrams(context, n);
27
+ let echoed = 0;
28
+ for (const g of out)
29
+ if (ctx.has(g))
30
+ echoed++;
31
+ return echoed / out.size;
32
+ }
33
+ /** Pull the assistant's text out of an Anthropic SSE stream (concatenated text_delta). */
34
+ export function extractAssistantTextFromSse(sse) {
35
+ let out = '';
36
+ for (const m of sse.matchAll(/"type":"text_delta","text":"((?:[^"\\]|\\.)*)"/g)) {
37
+ try {
38
+ out += JSON.parse(`"${m[1]}"`);
39
+ }
40
+ catch { /* skip malformed delta */ }
41
+ }
42
+ return out;
43
+ }
44
+ /** Assistant text from a non-streaming Anthropic response body's content blocks. */
45
+ export function extractAssistantTextFromContent(content) {
46
+ if (!Array.isArray(content))
47
+ return '';
48
+ return content
49
+ .filter(b => b && b.type === 'text' && typeof b.text === 'string')
50
+ .map(b => b.text)
51
+ .join('');
52
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * BM25 relevance scorer — a shared primitive so Squeezr's compressors can keep content
3
+ * by RELEVANCE to the current task, not just by position (head/tail) or recency.
4
+ *
5
+ * This is the single highest-leverage idea copied from headroom: their SmartCrusher,
6
+ * TextCrusher and CodeCompressor all route "what to keep" through one relevance scorer.
7
+ * Squeezr, until now, kept blindly. This module is that scorer — pure TS, zero deps,
8
+ * deterministic (→ cache-safe when used to drive byte-stable compression).
9
+ *
10
+ * BM25 (Okapi) is the standard, well-understood ranking function: term frequency with
11
+ * saturation (k1) and document-length normalization (b), weighted by inverse document
12
+ * frequency so rare query terms (an error code, a filename) count far more than common
13
+ * words. The idf uses +0.5 smoothing so it is always positive.
14
+ */
15
+ export interface BM25Options {
16
+ k1?: number;
17
+ b?: number;
18
+ }
19
+ /** Lowercase, split on non-alphanumerics, keep tokens of length >= 2. */
20
+ export declare function tokenize(text: string): string[];
21
+ /**
22
+ * BM25 score of each doc in `docs` against `query`. One score per doc, in order.
23
+ * A doc with no query-term overlap scores exactly 0.
24
+ */
25
+ export declare function bm25Scores(query: string, docs: string[], opts?: BM25Options): number[];
26
+ /** Document indices sorted by descending relevance (ties keep original order). */
27
+ export declare function rankByRelevance(query: string, docs: string[], opts?: BM25Options): number[];
@@ -0,0 +1,78 @@
1
+ /**
2
+ * BM25 relevance scorer — a shared primitive so Squeezr's compressors can keep content
3
+ * by RELEVANCE to the current task, not just by position (head/tail) or recency.
4
+ *
5
+ * This is the single highest-leverage idea copied from headroom: their SmartCrusher,
6
+ * TextCrusher and CodeCompressor all route "what to keep" through one relevance scorer.
7
+ * Squeezr, until now, kept blindly. This module is that scorer — pure TS, zero deps,
8
+ * deterministic (→ cache-safe when used to drive byte-stable compression).
9
+ *
10
+ * BM25 (Okapi) is the standard, well-understood ranking function: term frequency with
11
+ * saturation (k1) and document-length normalization (b), weighted by inverse document
12
+ * frequency so rare query terms (an error code, a filename) count far more than common
13
+ * words. The idf uses +0.5 smoothing so it is always positive.
14
+ */
15
+ const DEFAULT_K1 = 1.5;
16
+ const DEFAULT_B = 0.75;
17
+ /** Lowercase, split on non-alphanumerics, keep tokens of length >= 2. */
18
+ export function tokenize(text) {
19
+ const out = [];
20
+ for (const m of text.toLowerCase().matchAll(/[a-z0-9_]+/g)) {
21
+ if (m[0].length >= 2)
22
+ out.push(m[0]);
23
+ }
24
+ return out;
25
+ }
26
+ /**
27
+ * BM25 score of each doc in `docs` against `query`. One score per doc, in order.
28
+ * A doc with no query-term overlap scores exactly 0.
29
+ */
30
+ export function bm25Scores(query, docs, opts = {}) {
31
+ const k1 = opts.k1 ?? DEFAULT_K1;
32
+ const b = opts.b ?? DEFAULT_B;
33
+ const queryTerms = [...new Set(tokenize(query))];
34
+ const N = docs.length;
35
+ if (queryTerms.length === 0 || N === 0)
36
+ return new Array(N).fill(0);
37
+ // Tokenize docs once; compute lengths and average length.
38
+ const docTokens = docs.map(tokenize);
39
+ const docLen = docTokens.map(t => t.length);
40
+ const avgdl = docLen.reduce((s, l) => s + l, 0) / N || 1;
41
+ // Per-doc term-frequency maps.
42
+ const tf = docTokens.map(tokens => {
43
+ const m = new Map();
44
+ for (const t of tokens)
45
+ m.set(t, (m.get(t) ?? 0) + 1);
46
+ return m;
47
+ });
48
+ // Document frequency + idf per query term.
49
+ const idf = new Map();
50
+ for (const term of queryTerms) {
51
+ let df = 0;
52
+ for (const m of tf)
53
+ if (m.has(term))
54
+ df++;
55
+ // Always-positive smoothed idf.
56
+ idf.set(term, Math.log(1 + (N - df + 0.5) / (df + 0.5)));
57
+ }
58
+ return docTokens.map((_, i) => {
59
+ let score = 0;
60
+ const len = docLen[i];
61
+ for (const term of queryTerms) {
62
+ const f = tf[i].get(term) ?? 0;
63
+ if (f === 0)
64
+ continue;
65
+ const denom = f + k1 * (1 - b + (b * len) / avgdl);
66
+ score += (idf.get(term) ?? 0) * ((f * (k1 + 1)) / denom);
67
+ }
68
+ return score;
69
+ });
70
+ }
71
+ /** Document indices sorted by descending relevance (ties keep original order). */
72
+ export function rankByRelevance(query, docs, opts) {
73
+ const scores = bm25Scores(query, docs, opts);
74
+ return scores
75
+ .map((score, i) => ({ score, i }))
76
+ .sort((a, b) => (b.score - a.score) || (a.i - b.i))
77
+ .map(x => x.i);
78
+ }