mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 19:49:14 +00:00
feat(chronicle): doctor chronicle_projection_health probe (#2390)
Life Chronicle Phase B.13. An always-run doctor check (keyed off the event_page_id schema, NOT a migration verify-hook) counts timeline projections whose event page is soft-deleted — hidden at read time, surfaced here as a cleanup backlog (`gbrain integrity auto`). Tolerant of pre-migration brains. 1 detection test. (auto_chronicle / chronicle.tz flags already work via getConfig defaults; their docs land with document-release at ship.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8d0b779306
commit
dca35a6bd0
@@ -538,6 +538,33 @@ export async function doctorReportRemote(engine: BrainEngine): Promise<DoctorRep
|
||||
checks.push({ name: 'timeline_dedup_index', status: 'warn', message: 'Could not check idx_timeline_dedup shape' });
|
||||
}
|
||||
|
||||
// v0.42.x — Life Chronicle (#2390): orphaned event projections. Reads already
|
||||
// hide projections whose event page is soft-deleted (read-time correctness);
|
||||
// this always-run probe surfaces the cleanup backlog. Keyed off the real
|
||||
// schema (event_page_id), NOT a migration verify-hook, per
|
||||
// migration-verify-hook-never-runs-on-stamped-brains.
|
||||
try {
|
||||
const orphans = await engine.executeRaw<{ n: number }>(
|
||||
`SELECT count(*)::int AS n FROM timeline_entries te
|
||||
JOIN pages ep ON ep.id = te.event_page_id
|
||||
WHERE te.event_page_id IS NOT NULL AND ep.deleted_at IS NOT NULL`,
|
||||
);
|
||||
const n = Number(orphans[0]?.n ?? 0);
|
||||
checks.push(
|
||||
n === 0
|
||||
? { name: 'chronicle_projection_health', status: 'ok', message: 'No orphaned event projections' }
|
||||
: {
|
||||
name: 'chronicle_projection_health',
|
||||
status: 'warn',
|
||||
message:
|
||||
`${n} timeline projection(s) point to soft-deleted event pages ` +
|
||||
'(hidden at read time; clean up with `gbrain integrity auto`).',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
checks.push({ name: 'chronicle_projection_health', status: 'ok', message: 'no event projections yet' });
|
||||
}
|
||||
|
||||
// 3. Brain score
|
||||
try {
|
||||
const health = await engine.getHealth();
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* v0.42.x — Life Chronicle (#2390) doctor chronicle_projection_health (Phase B.13).
|
||||
* Verifies the orphan-detection query the doctor check runs: a timeline
|
||||
* projection whose event page is soft-deleted is counted (hidden at read time,
|
||||
* flagged for cleanup).
|
||||
*/
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||||
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
||||
import { runChronicleExtract, type ChronicleJudge } from '../src/core/chronicle/extract-events.ts';
|
||||
|
||||
let engine: PGLiteEngine;
|
||||
|
||||
const orphanQuery = `SELECT count(*)::int AS n FROM timeline_entries te
|
||||
JOIN pages ep ON ep.id = te.event_page_id
|
||||
WHERE te.event_page_id IS NOT NULL AND ep.deleted_at IS NOT NULL`;
|
||||
|
||||
beforeAll(async () => {
|
||||
engine = new PGLiteEngine();
|
||||
await engine.connect({ database_url: '' });
|
||||
await engine.initSchema();
|
||||
});
|
||||
afterAll(async () => { await engine.disconnect(); });
|
||||
|
||||
describe('chronicle_projection_health detection', () => {
|
||||
test('counts projections whose event page is soft-deleted; zero otherwise', async () => {
|
||||
await engine.putPage('meetings/d', { type: 'meeting', title: 'd', compiled_truth: 'x'.repeat(120), effective_date: new Date('2026-06-18T12:00:00Z') });
|
||||
const judge: ChronicleJudge = async () => ({ events: [{ when: '2026-06-18T12:00:00Z', who: [], what: 'an event', kind: 'meeting' }] });
|
||||
await runChronicleExtract(engine, { slug: 'meetings/d', judge });
|
||||
|
||||
let r = await engine.executeRaw<{ n: number }>(orphanQuery);
|
||||
expect(Number(r[0].n)).toBe(0); // healthy: event page live
|
||||
|
||||
// Soft-delete the event page → its projection becomes an orphan.
|
||||
await engine.executeRaw(`UPDATE pages SET deleted_at = now() WHERE type = 'event'`);
|
||||
r = await engine.executeRaw<{ n: number }>(orphanQuery);
|
||||
expect(Number(r[0].n)).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user