mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
* fix(calibration): resolve owner holder via config (default 'self'), fixes #2464 Takeover of #2467 (rebased onto master). consolidate writes owner takes with holder='self' while calibration-profile, calibration CLI/op, think's calibration block, emotional-weight, and doctor's calibration_freshness all defaulted to a hardcoded 'garry' — so getScorecard returned 0 resolved and the calibration profile never built on non-upstream brains. New src/core/owner-holder.ts is the single source of truth: resolveOwnerHolder({override, configValue}) = override > emotional_weight.user_holder config > 'self'. All six call sites route through it; doctor's freshness SQL is parameterized (). Upgrade note: upstream-owner brains with historical holder='garry' profiles should `gbrain config set emotional_weight.user_holder garry` to keep reading them. Co-authored-by: devty <devty@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(calibration): replace real-name holder fixture with charlie-example placeholder Privacy iron rule: no real people's names in checked-in code. The sanctioned placeholder mapping uses people/charlie-example. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local> Co-authored-by: devty <devty@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Garry Tan <garrytan@gmail.com>
119 lines
4.3 KiB
TypeScript
119 lines
4.3 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
import {
|
|
computeEmotionalWeight,
|
|
HIGH_EMOTION_TAGS,
|
|
DEFAULT_USER_HOLDER,
|
|
} from '../src/core/cycle/emotional-weight.ts';
|
|
|
|
describe('computeEmotionalWeight', () => {
|
|
test('empty inputs return 0', () => {
|
|
expect(computeEmotionalWeight({ tags: [], takes: [] })).toBe(0);
|
|
});
|
|
|
|
test('high-emotion tag alone gives 0.5', () => {
|
|
const w = computeEmotionalWeight({ tags: ['wedding'], takes: [] });
|
|
expect(w).toBeCloseTo(0.5, 5);
|
|
});
|
|
|
|
test('tag matching is case-insensitive', () => {
|
|
const w = computeEmotionalWeight({ tags: ['WEDDING'], takes: [] });
|
|
expect(w).toBeCloseTo(0.5, 5);
|
|
});
|
|
|
|
test('non-emotion tag gives no boost', () => {
|
|
const w = computeEmotionalWeight({ tags: ['hardware', 'product'], takes: [] });
|
|
expect(w).toBe(0);
|
|
});
|
|
|
|
test('multiple high-emotion tags do not stack (cap at 0.5)', () => {
|
|
const w = computeEmotionalWeight({ tags: ['family', 'wedding', 'love'], takes: [] });
|
|
expect(w).toBeCloseTo(0.5, 5);
|
|
});
|
|
|
|
test('takes-only: density caps at 0.3 with 3+ takes', () => {
|
|
const takes = Array.from({ length: 5 }, () => ({
|
|
holder: 'other',
|
|
weight: 0,
|
|
kind: 'fact',
|
|
active: true,
|
|
}));
|
|
// density = min(5*0.1, 0.3) = 0.3; avg-weight = 0; holder-ratio = 0 (none match user).
|
|
expect(computeEmotionalWeight({ tags: [], takes })).toBeCloseTo(0.3, 5);
|
|
});
|
|
|
|
test('takes-only: avg-weight contribution scales 0..0.1', () => {
|
|
const takes = [
|
|
{ holder: 'other', weight: 1.0, kind: 'take', active: true },
|
|
];
|
|
// density = 0.1; avg-weight = 1.0 * 0.1 = 0.1; holder-ratio = 0.
|
|
expect(computeEmotionalWeight({ tags: [], takes })).toBeCloseTo(0.2, 5);
|
|
});
|
|
|
|
test('user-holder takes give the 0.1 ratio bump', () => {
|
|
const takes = [
|
|
{ holder: DEFAULT_USER_HOLDER, weight: 0, kind: 'take', active: true },
|
|
];
|
|
// density = 0.1; avg-weight = 0; holder-ratio = 1.0 * 0.1 = 0.1.
|
|
expect(computeEmotionalWeight({ tags: [], takes })).toBeCloseTo(0.2, 5);
|
|
});
|
|
|
|
test('user-holder ratio is mixed-holder aware', () => {
|
|
const takes = [
|
|
{ holder: DEFAULT_USER_HOLDER, weight: 0, kind: 'take', active: true },
|
|
{ holder: 'other', weight: 0, kind: 'take', active: true },
|
|
];
|
|
// 1 of 2 active takes = 0.5 ratio * 0.1 = 0.05; density = 0.2; avg = 0.
|
|
expect(computeEmotionalWeight({ tags: [], takes })).toBeCloseTo(0.25, 5);
|
|
});
|
|
|
|
test('inactive takes are excluded from density + ratio + avg', () => {
|
|
const takes = [
|
|
{ holder: DEFAULT_USER_HOLDER, weight: 1, kind: 'take', active: false },
|
|
{ holder: DEFAULT_USER_HOLDER, weight: 1, kind: 'take', active: false },
|
|
];
|
|
expect(computeEmotionalWeight({ tags: [], takes })).toBe(0);
|
|
});
|
|
|
|
test('output is bounded to [0..1]', () => {
|
|
// High-emotion tag + max takes + max weights + all-user-holder = 0.5+0.3+0.1+0.1 = 1.0
|
|
const takes = Array.from({ length: 10 }, () => ({
|
|
holder: DEFAULT_USER_HOLDER,
|
|
weight: 1.0,
|
|
kind: 'take',
|
|
active: true,
|
|
}));
|
|
const w = computeEmotionalWeight({ tags: ['wedding'], takes });
|
|
expect(w).toBeCloseTo(1.0, 5);
|
|
expect(w).toBeLessThanOrEqual(1);
|
|
});
|
|
|
|
test('over-1 take weights are clamped before averaging', () => {
|
|
// weight=2.0 should clamp to 1.0 in the avg path.
|
|
const takes = [
|
|
{ holder: 'other', weight: 2.0, kind: 'take', active: true },
|
|
];
|
|
expect(computeEmotionalWeight({ tags: [], takes })).toBeCloseTo(0.2, 5);
|
|
});
|
|
|
|
test('custom highEmotionTags override default', () => {
|
|
const customTags: ReadonlySet<string> = new Set(['hardware-failure']);
|
|
const w = computeEmotionalWeight(
|
|
{ tags: ['hardware-failure'], takes: [] },
|
|
{ highEmotionTags: customTags },
|
|
);
|
|
expect(w).toBeCloseTo(0.5, 5);
|
|
// Default seed list still excludes hardware-failure so without override:
|
|
expect(computeEmotionalWeight({ tags: ['hardware-failure'], takes: [] })).toBe(0);
|
|
});
|
|
|
|
test('HIGH_EMOTION_TAGS includes the v1 seed list', () => {
|
|
expect(HIGH_EMOTION_TAGS.has('wedding')).toBe(true);
|
|
expect(HIGH_EMOTION_TAGS.has('family')).toBe(true);
|
|
expect(HIGH_EMOTION_TAGS.has('mental-health')).toBe(true);
|
|
});
|
|
});
|
|
|
|
test('DEFAULT_USER_HOLDER is the canonical owner holder self', () => {
|
|
expect(DEFAULT_USER_HOLDER).toBe('self');
|
|
});
|