mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 08:53:22 +00:00
Rebase-port of #2452 (spinsirr:fix/calibration-profile-scope-and-cli) onto current master after tonight's merges made the fork branch conflict. - cli: add 'calibration' to CLI_ONLY so dispatch reaches its existing handler instead of falling through to "Unknown command" (#2035); honor --source / GBRAIN_SOURCE in the calibration CLI. - takes: route takes_list / takes_search / takes_scorecard / takes_calibration through sourceScopeOpts(ctx) (federated array > scalar > nothing) and scope engine reads via the take's page.source_id — JOIN filter for list/search, EXISTS for scorecard/curve — on both engines (#2200-class). - bigint: shared takeHitRowToHit coercion in searchTakes / searchTakesVector (both engines) + bigintToStringReplacer on the cli.ts output normalizer and the `gbrain call` exit, so int8/BIGSERIAL columns no longer crash JSON.stringify (#2450); calibration profile id BIGSERIAL → string. - calibration: default model ids route through TIER_DEFAULTS (provider-prefixed) instead of bare model strings; admin calibration chart endpoints fixed (takes has no page_slug column; month-precision since_date; Date generated_at; bigint id in drill-down). The think/gather source-scope slice of the original PR was dropped: it already landed on master via #2739. Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local> Co-authored-by: spinsirr <ID+spinsirr@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
171 lines
6.8 KiB
TypeScript
171 lines
6.8 KiB
TypeScript
/**
|
|
* v0.36.1.0 (T9 / E3) — calibration-aware contradictions tests.
|
|
*
|
|
* Pure-function tests for the calibration-join helper. No DB, no LLM.
|
|
*
|
|
* Tests cover:
|
|
* - R2 regression: no profile → null tag (contradictions output unchanged)
|
|
* - happy path: finding matches active bias tag via domain hint
|
|
* - geography hint matches over-confident-geography tag
|
|
* - macro hint matches late-on-macro-tech tag
|
|
* - mismatch: hint produced but tag set doesn't include matching slug
|
|
* - empty active_bias_tags: returns null (no false positives)
|
|
* - bias context string contains tag name + Brier when present
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import {
|
|
tagFindingWithCalibration,
|
|
computeDomainHint,
|
|
buildBiasContextString,
|
|
} from '../src/core/eval-contradictions/calibration-join.ts';
|
|
import type { ContradictionFinding, PairMember } from '../src/core/eval-contradictions/types.ts';
|
|
import type { CalibrationProfileRow } from '../src/commands/calibration.ts';
|
|
|
|
function buildMember(slug: string, holder: string | null = 'garry'): PairMember {
|
|
return {
|
|
slug,
|
|
chunk_id: 1,
|
|
take_id: null,
|
|
source_tier: 'curated',
|
|
holder,
|
|
text: 'some text',
|
|
effective_date: '2024-01-01',
|
|
effective_date_source: 'frontmatter',
|
|
};
|
|
}
|
|
|
|
function buildFinding(slugA: string, slugB: string): ContradictionFinding {
|
|
return {
|
|
kind: 'cross_slug_chunks',
|
|
a: buildMember(slugA),
|
|
b: buildMember(slugB),
|
|
combined_score: 0.85,
|
|
verdict: 'contradiction',
|
|
severity: 'medium',
|
|
axis: 'evidence',
|
|
confidence: 0.8,
|
|
resolution_kind: 'manual_review',
|
|
resolution_command: 'gbrain takes resolve N --quality incorrect',
|
|
};
|
|
}
|
|
|
|
function buildProfile(activeTags: string[], brier: number | null = 0.21): CalibrationProfileRow {
|
|
return {
|
|
id: '1',
|
|
source_id: 'default',
|
|
holder: 'garry',
|
|
wave_version: 'v0.36.1.0',
|
|
generated_at: '2026-05-17T00:00:00Z',
|
|
published: false,
|
|
total_resolved: 12,
|
|
brier,
|
|
accuracy: 0.6,
|
|
partial_rate: 0.1,
|
|
grade_completion: 1.0,
|
|
pattern_statements: ['something'],
|
|
active_bias_tags: activeTags,
|
|
voice_gate_passed: true,
|
|
voice_gate_attempts: 1,
|
|
model_id: 'claude-sonnet-4-6',
|
|
};
|
|
}
|
|
|
|
// ─── R2 regression: no profile → byte-identical output ──────────────
|
|
|
|
describe('tagFindingWithCalibration — R2 regression', () => {
|
|
test('null profile returns null tag (contradictions output unchanged)', () => {
|
|
const finding = buildFinding('wiki/companies/acme-example', 'wiki/companies/widget-co');
|
|
expect(tagFindingWithCalibration(finding, null)).toBeNull();
|
|
});
|
|
|
|
test('profile with empty active_bias_tags returns null', () => {
|
|
const finding = buildFinding('wiki/companies/acme', 'wiki/companies/widget');
|
|
expect(tagFindingWithCalibration(finding, buildProfile([]))).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ─── computeDomainHint ──────────────────────────────────────────────
|
|
|
|
describe('computeDomainHint', () => {
|
|
test('companies slug → hiring/market-timing hint', () => {
|
|
expect(computeDomainHint(buildFinding('wiki/companies/a', 'wiki/companies/b'))).toMatch(/hiring|market-timing/);
|
|
});
|
|
|
|
test('people slug → founder-behavior hint', () => {
|
|
expect(computeDomainHint(buildFinding('wiki/people/a', 'wiki/people/b'))).toMatch(/founder-behavior|hiring/);
|
|
});
|
|
|
|
test('macro slug → macro hint', () => {
|
|
expect(computeDomainHint(buildFinding('wiki/macro/forecast', 'wiki/macro/timing'))).toBe('macro');
|
|
});
|
|
|
|
test('geography slug → geography hint', () => {
|
|
expect(computeDomainHint(buildFinding('wiki/geography/ny', 'wiki/geography/sf'))).toBe('geography');
|
|
});
|
|
|
|
test('unrecognized slug → empty hint', () => {
|
|
expect(computeDomainHint(buildFinding('wiki/random/x', 'wiki/random/y'))).toBe('');
|
|
});
|
|
});
|
|
|
|
// ─── Happy path: tag matches ────────────────────────────────────────
|
|
|
|
describe('tagFindingWithCalibration — match path', () => {
|
|
test('macro finding matches "late-on-macro-tech" tag', () => {
|
|
const finding = buildFinding('wiki/macro/forecast-2024', 'wiki/macro/forecast-2026');
|
|
const profile = buildProfile(['late-on-macro-tech']);
|
|
const tag = tagFindingWithCalibration(finding, profile);
|
|
expect(tag).not.toBeNull();
|
|
expect(tag!.bias_tag).toBe('late-on-macro-tech');
|
|
expect(tag!.context).toContain('late-on-macro-tech');
|
|
});
|
|
|
|
test('geography finding matches "over-confident-geography" tag', () => {
|
|
const finding = buildFinding('wiki/geography/ny-tech', 'wiki/geography/sf-tech');
|
|
const profile = buildProfile(['over-confident-geography']);
|
|
const tag = tagFindingWithCalibration(finding, profile);
|
|
expect(tag).not.toBeNull();
|
|
expect(tag!.bias_tag).toBe('over-confident-geography');
|
|
});
|
|
|
|
test('mismatch: companies finding does NOT match macro-only tag', () => {
|
|
const finding = buildFinding('wiki/companies/acme', 'wiki/companies/widget');
|
|
// Active tag is macro only; companies hint is hiring/market-timing, not macro.
|
|
const profile = buildProfile(['late-on-macro-tech']);
|
|
const tag = tagFindingWithCalibration(finding, profile);
|
|
expect(tag).toBeNull();
|
|
});
|
|
|
|
test('first-match-wins when multiple tags could match the hint', () => {
|
|
const finding = buildFinding('wiki/companies/acme', 'wiki/companies/widget');
|
|
const profile = buildProfile(['over-confident-hiring', 'under-calibrated-market-timing']);
|
|
const tag = tagFindingWithCalibration(finding, profile);
|
|
expect(tag).not.toBeNull();
|
|
// companies → first candidate is 'hiring'; the tag containing 'hiring' wins.
|
|
expect(tag!.bias_tag).toBe('over-confident-hiring');
|
|
});
|
|
});
|
|
|
|
// ─── buildBiasContextString ─────────────────────────────────────────
|
|
|
|
describe('buildBiasContextString', () => {
|
|
test('emits tag name + verdict + severity + Brier', () => {
|
|
const finding = buildFinding('wiki/companies/acme', 'wiki/companies/widget');
|
|
const profile = buildProfile(['over-confident-hiring'], 0.31);
|
|
const ctx = buildBiasContextString('over-confident-hiring', finding, profile);
|
|
expect(ctx).toContain('over-confident-hiring');
|
|
expect(ctx).toContain('contradiction'); // verdict
|
|
expect(ctx).toContain('medium'); // severity
|
|
expect(ctx).toContain('Brier 0.31');
|
|
});
|
|
|
|
test('omits Brier when null', () => {
|
|
const finding = buildFinding('wiki/companies/acme', 'wiki/companies/widget');
|
|
const profile = buildProfile(['over-confident-hiring'], null);
|
|
const ctx = buildBiasContextString('over-confident-hiring', finding, profile);
|
|
expect(ctx).not.toContain('Brier null');
|
|
expect(ctx).not.toContain('Brier NaN');
|
|
});
|
|
});
|