mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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', () => {
|
||||
Reference in New Issue
Block a user