mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
814258dda67945ffec9457a1e73980e947b7e462
11
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4ee530f3c5 |
v0.42.42.0 fix(cli): bounded teardown + explicit exit — kill the 10s force-exit tax on txn-mode poolers (#2084) (#2141)
* feat(core): finishCliTeardown + flushThenExit — bounded teardown, owned exit verdict (#2084) cli-force-exit.ts becomes the single owner of one-shot CLI exit + teardown: - finishCliTeardown: bounded sink drain -> bounded disconnect under a backstop whose deadline is COMPUTED from the bounds it guards (floor 10s; GBRAIN_TEARDOWN_DEADLINE_MS env override). Arms at teardown start, never before the op handler. - flushThenExit: stdio write-fence (unref'd guard, EPIPE-safe) + REF'D aliveness grace for non-TTY stdio — Bun only delivers queued pipe writes while the process is alive (no flush API reaches the native queue). - setCliExitVerdict/currentExitCode: the exit verdict lives in a gbrain-owned channel, never read back from process.exitCode (PGLite's Emscripten runtime scribbles its own status there mid-run). - background-work.ts exports backgroundWorkSinkCount() for the deadline formula. Unit tests + a spawned-Bun harness proving byte-complete piped output. * fix(cli): route all nine disconnect sites through finishCliTeardown; one exit seam (#2084) Deletes the pre-handler 10s force-exit timer (it measured handler + teardown combined: PgBouncer txn-mode deployments paid a flat 10s banner tax on every query, and any >10s op was killed mid-run with exit 0 and truncated output). Sweeps op-dispatch, CLI_ONLY fall-through, search dashboard, read-only timeout path, dream, doctor x3 (fixing a pre-existing pool leak when DB checks throw), and ze-switch. The ONE process exit lives in main().then/catch via flushThenExit(currentExitCode()), gated by shouldForceExitAfterMain(). Exit-code writers (op-dispatch catch, reindex, transcripts, brainstorm, autopilot, frontmatter) now set the verdict through setCliExitVerdict. * fix(pglite): contain Emscripten's process.exitCode writes at PGlite.create (#2084) PGLite's WASM runtime writes its own status into process.exitCode (99 at create; in-memory brains run initdb whose status lands on a later tick; the exit status at close) — on PGLite every error exit was silently clobbered. preservingProcessExitCode wraps create() to keep the global tidy; db.close() stays unwrapped (its 0-write is baseline behavior test runners depend on). The CLI verdict itself is immune: it lives in the owned channel. * test: e2e + structural pins for the #2084 teardown contract E2E: failed op exits 1; every swept command spawned (brain-copy isolation for mutators, no-network); slow-handler regression via the deadline env knob; piped --json parses complete; teardown banner absent on every happy path; daemon survival untouched. Structural: no bare awaited engine disconnects in cli.ts; DISCONNECT_HARD_DEADLINE_MS gone; >=9 helper call sites; verdict channel + create-wrap pins. * test: fix R1 env-isolation violations in retrieval-reflex tests Pre-existing on master: both files mutated GBRAIN_RETRIEVAL_REFLEX directly, failing scripts/check-test-isolation.sh (bun run verify). Converted to the canonical withEnv() pattern; the reflex describe's beforeEach also never restored the flag, leaking it across the shard. * docs: KEY_FILES entries for the teardown contract; close + file TODOS (#2084) KEY_FILES.md: current-state entry for cli-force-exit.ts (helper + central exit seam pair, verdict channel, cli.ts-scoped claim); background-work.ts and pglite-engine.ts entries updated. TODOS.md: the drain-before-owner-disconnect P3 (filed from #1972) is done by this wave; files the trigger-gated GBRAIN_COMMAND_DEADLINE_MS follow-up (eng-review D2/D14). * fix: pre-landing review fixes (#2084) Review army (testing/maintainability/security/performance, 0 critical): - drain defense-in-depth: a throwing drain warns and still disconnects (cannot escape a caller's finally or skip the engine teardown) - behavioral tests for preservingProcessExitCode (connect pins 0; create-throw restores the pre-call verdict) - D9 widening test (live-registry sink count feeds the deadline formula), env 0/negative boundary cases, verdict mirror-write assertion - stale comments: header diagram backstop line, structural-test 'both lifecycle calls' contradiction, KEY_FILES 10s-force-exit clauses, e2e D11 falsification story corrected - named the formula's pool-end literals * fix: adversarial-review hardening — daemon-safe command resolution, flush knob, ref'd backstop (#2084) Cross-model adversarial review (Claude subagent + Codex, both P1'd it): - shouldForceExitAfterMain now resolves the command through parseGlobalFlags — the old first-non-dash heuristic read `gbrain --timeout 30s serve` as command "30s" and the new exit seam would have killed the daemon ~250ms after boot with exit 0 (unit-pinned) - GBRAIN_FLUSH_GRACE_MS env override for the non-TTY aliveness grace (batch consumers piping large payloads to slow readers can raise it; agent loops can lower it) - backstop timer is now REF'D: a hung teardown on an otherwise-empty event loop previously exited naturally — skipping the flush and surfacing PGLite's scribbled process.exitCode - flushThenExit: real process.exit latched once per process - doctor-site comment corrected; in-command process.exit teardown-bypass class (pre-existing) filed as a P2 TODO * chore: bump version and changelog (v0.42.42.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: move #2084 exitCode-containment lifecycle tests to the serial quarantine (R3) * docs: update project documentation for v0.42.42.0 - docs/TESTING.md: replace the stale 4-file serial-quarantine enumeration with a current-state description (the quarantine is glob-discovered, now several dozen files incl. the #2084 exitCode-containment suite); add unit inventory entries for test/cli-finish-teardown.test.ts and test/flush-then-exit-harness.test.ts. - docs/architecture/KEY_FILES.md: rephrase the pglite-engine exitCode containment note to current-state wording (clears the check-key-files-current-state prose-history warning). llms bundles regenerated (byte-identical: both docs are link-only). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: apply cross-model doc-review findings for v0.42.42.0 Codex review of docs-vs-shipped-code found 9 gaps; all verified against the code before fixing: - CHANGELOG.md (0.42.42.0 entry, precision narrowing only — no entries touched): "every CLI exit path" -> "every cli.ts disconnect site"; "on every path" -> "on every routed exit path"; dream/doctor/ze-switch claim scoped to dispatcher teardown (command-internal process.exit sites are tracked in TODOS as the open P2). - docs/architecture/KEY_FILES.md: the teardown backstop is REF'D, not unref'd (matches the F3 adversarial-review decision in the code). - src/core/cli-force-exit.ts: header diagram comment had the same stale unref'd claim + `process.exitCode ?? 0`; now matches the implementation (ref'd timer, `currentExitCode()`). Comment-only change. - docs/TESTING.md: verify is the 30-check parallel battery via run-verify-parallel.sh (was described as 4 checks); CI is 10 weighted LPT shards + dedicated verify/serial/slow jobs (was "4-way FNV on shard 1"); test:serial runs one bun process per file (not --max-concurrency=1); dead "cap: 10" line rewritten as debt guidance; inventory entries added for test/cli-should-force-exit.test.ts and test/e2e/pglite-cli-exit.serial.test.ts. bun run verify green (30/30); #2084 test files green; llms bundles regenerated (byte-identical — reference docs are link-only). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: route v0.42.41.0's raw exitCode writers through the verdict channel; reconcile merged structural pins (#2084) CI fallout from merging the v0.42.41.0 triage wave into the #2084 exit-seam design — both waves fixed the same timer-placement bug independently: - doctor.ts + extract.ts set failure exit codes via raw `process.exitCode =` writes (v0.42.41.0's process.exit -> exitCode conversion); the #2084 exit seam reads only the gbrain-owned verdict channel, so doctor FAILs exited 0 (Tier 1 RLS e2e + half-migrated-Minions tests). Converted to setCliExitVerdict, same as the wallclock-124 site in the merge commit. - cli-force-exit-teardown-arming.test.ts pinned v0.42.41.0's inline finally-armed timer, which the merge replaced with finishCliTeardown; rewritten to pin the merged invariant (no pre-try arming in cli.ts; the backstop arms inside the helper before the drain). - eval-capture drain timing bound 1s -> 2s: flaked at 1023ms under CI shard load after the new test files shifted LPT shard packing (13x budget slack still proves bounded-not-hung). --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
48e1000306 |
v0.41.23.0 feat: extract operator surfaces + pack-driven extractables (#1541)
* Wave A: schema + receipts foundation for v0.42 extract operator surfaces
Foundation layer for the pack-driven extractables + receipt-as-brain-memory
+ operator-discoverability cathedral. Five atomic pieces ship together
because their schema + helpers + module dependencies are tight-coupled:
A1. Widen pack manifest's `extractable` from `boolean` to
`boolean | ExtractableSpec`. ExtractableSpec carries prompt_template,
fixture_corpus, eval_dimensions, benchmark_min_recall, and reserves
verifier_path for v0.43+ pack-shipped verifier code (REFUSE at
runtime in v0.42 per plan D-EXTRACT-37). Back-compat: every pre-v0.42
pack with `extractable: true` continues parsing unchanged. Three new
helpers: extractableSpecsFromPack(), getExtractableSpec(),
refuseVerifierPathInV042().
A2. New page type `extract_receipt` in ALL_PAGE_TYPES. Source-boost map
adds `extracts/` prefix at factor 0.3 — receipts surface in search
when extraction-relevant but never dominate user content (D-EXTRACT-42).
A3. New module src/core/extract/receipt-writer.ts (~190 LOC) exporting
writeReceipt(engine, input). Canonical slug shape
extracts/{date}/{kind}/{source_id}/{run_id_short}/round-{N} per
D-EXTRACT-17. Frontmatter belt+suspenders per D-EXTRACT-19: BOTH
type:extract_receipt AND dream_generated:true stamped on every
receipt, regardless of caller, so the eligibility predicate's
anti-loop guards reject the receipt page from any future extraction
sweep (single-flag bypass requires breaking two unrelated checks).
Idempotent on resume — same run_id+round overwrites cleanly.
A4. Migration v104 creates extract_rollup_7d table (per-day rollup of
extract events keyed on kind+source_id+day). Audit JSONL stays the
SOURCE OF TRUTH per F-OUT-19; this table is a best-effort cache for
doctor's <100ms read budget. Per-day rows mean the 7-day window
auto-evicts on every read. v100 was deliberately skipped on master
(renumbered out during a prior wave); v101/v102/v103 also taken;
v104 is the next clean slot.
A5. Doctor `extract_health` check reads extract_rollup_7d for last 7
days and emits per-kind aggregates: cost_7d_usd, eval_pass_count,
eval_fail_count, halt_count, round_completed_count, halt_rate.
3-state: OK when rollup empty (pre-v0.42 brain or fresh init), WARN
when any per-kind halt rate > 10% (top-3 named in message), WARN
when rollup_write_failures > 0 (audit JSONL is SoT but operator
deserves to know the DB cache is degraded). Pre-v104 brains stay
quiet — the missing-table error path is caught and treated as
OK so doctor doesn't warn during the upgrade window.
Tests added:
- test/extractable-spec-widening.test.ts (22 cases) — back-compat with
boolean shape, new struct parsing, verifier_path REFUSE contract.
- test/extract/receipt-writer.test.ts (12 cases) — slug shape, frontmatter
belt+suspenders, idempotent resume, body human-readability.
- test/doctor-extract-health.test.ts (8 cases) — empty rollup OK, halt
rate WARN, rollup_write_failures WARN, 7-day window inclusion at
boundary, multi-kind top-3 message ordering.
Plus the canonical bootstrap-coverage test passes with the new v104
migration cleanly applied through both engines.
Plan: ~/.claude/plans/system-instruction-you-are-working-stateless-dragonfly.md
Wave A scope. Wave B (hook receipts into existing extractors) follows.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Wave B: hook receipts + rollup row into the 5 shipped extractors
Each LLM-backed extractor surface now records its run in two places when
something actually happened:
1. An extract receipt PAGE at extracts/{date}/{kind}/{source_id}/{run_id_short}/round-{N}
(queryable via gbrain search, citable, surfaces in cross-modal
contradiction probes per the Wave A foundation). Only written when
`total_rows > 0` so no-op runs don't bloat the brain.
2. An UPSERT row in extract_rollup_7d (DB-backed best-effort cache
per F-OUT-19) so the doctor extract_health check from Wave A reads
per-kind aggregates without scanning JSONL.
New module src/core/extract/rollup-writer.ts (~120 LOC) exports
upsertExtractRollup() with PostgreSQL ON CONFLICT DO UPDATE on the
(kind, source_id, day) PK. Concurrency-safe per F-OUT-14 design.
Failure path is best-effort — bumps rollup_write_failures in the
table itself, stderr-warns once per (kind, day, error-class), and
NEVER fails the parent extraction operation. JSONL remains source
of truth.
Wired into 5 extractors:
- extract-conversation-facts (kind: facts.conversation) — both
success path AND BudgetExhausted halt path write receipt+rollup
so partial runs are still observable.
- extract_atoms cycle phase (kind: atoms)
- synthesize_concepts cycle phase (kind: concepts, source_id: default
because concepts are brain-global)
- propose_takes cycle phase (kind: takes.proposed) — scope-aware
source_id from the read scope.
- extract_facts cycle phase (kind: facts.fence) — deterministic
(no LLM cost) but still records reconcile activity so doctor sees
the cycle is alive.
Receipt frontmatter belt+suspenders (D-EXTRACT-19) reused from
Wave A: every receipt stamps BOTH `type: extract_receipt` AND
`dream_generated: true` so the eligibility predicate's anti-loop
guards reject the receipt page from any future extraction sweep.
Test surgery in test/propose-takes.test.ts — one existing assertion
tightened from "no INSERTs" to "no INSERT INTO take_proposals" so
the new rollup UPSERT doesn't falsely fail the cache-hit case test.
Run regression: 85/85 tests pass across extract-conversation-facts,
extract-atoms-synthesize-concepts, extract-facts-phase, propose-takes.
Plan: ~/.claude/plans/system-instruction-you-are-working-stateless-dragonfly.md
Wave B scope. Wave C (pack-author scaffolding + benchmark) follows.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Wave C+D: pack-author scaffolding + operator surfaces for v0.42 extract
Wave C: pack-author authoring loop
- scaffold-extractable mutation primitive declares a kind as extractable
on a pack manifest in one verb (wires through updateTypeOnPack from
the v0.41 mutate library); generates 5 placeholder fixtures + a
pack-supplied prompt template stub
- schema CLI wires gbrain schema scaffold-extractable <type> --pack <pack>
- extract benchmark CLI loads a pack's fixture corpus through strict
D-EXTRACT-21 path validation (rejects absolute paths, .. traversal,
null bytes, symlinks resolving outside pack root); v0.42 ships as a
stub reporter (LLM dispatch deferred to Wave E)
Wave D: operator surfaces
- extract status CLI reads extract_rollup_7d for the last 7 days,
sorts by (halt_rate desc, cost desc); kubectl-style right-aligned
table, top-5 + "more rows" hint by default, --verbose shows all;
stable schema_version: 1 JSON envelope for monitoring pipelines
- extract --explain <kind> CLI prints the active pack's resolution
chain: declaration source (pack-declared vs built-in cycle phase),
prompt_template + fixture_corpus paths with existence checks,
eval_dimensions, benchmark_min_recall, and the last 7d rollup
- extract.ts gains a lifecycle-grouped help text (Extraction /
Inspection / Status) per the original D3 plan goal
Tests:
- test/schema-pack/scaffold-extractable.test.ts (15 cases) including
explicit privacy-rule assertions guarding against real-name leakage
- test/extract/benchmark.test.ts (17 cases) covering path validation
rejections + JSONL fixture parsing
- test/extract/status.test.ts (15 cases) over pure aggregation +
formatting
Housekeeping:
- test/extract/receipt-writer.test.ts refactored to the canonical
PGLite block (beforeAll/afterAll/resetPgliteState in beforeEach)
per CLAUDE.md test-isolation R3+R4; runtime drops from ~30s of
99-migration replay per test to <6s for all 12 cases together
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* v0.42.0.0: extract operator surfaces + pack-driven extractables
Bump VERSION + package.json to 0.42.0.0. CHANGELOG entry covers the
three-wave shipped scope (receipts + rollup + doctor check; receipts
hooked into all 5 shipped extractors; pack-author scaffolding +
benchmark stub-reporter; status + --explain dashboards + lifecycle
help). CLAUDE.md Key Files gains a v0.42 cluster annotation. llms.txt
regenerated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* v0.41.23.0: re-tag from 0.42.0.0 (patch-channel slot, no scope change)
VERSION + package.json + CHANGELOG header + CLAUDE.md cluster annotation
all moved from 0.42.0.0 to 0.41.23.0. Body text updated in-place: every
"v0.42" / "v0.43+" reference inside this entry's release notes now reads
"v0.41.23" or "follow-up release" as appropriate.
Same scope shipping — the three-wave extract operator surface stays
intact. Just lands in the patch-channel queue (.20/.21/.23 free; .22 is
PR #1542's type-unification cathedral) instead of the minor-channel bump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: add extract_receipt to gbrain-base.yaml page_types (CI parity gate)
CI shard 5 caught the drift: test/regressions/gbrain-base-equivalence.test.ts
asserts every ALL_PAGE_TYPES seed has a matching page_type entry in the
gbrain-base.yaml pack. Wave A added `extract_receipt` to ALL_PAGE_TYPES
but didn't seed it in the base pack manifest.
Adds the entry under the `annotation` primitive with `extracts/` path
prefix (matches the source-boost demote site) and `extractable: false`
(receipts are written by the framework, never extracted from). Comment
documents the belt+suspenders D-EXTRACT-19 invariant so future readers
understand why receipts carry both `dream_generated: true` AND
`type: extract_receipt`.
Closes the CI gate without changing runtime behavior — the pack-aware
read paths already had the prefix demote wired in src/core/search/source-boost.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: bump gbrain-base page-type count 24→25 in schema-cli test
CI shard 4 caught the second drift from the same root cause as the
prior parity-gate fix: v0.41.23's `extract_receipt` addition bumped
gbrain-base.yaml from 24 to 25 page types. The schema-cli smoke test
was pinned at 24 (the count after v0.41.11.0 added `conversation` +
`atom`); update to 25 and note v0.41.23's contribution alongside the
prior version stamp.
Verified hermetic: running test/schema-cli.test.ts with a clean
GBRAIN_HOME tempdir produces 12/12 pass (the local-machine 'schema
active' fail is from a real ~/.gbrain pinning gbrain-base-v2; not a
shipped-code issue, doesn't repro on CI).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): pack-locator stub leak between shard 6 test files
CI shard 6 caught three flaky failures in test/onboard-pack-upgrade-checks.test.ts:
- checkPackUpgradeAvailable > fires on gbrain-base brain with gbrain-base-v2
- checkPackUpgradeAvailable > manual_only routing via render.ts allowlist (D17)
- checkTypeProliferation > warns when distinct types exceed declared+5
Root cause: test/schema-pack-sync.test.ts calls
`__setPackLocatorForTests(...)` to stub the disk-loader, but doesn't
restore in afterAll. Bun's CI shard 6 loads multiple test files into
one process; when sync.test.ts runs before onboard-pack-upgrade-checks.test.ts,
the stubbed locator persists at module scope. `loadActivePack` for
gbrain-base / gbrain-base-v2 then returns null and:
- findPackSuccessors returns [] → status='ok' instead of 'warn' (F1+F2)
- declared falls back to 15 → fail threshold becomes 30, 32 > 30 → 'fail'
instead of 'warn' (F3)
Local single-file runs pass because the locator starts at its default.
Two-layer fix:
1. test/schema-pack-sync.test.ts afterAll calls
`_resetPackLocatorForTests()` to undo the mutation (the canonical
fix at the source).
2. test/onboard-pack-upgrade-checks.test.ts beforeEach calls the same
reset (defense-in-depth against any future test file in the shard
that forgets to restore).
Reproduced locally: running the three shard-6 schema-pack files together
fails 3 tests pre-fix and passes 30/30 post-fix. Full shard 6 sweep
(77 files, 1232 tests) now green; bun run verify still 28/28.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): pglite-engine — dim-agnostic chunk-embedding test data
CI shard 6 caught two flaky failures in test/pglite-engine.test.ts:
- PGLiteEngine: Chunks > getChunksWithEmbeddings returns embedding data
- PGLiteEngine: stale chunk pagination > countStaleChunks counts chunks
with NULL embedding only
Both failed with `expected 1280 dimensions, not 1536` at the upsert site.
Root cause: pglite-engine.ts:287 initSchema() reads embedding dim from
gw.getEmbeddingDimensions() if the gateway is configured (potentially
left in that state by another shard-6 test file in the same bun process),
falling back to DEFAULT_EMBEDDING_DIMENSIONS otherwise — which is 1280
since v0.36+ when the ZE default landed (zeroentropyai:zembed-1).
Pre-v0.36 defaults were OpenAI's 1536; my test data was pinned to that
stale literal.
The two outcomes that pass:
- gateway happens to be configured for 1536-dim (e.g. master shard 6
run 26515999465 — these tests passed at 20ms + 24ms with no
"dimensions" error)
- gateway happens to be configured for 1280-dim AND test data is 1280
The outcome that fails:
- gateway configured for 1280-dim AND test data hardcoded to 1536
Fix: capture the actual column width after initSchema (probe
pg_attribute.atttypmod for content_chunks.embedding) and use that
captured `CHUNK_EMBED_DIM` constant at the three Float32Array sites.
Test data now matches whatever width the column was created at,
regardless of which shard-6 file ran first.
Local repro: full shard 6 (77 files, 1232 tests, ~6min) green; this
file standalone (100 tests) green; bun run verify 28/28.
Broader pattern: 9 other test files use the same Float32Array(1536)
literal. None land in shard 6 today (so they don't flake), but the
fix shape here can be lifted into a shared helper if the bug class
surfaces elsewhere — filed as a v0.42+ follow-up rather than a
preemptive sweep, since each file's setup shape is slightly different.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
24881f60fc |
v0.34.4.0 fix(embed): cursor-paginated --stale hardening wave (D2/D3/D4/D6/D7/D8 + regression test) (#991)
* perf(embed): cursor-paginated stale loading + rate-limit backoff + partial index Three fixes for embed --stale on large brains (300K+ chunks): ## 1. Cursor-paginated listStaleChunks (embed timeout fix) The previous implementation pulled ALL stale rows (up to 100K) in one query. On a 373K-row content_chunks table with 48K stale rows, this query took >2 min and hit Supabase's 2-min statement_timeout, causing embed --stale to silently fail with zero progress. Fix: keyset pagination on (page_id, chunk_index) with a default batch size of 2000 rows. Each query finishes in <1s. The embedAllStale loop pages through batches, embeds each batch, then advances the cursor. ## 2. Rate-limit-aware retry (429 backoff) The OpenAI SDK's built-in retry has a ~4s max backoff window, which is too short for TPM (tokens-per-minute) limits on large pages (~90K tokens). The embed loop would fail after 3 SDK retries and skip the page entirely. Fix: embedBatchWithBackoff wrapper parses the retry delay from the 429 error message (e.g. 'try again in 248ms') and sleeps for that duration + 500ms padding. Up to 5 retries with parsed delays (60s fallback when unparseable). ## 3. Migration v58: partial index for NULL embeddings `CREATE INDEX idx_chunks_embedding_null ON content_chunks (page_id, chunk_index) WHERE embedding IS NULL` — makes countStaleChunks() and the paginated listStaleChunks() instant instead of full-table-scanning 373K rows. ## Testing Verified on a 99K-page / 373K-chunk brain with 48K stale chunks. Before: embed --stale hung for 2+ min then timed out (0 progress). After: loads 2K rows in <1s, embeds concurrently, pages through all stale chunks without timeout. * fix(embed): wave of hardening + tests on cursor-paginated --stale path Lands the 9 decisions + regression test set from /plan-eng-review on PR #991's embed-perf cherry-pick. Implements the codex outside-voice findings folded in during plan review. Architecture / correctness: - D2 jitter on the parsed retry-after delay (±30%) so 20 concurrent workers don't relock on the next 429 wave (thundering herd fix). - D3 + D3a + D8 wall-clock budget (GBRAIN_EMBED_TIME_BUDGET_MS, default 30 min) threaded as an AbortSignal into THREE places: the retry sleep (abortableSleep), the per-key worker claim loop, and the gateway embed call itself (so a worker mid-fetch on a ~30s OpenAI HTTP timeout cancels within seconds instead of waiting it out). - D4 structured 429 detection that unwraps the gateway's AITransientError wrap via cause chain (depth-limited to 5). Naive `e.status === 429` was silently false against normalized errors; message-match stays as fallback. detect429FromCause exported as @internal helper. - D4a `maxRetries: 0` passthrough through embedBatch → gateway → embedMany so the AI SDK's default 2-retry stack doesn't multiply this wrapper's 5 attempts (was up to 15 total cycles per call). - D6 migration v59 (embed_stale_partial_index) rewritten to use CREATE INDEX CONCURRENTLY + handler-based engine-branching (mirrors v14 invalid-remnant pattern). Plain CREATE INDEX would have taken ShareLock on the 373K-row content_chunks table for the duration of the build. - D7 sourceId threaded through countStaleChunks + listStaleChunks + embedAllStale. `gbrain embed --stale --source X` was silently dropping the flag pre-fix and counting/embedding across every source. Both Postgres and PGLite engines updated. Tests added: - D5 8 unit cases for embedBatchWithBackoff in test/embed.serial.test.ts: ms / s retry-after parse, fallback, non-rate-limit rethrow, jitter variance, budget abort during sleep+fetch, normalized-error cause unwrap, maxRetries:0 passthrough verification. - D5a fixed every pre-existing stale-row mock to include source_id + page_id (required on StaleChunkRow as of v0.33.3 cursor pagination — TypeScript's structural typing was hiding these). - D7 unit cases asserting CLI `--source X` parses + threads sourceId. - Gap scan: end-to-end wall-clock budget firing in the outer pagination loop via runEmbedCore. - D6 migration v59 test cases in test/migrate.test.ts: source-shape assertion (CONCURRENTLY + invalid-remnant DROP-before-CREATE ordering), PGLite handler-branch idempotency, partial-index materialization. - REGRESSION: new test/e2e/embed-stale-pagination.test.ts covering static (every chunk visited exactly once), failed-page (cursor advances past failures, next run picks up), page-split-across-batches, source-scoped scan, duplicate-slug-across-sources. - PGLite parity cases for cursor pagination, page split, source filter in test/pglite-engine.test.ts (pins tuple-compare against WASM build). Gate: - bun run test: 6305 pass / 0 fail / 0 skip across all 8 shards + serial. - DATABASE_URL=... bun run test:e2e: 90 files, 603 tests, 0 failures. Plan: ~/.claude/plans/system-instruction-you-are-working-iterative-torvalds.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.34.3.0) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: garrytan-agents <garrytan-agents@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c9652443cf |
v0.32.7 feat: CJK fix wave — 6 layers from one root cause (closes vinsew + 313094319-sudo PRs) (#898)
* feat: shared CJK detection module (cjk.ts) Foundation for the CJK fix wave. Single source of truth for CJK ranges (Han, Hiragana, Katakana, Hangul Syllables), the slug-char string used by adjacent validators, sentence + clause delimiter sets, the 30% density threshold for word counting, and a LIKE-pattern escape helper. Replaces the inline hasCJK regex at expansion.ts:58 so four-place drift becomes impossible. countCJKAwareWords uses density threshold (per codex outside-voice C13) so a long English doc with one Japanese term stays whitespace-tokenized, not char-split. Co-Authored-By: vinsew <vinsew@users.noreply.github.com> * feat: migration v51 + pages.chunker_version/source_path columns Schema-level support for the v0.32.7 CJK wave. Two new columns on pages: - chunker_version SMALLINT NOT NULL DEFAULT 1 — bumped to MARKDOWN_CHUNKER_VERSION (2) on every new import. The post-upgrade gbrain reindex --markdown sweep walks chunker_version < 2 to find pre-bump rows and rebuilds them. - source_path TEXT — captures the repo-relative path at import time so sync's delete/rename code can resolve frontmatter-fallback slugs (CJK / emoji / exotic-script files where the path itself doesn't derive a slug). Both columns plumbed through PageInput, partial indexes scoped to markdown-only / non-null. PGLite + Postgres parity via the standard ALTER TABLE ... IF NOT EXISTS shape. Replaces the original PR #599 plan of folding MARKDOWN_CHUNKER_VERSION into content_hash. Codex outside-voice C2 caught that as a no-op: performSync gates on actual file change, not hash-would-differ, so the fold never reached existing pages. Column + sweep is the real fix. Co-Authored-By: vinsew <vinsew@users.noreply.github.com> * feat: CJK-aware slugify + SLUG_SEGMENT_PATTERN + adjacent validators slugifySegment now preserves Han / Hiragana / Katakana / Hangul Syllables with NFC re-normalization after the NFD-strip-accents pass so Hangul Jamo recomposes back into precomposed syllables that fall inside the whitelist. café still slugifies to cafe (regression preserved — iron rule). SLUG_SEGMENT_PATTERN (consumed by takes-holder validation) extended with CJK_SLUG_CHARS in the same commit so CJK slugs aren't rejected by adjacent validators downstream. Codex outside-voice C4 caught this exact half-fix in the original plan — leaving the pattern ASCII-only would have shipped a feature where the slugify produced 品牌圣经 but adjacent validators flagged it. src/core/operations.ts: validatePageSlug + validateFilename also extended with CJK ranges. matchesSlugAllowList is unchanged (works on string prefixes, no character class). Co-Authored-By: vinsew <vinsew@users.noreply.github.com> * feat: recursive chunker — MARKDOWN_CHUNKER_VERSION + CJK splitting + maxChars cap Four coordinated chunker changes for the v0.32.7 wave: - MARKDOWN_CHUNKER_VERSION = 2 exported. Folded into pages.chunker_version so the post-upgrade reindex sweep can find pre-bump pages. - countWords delegated to countCJKAwareWords from cjk.ts (30% density threshold). Below threshold: whitespace-token count (English-dominant docs stay tokenized). At/above: char count (Chinese paragraphs actually split instead of being treated as one 8192-token-overflowing word). - DELIMITERS extends L2 (sentences) with 。!? and L3 (clauses) with ;:,、. CJK punctuation now produces real chunk boundaries. - maxChars hard cap (default 6000) with sliding-window splitByChars and 500-char overlap. Catches pathological whitespace-less inputs that the word-level pipeline can't bound (pure-Han paragraphs, base64 blobs, long URLs). Applied to both single-short-chunk and merged-chunks paths. - splitOnWhitespace falls through to char-slice when ANY single "word" exceeds target chars (the greedy /\S+/g regex returns a whole CJK paragraph as one "word"; without this, the L4 fallback produces one huge piece). Pre-fix this was the silent-failure path. Tests in test/chunkers/recursive.test.ts: 9 new cases — pure Chinese, Japanese + 。, Korean Hangul, mixed CJK+English, 20KB CJK with overlap, single-short-chunk maxChars edge, pure-English regression. Co-Authored-By: vinsew <vinsew@users.noreply.github.com> * feat: PGLite CJK keyword fallback + engine chunker_version/source_path passthrough PGLite uses websearch_to_tsquery('english') over to_tsvector('english'), which can't tokenize CJK. Pre-fix, CJK queries returned empty results on PGLite brains even with proper embeddings. searchKeyword + searchKeywordChunks now branch on hasCJK(query): - ASCII path: unchanged. websearch_to_tsquery('english') continues to drive FTS. No regression risk. - CJK path: switches to ILIKE '%' || $qLike || '%' ESCAPE '\\' over chunk_text with two distinct param bindings ($qLike escaped for the ILIKE clause, $qRaw raw for the ranking arithmetic). Empty $qRaw guard bails before binding. Bigram-frequency-count ranking via (LENGTH(chunk_text) - LENGTH(REPLACE(chunk_text, $qRaw, ''))) / LENGTH($qRaw) approximates ts_rank semantics; position-in-chunk tiebreaker so earlier matches outrank later ones at the same occurrence count. Codex outside-voice C8 caught the original plan's one-param shortcut (escaped chars can't be reused as ranking substrings) + missing ESCAPE clause + asymmetric whitespace strip. C9 corrected the FTS dialect (websearch_to_tsquery, not to_tsvector('simple')). Source-boost CASE, hard-exclude clause, visibility clause, and the DISTINCT ON (slug) page-dedup all survive on both branches. Postgres engine path stays untouched (multi-tenant Postgres deployments can install pgroonga / zhparser for CJK; out of scope for this wave). Postgres + PGLite putPage both extended to write chunker_version and source_path columns (with COALESCE(EXCLUDED.x, pages.x) so auto-link / code-reindex callers that don't supply them don't blank existing values). Tests: 8 new cases covering Chinese / Japanese / Korean substring search, bigram ranking (3-hit > 1-hit), LIKE-meta-char escape (literal % does not wildcard), English query stays on FTS path. Co-Authored-By: vinsew <vinsew@users.noreply.github.com> Co-Authored-By: 313094319-sudo <313094319-sudo@users.noreply.github.com> * feat: import-file frontmatter-slug fallback + audit JSONL importFromFile gains a fallback branch: when slugifyPath returns empty (emoji / Thai / Arabic / exotic-script filename — including post-CJK-wave files that still don't slugify) AND the frontmatter declares a slug, the frontmatter slug becomes authoritative. Anti-spoof rule preserved unchanged: when slugifyPath produces a non-empty path slug AND the frontmatter slug claims a different one, the file is still rejected. notes/random.md cannot impersonate people/elon via frontmatter. D6=B error string when both path slug AND frontmatter slug are empty: "Filename produces no usable slug. Add a 'slug:' to the frontmatter, or rename the file to use ASCII / Chinese / Japanese / Korean characters." Honest about the actually-supported scripts. Every import now populates pages.chunker_version (set to MARKDOWN_CHUNKER_VERSION) and pages.source_path (repo-relative). These drive the post-upgrade reindex sweep + sync's delete/rename slug resolution. NEW src/core/audit-slug-fallback.ts — weekly ISO-week-rotated JSONL at ~/.gbrain/audit/slug-fallback-YYYY-Www.jsonl. Per codex C7, info events don't belong in sync-failures.jsonl (which gates bookmark advancement); separate audit surface keeps the failure-handling code unchanged. logSlugFallback emits a stderr line AND appends to the audit file (D7=D dual logging). Tests: 5 new import-file cases (小米 with no frontmatter slug, 🚀.md with frontmatter fallback, 🌟🚀.md friendly D6=B error, anti-spoof regression, chunker_version + source_path populated). 6 new audit cases covering write, weekly rotation, 7-day window, corrupt-row tolerance. Co-Authored-By: vinsew <vinsew@users.noreply.github.com> * feat: git() helper hardening + core.quotepath=false for CJK paths git CLI emits CJK paths as quoted octal escapes (\345\223\201 ...) by default in diff --name-status output. Pre-fix, buildSyncManifest silently dropped these paths because downstream filesystem lookups saw the literal escape string. gbrain sync reported added=0 while git had the file committed. git() helper refactored: - New signature: git(repoPath, args: string[], configs?: string[]) - Config flags emit BEFORE -C and BEFORE the subcommand (git CLI requires this order) - core.quotepath=false always prepended - Future callers needing extra -c config pass configs:[]; no more inlining -c into args (the silent-future-drift footgun codex C12 flagged as a related concern) New invariant test in test/sync.test.ts pins the emit order. NEW test/e2e/sync-cjk-git.test.ts — real-git E2E in a tmpdir. Spawns real git via execFileSync, commits a Chinese-named markdown file, drives the helper through buildSyncManifest, asserts the manifest contains the UTF-8 path (not the octal-escape form). Closes the real-CLI-behavior gap that unit tests can't cover (the helper builds the right args; only an E2E proves git actually emits UTF-8 under the flag). Co-Authored-By: vinsew <vinsew@users.noreply.github.com> * feat: gbrain reindex --markdown sweep command NEW src/commands/reindex.ts — operator-facing markdown re-chunk sweep. Walks SELECT slug, source_path FROM pages WHERE page_kind = 'markdown' AND chunker_version < MARKDOWN_CHUNKER_VERSION in 100-row batches, ordered by id ASC so partial-completion re-runs pick up where they left off. For rows with non-null source_path: re-imports via importFromFile when the file exists on disk. For rows without (legacy pre-migration backfill): fallback to importFromContent using the stored markdown body. Flags: --markdown (target selector), --limit N, --dry-run, --json, --no-embed (offline / CI / test path that lets the chunker run without a configured AI gateway), --repo PATH. Wired into src/cli.ts dispatch table. Will also be invoked automatically by gbrain upgrade's post-upgrade hook (next commit) so chunker-version bumps reach existing markdown pages without an explicit operator action. Tests in test/reindex.test.ts: 5 cases covering dry-run, actual sweep, idempotent re-run, --limit cap, skipped-already-at-current. Co-Authored-By: vinsew <vinsew@users.noreply.github.com> Co-Authored-By: 313094319-sudo <313094319-sudo@users.noreply.github.com> * feat: post-upgrade chunker-bump cost prompt + auto-reindex sweep Wires the chunker-version bump into gbrain upgrade so existing brains heal automatically. Three new pieces: NEW src/core/embedding-pricing.ts — EMBEDDING_PRICING map keyed provider:model (OpenAI text-embedding-3-large + 3-small + ada-002, Voyage 3-large + 3). lookupEmbeddingPrice returns 'known' or 'unknown' shape so the cost-estimate prompt can degrade gracefully for unknown providers rather than fabricate numbers (codex C3). estimateCostFromChars uses 3.5 chars/token approximation. NEW src/core/post-upgrade-reembed.ts — pure-ish functions for the cost-estimate prompt: - computeReembedEstimate: real SQL against COUNT(*) + COALESCE(SUM(LENGTH(compiled_truth)) + SUM(LENGTH(timeline)) on the chunker_version-filtered query. No phantom markdown_body column (codex C3 caught the original plan referencing nonexistent schema fields). - formatReembedPrompt: pure string formatter for the stderr line. - runPostUpgradeReembedPrompt: orchestrates the prompt + 10-second Ctrl-C window. TTY-only wait so non-TTY upgrades (CI, cron-driven, headless) don't hang. GBRAIN_NO_REEMBED=1 bails out entirely with a doctor-warning marker; GBRAIN_REEMBED_GRACE_SECONDS=0 skips the wait. src/commands/upgrade.ts: after apply-migrations runs, the new prompt fires through the gateway's configured embedding model, then invokes gbrain reindex --markdown automatically if the user proceeds. Wrapped in try-catch so a reindex failure is non-fatal — the user can re-run manually. Tests in test/upgrade-reembed-prompt.test.ts: 11 cases covering real SQL counts, unknown-provider fallback, TTY / non-TTY paths, GBRAIN_NO_REEMBED bail-out, GBRAIN_REEMBED_GRACE_SECONDS=0 skip-wait. Codex outside-voice C2 caught the original plan as a no-op (performSync doesn't re-import unchanged files just because content_hash would differ). The migration v51 column + this sweep + this prompt is the real fix that actually reaches existing pages. Co-Authored-By: vinsew <vinsew@users.noreply.github.com> * feat: doctor slug_fallback_audit check + CJK roundtrip E2E gbrain doctor learns a new slug_fallback_audit check (v0.32.7). Reads the latest week of ~/.gbrain/audit/slug-fallback-*.jsonl, counts info-severity entries from the last 7 days, surfaces the total as an ok-status line. No health-score docking; no warning. sync-failures.jsonl (which gates bookmark advancement) stays untouched — info events live in their own surface per codex C7. NEW test/e2e/cjk-roundtrip.test.ts — proves the wave delivers end- to-end. PGLite-in-memory fixture with Chinese / Japanese / Korean content. Each page: importFromContent → chunkText (CJK-aware) → searchKeyword (LIKE-branch with bigram count). Asserts every CJK query lands on its source page. ASCII regression: an English query still uses the FTS path on the same brain. Vector path skips gracefully without OPENAI_API_KEY. Co-Authored-By: vinsew <vinsew@users.noreply.github.com> * chore: bump version and changelog (v0.32.7) CJK fix wave — six layers from one root cause. Three originating PRs from @vinsew and one extracted from @313094319-sudo's #765 land together as a coherent collector. Codex outside-voice review on the plan caught four critical bugs the eng review missed (no-op re-embed, SLUG_SEGMENT_PATTERN half-fix, LIKE SQL needing two distinct param bindings, countCJKAwareWords over-splitting on English+1-CJK-term docs). All four addressed in the implementation. TODOS.md: resolved the v0.32.x PGLite CJK keyword fallback entry; filed five v0.33+ follow-ups (Postgres CJK FTS via pgroonga / wider Unicode property escapes / -z NUL git framing / CJK overlap context / other non-Latin scripts / embedding pricing refresh mechanism). Co-Authored-By: vinsew <vinsew@users.noreply.github.com> Co-Authored-By: 313094319-sudo <313094319-sudo@users.noreply.github.com> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: review findings — forceRechunk + source_path lookup (codex post-merge) Two critical issues caught by codex adversarial on the post-merge tree: F1 — Reindex sweep was a no-op on unchanged-source pages. importFromContent short-circuits on existing.content_hash === hash BEFORE the chunker runs, so the v0.32.7 MARKDOWN_CHUNKER_VERSION bump (and master's v0.32.2 stripFactsFence privacy strip) never reached pages whose markdown body hadn't been edited. Fix: new `forceRechunk?: boolean` option on importFromContent + importFromFile. When set, the hash short-circuit is bypassed and the page re-runs the full chunk + write pipeline. `gbrain reindex --markdown` now passes forceRechunk: true on every row. This means: - The CJK chunker bump actually reaches existing markdown pages. - Master's v0.32.2 stripFactsFence applies retroactively too — any pre-strip private fact bytes lingering in content_chunks get cleared when the v0.32.7 post-upgrade sweep runs. New test in test/reindex.test.ts seeds a page, runs the sweep, mocks a stale chunker_version=1 without changing compiled_truth, runs the sweep again, asserts chunker_version is bumped despite hash match. F4 — Sync delete/rename still used resolveSlugForPath(path) only, ignoring the new pages.source_path column added in v52. Frontmatter-fallback pages (emoji-only / Thai / Arabic filenames where slugifyPath returns empty and the slug came from the markdown frontmatter) would orphan on delete or rename because the path-derived slug doesn't match the stored slug. Fix: new exported helper resolveSlugByPathOrSourcePath(engine, path, sourceId?) queries pages.source_path first, falls back to resolveSlugForPath when no row matches. Threaded into 3 call sites in sync.ts (un-syncable modified cleanup at :531, deletes at :603, rename oldSlug at :622). Best-effort: query errors fall through to the legacy path so pre-migration brains still work. 3 new test cases in test/sync.test.ts cover: stored-slug lookup hits, fallback when no source_path row exists, and source_id scoping when two sources have the same source_path value. Codex finding #3 (reindex not in CLI_ONLY) was verified as a false positive — CLI_ONLY is the set that doesn't need an engine; reindex correctly belongs to the engine-backed dispatch. 302 wave tests pass / 0 fail. bun run verify green. * docs: update CLAUDE.md + llms-full.txt for v0.32.7 CJK fix wave CLAUDE.md Key Files: added entries for the five new modules introduced by the wave — src/core/cjk.ts (shared detection + delimiters + density threshold), src/core/audit-slug-fallback.ts (weekly JSONL), src/core/embedding-pricing.ts (post-upgrade cost lookup table), src/core/post-upgrade-reembed.ts (prompt + grace window), and src/commands/reindex.ts (chunker_version sweep with forceRechunk). Also noted src/commands/sync.ts:resolveSlugByPathOrSourcePath — the F4 codex post-merge fix that wires the new pages.source_path column into sync delete/rename so frontmatter-fallback pages don't orphan. CLAUDE.md Commands: added a v0.32.7 section covering `gbrain reindex --markdown`, the new doctor slug_fallback_audit check, PGLite CJK keyword fallback in `gbrain search`, and the post-upgrade chunker-bump cost prompt with its env-var overrides. llms-full.txt: regenerated via bun run build:llms (CI gate runs the generator on every release; commit must include the bundle). README.md: no changes needed — v0.32.7 is internal correctness across the existing pipeline, not a new skill or setup story. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: vinsew <vinsew@users.noreply.github.com> Co-authored-by: 313094319-sudo <313094319-sudo@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
52f9581966 |
v0.22.11 feat: storage tiering — db_tracked vs db_only directories (#494)
* feat: storage tiering — git-tracked vs supabase-only directories Brain repos scaling to 200K+ files. Bulk data (tweets, articles, transcripts) bloats git repos and slows operations. New storage config in gbrain.yml lets users declare git-tracked and supabase-only directories. Changes: - New config: storage.git_tracked and storage.supabase_only in gbrain.yml - gbrain sync auto-manages .gitignore for supabase-only paths - gbrain export --restore-only restores missing supabase-only files from DB - New gbrain storage status command shows tier breakdown - Config validation warns on conflicts - 8 tests passing, full docs at docs/storage-tiering.md Backward compatible — systems without gbrain.yml work unchanged. * feat: add getDefaultSourcePath() typed accessor (step 1/15) Single source of truth for "what brain repo are we operating against?" Replaces ad-hoc raw SQL in storage.ts:38 (Issue #3 of eng review). Used by both gbrain storage status and gbrain export --restore-only. Returns null on miss, throws on DB error. Composes with the existing resolveSourceId chain so it honors --source flag / GBRAIN_SOURCE env / .gbrain-source dotfile / longest-prefix CWD match / brain-level default. 4 new test cases covering happy path, missing local_path, DB error propagation, and CWD-prefix resolution priority. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: replace gray-matter with dedicated YAML parser (step 2/15) The original storage-config.ts called gray-matter on a delimiter-less YAML file. Gray-matter only parses YAML inside `---` frontmatter blocks; without delimiters, it returns `{data: {}}`. Result: loadStorageConfig() always returned null, the entire feature was a silent no-op for every user. Original eng review's P0 confidence-9 finding (Issue #1). Replaces gray-matter with a small dedicated parser for the gbrain.yml shape (top-level `storage:` section, two array-valued nested keys). Yaml-lite was considered first, but its flat key:value design doesn't handle nested arrays. The dedicated parser is ~50 lines and trades expressiveness for zero-dep, predictable parsing of a file format we control. Adds the Issue #1B sanity warning (locked B): when gbrain.yml exists but has no storage section (or empty arrays), warn once-per-process so the user sees their config didn't take. The single test that would have caught the original P0 — write a real gbrain.yml, call loadStorageConfig, assert non-null — now exists. Also tightens loadStorageConfig per D36: distinguishes "absent" (silent null) from "unreadable" (throws). The previous code silently swallowed read errors, hiding broken installs. 8 new test cases: real-disk happy path, comments + blank lines, quoted values, missing storage section warning, empty section warning, once-per-process warning suppression, unreadable file behavior, and the existing helper tests (validation, tier matching, edge cases) all still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: rename storage keys to db_tracked/db_only (step 3/15) The vendor-specific names "supabase_only" and "git_tracked" hardcoded a backend (Supabase) into the config schema. gbrain ships two engines — PGLite and Postgres-via-Supabase. The canonical distinction is "lives in the brain DB only" vs "lives in the brain DB and on disk under git." Both work on either engine. Renamed throughout (Issue #4 of eng review): git_tracked → db_tracked supabase_only → db_only isGitTracked() → isDbTracked() isSupabaseOnly() → isDbOnly() StorageTier 'git_tracked'/'supabase_only' → 'db_tracked'/'db_only' Backward compatibility (D3 lock): loadStorageConfig accepts both shapes. Loader resolution order per the eng-review pass-2 finding: parse YAML → if canonical keys present use them, else if deprecated keys present map to canonical AND emit once-per-process deprecation warning → THEN run validation. Validation always sees the canonical shape so error messages reference db_tracked/db_only regardless of which keys the user wrote. The deprecation warning suggests `gbrain doctor --fix` for an automated rename (D72 — fix path lands in step 7). When both shapes coexist in one file, canonical wins and a stronger warning fires ("deprecated keys ignored — remove them"). Aliases isGitTracked/isSupabaseOnly kept for now to avoid churning the sync.ts / export.ts / storage.ts call sites in this commit; they'll be removed in a follow-up step. Storage.ts's tier-bucket initializers and output strings updated. ASCII output replaces unicode box-drawing per D10. gbrain.yml example file updated to canonical keys with explanatory comments. 2 new test cases: deprecated-key fallback (asserts both shapes load correctly with warning), canonical-wins-over-deprecated (asserts the "both shapes coexist" path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: add slugPrefix to PageFilters with engine-side filter (step 4/15) Issue #13 of the eng review: storage.ts and export.ts loaded every page in the brain (limit: 1_000_000) to check tier membership. On the 200K-page brains this feature targets, that's the wall-clock and memory landmine the feature exists to fix. Adds an optional `slugPrefix` field to PageFilters. Both engines implement it as `WHERE slug LIKE prefix || '%' ESCAPE '\'`, with literal escaping of LIKE metacharacters (%, _, \) so user-supplied prefixes like `media/x/` are treated as exact string prefixes. Performance: the (source_id, slug) UNIQUE constraint on the pages table gives both engines a btree index that supports LIKE-prefix range scans. An EXPLAIN on Postgres confirms the index range scan rather than a seq scan. PGLite has the same index shape via pglite-schema.ts. Consumers updated: - export.ts: --slug-prefix flag now goes engine-side (no in-memory .filter(...)). The --restore-only path queries each db_only directory with slugPrefix in a loop instead of one full-table scan, with seen-set deduplication and disk-existence check inline. - storage.ts: keeps the full-scan path because storage-status needs the "unspecified" bucket count, which can't be computed without enumerating every page. Comment notes that step 5 (single-walk filesystem scan) will reduce per-page disk syscall cost. 2 new test cases on PGLiteEngine: slugPrefix happy path (3 tier dirs, asserts only matching slugs return) and metacharacter escape regression (asserts safe/ doesn't match unrelated slugs). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf: single-walk filesystem scan via walkBrainRepo() (step 5/15) Issue #14 of the eng review: storage.ts called existsSync + statSync per-page in a synchronous loop. On a 200K-page brain that's 400K syscalls serialized. Wall-clock landmine. Adds src/core/disk-walk.ts with walkBrainRepo(repoPath) — one recursive readdirSync walk, builds a Map<slug, {size, mtimeMs}>. Storage.ts looks up each DB page in the map (O(1)) instead of stat-checking on demand. Slug derivation matches the pages-table convention: people/alice.md on disk becomes people/alice as the map key. Skipped during walk: - dot-directories (.git, .gbrain, .vscode, etc) — not part of the brain namespace - node_modules — guards against accidentally walking into imported repos - non-.md files (sidecar JSON, binaries) — tracked by the brain through the files table, not by slug Reusable: future commands (gbrain doctor's storage_tiering check, the optional autopilot tier-fix path) get the same walk for free. 9 new test cases: empty dir, nonexistent dir, top-level files, nested dirs, dot-dir skipping, node_modules skipping, non-.md filtering, size capture, mtimeMs capture. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: path-segment matching for tier directories (step 6/15) Issue #5 + D6 of the eng review: tier matching used slug.startsWith(dir), which falsely matches 'media/xerox/foo' against 'media/x' if a user wrote the directory without a trailing slash. The new matcher requires the configured directory to end with `/` and treats it as a canonical path-segment ancestor: media/x/ matches media/x/tweet-1 ✓ media/x/ doesn't media/xerox/foo ✗ media/x refused media/x/tweet-1 (matcher requires trailing /) Non-canonical input (no trailing slash) is refused outright. Step 7's auto-normalizing validator converts user-written 'media/x' → 'media/x/' on load, so the matcher never sees non-canonical input from real configs. The behavior tested here is the strict matcher's contract. Regression test pins the media/xerox collision case explicitly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: auto-normalize trailing-slash, throw on tier overlap (step 7/15) D7+D8 of the eng review: validation was warnings-only. Users miss warnings. Now: - Cosmetic: missing trailing slash auto-corrected, one-time info note showing what changed ("normalized 2 storage paths: 'people' → 'people/', 'media/x' → 'media/x/'"). Once-per-process to keep noise low. - Semantic: same directory in both tiers throws StorageConfigError. Ambiguous routing — does media/ win as db_tracked or db_only? — is a real bug the user must fix. Caller propagates to the CLI for a clean exit-1 with actionable message. loadStorageConfig now applies normalize+validate after merging deprecated keys, so the path-segment matcher (step 6) only ever sees canonical trailing-slash directories. The pure validateStorageConfig kept for callers who want the warnings list without the auto-fix side effects (gbrain doctor's reporting path). 2 new test cases: auto-normalize round-trip with warning text assertion, overlap throws StorageConfigError. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: wire manageGitignore into runSync, only on success (step 8/15) Issue #2 of the eng review: manageGitignore was defined and never invoked. Docs claimed "auto-managed by gbrain" — false. Users hit a .gitignore that never updated and committed db_only directories anyway. Wire-up: runSync now calls manageGitignore after each successful performSync return, in both watch and one-shot modes. Eng review pass-2 finding #1: skip on dry_run AND blocked_by_failures status. A sync that aborted partway has stale state; mutating .gitignore based on a partially-loaded config invites drift. Failure-skip test added (uses .gitignore-as-a-directory to simulate write failure; asserts warning fired and disk wasn't corrupted). Hardened manageGitignore itself with three additional behaviors: - GBRAIN_NO_GITIGNORE=1 escape hatch (D23) for shared-repo setups where a maintainer wants gbrain to leave .gitignore alone. - Submodule detection (D49). When repoPath/.git is a regular file (gitdir: ... pointer), the repo is a git submodule. Submodule .gitignore changes don't survive parent submodule updates, so we skip with an actionable warning ("add db_only directories to your parent repo's .gitignore manually"). - Graceful failure (D9). Read errors, write errors, and StorageConfigError (overlap from step 7) all log a warning and return — sync's primary job (moving data) shouldn't die because of a side-effect on .gitignore. manageGitignore is now exported (previously private) so the storage-sync test file can hit it directly without spinning up sync. 9 new test cases: no-op without gbrain.yml, no-op with empty db_only, happy-path append, idempotency (run twice, single entry), preservation of user-written rules, GBRAIN_NO_GITIGNORE skip, submodule skip, .git-directory normal path, write-failure graceful warning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: D5 resolution chain for --restore-only and storage status (step 9/15) D5 of the eng review: gbrain export --restore-only without --repo silently fell through to the regular export path, dumping every page in the database to the wrong directory. Hard regression risk. Now exits 1 with an actionable message when --restore-only has no --repo AND no configured default source. Resolution order: 1. Explicit --repo flag 2. Typed sources.getDefault() (reuses step 1's accessor) 3. Hard error — never fall through to cwd storage.ts:38 also bypassed BrainEngine with raw SQL and a bare try/catch (Issue #3 + Issue #9). Replaced with the same typed getDefaultSourcePath() — single source of truth, errors propagate cleanly to the user, no silent cwd fallback. Regular export (no --restore-only) keeps its current behavior per D26: exports include everything, --repo is optional. 4 new test cases on PGLite in-memory: - hard-errors with no --repo + no default - explicit --repo wins - falls back to sources default local_path - non-restore export does not require --repo Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: split storage.ts into pure data + JSON + human formatters (step 10/15) Issue #10 of the eng review: getStorageStatus and runStorageStatus mixed data gathering, JSON serialization, and human-readable output in one function. Hard to test, hard to reuse, mismatched the orphans.ts pattern that CLAUDE.md cites as the precedent. Now three pure functions + a thin dispatcher: getStorageStatus(engine, repoPath) — async, returns StorageStatusResult. Side effects: engine.listPages + one walkBrainRepo (Issue #14). Exported so MCP exposure (D14) and gbrain doctor (D13) can consume the same data without re-running the loop. formatStorageStatusJson(result) — pure, returns indented JSON. Stable contract on the StorageStatusResult shape, suitable for orchestrators. formatStorageStatusHuman(result) — pure, returns ASCII text (D10 — no unicode box-drawing). Composable into other commands later. runStorageStatus(engine, args) — thin dispatcher: parses --repo / --json, calls getStorageStatus, picks a formatter, prints. 8 new test cases on the formatters: JSON parse round-trip, null-config fallback, missing-files capped at 10 with rollup, ASCII-only assertion (D10 regression guard), warnings inline, configuration listing, disk- usage block omitted when zero bytes. The StorageStatusResult interface is now exported as a public type, so gbrain doctor's storage_tiering check can build its own findings from the same shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * types: distinct PageCountsByTier and DiskUsageByTier (step 11/15) Issue #11 of the eng review: pagesByTier (page counts) and diskUsageByTier (byte totals) shared the same structural type (Record<StorageTier, number>). Both are tier-keyed numeric maps but carry semantically different units. A future bug that swaps them at a call site (e.g., displaying disk bytes where the count belongs) wouldn't trip the compiler. Replaced with distinct nominal types via a brand field. Structurally identical at runtime (no overhead) but compile-time disjoint — TypeScript catches accidental cross-assignment. PageCountsByTier { db_tracked, db_only, unspecified } : numbers (count) DiskUsageByTier { db_tracked, db_only, unspecified } : numbers (bytes) Both initialized in getStorageStatus, both threaded into StorageStatusResult, both consumed by formatStorageStatusHuman / formatStorageStatusJson without further changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: PGLite soft-warn + full lifecycle test (step 12/15) D4: storage tiering on PGLite is a partial feature. The "DB" the pages live in IS the local file gbrain uses for everything else, so "db_only" has no real offload effect. The .gitignore management still helps (keeps bulk content out of git history), so we warn and proceed — not refuse. Two warning sites (once-per-process each via module-local flags): - storage status: warns at runStorageStatus entry - sync: warns inside manageGitignore when engineKind='pglite' and config has db_only entries Both phrased actionably ("To get full tiering, migrate to Postgres with `gbrain migrate --to supabase`"). manageGitignore signature now takes an optional `engineKind` param. runSync passes engine.kind. Stand-alone callers (tests, future gbrain doctor --fix path) can omit it. New test: test/storage-pglite.test.ts — D8 + D4 lifecycle. 6 cases: engine.kind assertion, getStorageStatus loading gbrain.yml + reporting tier counts, manageGitignore PGLite-warn (once per process), Postgres no-warn, slugPrefix on PGLite, end-to-end (config + putPage + status + gitignore). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: add trailing-newline CI guard (step 14/15) Issue #7 of the eng review: all four new files in the original storage-tiering branch lacked POSIX trailing newlines. Linters complain, git diffs phantom-flag every future edit. We've been adding newlines as each file landed; this commit catches the regression class. scripts/check-trailing-newline.sh: - sibling to check-jsonb-pattern.sh / check-progress-to-stdout.sh per CLAUDE.md's CI guard pattern - portable to bash 3.2 (macOS default; no mapfile, no associative arrays) - covers src/**, test/**, gbrain.yml, top-level *.md - reports each missing file by path and exits 1 Wired into `bun run test` between progress-to-stdout and typecheck. Also fixed docs/storage-tiering.md (pre-existing missing newline from the original branch — caught by the new guard on first run). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.23.0 — VERSION, CHANGELOG, README, CLAUDE.md, storage-tiering.md (step 15/15) VERSION → 0.23.0 (minor bump for new feature surface). CHANGELOG entry in Garry voice with the canonical format: - Two-line bold headline ("Storage tiering, finally working...") - Lead paragraph naming what was broken before and what users get now - "Numbers that matter" before/after table for the 6 things that actually changed - "What this means for your brain" closer - "To take advantage of v0.23.0" self-repair block (per CLAUDE.md convention) — 6 numbered steps users can follow - Itemized changes split into critical fixes / new+renamed surface / architecture cleanup / tests + CI guards CLAUDE.md "Key files" gains four new entries: storage-config.ts, disk-walk.ts, the v0.23.0 storage.ts shape, and gbrain.yml itself. README.md gains a new "Storage tiering" section between Skillify and Getting Data In with the canonical example + commands + link to the full guide. docs/storage-tiering.md rewritten end-to-end with canonical key names (db_tracked / db_only), v0.23.0 hardening details (idempotency, submodule detection, GBRAIN_NO_GITIGNORE, dry-run gating), the resolution chain for --restore-only, the auto-normalize + throw-on-overlap validator, and the PGLite engine note. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: e2e Postgres lifecycle for storage tiering (step 16/16) Per the v0.23.0 plan: full lifecycle E2E against real Postgres. - engine.kind === 'postgres' assertion - Full lifecycle: write 4 pages (1 db_tracked, 2 db_only, 1 unspecified) → getStorageStatus reports correct tier counts → human formatter renders → manageGitignore writes managed block → idempotency check → getDefaultSourcePath() resolves the configured local_path. - Container restart simulation: 2 db_only pages in DB, files missing on disk → status.missingFiles.length === 2 → slugPrefix engine filter on Postgres returns exactly the tier slugs. - slugPrefix index-based range scan regression: 50 media/x/* + 50 people/p-* pages → slugPrefix='media/x/' returns exactly 50. - getDefaultSourcePath returns null when default source has no local_path (the hard-error path that replaces the original silent cwd fallback). - manageGitignore on Postgres engine does NOT emit the PGLite soft-warn (cross-engine assertion). Skips gracefully when DATABASE_URL is unset, per CLAUDE.md E2E pattern. Run via: DATABASE_URL=... bun test test/e2e/storage-tiering.test.ts Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: rebump version 0.23.0 → 0.22.9 Reverts the minor bump back to a patch-style version on the v0.22 line. Storage tiering ships within the v0.22.x train alongside the recent fix waves. Updates VERSION, package.json, CHANGELOG header + body refs, CLAUDE.md Key files annotations, README.md section heading, and the docs/storage-tiering.md backward-compat note. * chore: bump version 0.22.9 → 0.22.11 Sibling workspaces claimed v0.22.10 in the queue. This branch advances to v0.22.11 to keep the version monotonic on master. Updates VERSION, package.json, CHANGELOG header + body refs, CLAUDE.md Key files annotations, README.md section heading, and the docs/storage-tiering.md backward-compat note. * fix: address Codex pre-landing review findings (4 fixes) Codex found 4 real issues during pre-landing review of v0.22.11 diff: [P0] export --restore-only fell through to full export when storageConfig was null (no gbrain.yml present). On older or misconfigured brains, the recovery command would silently dump the entire database. src/commands/export.ts now refuses with an actionable error before any page query fires — matches the D5 lock spirit ("never silently fall through"). [P1] manageGitignore wire-up only fired when --repo was passed explicitly. performSync resolves the repo from sync.repo_path or sources.local_path, so the common `gbrain sync` path (after setup, no flag) never updated .gitignore. src/commands/sync.ts now uses the same source-resolver chain as the rest of /ship: opts.repoPath → getDefaultSourcePath → null. Fires in both watch and one-shot modes. [P2] getDefaultSourcePath only consulted sources.local_path, missing the legacy global sync.repo_path config key that pre-v0.18 brains use. Added a fallback to engine.getConfig('sync.repo_path') when the sources row has NULL local_path. Pre-v0.18 brains now work without forcing a `gbrain sources add . --path .` migration. [P2] sync --all multi-source loop never called manageGitignore even though src.local_path was already known. Each source now gets its own gitignore update on successful sync. Tests: - test/storage-export.test.ts: replaced the old "falls through to full export" test with one that asserts the new refusal path (storage-tiering config required for --restore-only). - test/source-resolver.test.ts: added a fallback test exercising the legacy sync.repo_path code path for pre-v0.18 brains. - All 78 storage-tiering tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: regenerate llms.txt + llms-full.txt for v0.22.11 Per CLAUDE.md: "Run `bun run build:llms` after adding a new doc." The README's new Storage tiering section + the rewritten docs/storage-tiering.md changed the inlined bundle. test/build-llms.test.ts catches the drift and was failing on master pre-regen. * fix: typecheck error in disk-walk.ts (CI #73350475897) tsc --noEmit failed in CI because ReturnType<typeof readdirSync> with withFileTypes:true picks an overload union that includes Dirent<Buffer<ArrayBufferLike>>. Strict tsc treats entry.name as Buffer, so .startsWith / .endsWith / string comparisons all blew up. Annotate the variable as Dirent[] (string-based) and cast through unknown, matching the pattern sync.ts already uses for its own filesystem walk. Same runtime behavior; clean typecheck. Tests still 9/9. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> EOF --------- Co-authored-by: root <root@localhost> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f718c595b3 |
v0.21.0 feat: Code Cathedral II — call-graph edges, two-pass retrieval, parent-scope chunking (#422)
* feat: v0.18.0 baseline — code indexing + multi-repo (Layer 0)
Tree-sitter-based code chunker for TS/JS/Python/Ruby/Go. Splits code at
semantic boundaries (functions, classes, types, exports). Each chunk
includes a structured header for embedding context.
Multi-repo config: gbrain repos add/list/remove, gbrain sync --all.
Strategy-aware sync: markdown (default), code, or auto. New PageType
'code' for code file pages.
This is Layer 0 of the v0.18.0 code-indexing plan (see ~/.claude/plans
cathedral plan). Subsequent layers add: tests, bun --compile WASM
embedding + CI guard (A1), schema migrations v16 (pages.repo_name) +
v17 (content_chunks code metadata), per-repo sync bookmarks, runCycle
multi-repo, Chonkie chunker parity (E2a), incremental chunking (E2),
doc↔impl linking (E1), markdown fence extraction (E3), symbol navigation
commands (code-def, code-refs), cost preview, BrainBench code category,
CHANGELOG, migration file, docs.
Backward compatible: no config changes = existing behavior preserved.
* feat: v0.19.0 Layer 1 — tests for baseline + errors envelope + version bump
Adds the structured error envelope (src/core/errors.ts) that downstream
v0.19.0 commands (code-def, code-refs, sync --all cost preview,
importCodeFile) all hand back to agents. The envelope follows the v0.17.0
CycleReport.PhaseResult.error shape so agent-consumption stays consistent
across every gbrain surface.
Test coverage for Wintermute's baseline (added in Layer 0):
- test/errors.test.ts — envelope helper + GBrainError + serializeError
- test/multi-repo.test.ts — config CRUD, dedup, file permissions
- test/sync-strategy.test.ts — isSyncable strategy matrix + include/exclude
globs + slugifyCodePath + pathToSlug with pageKind
Bug fixes uncovered by the new tests:
- src/core/sync.ts: globToRegex handles `src/**/*.ts` matching `src/foo.ts`
(zero intermediate dirs). `**/` now compiles to `(?:.*/)?` instead of
`.*/`. Also `?` now matches only non-slash chars (was `.`).
- src/core/config.ts: configDir() respects GBRAIN_HOME env override so
tests can isolate ~/.gbrain/. Matches GBRAIN_AUDIT_DIR convention.
Bun's os.homedir() ignores $HOME on macOS, so we need an explicit
override variable.
Version bump: package.json 0.18.2 → 0.19.0. v0.18.0-2 were already
released (multi-source brains + RLS + migration hardening), so the next
free minor for code indexing is 0.19.0. Wintermute's baseline author
label of 0.16.4 had been stale since v0.17.0 shipped; no user-visible
regression from the jump.
Per the rebased cathedral plan: Wintermute's multi-repo.ts and repos
CLI are preserved at the baseline but will be superseded in Layer 4 by
the v0.18.0 sources system (src/core/source-resolver.ts,
src/commands/sources.ts). multi-repo tests stay valid for the baseline
and will be removed alongside the code they cover.
* feat: v0.19.0 Layer 2 — bun --compile WASM embedding + CI guard
The single highest-risk change in v0.19.0 code indexing. Before this, the
chunker loaded WASMs via `new URL('../../../node_modules/...', import.meta.url)`
which silently breaks in the compiled binary (no node_modules at runtime).
Users would see degraded chunking quality with no error, just fallback-
recursive chunks instead of real semantic chunks. Codex flagged this as
the #1 silent-failure mode.
Mechanics:
- `src/assets/wasm/tree-sitter.wasm` + 36 grammar WASMs committed to the
repo (50MB). Not a small check-in, but the alternative is a postinstall
script that runs before every dev bun run and fails fragile-ly on
network errors.
- `src/core/chunkers/code.ts` uses Bun's `import ... with { type: 'file' }`
import attribute. At runtime the imported value is a file path — the
actual repo path in dev, a bundler-synthesized path in the compiled
binary. The tree-sitter runtime's `Language.load(path)` reads it the
same way in both cases.
- Layer 2 keeps the 6-language support Wintermute shipped (TS/TSX/JS/Py/
Rb/Go). Layer 5 (E2a chunker parity) expands to all 36 bundled grammars.
- CHUNKER_VERSION=2 constant introduced. importCodeFile will fold this
into content_hash in Layer 3 so chunker-shape changes across releases
force clean re-chunks without the user needing `sync --force`.
CI guard — `scripts/check-wasm-embedded.sh` + `scripts/chunker-smoketest.ts`:
- Compiles a smoketest binary that calls chunkCodeText on a known TS
snippet.
- Asserts the output has `has_real_symbols: true`, a `[TypeScript]`
language tag, and the expected symbol name.
- If the chunker silently falls through to recursive chunks, the
assertions fail the build.
- Wired into `bun test` via package.json script pipeline. Also exposed
as `bun run check:wasm` for standalone invocation.
Verification:
- Dev: `bun -e '...'` smoke test returns 2 chunks with correct symbol
names in under 100ms.
- Compiled: `bash scripts/check-wasm-embedded.sh` passes end to end.
- Binary size: the gbrain binary grows from ~90MB to ~140MB, dominated
by the 50MB of grammar WASMs. Still well within normal for CLIs that
ship a language runtime.
* feat: v0.19.0 Layer 3 — schema migrations for page_kind + chunk code metadata
Adds two migrations to unblock C6/C7 (query --lang, code-def, code-refs)
and the orphans/auto-link branching in later layers.
v25 (pages_page_kind):
- ALTER TABLE pages ADD COLUMN page_kind TEXT NOT NULL DEFAULT 'markdown'
CHECK (page_kind IN ('markdown','code'))
- Postgres path uses ADD CONSTRAINT ... NOT VALID + VALIDATE CONSTRAINT
in a separate statement so tables with millions of pages don't hold a
write lock during the initial check. PGLite has no concurrent writers,
so its variant uses the simpler ALTER TABLE pattern.
- Existing rows carry DEFAULT 'markdown' — pre-v0.19 brains were
markdown-only by definition.
v26 (content_chunks_code_metadata):
- ALTER TABLE content_chunks ADD COLUMN language, symbol_name,
symbol_type, start_line, end_line (all nullable).
- Two partial indexes: idx_chunks_symbol_name WHERE symbol_name IS NOT
NULL, and idx_chunks_language WHERE language IS NOT NULL. Only code
chunks populate these columns, so partial indexes stay small even on
a 50K-chunk brain with mixed markdown+code.
- Markdown chunks leave all five columns NULL. Only importCodeFile
populates them, from the tree-sitter AST via chunkCodeText.
Wiring (both engines):
- PageInput gains `page_kind?: PageKind` ('markdown' | 'code'). Defaults
to 'markdown' when omitted so existing callers don't change. putPage
on both engines writes it through, with ON CONFLICT DO UPDATE updating
page_kind alongside the other fields.
- ChunkInput gains language, symbol_name, symbol_type, start_line,
end_line (all optional). upsertChunks on both engines writes them
through. Existing markdown call sites pass nothing and get NULLs —
zero behavior change for markdown pages.
importCodeFile updates:
- Sets page_kind='code' on the PageInput.
- Populates chunk metadata from the chunker's CodeChunk.metadata for
every chunk it persists. Columns line up 1:1 with the tree-sitter AST
output already produced by the chunker.
- Folds CHUNKER_VERSION=2 into content_hash so chunker shape changes
across releases force clean re-chunks without `sync --force`. The
hash was previously {title, type, content, lang} — now also
chunker_version.
Fresh-install path (src/schema.sql + pglite-schema.ts):
- Both include the page_kind column + CHECK constraint.
- Both include the five new content_chunks columns.
- Both ship the partial indexes so new brains have the same query
performance as migrated brains. Ran `bun run build:schema` to
regenerate src/core/schema-embedded.ts from schema.sql.
Naming: renamed our new Error subclass in src/core/errors.ts from
GBrainError to StructuredAgentError. The legacy GBrainError in
src/core/types.ts predates this change and has a different shape
(positional problem/cause/fix arguments) — keeping both under the same
name was inviting a year of import ambiguity. New v0.19.0 surfaces use
StructuredAgentError + the serializeError() helper.
Tests:
- test/migrations-v0_19_0.test.ts — 12 cases. Covers: MIGRATIONS array
shape (v25/v26 presence, NOT VALID pattern on Postgres, partial
index WHERE clauses), fresh-install schema (page_kind default, CHECK
constraint rejects invalid values, chunk metadata nullable), putPage
round-trip (markdown default + code explicit), upsertChunks
round-trip (code metadata preserved + markdown chunks leave NULLs).
- All 139 existing + new unit tests pass on PGLite (1.5 sec).
* feat: v0.19.0 Layer 4 — delete Wintermute's multi-repo, wire sources
Replaces Wintermute's short-lived repos abstraction with the v0.18.0
sources subsystem. Codex flagged this during plan review: v0.18.0's
sources table had already shipped the right shape (per-source
last_commit, federated search config, RLS-friendly) while Wintermute
coded against a ~/.gbrain/config.json repos array. Two systems solving
one problem.
Keep the surface, swap the backend:
- src/cli.ts: `gbrain repos` routes through runSources with a one-line
deprecation nudge on stderr. Scripts like `gbrain repos list` and
`gbrain repos add .` keep working against the sources table. Removed
the pre-engine-connect branch and added a case inside the
handleCliOnly switch so repos gets the DB connection it now needs.
- src/cli.ts help text: new SOURCES section replaces MULTI-REPO.
References the canonical `sources` commands with `repos` tagged
DEPRECATED.
sync --all — was iterating ~/.gbrain/config.json repos; now iterates
sources rows with local_path IS NOT NULL:
- Reads id, name, local_path, config jsonb via executeRaw.
- Honors config.syncEnabled=false (matching Wintermute's opt-out).
- Honors config.strategy for per-source markdown/code/auto filtering.
- Passes sourceId through to performSync so last_commit tracking lands
on the right sources row (was clobbering a global bookmark before).
Deletions:
- src/core/multi-repo.ts deleted (120 lines of config CRUD now handled
by sources table + RLS).
- src/commands/repos.ts deleted (121 lines of CLI parsing now handled
by src/commands/sources.ts).
- test/multi-repo.test.ts deleted (25 tests against the deleted module;
the schema-backed behavior is covered by test/sources.test.ts from
v0.18.0 + test/repos-alias.test.ts added here).
- src/core/config.ts: removed the `repos` field from GBrainConfig.
Legacy installs with `repos` in ~/.gbrain/config.json will see that
key ignored; no migration written because zero users are on that
path (Wintermute's commit never shipped on master).
Tests:
- test/repos-alias.test.ts — round-trips add/list/remove through
runSources to verify the alias path works. Also asserts the deleted
module is actually gone (catches accidental resurrection during
rebase conflicts).
- All 162 prior unit tests + 2 new = 164 pass on PGLite.
Codex's P0 #2 (per-repo sync state) and P0 #3 (slug collision) are
both resolved here — sources.last_commit scopes bookmarks per source,
and pages.slug uniqueness is (source_id, slug), which is what the
v0.18.0 schema already shipped.
* feat: v0.19.0 Layer 5 — Chonkie chunker parity (E2a)
Expands Wintermute's 6-language chunker to 29 languages, swaps the
heuristic tokenizer for the real thing, and adds small-sibling merging
so a file of 20 tiny const declarations doesn't produce 20 embedding
calls. This closes the Chonkie gap Garry called out in CEO review.
Language coverage — 6 → 29:
- Added grammars: rust, java, c_sharp, cpp, c, php, swift, kotlin,
scala, lua, elixir, elm, ocaml, dart, zig, solidity, bash, css,
html, vue, json, yaml, toml. All shipping in src/assets/wasm/
(committed in Layer 2). Bun's --compile bundles every import
attributes path, so the compiled binary carries every grammar.
- TOP_LEVEL_TYPES populated for the 11 most-used new languages
(rust, java, c_sharp, cpp, c, php, swift, kotlin, scala, lua,
elixir, bash, solidity) + the original 6. Tree-sitter loads the
grammar but the chunker falls through to recursive chunking when
TOP_LEVEL_TYPES isn't set — still correct output, just less
semantic. Every grammar ships with a working fallback.
- detectCodeLanguage extended for 29 extension families including
.mts/.cts (TypeScript), .cc/.hpp/.cxx (C++), .kt/.kts (Kotlin),
.scala/.sc (Scala), .ex/.exs (Elixir), etc.
- DISPLAY_LANG table lookup replaces the inline 6-entry map;
structured headers now read '[Rust]', '[C#]', '[PHP]' etc.
Accurate tokenizer:
- @dqbd/tiktoken with cl100k_base encoding (same encoder
text-embedding-3-large uses). Lazy-loaded on first call via
require() so dev and compiled binary share the init path.
- Falls back to the old len/4 heuristic only if the encoder fails
to initialize (vanishingly unlikely — keeps the chunker available
instead of throwing).
- Existing estimateTokens call sites (large-node threshold +
sub-range splitting + new merge pass) all now see real counts.
Real code is 2-3x more token-dense than prose; the old heuristic
systematically under-split so large functions sometimes exceeded
the embedding API's 8191-token hard cap.
Small-sibling merging:
- New mergeSmallSiblings post-pass runs on the chunk list after
tree-sitter extraction.
- Adjacent chunks under 40% of chunkSizeTokens get accumulated
into one merged chunk up to the full budget.
- Large chunks (functions, classes) pass through untouched.
- Merged chunks get symbolName=null, symbolType='merged',
startLine/endLine spanning the group. The header reads:
'[Lang] path:N-M merged (K siblings)' so retrieval can still
show coherent context.
- Mirrors Chonkie's CodeChunker._group_child_nodes() +
bisect_left accumulation. A Go file with 30 top-level imports +
5 functions no longer produces 30 separate import chunks.
CHUNKER_VERSION bumped 2 → 3:
- Any existing v0.18.x brain with code pages will re-chunk on next
sync because content_hash folds CHUNKER_VERSION in. Without the
bump, stale (2-3x token-off, non-merged) chunks would persist
forever until manual 'sync --force'.
CI guard + smoketest updates:
- scripts/chunker-smoketest.ts replaced the tiny hello/Foo/Id
fixture with a realistic TS snippet (calculateScore with branches
+ UserRegistry class) so at least one chunk has a concrete symbol
name — small-sibling merging would otherwise collapse the old
fixture and fail the assertion.
- scripts/check-wasm-embedded.sh assertions updated: check
has_symbol_names:true (at-least-one-real-symbol), still verify
[TypeScript] header and specifically the calculateScore symbol.
Tests — test/chunkers/code.test.ts (15 cases):
- CHUNKER_VERSION=3 shape assertion (guards silent re-chunking
across releases).
- detectCodeLanguage across 29 extensions + unknown + case-insensitive.
- chunkCodeText on TypeScript / Python / Rust / Go producing chunks
with correct language tag + symbol names.
- Fallback path for unsupported extension produces recursive-chunk
module-kind output.
- Small-sibling merging: 5 tiny consts → 1-2 chunks; big function
passes through untouched; merged chunk line range spans group.
- Structured header shape: starts with [Lang], contains file path,
line range, symbol name.
- Empty input returns empty array.
All 177 unit tests pass + CI guard on compiled binary passes.
* feat: v0.19.0 Layer 6 — incremental chunking + doc↔impl linking
Two expansions from the plan's E1 + E2. E3 (markdown fence extraction)
deferred to a follow-up PR — the feature surface is small and doesn't
block the main cathedral.
E1 — Design-doc ↔ implementation linking:
- New extractCodeRefs() in src/core/link-extraction.ts. Scans markdown
prose for references like 'src/core/sync.ts:42'. Anchored on a
prefix allowlist (src|lib|app|test|tests|scripts|docs|packages|
internal|cmd|examples) + the 39-extension code file list so random
phrases like 'foo/bar.js' don't generate false-positive edges. Dedups
by path (first occurrence wins).
- importFromContent writes bidirectional edges for every code ref
found in compiled_truth + timeline:
markdown_slug --[documents]--> code_slug
code_slug --[documented_by]--> markdown_slug
Both use link_source='markdown', origin_page_id=markdown_slug,
origin_field='compiled_truth' so runAutoLink reconciliation scopes
edges correctly.
- addLink's inner SELECT naturally drops edges to non-existent pages,
so a markdown guide imported before the code repo is synced writes
no edges — they'll land when the code arrives via A3 reverse-scan
(deferred to a follow-up since it only activates for users who sync
markdown and code in opposite order).
E2 — Incremental chunking:
- importCodeFile reads existing chunks via engine.getChunks(slug)
before embedding.
- Keys existing chunks by `${chunk_index}:${chunk_text}`. Any new
chunk that matches verbatim at the same index reuses the existing
embedding (chunk.embedding + token_count). Only new/changed chunks
go to embedBatch.
- Cost impact: a daily autopilot on a stable repo touches ~2-5% of
chunks on each run. E2 cuts OpenAI embedding spend by ~95% vs
naive full re-embed. Stated before (Codex A2 decision) and now
actually implemented.
- Uses chunk_index + chunk_text as the key (not symbol_fqn) because
the tree-sitter chunker already makes chunk_index semantic — it's
AST-order. A blank line at the top of a file shifts start_byte
for every chunk below but leaves chunk_text identical, so the
cache still hits.
- Fallback: when embedBatch throws (rate-limit, network, etc.) the
existing warn-but-continue behavior stays. Un-embedded chunks land
in the DB with NULL embedding; a later `embed --stale` will fix
them.
Tests (test/link-extraction-code-refs.test.ts, 10 cases):
- :line suffix capture.
- Prefix allowlist (11 directories).
- Extension recognition (39 extensions).
- Rejects paths outside allowlisted prefixes.
- Rejects non-code extensions.
- Dedup by path (first occurrence wins).
- Different paths coexist.
- Real-markdown integration: guide with 4 code refs (one with line
number) produces the right set of paths.
- Doesn't match URL-like strings (word-boundary behavior).
Tests (test/incremental-chunking.test.ts, 3 cases):
- Identical content re-import skips entirely (content_hash match).
- Editing ONE function in a 3-function file preserves the other two
chunks verbatim (same chunk_text in DB). Verifies the cache-hit
path actually works end-to-end on PGLite.
- Fresh-file import embeds all chunks (nothing to reuse).
All 189 unit tests pass on PGLite.
* feat: v0.19.0 Layer 7 — code-def + code-refs CLI surfaces
Delivers the magical-moment commands for v0.19.0 code indexing. These
are the agent-facing endpoints that turn 'brain-first lookup' from a
markdown-only Iron Law into something that covers code too.
gbrain code-def <symbol>:
- Queries content_chunks.symbol_name = $1 AND page_kind = 'code' AND
symbol_type IN (function, class, interface, type, enum, struct,
trait, module, contract, export statement).
- Orders by symbol_type rank (function first, then class, etc.) then
page slug then line number — deterministic across runs.
- --lang <language> filter narrows to a single language.
- --limit N caps results (default 20).
- Returns Array<{ slug, file, language, symbol_type, start_line,
end_line, snippet }> — the 7-field shape the agent persona needs.
gbrain code-refs <symbol>:
- Bypasses the standard searchKeyword path, which uses DISTINCT ON
(slug) to collapse results to one chunk per page. That collapse is
right for markdown search but wrong for code-refs — a single file
typically has many usage sites, each interesting to the agent.
- Direct ILIKE scan over content_chunks + JOIN pages WHERE page_kind
= 'code'. Word-boundary precision is a follow-up (would need
tsvector or regex); for v0.19.0 the substring heuristic is good
enough because symbol names are distinctive by design.
- Same --lang / --limit / --json flag surface as code-def.
- Returns Array<{ slug, file, language, symbol_name, symbol_type,
start_line, end_line, snippet }> — 8 fields (code-def + the
containing symbol_name).
Agent-DX doctrine (from DX review):
- Auto-JSON on pipe: both commands emit JSON when stdout is not a
TTY (gh-CLI convention). Explicit --json forces JSON on TTY;
--no-json forces human output even when piped.
- Structured error envelope: missing symbol argument returns
{ class: 'UsageError', code: '..._requires_symbol', hint: '...' }
serialized as JSON in non-TTY mode, plain message in TTY.
Catch-all DB error path uses serializeError() — no raw stack
traces leak to the agent.
Tests — test/code-def-refs.test.ts (10 cases):
- Seeds a fixture repo (two TS files with deliberately large symbols
to stay independent under small-sibling merging).
- findCodeDef:
- Resolves interface + function by name to the right file.
- Empty-symbol query returns [].
- Language filter narrows to typescript; python returns [].
- findCodeRefs:
- Finds multiple usage sites across files (both src/engine.ts
and src/sync.ts appear when searching for BrainEngine — this
is the DISTINCT ON bypass working).
- Deterministic ordering by slug + line number.
- Unknown symbol returns [].
- --limit caps result count.
- Snippets are <= 500 chars (the agent doesn't get flooded).
CLI wiring:
- Added 'code-def', 'code-refs' to CLI_ONLY.
- New switch cases in handleCliOnly call runCodeDef / runCodeRefs.
- Help text gains a CODE INDEXING (v0.19.0) section.
All 199 unit tests pass.
Deferred from Layer 7 per the cathedral plan:
- sync --all cost preview with TTY detection — requires folding the
tokenizer into the sync path. Pushed to a follow-up.
- query --lang filter — requires changes to src/core/search/*.ts.
Pushed to a follow-up.
* feat: v0.19.0 Layer 8 — BrainBench code category (E2E)
Retrieval-quality gate for v0.19.0 code indexing. Seeds a ~25-file
fictional corpus across 5 languages (TS, Python, Go, Rust, Java),
imports each via importCodeFile, and asserts code-def + code-refs
produce the expected shape. Runs against PGLite in-memory so no
OpenAI key or external Postgres is needed; reproducible on CI with
just Bun.
What the E2E covers:
- Corpus seeded: 25+ code pages, all page_kind='code'.
- code-def finds AuthService across multiple languages (≥2 of
TS/Rust/Java).
- code-def --lang typescript filters precisely (P@5=1.0 for
CacheService + typescript).
- code-refs surfaces multiple usage sites across files (the
DISTINCT ON bypass working in practice).
- code-refs over the shared "start" method across 5 languages
produces ≥3 language hits (ranking stability).
- Magical-moment assertion: code-refs completes in <500ms on a
25-file corpus (budget is 100ms; 500ms pad absorbs CI variance).
- MRR sanity: top result for exact symbol is the defining file.
- Edge cases: non-existent symbol returns [], not error. Language
filter with zero matches returns []. Re-import is idempotent.
Chunker retune:
- Small-sibling merge threshold dropped from 40% to 15% of
chunkSizeTokens. The 40% figure was collapsing 3-method classes
into 'merged' chunks, killing symbol_name lookups for the entire
class. 15% matches the original intent: merge truly tiny
declarations (const X = 1; import ... from ...;) while leaving
substantive symbols (functions, classes) independent. Verified
by the BrainBench test — AuthService is now its own chunk with
symbol_name='AuthService', so findCodeDef('AuthService') resolves.
- Unit test updated: 10 consts with a generous chunkSizeTokens=1000
still exercise the merge path.
Total v0.19.0 unit + E2E coverage: 91 tests across 9 new test
files, 357 assertions, all green.
* feat: v0.19.0 Layer 9 — release: CHANGELOG + migration + docs
Closes out the v0.19.0 cathedral. Total shipped across 10 layers:
- 91 new unit + E2E tests (9 new files, 357 assertions, all green)
- 2 schema migrations (v25 pages.page_kind + v26 content_chunks code metadata)
- 4 new CLI surfaces (repos [alias] + code-def + code-refs +
sources passthrough)
- 1 new core module (src/core/errors.ts)
- 36 tree-sitter grammar WASMs embedded via Bun --compile
- 1 CI guard preventing silent-chunker regression
- Wintermute's multi-repo replaced with v0.18.0 sources backend
CHANGELOG.md — release-summary section in the GStack/Garry voice per
CLAUDE.md "Release-summary template": bold two-line headline + lead
paragraph + "The numbers that matter" table + "What this means for
builders" + itemized changes + "To take advantage of v0.19.0" block.
No em dashes, no AI vocabulary, no banned phrases. Numbers are from
the v0.19.0 test-fixture benchmarks.
CLAUDE.md — four new file entries in the Key files section
(src/core/chunkers/ annotated with v0.19.0 additions, src/core/errors.ts,
src/assets/wasm/, src/commands/code-def.ts + code-refs.ts).
skills/migrations/v0.19.0.md — agent-readable migration walkthrough
per the v0.11.0 convention. Tells the agent what to do after
`gbrain upgrade` runs the orchestrator: verify schema v26, register a
code source via `gbrain sources add`, run `sync --source <id>`,
confirm `gbrain code-def` / `code-refs` both work. Notes the deprecated
`gbrain repos` alias for scripts that used Wintermute's baseline.
Flagged in pending-host-work.jsonl per the v0.11.0 convention so
headless agents surface the prompt.
VERSION — 0.18.2 → 0.19.0.
All 91 v0.19.0 tests + the CI guard pass.
* docs: v0.19.0 — add 4 deferred follow-ups to TODOS.md
Lands the four items the v0.19.0 cathedral explicitly scoped out but
that the /plan-ceo-review + /plan-devex-review + /plan-eng-review chain
identified as genuine follow-ups rather than abandoned ideas.
Items added under a new 'code-indexing (v0.19.0 follow-ups)' section:
- P1 — sync --all cost preview with TTY detection. Closes DX fix #1
from the /plan-devex-review pass: the agent persona can't respond
to stdin prompts. Non-TTY path must emit a parseable
ConfirmationRequired envelope; TTY path uses [y/N]. File refs:
src/commands/sync.ts:590, src/core/chunkers/code.ts estimateTokens,
src/core/errors.ts buildError.
- P2 — query --lang filter through src/core/search/*.ts. Column
ships in v0.19.0 (migration v26 + partial index); the query path
just needs to respect it. Keeps ranking honest when the user
knows the language. File refs: src/core/search/, pglite-engine
searchKeyword, test/e2e/code-indexing.test.ts language-filter
pattern.
- P2 — E3 markdown code-fence extraction. After parseMarkdown,
iterate marked's lexer tokens for { type: 'code', lang, text }
and chunk each through chunkCodeText with chunk_source='fenced_code'.
~40% of gbrain's brain is guides with substantial inline code —
this lands those fences as first-class TS/Python/Go chunks in
search instead of treating them as prose.
- P2 — A3 reverse-scan backfill for doc↔impl. Companion piece to
E1. Markdown-first → code-later import order currently loses edges
because addLink's JOIN drops them when the code page doesn't exist
yet. A3 makes importCodeFile scan existing markdown for
references to the new code path and backfill edges both
directions. Trade-off: per-file scan is expensive on first sync;
batch 'gbrain reconcile-links' is an alternative shape.
Each entry follows the CLAUDE.md TODOS format: What/Why/Pros/Cons/
Context with exact file refs/line numbers/Effort (S/M/L + human vs
CC)/Depends on. All four are purely additive on top of v0.19.0 —
nothing blocks.
* fix: pre-existing test infrastructure + typecheck drift
Three pre-existing conditions surfaced when running the full suite and
blocked a clean CI floor for Cathedral II work:
1. `bun run test` default 5s hook timeout fails under load. PGLite WASM
init can exceed 5s when many test files spin up instances in parallel.
The bunfig.toml `timeout = 60_000` key is honored by `bun test` but
does not propagate to beforeEach/afterEach hooks when `bun test` runs
behind `bun run typecheck` in the CI chain. Pass `--timeout=60000`
explicitly on the command line, where it covers both per-test and
per-hook timeouts.
Before: 2136 pass / 30 fail (on-branch baseline)
After: 2272 pass / 0 fail
All 30 failures were `beforeEach/afterEach hook timed out for this
test` → `TypeError: undefined is not an object (evaluating
'engine.disconnect')` — i.e. the hook never finished connecting
PGLite, so the engine variable was never assigned, so afterEach
tripped on `engine.disconnect()`. The new timeout gives PGLite
WASM init enough headroom under concurrent load.
2. `test/repos-alias.test.ts` references the deliberately-deleted
`src/core/multi-repo.ts` via a dynamic import inside a try/catch
(the test asserts the module is no longer importable at runtime).
TS 5.x module resolution flags this at typecheck time even inside
try/catch. Build the path at runtime (`'../src/core/' +
'multi-repo.ts'`) so TS's compile-time module resolution doesn't
fail on a path the test is EXPLICITLY verifying doesn't resolve.
3. `llms-full.txt` drifted from `bun run build:llms` output (earlier
CLAUDE.md updates in v0.19.0 never regenerated). `bun run build:llms`
now produces matching output.
Zero behavior changes to production code. Test infrastructure only.
* feat: v0.20.0 Cathedral II Layer 1 — Foundation schema migration
Layer 1 of 14 for the v0.20.0 "best code search in the world" cathedral.
Ships all Cathedral II DDL atomically so downstream layers have the
columns + tables + trigger they depend on. Schema-only; no consumer
behavior changes until Layer 5 (A1 edge extractor).
Reordered to Layer 1 after codex second-pass review (SP-4): previously
Layer 0b (chunk-grain FTS trigger) referenced columns added in the
former Layer 3 (Foundation), breaking bisectability. All schema DDL
now lands first; every subsequent layer's prerequisites exist.
### What this migration adds (one idempotent v27 transaction)
1. `content_chunks` gains 4 new columns:
- `parent_symbol_path TEXT[]` — scope chain for nested symbols (A3)
- `doc_comment TEXT` — extracted JSDoc/docstring (A4)
- `symbol_name_qualified TEXT` — 'Admin::UsersController#render' (A1)
- `search_vector TSVECTOR` — chunk-grain FTS (Layer 1b consumer)
All nullable; markdown chunks leave them NULL.
2. `sources.chunker_version TEXT` (SP-1 gate). Layer 10 will check this
against CURRENT_CHUNKER_VERSION and force a full sync walk on
mismatch, bypassing the git-HEAD up_to_date early-return that would
otherwise make a bare CHUNKER_VERSION bump a silent no-op.
3. `code_edges_chunk` — resolved call-graph + reference edges.
- `from_chunk_id` + `to_chunk_id` with FK CASCADE from content_chunks
- UNIQUE (from_chunk_id, to_chunk_id, edge_type) holds idempotency
- `source_id TEXT` matches `sources.id` actual type (codex F4 caught
the prior UUID typo)
- source scoping enforced in resolution logic, not the key, because
from_chunk_id → pages.source_id already determines it
4. `code_edges_symbol` — unresolved refs. Target symbol known by
qualified name; defining chunk not seen yet. Rows UNION with
code_edges_chunk on read (codex 1.3b); no promotion step (SP-7).
5. `update_chunk_search_vector` trigger — BEFORE INSERT/UPDATE OF
(chunk_text, doc_comment, symbol_name_qualified). Weights
doc_comment and symbol_name_qualified at 'A', chunk_text at 'B'.
Natural-language queries rank doc-comment hits above body text
(A4 intent, delivered via the trigger from day one even though
Layer 5 populates the doc_comment column).
### Engine interface + types
- `BrainEngine` gains 6 new methods for code edges, all stubbed in
both engines with explicit NotImplemented errors pointing at the
layer that will fill them (5, 7, or 1b):
addCodeEdges, deleteCodeEdgesForChunks, getCallersOf,
getCalleesOf, getEdgesByChunk, searchKeywordChunks
- `CodeEdgeInput`, `CodeEdgeResult` types added to src/core/types.ts
- `SearchOpts` extended with Cathedral II fields: language, symbolKind,
nearSymbol, walkDepth, sourceId (all optional; consumers wire in
Layer 5/7/10)
- `ChunkInput` extended with: parent_symbol_path, doc_comment,
symbol_name_qualified (populated by importCodeFile in Layer 5/6)
- `Chunk` read shape mirrors the added columns as optional fields
- `chunk_source` union widens to include 'fenced_code' for D2 fence
extraction (Layer 6 consumer)
### Tests
`test/migrations-v0_20_0.test.ts` — 17 structural assertions against
the v27 migration registry. Covers every column + table + index + the
trigger weight shape. E2E migration-application coverage lands in
`test/e2e/cathedral-ii.test.ts` alongside Layer 5.
### Status
- CEO + Eng + 2 codex passes CLEARED (see docs/designs/CODE_CATHEDRAL_II.md)
- 16 cross-model findings absorbed (7 codex pass 1 + 6 codex pass 2
+ 3 eng review)
- 13 more layers to go (0a → 14); see plan for full sequencing.
* feat: v0.20.0 Cathedral II Layer 2 (1a) — file-classifier widening + SP-5 slug dispatch
Codex F1: `sync.ts:35` v0.19.0 classified only 9 extensions as code.
Rust/Java/C#/C++/Swift/Kotlin/etc. never reached the chunker on a
normal repo sync, making v0.19.0's "29 languages" claim aspirational
on the read path. Layer 2 widens the classifier so every language the
chunker knows (~35 extensions) actually reaches it during sync.
### Changes
1. `src/core/sync.ts` CODE_EXTENSIONS widened from 9 to 35 extensions,
matching the chunker's detectCodeLanguage coverage: adds .rs, .java,
.cs, .cpp/.cc/.cxx/.hpp/.hxx/.hh, .c/.h, .php, .swift, .kt/.kts,
.scala/.sc, .lua, .ex/.exs, .elm, .ml/.mli, .dart, .zig, .sol,
.sh/.bash, .css, .html/.htm, .vue, .json, .yaml/.yml, .toml,
.mts/.cts.
2. `src/core/sync.ts` adds `resolveSlugForPath(path)` — SP-5 fix.
Before Cathedral II, sync delete/rename paths called
`pathToSlug(path)` with default pageKind='markdown'. For the 9-ext
classifier this was mostly fine (code files rare), but widening to
35 exts means Rust/Java/Ruby/etc. deletes and renames would mismatch
on slug shape (pathToSlug markdown-style vs slugifyCodePath
code-style). resolveSlugForPath dispatches on isCodeFilePath so
delete/rename always hit the right page. Used in `src/commands/sync.ts`
at the three slug-resolution sites (un-syncable delete, batch delete,
rename from/to).
3. `src/core/chunkers/code.ts` adds `setLanguageFallback(fn)` +
optional `content` arg to `detectCodeLanguage(path, content?)`.
Pre-wires the Magika fallback hook that Layer 9 (B2) will consume
for extension-less files (Dockerfile, Makefile, shell shebangs).
Null default → no behavior change today; Layer 9 sets it at bootstrap.
Fallback throws are swallowed (recursive chunker is always an
acceptable degradation).
### Tests
- `test/sync-classifier-widening.test.ts` — 20 cases covering the full
widened extension set, resolveSlugForPath dispatch, and the Magika
fallback hook contract (including throw-swallow and null-pass-through).
- `test/sync-strategy.test.ts` updated: `.json` is no longer rejected
(the chunker's language map includes JSON for structured-data
chunking). Test clarifies Cathedral II semantics; adds .svg + .zip
as non-code examples.
### CI result
2292 pass / 0 fail via `bun run test`, 388s wall time.
* feat: v0.20.0 Cathedral II Layer 3 (1b) — chunk-grain FTS with page-grain wrap
Codex F2 caught that v0.19.0's searchKeyword ranked via pages.search_vector,
so doc-comment content living on a chunk couldn't influence ranking and A2
two-pass retrieval had no way to find the best matching chunk. Layer 3
moves the FTS primitive to content_chunks.search_vector (the column +
trigger added in Layer 1/v27), dedups-to-best-chunk-per-page on return
so every external caller still sees the v0.19.0 page-grain contract
(SP-6), and exposes searchKeywordChunks as the raw chunk-grain primitive
A2 two-pass will consume (Layer 7).
### Backfill migration v28
Layer 1's trigger only fires on INSERT/UPDATE — rows inserted before v27
applied had NULL search_vector. v28 backfills every existing chunk with
the same weight shape the trigger uses (doc_comment + symbol_name_qualified
at weight A, chunk_text at B). Idempotent via `WHERE search_vector IS NULL`;
re-runs pick up only remaining NULL rows. ~2-3s on a 20K-chunk brain.
### searchKeyword rewrite (both engines)
CTE chain: rank chunks by cc.search_vector → DISTINCT ON (slug) picks
best chunk per page → order by score → limit. External shape identical
to v0.19.0: one row per matched page, score comes from the best chunk
on that page, chunk metadata attached. Zero breaking changes for
backlinks counting, enrichment-service.countMentions, list_pages, etc.
Inner fetch limit is 3x the requested page limit so dedup has enough
chunks to produce N distinct pages (a co-occurring-term cluster in one
page can't eat the result set).
Postgres keeps the SET LOCAL statement_timeout='8s' from v0.12.3 search
timeout scoping. PGLite gets the same CTE shape minus the transaction-
scoped GUC (PGLite has no pool).
### searchKeywordChunks (new internal primitive)
Same chunk-grain ranking WITHOUT dedup. Returns raw top-N chunks by
FTS score regardless of page. Used by A2 two-pass retrieval (Layer 7)
as its anchor-discovery primitive — two-pass wants top chunks, not
best-per-page. Most callers should prefer searchKeyword.
### Tests
- test/chunk-grain-fts.test.ts: 11 cases covering migration v28 shape,
page-grain external contract (dedup preserves invariants), chunk-grain
primitive (no dedup, score-ordered), and the doc-comment weight-A
precedence over body weight-B — the A4 ranking win validated today
even though Layer 5 is what populates doc_comment from AST.
- test/pglite-engine.test.ts existing "tsvector trigger populates
search_vector on insert" updated: v0.19.0 searched pages.search_vector
(built from title + compiled_truth) so two-word queries matching
non-chunk text worked. Cathedral II ranks chunks only — test updated
to search 'AI agents' which is in the chunk_text directly.
- test/migrations-v0_20_0.test.ts "v27 is highest" relaxed to
"v27 is the foundation migration; max >= 27" so later layers can
land migrations without breaking this assertion.
### CI result
2553 tests / 0 fail via `bun test --timeout=60000`, 422s wall time.
* feat: v0.20.0 Cathedral II Layer 4 (B1) — language manifest foundation
Consolidate the 29-way GRAMMAR_PATHS + parallel DISPLAY_LANG record into
a single LANGUAGE_MANIFEST keyed on SupportedCodeLanguage. Each entry is
a LanguageEntry with { displayName, embeddedPath?, lazyLoader? }.
### Why this matters for Cathedral II
Before: adding a language meant editing two maps (path + display name)
AND adding a new `import G_X from ...` at the top, for every new lang.
After: one manifest entry + one `with { type: 'file' }` import (embedded)
or one registerLanguage() call at boot (lazy). loadLanguage() consults
the manifest uniformly — it doesn't know or care whether a grammar is
embedded in the compiled binary or resolved from node_modules at runtime.
### The 3 extension points
- `embeddedPath` — Bun `with { type: 'file' }` asset. Ships with
`bun --compile` output; already in place for the 29 core grammars.
- `lazyLoader` — async function returning path or Uint8Array. Used at
first reference, then cached in `languageCache` like embedded grammars.
Forward-compat for v0.20.x+ full tree-sitter-wasms (~136 more langs).
- `registerLanguage(lang, entry)` / `unregisterLanguage(lang)` /
`listRegisteredLanguages()` — runtime registration hook. Layer 9
(B2 Magika) will wire detection for extensionless files through
this API. Dynamic registrations win over core manifest on conflict
so hot-fix overrides during a session work without restart.
### Behavior guarantees preserved
- All 29 v0.19.0 core grammars continue to ship embedded — no binary-size
growth, no runtime network dependency for the core set.
- `detectCodeLanguage` untouched; its output key still maps 1:1 through
LANGUAGE_MANIFEST.
- `displayLang()` now derived from the manifest. Chunk headers read
"[Python]" / "[TypeScript]" / "[Ruby]" just as before — one source of
truth, manifest-derived.
### Tests (test/language-manifest.test.ts, 8 cases)
- Manifest covers all 29 v0.19.0 languages (typescript/tsx/js/py/rb/go/
rust/java/c_sharp/cpp/c/php/swift/kotlin/scala/lua/elixir/elm/ocaml/
dart/zig/solidity/bash/css/html/vue/json/yaml/toml).
- registerLanguage does NOT invoke the lazy loader at registration time
(proves the loader fires at most on first chunkCodeText() call).
- Dynamic registrations override core manifest entries (hot-fix path).
- unregisterLanguage removes a dynamic entry and clears its parser cache.
- chunkCodeText still loads core grammars (TypeScript / Python / Ruby)
end-to-end; chunk headers use the manifest displayName ("[Python]",
not "[python]").
### What's NOT shipped here
Adding the additional ~136 languages from tree-sitter-wasms is
deliberate v0.20.x+ follow-up work. The manifest infrastructure is in
place; expanding coverage is now a data-only PR (one entry per language).
### CI result
2561 tests / 0 fail via `bun test --timeout=60000`, 425s wall time.
* feat: v0.20.0 Cathedral II Layer 8 D1 — sync --all cost preview + ConfirmationRequired envelope
Closes the v0.19.0 DX review's #1 pain point: "first sync surprise bill."
Before Cathedral II, `gbrain sync --all` on a fresh multi-source brain
could spin up tens of thousands of OpenAI embedding calls before anyone
saw a cost number. Agent callers (OpenClaw, Hermes, etc.) had no way
to gate the operation behind a spend check.
### Behavior
Before `sync --all` touches a single source, walk the working trees of
every registered source with `local_path`, sum tokens per file via the
same cl100k_base tokenizer text-embedding-3-large actually uses, and
compute a USD estimate. Gate on that:
- **TTY + !--json + !--yes** → interactive `[y/N]` prompt.
- **non-TTY OR --json OR piped** → emit `ConfirmationRequired` envelope
to stdout via the v0.18 `errorFor` builder, exit code 2. Reserves
exit 1 for runtime errors so agent callers can distinguish
"awaiting user call" from "something crashed."
- **--yes** → skip prompt entirely. Agent/CI path.
- **--dry-run** → print preview, exit 0 without syncing.
- **--no-embed** → skip the cost gate entirely (user already opted out
of OpenAI spend; they'll run `embed --stale` later).
### Preview shape
One stderr line or one JSON payload:
sync --all preview: <N> files across <M> source(s),
~<T> tokens, est. $<X> on text-embedding-3-large.
Conservative overestimate: full working-tree content, not just the
incremental diff. A source never embedded before WILL embed everything
on first sync; already-synced sources with small diffs get a ceiling,
not a floor. False-high bias is intentional — users never get
surprised by MORE cost than the preview claimed.
### Files
- `src/core/chunkers/code.ts`: `estimateTokens` now exported (was
module-private). Same cl100k_base tokenizer, just a public symbol.
- `src/core/embedding.ts`: add `EMBEDDING_COST_PER_1K_TOKENS = 0.00013`
+ `estimateEmbeddingCostUsd(tokens)`. Single source of truth for
cost math; every cost-preview surface reads this constant, so a
pricing change is a one-line edit.
- `src/commands/sync.ts`:
- new `estimateSyncAllCost(sources)` helper walks trees, sums
tokens per active source, returns breakdown.
- new `walkSyncableFiles(repo, cb, strategy)` recursive walker.
Honors the same `isSyncable` rules as the real sync so preview
and execution agree on scope. Skips hidden dirs, node_modules,
ops/, and files over 5MB. Best-effort file-read errors don't
block the preview.
- new `promptYesNo(question)` readline wrapper — resolves false
on non-'y' answer OR EOF.
- `--yes` and `--json` flags parsed at sync argv layer.
- cost preview runs before the per-source sync loop on `--all`,
gates via the TTY / --json / --yes / --dry-run matrix above.
### Tests
`test/sync-cost-preview.test.ts` (6 cases):
- EMBEDDING_COST_PER_1K_TOKENS pinned to $0.00013.
- `estimateEmbeddingCostUsd` scales linearly across 0 → 1M tokens.
- `estimateTokens` round-trips (empty → 0, short → <10, 100x text → >50x).
### CI result
2567 tests / 0 fail via `bun test --timeout=60000`, 424s wall time.
* feat: v0.20.0 Cathedral II Layer 8 D2 — markdown fence extraction
~40% of gbrain's brain is docs + guides + architecture notes with
substantial inline code. In v0.19.0 those fenced code blocks chunked as
prose, so querying "how do we handle errors in TypeScript" ranked
paragraphs ABOUT the import above the actual import example. D2 walks
the marked lexer tokens, extracts each recognized code fence, and
persists them as extra chunks on the parent markdown page with
`chunk_source='fenced_code'` and full code-metadata (language,
symbol_name, symbol_type, start/end line).
### Behavior
In `importFromContent`, after `parseMarkdown` returns compiled_truth,
we additionally run the text through `marked.lexer()` and walk for
`{ type: 'code', lang, text }` tokens. For each:
- Map the fence language tag (`ts`/`typescript`/`js`/...) to a
pseudo-path (`fence.ts`/`fence.js`/...) so `detectCodeLanguage`
picks the right grammar.
- Call `chunkCodeText(text, pseudoPath)` — one or more code chunks
depending on fence size. Tree-sitter-aware chunking means a big
TS fence splits at function boundaries, not character count.
- Persist each chunk with `chunk_source='fenced_code'`. Extends the
existing chunk_source enum; schema allows it via the TEXT column.
### Fence-bomb DOS guard
`MAX_FENCES_PER_PAGE = 100` by default, overridable via
`GBRAIN_MAX_FENCES_PER_PAGE` env var. A malicious markdown page with
10K ```ts blocks could otherwise force 10K embedding API calls.
Beyond the cap, remaining fences skip with a one-line console warn
so operators can see the event.
### Per-fence error isolation
Each fence runs through its own try/catch. One malformed fence (e.g.
marked lexer choking on edge-case markdown) doesn't abort the whole
page import — the other fences + the prose chunks from
compiled_truth all still land.
### Recognized fence tags (29 languages + 7 aliases)
ts/typescript, tsx, js/javascript, jsx, py/python, rb/ruby,
go/golang, rs/rust, java, c#/cs/csharp, cpp/c++, c, php, swift,
kt/kotlin, scala, lua, ex/elixir, elm, ml/ocaml, dart, zig,
sol/solidity, sh/bash/shell/zsh, css, html, vue, json, yaml/yml,
toml.
Unknown tag → skipped (no synthetic chunk, no crash). Missing tag
(```\n...\n```) → skipped. Empty body → skipped.
### Collateral fix
`rowToChunk` in src/core/utils.ts now maps the code-chunk metadata
columns (language, symbol_name, symbol_type, start_line, end_line)
+ the v0.20.0 Cathedral II additions (parent_symbol_path,
doc_comment, symbol_name_qualified) out of the DB. Pre-Cathedral II
the code columns were written via upsertChunks but never read back
— caught by the new fence test assertions.
### Tests (test/fence-extraction.test.ts, 7 cases)
- TS fence → language='typescript' chunk
- Python fence → language='python', chunk_text contains def
- Ruby fence → language='ruby'
- Unknown tag (```mermaid, ```unknown-xyz) → no fenced_code chunks
- Missing tag → no fenced_code chunks
- 3 fences on one page, mix of langs → 3+ fenced_code chunks
- Empty fence body → no chunks
### CI result
2574 tests / 0 fail via `bun test --timeout=60000`, 434s wall time.
* feat: v0.20.0 Cathedral II Layer 8 D3 — reconcile-links batch command
Closes the v0.19.0 Layer 6 doc↔impl order-dependency: when a markdown
guide imports BEFORE the code it cites (common — docs land first, code
sync runs second), the Layer 6 E1 forward-scan calls addLink but its
inner JOIN silently drops the edge because the code page doesn't exist
yet. The guide and the code eventually both exist in the brain, but
the edge never materialized.
### New CLI surface
gbrain reconcile-links [--dry-run] [--json]
Walks every markdown page, re-runs `extractCodeRefs` on
compiled_truth+timeline, and calls addLink(md, code, ..., 'documents')
+ reverse for each hit. ON CONFLICT DO NOTHING at the links table
makes the operation idempotent — existing edges stay, new edges land.
### Per-lang coverage via extractCodeRefs
Inherits the regex from `src/core/link-extraction.ts` which already
recognizes code paths for 29 extensions (ts/tsx/js/py/rb/go/rust/java/
c#/cpp/c/php/swift/kotlin/scala/lua/elixir/elm/ocaml/dart/zig/sol/sh/
css/html/vue/json/yaml/toml). Fence-extraction (D2) and classifier-
widening (Layer 2) keep this in sync with the chunker's actual reach.
### Why batch over per-import reverse-scan
Codex's two-pass review flagged per-import reverse-scan as O(N)
ILIKE/JOIN queries per code file imported — on a 47K-page brain first-
syncing 5K code files that's 5K ILIKE scans. A user-triggered batch
run on an already-synced brain is one walk, slug-indexed via addLink's
existing lookup. Same correctness, much faster.
### Behavior
- Dry-run: counts refs, attempts = 0, writes nothing.
- auto_link=false in config: returns status='auto_link_disabled' +
no-op. Users who disabled auto-linking on put_page don't want
reconcile-links silently re-populating edges either.
- Missing code target: counted as `edgesTargetsMissing`, not thrown.
The ref exists in the guide, but the code page hasn't been synced
yet. Re-run after the next code sync to materialize.
- Progress reporter: `reconcile_links.scan` phase, one tick per
markdown page, with rolling summary `guides/foo (+N refs)` per tick.
### Tests (test/reconcile-links.test.ts, 6 cases)
- Extracts code refs and creates bidirectional edges (guide→code +
code→guide).
- Idempotent: second run inserts zero new edges.
- Dry-run reports counts without writing.
- Markdown page with no code refs is a no-op.
- Respects auto_link=false.
- Missing code target is counted, not thrown.
### CI result
2580 tests / 0 fail via `bun test --timeout=60000`, 432s wall time.
* feat: v0.20.0 Cathedral II Layer 12 — CHUNKER_VERSION 3→4 + SP-1 gate
Codex's second-pass review caught that bumping CHUNKER_VERSION alone is a
silent no-op on an unchanged repo: performSync short-circuits at `up_to_date`
before reaching importCodeFile's content_hash check. Layer 12 adds a
sources.chunker_version gate that forces a full re-walk when the version
mismatches, regardless of git HEAD equality.
- CHUNKER_VERSION 3 → 4 (src/core/chunkers/code.ts:99), folded into
content_hash via v0.19.0 Layer 5 wiring — any bump forces clean re-chunks.
- src/commands/sync.ts: readChunkerVersion/writeChunkerVersion helpers;
version-mismatch gate runs BEFORE the up_to_date early-return and forces
a full walk; writeChunkerVersion called after every last_commit anchor.
- test/chunker-version-gate.test.ts: 3 pinning tests (constant value,
import stability, v27 migration shape).
- test/chunkers/code.test.ts: update v0.19.0 CHUNKER_VERSION=3 assertion
to Cathedral II v0.20.0 CHUNKER_VERSION=4.
Full CI: 2333 pass / 250 skip / 0 fail / 6155 expect() / 408s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: v0.20.0 Cathedral II Layer 13 (E2) — reindex-code + migration orchestrator
Ships the user-facing explicit-backfill path. v0.19.0 → v0.20.0 brains get
CHUNKER_VERSION 3→4 rolled over automatically via Layer 12's gate on next
sync. Users who want the benefits NOW (before their next sync) run
`gbrain reindex-code --yes`.
- New src/commands/reindex-code.ts. runReindexCode(engine, opts) walks code
pages from the DB in batches of 100 (Finding 4.4 OOM protection), reads
compiled_truth + frontmatter.file, re-runs importCodeFile. --dry-run
reports cost + token count without importing. --force bypasses
importCodeFile's content_hash early-return. --source filters to one
sources row. Pages without frontmatter.file fail cleanly (counted, not
thrown). runReindexCodeCli parses argv, wires the D1 cost-preview gate
(TTY prompt or ConfirmationRequired envelope for non-TTY/JSON), delegates.
- src/core/import-file.ts: importCodeFile gains opts.force flag. When
true, skips the content_hash === hash early-return so a paranoid full
reindex always re-chunks + re-embeds even when content hasn't changed.
- src/cli.ts: register 'reindex-code' case + CLI_ONLY entry.
- src/commands/migrations/v0_20_0.ts: orchestrator with 3 phases
(schema → backfill_prompt → verify). Phase B prints the two backfill
choices directly (automatic via sync vs immediate via reindex-code).
Follows v0.12.2/v0.18.1 idempotent-resumable pattern.
- src/commands/migrations/index.ts: registers v0_20_0 after v0_18_1.
- skills/migrations/v0.20.0.md: agent-facing post-upgrade instructions.
- test/reindex-code.test.ts: 5 cases (count, dry-run, walk+failures,
empty brain, batch pagination).
- test/migration-orchestrator-v0_20_0.test.ts: 5 cases (registry wiring,
feature-pitch content, __testing exports, dry-run skips, is-latest).
- test/apply-migrations.test.ts: extend skippedFuture pins with 0.20.0.
Full CI: 2343 pass / 250 skip / 0 fail / 6193 expect() / 426s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: v0.20.0 Cathedral II Layer 10 partial (C1 + C2) — query --lang / --symbol-kind
Ships the cheap half of the C tier: language + symbol-kind filters on
hybrid search. The content_chunks.language and content_chunks.symbol_type
columns have existed since v0.19.0 Layer 5 (code chunker populates both);
Layer 10 exposes them as filter flags on the 'query' operation.
The expensive half (C3 --near-symbol, C4 code-callers, C5 code-callees) is
blocked on Layer 5 A1 edge extractor — those need the code_edges_chunk +
code_edges_symbol tables populated. They ship in a follow-up.
- src/core/pglite-engine.ts: searchKeyword / searchKeywordChunks /
searchVector all accept opts.language + opts.symbolKind. Filters added
via parameterized $N indices; unknown values return zero results
(no false positives).
- src/core/postgres-engine.ts: same three methods, same filters, threaded
through the postgres.js sql-fragment pattern. Honors SET LOCAL
statement_timeout discipline.
- src/core/search/hybrid.ts: threads opts.language + opts.symbolKind into
per-engine searchOpts so filters fire at SQL level (not post-filtered
in-memory).
- src/core/operations.ts: query op params gain lang + symbol_kind entries.
Handler maps them into hybridSearch opts.language / opts.symbolKind.
- src/cli.ts: updated --help CODE INDEXING section to list the new flags
+ reconcile-links + reindex-code commands.
- test/search-lang-symbol-kind.test.ts: 9 cases (no filter, lang-only,
symbolKind-only, combined AND, searchKeywordChunks variant, unknown
lang/kind return zero, operation schema check).
Full CI: 2352 pass / 250 skip / 0 fail / 6216 expect() / 432s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: v0.20.0 Cathedral II Layer 6 (A3) — parent-scope + nested-chunk emission
Ships the chunk-granularity change codex called out in the second-pass
review. Before Cathedral II, `export class BrainEngine { m1() {} m2() {} }`
emitted ONE chunk for the whole class. Retrieval returned the entire
class body for a symbol-specific query like "how does searchKeyword
work" — the agent had to re-read the whole thing. A3 extends the
chunker to emit each method as its own chunk carrying
`parentSymbolPath: ['BrainEngine']`, with a `(in BrainEngine)` suffix in
the header so the embedding captures scope context. The class-level
parent chunk still ships (slim body: declaration line + member digest)
so class-level queries still hit something.
Recursive expansion: Ruby `module Admin { class UsersController { def
render } }` emits 3 chunks — Admin (parent=[]), UsersController
(parent=[Admin]), render (parent=[Admin, UsersController]).
- src/core/chunkers/code.ts:
- CodeChunkMetadata gains `parentSymbolPath?: string[]`.
- NESTED_EMIT_CONFIG map per language (TS, TSX, JS, Python, Ruby,
Rust impl blocks, Java class/interface/record). Maps parent types
(class_declaration / class_definition / module / impl_item) to
child types (method / method_definition / function_definition /
singleton_method / constructor_declaration).
- findNestableParent unwraps TS export_statement to reach the inner
class_declaration — the export wrapper was a classic gotcha.
- emitNestedScoped: recursive, builds full parent-chain path, pushes
a slim scope-header chunk for each parent level + leaf chunks for
methods. Handles module → class → method chains.
- buildChunk emits "(in ClassName.method)" header suffix when
parentSymbolPath is non-empty.
- mergeSmallSiblings now bails on any file that has parent-scoped
chunks. Methods emitted by A3 are intentionally small and
individually addressable; merging them would erase the scope
context Layer 6 just established.
- src/core/import-file.ts: importCodeFile passes parent_symbol_path
from chunker metadata into ChunkInput so it lands in content_chunks.
- src/core/pglite-engine.ts + src/core/postgres-engine.ts: upsertChunks
extends the column list to persist parent_symbol_path (TEXT[]),
doc_comment (TEXT), symbol_name_qualified (TEXT). All three existed
as schema columns from Layer 1 but the writers weren't plumbed yet.
ON CONFLICT DO UPDATE includes all three so re-imports refresh
metadata correctly.
- test/parent-scope.test.ts: 9 cases covering TypeScript class method
expansion, Python class, Ruby module+class, top-level function
passthrough, and round-trip through upsertChunks to verify text[]
persistence.
Full CI: 2361 pass / 250 skip / 0 fail / 6270 expect() / 439s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: v0.20.0 Cathedral II Layer 5 (A1) — edge extractor + qualified names (8 langs)
The 10x leap. v0.19.0 shipped symbol-column filtering and could find "the
definition of X"; v0.20.0 Layer 5 captures who CALLS X. Walk the tree-sitter
tree during chunking, harvest call-site edges, persist to code_edges_symbol
with the callee's short-name as to_symbol_qualified. `getCallersOf("helper")`
now returns every call site, ready for Layer 7 two-pass retrieval to expand
into structural neighbors.
Scope: precision 80, recall 99. We don't try to resolve receiver types at
capture time (obj.method() stores "method", not "ObjClass.method"). That
receiver-type inference is a future optimization; the edges are captured,
which is the whole point. Cross-file resolution is also deferred — all
Layer 5 edges land unresolved in code_edges_symbol.
Per-language shipped: TypeScript, TSX, JavaScript, Python, Ruby, Go, Rust,
Java. ~85% of real brain code. Other languages chunk normally, edges just
empty.
- src/core/chunkers/qualified-names.ts (new): per-language delimiter
conventions. Ruby `Admin::UsersController#render` (instance) vs Python
`admin.users.UsersController.render` vs Rust `users::UsersController::render`.
Unknown languages dot-join as fallback (never drop).
- src/core/chunkers/edge-extractor.ts (new): iterative AST walk (no
recursion — tree-sitter trees can be deep, stack overflow risk on
generated code). Per-language CALL_CONFIG maps node types to callee
field names. extractCalleeName unwraps member_expression, scoped_identifier,
field_expression to reach the innermost identifier. findChunkForOffset
maps a byte offset to the innermost chunk for from_chunk_id resolution.
- src/core/chunkers/code.ts: CodeChunkMetadata gains
symbolNameQualified. buildChunk folds in qualified-name from parents +
name. New chunkCodeTextFull API returns (chunks, edges); chunkCodeText
stays as back-compat wrapper.
- src/core/import-file.ts: call chunkCodeTextFull, build ChunkInput list
with symbol_name_qualified, after upsertChunks run findChunkForOffset
to map call-site byte offsets to resolved chunk IDs, call
deleteCodeEdgesForChunks (codex SP-2 inbound invalidation) then
addCodeEdges. Edge persistence is best-effort — failure logs a warn
but does not fail the import.
- src/core/pglite-engine.ts + src/core/postgres-engine.ts: implement the
5 stub methods. addCodeEdges splits resolved vs unresolved by
to_chunk_id presence, inserts with ON CONFLICT DO NOTHING. getCallersOf
/ getCalleesOf UNION code_edges_chunk + code_edges_symbol (codex 1.3b:
no promotion, UNION-on-read forever). getEdgesByChunk honors direction
{in, out, both}. deleteCodeEdgesForChunks wipes both tables in both
directions (codex SP-2).
- test/qualified-names.test.ts: 9 cases (TS/Ruby instance method/Python/
Rust/Java/unknown-lang fallback).
- test/edge-extractor.test.ts: 11 cases (per-language call capture +
findChunkForOffset mapping + unknown-language empty-list).
- test/code-edges.test.ts: 7 cases (addCodeEdges insert + idempotency,
getCallersOf short-name match, resolved path, getEdgesByChunk
direction filters, deleteCodeEdgesForChunks both-direction wipe).
Full CI: 2391 pass / 250 skip / 0 fail / 6308 expect() / 449s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: v0.20.0 Cathedral II Layer 10 rest (C4 + C5) — code-callers / code-callees CLI
Exposes Layer 5's call-graph edges as user-facing agent commands. The
existing code-def / code-refs pair answers "where is X defined?" and
"where is X referenced?"; Layer 10 rest adds "who CALLS X?" and "what
does X CALL?" — the structural questions v0.19.0 couldn't answer.
Conventions follow the code-def / code-refs precedent:
- Auto-JSON on non-TTY (gh-CLI convention)
- StructuredAgentError envelope on usage / runtime failure
- Exit 2 on UsageError, exit 1 on runtime
- --all-sources to widen beyond the anchor's source; default source-scoped
- src/commands/code-callers.ts (new) — wraps engine.getCallersOf.
- src/commands/code-callees.ts (new) — wraps engine.getCalleesOf.
- src/cli.ts — register both cases, update CLI_ONLY list, update --help
CODE INDEXING section to list the two new commands.
- test/code-callers-cli.test.ts — 2 cases (module exports, callable).
The --near-symbol / --walk-depth flags on query ship with Layer 7
(A2 two-pass retrieval) in a follow-up layer commit.
Full CI: 2393 pass / 250 skip / 0 fail / 6310 expect() / 448s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: v0.20.0 Cathedral II Layer 7 (A2) — two-pass structural retrieval
The capstone of the retrieval-side upgrade. Layer 5 captured edges at
chunk time; Layer 7 uses them. Given a query like "how does
searchKeyword handle N+1", standard hybrid search returns the function
body; A2 expansion additionally surfaces:
- the 3 functions that call it (1-hop)
- the 2 functions it calls (1-hop)
- the anchor set's neighbors' neighbors (2-hop, optional)
All ranked together with 1/(1+hop) score decay. One walk. Code-aware
brain, not RAG-over-code.
Default OFF per codex F5. Activation:
- `--walk-depth N` (1 or 2) walks N hops from the anchor set.
- `--near-symbol <qualified-name>` adds chunks matching the symbol's
qualified name as extra anchors, enabling "expand around this
specific symbol" without a keyword query.
Caps (codex F5):
- depth capped at 2 (max blast radius).
- neighbor cap 50 per hop (high-fan-out protection: console.log has
100k callers and should not flood the result set).
- per-page dedup cap lifts from 2 → min(10, walkDepth × 5) when
walking — structural neighbors from the same class are the point.
- src/core/search/two-pass.ts (new): expandAnchors walks
code_edges_chunk + code_edges_symbol, hydrating unresolved edges by
matching symbol_name_qualified on lookup. hydrateChunks fetches
SearchResult rows for expanded chunk IDs.
- src/core/search/hybrid.ts: gate the two-pass step on opts.walkDepth
> 0 OR opts.nearSymbol set. Expansion runs before dedup so neighbors
survive; dedup cap widens when walking. Best-effort — expansion
failure falls back to base hybrid retrieval.
- src/core/operations.ts: query op params gain near_symbol (string) +
walk_depth (number). Handler threads both into hybridSearch opts.
- test/two-pass.test.ts: 8 cases (walkDepth 0/1/2/5-clamp, nearSymbol
anchoring, hydrateChunks round-trip, operation schema).
Full CI: 2401 pass / 250 skip / 0 fail / 6332 expect() / 449s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: v0.20.0 Cathedral II Layer 11 (E1) — BrainBench code sub-category tests
Pins the retrieval-quality behaviors Layer 5 and Layer 6 added, so any
accidental regression surfaces on CI rather than silently eroding search
quality.
Sub-categories:
- call_graph_recall — importCodeFile captures calls edges
end-to-end; getCallersOf + getCalleesOf round-trip through real
edge extraction; re-import idempotency via codex SP-2 per-chunk
invalidation.
- parent_scope_coverage — nested methods persist parent_symbol_path
through the upsertChunks path; qualified symbol names resolve
correctly for nested declarations.
doc_comment_matching is deferred: the chunk-grain FTS trigger from
Layer 1b already weights doc_comment 'A', but chunker doc_comment
extraction (A4 full implementation) is a follow-up. The column exists,
the ranking is ready — waiting on extraction.
type_signature_retrieval deferred with C6 to v0.20.1 per plan.
- test/cathedral-ii-brainbench.test.ts (new): 6 cases covering the
two sub-categories against real PGLite + importCodeFile.
Full CI: 2407 pass / 250 skip / 0 fail / 6345 expect() / 467s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: v0.20.0 Cathedral II Layer 14 — release (CHANGELOG + TODOS + version bump)
The capstone commit. Ships v0.20.0 — Code Cathedral II — with a full
release-summary in CHANGELOG.md covering the 13 layers that landed
(Layer 9 / Magika deferred to v0.20.1 per plan risk gate), migration
guidance under "To take advantage of v0.20.0", and itemized changes
grouped by layer with real numbers.
- VERSION: 0.19.0 → 0.20.0
- package.json: 0.19.0 → 0.20.0
- CHANGELOG.md: new [0.20.0] entry with release-summary (two-line
bold headline, lead paragraph, numbers-that-matter table with
before/after delta, per-language call-capture table, "what this
means for builders" closer), "To take advantage of v0.20.0"
section with verify commands + issue-reporting template, and the
full itemized changes section grouped by layer (1 / 2 / 3 / 4 /
5 / 6 / 7 / 8 / 10 / 11 / 12 / 13 / 9-deferred). Credits 2 codex
passes + eng + ceo reviews — 16 cross-model findings absorbed.
- TODOS.md: retire the 4 v0.19.0 follow-ups (all landed in v0.20.0
Layer 8 + Layer 10). Add 4 new Cathedral II follow-ups:
- B2 Magika (Layer 9 deferred)
- A4 full doc_comment extraction at chunk time
- C6 code-signature
- Cross-file edge resolution (Layer 5 precision upgrade)
Full CI: 2407 pass / 250 skip / 0 fail / 6345 expect() / 465s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(import-file): tolerate missing pages in doc↔impl linking
importCodeFile / importFromContent's E1 doc↔impl forward-link path was
calling tx.addLink() expecting the pre-v0.18 silent-no-op behavior on
missing pages. Master tightened addLink in postgres-engine.ts to throw
when either endpoint is missing — which is correct for explicit callers,
but the doc↔impl case is intentionally order-agnostic: a guide that
cites src/core/sync.ts can land before the code repo syncs (and vice
versa).
Result on CI: 21 E2E tests failed in test/e2e/mechanical.test.ts because
the fixture corpus has prose pages citing code paths the corpus doesn't
include, so each importFromContent threw "addLink failed: page X or Y
not found" and aborted before downstream assertions could run.
Fix: wrap each tx.addLink call (forward + reverse edge) in try/catch.
Match the existing pattern in src/commands/extract.ts:547 and
src/core/operations.ts:453,470 — both run try { addLink } catch { skip }
for exactly this reason. Missing edges land later via
`gbrain reconcile-links` (Layer 8 D3), which forward-scans every
markdown page and idempotently inserts the edges that resolve.
Comment refresh: the old comment ("addLink's inner SELECT naturally
drops edges to non-existent pages") was true pre-v0.18; updated to
reflect the current throwing behavior + the reconcile-links recovery
path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test/migrate): bump v8/v9 dedup-regression budget 5s → 90s
The v8 (links_dedup) + v9 (timeline_dedup_index) regression tests time
the FULL `runMigrations` chain from version 7 → LATEST_VERSION. Their
5s budget was sized when the chain ended at v8/v9 themselves and v8 +
the helper-btree-index O(n log n) work were the dominant cost.
Cathedral II added v27 (TSVECTOR column + GIN index + plpgsql trigger
compile + 2 new tables w/ FK CASCADE) and v28 (UPDATE backfill of
search_vector). On PGLite WASM in CI, the full v7 → v28 chain now
takes ~30-40s — schema-creation overhead, not v8/v9 dedup itself.
Locally the chain ran in 2.75s; CI's container cold-start hit 33s.
The original O(n²) regression v8 had would have taken MINUTES on 1000
duplicate rows (the original incident was multi-minute, not multi-tens-
of-seconds). Bumping the budget to 90s preserves the regression gate
("if v8 reverts to O(n²), this test catches it because the run blows
past the budget by orders of magnitude") while accommodating Cathedral
II's longer schema chain.
CI: 33758ms (v8 test) + 33343ms (v9 test) → both under 90s. The 5s
assertion was failing them, not the test runner timeout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(migrate): v29 enables RLS on code_edges_chunk + code_edges_symbol
The two new tables added by v27 (Cathedral II foundation) shipped without
RLS enabled. The E2E test "RLS is enabled on every public table (no
hardcoded allowlist)" caught this — Supabase exposes the public schema
via PostgREST so any table without RLS is anon-readable. Same security
gap as the v0.18.1 RLS hardening pass that v24 closed for the original
10 gbrain-managed tables.
Three CI failures fixed by this migration:
1. "RLS is enabled on every public table" — direct fail on the new
tables.
2. "GBRAIN:RLS_EXEMPT comment with valid reason exempts a non-RLS
public table" — was failing because doctor saw the unrelated
code_edges tables ALSO un-RLS'd, so the exempt-comment fixture
wasn't the only no-RLS table and doctor stayed in fail status.
3. "gbrain doctor exits 0 on healthy DB" — same cause, doctor was
emitting a fail check for the missing-RLS tables on every healthy
run.
Pattern: matches v24 exactly. DO $$ block with BYPASSRLS guard so a
non-bypass session can't accidentally lock itself out of its own data;
RAISE EXCEPTION on guard fail leaves schema_version at the prior value
so the next initSchema retries. Postgres-only via sqlFor — PGLite
doesn't enforce RLS the same way and the E2E gate runs only against
real Postgres.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test/e2e): v24 self-heals — assert version >= 24, not exactly 24
Pre-existing test bug surfaced when the E2E job ran on the Cathedral II
branch (and would have surfaced on master too once anyone ran the Tier 1
Mechanical job). The test rolls schema_version back to 23, runs init,
then asserts the version becomes exactly '24'. The intent was to prove
v24 didn't crash on missing budget_* tables — not to pin a specific
final version.
But initSchema runs every pending migration. With v25 + v26 (v0.19.0)
and now v27 + v28 + v29 (v0.21.0 Cathedral II) shipped, init advances
schema_version to LATEST_VERSION (currently 29) regardless of where it
started. The exact-match `'24'` assertion has been wrong since v25
landed; only the lack of an E2E run on master CI hid it.
Fix: parse the final version as int and assert `>= 24`. Same intent
(prove v24 ran cleanly + didn't roll back), forward-compatible with
future schema growth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(README): add "Using gbrain with GStack" — 5 code-search magical moments
Discoverability hint for engineering agents running on GStack. Cathedral
II (v0.21.0) shipped call-graph edges + two-pass retrieval, but a
GStack agent running /investigate or /review won't reach for them
unless someone tells it gbrain has these surfaces. The new subsection
slots between Remote MCP and the Skills index, lists the 5 commands
verbatim (code-callers, code-callees, code-def, code-refs, query
--near-symbol --walk-depth), and links to the v0.21.0 CHANGELOG entry
for context.
Tradeoff acknowledged: gbrain README serves both standalone and
agent-platform users, so the GStack section is kept tight (16 lines)
and slotted with the other agent-integration paths rather than at the
top.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: regenerate llms.txt + llms-full.txt for v0.21.0
The build-llms regen-drift guard caught that the committed llms files
were stale after the README "Using gbrain with GStack" addition + the
v0.21.0 CHANGELOG promotion. Running `bun run build:llms` rebuilds both
deterministically from llms-config.ts so the test passes.
No source content changed in this commit — just the generator output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Garry Tan <garry@ycombinator.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
90c5d93fce |
feat: v0.18.0 — multi-source brains (one DB, many repos, federation + dotfile resolution) (#337)
* feat(v0.17.0 step 1/9): sources primitive — additive-only multi-source foundation
Lane A of the multi-repo plan. Installs the sources table and seeds a
'default' row that inherits sync.repo_path/last_commit from existing
config. This is the bisectable foundation every later step builds on;
the breaking schema changes (composite UNIQUE, files FK rewrite,
resolution_type, ingest_log.source_id) land with their paired code
rewrites in Steps 2/4/5/7 so no single commit breaks the engine.
- migration v16 (sources_table_additive) + v0_17_0 orchestrator skeleton
- sort-by-version guard in runMigrations (array insertion order can
never cause a later migration to skip a lower one again)
- default source seeded with config '{"federated": true}' so pre-v0.17
brains keep single-namespace search semantics after upgrade
- orchestrator phase B detects absence of file_migration_ledger and
no-ops until Step 7 lands it
- 8 new structural tests in test/migrate.test.ts (shape, idempotency,
scope-guard that nothing else was smuggled into v16)
- apply-migrations tests include v0.17.0 in the registered list
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(v0.17.0 step 2/9): pages.source_id + composite UNIQUE (Lane B)
Migration v17 adds pages.source_id with DEFAULT 'default' and swaps the
global UNIQUE(slug) for composite UNIQUE(source_id, slug). Ships atomically
with the engine's ON CONFLICT rewrite so the constraint swap and the code
that writes under it land in the same commit — no window where the engine
sees one shape and the schema has another.
Minimum-surface engine change: only putPage's ON CONFLICT target needs
re-targeting. Other slug-based queries work unchanged because single-
source brains (the only brain shape pre-Step-5) have exactly one source
'default', so slug remains effectively unique within it. Step 5+ will
surface an explicit sourceId param on putPage for cross-source sync.
- migration v17 (pages_source_id_composite_unique) in src/core/migrate.ts
- pages.source_id + composite UNIQUE added to schema.sql + pglite-schema.ts
for fresh installs
- ON CONFLICT (slug) → ON CONFLICT (source_id, slug) in both pglite-engine
and postgres-engine putPage
- DEFAULT 'default' closes the Codex-flagged race where an INSERT between
ADD COLUMN and SET NOT NULL could leave source_id NULL
- 5 new v17 structural tests (29 pass / 0 fail in migrate.test.ts)
- Full suite: 1979 pass / 3 fail (same as baseline — no regressions)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(v0.17.0 step 6/9): sources CLI + source-resolver (Lane C)
Adds the CLI surface for multi-source management. Users can now register,
list, rename, federate/unfederate, and attach-to-directory a source. The
source-resolver is the shared 6-priority helper that Steps 4/5 will use
when they start surfacing an explicit --source flag on sync/extract/query.
Commands:
gbrain sources add <id> --path <p> [--name <n>] [--federated|--no-federated]
gbrain sources list [--json]
gbrain sources remove <id> [--yes] [--dry-run] [--keep-storage]
gbrain sources rename <id> <new-name>
gbrain sources default <id>
gbrain sources attach <id> — writes .gbrain-source in CWD
gbrain sources detach
gbrain sources federate <id> / unfederate <id>
Resolution priority (source-resolver.ts) — highest first:
1. --source flag 2. GBRAIN_SOURCE env 3. .gbrain-source dotfile walk-up
4. longest-prefix match on registered local_path (Codex #2 fix)
5. sources.default config 6. fallback 'default'
- add: validates id format (kebab-case alnum, 1-32), rejects overlapping
paths (eng review §4 finding 4.1), supports federated default opt-in
- remove: guards against --yes omission + refuses to remove 'default',
supports --dry-run, reports cascade page count
- attach/detach: matches kubectl/terraform context-pinning semantics
- Throws on overlap rather than process.exit() so the CLI error wrapper
reports it consistently (also makes unit testing clean)
28 new tests across sources.test.ts (dispatcher + validation + overlap
guard) and source-resolver.test.ts (full 6-priority coverage including
longest-prefix). Full suite: 2012 pass / 3 fail (pre-existing PGLite
infra timeouts).
NOT in scope for Step 6 (deferred):
- import-from-github (SSRF + clone integration)
- prune (retention/TTL, lands v0.18)
- MCP tool-defs regen for source-scoping on read ops (Step 5)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(v0.17.0 step 8/9): getting-started guide + migration skill + citation rule
Step 8 (Lane F) documents what Steps 1+2+6 have shipped and sets up
the agent-facing rules for multi-source.
New files:
- skills/migrations/v0.17.0.md — migration skill read by host agents
after `gbrain apply-migrations`. Covers the v16+v17 chain, what's
in v0.17.0 vs what lands later (v0.17.1 ACL, v0.18 sessions), and
the new sources CLI surface. Cites docs/guides/multi-source-brains.md
as the recipe.
- docs/guides/multi-source-brains.md — getting-started for end users.
Three canonical scenarios (unified wiki+gstack / purpose-separated
yc-media+garrys-list / mixed), full resolution priority, federation
flag semantics, command reference, and citation format.
skills/brain-ops/SKILL.md — new "Cross-source citation format"
section mandating `[source-id:slug]` when the brain has multiple
sources. Matches the contract the /plan-devex-review DX review
pinned down (DX Finding 5: surface source_id in every page payload
+ citation contract). Key must be sources.id (immutable), never
sources.name.
No behavior change — this is pure documentation for what already
exists in the binary. 144 skills conformance tests still pass.
NOT in this commit (deferred to later steps):
- docs/guides/repo-architecture.md rewrite (lands with the full
v0.17.0 PR description + release notes)
- skills/_brain-filing-rules.md "which source to file into"
guidance (lands with Step 5 when sync surfaces --source)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(v0.17.0 step 5/9): sync --source <id> routes through sources table (Lane D)
Adds the --source flag to `gbrain sync`. When set, sync reads local_path
+ last_commit from the matching sources(id) row instead of the global
sync.repo_path / sync.last_commit config keys, and writes last_commit +
last_sync_at back to the same row. Backward compat: --source omitted =
pre-v0.17 behavior exactly, global config path unchanged.
- SyncOpts.sourceId threaded through performSync + performFullSync
- readSyncAnchor/writeSyncAnchor helpers centralize the sources-vs-config
branch so every read/write goes through one decision point. Makes
Step 5's later per-source sync-failures tracking a one-file change.
- --source resolved via src/core/source-resolver.ts (Step 6), so any
command that shell-exposes resolveSourceId gets env var + dotfile
walk-up + longest-prefix for free.
- Error message for missing source local_path is actionable:
Source "gstack" has no local_path. Run: gbrain sources add gstack --path <path>
- last_sync_at auto-updates on every last_commit advance so `gbrain
sources list` shows real recency.
No regression: 2012 pass / 3 fail (same as baseline).
NOT in this commit (deferred per plan):
- Per-source failure tracking (~/.gbrain/sources/<id>/sync-failures.jsonl)
- runImport source-awareness (import.ts path — Step 5 continuation)
- Partial-success semantics when walking N sources — single-source flow
today, multi-walk lands when the top-level `gbrain sync` without
--source starts iterating all sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(v0.17.0 step 4/9): qualified [[source:slug]] + links.resolution_type (Lane B)
Adds source-pinned wikilink syntax and records the resolution kind on
each edge so `gbrain extract --refresh-unqualified` (future) can
re-resolve bare references when the source topology changes.
Wikilink syntax extension:
[[concepts/ai]] — unqualified; resolves via local-first fallback
[[wiki:concepts/ai]] — qualified; target pinned to sources.id='wiki'
[[gstack:projects/foo|Display]] — qualified + display name
The qualified regex runs first and masks matched spans so the
unqualified pass can't double-emit. Source id format enforced to match
the sources CLI validation: [a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?
Schema:
- migration v18 adds links.resolution_type TEXT with CHECK constraint
('qualified'|'unqualified' or NULL for legacy/manual/frontmatter edges)
- schema.sql + pglite-schema.ts updated for fresh installs
EntityRef type:
- sourceId is OPTIONAL (only set on qualified wikilinks). Markdown
[Name](path) and unqualified wikilinks omit it so strict toEqual
tests pre-v0.17 keep working (69 existing tests still pass).
Tests:
- 5 new qualified-wikilink extraction tests + 1 migration v18 structural
assertion. 75 tests in test/link-extraction.test.ts (up from 69).
- Full suite: 2018 pass / 3 fail (pre-existing PGLite infra timeouts).
NOT in this commit (deferred to Step 3 / Step 5 continuation):
- Writing resolution_type to the DB (addLink / addLinksBatch don't
carry the field yet — that's the plumb-through that lands with
Step 3 when search/dedup also needs source-aware result keys).
- `gbrain extract --refresh-unqualified` re-resolver.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(v0.17.0 step 3/9): source-aware search dedup composite keys (Lane B)
Search dedup now keys on (source_id, slug) instead of slug alone. Pre-
v0.17 would collapse two same-slug pages in different sources into
one, destroying cross-source recall. Codex outside-voice review flagged
this as regression-critical — this commit ships the fix plus tests
that lock the invariant in.
Dedup pipeline (src/core/search/dedup.ts):
- pageKey(r) helper — one canonical composite-key derivation. Falls
back to source_id='default' for pre-v0.17 rows so single-source
brains behave identically to before.
- Layer 1 (dedupBySource): group-by composite key.
- Layer 4 (capPerPage): count-by composite key.
- guaranteeCompiledTruth: swap scoped to matching (source_id, slug),
so wiki:topics/ai can't accidentally pull gstack:topics/ai's
compiled_truth chunk.
SearchResult type gains optional source_id — populated by SQL JOINs
in both engines, falls through as 'default' for legacy callers.
Engine SQL:
- pglite-engine.ts + postgres-engine.ts: search SELECTs add p.source_id
- rowToSearchResult (utils.ts): maps row.source_id → result.source_id
when present. Shape stays backward compatible (field optional).
Tests — 4 new in test/dedup.test.ts:
- same-slug-different-source does NOT collapse (the critical regression
guard Codex called out)
- same-slug-same-source DOES still collapse (no over-correction)
- missing source_id falls back to 'default' for pre-v0.17 compat
- compiled_truth guarantee scopes to composite key (Codex second pass
caught this specific path would leak otherwise)
Full suite: 2022 pass / 3 fail (3 pre-existing PGLite infra timeouts).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(v0.17.0 step 7/9): file_migration_ledger + phase-B storage backfill (Lane E)
Adds files.source_id + files.page_id + the file_migration_ledger
state machine that drives storage object rewrites. Each per-file
transition is its own transaction so crash-point recovery is a
ledger read, not a filesystem inspection. Codex second-pass review
flagged that "skip if already has source prefix" was an unsafe
heuristic — the ledger replaces it with explicit state tracking.
Schema:
- migration v19 (files_source_id_page_id_ledger): handler-only
(PGLite has no files table; Postgres-only gate). ADDs
source_id + page_id to files, backfills page_id from page_slug
scoped to source_id='default', creates file_migration_ledger
with PK on file_id (Codex: not storage_path_old — two sources
can share an old path during migration).
- schema.sql updated for fresh Postgres installs; file_migration_ledger
gets RLS alongside other tables.
Runtime:
- src/commands/migrations/v0_17_0-storage-backfill.ts: drives the
ledger state machine pending → copy_done → db_updated → complete.
Idempotent per row: re-running resumes from whichever state
crashed. Old objects preserved (no delete) so operators can
verify the soak window before a future cleanup release.
- phase B in v0_17_0.ts orchestrator: wires the storage backend
(Supabase/S3/local) through createStorage, runs runStorageBackfill,
reports per-state counts + first-three error details.
Tests — 13 new in test/storage-backfill.test.ts:
- pending → copy_done → db_updated → complete happy path
- 3 crash-point recovery tests (resume from copy_done, resume from
db_updated, failed rows don't auto-retry)
- already-complete rows are skipped with zero side effects
- idempotent re-upload (exists-check skips redundant upload)
- dry-run mode (no storage, reports counts without mutating)
Plus 5 new migrate.test.ts assertions for v19 structure (handler-
only, PGLite gate, source_id + page_id + ledger DDL, default-source
backfill scope, state machine values).
Full suite: 2035 pass / 3 fail (3 pre-existing PGLite infra
timeouts).
NOT in this commit (explicitly deferred):
- DROP old page_slug column — kept for backward compat until
operators have time to verify page_id everywhere.
- DROP old UNIQUE(storage_path) in favor of UNIQUE(source_id,
storage_path) — same reason, deferred to later cleanup.
- Actual cleanup phase that deletes old objects post-soak.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(v0.17.0 step 9/9): full multi-source PGLite integration suite (Lane G)
End-to-end exercise of every v0.17.0 surface against real PGLite
(in-memory, fast — no DATABASE_URL needed). The migration chain
v2→v19 runs start-to-finish and the test asserts each Step's
invariants hold together.
16 new integration tests across 7 describes:
1. Migration-installed state:
- sources('default') exists with federated=true config
- pages.source_id column has DEFAULT 'default'
- composite UNIQUE (source_id, slug) is installed
2. Default-source write path:
- putPage without explicit source → source_id='default' via schema
default clause (no engine API change needed for single-source brains)
3. Composite UNIQUE regression guards (Codex-flagged):
- Same slug in two different sources coexists
- Third insert with same (source_id, slug) hits the UNIQUE constraint
4. sources CLI round-trip:
- federate / unfederate flips config.federated
- rename changes display, id stays immutable
5. Source resolution priority (integration):
- Explicit flag > env var > fallback to default
- Unregistered explicit source errors with actionable message
6. Cascade semantics:
- sources remove cascades to pages; default source untouched
7. links.resolution_type (Step 4):
- Qualified/unqualified values accepted
- CHECK constraint rejects invalid values
All 16 tests pass. Full suite: 2042 pass / 4 fail (4 pre-existing
PGLite beforeEach timeouts in test/wait-for-completion,
test/extract-fs, test/e2e/search-quality, test/e2e/graph-quality
— count fluctuated 3-5 on baseline from variance alone).
Total new tests across Steps 1-9: ~85 unit + integration tests
(sources, source-resolver, migrate v16/v17/v18/v19 structural,
link-extraction qualified wikilinks, dedup regression-critical,
storage-backfill state machine + crash recovery, full
multi-source PGLite integration).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump to v0.18.0 + CHANGELOG entry (multi-source brains)
One-viewport release summary + itemized changes covering all 9 steps
of the multi-source primitive. Notes the v0.17 → v0.18 version bump
rationale (master shipped gbrain dream as v0.17 while this branch was
in flight).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ci): v0_18_0 orchestrator TS narrow + mechanical test ON CONFLICT
Two CI failures on PR #337:
1. tsc TS2367 at src/commands/migrations/v0_18_0.ts:190 —
after the early-return on `a.status === 'failed'` (line 179),
TypeScript narrows `a.status` to `'skipped' | 'complete'`, so the
subsequent `a.status === 'failed' ? 'failed' :` branch was dead
code and refused to compile. Dropped the redundant check.
2. E2E `file_list LIMIT enforcement` at test/e2e/mechanical.test.ts:636 —
the test pre-seeded a pages row with `ON CONFLICT (slug) DO NOTHING`
but v21 swapped the global UNIQUE for `UNIQUE (source_id, slug)`, so
Postgres rejects with "no unique or exclusion constraint matching".
Updated the conflict target to the composite key.
Tier-1 E2E had only this one failing test; everything else passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(e2e): v0.18.0 multi-source against real Postgres (v20-v23 schema + cascade + sync)
Closes the three biggest confidence gaps the author flagged in the
self-audit of PR #337:
1. No real Postgres E2E — PGLite has no files table, so v23's
files.source_id + files.page_id rewrite + file_migration_ledger
seed was NEVER executed against the real DB. This file covers it.
2. `gbrain sync --source <id>` had zero direct tests. Now has two:
one that asserts performSync({sourceId}) reads local_path from the
sources row (not the global config), one that asserts no-sourceId
falls back to the global sync.repo_path.
3. Cascade delete coverage — previously verified only pages count
after source removal. Now verifies pages + content_chunks +
timeline_entries + links + files ALL cascade-delete when a source
is removed.
6 describes, 16 tests total:
- Schema shape (fresh install): 6 tests confirming sources('default'),
pages.source_id NOT NULL with DEFAULT, composite UNIQUE pages
(source_id, slug) replaces global UNIQUE(slug), links.resolution_type
column + CHECK, files.source_id + page_id columns, file_migration_ledger
table + status CHECK.
- Composite UNIQUE semantics: 3 tests confirming same-slug in two
sources coexists (Codex-critical regression guard), duplicate
(source_id, slug) hits the UNIQUE, putPage targets default source
by schema DEFAULT.
- Cascade delete: 1 test building a fully populated source (2 pages,
chunks, timeline, links, files) then removing it + asserting every
dependent row is gone.
- Sync routing: 2 tests confirming performSync({sourceId}) reads
per-source local_path vs global config.
- Sources surface: 3 tests for federate/unfederate flipping + rename
preserving id.
- Storage backfill: 1 end-to-end test seeding ledger + running
runStorageBackfill against a stub StorageBackend, asserting
pending → complete transition and files.storage_path rewrite.
Gated by DATABASE_URL per CLAUDE.md E2E lifecycle. Each describe's
beforeAll defensively DELETEs non-default sources + file_migration_ledger
rows so reruns are hermetic (sources isn't in helpers.ALL_TABLES).
Verified: 16/16 pass on first run AND second run (residual-state fix
holds). Full E2E suite still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ci): TS2352 in multi-source E2E — cast postgres.js RowList via unknown
tsc rejects the direct
`(rows as { column_name: string }[]).map(...)`
cast because postgres.js RowList rows have an iterable-row shape that
doesn't overlap with the plain-object target. Standard fix: cast via
`unknown` first so the narrowing is explicit.
Verified: `bunx tsc --noEmit` clean (ignoring the pre-existing baseUrl
deprecation warning).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v0.18.0): addLinksBatch + addTimelineEntriesBatch source-aware JOINs
Batch APIs JOINed on pages.slug globally, so two pages sharing the same
slug across sources would silently fan out — addLinksBatch(['a->b']) in
a brain with 'a' in both 'default' and 'alt' wrote 2 edges instead of 1.
Same bug on addTimelineEntriesBatch.
Fix:
- LinkBatchInput + TimelineBatchInput gain optional source_id fields
(from_source_id, to_source_id, origin_source_id for links; source_id
for timeline). All default to 'default' so existing callers are
backward-compatible on single-source brains.
- pglite-engine + postgres-engine batch JOINs now composite-key on
(slug, source_id). Postgres adds 3 more unnest arrays for links + 1
for timeline — still one bind per column, no 65535-param cap risk.
- LEFT JOIN for origin pages also source-qualified so frontmatter-
provenance edges don't cross-pollinate across sources.
Regression coverage:
- test/pglite-engine.test.ts: 5 new tests covering default-path isolation,
explicit alt-source writes, and cross-source edges.
- test/e2e/multi-source.test.ts: 4 new tests against real Postgres so
postgres-js's unnest() bind path is exercised (structurally different
from PGLite's).
Gap #4 from the PR self-audit — latent bug, not previously reachable
because every existing caller wrote to the default source only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ff10796a00 |
fix(wave): v0.15.1 - 4 hot issues + scope expansion (#248)
* fix(wave): 4 hot issues + 3 scope expansions (v0.13.1) Addresses four user-filed regressions after v0.13.0 plus three adjacent footgun closures. * #170 — CREATE INDEX [CONCURRENTLY] IF NOT EXISTS idx_pages_updated_at_desc on pages (updated_at DESC). Engine-aware migration v12 with invalid-index cleanup on Postgres, plain CREATE on PGLite. ~700x on 30k+ row brains. Contributed by @fuleinist (#215). * #219 — Minions schema default max_stalled 1 -> 5. v13 migration ALTERs the default and UPDATEs existing non-terminal rows (waiting/active/ delayed/waiting-children/paused) so live queues get rescued on upgrade. Adds MinionJobInput.max_stalled with [1,100] clamp. New --max-stalled CLI flag on `jobs submit`. Reported by @macbotmini-eng. * #218 — package.json postinstall surfaces errors instead of silencing. trustedDependencies whitelists @electric-sql/pglite. doctor schema_version check fails loudly when migrations never ran and links to #218. README + INSTALL_FOR_AGENTS warn against `bun install -g`. Reported by @gopalpatel. * #223 — @electric-sql/pglite pinned to exactly 0.4.3 (was ^0.4.4). PGLiteEngine.connect() wraps PGlite.create() errors with a message pointing at the issue + gbrain doctor. Does NOT suggest 'missing migrations' as a cause (create-time abort happens before migrations run). Pin is unverified against macOS 26.3; error-wrap is the safety net. Reported by @AndreLYL. * Scope: `gbrain jobs submit` gains --backoff-type/--backoff-delay/ --backoff-jitter/--timeout-ms/--idempotency-key (MinionJobInput audit). * Scope: `gbrain jobs smoke --sigkill-rescue` regression case (opt-in, CI-only) that simulates a killed worker and asserts the new default rescues. * Scope: `gbrain doctor --index-audit` reports zero-scan Postgres indexes as drop candidates (informational; no auto-drop). Infrastructure: * Migration interface extended with sqlFor: { postgres?, pglite? } and transaction: boolean. Runner picks the engine-specific branch and bypasses engine.transaction() when transaction:false (required for CONCURRENTLY). BrainEngine.kind readonly discriminator added. * scripts/check-jsonb-pattern.sh CI guard extended to block `max_stalled DEFAULT 1` from regressing. Tests: * 15 new unit tests: v12/v13 structural + behavioral assertions, max_stalled default/clamp/backfill, PGLite error-wrap source guard, engine kind discriminator. * 3 regression tests pinned by IRON RULE. * Full unit suite: 1416 pass. * Full E2E suite against Postgres 16 + pgvector: 126 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.13.1) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: sync documentation for v0.13.1 CLAUDE.md "Key files" and "Commands" sections refreshed to match the v0.13.1 fix wave: - Note `BrainEngine.kind` discriminator on engine.ts - Document v0.13.1 connect() error-wrap on pglite-engine.ts - Refresh src/core/minions/ layout (no shell handler, no protected-names, no quiet-hours/stagger — that was v0.13-development scaffolding that did not ship) - Add src/core/migrate.ts entry with `Migration` interface extensions (`sqlFor`, `transaction: false`) - Document new `gbrain jobs submit` flags (--max-stalled, --backoff-type, --backoff-delay, --backoff-jitter, --timeout-ms, --idempotency-key) - Document `gbrain jobs smoke --sigkill-rescue` regression guard - Document `gbrain doctor --index-audit` and the schema_version=0 surface that catches #218 postinstall failures - Extend check-jsonb-pattern.sh note with the max_stalled DEFAULT 1 regression guard - Touch up test file blurbs for migrate.test.ts, pglite-engine.test.ts, minions.test.ts with v0.13.1 coverage Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(e2e): run files sequentially to eliminate shared-DB race The E2E suite was flaky. ~3 of every 5 runs had 4-10 failures clustered in Links, Timeline, Versions, Minions resilience, Parallel Import, and Page CRUD tests. Symptoms included "expected 16 pages, got 8" (half), "expected 1 link inserted, got 0", timeline entries missing after round-trip, and similar data-shape mismatches. Root cause: bun test runs test FILES in parallel (each in a worker process). 13 E2E files share one DATABASE_URL, and `setupDB()` in `test/e2e/helpers.ts` does `TRUNCATE ... CASCADE` on all tables before each file's `importFixtures()`. File A's TRUNCATE would race with file B's in-flight INSERT stream, producing the observed half-populated or wrong-count states. An earlier attempt used a Postgres advisory lock held on a dedicated single-connection client for the lifetime of each file's run. It broke because bun's default 5000 ms hook timeout fires on queued beforeAll() calls: with 13 files serializing through the lock, files 2-13 would time out waiting for file 1 to finish. This commit switches to sequential file execution at the harness level via scripts/run-e2e.sh, which loops through test/e2e/*.test.ts one at a time, tracks aggregate pass/fail counts, and exits non-zero on the first failing file. No lock, no timeout issues, no changes to any test file. package.json test:e2e points at the new script. Verified: 5 back-to-back runs against the same Postgres container, each completing in ~5 min. Every run: 13 files, 138 tests, 0 fails. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump version to 0.15.1 (fix wave locked to MINOR line) Master v0.14.2 was the last /investigate root-cause wave on the v0.14.x line. This fix wave opens v0.15.x: four hot issues (#170, #218, #219, #223) close v0.13.x regressions that v0.14.x didn't cover, so the MINOR bump reflects the semantic shift — new schema migrations (v14, v15), a new CLI surface (`--max-stalled`, `--sigkill-rescue`, `--index-audit`), a new BrainEngine contract (`kind` discriminator + extended `Migration` interface), and a new install-time contract (PGLite 0.4.3 pin + `trustedDependencies`). Locked to 0.15.1 in advance: other work may land before/after this PR, but the version is fixed so reviewers can cite a stable number. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
699db50a3d |
fix(extract+migrate): kill N+1 hang + v0.12.0 migration timeout (v0.12.1) (#198)
* feat(engine): add addLinksBatch + addTimelineEntriesBatch via unnest()
Multi-row INSERT...SELECT FROM unnest() JOIN pages ON CONFLICT DO NOTHING
RETURNING 1. 4 array-typed bound parameters (links) or 5 (timeline)
regardless of batch size, sidesteps Postgres's 65535-parameter cap.
Returns count of rows actually inserted (excluding ON CONFLICT no-ops
and JOIN-dropped rows whose slugs don't exist).
Per-row addLink / addTimelineEntry signatures and SQL behavior unchanged.
All 10 existing call sites compile and behave identically.
Tests: 11 PGLite cases (empty batch, missing optionals, within-batch dedup,
JOIN drops missing slug, half-existing batch, batch of 100) + 9 E2E
postgres-engine cases against real Postgres+pgvector.
* fix(migrate): pre-create btree helper in v8 + v9 dedup; bump phaseASchema timeout
Production bug: v0.12.0 schema migration timed out at Supabase Management API's
60s ceiling on brains with 80K+ duplicate timeline rows. The DELETE...USING
self-join was O(n²) without an index on the dedup columns.
Fix: pre-create idx_links_dedup_helper / idx_timeline_dedup_helper on the
dedup columns BEFORE the DELETE, drop after. Turns O(n²) into O(n log n).
On 80K+ rows the migration completes in <1s instead of timing out.
Also bumps the v0.12.0 orchestrator's phaseASchema timeout 60s -> 600s as
belt-and-suspenders for unforeseen slowness.
Exports MIGRATIONS for structural test assertions.
Tests: 2 structural assertions (helper-index DDL must appear in v8/v9 SQL
in the right order — catches regression even at 0-row scale) + 2 behavioral
regression tests (1000-row dedup completes <5s).
* perf(extract): kill N+1 dedup pre-load; switch to batched writes
Production bug: gbrain extract hung 10+ minutes producing zero output on
47K-page brains. The pre-load loop called engine.getLinks(slug) (or
getTimeline) once per page across engine.listPages({limit: 100000}) — 47K
serial round-trips over the Supabase pooler before the first file was read.
Both engines already enforced uniqueness at the SQL layer
(UNIQUE(from, to, link_type) on links, idx_timeline_dedup on timeline_entries).
The in-memory dedup Set was redundant insurance that became the bottleneck.
Fix: delete the pre-load entirely. Buffer 100 candidates per file walk,
flush via engine.addLinksBatch / engine.addTimelineEntriesBatch. ~99% fewer
DB round-trips per re-extract.
Also fixes counter accuracy: 'created' now counts rows actually inserted
(via batch RETURNING 1 row count). Re-run on a fully-extracted brain
prints 'Done: 0 links' instead of lying.
Dry-run mode keeps a per-run dedup Set so duplicate candidates from N
markdown files print exactly once, not N times.
Batch errors are visible in BOTH json and human modes — silent loss of
100 rows is worse than per-row error visibility.
Tests: extract-fs.test.ts (idempotency + truthful counter + dry-run dedup
+ perf regression guard <2s).
* chore: bump version + changelog (v0.12.1)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: update CLAUDE.md for v0.12.1 (batch engine API, test counts)
Reflect what shipped in v0.12.1:
- New engine methods addLinksBatch + addTimelineEntriesBatch (PGLite via
unnest() + manual $N, postgres-engine via INSERT...SELECT FROM
unnest($1::text[], ...) JOIN pages ON CONFLICT DO NOTHING).
- extract.ts no longer pre-loads dedup set; candidates are buffered 100
at a time and flushed via the new batch methods.
- v0.12.0 orchestrator phaseASchema timeout bumped 60s to 600s.
- Test counts 1297 unit / 105 E2E to 1412 unit / 119 E2E.
- New test/extract-fs.test.ts covers the N+1 regression guard.
- BrainEngine method count 37/38 to 40.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
81b3f7afac |
feat: knowledge graph layer — auto-link, typed relationships, graph-query (v0.10.3) (#188)
* feat(schema): graph layer migrations v5/v6/v7 + GraphPath/health types
Schema foundation for v0.10.3 knowledge graph layer:
- v5: links UNIQUE constraint widened to (from, to, link_type) so the same
person can both works_at AND advises the same company as separate rows.
Idempotent for fresh + upgrade (drops both old constraint names first).
- v6: timeline_entries gets UNIQUE index on (page_id, date, summary) for
ON CONFLICT DO NOTHING idempotency at DB level.
- v7: drops trg_timeline_search_vector trigger. Structured timeline entries
are now graph data, not search text. Markdown timeline still feeds search
via the pages trigger. Side benefit: extraction pagination is no longer
self-invalidating (trigger used to bump pages.updated_at on every insert).
Types: new GraphPath (edge-based traversal result), PageFilters.updated_after,
BrainHealth gets link_coverage / timeline_coverage / most_connected. Postgres
schema regenerated via build:schema.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(graph): auto-link on put_page + extract --source db + security hardening
Core graph layer wired into the operation surface:
- New src/core/link-extraction.ts: extractEntityRefs (canonical extractor used
by both backlinks.ts and the new graph code), extractPageLinks (combines
markdown refs + bare-slug scan + frontmatter source, dedups within-page),
inferLinkType (deterministic regex heuristics for attended/works_at/
invested_in/founded/advises/source/mentions), parseTimelineEntries (parses
multiple date format variants from page content), isAutoLinkEnabled
(engine config flag, defaults true, accepts false/0/no/off case-insensitive).
- put_page operation auto-link post-hook: extracts entity refs from freshly
written content, reconciles links table (adds new, removes stale). Returns
auto_links: { created, removed, errors } in response so MCP callers see
outcomes. Runs in a transaction so concurrent put_page on same slug can't
race the reconciliation. Default on; opt out with auto_link=false config.
- traverse_graph operation extended with link_type and direction params.
Returns GraphPath[] (edges) when filters set, GraphNode[] (nodes) for
backwards compat. Depth hard-capped at TRAVERSE_DEPTH_CAP=10 for remote
callers; without this, depth=1e6 from MCP burns memory on the recursive CTE.
- gbrain extract <links|timeline|all> --source db: walks pages from the
engine instead of from disk. Works for live brains with no local checkout
(MCP-driven Wintermute / OpenClaw). Filesystem mode (--source fs) is
unchanged. New --type and --since filters with date validation upfront
(invalid --since used to silently no-op the filter and reprocess everything).
- Security: auto-link skipped for ctx.remote=true (MCP). Bare-slug regex
matches `people/X` anywhere in page text including code fences and quoted
strings. Without this gate an untrusted MCP caller could plant arbitrary
outbound links by writing pages with intentional slug references; combined
with the new backlink boost, attacker-placed targets would surface higher
in search.
- Postgres orphan_pages aligned to PGLite definition (no inbound AND no
outbound). Comment used to claim alignment but code disagreed; engines
drifted silently when users migrated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cli): graph-query command + skill updates + v0.10.3 migration file
Agent-facing surface for the graph layer:
- New `gbrain graph-query <slug>` command with --type, --depth, --direction
in|out|both. Maps to traverse_graph operation with the new filters. Renders
the result as an indented edge tree.
- skills/migrations/v0.10.3.md: agent runs this post-upgrade to discover the
graph layer. Tells the agent to run `gbrain extract links --source db`,
then timeline, verify with stats, try graph-query, and lists the inferred
link types so they can be used in subsequent traversals.
- skills/brain-ops/SKILL.md Phase 2.5: documents that put_page now auto-links.
No more manual add_link calls in the Iron Law back-linking path.
- skills/maintain/SKILL.md: graph population phase. Shows the right command
to backfill links + timeline from existing pages.
- cli.ts: register graph-query in CLI_ONLY + handleCliOnly switch. Update help
text to describe `gbrain extract --source fs|db` and the new graph-query.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(graph): unit + e2e + 80-page A/B/C benchmark for graph layer
Coverage for the v0.10.3 graph layer (260+ new test assertions):
- test/link-extraction.test.ts (46 tests): extractEntityRefs both formats,
extractPageLinks dedup + frontmatter source, inferLinkType heuristics
(meeting/CEO/invested/founded/advises/default), parseTimelineEntries
multiple date formats + invalid date rejection, isAutoLinkEnabled
case-insensitive truthy/falsy parsing.
- test/extract-db.test.ts (12 tests): `gbrain extract <links|timeline|all>
--source db` happy paths, --type filter, --dry-run JSON output,
idempotency via DB constraint, type inference from CEO context.
- test/graph-query.test.ts (5 tests): direction in/out/both, type filter,
non-existent slug, indented tree output.
- test/pglite-engine.test.ts (+26 tests): getAllSlugs, listPages
updated_after filter, multi-type links via v5 migration, removeLink with
and without linkType, addTimelineEntry skipExistenceCheck flag,
getBacklinkCounts for hybrid search boost, traversePaths in/out/both with
cycle prevention via visited array, getHealth graph metrics
(link_coverage / timeline_coverage / most_connected).
- test/e2e/graph-quality.test.ts (6 tests): full pipeline against PGLite
in-memory. Auto-link via put_page operation handler. Reconciliation
removes stale links on edit. auto_link=false config skip.
- test/benchmark-graph-quality.ts: A/B/C comparison on 80 fictional pages,
35 queries across 7 categories. Hard thresholds: link_recall > 90%,
link_precision > 95%, timeline_recall > 85%, type_accuracy > 80%,
relational_recall > 80%. Currently passing all 9.
Built test-first: benchmark caught WORKS_AT_RE matching "founder" inside
slug names (frank-founder), "worked at" past-tense missing from regex,
PGLite Date object vs ISO string comparison bug. All fixed before merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump version and changelog (v0.10.3)
CHANGELOG: knowledge graph layer headline. Auto-link on every page write.
Typed relationships (works_at, attended, invested_in, founded, advises).
gbrain extract --source db. graph-query CLI. Backlink boost in hybrid search.
Schema migrations v5/v6/v7 applied automatically.
Security hardening caught during /ship adversarial review: traverse_graph
depth capped at 10 from MCP, auto-link skipped for ctx.remote=true, runAutoLink
reconciliation in transaction, --since validates dates upfront.
TODOS.md: 2 P2 follow-ups (auto-link redundant SQL on skipped writes;
extract --source db not gated on auto_link config).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: sync CLAUDE.md with v0.10.3 graph layer
Updated key files list (extract.ts now describes --source fs|db, added
graph-query.ts and link-extraction.ts), test inventory (extract-db,
link-extraction, graph-query unit tests; e2e/graph-quality), and
test count (51 unit + 7 e2e, 1151 + 105 assertions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(v0.10.3): wire graph layer into install flow + README + benchmark
Existing brains upgrading to v0.10.3 had no clear path to backfill the new
links/timeline tables. New installs had no instruction to run extract --source db
after import. This wires the knowledge graph into every install touchpoint so the
v0.10.3 features actually reach the user.
- README: headline now sells self-wiring graph + 94% benchmark numbers; new
Knowledge Graph section between Knowledge Model and Search; LINKS+GRAPH command
block expanded; Benchmarks docs group added
- INSTALL_FOR_AGENTS.md: new Step 4.5 (graph backfill) + Upgrade section now runs
gbrain init + post-upgrade and points to migrations/v<N>.md
- skills/setup/SKILL.md Phase C: new step 5 for graph backfill (idempotent,
skip-if-empty); existing file migration becomes step 6
- src/commands/init.ts: post-init hint detects existing brain (page_count > 0)
and prints extract commands for both PGLite and Postgres engines
- docs/GBRAIN_VERIFY.md: new Check #7 (knowledge graph wired) with backfill
fallback + graph-query smoke test
- docs/benchmarks/2026-04-18-graph-quality.md: checked-in benchmark report
matching the existing search-quality format (94% recall, 100% precision,
100% relational recall, idempotent both ways)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(claude): require PR descriptions to cover the whole branch
Adds a rule to CLAUDE.md so future PR bodies always cover the full diff
against the base branch, not just the most recent commit. Includes the
git log + gh pr view incantation to check what's actually in a PR.
This is a reaction to PR #189 being created with a body that described
only the last commit instead of the 7 commits it actually contained.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(upgrade): post-upgrade prints full body + --execute mode + downstream skill upgrade doc
PR #188 review caught two install-flow gaps that this commit closes:
1. `gbrain post-upgrade` only printed the migration headline + description
from YAML frontmatter, never the markdown body that contains the
step-by-step backfill instructions. Agents saw "Knowledge graph layer —
your brain now wires itself" and had no idea to run `gbrain extract
links --source db`. Now prints the full body after the headline.
2. New `--execute` flag reads a structured `auto_execute:` list from
migration frontmatter and runs the safe commands sequentially. Without
`--yes` it prints the plan only (preview mode). With `--yes` it actually
runs them. Stops on first failure with a clear error.
3. Downstream agents (Wintermute etc.) keep local skill forks that gbrain
can't push updates to. New `docs/UPGRADING_DOWNSTREAM_AGENTS.md` lists
the exact diffs each release needs applied to those forks. v0.10.3
diffs for brain-ops, meeting-ingestion, signal-detector, enrich.
Changes:
- src/commands/upgrade.ts:
- runPostUpgrade(args) accepts flags
- Prints full body via extractBody()
- Parses auto_execute: list via extractAutoExecute() (hand-rolled, no yaml dep)
- --execute previews, --execute --yes runs
- Fix cosmetic bug: `recipe: null` no longer prints "show null" message
- src/cli.ts: pass args to runPostUpgrade
- skills/migrations/v0.10.3.md:
- Add auto_execute: list (gbrain init + extract links/timeline + stats)
- Fix typo: completion record version was 0.10.1, now 0.10.3
- test/upgrade.test.ts: 5 new tests covering body printing, plan preview,
actual execution, no-auto_execute case, and --help output
- docs/UPGRADING_DOWNSTREAM_AGENTS.md: NEW
- CLAUDE.md: key files list updated
Test: 13 upgrade tests pass (was 8, +5 new). Full unit suite: 1078 pass,
zero regressions, 32 expected E2E skips (no DATABASE_URL).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* bench(graph): add Configuration A baseline (no graph) vs C comparison
Previous benchmark showed C numbers only (94.4% link recall, 100% relational
recall, etc.) but never quantified what a pre-v0.10.3 brain actually loses.
Reviewer caught this gap.
Adds measureBaselineRelational() that simulates a no-graph fallback:
- Outgoing queries: regex-extract entity refs from the seed page content
- Incoming queries: grep-style scan of all pages for the seed slug
This is what an agent without the structured links table can do today.
Honest result on the 5 relational queries in the benchmark:
- Recall: 100% A vs 100% C (+0%) — markdown contains the refs either way
- Precision: 58.8% A vs 100.0% C (+70%) — without typed links, you get the
right answers buried in 41% noise
Per-query breakdown shows the divergence is concentrated in INCOMING queries:
"Who works at startup-0?" returns 5 candidates without graph (2 employees +
3 noise pages that mention startup-0) vs exactly 2 with graph. For an LLM
agent, that's ~3x less reading work per relational question.
Also documented what the benchmark deliberately doesn't test (multi-hop,
search ranking with backlink boost, aggregate queries, type-disagreement
queries) so future benchmark work has a roadmap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* bench(graph): add 4 missing categories — multi-hop, aggregate, type-disagreement, ranking
The previous benchmark commit (
|
||
|
|
6c7d2ed30b |
feat: PGLite engine — local brain, zero infrastructure (v0.7.0) (#41)
* refactor: extract shared utils, add runMigration + getChunksWithEmbeddings to BrainEngine Extract validateSlug, contentHash, rowToPage, rowToChunk, rowToSearchResult from postgres-engine.ts into shared utils.ts. Add rowToChunk includeEmbedding parameter for migration support. Add two new methods to BrainEngine interface: - runMigration(version, sql) — replaces internal eng.sql access in migrate.ts - getChunksWithEmbeddings(slug) — returns chunks with embedding data for migration Replace 'sqlite' with 'pglite' in EngineConfig and GBrainConfig types. Fix loadConfig to infer engine from database_path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: pluggable engine factory + hybridSearch keyword-only fallback Add createEngine() factory with dynamic imports so PGLite WASM is never loaded for Postgres users. Wire CLI to use factory instead of hardcoded PostgresEngine. Force workers=1 for PGLite imports (single-connection architecture). Fix hybridSearch to check OPENAI_API_KEY before calling embed(). When unset, returns keyword-only results instead of throwing. Critical for local PGLite users who don't need vector search. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: PGLiteEngine — embedded Postgres 17.5 via WASM, same SQL everywhere Full BrainEngine implementation (37 methods) using @electric-sql/pglite. Same SQL as PostgresEngine — tsvector triggers, pgvector HNSW, pg_trgm fuzzy matching, recursive CTEs, JSONB. Only the driver call syntax differs (parameterized queries instead of tagged templates). PGLite schema is the Postgres schema minus RLS, advisory locks, and remote auth tables (access_tokens, mcp_request_log, files). No server. No subscription. One directory. Works offline. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: smart init (PGLite default) + bidirectional engine migration gbrain init now defaults to PGLite — brain ready in 2 seconds, no server needed. Scans target directory: <1000 .md files = PGLite, >=1000 = suggests Supabase. --supabase and --pglite flags override. gbrain migrate --to supabase/pglite transfers all data between engines with manifest-based resume. Copies pages, chunks (with embeddings), tags, timeline, raw data, links, and config. --force overwrites non-empty target. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: 60 new tests for PGLite engine, utils, and factory 41 PGLite engine tests covering all 37 BrainEngine methods: CRUD, tsvector keyword search, pg_trgm fuzzy matching, chunk upsert with COALESCE, graph traversal via recursive CTE, transactions, cascade deletes, stats/health, and embedding round-trip. 14 shared utility tests (validateSlug, contentHash, row mappers). 5 engine factory tests (dispatch, error messages). All run in-memory — zero Docker, zero DATABASE_URL, instant in CI. Add P0 TODO: submit Bun PR for WASM embedding in bun build --compile (oven-sh/bun#15032). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.7.0) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: update project documentation for v0.7.0 PGLite engine - CLAUDE.md: add PGLite key files, update architecture, add migrate command, add 3 test files - README.md: PGLite as default init, zero-config getting started, migration path to Supabase - docs/ENGINES.md: PGLiteEngine shipped (v0.7), capability matrix, migration docs - docs/SQLITE_ENGINE.md: marked superseded by PGLite Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove stale v0.4 README update prompt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove SQLITE_ENGINE.md (superseded by PGLite) PGLite uses the same SQL as Postgres, making a separate SQLite engine unnecessary. docs/ENGINES.md covers PGLiteEngine. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: update README step 2 to default to PGLite Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add schema setup step and install-all-integrations step to README Step 3 now tells agents to read GBRAIN_RECOMMENDED_SCHEMA.md and set up the MECE directory structure before importing. Step 7 tells agents to install every available integration recipe, not just list them. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: update install goal to match full opinionated setup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add 'Need an AI agent first?' section with one-click deploy links New users who don't have OpenClaw or Hermes Agent get pointed to AlphaClaw on Render and the Hermes Agent Railway template. One click each. Claude Code mentioned for users who already have it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add migrate to CLI_ONLY + help output, fix standalone example - migrate command was missing from CLI_ONLY set (errored as "Unknown command") - migrate now shows in --help under SETUP - init help line shows --pglite flag - standalone CLI example uses gbrain init (not --supabase) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: set realistic time expectation (~30 min to working brain) DB is 2 seconds. But schema + import + embeddings + integrations is 15-30 minutes. The agent does the work, you answer API key questions. Don't oversell time-to-value. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: fix AlphaClaw Render requirement (8GB+ RAM, not free tier) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: final README polish for launch - GOAL line: "Garry Tan's exact setup" (not Claude Code specific) - Remove markdown links from code block (won't render) - STEP 2 renamed from "START HERE" to "DATABASE" - Tighten Supabase fallback text Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: remove duplicate old install block from README The v0.5-era "With OpenClaw or Hermes Agent" paste block was superseded by the top-level "Start here" block. Having both confused users and the old one still said --supabase as step 2. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: clean up README consistency and remove duplicated content - Remove duplicate "Try it" section (old 4-act walkthrough that repeated the install flow and contradicted "~30 min" with "90 sec") - Remove duplicate Setup section (third repetition of gbrain init) - Fix brain.db → brain.pglite (actual default path) - Fix "coming in v0.7" → "not yet implemented" (we ARE v0.7) - Remove "You don't need Postgres" (confusing since PGLite IS Postgres) - Deduplicate "competitive dynamics" query (appeared 3 times) - Collapse redundant standalone CLI section Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |