Files
gbrain/skills/schema-unify/SKILL.md
T
5d42f3295e v0.41.22.0 feat: type-unification cathedral — 94 types → 15 canonical (closes #1479) (#1542)
* Merge branch 'master' into garrytan/type-taxonomy-unification

Resolve VERSION, package.json, CHANGELOG conflicts with v0.41.22.0
on top, preserving master's v0.41.19.0 entry below.

* feat: v0.41.22.0 type-unification cathedral — collapse 94 types to 15 (closes #1479)

Ships gbrain-base-v2 as the new install default (15 canonical types: 14
+ note catch-all) and the unify-types PROTECTED Minion handler that
runs the gbrain-base→v2 migration end-to-end on existing brains.

What this delivers:
- gbrain-base-v2.yaml standalone schema pack (no extends:) with 14
  canonical page_types + 9 cluster mapping_rules + catch-all sentinel
- 3 new schema-pack primitives: runRetypeCore (chunked UPDATE with
  legacy_type stamping), runPageToLinkCore (edge-shaped pages →
  link rows), runPageToAliasCore (concept-redirect → slug_aliases)
- rewriteLinksBatch for N-pair atomic FK rewrite
- Migration v104 slug_aliases table (forward-bootstrap probed on both
  engines for safe upgrade chain)
- New engine method resolveSlugWithAlias(slug, sourceOrSources) on
  both Postgres + PGLite with multi-source ambiguity warning
- inferTypeAndSubtypeFromPack overload + subtypes: + mapping_rules:
  + migration_from: schema-pack manifest extensions
- findPackSuccessors version-range walker (1.x / 1.0.x / exact match)
- expandTypeFilter for --type back-compat (D14): legacy aliases route
  through mapping_rules → canonical+subtype before the SQL filter fires
- 3 new onboard checks: pack_upgrade_available, type_proliferation,
  dangling_aliases (source-scoped per F12)
- unify-types Minion handler (PROTECTED, manual_only via render.ts
  allowlist per D17): retype-explicit → retype-catch-all →
  page-to-link → page-to-alias → final sync → active-pack flip
- alias_resolved 1.05x post-fusion search boost stage; KNOBS_HASH_VERSION
  bumped 5→6 (one-time cache miss on upgrade, self-healing in TTL)
- ELIGIBLE_TYPES for facts extraction extended with v2 canonicals
  (codex F-ELIGIBLE: blocker not v0.43 follow-up)

Tests: 79 new unit/integration cases + 3 E2E cases covering all 9
production clusters end-to-end. 124-case verification on the cache-key
+ build-llms fixes. KNOBS_HASH_VERSION assertions updated in 3 tests.

Plan: ~/.claude/plans/system-instruction-you-are-working-transient-elephant.md
(16 locked decisions D1-D17, 12 baseline fixes F7-F21 absorbed from
codex outside voice).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: CI verify failures — system-of-record allow-comment + schema-unify manifest registration

Two CI failures on PR #1542:

1. check:system-of-record flagged page-to-link.ts:207 addLinksBatch as
   a direct write to a derived table. The call IS the reconcile surface
   for page_to_link mapping_rules — it converts edge-shaped pages into
   canonical link rows under the PROTECTED unify-types Minion handler,
   source-scoped, atomic per-rule. Added the canonical
   `// gbrain-allow-direct-insert: <reason>` comment on the same line.

2. check:resolver emitted 11 orphan_trigger warnings for `schema-unify`
   because the skill was added to skills/RESOLVER.md without a
   corresponding entry in skills/manifest.json. Added the registration
   under the existing skills[] array.

bun run verify: 28/28 checks pass locally.

* fix: CI test failures — schema-unify conformance + eligibility regression

Six test failures across shards 2 + 10 on PR #1542:

1. resolver.test.ts: round-trip parser requires frontmatter triggers to
   be quoted (`- "..."` or `- '...'`). schema-unify shipped with bare
   YAML strings; quoted the 10 triggers to round-trip correctly.

2. skills-conformance.test.ts (×3): schema-unify SKILL.md was missing
   the required Contract, Anti-Patterns, and Output Format sections
   that every conformant skill must declare. Added all three:
   - Contract: inputs / outputs / side effects / failure modes
   - Anti-Patterns: 5 DON'Ts including the autopilot trust boundary
   - Output Format: per-phase stderr lines + celebration summary +
     JSON envelope shape

3. facts-eligibility.test.ts (×2): the v0.41.22 ELIGIBLE_TYPES
   expansion added `concept` to the eligible list, but the existing
   test suite pins concept as rejected (it's `extractable: true` in
   the schema pack but the v0.41.11 contract documented this as
   "cosmetic on the backstop path because backstop uses hardcoded
   ELIGIBLE_TYPES"). Removed `concept` from the expansion; other v2
   canonicals (media, tweet, atom, analysis) stay. Comment updated
   to document the deliberate omission.

All 6 failing tests now pass locally (370/370 across the 3 affected
files). bun run verify: 28/28 checks green.

* fix: harden findPackSuccessors test against shard pollution

CI shard 8 reported 1 fail (1.00ms — too fast for any real loadActivePack
file I/O) on `finds gbrain-base-v2 as successor of gbrain-base@1.0.0`.
Local triple-run passes 9/9 in isolation.

Root cause: the existing afterEach reset clears the module-level pack
cache AFTER each test, but the FIRST test in the file inherits whatever
state sibling files in the same bun shard process left behind. With
24+ schema-pack tests in shard 8 (mutate, mutate-audit, best-effort,
registry-reload, manifest-v041_2, etc.) running before this file, the
first test can read a poisoned cache.

Fix: add `beforeEach(_resetPackCacheForTests)`. Two-sided reset
guarantees clean state regardless of file ordering within the shard.

bun run verify: 28/28 checks pass.

* fix: quarantine two flaky tests to serial runner

CI shard 1 + shard 8 each surfaced one intermittent failure:

shard 1: buildBrainTools > execute() on put_page with valid namespace
shard 8: findPackSuccessors > finds gbrain-base-v2 as successor

Both pass cleanly in isolation. Both are concurrency races against
shared in-shard state:

- brain-allowlist.test.ts shares a singleton PGLiteEngine across 18
  tests with a beforeEach DELETE FROM pages. With max-concurrency=4,
  two put_page tests can interleave their TRUNCATE + write phases,
  so the auto-link/extract sub-steps inside put_page race against
  the sibling test's DELETE.
- schema-pack-find-pack-successors.test.ts reads bundled YAML packs
  via loadActivePack. The module-level pack cache is shared across
  parallel tests in the same shard; the previous beforeEach reset
  helped but didn't fully isolate against concurrent file reads
  under CI load.

Fix per CLAUDE.md test-isolation lint rule R2 (concurrency-fragile
files belong in the .serial.test.ts quarantine): rename both files
to *.serial.test.ts. Serial runner picks them up at max-concurrency=1.
49/49 serial files pass locally. 28/28 verify checks pass.

* fix: quarantine embed-stale test to serial runner

CI shard 9 reported 6 failures, all from the embedStaleForSource describe
block, all ~120-150ms each — classic shared-engine concurrency race shape.
Passes 7/7 locally in isolation.

Root cause: embed-stale.test.ts shares a singleton PGLiteEngine across 7
tests with beforeEach resetPgliteState. Under bun's max-concurrency=4 in
the parallel shard, two tests can interleave their TRUNCATE + seedPage +
upsertChunks + embedStaleForSource flow, so one test's stale-chunk count
sees another test's mid-flight writes.

Same fix as brain-allowlist.serial.test.ts and
schema-pack-find-pack-successors.serial.test.ts: rename to *.serial.test.ts
so the serial runner picks it up at max-concurrency=1.

bun run verify: 28/28 checks pass. 7/7 embed-stale tests pass via serial.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 07:01:28 -07:00

11 KiB

name, description, brain_first, tools, triggers
name description brain_first tools triggers
schema-unify Migrate a brain from gbrain-base (or any pack) to gbrain-base-v2's 14-canonical-type taxonomy via gbrain onboard --check + the unify-types Minion handler. Collapses 94 noisy types to 15 canonical with subtypes, alias rows, and link rows. Triggers when an agent notices pack_upgrade_available, type_proliferation, or asks "what is the canonical taxonomy / how do I clean up my page types". exempt
gbrain onboard --check
gbrain onboard --check --explain
gbrain onboard --check --json
gbrain jobs submit unify-types
gbrain jobs follow
gbrain schema active
gbrain schema use
gbrain schema stats
gbrain pages restore
mcp:run_onboard
unify my types
migrate to gbrain-base-v2
94 types to 14
apply canonical taxonomy
clean up my page types
pack upgrade
shrink type proliferation
what does the canonical taxonomy look like
consolidate page types
retype pages to canonical

Schema Unification (gbrain-base → gbrain-base-v2)

v0.41.22 ships gbrain-base-v2 — a 15-type DRY/MECE taxonomy (14 canonical + note catch-all) — as the install default for new brains. Existing brains on gbrain-base can opt in via the pack_upgrade_available onboard finding + the unify-types PROTECTED Minion handler.

This skill is the playbook for that migration.

brain_first: exempt

This skill is ABOUT the brain's shape — it can't depend on the brain it's reshaping. No gbrain search lookup first; jump straight to onboard.

When this skill fires

  • Agent runs gbrain onboard --check and sees pack_upgrade_available or type_proliferation warnings
  • User asks "what is the canonical taxonomy / how do I clean up my page types / migrate to v2"
  • A dangling_aliases finding surfaces (post-unify GC)
  • An agent ingesting from a custom pack wants to consult the v2 taxonomy as a reference

Mental model (one paragraph)

A production gbrain brain accreted 94 distinct pages.type values over years of ingestion: tweet / tweet-thread / tweet-bundle / tweet-single / media/x-tweet/bundle / tweet-stub all coexisting; 5.5K concept-redirect pages; atom-partner-link pages that should be links; civic / framework / insight / memo / anecdote one-offs. The cure: collapse to 15 canonical types (person, company, media, tweet, social-digest, analysis, atom, concept, source, deal, email, slack, writing, project, note) with subtypes/format/origin pushed to frontmatter, alias-rows for redirects, real link-rows for edge-shaped pages, and a catch-all that bins long-tail unknowns to note with frontmatter.legacy_type = <original> for rollback.

Workflow

Phase 1: Discovery

Confirm the brain is actually on gbrain-base (not already on v2).

gbrain schema active --json | jq -r '.identity'

Expected: gbrain-base@1.0.0+<sha>. If you see gbrain-base-v2@..., the brain is already on v2 — skip the migration.

Then run onboard to see what would change:

gbrain onboard --check

Look for the pack_upgrade_available finding. If it's ok, there's no successor declared for the active pack — done.

Phase 2: Preview

Run the per-cluster narrative:

gbrain onboard --check --explain

This invokes the unify-types handler in dry-run mode and prints:

  • How many pages would retype per cluster (tweets, articles, companies, etc.)
  • How many concept-redirect pages would become alias rows
  • How many edge-shaped pages would convert to real links
  • The synthesized catch-all rules for unknown types

Review the output. If the proposed changes look wrong, don't proceed — file an issue or write a custom pack with adjusted mapping_rules.

Phase 3: Apply

The handler is PROTECTED (manual_only per D17) — autopilot will never auto-fire it. Submit explicitly:

gbrain jobs submit unify-types \
  --allow-protected \
  --params '{"target_pack":"gbrain-base-v2"}'

Watch progress per phase:

gbrain jobs follow <job_id>

On a 186K-page brain expect ~10 minutes. The handler runs:

  1. Preflight (validate target pack has mapping_rules:)
  2. Stats snapshot (pre-state for celebration summary)
  3. Acquire gbrain-unify db-lock (60min TTL)
  4. Apply phases:
    • Explicit retype rules (tweets, articles, companies, etc.)
    • Catch-all retype (unknown types → note with legacy_type)
    • Page-to-link rules (atom-partner-link, symlink)
    • Page-to-alias rules (concept-redirect)
  5. Final sync (untyped rows by path-prefix)
  6. Flip active pack to gbrain-base-v2 (D13)
  7. Verify + celebration summary

Phase 4: Verify

gbrain onboard --check
gbrain schema stats

Expected:

  • pack_upgrade_availableok (active pack is now v2)
  • type_proliferationok (≤16 distinct typed values)
  • dangling_aliasesok (slug_aliases all point at active canonicals)
  • gbrain schema stats shows ≤16 distinct types

Phase 5: Post-migration

Anything that used --type article keeps working post-unify if your CLI calls go through the expandTypeFilter helper (it expands article to media+subtype=article automatically). Direct SQL against pages.type needs updating to the canonical types.

Search queries get a small ranking signal: pages reached via slug_aliases (canonicals of one or more aliases) get a 1.05x boost. Visible via gbrain search --explain.

Rollback

Every retyped page preserves frontmatter.legacy_type = <original> per D8. Restore types via:

UPDATE pages SET type = frontmatter->>'legacy_type'
WHERE source_id = 'default' AND frontmatter->>'legacy_type' IS NOT NULL;

Page-to-alias and page-to-link source pages soft-delete with 72h TTL. Restore within that window:

gbrain pages restore <slug>

Revert the active pack flip:

gbrain schema use gbrain-base

Anti-patterns

  • Don't run unify-types under autopilot. It's manual_only by design. Autopilot remediation should never silently change your taxonomy.
  • Don't expect mapping_rules to cover every legacy type explicitly. Use the catch-all (*unknown*) for the long tail. Pages get retyped to note with legacy_type preserved.
  • Don't rewrite body-text wikilinks. D15: the slug_aliases table IS the resolver. [[old-redirect-slug]] keeps working via engine.resolveSlugWithAlias short-circuit.
  • Don't bypass the dry-run. Always run --explain before applying. The trust delta is real.
  • Don't run two unify jobs concurrently. The gbrain-unify db-lock serializes them; the second submission rejects with "already in progress."

Decision tree

Active pack already gbrain-base-v2?
  → Skip migration.

Custom pack with own mapping_rules?
  → Run --check --explain to see if your pack declares migration_from
    for the active pack. If yes, target_pack = your pack name.

Brain has many custom types not covered by gbrain-base-v2 mapping_rules?
  → The catch-all retype binds them to `note` with legacy_type preserved.
    Review by inspecting frontmatter.legacy_type after the migration.

Federated brain (multiple sources)?
  → Add --params source_id to scope the migration per-source. Each
    source can be migrated independently.

Worried about a specific cluster's mapping?
  → Fork gbrain-base-v2 (`gbrain schema fork gbrain-base-v2 my-pack`),
    edit mapping_rules in your fork, then target the fork.

Contract

Inputs:

  • A brain on gbrain-base (or any pack with migration_from: gbrain-base-v2).
  • Write access to submit a PROTECTED Minion handler (--allow-protected).
  • ~10 min wallclock on a 186K-page brain.

Outputs:

  • Pages retyped to canonical types with frontmatter.legacy_type preserved (per-page rollback signal).
  • slug_aliases rows for concept-redirect pages (alias table IS the resolver — no link rewrite).
  • Real links rows for edge-shaped pages (atom-partner-link, symlink, etc.).
  • Active pack flipped to gbrain-base-v2 atomically at end of successful run.

Side effects:

  • Source pages soft-deleted with 72h restore TTL (gbrain pages restore <slug>).
  • One-time cache invalidation on KNOBS_HASH_VERSION bump (5→6); self-healing in cache.ttl_seconds.
  • Query-time --type X alias-expands via expandTypeFilter (D14 back-compat).

Failure modes:

  • Concurrent submission rejected by the gbrain-unify db-lock; second call exits gracefully.
  • Catch-all retype excludes page_to_link + page_to_alias source types (caught in E2E pre-merge).
  • Phase failures abort the run before active_pack_flipped; partial state restorable via op_checkpoint resume.

Anti-Patterns

DON'T:

  • Submit unify-types directly via the MCP submit_job op without --allow-protected. PROTECTED handlers require trusted local callers; remote MCP rejection is the intentional trust boundary.
  • Edit mapping_rules in gbrain-base-v2.yaml to skip clusters you don't trust. Fork the pack instead (gbrain schema fork) so the source-of-truth migration stays consistent across brains.
  • Run unify-types from inside an autopilot tick. The check is manual_only per D17 — autopilot deliberately never auto-fires it because pack upgrades are one-time consenting taxonomy decisions.
  • Hard-delete soft-deleted source pages before the 72h restore window. Use gbrain pages restore <slug> first if rollback is needed.
  • Assume frontmatter.legacy_type survives every roundtrip. The marker is canonical for the immediate post-migration window; downstream re-imports may overwrite it.

Output Format

Per phase, the handler emits to stderr:

[unify-types] phase=retype-explicit applied=N skipped=M  cost=USD  ttl=Ns
[unify-types] phase=retype-catch-all applied=N
[unify-types] phase=page-to-link converted=N pages soft-deleted
[unify-types] phase=page-to-alias aliased=N pages soft-deleted
[unify-types] phase=sync residual=N
[unify-types] active_pack flipped from gbrain-base to gbrain-base-v2

Final celebration summary to stderr:

═══════════════════════════════════════════════════════════
  gbrain-base-v2 migration complete
═══════════════════════════════════════════════════════════
  Before: 94 distinct page types
  After:  15 canonical types
  Retyped:      25,632 pages
  Aliased:       5,521 redirects → slug_aliases table
  Linkified:        65 ghost pages → real link rows
  Soft-deleted:  5,586 pages (restorable for 72h)
═══════════════════════════════════════════════════════════

JSON output (gbrain jobs follow <id> --json) returns the structured UnifyTypesResult shape with per_phase, pack_identity_after, active_pack_flipped.

Reference

  • Plan + decisions: ~/.claude/plans/system-instruction-you-are-working-transient-elephant.md
  • Architecture: docs/architecture/type-taxonomy.md
  • Pack-upgrade mechanism: docs/architecture/pack-upgrade-mechanism.md
  • Issue: https://github.com/garrytan/gbrain/issues/1479