* 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>
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 |
|
|
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 --checkand seespack_upgrade_availableortype_proliferationwarnings - User asks "what is the canonical taxonomy / how do I clean up my page types / migrate to v2"
- A
dangling_aliasesfinding 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:
- Preflight (validate target pack has
mapping_rules:) - Stats snapshot (pre-state for celebration summary)
- Acquire
gbrain-unifydb-lock (60min TTL) - 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)
- Final sync (untyped rows by path-prefix)
- Flip active pack to gbrain-base-v2 (D13)
- Verify + celebration summary
Phase 4: Verify
gbrain onboard --check
gbrain schema stats
Expected:
pack_upgrade_available→ok(active pack is now v2)type_proliferation→ok(≤16 distinct typed values)dangling_aliases→ok(slug_aliases all point at active canonicals)gbrain schema statsshows ≤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 tonotewithlegacy_typepreserved. - Don't rewrite body-text wikilinks. D15: the slug_aliases table IS the resolver.
[[old-redirect-slug]]keeps working viaengine.resolveSlugWithAliasshort-circuit. - Don't bypass the dry-run. Always run
--explainbefore applying. The trust delta is real. - Don't run two unify jobs concurrently. The
gbrain-unifydb-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 withmigration_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_typepreserved (per-page rollback signal). slug_aliasesrows for concept-redirect pages (alias table IS the resolver — no link rewrite).- Real
linksrows for edge-shaped pages (atom-partner-link,symlink, etc.). - Active pack flipped to
gbrain-base-v2atomically 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 Xalias-expands viaexpandTypeFilter(D14 back-compat).
Failure modes:
- Concurrent submission rejected by the
gbrain-unifydb-lock; second call exits gracefully. - Catch-all retype excludes
page_to_link+page_to_aliassource 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-typesdirectly via the MCPsubmit_jobop without--allow-protected. PROTECTED handlers require trusted local callers; remote MCP rejection is the intentional trust boundary. - Edit
mapping_rulesingbrain-base-v2.yamlto 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-typesfrom inside an autopilot tick. The check ismanual_onlyper 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_typesurvives 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