eval(patch-probe): billing grid + OCR phase-sweep findings

Probe 1: both fable-5 and sonnet-5 bill vision on a 28x28 patch grid
(image_tokens = 3 + ceil(W/28)*ceil(H/28)); snap rows≡6 (mod 7),
cols≡4 (mod 28) to avoid stranding paid patch area.

Probe 2: patch-boundary straddling does NOT affect OCR accuracy
(cols: 4.47% vs 4.60%; rows: z=-0.45, 7-offset paired sweep with
line fixed effects). Real misread drivers: high-entropy runs
(bimodal derailment on base64 blobs), 5x8 confusables (w/W, 8/0),
line wraps. Harnesses: count-tokens-sweep, accuracy-phase-probe
(line-DP-aligned scoring), rescore-sweep (offline paired analysis).
This commit is contained in:
teamchong
2026-07-12 18:59:18 -04:00
parent 44b558ff94
commit 455ab636a2
8 changed files with 802 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
# Patch-grid probe findings
## Probe 1 — billing staircase (count_tokens, free) ✅
- Both `claude-fable-5` and `claude-sonnet-5` bill vision on a **28×28 px patch
grid**. The "Sonnet uses 32×32" rumor is false: image_tokens steps occur
exactly when W or H crosses a multiple of 28, on both axes, both models
(CSVs in this dir).
- Formula: `image_tokens = 3 + ceil(W/28) * ceil(H/28)` (fixed +3 per image).
Spot check at H=28: W=53..56 → 5 tokens (2 patches), W=57..60 → 6 (3 patches).
- The docs' `(W×H)/750` is just an approximation of 784 px²/patch (28²) plus
the constant.
- No server-side resample at ≤1568 px long edge: a 1.73 MP image bills the full
grid, so there is no ~1.15 MP cap kicking in — what we render is exactly what
the model sees.
## Geometry consequences for pxpipe (CELL 5×8, PAD_X = PAD_Y = 4)
- Height = 8 + 8·rows → hits a 28-multiple iff **rows ≡ 6 (mod 7)**
(every 7 rows = 56 px = exactly 2 patch rows).
`MAX_HEIGHT_PX = 728` at 90 rows = 26 patch rows, a perfect fit.
- Width = 8 + 5·cols (+ atlas slack) → hits a 28-multiple iff
**cols ≡ 4 (mod 28)** (every 28 cols = 140 px = 5 patch cols).
312 cols → 1568 px = 56 patch cols, also a perfect fit at the width cap.
- Full-cap page: 312 × 90 = 28,080 chars for 3 + 56·26 = **1459 tokens ≈ 19.2
chars/token** — the density ceiling for this geometry.
- Snapping rule: pick rows ≡ 6 (mod 7) and cols ≡ 4 (mod 28); anything else
strands already-paid patch area (up to 27 px per axis, ~13% of the bill).
- Phase structure: gcd(5,28)=1 → all 28 horizontal glyph↔patch phases occur in
every image; gcd(8,28)=4 → only 7 distinct vertical phases.
## Probe 2 — accuracy vs phase ✅ (verdict: phase alignment does NOT matter)
- Question: do glyphs straddling a patch boundary misread more? A 5 px glyph
straddles when `x mod 28 ≥ 24`; an 8 px glyph row straddles when
`y mod 28 ≥ 21`. If straddle phases dominate errors, phase-locked pitch
(fractional 5.6 / 9.33 px advances, ≈24% density cost) could pay for itself;
if flat, keep packed 5×8 and close the alignment theory.
- Method notes (pitfalls that produced false signals first):
- Pure-random char grids trip the safety layer ("looks like credentials") —
use real repo source as content.
- `temperature` is rejected by fable/sonnet-5 → sampling is stochastic;
single runs are NOT repeatable (same image scored 169 vs 237 errs).
- Models wrap/merge lines (sonnet emitted 101 lines for 90); positional
line pairing turns one slip into a phase-flat ~27% error smear. The
harness now does banded line-level DP alignment before char scoring
(sonnet went 72.54% → 99.87% on the identical response).
- Within one image, row phase aliases content line-type every 7 lines
(8·7 = 56 = 2 patches) → row buckets are confounded. Controlled sweep:
prepend k = 0..6 blank lines (shifts content by 8k px through all 7 row
phases, content identical), then score with per-line fixed effects
(`rescore-sweep.mjs`, offline, from dumped responses).
- **Results (claude-fable-5, 312×90 production geometry):**
- Columns (within-image, content-controlled by construction): straddle
4.47% vs aligned 4.60% on 3,691 chars — null, per-phase table flat.
- Rows (7-offset paired sweep, 439 line×run cells): straddle excess
0.16%±0.28 vs aligned +0.03%±0.29, **z = 0.45** — null.
- Real code at full page: fable 99.96% (1/2,276), sonnet 99.87% (3/2,276).
28,080 chars for 1,459 image tokens ≈ 19.2 chars/token with ~0.1% CER.
- **What actually causes misreads** (in error-yield order):
1. Long high-entropy runs (base64/hex blobs): bimodal derailment — the
same line on the same pixels scored 4% and 73% error across runs. The
decoder loses lock mid-run with no language prior to recover; this, not
geometry, is the production misread mechanism.
2. 5×8 confusables: `w→W` (11× in one page), `s→S`, `c→C`, `K→H`, `M→N`,
`8→0`, `(→O`, `:→.` — legibility floor, context-corrected in real code.
3. Line wraps on long lines — harmless after alignment, but consumers that
trust exact line numbers must re-align.
- Recommendations: keep packed 5×8 (fractional-advance idea rejected); snap
dims per Probe 1 for billing only; route/flag high-entropy lines (e.g.
>64 chars of base64-ish content) as literal text instead of pixels.
+192
View File
@@ -0,0 +1,192 @@
#!/usr/bin/env node
// Probe 2: OCR accuracy vs glyph↔patch phase, using the PRODUCTION renderer.
// gcd(CELL_W=5,28)=1 → one image contains all 28 horizontal phases;
// gcd(CELL_H=8,28)=4 → 7 vertical phases. Content is gibberish lowercase
// 4-letter groups (no language prior, not credential-shaped → avoids the
// refusal classifier that fires on mixed-case alnum strings). Every 5th cell
// is a space; cols must be a multiple of lcm(5,28)=140 so each phase sees an
// equal number of letter cells (4 letters + 1 space per phase per 140 cols).
//
// Usage: CC_OAUTH_TOKEN=... node accuracy-phase-probe.mjs [model] [cols] [rows] [seed]
// cols: multiple of 140, rows: multiple of 7.
// NOTE: real inference — costs output tokens.
import { writeFile } from 'node:fs/promises';
import { renderTextToPngs, PAD_X, PAD_Y, CELL_W, CELL_H } from '../../dist/core/render.js';
const model = process.argv[2] ?? 'claude-fable-5';
const COLS = +(process.argv[3] ?? 140);
const ROWS = +(process.argv[4] ?? 21);
const seedArg = process.argv[5] ?? '1';
const PADL = +(process.argv[6] ?? 0); // blank lines prepended to the IMAGE only: shifts row phase by PADL*CELL_H px, content identical
const fileMode = !/^\d+$/.test(seedArg); // non-numeric 5th arg = path to a real text file
const seed = fileMode ? 1 : +seedArg;
const P = 28;
if (!fileMode && (COLS % 140 || ROWS % 7)) console.error(`warn: cols%140=${COLS % 140} rows%7=${ROWS % 7} — phases unbalanced`);
let s = (seed >>> 0) || 1;
const rnd = () => (s ^= s << 13, s ^= s >>> 17, s ^= s << 5, (s >>> 0) / 2 ** 32);
// Random-order REAL dictionary words: refusal-safe, production-representative.
// Language prior is phase-independent, so straddle effects survive as relative
// error-rate differences. Phase balance is statistical, not exact (per-phase
// totals are tracked, so unevenness is handled in the rates).
const { readFileSync } = await import('node:fs');
let grid;
if (fileMode) {
// Production-representative content: real source text, pre-wrapped to COLS.
grid = readFileSync(seedArg, 'utf8').split('\n')
.map(l => l.replace(/\t/g, ' ').replace(/[^\x20-\x7e]/g, '?').trimEnd().slice(0, COLS))
.filter(l => l.trim().length >= 8)
.slice(0, ROWS);
if (grid.length < ROWS) console.error(`warn: file only yielded ${grid.length} lines`);
} else {
const WORDS = readFileSync('/usr/share/dict/words', 'utf8').split('\n')
.filter(w => /^[a-z]{3,7}$/.test(w));
grid = Array.from({ length: ROWS }, () => {
let line = '';
for (;;) {
const w = WORDS[(rnd() * WORDS.length) | 0];
if (line.length + w.length + (line ? 1 : 0) > COLS) break;
line += (line ? ' ' : '') + w;
}
return line;
});
}
const imgs = await renderTextToPngs('\n'.repeat(PADL) + grid.join('\n'), COLS);
if (PADL) console.error(`padLines=${PADL} (row shift ${PADL * CELL_H}px, phase +${(PADL * CELL_H) % 28} mod 28)`);
if (imgs.length !== 1) throw new Error(`expected 1 image, got ${imgs.length}`);
const img = imgs[0];
const png = img.png ?? img.data ?? img.buffer ?? Object.values(img).find(v => v instanceof Uint8Array);
if (!png) throw new Error('no png buffer; keys=' + Object.keys(img).join(','));
console.error(`image: ${img.width ?? '?'}x${img.height ?? '?'}px ${png.length}B; predicted image_tokens=` +
(img.width && img.height ? 3 + Math.ceil(img.width / P) * Math.ceil(img.height / P) : '?'));
const BASE = process.env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com';
const res = await fetch(`${BASE}/v1/messages`, {
method: 'POST',
headers: {
'authorization': `Bearer ${process.env.CC_OAUTH_TOKEN}`,
'anthropic-version': '2023-06-01',
'anthropic-beta': 'oauth-2025-04-20',
'content-type': 'application/json',
},
body: JSON.stringify({
model,
max_tokens: ROWS * (COLS + 2) + 1500,
system: "You are Claude Code, Anthropic's official CLI for Claude.",
messages: [{
role: 'user', content: [
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: Buffer.from(png).toString('base64') } },
{ type: 'text', text: `OCR this image to plain text. Preserve line breaks. Output only the text.` },
],
}],
}),
});
const j = await res.json();
if (!res.ok) { console.error('API error:', JSON.stringify(j)); process.exit(1); }
await writeFile(`/tmp/phase-probe-${model}-${Date.now()}.json`, JSON.stringify({ model, cols: COLS, rows: ROWS, seedArg, padLines: PADL, grid, resp: j }, null, 2));
const out = (j.content?.find(b => b.type === 'text')?.text ?? '')
.replace(/```[a-z]*\n?/g, '').split('\n').map(l => l.trimEnd()).filter(l => l.length);
console.error(`usage=${JSON.stringify(j.usage)} stop=${j.stop_reason} lines=${out.length}/${ROWS}`);
if (out.length < ROWS) console.error('rawTextHead: ' + JSON.stringify((j.content?.find(b => b.type === 'text')?.text ?? '').slice(0, 200)));
// Alignment-based scoring: Levenshtein backtrace marks which truth positions
// matched exactly; everything else (sub or indel) is an error at its truth pos.
function alignOk(truth, got, subs) {
const T = truth.length, G = got.length;
const dp = Array.from({ length: T + 1 }, () => new Array(G + 1).fill(0));
for (let i = 0; i <= T; i++) dp[i][0] = i;
for (let jj = 0; jj <= G; jj++) dp[0][jj] = jj;
for (let i = 1; i <= T; i++) for (let jj = 1; jj <= G; jj++)
dp[i][jj] = Math.min(dp[i - 1][jj - 1] + (truth[i - 1] === got[jj - 1] ? 0 : 1), dp[i - 1][jj] + 1, dp[i][jj - 1] + 1);
const ok = new Array(T).fill(false);
let i = T, jj = G;
while (i > 0 && jj > 0) {
if (dp[i][jj] === dp[i - 1][jj - 1] + (truth[i - 1] === got[jj - 1] ? 0 : 1)) {
if (truth[i - 1] === got[jj - 1]) ok[i - 1] = true;
else subs.set(`${truth[i - 1]}->${got[jj - 1]}`, (subs.get(`${truth[i - 1]}->${got[jj - 1]}`) ?? 0) + 1);
i--; jj--;
} else if (dp[i][jj] === dp[i - 1][jj] + 1) i--;
else jj--;
}
return ok;
}
// Line-level alignment BEFORE char scoring: the model may emit preamble lines,
// wrap long lines (1 truth : 2 out), or drop lines. Positional r->r pairing
// turns one such slip into a phase-flat error smear. Banded DP, merge-aware.
function lev(a, b) {
const m = b.length;
let prev = Array.from({ length: m + 1 }, (_, k) => k), cur = new Array(m + 1);
for (let i = 1; i <= a.length; i++) {
cur[0] = i;
for (let k = 1; k <= m; k++)
cur[k] = Math.min(prev[k - 1] + (a[i - 1] === b[k - 1] ? 0 : 1), prev[k] + 1, cur[k - 1] + 1);
[prev, cur] = [cur, prev];
}
return prev[m];
}
function alignLines(truth, got) {
const T = truth.length, G = got.length, BAND = 25, INF = 1e9;
const dp = Array.from({ length: T + 1 }, () => new Array(G + 1).fill(INF));
const bt = Array.from({ length: T + 1 }, () => new Array(G + 1).fill(null));
dp[0][0] = 0;
for (let jj = 1; jj <= G; jj++) { dp[0][jj] = dp[0][jj - 1] + 2; bt[0][jj] = ['spur']; }
for (let i = 1; i <= T; i++) {
dp[i][0] = dp[i - 1][0] + truth[i - 1].length; bt[i][0] = ['miss'];
for (let jj = Math.max(1, i - BAND); jj <= Math.min(G, i + BAND); jj++) {
let c = dp[i - 1][jj - 1] + lev(truth[i - 1], got[jj - 1]), b = ['m11'];
if (jj >= 2 && dp[i - 1][jj - 2] < INF) {
const merged = [got[jj - 2] + ' ' + got[jj - 1], got[jj - 2] + got[jj - 1]];
for (const mtxt of merged) {
const c2 = dp[i - 1][jj - 2] + lev(truth[i - 1], mtxt);
if (c2 < c) { c = c2; b = ['m12', mtxt]; }
}
}
if (dp[i][jj - 1] + 2 < c) { c = dp[i][jj - 1] + 2; b = ['spur']; }
if (dp[i - 1][jj] + truth[i - 1].length < c) { c = dp[i - 1][jj] + truth[i - 1].length; b = ['miss']; }
dp[i][jj] = c; bt[i][jj] = b;
}
}
const matched = new Array(T).fill(null);
let i = T, jj = G;
while ((i > 0 || jj > 0) && bt[i][jj]) {
const b = bt[i][jj];
if (b[0] === 'spur') jj--;
else if (b[0] === 'miss') i--;
else if (b[0] === 'm12') { matched[i - 1] = b[1]; i--; jj -= 2; }
else { matched[i - 1] = got[jj - 1]; i--; jj--; }
}
return matched;
}
const mk = () => ({ err: new Array(P).fill(0), tot: new Array(P).fill(0) });
const col = mk(), row = mk();
let errs = 0, tot = 0, missedLines = 0;
const subs = new Map();
const matched = alignLines(grid, out);
for (let r = 0; r < ROWS && r < grid.length; r++) {
const truth = grid[r];
if (matched[r] == null) { missedLines++; continue; } // structural, not phase-scorable
const ok = alignOk(truth, matched[r], subs);
for (let c = 0; c < truth.length; c++) {
if (truth[c] === ' ') continue; // letters only
const cp = (PAD_X + c * CELL_W) % P, rp = (PAD_Y + (r + PADL) * CELL_H) % P;
col.tot[cp]++; row.tot[rp]++; tot++;
if (!ok[c]) { errs++; col.err[cp]++; row.err[rp]++; }
}
}
const agg = (m, pred) => {
let e = 0, t = 0;
for (let p = 0; p < P; p++) if (m.tot[p] && pred(p)) { e += m.err[p]; t += m.tot[p]; }
return t ? `${(100 * e / t).toFixed(2)}% (${e}/${t})` : 'n/a';
};
console.log(`model=${model} cols=${COLS} rows=${ROWS} seed=${seed}`);
console.log(`overall acc=${(100 * (1 - errs / tot)).toFixed(2)}% errs=${errs}/${tot} scoredLines=${grid.length - missedLines}/${ROWS} missedLines=${missedLines} rawOutLines=${out.length}`);
console.log(`colStraddle(x%28>=24): ${agg(col, p => p >= 24)} vs aligned: ${agg(col, p => p < 24)}`);
console.log(`rowStraddle(y%28>=21): ${agg(row, p => p >= 21)} vs aligned: ${agg(row, p => p < 21)}`);
console.log('axis,phase,err,tot,errPct');
for (let p = 0; p < P; p++) if (col.tot[p]) console.log(`col,${p},${col.err[p]},${col.tot[p]},${(100 * col.err[p] / col.tot[p]).toFixed(1)}`);
for (let p = 0; p < P; p++) if (row.tot[p]) console.log(`row,${p},${row.err[p]},${row.tot[p]},${(100 * row.err[p] / row.tot[p]).toFixed(1)}`);
console.log('topConfusions: ' + [...subs.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12).map(([k, n]) => `${k}×${n}`).join(' '));
+77
View File
@@ -0,0 +1,77 @@
/**
* Probe 1: billing-staircase sweep against /v1/messages/count_tokens (free, unbilled).
*
* Sends blank PNGs of swept dimensions and records input_tokens. If image cost
* quantizes as ceil(W/P)*ceil(H/P)*k, step positions in the W (or H) direction
* reveal the vision patch size P (28 vs 32 hypothesis). If cost is smooth
* ~(W*H)/750, billing is decoupled from the encoder grid and this channel is silent.
*
* Usage:
* CC_OAUTH_TOKEN=... node count-tokens-sweep.mjs <model> <axis W|H> <fixed> <from> <to>
* Output: CSV w,h,input_tokens,image_tokens (image_tokens = delta vs no-image baseline)
*/
import sharp from 'sharp';
const TOKEN = process.env.CC_OAUTH_TOKEN;
if (!TOKEN) {
console.error('CC_OAUTH_TOKEN not set');
process.exit(1);
}
const API = 'https://api.anthropic.com/v1/messages/count_tokens';
async function count(model, content) {
const body = {
model,
// Constant across all calls -> cancels in the baseline delta.
system: [{ type: 'text', text: "You are Claude Code, Anthropic's official CLI for Claude." }],
messages: [{ role: 'user', content }],
};
const res = await fetch(API, {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${TOKEN}`,
'anthropic-version': '2023-06-01',
'anthropic-beta': 'oauth-2025-04-20',
},
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`);
return (await res.json()).input_tokens;
}
const blankPng = (w, h) =>
sharp({ create: { width: w, height: h, channels: 3, background: { r: 255, g: 255, b: 255 } } })
.png()
.toBuffer();
const model = process.argv[2] ?? 'claude-fable-5';
const axis = (process.argv[3] ?? 'W').toUpperCase();
const fixed = parseInt(process.argv[4] ?? '56', 10);
const from = parseInt(process.argv[5] ?? '20', 10);
const to = parseInt(process.argv[6] ?? '100', 10);
const baseline = await count(model, [{ type: 'text', text: 'x' }]);
console.log(`# model=${model} axis=${axis} fixed=${fixed} baseline=${baseline}`);
console.log('w,h,input_tokens,image_tokens');
for (let v = from; v <= to; v++) {
const [w, h] = axis === 'W' ? [v, fixed] : [fixed, v];
const data = (await blankPng(w, h)).toString('base64');
let t;
for (let attempt = 0; ; attempt++) {
try {
t = await count(model, [
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data } },
{ type: 'text', text: 'x' },
]);
break;
} catch (e) {
if (attempt >= 3) throw e;
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1))); // ride out RPM 429s
}
}
console.log(`${w},${h},${t},${t - baseline}`);
await new Promise((r) => setTimeout(r, 60));
}
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env node
// Offline paired re-analysis of the row-phase sweep. Loads dumped responses
// (/tmp/phase-probe-claude-fable-5-*.json), maps each to its padLines k via the
// usage token counts in /tmp/phase-sweep-k*.txt, then measures the phase effect
// with per-line fixed effects: excess(,k) = errRate(,k) mean_k errRate(,·).
// Content cancels; only geometry remains. No API calls.
import { readFileSync, readdirSync } from 'node:fs';
import { PAD_Y, CELL_H } from '../../dist/core/render.js';
const P = 28;
function lev(a, b) {
const m = b.length;
let prev = Array.from({ length: m + 1 }, (_, k) => k), cur = new Array(m + 1);
for (let i = 1; i <= a.length; i++) {
cur[0] = i;
for (let k = 1; k <= m; k++)
cur[k] = Math.min(prev[k - 1] + (a[i - 1] === b[k - 1] ? 0 : 1), prev[k] + 1, cur[k - 1] + 1);
[prev, cur] = [cur, prev];
}
return prev[m];
}
function alignLines(truth, got) {
const T = truth.length, G = got.length, BAND = 25, INF = 1e9;
const dp = Array.from({ length: T + 1 }, () => new Array(G + 1).fill(INF));
const bt = Array.from({ length: T + 1 }, () => new Array(G + 1).fill(null));
dp[0][0] = 0;
for (let jj = 1; jj <= G; jj++) { dp[0][jj] = dp[0][jj - 1] + 2; bt[0][jj] = ['spur']; }
for (let i = 1; i <= T; i++) {
dp[i][0] = dp[i - 1][0] + truth[i - 1].length; bt[i][0] = ['miss'];
for (let jj = Math.max(1, i - BAND); jj <= Math.min(G, i + BAND); jj++) {
let c = dp[i - 1][jj - 1] + lev(truth[i - 1], got[jj - 1]), b = ['m11'];
if (jj >= 2 && dp[i - 1][jj - 2] < INF) {
for (const mtxt of [got[jj - 2] + ' ' + got[jj - 1], got[jj - 2] + got[jj - 1]]) {
const c2 = dp[i - 1][jj - 2] + lev(truth[i - 1], mtxt);
if (c2 < c) { c = c2; b = ['m12', mtxt]; }
}
}
if (dp[i][jj - 1] + 2 < c) { c = dp[i][jj - 1] + 2; b = ['spur']; }
if (dp[i - 1][jj] + truth[i - 1].length < c) { c = dp[i - 1][jj] + truth[i - 1].length; b = ['miss']; }
dp[i][jj] = c; bt[i][jj] = b;
}
}
const matched = new Array(T).fill(null);
let i = T, jj = G;
while ((i > 0 || jj > 0) && bt[i][jj]) {
const b = bt[i][jj];
if (b[0] === 'spur') jj--;
else if (b[0] === 'miss') i--;
else if (b[0] === 'm12') { matched[i - 1] = b[1]; i--; jj -= 2; }
else { matched[i - 1] = got[jj - 1]; i--; jj--; }
}
return matched;
}
// --- map dumps to k via usage token counts in sweep stdout files ---
const keyOf = u => `${u.input_tokens}/${u.output_tokens}`;
const kByKey = new Map();
for (const f of readdirSync('/tmp').filter(f => /^phase-sweep-k\d\.txt$/.test(f))) {
const txt = readFileSync('/tmp/' + f, 'utf8');
const m = txt.match(/usage=(\{.*?\}) stop=/s);
if (m) kByKey.set(keyOf(JSON.parse(m[1])), +f.match(/k(\d)/)[1]);
}
const runs = [];
for (const f of readdirSync('/tmp').filter(f => f.startsWith('phase-probe-claude-fable-5-') && f.endsWith('.json'))) {
const d = JSON.parse(readFileSync('/tmp/' + f, 'utf8'));
if (!(d.seedArg ?? '').includes('atlas')) continue;
const k = kByKey.get(keyOf(d.resp.usage));
runs.push({ file: f, k: k ?? 0, replicate: k == null, grid: d.grid, resp: d.resp });
}
console.log(`runs: ${runs.map(r => `k=${r.k}${r.replicate ? '(rep)' : ''}`).join(' ')}`);
// --- per-line error rates ---
const L = runs[0].grid.length;
const cells = []; // {l, k, rate, phase, straddle}
for (const run of runs) {
const out = (run.resp.content?.find(b => b.type === 'text')?.text ?? '')
.replace(/```[a-z]*\n?/g, '').split('\n').map(l => l.trimEnd()).filter(l => l.length);
const matched = alignLines(run.grid, out);
for (let l = 0; l < L; l++) {
if (matched[l] == null) continue;
const phase = (PAD_Y + (l + run.k) * CELL_H) % P;
cells.push({ l, k: run.k, rate: lev(run.grid[l], matched[l]) / run.grid[l].length, phase, straddle: phase >= 21 });
}
}
// per-line means (fixed effect)
const byLine = new Map();
for (const c of cells) (byLine.get(c.l) ?? byLine.set(c.l, []).get(c.l)).push(c.rate);
const lineMean = new Map([...byLine].map(([l, rs]) => [l, rs.reduce((a, b) => a + b, 0) / rs.length]));
for (const c of cells) c.excess = c.rate - lineMean.get(c.l);
const bucket = (sel) => {
const xs = cells.filter(sel).map(c => c.excess);
const n = xs.length, mean = xs.reduce((a, b) => a + b, 0) / n;
const sd = Math.sqrt(xs.reduce((a, b) => a + (b - mean) ** 2, 0) / (n - 1));
return { n, mean, se: sd / Math.sqrt(n) };
};
console.log('\nphase, n(line×run), excessErr%, ±SE%');
for (const p of [...new Set(cells.map(c => c.phase))].sort((a, b) => a - b)) {
const { n, mean, se } = bucket(c => c.phase === p);
console.log(`${String(p).padStart(2)}${p >= 21 ? '*' : ' '} , ${n}, ${(100 * mean).toFixed(2)}, ±${(100 * se).toFixed(2)}`);
}
const s = bucket(c => c.straddle), a = bucket(c => !c.straddle);
console.log(`\nstraddle(≥21): ${(100 * s.mean).toFixed(2)}${(100 * s.se).toFixed(2)} (n=${s.n}) aligned: ${(100 * a.mean).toFixed(2)}${(100 * a.se).toFixed(2)} (n=${a.n}) z=${((s.mean - a.mean) / Math.hypot(s.se, a.se)).toFixed(2)}`);
// hardest lines: err across k to eyeball content-vs-geometry
const hard = [...lineMean].sort((x, y) => y[1] - x[1]).slice(0, 6);
console.log('\nhardest lines (idx, meanErr%, per-run rate% by k, head):');
for (const [l, m] of hard) {
const per = cells.filter(c => c.l === l).sort((x, y) => x.k - y.k).map(c => `k${c.k}${c.straddle ? '*' : ''}:${(100 * c.rate).toFixed(0)}`);
console.log(`#${l} ${(100 * m).toFixed(1)}% [${per.join(' ')}] ${JSON.stringify(runs[0].grid[l].slice(0, 48))}`);
}
+87
View File
@@ -0,0 +1,87 @@
# model=claude-fable-5 axis=H fixed=56 baseline=31
w,h,input_tokens,image_tokens
56,16,36,5
56,17,36,5
56,18,36,5
56,19,36,5
56,20,36,5
56,21,36,5
56,22,36,5
56,23,36,5
56,24,36,5
56,25,36,5
56,26,36,5
56,27,36,5
56,28,36,5
56,29,38,7
56,30,38,7
56,31,38,7
56,32,38,7
56,33,38,7
56,34,38,7
56,35,38,7
56,36,38,7
56,37,38,7
56,38,38,7
56,39,38,7
56,40,38,7
56,41,38,7
56,42,38,7
56,43,38,7
56,44,38,7
56,45,38,7
56,46,38,7
56,47,38,7
56,48,38,7
56,49,38,7
56,50,38,7
56,51,38,7
56,52,38,7
56,53,38,7
56,54,38,7
56,55,38,7
56,56,38,7
56,57,40,9
56,58,40,9
56,59,40,9
56,60,40,9
56,61,40,9
56,62,40,9
56,63,40,9
56,64,40,9
56,65,40,9
56,66,40,9
56,67,40,9
56,68,40,9
56,69,40,9
56,70,40,9
56,71,40,9
56,72,40,9
56,73,40,9
56,74,40,9
56,75,40,9
56,76,40,9
56,77,40,9
56,78,40,9
56,79,40,9
56,80,40,9
56,81,40,9
56,82,40,9
56,83,40,9
56,84,40,9
56,85,42,11
56,86,42,11
56,87,42,11
56,88,42,11
56,89,42,11
56,90,42,11
56,91,42,11
56,92,42,11
56,93,42,11
56,94,42,11
56,95,42,11
56,96,42,11
56,97,42,11
56,98,42,11
56,99,42,11
56,100,42,11
1 # model=claude-fable-5 axis=H fixed=56 baseline=31
2 w,h,input_tokens,image_tokens
3 56,16,36,5
4 56,17,36,5
5 56,18,36,5
6 56,19,36,5
7 56,20,36,5
8 56,21,36,5
9 56,22,36,5
10 56,23,36,5
11 56,24,36,5
12 56,25,36,5
13 56,26,36,5
14 56,27,36,5
15 56,28,36,5
16 56,29,38,7
17 56,30,38,7
18 56,31,38,7
19 56,32,38,7
20 56,33,38,7
21 56,34,38,7
22 56,35,38,7
23 56,36,38,7
24 56,37,38,7
25 56,38,38,7
26 56,39,38,7
27 56,40,38,7
28 56,41,38,7
29 56,42,38,7
30 56,43,38,7
31 56,44,38,7
32 56,45,38,7
33 56,46,38,7
34 56,47,38,7
35 56,48,38,7
36 56,49,38,7
37 56,50,38,7
38 56,51,38,7
39 56,52,38,7
40 56,53,38,7
41 56,54,38,7
42 56,55,38,7
43 56,56,38,7
44 56,57,40,9
45 56,58,40,9
46 56,59,40,9
47 56,60,40,9
48 56,61,40,9
49 56,62,40,9
50 56,63,40,9
51 56,64,40,9
52 56,65,40,9
53 56,66,40,9
54 56,67,40,9
55 56,68,40,9
56 56,69,40,9
57 56,70,40,9
58 56,71,40,9
59 56,72,40,9
60 56,73,40,9
61 56,74,40,9
62 56,75,40,9
63 56,76,40,9
64 56,77,40,9
65 56,78,40,9
66 56,79,40,9
67 56,80,40,9
68 56,81,40,9
69 56,82,40,9
70 56,83,40,9
71 56,84,40,9
72 56,85,42,11
73 56,86,42,11
74 56,87,42,11
75 56,88,42,11
76 56,89,42,11
77 56,90,42,11
78 56,91,42,11
79 56,92,42,11
80 56,93,42,11
81 56,94,42,11
82 56,95,42,11
83 56,96,42,11
84 56,97,42,11
85 56,98,42,11
86 56,99,42,11
87 56,100,42,11
+87
View File
@@ -0,0 +1,87 @@
# model=claude-fable-5 axis=W fixed=56 baseline=31
w,h,input_tokens,image_tokens
16,56,36,5
17,56,36,5
18,56,36,5
19,56,36,5
20,56,36,5
21,56,36,5
22,56,36,5
23,56,36,5
24,56,36,5
25,56,36,5
26,56,36,5
27,56,36,5
28,56,36,5
29,56,38,7
30,56,38,7
31,56,38,7
32,56,38,7
33,56,38,7
34,56,38,7
35,56,38,7
36,56,38,7
37,56,38,7
38,56,38,7
39,56,38,7
40,56,38,7
41,56,38,7
42,56,38,7
43,56,38,7
44,56,38,7
45,56,38,7
46,56,38,7
47,56,38,7
48,56,38,7
49,56,38,7
50,56,38,7
51,56,38,7
52,56,38,7
53,56,38,7
54,56,38,7
55,56,38,7
56,56,38,7
57,56,40,9
58,56,40,9
59,56,40,9
60,56,40,9
61,56,40,9
62,56,40,9
63,56,40,9
64,56,40,9
65,56,40,9
66,56,40,9
67,56,40,9
68,56,40,9
69,56,40,9
70,56,40,9
71,56,40,9
72,56,40,9
73,56,40,9
74,56,40,9
75,56,40,9
76,56,40,9
77,56,40,9
78,56,40,9
79,56,40,9
80,56,40,9
81,56,40,9
82,56,40,9
83,56,40,9
84,56,40,9
85,56,42,11
86,56,42,11
87,56,42,11
88,56,42,11
89,56,42,11
90,56,42,11
91,56,42,11
92,56,42,11
93,56,42,11
94,56,42,11
95,56,42,11
96,56,42,11
97,56,42,11
98,56,42,11
99,56,42,11
100,56,42,11
1 # model=claude-fable-5 axis=W fixed=56 baseline=31
2 w,h,input_tokens,image_tokens
3 16,56,36,5
4 17,56,36,5
5 18,56,36,5
6 19,56,36,5
7 20,56,36,5
8 21,56,36,5
9 22,56,36,5
10 23,56,36,5
11 24,56,36,5
12 25,56,36,5
13 26,56,36,5
14 27,56,36,5
15 28,56,36,5
16 29,56,38,7
17 30,56,38,7
18 31,56,38,7
19 32,56,38,7
20 33,56,38,7
21 34,56,38,7
22 35,56,38,7
23 36,56,38,7
24 37,56,38,7
25 38,56,38,7
26 39,56,38,7
27 40,56,38,7
28 41,56,38,7
29 42,56,38,7
30 43,56,38,7
31 44,56,38,7
32 45,56,38,7
33 46,56,38,7
34 47,56,38,7
35 48,56,38,7
36 49,56,38,7
37 50,56,38,7
38 51,56,38,7
39 52,56,38,7
40 53,56,38,7
41 54,56,38,7
42 55,56,38,7
43 56,56,38,7
44 57,56,40,9
45 58,56,40,9
46 59,56,40,9
47 60,56,40,9
48 61,56,40,9
49 62,56,40,9
50 63,56,40,9
51 64,56,40,9
52 65,56,40,9
53 66,56,40,9
54 67,56,40,9
55 68,56,40,9
56 69,56,40,9
57 70,56,40,9
58 71,56,40,9
59 72,56,40,9
60 73,56,40,9
61 74,56,40,9
62 75,56,40,9
63 76,56,40,9
64 77,56,40,9
65 78,56,40,9
66 79,56,40,9
67 80,56,40,9
68 81,56,40,9
69 82,56,40,9
70 83,56,40,9
71 84,56,40,9
72 85,56,42,11
73 86,56,42,11
74 87,56,42,11
75 88,56,42,11
76 89,56,42,11
77 90,56,42,11
78 91,56,42,11
79 92,56,42,11
80 93,56,42,11
81 94,56,42,11
82 95,56,42,11
83 96,56,42,11
84 97,56,42,11
85 98,56,42,11
86 99,56,42,11
87 100,56,42,11
+87
View File
@@ -0,0 +1,87 @@
# model=claude-sonnet-5 axis=H fixed=56 baseline=31
w,h,input_tokens,image_tokens
56,16,36,5
56,17,36,5
56,18,36,5
56,19,36,5
56,20,36,5
56,21,36,5
56,22,36,5
56,23,36,5
56,24,36,5
56,25,36,5
56,26,36,5
56,27,36,5
56,28,36,5
56,29,38,7
56,30,38,7
56,31,38,7
56,32,38,7
56,33,38,7
56,34,38,7
56,35,38,7
56,36,38,7
56,37,38,7
56,38,38,7
56,39,38,7
56,40,38,7
56,41,38,7
56,42,38,7
56,43,38,7
56,44,38,7
56,45,38,7
56,46,38,7
56,47,38,7
56,48,38,7
56,49,38,7
56,50,38,7
56,51,38,7
56,52,38,7
56,53,38,7
56,54,38,7
56,55,38,7
56,56,38,7
56,57,40,9
56,58,40,9
56,59,40,9
56,60,40,9
56,61,40,9
56,62,40,9
56,63,40,9
56,64,40,9
56,65,40,9
56,66,40,9
56,67,40,9
56,68,40,9
56,69,40,9
56,70,40,9
56,71,40,9
56,72,40,9
56,73,40,9
56,74,40,9
56,75,40,9
56,76,40,9
56,77,40,9
56,78,40,9
56,79,40,9
56,80,40,9
56,81,40,9
56,82,40,9
56,83,40,9
56,84,40,9
56,85,42,11
56,86,42,11
56,87,42,11
56,88,42,11
56,89,42,11
56,90,42,11
56,91,42,11
56,92,42,11
56,93,42,11
56,94,42,11
56,95,42,11
56,96,42,11
56,97,42,11
56,98,42,11
56,99,42,11
56,100,42,11
1 # model=claude-sonnet-5 axis=H fixed=56 baseline=31
2 w,h,input_tokens,image_tokens
3 56,16,36,5
4 56,17,36,5
5 56,18,36,5
6 56,19,36,5
7 56,20,36,5
8 56,21,36,5
9 56,22,36,5
10 56,23,36,5
11 56,24,36,5
12 56,25,36,5
13 56,26,36,5
14 56,27,36,5
15 56,28,36,5
16 56,29,38,7
17 56,30,38,7
18 56,31,38,7
19 56,32,38,7
20 56,33,38,7
21 56,34,38,7
22 56,35,38,7
23 56,36,38,7
24 56,37,38,7
25 56,38,38,7
26 56,39,38,7
27 56,40,38,7
28 56,41,38,7
29 56,42,38,7
30 56,43,38,7
31 56,44,38,7
32 56,45,38,7
33 56,46,38,7
34 56,47,38,7
35 56,48,38,7
36 56,49,38,7
37 56,50,38,7
38 56,51,38,7
39 56,52,38,7
40 56,53,38,7
41 56,54,38,7
42 56,55,38,7
43 56,56,38,7
44 56,57,40,9
45 56,58,40,9
46 56,59,40,9
47 56,60,40,9
48 56,61,40,9
49 56,62,40,9
50 56,63,40,9
51 56,64,40,9
52 56,65,40,9
53 56,66,40,9
54 56,67,40,9
55 56,68,40,9
56 56,69,40,9
57 56,70,40,9
58 56,71,40,9
59 56,72,40,9
60 56,73,40,9
61 56,74,40,9
62 56,75,40,9
63 56,76,40,9
64 56,77,40,9
65 56,78,40,9
66 56,79,40,9
67 56,80,40,9
68 56,81,40,9
69 56,82,40,9
70 56,83,40,9
71 56,84,40,9
72 56,85,42,11
73 56,86,42,11
74 56,87,42,11
75 56,88,42,11
76 56,89,42,11
77 56,90,42,11
78 56,91,42,11
79 56,92,42,11
80 56,93,42,11
81 56,94,42,11
82 56,95,42,11
83 56,96,42,11
84 56,97,42,11
85 56,98,42,11
86 56,99,42,11
87 56,100,42,11
+87
View File
@@ -0,0 +1,87 @@
# model=claude-sonnet-5 axis=W fixed=56 baseline=31
w,h,input_tokens,image_tokens
16,56,36,5
17,56,36,5
18,56,36,5
19,56,36,5
20,56,36,5
21,56,36,5
22,56,36,5
23,56,36,5
24,56,36,5
25,56,36,5
26,56,36,5
27,56,36,5
28,56,36,5
29,56,38,7
30,56,38,7
31,56,38,7
32,56,38,7
33,56,38,7
34,56,38,7
35,56,38,7
36,56,38,7
37,56,38,7
38,56,38,7
39,56,38,7
40,56,38,7
41,56,38,7
42,56,38,7
43,56,38,7
44,56,38,7
45,56,38,7
46,56,38,7
47,56,38,7
48,56,38,7
49,56,38,7
50,56,38,7
51,56,38,7
52,56,38,7
53,56,38,7
54,56,38,7
55,56,38,7
56,56,38,7
57,56,40,9
58,56,40,9
59,56,40,9
60,56,40,9
61,56,40,9
62,56,40,9
63,56,40,9
64,56,40,9
65,56,40,9
66,56,40,9
67,56,40,9
68,56,40,9
69,56,40,9
70,56,40,9
71,56,40,9
72,56,40,9
73,56,40,9
74,56,40,9
75,56,40,9
76,56,40,9
77,56,40,9
78,56,40,9
79,56,40,9
80,56,40,9
81,56,40,9
82,56,40,9
83,56,40,9
84,56,40,9
85,56,42,11
86,56,42,11
87,56,42,11
88,56,42,11
89,56,42,11
90,56,42,11
91,56,42,11
92,56,42,11
93,56,42,11
94,56,42,11
95,56,42,11
96,56,42,11
97,56,42,11
98,56,42,11
99,56,42,11
100,56,42,11
1 # model=claude-sonnet-5 axis=W fixed=56 baseline=31
2 w,h,input_tokens,image_tokens
3 16,56,36,5
4 17,56,36,5
5 18,56,36,5
6 19,56,36,5
7 20,56,36,5
8 21,56,36,5
9 22,56,36,5
10 23,56,36,5
11 24,56,36,5
12 25,56,36,5
13 26,56,36,5
14 27,56,36,5
15 28,56,36,5
16 29,56,38,7
17 30,56,38,7
18 31,56,38,7
19 32,56,38,7
20 33,56,38,7
21 34,56,38,7
22 35,56,38,7
23 36,56,38,7
24 37,56,38,7
25 38,56,38,7
26 39,56,38,7
27 40,56,38,7
28 41,56,38,7
29 42,56,38,7
30 43,56,38,7
31 44,56,38,7
32 45,56,38,7
33 46,56,38,7
34 47,56,38,7
35 48,56,38,7
36 49,56,38,7
37 50,56,38,7
38 51,56,38,7
39 52,56,38,7
40 53,56,38,7
41 54,56,38,7
42 55,56,38,7
43 56,56,38,7
44 57,56,40,9
45 58,56,40,9
46 59,56,40,9
47 60,56,40,9
48 61,56,40,9
49 62,56,40,9
50 63,56,40,9
51 64,56,40,9
52 65,56,40,9
53 66,56,40,9
54 67,56,40,9
55 68,56,40,9
56 69,56,40,9
57 70,56,40,9
58 71,56,40,9
59 72,56,40,9
60 73,56,40,9
61 74,56,40,9
62 75,56,40,9
63 76,56,40,9
64 77,56,40,9
65 78,56,40,9
66 79,56,40,9
67 80,56,40,9
68 81,56,40,9
69 82,56,40,9
70 83,56,40,9
71 84,56,40,9
72 85,56,42,11
73 86,56,42,11
74 87,56,42,11
75 88,56,42,11
76 89,56,42,11
77 90,56,42,11
78 91,56,42,11
79 92,56,42,11
80 93,56,42,11
81 94,56,42,11
82 95,56,42,11
83 96,56,42,11
84 97,56,42,11
85 98,56,42,11
86 99,56,42,11
87 100,56,42,11