mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 19:49:14 +00:00
put_page upserts with ON CONFLICT (source_id, slug) on both engines. When
the arbiter unique index is missing, every write fails with "no unique or
exclusion constraint matching the ON CONFLICT specification" while reads
stay green — and there was no recovery path: initSchema() is additive-only
and apply-migrations --force-schema re-runs from the current version (a
no-op at head). The root cause is that migration v23 (Postgres) / v21
(PGLite) guarded the ADD CONSTRAINT on pg_constraint.conname — a NAME
match, not a shape match.
Fix:
- src/core/pages-unique-repair.ts: shared column-shape probe over pg_index
(non-partial, non-expression unique index with key columns exactly
{source_id, slug}, either order, constraint or bare index — what ON
CONFLICT inference actually accepts), check + repair helpers, and the
shape-guarded DDL shared by the migrations and the self-heal. Falls back
to a different constraint name when pages_source_slug_key is taken by a
wrong-shaped relation; never auto-deletes duplicate pages.
- src/core/migrate.ts: v23/v21 guards now use the shared column-based DDL;
runMigrations runs the repair on EVERY pass (same always-run slot as the
#2038 timeline heal), so a wedged brain heals on the next init/migrate.
- src/commands/doctor.ts: new pages_unique_index check (BRAIN category,
wired into both buildChecks and doctorReportRemote) that FAILs with the
literal repair SQL; OK/skip when pages or source_id doesn't exist yet.
- test/pages-unique-repair.test.ts: reproduction, migrate-pass heal
(fails behaviorally on master), shape-not-name detection, partial-index
rejection, name-collision fallback, idempotency, fresh-brain skip.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>