v0.41.13.0 fix(sync): infiniteGameExp + foxhoundinc 5-bug wave (#1422, #1433, #1434, #1309, #1436) (#1456)
* fix(sync): infiniteGameExp + foxhoundinc 5-bug wave (#1422, #1433, #1434, #1309, #1436) Five real production bugs from infiniteGameExp (PostgreSQL onboarding) and foxhoundinc (dream-cycle reproduction), each silent-failure shape where gbrain told the user the operation succeeded when it didn't. * #1422 — `gbrain dream` swallowed connectEngine errors. Bind the caught error and surface `[dream] WARNING: could not connect to DB (...)` on stderr before falling through to filesystem-only phases. runDream(null) no-DB fallback preserved. * #1433 — `gbrain sync` deleted previously-indexed log.md / schema.md / index.md / README.md pages on every re-sync. Refactor isSyncable through private classifySync helper; expose unsyncableReason (companion returning the same tagged reason) and SYNC_SKIP_FILES named export. Cleanup loop guards on reason === 'metafile' before deleting. * #1434 — `gbrain sync` without --source on single-vault brains routed to source_id='default' (zero pages) and silently failed. Add resolver tier 5.5 'sole_non_default' AFTER brain_default (explicit user intent wins). Wire runSync + runImport to call resolveSourceWithTier unconditionally so the tier actually fires. Stderr nudge on tier hit; suppress with GBRAIN_NO_SOLE_NON_DEFAULT_NUDGE=1. * #1309 — overlapping ingest roots created duplicate pages. New BrainEngine.findDuplicatePage?(sourceId, {hash, frontmatterId}) with identity-based posture: SKIP when frontmatter.id matches (true external duplicate), WARN-ALWAYS on content_hash collision with different/missing fm.id, FAIL CLOSED on lookup error. Migration v95 adds partial index pages_dedup_idx (Postgres CONCURRENTLY, PGLite plain CREATE). * #1436 — MCP fuzzy get_page returned slug candidates from sources outside caller's scope. resolveSlugs signature extended with {sourceId?, sourceIds?} matching the sourceScopeOpts helper output; operations.ts threads it through. Both engines preserve unscoped back-compat for internal CLI callers. Plus a stable tiebreaker on searchVector ORDER BY (score DESC, page_id ASC, chunk_id ASC) in both engines. Caught while wiring the index above — basis-vector eval fixtures with tied scores depend on planner row order, which any new index on pages could flip. Pins eval-replay-gate ranking determinism against future index changes. Per codex review of the original plan: caught 6 load-bearing gaps that the engineering review missed (runSync bypass, #1436 misclassified as fixed, dedup fail-open, content-hash-alone too aggressive, soft-delete filter missing, tier-ordering contradiction). All folded in pre-merge. Tests: 65 new wave cases across 7 new files + 1 extended; all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.41.13.0) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -809,6 +809,29 @@ export class PostgresEngine implements BrainEngine {
|
||||
return rowToPage(rows[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.41.13 (#1309) — identity-based dedup pre-check.
|
||||
* See `BrainEngine.findDuplicatePage` for the contract.
|
||||
*/
|
||||
async findDuplicatePage(
|
||||
sourceId: string,
|
||||
opts: { hash: string; frontmatterId?: string | null },
|
||||
): Promise<{ slug: string; id: number } | null> {
|
||||
const sql = this.sql;
|
||||
const fmId = opts.frontmatterId ?? null;
|
||||
const rows = await sql`
|
||||
SELECT id, slug FROM pages
|
||||
WHERE source_id = ${sourceId}
|
||||
AND deleted_at IS NULL
|
||||
AND (content_hash = ${opts.hash} OR (frontmatter->>'id' = ${fmId} AND ${fmId}::text IS NOT NULL))
|
||||
ORDER BY id
|
||||
LIMIT 1
|
||||
`;
|
||||
if (rows.length === 0) return null;
|
||||
const r = rows[0] as { id: number | string; slug: string };
|
||||
return { slug: r.slug, id: Number(r.id) };
|
||||
}
|
||||
|
||||
async putPage(slug: string, page: PageInput, opts?: { sourceId?: string }): Promise<Page> {
|
||||
slug = validateSlug(slug);
|
||||
const sql = this.sql;
|
||||
@@ -1275,18 +1298,31 @@ export class PostgresEngine implements BrainEngine {
|
||||
}));
|
||||
}
|
||||
|
||||
async resolveSlugs(partial: string): Promise<string[]> {
|
||||
async resolveSlugs(partial: string, opts?: { sourceId?: string; sourceIds?: string[] }): Promise<string[]> {
|
||||
const sql = this.sql;
|
||||
|
||||
// v0.41.13 #1436: source scope via postgres.js tagged-template
|
||||
// fragments. When neither opt is set the resolver stays unscoped
|
||||
// for back-compat with internal callers. The `deleted_at IS NULL`
|
||||
// filter excludes soft-deleted rows (v0.26.5) from fuzzy candidates
|
||||
// — they're not legitimate match targets for a remote `get_page`.
|
||||
const sources = opts?.sourceIds ?? null;
|
||||
const scalar = opts?.sourceId ?? null;
|
||||
const scopeFragment = sources
|
||||
? sql` AND source_id = ANY(${sources}::text[])`
|
||||
: scalar
|
||||
? sql` AND source_id = ${scalar}`
|
||||
: sql``;
|
||||
|
||||
// Try exact match first
|
||||
const exact = await sql`SELECT slug FROM pages WHERE slug = ${partial}`;
|
||||
const exact = await sql`SELECT slug FROM pages WHERE slug = ${partial} AND deleted_at IS NULL${scopeFragment}`;
|
||||
if (exact.length > 0) return [exact[0].slug];
|
||||
|
||||
// Fuzzy match via pg_trgm
|
||||
const fuzzy = await sql`
|
||||
SELECT slug, similarity(title, ${partial}) AS sim
|
||||
FROM pages
|
||||
WHERE title % ${partial} OR slug ILIKE ${'%' + partial + '%'}
|
||||
WHERE deleted_at IS NULL AND (title % ${partial} OR slug ILIKE ${'%' + partial + '%'})${scopeFragment}
|
||||
ORDER BY sim DESC
|
||||
LIMIT 5
|
||||
`;
|
||||
@@ -1722,7 +1758,9 @@ export class PostgresEngine implements BrainEngine {
|
||||
raw_score * ${sourceFactorCaseOnSlug} AS score,
|
||||
false AS stale
|
||||
FROM hnsw_candidates
|
||||
ORDER BY score DESC
|
||||
-- v0.41.13: stable tiebreaker for tied scores. See pglite-engine for
|
||||
-- rationale (basis-vector test fixtures, planner-dependent ordering).
|
||||
ORDER BY score DESC, page_id ASC, chunk_id ASC
|
||||
LIMIT ${limitParam}
|
||||
OFFSET ${offsetParam}
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user