feat(chronicle): feature eval — gbrain eval chronicle (PRIMARY proof) (#2390)

Life Chronicle Phase A.9, the North-Star proof. A deterministic, CI-safe eval
(brings its own in-memory PGLite; no LLM, no gateway) builds a synthetic month
corpus with a known gold chronology + a planted ontology supersession + a
planted conflict, then scores the chronicle layer on six gold tasks: day
reconstruction (intra-day order), last-seen exact date, ontology supersession,
--asof valid-time travel, contradiction surfacing, and source isolation.
`gbrain eval chronicle [--json]` exits 0 iff all pass — currently 6/6. The OFF
baseline (raw meeting pages) structurally can't order intra-day events or
time-travel ontology; the ON path does. (The live-LLM OFF-vs-ON agent arm +
LongMemEval temporal slice are a follow-up; this deterministic bar gates CI.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-06-24 15:28:37 -07:00
co-authored by Claude Opus 4.8
parent 41b94ae21e
commit d63f605e26
4 changed files with 166 additions and 0 deletions
+8
View File
@@ -1396,6 +1396,14 @@ async function handleCliOnly(command: string, args: string[]) {
return;
}
// v0.42.x (#2390): `gbrain eval chronicle` is deterministic — brings its own
// in-memory PGLite, no DB/gateway. CI fixture gate runs anywhere.
if (command === 'eval' && args[0] === 'chronicle') {
const { runEvalChronicle } = await import('./commands/eval-chronicle.ts');
setCliExitVerdict(await runEvalChronicle(args.slice(1)));
return;
}
// v0.41.13.0: `gbrain eval conversation-parser` is pure-function
// (parses fixture JSONL, runs parseConversation, scores results).
// No DB access; bypass connectEngine entirely so the CI fixture
+44
View File
@@ -0,0 +1,44 @@
// v0.42.x — Life Chronicle (#2390) `gbrain eval chronicle` (Phase A.9).
// Deterministic, brings its own in-memory PGLite (no DB, no gateway), so the
// CI fixture gate runs anywhere. Exit 0 only on a perfect score.
import { PGLiteEngine } from '../core/pglite-engine.ts';
import { runChronicleEval } from '../eval/chronicle/harness.ts';
const HELP = `Usage: gbrain eval chronicle [--json]
Deterministic Life Chronicle (#2390) feature eval. Builds a synthetic month
corpus with a known gold chronology + a planted ontology supersession + a
planted conflict, then scores the chronicle layer on: day reconstruction
(intra-day order), last-seen exact date, ontology supersession + --asof
time-travel, contradiction surfacing, and source isolation.
Exit code 0 iff every task passes.
`;
export async function runEvalChronicle(args: string[]): Promise<number> {
if (args.includes('--help') || args.includes('-h')) {
process.stdout.write(HELP);
return 0;
}
const json = args.includes('--json');
const engine = new PGLiteEngine();
await engine.connect({ database_url: '' });
await engine.initSchema();
try {
const result = await runChronicleEval(engine);
if (json) {
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
} else {
process.stderr.write(
`[eval chronicle] ${result.passed}/${result.total} tasks passed ` +
`(score ${(result.score * 100).toFixed(0)}%)\n`,
);
for (const t of result.tasks) {
process.stderr.write(` ${t.passed ? 'PASS' : 'FAIL'} ${t.id}${t.detail}\n`);
}
}
return result.score === 1 ? 0 : 1;
} finally {
await engine.disconnect();
}
}
+89
View File
@@ -0,0 +1,89 @@
// v0.42.x — Life Chronicle (#2390) feature eval (Phase A.9), the PRIMARY proof.
//
// Deterministic + CI-safe (no LLM): builds a small synthetic corpus with a KNOWN
// gold chronology + a planted ontology supersession + a planted conflict, then
// asserts the chronicle layer answers the temporal/longitudinal questions
// correctly. This is the "does the feature deliver value" bar from the North
// Star — chronology reconstruction, last-seen, supersession time-travel,
// contradiction surfacing, source isolation — measured against gold, not vibes.
//
// The OFF baseline (raw meeting pages) structurally CANNOT order intra-day
// events or time-travel ontology; the ON path (chronicle ops) does. We score
// the ON path against gold; a perfect score is the proof the ops are correct.
import type { BrainEngine } from '../../core/engine.ts';
import { runChronicleExtract, type ChronicleJudge } from '../../core/chronicle/extract-events.ts';
export interface ChronicleEvalTask { id: string; passed: boolean; detail: string }
export interface ChronicleEvalResult {
tasks: ChronicleEvalTask[];
passed: number;
total: number;
score: number; // passed / total, [0,1]
}
const ALICE = 'people/alice-example';
const BOB = 'people/bob-example';
/** Seed the synthetic corpus into a fresh engine (schema already initialized). */
export async function seedChronicleEvalCorpus(engine: BrainEngine): Promise<void> {
// Two same-day meetings → two events, gold intra-day order AM then PM.
await engine.putPage('meetings/2026-03-02-am', {
type: 'meeting', title: 'Morning standup', compiled_truth: 'x'.repeat(120),
frontmatter: { attendees: [ALICE] }, effective_date: new Date('2026-03-02T09:00:00Z'),
});
await engine.putPage('meetings/2026-03-02-pm', {
type: 'meeting', title: 'Afternoon review', compiled_truth: 'y'.repeat(120),
frontmatter: { attendees: [ALICE] }, effective_date: new Date('2026-03-02T15:00:00Z'),
});
const judge = (when: string, what: string): ChronicleJudge => async () => ({
events: [{ when, who: [ALICE], what, kind: 'meeting' }],
});
await runChronicleExtract(engine, { slug: 'meetings/2026-03-02-am', judge: judge('2026-03-02T09:00:00Z', 'Morning standup') });
await runChronicleExtract(engine, { slug: 'meetings/2026-03-02-pm', judge: judge('2026-03-02T15:00:00Z', 'Afternoon review') });
// Ontology: alice was a founder, became an advisor (forward supersession).
await engine.mergeOntologyFact({ entitySlug: ALICE, dimension: 'role', value: 'founder', source: 'meetings/2024-01-10', validFrom: '2024-01-01' });
await engine.mergeOntologyFact({ entitySlug: ALICE, dimension: 'role', value: 'advisor', source: 'meetings/2026-03-02-pm', validFrom: '2026-03-01' });
// Planted conflict: bob has two backdated-vs-forward open values from 2 sources.
await engine.mergeOntologyFact({ entitySlug: BOB, dimension: 'role', value: 'advisor', source: 'meetings/a', validFrom: '2026-05-01' });
await engine.mergeOntologyFact({ entitySlug: BOB, dimension: 'role', value: 'founder', source: 'meetings/b', validFrom: '2026-01-01' });
}
export async function runChronicleEval(engine: BrainEngine): Promise<ChronicleEvalResult> {
await seedChronicleEvalCorpus(engine);
const tasks: ChronicleEvalTask[] = [];
const check = (id: string, ok: boolean, detail: string) => tasks.push({ id, passed: ok, detail });
// 1. Day reconstruction in correct intra-day order.
const day = await engine.getTimelineForDate('2026-03-02', { sourceId: 'default' });
const order = day.map((r) => r.summary);
check('day_order', JSON.stringify(order) === JSON.stringify(['Morning standup', 'Afternoon review']),
`order=${JSON.stringify(order)}`);
// 2. Last-seen exact date.
const ls = await engine.getLastSeen(ALICE, { sourceId: 'default' });
check('last_seen', ls.last_date === '2026-03-02', `last_date=${ls.last_date}`);
// 3. Supersession: current value is the new one.
const now = await engine.getOntology(ALICE, { sourceId: 'default' });
const roleNow = now.find((v) => v.dimension === 'role')?.value;
check('supersession_now', roleNow === 'advisor', `role_now=${roleNow}`);
// 4. Valid-time travel: as-of before the change returns the prior value.
const past = await engine.getOntology(ALICE, { sourceId: 'default', asof: '2025-01-01' });
const rolePast = past.find((v) => v.dimension === 'role')?.value;
check('supersession_asof', rolePast === 'founder', `role_asof=${rolePast}`);
// 5. Contradiction surfaced (genuine disagreement, not supersession).
const conflicts = await engine.findOntologyConflicts({ sourceId: 'default' });
check('conflict_surfaced', conflicts.some((c) => c.entity_slug === BOB && c.dimension === 'role'),
`conflicts=${conflicts.length}`);
// 6. Source isolation: querying another source leaks nothing.
const otherSource = await engine.getOntology(ALICE, { sourceId: 'nonexistent-source' });
check('source_isolation', otherSource.length === 0, `leaked=${otherSource.length}`);
const passed = tasks.filter((t) => t.passed).length;
return { tasks, passed, total: tasks.length, score: tasks.length ? passed / tasks.length : 0 };
}
+25
View File
@@ -0,0 +1,25 @@
/**
* v0.42.x — Life Chronicle (#2390) feature eval harness (Phase A.9, PRIMARY proof).
* The chronicle layer must answer EVERY gold task on the synthetic corpus.
*/
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { runChronicleEval } from '../src/eval/chronicle/harness.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({ database_url: '' });
await engine.initSchema();
});
afterAll(async () => { await engine.disconnect(); });
describe('runChronicleEval', () => {
test('scores a perfect 6/6 on the synthetic corpus (the value proof)', async () => {
const result = await runChronicleEval(engine);
const failed = result.tasks.filter((t) => !t.passed).map((t) => `${t.id}: ${t.detail}`);
expect(failed).toEqual([]); // surfaces which gold task failed, if any
expect(result.score).toBe(1);
expect(result.total).toBe(6);
});
});