# Convention: schema evolution — when to add a type vs alias vs prefix Cross-cutting convention for any skill that proposes a change to the active schema pack. Read first before invoking `schema-author`. The goal: keep the pack small enough that an agent can hold the whole type graph in its head, but expressive enough that custom domains (research, legal, founder ops) get first-class types. ## Decision tree ``` You see a cluster of pages that share a domain meaning. │ ▼ How many pages in the cluster? │ ┌─────┴───────┬──────────────┐ ▼ ▼ ▼ <20 20-100 100+ │ │ │ ▼ ▼ ▼ One-off. Big enough. First-class. Don't pack- Add an alias Add a new codify. to an existing page_type with type OR a its own prefix, Use the narrow prefix primitive, and nearest branch. flags. existing type + frontmatter tag. ``` ### Concrete examples **One-off (don't add to pack):** > "I have 3 pages under `2026-projects/skunkworks-spec/`. Should I add > a `skunkworks` type?" No. Three pages doesn't justify a permanent pack entry. Type these as the nearest existing match (`concept` or `note`) and use a frontmatter `project:` tag. If the cluster grows to 20+, revisit. **20-100 pages — alias OR narrow prefix:** > "I have 50 pages under `people/researchers/` that overlap with my > `person` type. Should I add a `researcher` type?" Two valid options: 1. **Alias on `person`** — `add-alias person researcher`. Closure queries for `researcher` will surface `person` rows too. 2. **New type sharing the `entity` primitive** — `add-type researcher --primitive entity --prefix people/researchers/`. Distinct type, can be marked `--extractable` or `--expert` independently. Pick alias when researchers are people first, researchers second (they share enrichment rules, expert-routing semantics, link verbs). Pick new type when researcher-specific behavior diverges (different extractable rules, different link verbs, different rubric). **100+ pages — first-class type:** > "I have 4000 pages under `meetings/`. I want them typed as `meeting`, > not the legacy default `note`." Add the type: ``` gbrain schema add-type meeting \ --primitive temporal \ --prefix meetings/ \ --extractable gbrain schema sync --apply ``` The `sync --apply` backfills all 4000 pages. From here forward, imports under `meetings/` infer `meeting` type via the pack. ## Don'ts - **Don't add a type for a directory you imported once for triage.** Pack types are permanent decisions; one-time imports are not. - **Don't add a type just to silence `dead_prefixes` in `schema stats`.** A dead prefix is a *signal* that the prefix is mis-declared or the corpus moved. Remove the prefix or migrate the content, don't add an empty type. - **Don't promote a candidate from `schema suggest` without verifying the path prefix matches real content.** The suggester is heuristic; it can propose types that overlap existing ones. Run `lint --with-db` before `add-type` to catch prefix collisions pre-write. - **Don't add `--expert` to a type that has no `path_prefixes`.** The `expert_routing_without_prefix` lint rule warns about this exact shape: an expert-routed type with no prefix never matches a put_page inference, so `whoknows` silently never surfaces it. - **Don't mutate `gbrain-base` or `gbrain-recommended`.** Fork first. ## When to remove a type Removing a type is RARE. Only do it when: 1. The type was added in error (typo, premature abstraction). 2. The corpus the type was meant for has been migrated to a different type. 3. The type is dangling (no `path_prefixes` actually match pages, no queries reference it, no other type's aliases/link_types reference it). `remove-type` is guarded by the `STILL_REFERENCED` check (codex C14): if ANY other type's aliases / enrichable_types / link_types / frontmatter_links references the target, the remove fails loud with the reference list. Break those references first. ## When to commit the pack If your pack lives in source control (`~/.gbrain/schema-packs//` is a git repo), commit after every batch of mutations. The `mutation_count_anomaly` lint rule warns at >50 mutations in 7 days — that's the hint to start committing rather than relying on disk-only state. ## When to upgrade your pack (v0.42+) A pack can declare `migration_from: {pack: , version: }` to register itself as the successor to another pack. When a brain's active pack matches the declared `from`, the `pack_upgrade_available` onboard check surfaces the successor + a `manual_only` RemediationStep pointing at the `unify-types` PROTECTED Minion handler. v0.41.22 ships **gbrain-base-v2** as the declared successor to gbrain-base@1.x — collapses 94 noisy types to 15 canonical via declarative mapping_rules. Run via `gbrain onboard --check --explain` (preview) → `gbrain jobs submit unify-types --params '{"target_pack":"gbrain-base-v2","apply":true}'` (apply — `apply` defaults to false, so a bare submit is a dry run). See `skills/schema-unify/SKILL.md` for the full playbook. Authoring a successor pack: declare `migration_from: {pack: , version: "1.x"}` in the manifest plus `mapping_rules:` (discriminated union over retype / page_to_link / page_to_alias kinds). Catch-all sentinel `from_type: '*unknown*'` MUST appear last. Subtype_field is restricted to ALLOWED_SUBTYPE_FIELDS (`subtype, legacy_type, origin, format, kind, period, domain`) per codex D9 — third-party packs cannot inject `title` / `slug` / `type`. When NOT to upgrade: - Custom types not covered by the successor's mapping_rules → fork the successor first (`gbrain schema fork gbrain-base-v2 my-pack`), edit rules, then target your fork. - Mid-ingest or autopilot maintenance → wait. Unify holds the `gbrain-unify` db-lock for ~10 min on big brains. - Federated brain with sources you don't want to touch → scope per source via `--params sourceId`.