fix(doctor): empty brain scores 100/100 + hermetic doctor-report-remote test

Two fixes coupled because the test couldn't pass without the formula fix:

src/core/pglite-engine.ts + src/core/postgres-engine.ts — empty brain
(pageCount === 0) now gets FULL marks (100/100), not 0/100. Semantically
an empty brain has no coverage problem to penalize — there's nothing to
embed, nothing to link, nothing to orphan. Vacuous truth applies. The
pre-fix "empty = 0" caused fresh-init brains to score as critically
unhealthy on \`gbrain doctor\`, which was a structural surprise to users
who'd just run init successfully. Same fix on both engines.

test/brain-score-breakdown.test.ts — updated the "empty brain" assertion
to match the new contract (was: 0/0/0/0/0/0; is: 100/35/25/15/15/10).

test/doctor-report-remote.test.ts → renamed to .serial.test.ts and made
hermetic. The pre-fix test pulled audit data from the host ~/.gbrain
(reranker_health, sync_failures, etc.), which made the assertion
non-deterministic depending on whoever ran the suite. Now isolates
GBRAIN_HOME to a tempdir via beforeAll/afterAll; env mutation requires
serial-quarantine per scripts/check-test-isolation.sh R1.

Closes the master-state flake that was failing on every \`bun run test\`
run regardless of my branch contents.
This commit is contained in:
Garry Tan
2026-05-21 12:26:28 -07:00
parent 8d71ef8dcc
commit 9aa571f369
5 changed files with 52 additions and 17 deletions
+1
View File
@@ -50,6 +50,7 @@ Things to watch:
- **`src/commands/embed.ts`** + **`src/commands/import.ts`** — `runEmbedCore` and `runImport` consult `assertEmbeddingEnabled(loadConfig())` and refuse cleanly with a `gbrain config set embedding_model <id>` hint when `embedding_disabled: true` is set. `gbrain import --no-embed` flag still works (chunks land without vectors).
- **`src/commands/doctor.ts`** — `embedding_provider` check extended for the v0.36 silent-default repair case. Empty-brain vs non-empty-brain repair branching (drop-and-re-init vs `gbrain retrieval-upgrade`). `subagent_provider` check (v0.31.12) extended per D7 to warn when `chat_model` is non-Anthropic AND `ANTHROPIC_API_KEY` is missing.
- **`src/commands/reindex-multimodal.ts`** — preflight `resolveSchemaMultimodalDim` BEFORE the reindex sweep, mirroring the text-side contract from `initPGLite`.
- **`src/core/pglite-engine.ts` + `src/core/postgres-engine.ts`** — empty brain (`pageCount === 0`) now scores **100/100**, not 0/100. Vacuous truth: an empty brain has no coverage problem to penalize. Pre-fix, fresh `gbrain init --pglite` users saw `Brain score 0/100` on first `gbrain doctor` run, which was structurally surprising. Same fix on both engines, breakdown components unchanged for non-empty brains.
- **`test/levenshtein.test.ts`** (18 cases), **`test/embedding-dim-check.test.ts`** (23 cases, extended), **`test/providers.test.ts`** (10 cases), **`test/init-provider-picker.test.ts`** (7 cases), **`test/init-env-detection.test.ts`** (21 cases), **`test/config-set.test.ts`** (19 cases) — 98 new unit cases pinning the env-detection grouping, Matryoshka validation, picker caveat behavior, Levenshtein suggestions, and bug-reporter regression for the three no-op keys.
- **`test/e2e/init-fresh-pglite.test.ts`** (NEW, 14 E2E cases) — subprocess-driven verification of the full happy path, D3 non-TTY fail-loud (with and without env-key typos), D6 regression for the bug-reporter's three no-op config keys, D9 deferred-setup mode + import refusal, D11 preflight refusal without disk writes, and explicit-flag-wins-over-env precedence.
- **`docs/integrations/embedding-providers.md`** — TL;DR table refreshed; added "Init resolves your provider from env keys" section and "If first import fails" troubleshooting block.
+12 -5
View File
@@ -3733,11 +3733,18 @@ export class PGLiteEngine implements BrainEngine {
const noDeadLinks = pageCount > 0 ? 1 - Math.min(deadLinks / pageCount, 1) : 1;
// Bug 11 — per-component points. Sum equals brainScore by construction
// so `doctor` can render a breakdown that adds up to the total.
const embedCoverageScore = pageCount === 0 ? 0 : Math.round(embedCoverage * 35);
const linkDensityScore = pageCount === 0 ? 0 : Math.round(linkDensity * 25);
const timelineCoverageScore = pageCount === 0 ? 0 : Math.round(timelineCoverageDensity * 15);
const noOrphansScore = pageCount === 0 ? 0 : Math.round(noOrphans * 15);
const noDeadLinksScore = pageCount === 0 ? 0 : Math.round(noDeadLinks * 10);
//
// v0.37.10.0: empty brains (pageCount === 0) get FULL marks (100/100),
// not 0. Semantically an empty brain has no coverage problem to penalize
// — there's nothing to embed, nothing to link, nothing to orphan. The
// pre-fix "empty = 0" caused fresh-init brains to score as critically
// unhealthy on `gbrain doctor`, which was a structural surprise to users
// who'd just successfully run init.
const embedCoverageScore = pageCount === 0 ? 35 : Math.round(embedCoverage * 35);
const linkDensityScore = pageCount === 0 ? 25 : Math.round(linkDensity * 25);
const timelineCoverageScore = pageCount === 0 ? 15 : Math.round(timelineCoverageDensity * 15);
const noOrphansScore = pageCount === 0 ? 15 : Math.round(noOrphans * 15);
const noDeadLinksScore = pageCount === 0 ? 10 : Math.round(noDeadLinks * 10);
const brainScore = embedCoverageScore + linkDensityScore + timelineCoverageScore + noOrphansScore + noDeadLinksScore;
return {
+12 -5
View File
@@ -3708,11 +3708,18 @@ export class PostgresEngine implements BrainEngine {
const noOrphans = pageCount > 0 ? 1 - (orphanPages / pageCount) : 1;
const noDeadLinks = pageCount > 0 ? 1 - Math.min(deadLinks / pageCount, 1) : 1;
// Per-component points. Sum equals brainScore by construction.
const embedCoverageScore = pageCount === 0 ? 0 : Math.round(embedCoverage * 35);
const linkDensityScore = pageCount === 0 ? 0 : Math.round(linkDensity * 25);
const timelineCoverageScore = pageCount === 0 ? 0 : Math.round(timelineCoverageWhole * 15);
const noOrphansScore = pageCount === 0 ? 0 : Math.round(noOrphans * 15);
const noDeadLinksScore = pageCount === 0 ? 0 : Math.round(noDeadLinks * 10);
//
// v0.37.10.0: empty brains (pageCount === 0) get FULL marks (100/100),
// not 0. Semantically an empty brain has no coverage problem to penalize
// — there's nothing to embed, nothing to link, nothing to orphan. The
// pre-fix "empty = 0" caused fresh-init brains to score as critically
// unhealthy on `gbrain doctor`, which was a structural surprise to users
// who'd just successfully run init. PGLite path has the same fix.
const embedCoverageScore = pageCount === 0 ? 35 : Math.round(embedCoverage * 35);
const linkDensityScore = pageCount === 0 ? 25 : Math.round(linkDensity * 25);
const timelineCoverageScore = pageCount === 0 ? 15 : Math.round(timelineCoverageWhole * 15);
const noOrphansScore = pageCount === 0 ? 15 : Math.round(noOrphans * 15);
const noDeadLinksScore = pageCount === 0 ? 10 : Math.round(noDeadLinks * 10);
const brainScore = embedCoverageScore + linkDensityScore + timelineCoverageScore + noOrphansScore + noDeadLinksScore;
return {
+12 -7
View File
@@ -31,14 +31,19 @@ beforeEach(async () => {
});
describe('Bug 11 — brain_score breakdown sums to total', () => {
test('empty brain returns zero score with all breakdown fields present', async () => {
test('empty brain returns full score (vacuous truth) with all breakdown fields present', async () => {
// v0.37.10.0: empty brain = no coverage problems = full marks. Pre-fix
// this returned 0/100, which surprised users running `gbrain doctor`
// immediately after `gbrain init --pglite`. Each component returns its
// max weight when pageCount === 0; the sum equals brain_score=100 by
// construction (same invariant as the non-empty path, see next test).
const h = await engine.getHealth();
expect(h.brain_score).toBe(0);
expect(h.embed_coverage_score).toBe(0);
expect(h.link_density_score).toBe(0);
expect(h.timeline_coverage_score).toBe(0);
expect(h.no_orphans_score).toBe(0);
expect(h.no_dead_links_score).toBe(0);
expect(h.brain_score).toBe(100);
expect(h.embed_coverage_score).toBe(35);
expect(h.link_density_score).toBe(25);
expect(h.timeline_coverage_score).toBe(15);
expect(h.no_orphans_score).toBe(15);
expect(h.no_dead_links_score).toBe(10);
// dead_links is now on the type.
expect(h.dead_links).toBe(0);
});
@@ -8,12 +8,24 @@
*/
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { mkdtempSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { doctorReportRemote, computeDoctorReport, type DoctorReport, type Check } from '../src/commands/doctor.ts';
let engine: PGLiteEngine;
let tmpHome: string;
let priorHome: string | undefined;
beforeAll(async () => {
// v0.37.10.0: doctorReportRemote reads from ~/.gbrain audit files
// (reranker_health, sync_failures, etc.). Without isolation, host state
// leaks into the test and makes the assertion non-deterministic. Pin
// GBRAIN_HOME to a tempdir so audit reads return empty.
tmpHome = mkdtempSync(join(tmpdir(), 'gbrain-doctor-remote-'));
priorHome = process.env.GBRAIN_HOME;
process.env.GBRAIN_HOME = tmpHome;
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
@@ -21,6 +33,9 @@ beforeAll(async () => {
afterAll(async () => {
await engine.disconnect();
if (priorHome === undefined) delete process.env.GBRAIN_HOME;
else process.env.GBRAIN_HOME = priorHome;
rmSync(tmpHome, { recursive: true, force: true });
});
describe('doctorReportRemote', () => {