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:
Garry Tan
2026-06-24 15:21:30 -07:00
co-authored by Claude Opus 4.8
parent 8d0b779306
commit dca35a6bd0
2 changed files with 65 additions and 0 deletions
+27
View File
@@ -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();
+38
View File
@@ -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);
});
});