From 8b325041ee0089a50a55a9f730b73d46b3a8faff Mon Sep 17 00:00:00 2001 From: raymeboltd <77594828+raymeboltd@users.noreply.github.com> Date: Mon, 20 Jul 2026 19:46:46 +0200 Subject: [PATCH] v0.42.63.0 fix: preserve configured PGLite schema database path (#3016) * fix(schema): preserve configured PGLite database path * chore: bump version and changelog (v0.42.63.0) Co-Authored-By: OpenAI Codex --------- Co-authored-by: OpenAI Codex --- CHANGELOG.md | 23 +++++++ VERSION | 2 +- docs/architecture/KEY_FILES.md | 2 +- package.json | 2 +- src/commands/schema.ts | 10 +-- test/schema-cli-database-path.serial.test.ts | 67 ++++++++++++++++++++ test/schema-cli.test.ts | 8 ++- 7 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 test/schema-cli-database-path.serial.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cd26fce76..57b52cbc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ All notable changes to GBrain will be documented in this file. +## [0.42.63.0] - 2026-07-20 + +**Schema commands now open the local brain you actually configured.** + +If your PGLite brain lives at a custom path, commands such as `gbrain schema stats` previously ignored that path and could inspect the default brain instead. That made a healthy configured brain look empty or report the wrong schema counts. Schema commands now use the same complete database configuration as the rest of GBrain. PostgreSQL behavior is unchanged, and no migration is required. + +### How to use it + +Upgrade, then run the schema command normally: + +```bash +gbrain upgrade +gbrain schema stats --json +``` + +The reported page and type counts now come from the `database_path` in `~/.gbrain/config.json` when the engine is PGLite. + +### Itemized changes + +#### Fixed +- **Schema CLI commands preserve configured PGLite paths.** Engine construction and connection now receive the canonical complete engine configuration, including both `database_path` and `database_url` where applicable. +- **CLI tests are isolated from ambient database URLs.** Schema subprocess tests explicitly clear inherited PostgreSQL URL variables, and a persistent-PGLite regression test proves `schema stats` reads the configured database rather than the default brain. + ## [0.42.62.0] - 2026-07-17 **If your brain holds more than one source, everything now lands in the right one. Link extraction, timeline extraction, background cycles, and webhook captures used to quietly file some of their output under the default source; all of those paths now carry the correct source identity. Background agent jobs got tougher too: a failed database reconnect can no longer wedge the engine, and workers recover from dropped connections instead of crash-looping. If you run the admin dashboard behind a reverse proxy, the live activity panel finally connects. Long agent conversations cost less because repeated context is reused between turns on Anthropic calls. Local LiteLLM proxies work out of the box. Nested sources scan correctly again instead of reporting zero files. And the project's automated checks now include dependency vulnerability scanning, static code-security analysis, and signed provenance for release builds. Thirty merged changes in all, the largest batch to date, each one reviewed and verified against the live codebase before landing.** diff --git a/VERSION b/VERSION index 92762a88b..4c2cace61 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.42.62.0 +0.42.63.0 diff --git a/docs/architecture/KEY_FILES.md b/docs/architecture/KEY_FILES.md index a153c2b43..c81458c2a 100644 --- a/docs/architecture/KEY_FILES.md +++ b/docs/architecture/KEY_FILES.md @@ -483,7 +483,7 @@ Key files (v0.40.7.0 additions): - `src/core/schema-pack/mutate.ts` — 8-step `withMutation` skeleton (bundled-guard → lock → read → mutator → validate → atomic write → audit → invalidate). 11 mutation primitives: `addTypeToPack`, `removeTypeFromPack` (with reference check), `updateTypeOnPack`, `addAliasToType`, `removeAliasFromType`, `addPrefixToType`, `removePrefixFromType`, `addLinkTypeToPack`, `removeLinkTypeFromPack`, `setExtractableOnType`, `setExpertRoutingOnType`. Atomic write via `.tmp + fsync + rename` — the pack file on disk is NEVER partial. Inline minimal JSON→YAML emitter so YAML packs stay YAML (does NOT preserve comments — pin pack.json if you care about layout). - `src/core/schema-pack/stats.ts` — `runStatsCore(engine, opts)` returns per-source + aggregate page counts + coverage % + `dead_prefixes` (declared prefixes with zero matching pages — agent drilldown signal). Multi-source aware (`sourceIds[]` federated, `sourceId` single, or whole-brain). PGLite + Postgres parity via `executeRaw`. Empty brain → coverage:1.0 (vacuous truth). - `src/core/schema-pack/sync.ts` — `runSyncCore(engine, opts)` chunked UPDATE in 1000-row batches per declared prefix. Concurrent writers never block on a single row >100ms. Write-side scoping via `ctx.sourceId` directly (NOT `sourceScopeOpts`, which inherits OAuth read federation). Idempotent on `--apply` re-run. -- `src/commands/schema.ts` extension — 14 CLI verbs in the dispatch table: `add-type`, `remove-type`, `update-type`, `add-alias`, `remove-alias`, `add-prefix`, `remove-prefix`, `add-link-type`, `remove-link-type`, `set-extractable`, `set-expert-routing`, `stats`, `sync`, `reload`. `withConnectedEngine` defensive fix retained. Lifecycle-grouped help text (Inspection / Activation / Authoring / Discovery+repair). +- `src/commands/schema.ts` extension — 14 CLI verbs in the dispatch table: `add-type`, `remove-type`, `update-type`, `add-alias`, `remove-alias`, `add-prefix`, `remove-prefix`, `add-link-type`, `remove-link-type`, `set-extractable`, `set-expert-routing`, `stats`, `sync`, `reload`. `withConnectedEngine` routes `loadConfig()` through the canonical `toEngineConfig()` helper and passes the complete result (`database_url` and `database_path`) to factory construction and connect, so PGLite schema commands open the configured brain. Lifecycle-grouped help text (Inspection / Activation / Authoring / Discovery+repair). Pinned by `test/schema-cli-database-path.serial.test.ts`. - `src/core/operations.ts` extension — 9 MCP ops: `get_active_schema_pack`, `list_schema_packs`, `schema_stats`, `schema_lint`, `schema_graph`, `schema_explain_type`, `schema_review_orphans` (all read-scope, NOT localOnly), plus `schema_apply_mutations` (admin scope, NOT localOnly so remote agents can author packs over HTTPS MCP — batched, one MCP tool taking a `mutations[]` array atomically inside ONE `withPackLock`, audit log captures `actor: mcp:`) and `reload_schema_pack` (admin, NOT localOnly). Trust posture: per-call `schema_pack` opt STAYS rejected for remote callers via `op-trust-gate.ts`. - `src/commands/whoknows.ts` + `src/core/operations.ts:find_experts` — T1.5 wiring sites. Pack-aware via `expertTypesFromPack(pack.manifest)` from `best-effort.ts`. Pack-load failure → EMPTY filter (NOT hardcoded `['person', 'company']` defaults). A `researcher` type declared `--expert` now surfaces in `whoknows` results. - `skills/schema-author/SKILL.md` — Agent dispatcher for "evolve the schema pack." Triggers: 15+ phrasings incl. "add a page type", "my brain has untyped pages", "propose new types from my corpus", "backfill page types". Explicit Non-goals callout to `brain-taxonomist` (files one page) and `eiirp` (schema-check during iteration) so agents pick the right surface. 7-phase workflow: brain → assess → propose → apply → sync → verify → commit. Lists every gbrain schema CLI verb + every MCP op the skill uses. `brain_first: exempt` frontmatter. Required conformance sections: Contract, Anti-Patterns, Output Format. diff --git a/package.json b/package.json index a14d42cec..2f812de26 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "bun": ">=1.3.10" }, "license": "MIT", - "version": "0.42.62.0", + "version": "0.42.63.0", "overrides": { "@hono/node-server": "^1.19.13", "fast-uri": "^3.1.2", diff --git a/src/commands/schema.ts b/src/commands/schema.ts index 27c31875f..bdb0e3469 100644 --- a/src/commands/schema.ts +++ b/src/commands/schema.ts @@ -48,7 +48,7 @@ import { } from '../core/schema-pack/index.ts'; import type { SchemaPackManifest, PackPrimitive } from '../core/schema-pack/manifest-v1.ts'; import { PACK_PRIMITIVES } from '../core/schema-pack/manifest-v1.ts'; -import { gbrainPath, loadConfig, configPath } from '../core/config.ts'; +import { gbrainPath, loadConfig, configPath, toEngineConfig } from '../core/config.ts'; export async function runSchema(args: string[]): Promise { const sub = args[0]; @@ -434,16 +434,12 @@ function parseFlags(args: string[]): ParsedFlags { async function withConnectedEngine(fn: (engine: import('../core/engine.ts').BrainEngine) => Promise): Promise { const { createEngine } = await import('../core/engine-factory.ts'); - const cfg = loadConfig() ?? {}; - const engineKind = (cfg as { engine?: string }).engine === 'postgres' ? 'postgres' : 'pglite'; + const cfg = loadConfig() ?? { engine: 'pglite' as const }; // PR #1321 (closed) defensive fix retained: build the EngineConfig once and // pass it to BOTH createEngine and engine.connect. The factory captures // config at construction; explicit re-pass at connect() is defense in depth // against future engine implementations that read URL from connect-time. - const connectConfig: import('../core/types.ts').EngineConfig = { - engine: engineKind, - database_url: (cfg as { database_url?: string }).database_url, - }; + const connectConfig = toEngineConfig(cfg); const engine = await createEngine(connectConfig); await engine.connect(connectConfig); try { diff --git a/test/schema-cli-database-path.serial.test.ts b/test/schema-cli-database-path.serial.test.ts new file mode 100644 index 000000000..bd54fe197 --- /dev/null +++ b/test/schema-cli-database-path.serial.test.ts @@ -0,0 +1,67 @@ +/** + * Regression for schema CLI engine routing. + * + * Serial because it opens a persistent PGLite database and then hands that + * database to a CLI subprocess. The subprocess must read the configured path, + * not silently fall back to the default brain. + */ +import { describe, expect, test } from 'bun:test'; +import { spawnSync } from 'node:child_process'; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { PGLiteEngine } from '../src/core/pglite-engine.ts'; + +const REPO_ROOT = join(import.meta.dir, '..'); + +describe('gbrain schema configured PGLite routing', () => { + test('schema stats reads database_path from config', async () => { + const home = mkdtempSync(join(tmpdir(), 'gbrain-schema-db-path-')); + const gbrainDir = join(home, '.gbrain'); + const dbPath = join(gbrainDir, 'configured-brain.pglite'); + mkdirSync(gbrainDir, { recursive: true }); + + const engine = new PGLiteEngine(); + try { + await engine.connect({ engine: 'pglite', database_path: dbPath }); + await engine.initSchema(); + await engine.putPage('people/alice-example', { + type: 'person', + title: 'Alice Example', + compiled_truth: 'Example page', + }); + } finally { + await engine.disconnect(); + } + + writeFileSync( + join(gbrainDir, 'config.json'), + JSON.stringify({ engine: 'pglite', database_path: dbPath, schema_pack: 'gbrain-base' }), + 'utf-8', + ); + + try { + const result = spawnSync( + 'bun', + ['run', 'src/cli.ts', 'schema', 'stats', '--json'], + { + cwd: REPO_ROOT, + encoding: 'utf-8', + env: { + ...process.env, + GBRAIN_DATABASE_URL: '', + DATABASE_URL: '', + GBRAIN_HOME: home, + }, + timeout: 60_000, + }, + ); + expect(result.status).toBe(0); + const stats = JSON.parse(result.stdout ?? ''); + expect(stats.aggregate.total_pages).toBe(1); + expect(stats.aggregate.by_type).toContainEqual({ type: 'person', count: 1 }); + } finally { + rmSync(home, { recursive: true, force: true }); + } + }, 90_000); +}); diff --git a/test/schema-cli.test.ts b/test/schema-cli.test.ts index 9ec500030..0019a1ffa 100644 --- a/test/schema-cli.test.ts +++ b/test/schema-cli.test.ts @@ -36,7 +36,13 @@ function gbrain( // bun's spawnSync does NOT inherit env mutations done via process.env = ..., // so pass env explicitly. CLAUDE.md flags this pattern as load-bearing for // any subprocess test that needs GBRAIN_HOME isolation. - const env = { ...process.env, GBRAIN_HOME: DEFAULT_GBRAIN_HOME, ...extraEnv }; + const env = { + ...process.env, + GBRAIN_DATABASE_URL: '', + DATABASE_URL: '', + GBRAIN_HOME: DEFAULT_GBRAIN_HOME, + ...extraEnv, + }; const result = spawnSync('bun', ['run', 'src/cli.ts', ...args], { cwd: REPO_ROOT, encoding: 'utf-8',