* 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>
12 KiB
Pack-Upgrade Mechanism (v0.41.22)
How
gbrain-base@1.x → gbrain-base-v2@1.0.0(and any future pack succession) wires through the onboard cathedral.
The contract
A schema pack manifest can declare a migration_from field:
api_version: gbrain-schema-pack-v1
name: gbrain-base-v2
version: 1.0.0
migration_from:
pack: gbrain-base
version: "1.x"
When this declaration is present + a mapping_rules: block is
populated, the pack registers itself as the successor to
(parent_pack, version_range). Any brain whose active pack matches
that tuple lights up the pack_upgrade_available onboard check.
End-to-end flow
┌────────────────────────────────────────────────────────────────┐
│ PACK AUTHORING │
│ │
│ Author declares: migration_from: {pack: P, version: R} │
│ + mapping_rules: [retype/page_to_link/page_to_alias] │
│ Pack ships bundled OR via ~/.gbrain/schema-packs/<name>/ │
└──────────────────────────┬─────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────────────┐
│ ONBOARD CHECK DISCOVERY │
│ │
│ checkPackUpgradeAvailable(engine) at src/core/onboard/ │
│ checks.ts: │
│ 1. Read engine.getConfig('schema_pack') for dbConfig tier │
│ 2. loadActivePack({cfg: null, remote: false, dbConfig}) │
│ 3. findPackSuccessors(active.name, active.version) │
│ → walks BUNDLED_PACK_NAMES + ~/.gbrain/schema-packs/ │
│ → matches via _versionRangeMatches(version, range) │
│ → returns ResolvedPack[] sorted by successor version │
│ 4. If successors.length > 0, emit OnboardCheckResult │
│ with RemediationStep targeting `unify-types` handler │
│ + protected: true (D17 → manual_only via render │
│ allowlist) │
└──────────────────────────┬─────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────────────┐
│ USER DECIDES │
│ │
│ gbrain onboard --check shows finding │
│ gbrain onboard --check --explain shows per-cluster narrative │
│ User reviews; if OK, runs: │
│ gbrain jobs submit unify-types --allow-protected \ │
│ --params '{"target_pack":"gbrain-base-v2"}' │
│ (Autopilot never auto-fires this; manual_only) │
└──────────────────────────┬─────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────────────┐
│ HANDLER EXECUTION (src/core/schema-pack/unify-types-handler.ts) │
│ │
│ 1. Preflight: load target pack; assert mapping_rules present │
│ 2. Stats snapshot (pre-state for celebration) │
│ 3. Acquire gbrain-unify db-lock (60min TTL) │
│ 4. Apply phases (4): │
│ a. Explicit retype rules (chunked UPDATE 1000/batch) │
│ - frontmatter.legacy_type ALWAYS preserved (D8) │
│ - frontmatter.subtype stamped when subtype set │
│ b. Catch-all retype: synthesize per-unknown-type rule │
│ excluding declared types + explicit targets + page_to_ │
│ link/alias sources (D12 + critical bug fix) │
│ c. Page-to-link: parse body+frontmatter, insert link row, │
│ soft-delete source page (per-page atomicity per F7) │
│ d. Page-to-alias: insert slug_aliases row, soft-delete │
│ source page (NO rewriteLinks per D15) │
│ 5. Final sync: path-prefix typing for residual UNTYPED rows │
│ 6. ACTIVE-PACK FLIP (D13): │
│ - engine.setConfig('schema_pack', target_pack) │
│ - saveConfig({...existing, schema_pack: target_pack}) │
│ 7. Verify: re-run stats; warn if ≤ declared + 5 violated │
│ 8. Celebration summary to stderr + audit JSONL │
│ 9. Release db-lock │
└──────────────────────────┬─────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────────────┐
│ POST-UPGRADE STATE │
│ │
│ • pages.type updated with canonical types │
│ • frontmatter.legacy_type preserved for rollback │
│ • slug_aliases populated for old-slug → canonical lookup │
│ • links table has new partner_of / relates_to rows │
│ • Source pages soft-deleted (72h TTL for restore) │
│ • Active pack flipped to target_pack │
│ • Next gbrain onboard --check shows ok │
└────────────────────────────────────────────────────────────────┘
Version-range semantics
migration_from.version accepts three shapes:
| Form | Matches |
|---|---|
1.0.0 (exact literal) |
1.0.0 only |
1.x (major wildcard) |
1.0.0, 1.5.2, 1.99.99 |
1.0.x (minor wildcard) |
1.0.0, 1.0.5, 1.0.99 |
* is accepted as an alias for x.
Implementation: _versionRangeMatches(version, range) in
src/core/schema-pack/load-active.ts. Pinned by
test/schema-pack-find-pack-successors.test.ts.
findPackSuccessors discovery
Walks BUNDLED_PACK_NAMES (currently gbrain-base,
gbrain-recommended, gbrain-creator, gbrain-investor,
gbrain-engineer, gbrain-everything, gbrain-base-v2). For each
candidate ≠ the active pack name, loads the manifest via
loadActivePack({ perCall: candidate }), checks
migration_from.pack === activeName && _versionRangeMatches(activeVer, migration_from.version). Returns matching packs sorted by version
descending.
v0.41.22 covers bundled packs only. v0.43+ TODO: enumerate user-installed
packs at ~/.gbrain/schema-packs/*/pack.yaml (defer to v0.43 since the
filesystem-scan cost needs the cache invalidation strategy from
registry.ts).
The manual_only apply policy
The shipped onboard contract has 3 apply_policy values:
| Policy | Meaning |
|---|---|
auto_apply |
Autopilot runs unattended |
prompt_required |
Autopilot in --auto-with-prompt mode prompts user |
manual_only |
Autopilot NEVER auto-fires; user must explicitly submit |
pack_upgrade_available emits a RemediationStep with protected: true + job: 'unify-types'. toOnboardRecommendation in
src/core/onboard/render.ts maps this to manual_only via the
MANUAL_ONLY_PROTECTED_JOBS allowlist (which also contains
extract-takes-from-pages per v0.41.18 A12+A24).
Rationale: pack upgrades change the brain's taxonomy. Taxonomy is a
user judgment call — not autopilot's call. Even with --auto-with- prompt, prompting the user to confirm a pack upgrade mid-tick is the
wrong UX (the user came to fix orphans, not to be interrupted with
"hey want to migrate your taxonomy?"). Explicit submission is the
right boundary.
Authoring a successor pack
Minimal example for an academic-research brain that adds a
researcher canonical:
api_version: gbrain-schema-pack-v1
name: gbrain-academic-v1
version: 1.0.0
description: Academic research brain — adds researcher canonical
gbrain_min_version: 0.42.0
extends: null
migration_from:
pack: gbrain-base-v2
version: "1.x"
page_types:
# Inherit gbrain-base-v2's 15 types here (or use extends to merge
# automatically once v0.43+ extends-chain composition lands)
- { name: person, primitive: entity, path_prefixes: [people/], expert_routing: true }
- { name: company, primitive: entity, path_prefixes: [companies/], expert_routing: true }
# ... all 13 other v2 canonicals ...
- { name: note, primitive: concept, path_prefixes: [notes/], extractable: true }
# Academic addition:
- name: researcher
primitive: entity
path_prefixes: [researchers/]
aliases: [academic, professor, scholar]
extractable: false
expert_routing: true
mapping_rules:
# All v2 mapping rules (copy from v2 yaml)
# ... ~40 rules ...
# Custom: relocate v2-tagged academics to researcher
- { kind: retype, from_type: person, to_type: researcher, path_filter: 'researchers/%' }
# Catch-all
- kind: retype
from_type: "*unknown*"
to_type: note
subtype_field: legacy_type
subtype: "*original_type*"
Drop at ~/.gbrain/schema-packs/gbrain-academic-v1/pack.yaml.
Discoverable via gbrain schema list. Activatable via
gbrain schema use gbrain-academic-v1. Once active, the
pack_upgrade_available check fires for any brain on
gbrain-base-v2@1.x and surfaces a unify-types RemediationStep
targeting your pack.
Lock + concurrency
gbrain-unify is a dedicated gbrain_cycle_locks row name (60min
TTL). The handler acquires it before any apply phase + releases in
finally. Two simultaneous gbrain jobs submit unify-types
invocations: second one fails fast at lock acquisition with a clear
error. Same pattern as gbrain-sync (v0.22.13 PR #490).
Audit trail
Every unify run writes to ~/.gbrain/audit/schema-unify-YYYY-Www.jsonl
(ISO-week rotation, mirrors existing audit channels). Records: pack
identities (before + after), per-phase counts (would_apply + applied),
warnings, completion timestamp. Privacy: page slugs are NOT logged in
bulk (only the per-rule sample_slugs[≤10]); for forensic debugging
add GBRAIN_AUDIT_FULL=1 (v0.43+ TODO; not yet wired).
What's NOT yet supported
- Subprocess sandbox for the publish-gate (v0.43+ TODO)
- Per-source pack-upgrade (the handler accepts
sourceIdbutfindPackSuccessorsdoesn't yet pass it through) - Cross-brain federated mounts that disagree on canonical packs
- Automatic rollback (today: manual SQL or
gbrain pages restore) - LLM-assisted mapping_rules codegen from production data (
gbrain schema detect-mappings; deferred to v0.43+)
Reference
- Pack file:
src/core/schema-pack/base/gbrain-base-v2.yaml - Manifest extension:
src/core/schema-pack/manifest-v1.ts - Successor walker:
src/core/schema-pack/load-active.ts:findPackSuccessors - Onboard check:
src/core/onboard/checks.ts:checkPackUpgradeAvailable - Render allowlist:
src/core/onboard/render.ts:MANUAL_ONLY_PROTECTED_JOBS - Handler:
src/core/schema-pack/unify-types-handler.ts - Migration:
src/core/migrate.ts:105(slug_aliases table) - Type taxonomy doc:
docs/architecture/type-taxonomy.md - Skill:
skills/schema-unify/SKILL.md