Most applications contain a handful of models that are not their idea. Users, groups, roles, permissions. A trace row for every call to a language model. You did not invent them, you will not differentiate on them, and you will spend a day re-deriving them anyway — badly enough that six months later someone asks why a grant is a string compared to a literal in a branch.
A library is that design, declared as metadata and shipped with MetaObjects:
An agent working in your repo can now see that the capability exists, resolve against it, and build on it. Nothing else happens — which is the part worth reading twice.
A library ships its core model, its DB persistence and (where it has one) its UI rendering as separate layers. You take as much of it as you want:
"libraries": ["iam"] // core model only — inert
"libraries": ["iam", "iam/db"] // + persistence: the tables
"libraries": ["iam", "iam/db", "iam/ui"] // + UIThe core layer declares no source.rdb, and a sourceless object is inert by a
contract that already shipped. meta migrate skips an object with no writable source
and codegen emits no route, queries, hooks, grid or form for one (both citing #248:
persistability derives from source presence, never from the object subtype). So
libraries: ["iam"] adds zero tables, and no persistence, API or client code. The
design is present and resolvable; the schema is a second, separate decision. The entity
generator, if you have wired it, does emit one type-only module per library entity —
which is the next paragraph, and the whole of what the core layer costs you.
A sourceless object still gets a type-only interface, so extends and references work
against it from day one.
"iam/db" implies "iam", and the implication is not a convenience: a db layer is
nothing but overlay: true redeclarations, and an overlay whose target was never
declared is ERR_OVERLAY_NO_TARGET. A token whose LAYER is unknown is dropped WHOLE
rather than reduced to its core — implying the core from a mistyped iam/database would
hand you an inert core and no tables, with no diagnostic.
An unknown library name is refused by the config reader with the tokens this build
actually ships (ERR_UNKNOWN_LIBRARY). Skipping it silently would resurface later as
ERR_UNRESOLVED_SUPER pointing at your own metadata — the wrong place to go looking.
There is one door, not two:
meta gen --list --format json --probe
Library rows carry kind: "library", a useWhen sentence, the layers you can select,
the packages the library owns, and a computed provides — how many entities, how many
abstracts, how many requirements are in the box. With a project present there is also a
project block: which layers you selected, how many tables and requirements they added
here, which of your entities extends into the library, and any generator the library
implies that you have not wired.
The rule for an agent is the one the metaobjects-codegen skill states: if a
useWhen matches the capability you are about to model, opt in and adapt rather than
author.
A library is first a reference — something to copy and make your own. That is the same ruling ADR-0034 made about generators: the reference templates are copied into your repo because you own your code. Metadata is no different.
| you want to | door |
|---|---|
| the design, as a starting point you own — rename the package, delete what you do not need, change a PK strategy, keep the requirements and edit them | meta eject <library> — the expected path |
| a new shape sharing a library base, tracking upstream | extends |
| add to a shipped node while tracking upstream (fields, indexes, views) | overlay: true on the same (type, metaobjects::<lib>::Name) |
| change a shipped requirement's verdict while tracking upstream | overlay: true on the requirement node |
meta eject iam
copies every layer into your project's first declared source root (resolved through
your sources — never a hard-coded directory name), as meta.iam.model.yaml,
meta.iam.db.yaml, meta.iam.requirements.yaml, each stamped with a provenance header.
It does not edit your config, and it never overwrites a file without --force.
One step remains, and the loader enforces it: remove the library from libraries.
Left there, the shipped tree and your copy both load and merge — and the merge is
asymmetric. Additions in your copy take effect; deletions do not, because the library
still declares what you removed. That is ERR_LIBRARY_PACKAGE_COLLISION, refused at
load rather than left to be discovered.
The mirror error is ERR_LIBRARY_PACKAGE_NOT_OWNED: a new node declared into a
package a library owns while that library is opted in. A later release of the library
may ship a node of that name and merge into yours. Declare it in a package you own and
extends the library's node, or say overlay: true and mean it.
meta eject --list
reports, per ejected library, whether your copy is identical to the shipped tree or
differs, with three counts: nodes changed, nodes only upstream (the library
gained one, or you deleted it) and nodes only yours. The comparison runs through the
canonical serializer in own mode, so re-indentation and key order never show up — only a
declaration that actually changed. Nodes are matched by NAME with the package
neutralized, because renaming the package is something you are invited to do and is not
drift.
meta verify also advises on any node under metaobjects:: in your own metadata that
carries no ejection provenance — a hand-copy whose origin nothing can reconstruct, or a
package name that will collide the day a library ships into it.
A library ships requirement.* nodes describing what its model promises, and on opt-in
they enter your ledger with no new machinery. One reading rule, and it is load-bearing:
live in a library means "the model as shipped realises this", never "your
application does". Behaviour the model cannot carry ships as partial +
disposition: accepted with a note naming what you must do.
Object coverage activates on requirements YOU authored. A library cannot volunteer
you for the unclaimed-entity gate: opt into iam with no ledger of your own and
meta verify prints its entries, checks them, and says coverage: not measured (no project-authored requirements). Write your first requirement and coverage turns on —
over the library's entities too, which by then its own ledger claims, so they add no
warnings.
To disagree with a shipped requirement, overlay it: same (type, package::path),
overlay: true, your status / disposition / notes. Addressing a nested node means
re-declaring its ancestors, and every one of them must also carry overlay: true —
left plain, each emits WARN_DUPLICATE_DECLARATION, so a depth-4 tree costs three
warnings to change one leaf.
A library may declare generators its design implies, each with an anchor — the
library node the generator keys on. ai declares trace-helper, anchored on
metaobjects::ai::LlmCallBase.
Nothing is wired for you. Two self-extinguishing warnings do the rest: library opted in, implied generator not wired and generator wired, its library not opted in. The second matters more than it looks: a generator whose anchor is absent matches nothing and emits zero files, which reads exactly like "my model has no trace entities yet".
| library | what it is | stability |
|---|---|---|
iam |
Users, nestable typed groups, roles as permission bundles, grants global or scoped to a group. Nine entities; grants are two junctions rather than one with a nullable scope, because a NULL in a unique key is distinct from every other NULL in SQL. | preview |
ai |
The LLM-call trace envelope: what was asked, what came back, what it cost, how long it took. | stable |
preview means the SHAPE may change within a MINOR; stable means additive-only. See
compatibility-policy.md. Copy-and-own is the real answer
to both: a later change to a library reaches only the adopters who chose to track it.
A library's nodes are generated, migrated and ledgered as if you had written them —
the opposite of a metadata dependency, which is someone
else's model you must not drift from and is excluded from your codegen, schema and
ledger by default. You opt into a library to have the thing. The layering is what
keeps that from meaning "nine tables you did not ask for": the core layer produces
nothing until you add db.