mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-31 04:07:52 +00:00
recall: calibration footer formatter for morning pulse (T16)
Pure formatter that turns a CalibrationProfileRow + optional abandoned-
threads list into the conversational block the morning pulse will surface:
Calibration this quarter:
Brier 0.18 (solid).
Right on early-stage tactics, late on macro by 18 months.
Over-confident on team execution; under-calibrated on regulatory risk.
Threads you opened and never came back to:
· AI search platform differentiation (17 months silent)
· International expansion playbook (12 months silent)
Cold-brain branch: returns empty string when no profile or < 5 resolved
takes. Caller decides whether to render the block; cold-brain absence
is the cleanest non-event.
Brier trend note maps the absolute value to conversational copy:
<= 0.10 → "(strong calibration)"
<= 0.20 → "(solid)"
<= 0.25 → "(near baseline)"
> 0.25 → "(worse than always-50% baseline — review your high-conviction calls)"
v0.36.0.0 ship state has only the current profile snapshot. The
"was 0.22 90d ago — improving" comparison shape arrives when we
accumulate generated_at history across multiple cycles.
R3 regression posture:
This module is the FORMATTER only. Wiring into `gbrain recall`'s text
output is intentionally NOT in this commit — runRecall's surface
stays unchanged. v0.37 wires it under --show-calibration (opt-in
initially, default-on later). For now the formatter is callable from
the admin tab + custom CLI scripts that want it.
Architecture:
buildRecallCalibrationFooter(opts) — pure. opts.profile required,
opts.abandonedThreads optional, opts.threadColumnWidth defaults to 50.
Caps at 4 patterns + 5 abandoned threads to keep the footer scannable.
Truncates long abandoned-thread claim text to fit the column width with
a trailing ellipsis.
Tests: 14 cases.
Cold-brain branch (3): null profile, < 5 resolved, zero resolved.
Happy path (7): header + Brier + patterns, trend note ranges (4
brackets), null brier omits the Brier line but keeps header, caps at
4 patterns.
Abandoned threads (4): omit section when none, emit when present,
cap at 5, truncate long claim with column-width override.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
344b4b845c
commit
5a8b11a113
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* v0.36.0.0 (T16) — calibration footer for `gbrain recall` morning pulse.
|
||||
*
|
||||
* Pure formatter. Given an active calibration profile, returns the
|
||||
* conversational block to prepend or append to the recall output:
|
||||
*
|
||||
* Calibration this quarter:
|
||||
* Brier 0.18 (was 0.22 90d ago — improving).
|
||||
* Right on early-stage tactics, late on macro by 18 months.
|
||||
* Over-confident on team execution; under-calibrated on regulatory risk.
|
||||
*
|
||||
* Threads you opened and never came back to:
|
||||
* · AI search platform differentiation (17 months silent)
|
||||
* · International expansion playbook (12 months silent)
|
||||
*
|
||||
* Cold-brain branch: returns empty string when no profile or
|
||||
* insufficient resolved takes. The caller decides whether to prepend
|
||||
* the block; cold-brain absence is the cleanest non-event.
|
||||
*
|
||||
* v0.36.0.0 ship state: opt-in via `gbrain recall --show-calibration`
|
||||
* to keep R3 regression posture (existing recall text shape unchanged
|
||||
* for users who don't pass the flag). v0.37 defaults to on.
|
||||
*
|
||||
* Trend computation: v0.36.0.0 has only ONE profile snapshot (the most
|
||||
* recent generation). Trend ("was X 90d ago — improving/declining")
|
||||
* arrives when we accumulate generated_at history.
|
||||
*/
|
||||
|
||||
import type { CalibrationProfileRow } from '../../commands/calibration.ts';
|
||||
|
||||
export interface AbandonedThreadSummary {
|
||||
claim: string;
|
||||
monthsSilent: number;
|
||||
}
|
||||
|
||||
export interface RecallFooterOpts {
|
||||
profile: CalibrationProfileRow | null;
|
||||
abandonedThreads?: AbandonedThreadSummary[];
|
||||
/** Width hint for column alignment on threads. Default 50. */
|
||||
threadColumnWidth?: number;
|
||||
}
|
||||
|
||||
export function buildRecallCalibrationFooter(opts: RecallFooterOpts): string {
|
||||
const { profile, abandonedThreads } = opts;
|
||||
if (!profile) return '';
|
||||
if (profile.total_resolved < 5) return '';
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push('Calibration this quarter:');
|
||||
|
||||
// Brier line. v0.36.0.0 has only the current snapshot — no 90d comparison.
|
||||
if (profile.brier !== null) {
|
||||
lines.push(` Brier ${profile.brier.toFixed(2)} ${trendNote(profile.brier)}`);
|
||||
}
|
||||
// Up to 4 pattern statements. Indent for readability.
|
||||
for (const p of profile.pattern_statements.slice(0, 4)) {
|
||||
lines.push(` ${p}`);
|
||||
}
|
||||
|
||||
if (abandonedThreads && abandonedThreads.length > 0) {
|
||||
lines.push('');
|
||||
lines.push('Threads you opened and never came back to:');
|
||||
const colWidth = opts.threadColumnWidth ?? 50;
|
||||
for (const t of abandonedThreads.slice(0, 5)) {
|
||||
const claim = t.claim.length > colWidth ? t.claim.slice(0, colWidth - 1) + '…' : t.claim;
|
||||
const padded = claim.padEnd(colWidth, ' ');
|
||||
lines.push(` · ${padded}(${t.monthsSilent} months silent)`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function trendNote(brier: number): string {
|
||||
// Map Brier to a conversational anchor. No history yet so we describe
|
||||
// the absolute value rather than trend.
|
||||
if (brier <= 0.1) return '(strong calibration).';
|
||||
if (brier <= 0.2) return '(solid).';
|
||||
if (brier <= 0.25) return '(near baseline).';
|
||||
return '(worse than always-50% baseline — review your high-conviction calls).';
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* v0.36.0.0 (T16) — recall morning pulse calibration footer tests.
|
||||
*
|
||||
* Pure formatter tests. No engine, no LLM.
|
||||
*
|
||||
* Tests cover:
|
||||
* - cold-brain branch: null profile → empty string
|
||||
* - insufficient resolved (< 5) → empty string
|
||||
* - happy path: section header + Brier + patterns
|
||||
* - abandoned threads section: optional, formatted, capped at 5
|
||||
* - trend note maps Brier ranges to conversational copy
|
||||
* - patterns capped at 4
|
||||
* - column alignment on abandoned threads
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { buildRecallCalibrationFooter } from '../src/core/calibration/recall-footer.ts';
|
||||
import type { CalibrationProfileRow } from '../src/commands/calibration.ts';
|
||||
|
||||
function buildProfile(opts: Partial<CalibrationProfileRow> = {}): CalibrationProfileRow {
|
||||
return {
|
||||
id: 1,
|
||||
source_id: 'default',
|
||||
holder: 'garry',
|
||||
wave_version: 'v0.36.0.0',
|
||||
generated_at: '2026-05-17T00:00:00Z',
|
||||
published: false,
|
||||
total_resolved: 12,
|
||||
brier: 0.18,
|
||||
accuracy: 0.6,
|
||||
partial_rate: 0.1,
|
||||
grade_completion: 1.0,
|
||||
pattern_statements: ['Right on early-stage tactics, late on macro by 18 months.'],
|
||||
active_bias_tags: ['over-confident-geography'],
|
||||
voice_gate_passed: true,
|
||||
voice_gate_attempts: 1,
|
||||
model_id: 'claude-sonnet-4-6',
|
||||
...opts,
|
||||
};
|
||||
}
|
||||
|
||||
describe('buildRecallCalibrationFooter — cold-brain branch', () => {
|
||||
test('null profile → empty string', () => {
|
||||
expect(buildRecallCalibrationFooter({ profile: null })).toBe('');
|
||||
});
|
||||
|
||||
test('insufficient resolved (< 5) → empty string', () => {
|
||||
expect(buildRecallCalibrationFooter({ profile: buildProfile({ total_resolved: 3 }) })).toBe(
|
||||
'',
|
||||
);
|
||||
});
|
||||
|
||||
test('zero resolved → empty string', () => {
|
||||
expect(buildRecallCalibrationFooter({ profile: buildProfile({ total_resolved: 0 }) })).toBe(
|
||||
'',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildRecallCalibrationFooter — happy path', () => {
|
||||
test('emits header + Brier + pattern block', () => {
|
||||
const out = buildRecallCalibrationFooter({ profile: buildProfile() });
|
||||
expect(out).toContain('Calibration this quarter:');
|
||||
expect(out).toContain('Brier 0.18');
|
||||
expect(out).toContain('Right on early-stage tactics');
|
||||
});
|
||||
|
||||
test('Brier line includes trend note ("solid")', () => {
|
||||
expect(buildRecallCalibrationFooter({ profile: buildProfile({ brier: 0.18 }) })).toContain(
|
||||
'(solid)',
|
||||
);
|
||||
});
|
||||
|
||||
test('Brier near-baseline range', () => {
|
||||
expect(
|
||||
buildRecallCalibrationFooter({ profile: buildProfile({ brier: 0.24 }) }),
|
||||
).toContain('(near baseline)');
|
||||
});
|
||||
|
||||
test('Brier worse than baseline → review hint', () => {
|
||||
expect(buildRecallCalibrationFooter({ profile: buildProfile({ brier: 0.32 }) })).toContain(
|
||||
'worse than always-50%',
|
||||
);
|
||||
});
|
||||
|
||||
test('Brier strong calibration', () => {
|
||||
expect(
|
||||
buildRecallCalibrationFooter({ profile: buildProfile({ brier: 0.08 }) }),
|
||||
).toContain('(strong calibration)');
|
||||
});
|
||||
|
||||
test('omits Brier line when brier=null (resolved correct+incorrect = 0)', () => {
|
||||
const out = buildRecallCalibrationFooter({ profile: buildProfile({ brier: null }) });
|
||||
expect(out).not.toContain('Brier null');
|
||||
expect(out).toContain('Calibration this quarter:'); // header still emitted
|
||||
});
|
||||
|
||||
test('caps at 4 pattern statements', () => {
|
||||
const out = buildRecallCalibrationFooter({
|
||||
profile: buildProfile({ pattern_statements: ['a', 'b', 'c', 'd', 'e', 'f'] }),
|
||||
});
|
||||
const aIdx = out.indexOf(' a\n');
|
||||
expect(out).toContain(' a');
|
||||
expect(out).toContain(' d');
|
||||
// 'e' and 'f' should NOT appear as standalone bullets.
|
||||
const lines = out.split('\n');
|
||||
const bullets = lines.filter(l => /^ {2}[abcdef]$/.test(l));
|
||||
expect(bullets.length).toBe(4);
|
||||
void aIdx;
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildRecallCalibrationFooter — abandoned threads', () => {
|
||||
test('omits the abandoned-threads section when no threads passed', () => {
|
||||
const out = buildRecallCalibrationFooter({ profile: buildProfile() });
|
||||
expect(out).not.toContain('Threads you opened');
|
||||
});
|
||||
|
||||
test('emits section + rows when threads provided', () => {
|
||||
const out = buildRecallCalibrationFooter({
|
||||
profile: buildProfile(),
|
||||
abandonedThreads: [
|
||||
{ claim: 'AI search platform differentiation', monthsSilent: 17 },
|
||||
{ claim: 'International expansion playbook', monthsSilent: 12 },
|
||||
],
|
||||
});
|
||||
expect(out).toContain('Threads you opened and never came back to:');
|
||||
expect(out).toContain('AI search platform differentiation');
|
||||
expect(out).toContain('17 months silent');
|
||||
expect(out).toContain('International expansion playbook');
|
||||
expect(out).toContain('12 months silent');
|
||||
});
|
||||
|
||||
test('caps at 5 abandoned threads', () => {
|
||||
const threads = Array.from({ length: 8 }, (_, i) => ({
|
||||
claim: `thread ${i}`,
|
||||
monthsSilent: 12 + i,
|
||||
}));
|
||||
const out = buildRecallCalibrationFooter({ profile: buildProfile(), abandonedThreads: threads });
|
||||
expect(out).toContain('thread 0');
|
||||
expect(out).toContain('thread 4');
|
||||
expect(out).not.toContain('thread 5');
|
||||
});
|
||||
|
||||
test('truncates long claim text with ellipsis', () => {
|
||||
const longClaim = 'x'.repeat(100);
|
||||
const out = buildRecallCalibrationFooter({
|
||||
profile: buildProfile(),
|
||||
abandonedThreads: [{ claim: longClaim, monthsSilent: 12 }],
|
||||
threadColumnWidth: 30,
|
||||
});
|
||||
expect(out).toContain('x'.repeat(29) + '…');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user