Compare commits

...
Author SHA1 Message Date
Garry TanandClaude Fable 5 1e74fe0605 fix(test): use withEnv() for OPENAI_API_KEY in #895 receipt test
check:test-isolation R1 flagged the direct process.env mutation in
beforeAll/afterAll. Scope the key removal to the hybridSearch call via
the canonical withEnv() helper instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 10:54:43 -07:00
Garry TanandClaude Fable 5 cb2b44ba11 fix(doctor,cli,search): singleton P0s — NaN score prefix, missing put_page arbiter check, ranking-inversion receipt
- #468: gbrain query printed '[NaN]' when a non-finite score reached the
  formatter (NaN.toFixed(4) is truthy, so the '?' fallback never fired).
  Gate on Number.isFinite in src/cli.ts formatResult.
- #550: new doctor check pages_slug_unique_index probes pg_indexes for a
  non-partial unique index on pages(source_id, slug) — matched by COLUMNS,
  not constraint name — and fails with an apply-migrations --force-schema
  hint when absent. Wired into both the local buildChecks db phase and
  doctorReportRemote (mirrors the #2038 timeline_dedup shape check).
- #895: repro receipt — the reported inversion (backlink-magnet concept
  page outranking the exact-name person page on 'Who is Zhang San') no
  longer reproduces on master (title-match boost + backlink floor gating).
  Pinned by test/ranking-exact-name-backlink-inversion.test.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 14:36:24 -07:00
6 changed files with 263 additions and 1 deletions
+3 -1
View File
@@ -888,7 +888,9 @@ export function formatResult(opName: string, result: unknown): string {
return formatResultsExplain(results);
}
return results.map(r =>
`[${r.score?.toFixed(4) || '?'}] ${r.slug} -- ${r.chunk_text?.slice(0, 100) || ''}${r.stale ? ' (stale)' : ''}`,
// #468: NaN.toFixed(4) is the truthy string 'NaN', so `?.toFixed() || '?'`
// printed '[NaN]'. Gate on Number.isFinite instead.
`[${Number.isFinite(r.score) ? r.score.toFixed(4) : '?'}] ${r.slug} -- ${r.chunk_text?.slice(0, 100) || ''}${r.stale ? ' (stale)' : ''}`,
).join('\n') + '\n';
}
case 'get_tags': {
+57
View File
@@ -528,6 +528,56 @@ export async function childTableOrphansCheck(engine: BrainEngine): Promise<Check
};
}
/**
* #550 pages(source_id, slug) unique-index presence.
*
* `putPage` in BOTH engines upserts via `ON CONFLICT (source_id, slug)`, which
* needs a non-partial unique index whose column set is exactly
* {source_id, slug} as its arbiter. Migration v23 adds `pages_source_slug_key`
* by NAME, so a brain whose constraint was dropped/renamed by an external
* migration is stamped past v23 with silently broken writes ("no unique or
* exclusion constraint matching the ON CONFLICT specification") that the
* version counter can't see. Match by COLUMNS, not name any conforming
* unique index satisfies the arbiter (mirror of the #2038 timeline_dedup
* shape check).
*/
export async function checkPagesSlugUniqueIndex(engine: BrainEngine): Promise<Check> {
const name = 'pages_slug_unique_index';
try {
const rows = await engine.executeRaw<{ indexdef: string }>(
`SELECT indexdef FROM pg_indexes WHERE tablename = 'pages'`,
);
const ok = rows.some(r => {
const def = r.indexdef;
// Partial indexes can't arbitrate a bare ON CONFLICT (source_id, slug).
if (!/^CREATE UNIQUE INDEX/i.test(def) || /\bWHERE\b/i.test(def)) return false;
const open = def.lastIndexOf('(');
const close = def.lastIndexOf(')');
if (open < 0 || close < open) return false;
const cols = def
.slice(open + 1, close)
.split(',')
.map(c => c.trim().split(/\s+/)[0]) // drop DESC / opclass suffixes
.filter(Boolean)
.sort();
return cols.length === 2 && cols[0] === 'slug' && cols[1] === 'source_id';
});
if (ok) {
return { name, status: 'ok', message: 'pages has a unique index on (source_id, slug)' };
}
return {
name,
status: 'fail',
message:
'No unique index on pages(source_id, slug) — every put_page write fails with ' +
'"no unique or exclusion constraint matching the ON CONFLICT specification" (#550). ' +
'Run `gbrain apply-migrations --force-schema` to restore it.',
};
} catch {
return { name, status: 'warn', message: 'Could not check pages(source_id, slug) unique index' };
}
}
export async function doctorReportRemote(engine: BrainEngine): Promise<DoctorReport> {
const checks: Check[] = [];
@@ -602,6 +652,9 @@ export async function doctorReportRemote(engine: BrainEngine): Promise<DoctorRep
checks.push({ name: 'timeline_dedup_index', status: 'warn', message: 'Could not check idx_timeline_dedup shape' });
}
// 2c. #550: pages(source_id, slug) unique index — putPage's ON CONFLICT arbiter.
checks.push(await checkPagesSlugUniqueIndex(engine));
// 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
@@ -5249,6 +5302,10 @@ export async function buildChecks(
progress.heartbeat('pgvector');
checks.push(await pgvectorCheck(engine));
// 4a. #550: pages(source_id, slug) unique index — putPage's ON CONFLICT arbiter.
progress.heartbeat('pages_slug_unique_index');
checks.push(await checkPagesSlugUniqueIndex(engine));
// 4b. PgBouncer / prepared-statement compatibility.
// URL-only inspection — no DB roundtrip — so this is cheap and works
// regardless of whether the caller is the module singleton or a
+1
View File
@@ -170,6 +170,7 @@ export const META_CHECK_NAMES: ReadonlySet<string> = new Set([
'eval_capture',
'minions_migration',
'multi_source_drift',
'pages_slug_unique_index',
'schema_pack_active',
'schema_pack_consistency',
'schema_pack_source_drift',
+35
View File
@@ -0,0 +1,35 @@
/**
* #468 — `[NaN]` score prefix in `gbrain query` output.
*
* NaN.toFixed(4) returns the truthy string 'NaN', so the old
* `r.score?.toFixed(4) || '?'` fallback never fired and the CLI printed
* '[NaN] slug -- ...'. The formatter must gate on Number.isFinite.
*/
import { describe, test, expect } from 'bun:test';
import { formatResult } from '../src/cli.ts';
describe('formatResult — query score prefix (#468)', () => {
test('finite score renders 4 decimal places', () => {
const out = formatResult('query', [
{ score: 0.4922, slug: 'people/zhangsan', chunk_text: 'Zhang San' },
]);
expect(out).toContain('[0.4922] people/zhangsan');
});
test('NaN score renders ? — never the string NaN', () => {
const out = formatResult('query', [
{ score: NaN, slug: 'people/zhangsan', chunk_text: 'Zhang San' },
]);
expect(out).toContain('[?] people/zhangsan');
expect(out).not.toContain('NaN');
});
test('missing and Infinity scores also render ?', () => {
const out = formatResult('search', [
{ slug: 'a', chunk_text: '' },
{ score: Infinity, slug: 'b', chunk_text: '' },
]);
expect(out).toContain('[?] a');
expect(out).toContain('[?] b');
});
});
+65
View File
@@ -0,0 +1,65 @@
/**
* #550 — doctor check for the pages(source_id, slug) unique index.
*
* putPage upserts via ON CONFLICT (source_id, slug); a brain whose
* pages_source_slug_key constraint was dropped/renamed by an external
* migration has silently broken writes the version counter can't see.
* The check must match by COLUMNS, not by constraint name.
*/
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { checkPagesSlugUniqueIndex } from '../src/commands/doctor.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
}, 60_000);
afterAll(async () => {
await engine.disconnect();
});
describe('checkPagesSlugUniqueIndex (#550)', () => {
test('fresh schema → ok', async () => {
const check = await checkPagesSlugUniqueIndex(engine);
expect(check.status).toBe('ok');
});
test('constraint dropped → fail with apply-migrations hint, and putPage really breaks', async () => {
await engine.executeRaw('ALTER TABLE pages DROP CONSTRAINT pages_source_slug_key');
const check = await checkPagesSlugUniqueIndex(engine);
expect(check.status).toBe('fail');
expect(check.message).toContain('apply-migrations');
// The condition doctor now detects is a real write outage:
await expect(
engine.putPage('people/x', { type: 'person', title: 'X', compiled_truth: 'x' }),
).rejects.toThrow(/unique or exclusion constraint/);
});
test('conforming unique index under a DIFFERENT name/order → ok (columns, not name)', async () => {
await engine.executeRaw(
'CREATE UNIQUE INDEX pages_custom_uniq ON pages (slug, source_id)',
);
const check = await checkPagesSlugUniqueIndex(engine);
expect(check.status).toBe('ok');
await engine.executeRaw('DROP INDEX pages_custom_uniq');
});
test('partial unique index does NOT satisfy the arbiter → still fail', async () => {
await engine.executeRaw(
"CREATE UNIQUE INDEX pages_partial_uniq ON pages (source_id, slug) WHERE type = 'person'",
);
const check = await checkPagesSlugUniqueIndex(engine);
expect(check.status).toBe('fail');
await engine.executeRaw('DROP INDEX pages_partial_uniq');
// Restore the canonical constraint so later suites sharing this DB stay valid.
await engine.executeRaw(
'ALTER TABLE pages ADD CONSTRAINT pages_source_slug_key UNIQUE (source_id, slug)',
);
const check2 = await checkPagesSlugUniqueIndex(engine);
expect(check2.status).toBe('ok');
});
});
@@ -0,0 +1,102 @@
/**
* #895 — ranking inversion repro receipt.
*
* Reported on v0.31.3 (PGLite): `gbrain query "Who is Zhang San"` ranked a
* heavily-backlinked concept page (concepts/memory-augmented-retrieval,
* score 1.07) ABOVE the exact-name person page (people/zhangsan, 0.49).
* The pipeline has since gained title-match boost, backlink floor gating,
* and NaN guards. This test pins the fixture so the inversion can't return:
* the exact-name page must outrank the backlink-magnet concept page.
*
* Keyword-only path (no embedding provider in test) — the backlink boost
* applies post-fusion regardless of which recall arm produced the result.
*/
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { hybridSearch } from '../src/core/search/hybrid.ts';
import { withEnv } from './helpers/with-env.ts';
let engine: PGLiteEngine;
const FIXTURE: Array<{ slug: string; type: string; title: string; body: string }> = [
{
slug: 'people/zhangsan',
type: 'person',
title: 'Zhang San',
body: 'Zhang San, famously known as the forgetful founder, builds memory tools.',
},
{
slug: 'people/lisi',
type: 'person',
title: 'Li Si',
body: 'Li Si, nicknamed "The Spender", works with Zhang San on retrieval experiments.',
},
{
slug: 'companies/goldfish-memory-tech',
type: 'company',
title: 'Goldfish Memory Tech',
body: 'Goldfish Memory Tech was founded by Zhang San to commercialize memory augmentation.',
},
{
slug: 'concepts/memory-augmented-retrieval',
type: 'concept',
title: 'Memory Augmented Retrieval',
body: 'Memory Spa Method. Zhang San popularized memory augmented retrieval as a discipline.',
},
{
slug: 'meetings/may-2026-meetup',
type: 'meeting',
title: 'May 2026 Meetup',
body: 'The symposium was held in May. Zhang San presented the Memory Spa Method.',
},
];
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
for (const p of FIXTURE) {
await engine.putPage(p.slug, { type: p.type, title: p.title, compiled_truth: p.body });
// putPage never chunks; the keyword arm joins content_chunks.
await engine.upsertChunks(p.slug, [
{ chunk_index: 0, chunk_text: p.body, chunk_source: 'compiled_truth' },
]);
}
// Make the concept page a backlink magnet — the boost source of the
// reported inversion. Every other page links to it.
await engine.addLinksBatch(
FIXTURE.filter(p => p.slug !== 'concepts/memory-augmented-retrieval').map(p => ({
from_slug: p.slug,
to_slug: 'concepts/memory-augmented-retrieval',
link_type: 'mentions',
link_source: 'markdown',
context: '',
})),
);
}, 60_000);
afterAll(async () => {
await engine.disconnect();
});
describe('exact-name page vs backlink-magnet concept page (#895)', () => {
test('"Who is Zhang San" ranks people/zhangsan first, with finite scores', async () => {
// Keyword-only path: no embedding provider during the search call.
const results = await withEnv({ OPENAI_API_KEY: undefined }, () =>
hybridSearch(engine, 'Who is Zhang San', { limit: 5 }),
);
expect(results.length).toBeGreaterThan(1);
expect(results[0].slug).toBe('people/zhangsan');
for (const r of results) {
expect(Number.isFinite(r.score)).toBe(true);
}
// The concept page must not outrank the exact-name page even with
// backlinks from every other page in the corpus. It IS in the result
// set (it mentions Zhang San) and it IS backlink-boosted — that's the
// contested comparison from the report.
const concept = results.find(r => r.slug === 'concepts/memory-augmented-retrieval');
expect(concept).toBeDefined();
expect(concept!.backlink_boost ?? 1).toBeGreaterThan(1);
expect(results[0].score).toBeGreaterThanOrEqual(concept!.score);
});
});