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:
Garry Tan
2026-05-17 16:53:15 -07:00
co-authored by Claude Opus 4.7
parent 344b4b845c
commit 5a8b11a113
2 changed files with 235 additions and 0 deletions
+81
View File
@@ -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).';
}
+154
View File
@@ -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) + '…');
});
});