From add9aa22bfef7906adb7ef2860c5121e064aceb3 Mon Sep 17 00:00:00 2001 From: Tyler Robinson Date: Thu, 23 Jul 2026 11:01:46 -0700 Subject: [PATCH] fix(health): count 'entity' pages in graph health metrics (#2639) Reland of #2639, reverted with its batch in 68e4cebd. getHealth's entity_pages CTE and the top-linked-pages query only match the legacy 'person' and 'company' types, so brains using the gbrain-base-v2 pack's 'entity' type report 0% entity link/timeline coverage in `gbrain health`. Add 'entity' to both queries in both engines (PGLite + Postgres, in lockstep per the engine-parity rule). Reland fix (the batch-red root cause): the original PR's test expected the entities/project-x page to appear in orphan_pages, but #3023's shared orphan-reporting policy (landed before #2639 merged) excludes the 'entities' first segment from orphan reporting, so the test failed on master. Orphan expectations now account for the policy exclusion. Co-Authored-By: Claude Fable 5 --- src/core/pglite-engine.ts | 4 ++-- src/core/postgres-engine.ts | 4 ++-- test/pglite-engine.test.ts | 13 ++++++++----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/core/pglite-engine.ts b/src/core/pglite-engine.ts index 3b6e422c9..e27d80501 100644 --- a/src/core/pglite-engine.ts +++ b/src/core/pglite-engine.ts @@ -5261,7 +5261,7 @@ export class PGLiteEngine implements BrainEngine { // dashboard, v0.10.3 metrics give entity-page-level granularity. const { rows: [h] } = await this.db.query(` WITH entity_pages AS ( - SELECT id, slug FROM pages WHERE type IN ('person', 'company') + SELECT id, slug FROM pages WHERE type IN ('entity', 'person', 'company') ) SELECT (SELECT count(*) FROM pages) as page_count, @@ -5289,7 +5289,7 @@ export class PGLiteEngine implements BrainEngine { SELECT p.slug, (SELECT count(*) FROM links l WHERE l.from_page_id = p.id OR l.to_page_id = p.id)::int as link_count FROM pages p - WHERE p.type IN ('person', 'company') + WHERE p.type IN ('entity', 'person', 'company') ORDER BY link_count DESC LIMIT 5 `); diff --git a/src/core/postgres-engine.ts b/src/core/postgres-engine.ts index b86292b2a..94f90ba33 100644 --- a/src/core/postgres-engine.ts +++ b/src/core/postgres-engine.ts @@ -5364,7 +5364,7 @@ export class PostgresEngine implements BrainEngine { // dashboard health. const [h] = await sql` WITH entity_pages AS ( - SELECT id, slug FROM pages WHERE type IN ('person', 'company') + SELECT id, slug FROM pages WHERE type IN ('entity', 'person', 'company') ) SELECT (SELECT count(*) FROM pages) as page_count, @@ -5389,7 +5389,7 @@ export class PostgresEngine implements BrainEngine { SELECT p.slug, (SELECT count(*) FROM links l WHERE l.from_page_id = p.id OR l.to_page_id = p.id)::int as link_count FROM pages p - WHERE p.type IN ('person', 'company') + WHERE p.type IN ('entity', 'person', 'company') ORDER BY link_count DESC LIMIT 5 `; diff --git a/test/pglite-engine.test.ts b/test/pglite-engine.test.ts index fe24f1463..e41b3c4f8 100644 --- a/test/pglite-engine.test.ts +++ b/test/pglite-engine.test.ts @@ -1434,6 +1434,7 @@ describe('PGLiteEngine: getHealth graph metrics', () => { await engine.putPage('people/alice', { ...testPage, type: 'person', title: 'Alice' }); await engine.putPage('people/bob', { ...testPage, type: 'person', title: 'Bob' }); await engine.putPage('companies/acme', { ...testPage, type: 'company', title: 'Acme' }); + await engine.putPage('entities/project-x', { ...testPage, type: 'entity', title: 'Project X' }); }); test('link_coverage = 0 when no links exist', async () => { @@ -1442,17 +1443,17 @@ describe('PGLiteEngine: getHealth graph metrics', () => { }); test('link_coverage = % of entity pages with >= 1 inbound link', async () => { - // Acme gets 1 inbound link (from Alice), Alice/Bob get 0 inbound. - // 1 of 3 entity pages has inbound links -> 33%. + // Acme gets 1 inbound link (from Alice); Alice/Bob/Project X get 0 inbound. + // 1 of 4 entity pages has inbound links -> 25%. await engine.addLink('people/alice', 'companies/acme', '', 'works_at'); const h = await engine.getHealth(); - expect(h.link_coverage).toBeCloseTo(1 / 3, 2); + expect(h.link_coverage).toBeCloseTo(1 / 4, 2); }); test('timeline_coverage = % with >= 1 timeline entry', async () => { await engine.addTimelineEntry('people/alice', { date: '2026-01-15', summary: 'Joined' }); const h = await engine.getHealth(); - expect(h.timeline_coverage).toBeCloseTo(1 / 3, 2); + expect(h.timeline_coverage).toBeCloseTo(1 / 4, 2); }); test('most_connected lists top entities by link count', async () => { @@ -1465,7 +1466,9 @@ describe('PGLiteEngine: getHealth graph metrics', () => { }); test('orphan_pages: pages with neither inbound nor outbound links', async () => { - // All 3 pages start with no links. Expect 3 orphans. + // All 4 pages start with no links, but entities/project-x is excluded + // from orphan reporting by the shared orphan policy ('entities' is a + // first-segment exclusion in orphan-policy.ts). Expect 3 orphans. const h = await engine.getHealth(); expect(h.orphan_pages).toBe(3);