11. Packages
A package is what gets published: a named, versioned set of facets, classes, rules, contracts, views and recipes, with the kinds they work on, the packages they build on, and the trust order that settles disagreements. Our catalog is a package; so is commerce; so will be anyone else's.
11.1 The package file #
package: me /2026-10-13
owner: mediaencyclopedia.com
label: Media Encyclopedia
crubal: 2
licence: CC-BY-4.0
extends: commerce /2026-10-01
prefixes:
commerce: cashierfu.com/commerce
wd: https://www.wikidata.org/entity/
schema: https://schema.org/
ebay: ebay.com
units: crubal.com/units /2026-10-15
confirm: 2
split: 3
| Setting | Holds |
|---|---|
package |
the package's name and version |
owner |
the domain that owns the names and signs the package. The package's full identity is owner/name: mediaencyclopedia.com/me, globally unique because only the domain's owner can publish under it |
label · describe |
what people see |
crubal |
the language version it is written in |
licence |
an SPDX licence identifier |
extends |
the packages it builds on, each pinned to a version (11.4) |
prefixes |
nicknames its files use, each mapped to a full identity: a package (domain/name), a vocabulary's base URL, or a party's domain. The package's own name is its default nickname for itself |
units |
the units table it uses, pinned by identity and version (11.9) |
confirm · split · recur |
thresholds: independent observations to confirm a value (default 2), instances to propose a version split (default 3), distinct records before a recurring notable or unknown key is proposed as a facet (default 5) (chapter 9) |
trust · facet-trust |
the trust order (11.3) |
terms |
how each audience may use what it's shown: public: search yes, answer yes, train no (chapter 12.5.1) |
The rest of the package is its member files: facet:, class:, rule:, contract:, kind:, view:,
shape:, display:, filter:, role: (chapter 12), migration:, permissions: and ownership: (chapter 14) and
utility: blocks, in any number of files. A package MAY also publish reference records (data files in
the text form, chapter 2) that others read but don't own copies of: the units table is one.
Utilities. A platform that mounts tools for the people of the owners it hosts declares each tool in the package that holds its vocabulary, so that what a tool needs and whom it serves is read from the package and nowhere else:
utility: orders /2026-10-16
label: Orders
serves: controller, fulfiller(ship, returns), member(buyer)
needs: cashierfu.com/commerce, cashierfu.com/location, notifications
setup: connection stripe, connection shippo, setting shippingOrigin
| Setting | Holds |
|---|---|
utility |
its name (a lower-case word, its identity like a contract's) and dated version |
label · describe |
what people see |
serves |
whom it mounts for: presets, relations or audiences (chapter 14.6, 12.4), each optionally narrowed in parentheses to the presets, menu items or relations it shows screens for |
needs |
the packages (domain/name, optionally with a version) and the other utilities it depends on |
setup |
what a deployment provides before it runs: connection <name> (a credential the host holds, chapter 7.6) and setting <facet> (a value on the deployment's own record) |
A utility is a member like any other: versioned, listed in members, carried in the bundle (11.6). It
grants nothing: who may use it is still edges (chapter 14). A package MUST be refused at publish when a
utility serves a name that isn't a preset, relation or audience, narrows to something that isn't a preset,
menu item or relation, or needs a setting that isn't a facet. A reader that doesn't know the block carries
it unchanged (chapter 13.5).
Prefixes are nicknames. Identity is always the full domain/name; a prefix only abbreviates it inside a
package or a wrapper, and readers compare after resolving (chapter 1.3). Two packages may use different
nicknames for one identity, or one nickname for different identities, without clashing.
Reserved prefixes. crubal names the core, wherever its home turns out to be. A package MUST NOT map its
own name, or crubal, to anything else, and within one package two prefixes MUST NOT name the same identity.
11.2 Kinds #
Kinds are declared in packages (chapter 1.2):
kind: item
levels: group, product, unit
base: Item
schema:
group: schema:ProductGroup
product: schema:Product
unit: schema:IndividualProduct
vary:
product: edition, size, colour
unit: condition, price, sealed
follows:
label: edition
weight: size
identify:
product: asin
unit: serial
match:
product: gtin; title + format + releaseDate
archive: independent
| Setting | Holds |
|---|---|
levels |
the levels, top to bottom |
base |
the class every record of the kind wears |
schema |
the schema.org type, one for the kind or one per level |
vary |
for each level below the top, the facets that vary at the step into it (chapter 1.7). At the step into product, these are the group's forks (edition, size × colour), which shoppers pick along |
follows |
facets that follow a fork (Label follows Edition): they are dependent, so filters count them together |
identify |
per level, the strong keys: a value held by one record names that record, so an arrival carrying it is bound there (chapter 9.2a). Each MUST also be unique within the level |
match |
per level, the weak keys, separated by ;: an arrival sharing one is probably the same record and goes to review, never bound by itself. a + b means all of them together. A key that real data reuses (a recycled barcode) belongs here, not in identify |
archive |
what archiving a record does to the records below it: cascade of, cascade within or cascade both (they are archived too, with reason parent archived, along that link) or independent (the default) (chapter 1.12) |
Classes add vary steps for their wearers with level prefixes (chapter 5.3.3). A package that extends
another MAY add levels' vary steps to that package's kinds; it MUST NOT remove any.
11.3 The trust order #
When two sources disagree (a conflict, chapter 9.3), the trust order picks the current value.
trust:
person
me:unitizer
observed
declared
distributor.example
reported
facet-trust:
runtime: observed, person
weight: observed, person
- Each entry is
person(a person's write through any form), a contract name, a party's domain, or an origin (observed,declared,derived,reported). - A source's rank is its first matching entry: a person's write matches
person; a value from a contract matches the contract, then its origin; a value from another owner matches the domain, then its origin. - Of two sources, the one ranked earlier wins. Equal ranks keep the current value.
- A source matching no entry ranks last.
facet-trustgives a facet its own order, tried before the package's: a source matching an entry of the facet's order ranks by that entry; a source matching none of them ranks after all of them, by its place in the package's order.- The default order, when a package has none, is
person,observed,declared,reported,derived.
Adopted values (chapter 8.2) are outside the trust order: they don't change until they are given back.
11.4 Versions and extending #
- A package version is a frozen set of its members' versions. Publishing produces a members list naming every facet, class, rule, contract, kind, view, shape, filter, recipe and permission declaration with its version, and the package's version names that list.
- Inside a package, everything follows current. Its own members always refer to each other's current
versions. The owner's working package is the set of its members as they now stand: every change to a
member is a
definewrite in the owner's log (chapter 8.5), made by a person or by a review decision (chapter 9.5), so the working package is rebuilt from the log like the data is. Publishing cuts a version from it (11.5); nothing else does. - One key, one definition. A package MUST NOT define a facet another package in the same dictionary defines with a different type; a store refuses the install, since the later definition would silently change what the earlier package's records mean. A package that needs the same idea on its own records names its own key.
- Outside, adopters pin.
extends: commerce /2026-10-01uses exactly that version until the package moves its pin, which is itself a new version of the package. - Extending (UCP's word, kept for packages and contracts) lets a package add facets, classes, rules, contracts and views, add aliases and values to the extended package's facets, add vary steps, and narrow its classes in classes of its own. It MUST NOT change the extended package's definitions or widen them.
- A new package version that breaks its members (chapter 4.8) ships the recipes (chapter 10).
11.5 Publishing #
A package is published as a bundle: one JSON document (11.6) holding the header, the members list,
every member in its JSON form, the recipes since the previous version, its reference records, the version
it follows as previous, its hash (the SHA-256 of its canonical form without hash and sig) and the
owner's sig over the hash. The text files are served beside it for people; a reader turns them into the
same document, so they need no signature of their own.
Publishing MUST run, and the bundle MUST pass, every well-formedness check:
| Chapter | Checks |
|---|---|
| 4.7 | definitions |
| 5.10 | classes |
| 6.8 | rule sets: layering, contradictions, one spelling |
| 7.10 | contracts, including that no secret appears |
| 10.5 | recipes, and the instruction a stopped projection carries (10.7) |
| 11.1–11.4 | prefixes; kinds (a vary step names a level of its kind and facets of the package); trust order entries name real contracts and origins; extends doesn't widen |
| 12.8 | views, shapes, filters and roles |
| 14.6 | permission menus, presets, relations and ownership |
A published version MUST NOT change. A mistake is fixed by a new version.
11.6 The JSON form #
{
"package": "me",
"version": "2026-10-13",
"owner": "mediaencyclopedia.com",
"id": "mediaencyclopedia.com/me",
"crubal": "2",
"licence": "CC-BY-4.0",
"extends": [ { "package": "cashierfu.com/commerce", "version": "2026-10-01" } ],
"prefixes": { "commerce": "cashierfu.com/commerce", "wd": "https://www.wikidata.org/entity/" },
"units": { "package": "crubal.com/units", "version": "2026-10-15" },
"trust": ["person", "me:unitizer", "observed", "declared", "reported"],
"facet-trust": { "runtime": ["observed", "person"] },
"members": { "facet:videoCodec": "2026-10-13", "class:OpticalDisc": "2026-10-13" },
"utilities": [ { "utility": "orders", "version": "2026-10-16", "serves": [ { "as": "controller" }, { "as": "fulfiller", "items": ["ship", "returns"] } ],
"needs": [ { "name": "cashierfu.com/commerce" } ], "setup": [ { "connection": "stripe" }, { "setting": "shippingOrigin" } ] } ]
}
The member sections (kinds, facets, classes, rules, contracts, views, shapes, displays,
filters, roles, permissions, ownership, recipes, utilities) each hold their members' JSON forms
as the chapters define them.
11.7 The home: crubal.com #
The core is the package crubal.com/core, and the prefix crubal always names it. Its names (envelope
lines, origins, audiences, the core facets crubal:notables, crubal:count and crubal:clonedFrom, the
shape crubal:portable) are fixed by this specification.
The standard packages are published by crubal.com for anyone to extend:
| Identity | Holds |
|---|---|
crubal.com/core |
the core's names |
crubal.com/units |
the units table (11.9): units by dimension, with factors, offsets and accepted spellings, and ISO 4217 currencies |
crubal.com/party |
the person, organization and system kinds, their base classes, and the caller facets that edges, contracts and the trust order read (chapter 14) |
That is all crubal.com publishes: the engine is the silent layer, and nothing a person sees comes from it. Every
other vocabulary is a package of its owner's domain, published from that domain's well-known file like any
other: what a platform sells with (items, services, offers, orders) is the platform's, and what one of its
tenants deals in (films and discs) is the tenant's. In this spec's examples those are cashierfu.com/commerce
and mediaencyclopedia.com/media; both are examples (README).
Where a package is. A package's identity domain/name resolves through the domain's well-known file:
https://<domain>/.well-known/crubal.json
{
"crubal": "2",
"owner": "crubal.com",
"keys": { "crubal.com": { "alg": "Ed25519", "public": "…" } },
"packages": [
{ "package": "crubal.com/party", "latest": "2026-10-16",
"versions": { "2026-10-16": "https://crubal.com/party/2026-10-16/package.json" } }
]
}
keysholds the owner's published signing keys (chapter 8.5), each MAY carryfromanduntilso a rotated key still verifies what it signed, and MAY list keys of the people it hosts by their IDs (chapter 14.1); a party with no published key has unsigned data, which readers MAY refuse.emergency, for a platform that hosts owners: its emergency-access terms (chapter 14.13), as{"approvers": 2, "longest": "PT24H", "notify": "…", "keys": {…}}, with the separate keys its approvers sign with. A platform without it can make no emergency edge.- Each package lists its versions and where each version's bundle is: the package's definitions in the JSON form, the same as its text files read through a Reader (chapters 4.6, 5.9, 6.10, 7.9, 10.6, 11.6).
- A domain MAY also serve its packages as text, beside the JSON (
…/package.crubal).
crubal.com serves the standard packages at https://crubal.com/<name>/, a page for people, with each version
at https://crubal.com/<name>/<version>/package.crubal and …/package.json.
The registry. https://crubal.com/registry/ lists known packages from any domain, and
https://crubal.com/registry/index.json lists the same as data. Listing is optional and never changes where a
package is or who owns it: the owner's well-known file is the truth. A package is listed by adding its
identity to the registry file in the CRUBAL repository.
11.8 Language versions #
crubal: 2 names the language version: a whole number. The specification and its corpus are dated
within it (chapter 13.3), and a dated revision only adds: a setting, a part, a write kind, a verb, a
shape. A reader at a later date reads everything written at an earlier date of the same version, and
carries what it doesn't know unchanged (chapter 13.5).
What a new version is for. The next whole number is for a change that breaks what readers already hold: the text form (chapter 2), the JSON form or its tags (chapter 3), the envelope (chapter 1.1), the canonical form and so every hash (chapter 3.9), or the meaning of a setting, part, verb or write that exists. Nothing else moves the number.
Reading across versions.
- A reader at version N reads N and every version below it.
- A reader MUST NOT read above its version: it refuses a file, naming the line that says so, and holds an intake until it upgrades (chapter 10.1), never guessing.
- A border wrapper names the version of what it carries (chapter 3.8), so two owners at different versions exchange data only in the lower one, which the higher reads.
Packages move by publishing. A package names the version it is written in, and MUST NOT extend a package at a higher one: it could not read it. A package moves to N+1 by publishing a new package version written in it; adopters pin (11.4), so each moves when it is ready and nothing moves under it.
Data moves by a form recipe. With the specification of N+1, crubal.com publishes a form recipe: a deterministic transformation of canonical JSON from N to N+1, proven by corpus cases of its own. A store that upgrades applies it to its records, its working package and its projections, as it applies a package recipe (chapter 10.4): intake records keep their bytes, and replay folds them under the new form.
The log keeps its past. Entries written under N stay as written: an entry carries crubal when its
version differs from the entry before it, and each entry's hash and signature verify under the canonical
form of its own version (chapter 8.5). A chain therefore reads as segments, one per version, and a rebuild
(chapter 8.5.3) applies each entry under its own form and the form recipes between.
11.9 The units table #
The units table is the package crubal.com/units (11.7): reference records, one per unit and one per
currency, that every package pins (units:) and every store reads the same way. Quantities (chapter 1.4.3)
name its spellings, intake folds through it (chapter 9.2), facets convert by it (chapter 4.2) and recipes
convert by it (chapter 10.3). The table is data, not core: a store with a different pin converts
differently, and that is the pin's job.
A unit is a record of the kind measure: its ID is the canonical spelling (min, floz, °C), and it
holds its dimension, its factor to the dimension's base unit, an offset when its zero isn't the base's
(°C: an amount in base units is amount × factor + offset), its accepted spellings, and its UN/CEFACT
code as uncefact, never as a spelling. A dimension's base unit has factor 1 and states the rounding a
conversion keeps. A currency is a record of the kind currency: its ISO 4217 code, its name and its
minor-unit places, for a border that asks for cents (chapter 1.4.4). A three-letter uppercase spelling is
always a currency and never a unit.
- Conversion goes through the base: an accepted unit's amount becomes the facet's unit by the two
factors and offsets, kept to the facet's
placeswhen it has them, else to the dimension's rounding at most. Units convert only within one dimension. - Compound units (
km/h,kWh) are listed as units of their own, with their dimension and factor. There is no grammar of products and quotients. - Spellings are folding aliases (chapter 4.3): case, spaces and punctuation runs fold before matching,
so
MIN,min.andMinneed no entry, and a spelling in another language is one more alias. There is no localization machinery. - The first table holds time, length, mass, volume, area, temperature, data and count, and the currencies the standard packages use. It grows by dated versions of the package, additions only.
Extending the table. A package MAY publish units of its own, prefixed like everything else of its own
(me:slab), as measure: records with a dimension, a factor to the base where one exists, and spellings;
and it MAY add spellings to a standard unit by a record of that unit's name holding only spellings. It
MUST NOT change a standard unit's factor, offset, dimension or canonical spelling. A store's table in force
is the standard table at its pin plus the owner's extension. When a store moves its pin to a newer standard
table, a unit of its extension that now matches a standard unit by UN/CEFACT code folds into it, the
extension keeping its spellings as aliases: an install and restamp (chapter 10), never a lookup.
A unit question. A quantity whose spelling folds to no unit of the table in force is not a
contradiction and not a unit: it is a unit question for review (chapter 9.5), the field landing as new
with cites: {"unit": <spelling>} and the raw text kept on the intake record. Review decides once, by
spelling across every record it reached: an alias (the spelling joins a unit's spellings), a new unit
(dimension, factor or offset, code; it joins the extension), or not a unit (the field stays raw). Each
decision is a review write and a define write (chapter 8.5), and the held intakes replay. A store MAY
look the spelling up (UN/CEFACT, UCUM, an AI) to propose an answer, shown beside the evidence; it never
applies one. Nothing in the table is created because it was seen.