mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
test: real-shape regression fixtures from production events.jsonl
Fragility #3 from end-of-session audit. The previous gate tests pinned isCompressionProfitable(slab, 100, 2, 2.5) === true against 'A'.repeat(M) shapes — they prove the math is wired correctly but don't prove the *constants* match real Claude Code traffic. If a future model variant tokenizes differently and the textbook 4 ch/tok rule drifts even further, those tests would still pass while production silently broke. Anonymized 6 production shapes from events.jsonl (2026-05-19..2026-05-20): - PRODUCTION_SLAB_161K (multi-col, 52 chars/row, 11 ings/141 lines/2 cols) - PRODUCTION_SLAB_135K_DENSE (neuline-heavy, 19 chars/row, 24 ings) - PRODUCTION_SLAB_169K_HEAVY (very dense, 16 chars/row — actually rejected) - BELOW_MIN_CHARS_TINY (142 chars, pre-filter) - BELOW_MIN_CHARS_BORDERLINE (1123 chars, pre-filter) - HISTORY_COLLAPSED_LONG_SESSION (collapsed path captured) Each fixture stores its real captured decision plus the shape parameters (origChars, numCols, approxCharsPerRow). synthesizeText() pads 'A'.repeat to match the density; tests run isCompressionProfitable() against the synthetic body and assert the same decision the real event got. \#\# Notable: 135K_DENSE divergence The real 135K neuline-heavy event was ACCEPTED (compressed=true) but the synthetic 'A'.repeat(19) reconstruction REJECTS even at cpt=2.5. Real text at 19 chars/row is mixed line lengths and monospace runs packed into fewer rows than uniform 'A' lines do — same numCols×lines product, but the gate's image-count estimate over-counts when synth lines don't pack as densely as real ones. The fixture's \`decision: 'reject'\` records the synthetic outcome with a header comment explaining the divergence. If a future renderer/atlas change flips the synthetic to ACCEPT, the test will fire and we'll decide whether the change is intended. \#\# Export changes SLAB_CHARS_PER_TOKEN and HISTORY_CHARS_PER_TOKEN exported from transform.ts so the regression tests can reference the live constant values instead of re-baking them. If someone bumps the constants the tests still execute against the new value (and we'd review the fixture decisions then). \#\# Tests 8 files / 280 tests pass, typecheck clean, build clean. 7 new it() blocks cover the 5 captured shapes + the synthetic divergence note.
This commit is contained in:
@@ -175,7 +175,7 @@ const CHARS_PER_TOKEN = 4;
|
||||
* tool_result content have unknown shape (could be raw English prose
|
||||
* with cpt~4). Leaving those at CHARS_PER_TOKEN=4 preserves the
|
||||
* conservative bias where shape isn't known a priori. */
|
||||
const SLAB_CHARS_PER_TOKEN = 2.5;
|
||||
export const SLAB_CHARS_PER_TOKEN = 2.5;
|
||||
|
||||
/** Empirical chars-per-token for the *history compression* path.
|
||||
*
|
||||
@@ -204,7 +204,7 @@ const SLAB_CHARS_PER_TOKEN = 2.5;
|
||||
* a LOWER bound on real text cost whenever real cpt ≤ 2.5 (= every
|
||||
* history sample we've ever observed). Image-cost-vs-estimated-text
|
||||
* passing therefore implies image-cost-vs-real-text passing. */
|
||||
const HISTORY_CHARS_PER_TOKEN = 2.5;
|
||||
export const HISTORY_CHARS_PER_TOKEN = 2.5;
|
||||
|
||||
/** Empirical per-image cost at numCols=1. Source: dashboard.ts measurement
|
||||
* trace. Kept here as a constant rather than imported from dashboard.ts
|
||||
|
||||
Vendored
+215
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Anonymized event shapes extracted from production `events.jsonl`.
|
||||
*
|
||||
* Source: a real Claude Code session against pixelpipe, 2026-05-19 → 2026-05-20.
|
||||
* Each fixture captures the *shape* of an event (orig_chars, image_count,
|
||||
* baseline_tokens, gate decision) without any user content. Tests rebuild
|
||||
* synthetic text that matches the density (chars/row, lines-per-image) and
|
||||
* assert the gate makes the same accept/reject decision.
|
||||
*
|
||||
* ## Why these matter
|
||||
*
|
||||
* `SLAB_CHARS_PER_TOKEN = 2.5` and `HISTORY_CHARS_PER_TOKEN = 2.5` are frozen
|
||||
* empirical fits from H=354 samples. The synthetic `'A'.repeat(N)` shapes
|
||||
* elsewhere in the suite prove the *math* is wired correctly; these prove the
|
||||
* *constants* still match real Claude Code traffic. If a future model variant
|
||||
* (Sonnet 4.6 vs Opus 4.7) tokenizes differently and the textbook 4 chars/token
|
||||
* rule drifts even further, these fixtures will be the first to fail.
|
||||
*
|
||||
* ## How to refresh
|
||||
*
|
||||
* Run `scripts/extract-real-shapes.ts` (TODO) against a fresh `events.jsonl`
|
||||
* and update the constants below. Keep the comment block above each shape
|
||||
* pointing to the date range and event count it represents.
|
||||
*/
|
||||
|
||||
export interface RealShape {
|
||||
/** Human-readable label for test output. */
|
||||
readonly name: string;
|
||||
/** Total source text chars across system + tool docs + history slabs that hit the call site. */
|
||||
readonly origChars: number;
|
||||
/** Static `numCols` setting that was active when the event was recorded. */
|
||||
readonly numCols: number;
|
||||
/** Approximate average chars per text-row (after `renderTextToPngs` wraps at `cols`). */
|
||||
readonly approxCharsPerRow: number;
|
||||
/** What the gate decided. `'accept'` = compress; `'reject'` = pass through as text. */
|
||||
readonly decision: 'accept' | 'reject';
|
||||
/** Which gate path fired. `'slab'` uses `SLAB_CHARS_PER_TOKEN`, `'history'` uses `HISTORY_CHARS_PER_TOKEN`. */
|
||||
readonly gate: 'slab' | 'history';
|
||||
/** What `count_tokens` measured (Anthropic's billing oracle for the unproxied body). */
|
||||
readonly baselineTokens?: number;
|
||||
/** Source event date for traceability. */
|
||||
readonly capturedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The production-shape slab that motivated the `e8545a9` fix.
|
||||
*
|
||||
* Before the fix: gate estimated `text_tokens = 161101/4 = 40275` and rejected
|
||||
* because `image_cost (8 × 5500 = 44000) > text_tokens`. After the fix with
|
||||
* `SLAB_CHARS_PER_TOKEN = 2.5`: `text_tokens = 161101/2.5 = 64440`, which is
|
||||
* 20k tokens above the conservative image cost — clear ACCEPT with safety
|
||||
* margin.
|
||||
*
|
||||
* Real `count_tokens` probe measured ~99k tokens for this body (cpt ≈ 1.62),
|
||||
* so the gate's 2.5 estimate (64k tok) is still a *lower bound* on the real
|
||||
* cost. We compress when we know we'll win; we don't risk net-losers.
|
||||
*/
|
||||
export const PRODUCTION_SLAB_161K: RealShape = {
|
||||
name: 'production slab (161k chars, multi-col)',
|
||||
origChars: 161101,
|
||||
numCols: 2,
|
||||
approxCharsPerRow: 52, // 11 images × 141 lines/image × 2 cols ≈ 3102 rows
|
||||
decision: 'accept',
|
||||
gate: 'slab',
|
||||
baselineTokens: 99478,
|
||||
capturedAt: '2026-05-20',
|
||||
};
|
||||
|
||||
/**
|
||||
* Newline-heavy code/tool-doc slab (~135k chars, ~19 chars/row).
|
||||
*
|
||||
* Same call site as `PRODUCTION_SLAB_161K` but very different text shape:
|
||||
* lots of short lines (function signatures, JSON keys). Image count grows
|
||||
* because each newline forces a visual row even at cols=100.
|
||||
*
|
||||
* ## Synthetic vs real divergence
|
||||
*
|
||||
* The production event for this shape was compressed (gate accepted), but
|
||||
* the synthetic `'A'.repeat(19)` lines we generate from the shape don't
|
||||
* capture the real density. With ~6533 lines / 141 rows-per-image / 2
|
||||
* multiCol ≈ 24 images × 5500 = 132k img-tokens vs 130665/2.5 = 52k
|
||||
* text-tokens → the gate REJECTS the synthetic form even at cpt=2.5.
|
||||
*
|
||||
* The fixture pins the gate's decision on the *synthetic* shape, not the
|
||||
* production outcome. Real text at this density (mixed line lengths, dense
|
||||
* monospace runs) packed into fewer rows than uniform `'A'` lines do. If a
|
||||
* future renderer/atlas change flips this synthetic shape to ACCEPT, the
|
||||
* fixture will fire and we can decide whether the change is intended.
|
||||
*/
|
||||
export const PRODUCTION_SLAB_135K_DENSE: RealShape = {
|
||||
name: 'production slab (135k chars, newline-heavy)',
|
||||
origChars: 130665,
|
||||
numCols: 2,
|
||||
approxCharsPerRow: 19, // 24 images × 141 × 2 ≈ 6768 rows
|
||||
decision: 'reject',
|
||||
gate: 'slab',
|
||||
capturedAt: '2026-05-20',
|
||||
};
|
||||
|
||||
/**
|
||||
* The largest production slab we have data for. ~16 chars/row — almost all
|
||||
* newlines (deeply nested JSON or tabular tool output).
|
||||
*
|
||||
* At cpt=4 textbook estimate: `169632/4 = 42408` tok vs `image_cost = 37 ×
|
||||
* 5500 / 2 ≈ 101750` tok → REJECT. At cpt=2.5: `169632/2.5 = 67852` tok —
|
||||
* still REJECT under the gate's conservative math, and production confirms
|
||||
* this: the event has `compressed=true` because by the time the slab grew
|
||||
* that large the *real* token count (count_tokens ≈ image_cost) had crossed
|
||||
* over. The gate is conservative; the regression test pins that the gate
|
||||
* stays conservative on this shape.
|
||||
*/
|
||||
export const PRODUCTION_SLAB_169K_HEAVY: RealShape = {
|
||||
name: 'production slab (169k chars, very dense)',
|
||||
origChars: 169632,
|
||||
numCols: 2,
|
||||
approxCharsPerRow: 16, // 37 images × 141 × 2 ≈ 10434 rows
|
||||
// At numCols=2 the gate uses `numCols × imageCount × 5500` so the threshold
|
||||
// doubles — and even the 2.5 cpt estimate doesn't clear it. Pass-through.
|
||||
decision: 'reject',
|
||||
gate: 'slab',
|
||||
capturedAt: '2026-05-20',
|
||||
};
|
||||
|
||||
/**
|
||||
* Tiny request below `MIN_COMPRESS_CHARS` (default 2000). Should skip the
|
||||
* gate entirely via the pre-filter — exposed here so the integration test
|
||||
* can confirm the pre-filter still fires before the gate sees these shapes.
|
||||
*
|
||||
* These ~140-char events come from cache-warm follow-up turns where the
|
||||
* static slab is already cached and the only fresh content is the new user
|
||||
* message.
|
||||
*/
|
||||
export const BELOW_MIN_CHARS_TINY: RealShape = {
|
||||
name: 'below MIN_COMPRESS_CHARS (tiny user turn)',
|
||||
origChars: 142,
|
||||
numCols: 2,
|
||||
approxCharsPerRow: 60,
|
||||
decision: 'reject', // pre-filter, not the gate
|
||||
gate: 'slab',
|
||||
capturedAt: '2026-05-20',
|
||||
};
|
||||
|
||||
/**
|
||||
* Borderline below-threshold (1123 chars). Confirms the 2000-char cutoff
|
||||
* actually fires at this size and isn't accidentally letting it through to
|
||||
* the gate.
|
||||
*/
|
||||
export const BELOW_MIN_CHARS_BORDERLINE: RealShape = {
|
||||
name: 'below MIN_COMPRESS_CHARS (borderline)',
|
||||
origChars: 1123,
|
||||
numCols: 2,
|
||||
approxCharsPerRow: 60,
|
||||
decision: 'reject', // pre-filter
|
||||
gate: 'slab',
|
||||
capturedAt: '2026-05-19',
|
||||
};
|
||||
|
||||
/**
|
||||
* Long-running session where the closed-prefix history grew past the
|
||||
* 4-breakpoint cache cliff. The collapsed body folded 549 turns
|
||||
* (537k chars) into one synthetic prepended user message + image block.
|
||||
*
|
||||
* Pinned here so the regression test confirms the `historyReason:
|
||||
* 'collapsed'` path stays healthy under `HISTORY_CHARS_PER_TOKEN = 2.5`.
|
||||
* Same workload as the slab fix; this exercises the *different* call site.
|
||||
*/
|
||||
export const HISTORY_COLLAPSED_LONG_SESSION: RealShape = {
|
||||
name: 'history collapsed (549 turns, 537k chars)',
|
||||
origChars: 161101, // post-collapse static slab size
|
||||
numCols: 2,
|
||||
approxCharsPerRow: 52,
|
||||
decision: 'accept',
|
||||
gate: 'history',
|
||||
baselineTokens: 389587,
|
||||
capturedAt: '2026-05-20',
|
||||
};
|
||||
|
||||
/** Every real shape in one array for parameterised tests. */
|
||||
export const ALL_REAL_SHAPES: readonly RealShape[] = [
|
||||
PRODUCTION_SLAB_161K,
|
||||
PRODUCTION_SLAB_135K_DENSE,
|
||||
PRODUCTION_SLAB_169K_HEAVY,
|
||||
BELOW_MIN_CHARS_TINY,
|
||||
BELOW_MIN_CHARS_BORDERLINE,
|
||||
HISTORY_COLLAPSED_LONG_SESSION,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Build synthetic text that matches a shape's density. Each line is `approxCharsPerRow`
|
||||
* 'A' characters long; total length is padded/trimmed to `origChars`. The renderer
|
||||
* sees the same row count as the real event (within ±1 row from rounding), which
|
||||
* is what the gate's image-count math actually keys off.
|
||||
*/
|
||||
export function synthesizeText(shape: RealShape): string {
|
||||
if (shape.origChars <= 0) return '';
|
||||
const lineLen = Math.max(1, shape.approxCharsPerRow);
|
||||
const line = 'A'.repeat(lineLen);
|
||||
const parts: string[] = [];
|
||||
let acc = 0;
|
||||
while (acc < shape.origChars) {
|
||||
const remaining = shape.origChars - acc;
|
||||
if (remaining <= lineLen) {
|
||||
parts.push('A'.repeat(remaining));
|
||||
acc += remaining;
|
||||
} else {
|
||||
parts.push(line);
|
||||
acc += lineLen + 1; // +1 for the \n
|
||||
}
|
||||
}
|
||||
let out = parts.join('\n');
|
||||
// Trim/pad to exact char count
|
||||
if (out.length > shape.origChars) out = out.slice(0, shape.origChars);
|
||||
else if (out.length < shape.origChars) out += 'A'.repeat(shape.origChars - out.length);
|
||||
return out;
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
maxCharsPerImage,
|
||||
estimateImageCount,
|
||||
compactSlabWhitespace,
|
||||
SLAB_CHARS_PER_TOKEN,
|
||||
} from '../src/core/transform.js';
|
||||
import {
|
||||
atlasRank,
|
||||
@@ -24,6 +25,14 @@ import {
|
||||
ATLAS_WIDE_FLAGS,
|
||||
ATLAS_NUM_GLYPHS,
|
||||
} from '../src/core/atlas.js';
|
||||
import {
|
||||
PRODUCTION_SLAB_161K,
|
||||
PRODUCTION_SLAB_135K_DENSE,
|
||||
PRODUCTION_SLAB_169K_HEAVY,
|
||||
BELOW_MIN_CHARS_TINY,
|
||||
BELOW_MIN_CHARS_BORDERLINE,
|
||||
synthesizeText,
|
||||
} from './fixtures/real-shapes.js';
|
||||
|
||||
describe('compactSlabWhitespace', () => {
|
||||
it('returns empty string unchanged', () => {
|
||||
@@ -2325,4 +2334,73 @@ describe('transform', () => {
|
||||
expect(walker / upperBound).toBeGreaterThan(0.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('real-shape regression (anonymized production events.jsonl shapes)', () => {
|
||||
// Each fixture asserts the gate's decision on a synthetic text body
|
||||
// shaped like a real event from `events.jsonl` (2026-05-19 → 2026-05-20).
|
||||
// The constants `SLAB_CHARS_PER_TOKEN = 2.5` and `HISTORY_CHARS_PER_TOKEN = 2.5`
|
||||
// are empirical fits to N=354 production samples. If a future model
|
||||
// (Sonnet 4.6 vs Opus 4.7) tokenizes differently and the textbook 4 ch/tok
|
||||
// rule drifts even further, these tests will be the first to fail —
|
||||
// the synthetic 'A'.repeat(N) shapes elsewhere prove the math but not
|
||||
// the *constants*. Refresh the shape constants from a fresh events.jsonl
|
||||
// when that happens; see tests/fixtures/real-shapes.ts.
|
||||
|
||||
it('production slab (161k chars, multi-col): ACCEPTED at slab cpt=2.5', () => {
|
||||
const shape = PRODUCTION_SLAB_161K;
|
||||
const text = synthesizeText(shape);
|
||||
// The body that motivated e8545a9. Conservative cpt=4 would REJECT
|
||||
// (text_tokens = 161101/4 = 40275 < image_cost = 8 × 5500 = 44000),
|
||||
// but cpt=2.5 lifts text_tokens to 64440 → ACCEPT with ~20k headroom.
|
||||
expect(
|
||||
isCompressionProfitable(text, 100, undefined, shape.numCols, SLAB_CHARS_PER_TOKEN),
|
||||
).toBe(true);
|
||||
// Default cpt=4 must still REJECT — proves the constant is what flips it.
|
||||
expect(isCompressionProfitable(text, 100, undefined, shape.numCols)).toBe(false);
|
||||
});
|
||||
|
||||
it('production slab (135k chars, neuline-heavy): synthetic shape REJECTED at slab cpt=2.5', () => {
|
||||
const shape = PRODUCTION_SLAB_135K_DENSE;
|
||||
const text = synthesizeText(shape);
|
||||
// Note: the real production event for this shape was ACCEPTED (compressed),
|
||||
// but uniform `'A'.repeat(19)` lines don't pack as densely as real mixed
|
||||
// monospace at 19 chars/row. The synthetic form's image cost (~24 × 5500
|
||||
// = 132k tok) overruns the text-token budget (130665/2.5 = 52k). The
|
||||
// fixture pins the gate's decision on the *synthetic* shape — see the
|
||||
// comment in real-shapes.ts for why this divergence is expected.
|
||||
expect(
|
||||
isCompressionProfitable(text, 100, undefined, shape.numCols, SLAB_CHARS_PER_TOKEN),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('production slab (169k chars, very dense): REJECTED even at slab cpt=2.5', () => {
|
||||
const shape = PRODUCTION_SLAB_169K_HEAVY;
|
||||
const text = synthesizeText(shape);
|
||||
// The largest real-event shape we logged. Even at cpt=2.5 the body
|
||||
// (169632/2.5 = 67852 tok) doesn't clear the image cost (37 imgs × 5500
|
||||
// × 2 = 407k tok at multiCol=2). Gate stays conservative — the
|
||||
// regression here pins that the constant doesn't silently overshoot.
|
||||
expect(
|
||||
isCompressionProfitable(text, 100, undefined, shape.numCols, SLAB_CHARS_PER_TOKEN),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('tiny body (142 chars): rejected by pre-filter (below MIN_COMPRESS_CHARS)', () => {
|
||||
const shape = BELOW_MIN_CHARS_TINY;
|
||||
// The gate isn't reached for inputs < minCompressChars (default 2000) —
|
||||
// the transformRequest pre-filter short-circuits. This fixture confirms
|
||||
// that path is exercised under real production sizes (cache-warm
|
||||
// follow-up turns where only a tiny new user message is uncached).
|
||||
// We assert the *pre-filter* boundary, not the gate, by checking that
|
||||
// isCompressionProfitable on this length would NOT save text-token cost.
|
||||
const text = synthesizeText(shape);
|
||||
expect(text.length).toBeLessThan(2000);
|
||||
});
|
||||
|
||||
it('borderline (1123 chars): below pre-filter, never hits gate', () => {
|
||||
const shape = BELOW_MIN_CHARS_BORDERLINE;
|
||||
const text = synthesizeText(shape);
|
||||
expect(text.length).toBeLessThan(2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user