Version: 1.8.0 Date: September 2026 Status: Draft
License: This annex is part of the Open Okyline Language Specification and is subject to the same license terms (CC BY-SA 4.0). See the Core Specification for full license details.
This annex extends Annex D to enable cross-schema composition across teams and organizations. It defines schema identity ($id, $version), versioned dependencies ($deps), and external imports ($import). Organizations can publish schemas to a registry and import definitions from other contracts while maintaining strict version control and compatibility guarantees.
This annex extends Annex D by adding support for:
$id, $version)$deps)$import)Annex D defines the core mechanisms for structural composition ($ref, $defs, $override, $remove). This annex enables those same mechanisms to operate across schema boundaries through versioned imports.
Implementations MAY support Annex D without Annex E, providing internal composition only. Conformance to this annex requires Annex D (Core, Conformance).
Across teams and organizations, schemas often need to:
Okyline addresses these needs through a two-layer architecture:
$defs + $ref for composition within a single document$import + $deps for importing definitions from other schemasExternal definitions, once imported via $import, behave exactly like internal definitions in $defs. The composition mechanisms ($ref, $override, $remove) work identically regardless of origin.
$id, $version, and $stateEach publishable Okyline schema is identified by a pair ($id, $version) that defines a unique contract in the registry.
The format and validation rules for $id and $version are defined in the Core Specification [§7.3 Optional Metadata Keys]. These fields can be used as simple metadata for traceability even without this annex.
Example:
{
"$id": "sales.orders",
"$version": "1.3.4",
"$oky": { }
}$id and $version Become Required$id and $version are optional in the Core specification. Both become required when the contract is published.
When resolving dependencies, pre-release versions follow semver ordering:
2.0.0-alpha < 2.0.0-alpha.1 < 2.0.0-beta < 2.0.0-rc.1 < 2.0.0
Registry behavior:
+build.123) is accepted but ignored for registry resolution (it does not affect version ordering)$stateThe optional $state field declares the lifecycle state of the contract. It carries the publisher’s intent regarding whether the schema is still being shaped or has been frozen for consumption.
Allowed values:
| Value | Semantics |
|---|---|
"DRAFT" |
The schema is work-in-progress and MAY evolve without prior notice, even at the same $version. Consumers MUST consider its content unstable. |
"DRAFT-FINAL" |
The schema is still being shaped and MAY evolve like a DRAFT - but it resolves its dependencies as a FINAL would, admitting only FINAL and DRAFT-FINAL candidates (§E.6.6). It is the state of a contract preparing its release: strict before being frozen, so that promoting it to FINAL changes nothing in what it binds to. Consumers MUST still consider its content unstable; no contract may depend on it as it would on a FINAL. |
"FINAL" |
The schema is frozen. Once a (id, version) pair is published as FINAL, its content MUST NOT change. Any modification requires a new $version. |
Default:
If $state is absent, the schema MUST be treated as "DRAFT". Implementations MAY enforce explicit $state declaration at publication boundaries (e.g. in a registry workflow), but the language parser itself MUST accept the absence and apply the default.
Validation:
Any value other than "DRAFT", "DRAFT-FINAL" or "FINAL" MUST cause schema loading to fail.
Example:
{
"$id": "sales.orders",
"$version": "1.0.0",
"$state": "FINAL",
"$oky": { }
}Note: The lifecycle state has no effect on the validation of data; it informs registry, packaging, and dependency-binding tooling.
It does, however, constrain dependency resolution: a contract admits as dependencies only contracts at least as strict as itself -
FINALbinds onlyFINAL,DRAFT-FINALbindsFINALandDRAFT-FINAL. See §E.6.6 for the rule and its enforcement. Registry-level consequences beyond resolution (storage immutability, publication workflows) remain out of scope for this annex.
$entries$entries is defined in Annex D §D.2.5. An entry MAY name an imported $import alias; the external form &schemaId.Name is not allowed there.
A registry MAY hold contracts published by different authorities. Each entry it holds carries an origin: an opaque label assigned by the registry host when the contract is loaded into it.
(registry key) = (origin, $id, $version)
| Origin | Meaning |
|---|---|
"" (empty) |
Native - the contract belongs to the catalog itself |
| non-empty label | External - the catalog holds it on behalf of another authority |
Normative Rules:
$id MUST NOT contain a :. Origin is metadata held by the registry, never by the content: the same contract is native in its own catalog and labelled in a catalog that imported it.^[a-z][a-z0-9-]{2,38}$.($id, $version) under different origins are distinct contracts. An implementation MUST NOT unify them.An implementation serving a single publishing authority uses the empty origin everywhere. Every rule below then reduces to its unqualified form, and schemas written before this version keep their exact meaning.
$depsThe $deps block declares external dependencies with version constraints.
{
"$id": "orders",
"$version": "2.1.0",
"$oky": { },
"$deps": {
"common": "~1.3.0",
"crm": "^2.0.0",
"billing": "1.5.0"
}
}Okyline supports three constraint modes, following semver conventions:
| Syntax | Name | Matches | Example |
|---|---|---|---|
1.3.4 |
Exact | Only this version | 1.3.4 only |
~1.3.0 |
Tilde (patches) | Same MAJOR.MINOR, any PATCH | 1.3.0, 1.3.1, 1.3.17… but not 1.4.0 |
^1.3.0 |
Caret (compatible) | Same MAJOR, any MINOR/PATCH | 1.3.0, 1.4.0, 1.9.5… but not 2.0.0 |
The choice of constraint mode is a governance decision left to each organization. The specification does not mandate or recommend a particular strategy.
$deps is required when the schema uses at least one external definition via $import$deps SHOULD be omitted$deps MUST:
$id of other contracts)$deps MUST be present whenever the schema contains at least one non-empty $import import block, whether inside $defs, $nomenclature, $format, or $compute. A schema that declares $import imports without a corresponding $deps section MUST be rejected at load time.
If no $import import block is present (or all are empty), $deps SHOULD be omitted, but a $deps entry that is not referenced by any $import import is tolerated. Hygiene of unused dependency declarations is a governance concern that implementations MAY surface through separate linting tooling, not through schema rejection. This aligns with the convention adopted by mainstream package managers (npm, Maven, Cargo).
A $deps key names its dependency within an origin (§E.2.6):
dep-key = [ origin-label ":" ] schemaId
"$deps": {
"common.address": "^1.0.0",
"globex:common.address": "^1.0.0"
}Normative Rules:
"").label:id resolves only among entries whose origin is exactly label.globex.common.address is an $id whose first segment happens to be globex; globex:common.address is the id common.address under the origin globex. The two are unrelated.: is present it MUST be the first one, MUST be preceded by a valid origin label, and the remainder MUST be a valid $id. Otherwise schema loading MUST fail.$deps block for the same id, as above: they declare two distinct dependencies.$importThe $import block creates local aliases for definitions from external schemas.
{
"$id": "orders",
"$version": "2.1.0",
"$oky": {
"order": {
"shippingAddress|@": "&Address",
"customerEmail|@": "&Email"
}
},
"$defs": {
"$import": {
"Address": "&common.Address",
"Email": "&common.Email"
}
},
"$deps": {
"common": "~1.3.0"
}
}$import maps local aliases to external definitions"&common.Address" means:
common is the $id of another Okyline schemaAddress is the name of a definition in common’s $defscommon is resolved from $deps["common"] by the registryOnce imported, aliases in $import behave exactly like entries in $defs:
&Name syntax (not &schemaId.Name)$ref for property-level or object-level composition$override, $amend and $remove for adaptation$import is optional$import is non-empty, $deps MUST be present and non-empty"Alias": "&schemaId.Name" in $import:
[label:]schemaId part of the reference MUST appear verbatim as a key in $deps (§E.3.4). A reference qualified with a label requires the qualified key; an unqualified reference requires the unqualified key. Importing &common.Address while $deps declares only globex:common MUST fail as an undeclared dependency.schemaId, $deps[schemaId]) MUST existName in its $defs$defs and $import - if such a collision occurs, the schema MUST be rejected$oky and $defs, references MUST use the local alias &Name, not &schemaId.Name$import MUST be declared inside one of the four sub-blocks; each sub-block restricts what kinds of definitions can be imported through it:
$import block declared inside $defs MUST import only type and object definitions.$import block declared inside $nomenclature MUST import only nomenclatures.$import block declared inside $format MUST import only formats.$import block declared inside $compute MUST import only computed expressions.Any $import entry that attempts to import an element of a different kind than its enclosing block MUST cause schema loading to fail.
A $import block declared at the root of the schema document (not inside one of the four sub-blocks) MUST cause schema loading to fail.
Support for external imports via $import requires support for $ref resolution as defined in Annex D.
An implementation that does not implement $ref MUST reject any schema that contains at least one non-empty $import import block.
When multiple external schemas define a definition with the same name, $import aliases resolve the ambiguity:
{
"$id": "shipping",
"$version": "1.0.0",
"$oky": {
"Shipment": {
"destination|@": "&Address",
"legacyOrigin|?": "&LegacyAddress"
}
},
"$defs": {
"$import": {
"Address": "&common.Address",
"LegacyAddress": "&legacy.Address"
}
},
"$deps": {
"common": "~1.0.0",
"legacy": "~2.0.0"
}
}Both common and legacy define Address, but the local aliases Address and LegacyAddress are unambiguous within this schema.
$public and $privateEach sub-block ($defs, $nomenclature, $format, $compute) MAY declare a visibility rule that controls which of its entries are exposable to external imports via $import. The two directives are mutually exclusive within a single sub-block.
Syntax:
{
"$defs": {
"Animal": { "name|@": "Rex" },
"Helper": { "x|@": "v" },
"$private": ["Helper"]
},
"$nomenclature": {
"STATUS": "DRAFT,FINAL",
"$public": ["STATUS"]
},
"$format": {
"OrderId": "^ORD-[0-9]+$",
"Internal": "^INT-[0-9]+$",
"$private": ["Internal"]
}
}Semantics:
| Directive present in a sub-block | Effect |
|---|---|
Neither $public nor $private |
All entries are exposed (default - backward-compatible). |
$public declared |
Strict allow-list: only listed entries are exposed. Adding a new entry leaves it private by construction. |
$private declared |
Deny-list: every entry is exposed except the listed ones. |
| Both declared | Error - mutually exclusive. |
The listed names are bare (no sigil); the enclosing sub-block determines the kind unambiguously.
Normative Rules:
$public or $private. Declaring both within the same sub-block MUST cause schema loading to fail.$public: [] is accepted and locks the entire sub-block (nothing exposed).$private: [] MUST be rejected as ambiguous - equivalent to absence.$import alias). If it does not, schema loading MUST fail.Effect on imports:
An external $import import that references a hidden name (entry present in the source sub-block but not exposed) MUST fail with the same error as if the name did not exist. The source schema MUST NOT disclose whether the name is genuinely absent or merely hidden.
Example:
{
"$id": "common",
"$version": "1.0.0",
"$defs": {
"Address": { "city|@": "Paris" },
"Internal": { "x|@": "v" },
"$private": ["Internal"]
},
"$oky": { "f|@": "v" }
}A consumer schema MAY import &common.Address but MUST NOT import &common.Internal; attempting the latter is treated as an unknown name.
Note: Visibility is a barrier to import, not a runtime gate. A type that is exposed but uses a hidden nomenclature/format/compute/type internally continues to resolve those internal references at validation time - the source’s own scope is preserved (§E.8.5).
A $import alias is itself a valid import target. An external import &schemaId.Name succeeds whether Name is a local entry of the target sub-block OR an exposed $import alias of that target. This applies symmetrically to all four kinds (types, nomenclatures, formats, computes).
Re-export is subject to the same visibility rules as direct exposure (§E.4.5). Cycles in alias chains MUST cause schema loading to fail.
Example: an intermediate lib region re-exports common.Address as Addr; a consumer app imports ®ion.Addr and resolves transparently to common.Address.
$import OnlyExternal references use the syntax:
&[origin-label:]schemaId.DefinitionName
Examples:
"$defs": {
"$import": {
"Address": "&common.Address",
"Customer": "&crm.Customer",
"Invoice": "&fr.billing.Invoice",
"PartnerAddress": "&globex:common.address.Address"
}
}Structure:
& - reference prefixorigin-label: - optional; the origin the target contract is registered under (§E.2.6). Absent = native.schemaId - the $id of the target schemaDefinitionName - the definition name within that schema’s $defsParsing. The first : delimits the origin label (a label contains neither : nor .); the last . separates the - possibly multi-segment - schemaId from the definition name (a definition name contains no .).
External reference syntax (&[label:]schemaId.Name) is only valid in $import.
Inside $oky and $defs, all references MUST use local names:
// ✅ Correct - local alias
"address": "&Address"
// ❌ Invalid - external syntax not allowed in $oky
"address": "&common.Address"This restriction ensures that:
$import$import, not the entire schemaA schema registry maps ($id, $version) to Okyline schema documents.
Constraint matching follows strict semver ordering. A pre-release version is, by semver, strictly less than its stable counterpart at the same {major, minor, patch} (e.g. 1.3.0-rc.1 < 1.3.0). Consequently, ~, ^, and exact constraints do not match pre-releases of the base {major, minor, patch} unless the constraint itself names a pre-release (see E.6.4 for the explicit opt-in form). This aligns Okyline with the mainstream package-manager convention (npm, Cargo, Maven).
When resolving a dependency, the resolver performs a two-pass selection:
If neither pass yields a candidate, the resolution fails and the schema MUST be rejected.
A schema whose own $version carries a pre-release suffix is itself unstable. To support development workflows where consumer and dependency schemas evolve in parallel pre-release cycles, an unstable consumer is allowed to fall back to a matching pre-release dependency (Pass 2 above). A stable consumer is not so allowed: it sees only stable matches, unless the constraint itself opts in to pre-releases.
The resolution table below illustrates both consumer modes:
| Constraint | Available in registry | Stable consumer | Pre-release consumer |
|---|---|---|---|
"2.0.0" |
2.0.0 |
2.0.0 |
2.0.0 |
"~1.3.0" |
1.3.0, 1.3.1, 1.3.2-beta |
1.3.1 |
1.3.1 |
"~1.3.0" |
1.3.0, 1.3.5-rc.1 |
1.3.0 |
1.3.0 |
"~1.3.0" |
1.3.5-rc.1, 1.3.5-rc.2 |
no match | 1.3.5-rc.2 |
"~1.3.0" |
1.3.0-rc.1, 1.3.0-rc.2 |
no match | no match |
"^2.0.0" |
2.0.0, 2.1.0, 2.2.0-rc.1 |
2.1.0 |
2.1.0 |
Note the last two rows: pre-releases of the constraint base patch (e.g. 1.3.0-rc.1 for ~1.3.0) are not matched even by an unstable consumer, because they rank below the base in strict semver order. To allow them, the consumer must opt in explicitly via the constraint syntax described in §E.6.4.
Okyline’s resolution combines two regimes:
1.3.0-rc.1 matching ~1.3.0): Okyline is strict, aligned with npm, Cargo and Maven. The consumer must opt in explicitly via a pre-release suffix in the constraint (§E.6.4).1.3.5-rc.1 matching ~1.3.0): Okyline is more permissive than npm/Cargo when the consumer is itself a pre-release. Such pre-releases are picked up automatically as a fallback (Pass 2) without requiring a flag or an explicit pre-release suffix in the constraint. Mainstream package managers would require an opt-in (e.g. npm’s --include-prereleases).This combination is intentional: it preserves predictable resolution for released schemas (a stable consumer never accidentally pulls in a pre-release dep), while keeping in-flight development workflows ergonomic (an unstable consumer can iterate against the latest pre-release patches without continuously rewriting its $deps).
To target a base patch that has not yet released as stable, the consumer writes the constraint with a pre-release suffix:
"$deps": { "lib": "~1.3.0-rc.0" }
Such a constraint matches:
>= rc.0 per semver ordering (e.g. 1.3.0-rc.1, 1.3.0-rc.2),1.3.0,1.3.x train.This makes the intent explicit (the consumer knows it is targeting an unreleased version) and removes the ambiguity that would otherwise come with implicit pre-release fallback for stable-base constraints.
These guarantees describe the resolution of a schema loaded from its own text against a registry. An artifact that carries its own resolved dependency closure - a sealed package produced by publishing tooling - replays the resolution recorded when it was built instead of resolving anew: its $deps constraints remain declarations of intent and are never re-evaluated, and a recorded target that is unavailable MUST fail the load rather than fall back to a fresh resolution. Because the binding is recorded and not recomputed, the origin scoping of §E.3.4 governs resolution from text only: a recorded target MAY carry an origin label the producing side never saw, a contract native to its producer being labelled in the catalog that imported it. The reference text is never rewritten. The form of such artifacts is outside the scope of this annex.
$state Filter$state (§E.2.4) participates in resolution, symmetrically to the maturity rule of §E.6.3. Like every rule of this section, it governs resolution from text (§E.6.5); a sealed artifact is never re-resolved.
Rule (normative). The three states are ordered by strictness: DRAFT < DRAFT-FINAL < FINAL. A contract MUST NOT depend, directly or transitively, on a contract less strict than itself.
The rule is enforced during selection, not as a check after the fact. A candidate is admissible when:
admissible = matches the constraint
∧ maturity rule (§E.6.3, per the consumer's own $version)
∧ candidate $state ≥ consumer $state
Consumer $state |
Candidates it may select |
|---|---|
DRAFT (or absent) |
any state - free selection |
DRAFT-FINAL |
DRAFT-FINAL and FINAL |
FINAL |
FINAL only |
There is no symmetric rule: a DRAFT consumer selects the highest satisfying version whatever its state.
Consequence. A FINAL consumer declaring ^1.0.0, against a registry holding 1.1.0 (FINAL) and 1.2.0 (DRAFT), resolves to 1.1.0: the draft is not a candidate, so the higher version is skipped during selection rather than selected and then rejected.
Failure. When candidates satisfy the constraint but all of them are less strict than the consumer, resolution fails, and the failure MUST be distinguishable from “no version matches the constraint” - the two call for different actions (promote the dependency, versus widen the constraint or publish a version). The diagnostic MUST name the offending dependency and the state it lacked.
A FINAL consumer does not settle below a DRAFT-FINAL. A candidate is a version of the dependency that matches the constraint (§E.6.2). When a FINAL consumer selects a candidate while a DRAFT-FINAL candidate ranks higher in the same selection pass, resolution MUST fail; the diagnostic MUST name the candidate to promote. A DRAFT in the same position is skipped silently.
Two bounds follow from the order of the filters: a candidate outside the constraint was never a candidate, so pinning an exact version silences the rule; and the comparison is made within the pass that selected the candidate, so a pre-release DRAFT-FINAL never alerts a stable consumer.
The rule applies whenever a FINAL consumer is resolved from its text - when it is loaded as a schema, and when a package is built from it. A package, once built, replays the closure recorded at its build and is not resolved again (§E.6.5): what it binds to was settled, and checked, when it was made.
Scope of enforcement. Each load enforces the rule on the schema’s own $deps. Transitivity follows by induction: a FINAL dependency was subject to the same rule when it was loaded and published. An implementation that seals a dependency closure into a distributable artifact SHOULD re-check the whole closure when verifying such an artifact, since a corrupted or non-conforming producer could otherwise smuggle a draft in.
Once imported via $import, external definitions join the same namespace as $defs entries:
Available definitions = $defs ∪ $import
All Annex D mechanisms work identically on both:
| Mechanism | Internal ($defs) |
External ($import) |
|---|---|---|
$ref (property-level) |
✓ | ✓ |
$ref (object-level) |
✓ | ✓ |
$override |
✓ | ✓ |
$amend |
✓ | ✓ |
$remove |
✓ | ✓ |
The restrictions attached to those mechanisms cross the boundary with them. In particular, the Annex D rules on stateful templates apply identically to an imported definition: a template carrying conditional rules or %Compute expressions MUST NOT be combined with $remove (D.5.3, D.6.3), and every name listed in a $remove MUST exist in the referenced template. Implementations SHOULD report these failures identically whether the template is local or imported - it is the same rule.
A definition name MUST NOT appear as both a local entry of $defs and an alias inside $defs.$import:
// ❌ Invalid - collision
{
"$oky": null,
"$defs": {
"Address": { "city|@": "Paris" },
"$import": {
"Address": "&common.Address"
}
}
}If a collision occurs, the schema MUST be rejected at load time.
External schemas may themselves have dependencies. The registry is responsible for resolving the full dependency graph.
Rules:
$id MAY coexist in the resolved dependency graph, provided each consumer’s $deps constraint resolves unambiguously to a single version. This supports incremental migration scenarios where a consumer schema and its transitive dependencies pin different versions of a shared library.$id under different origins MAY likewise coexist in the resolved graph. They are unrelated contracts (§E.2.6): each consumer binds the one named by the key form it declared, and no implementation may treat them as versions of one another.This section defines the global resolution scope rules for Okyline.
These rules apply uniformly to all reusable elements defined or imported by a schema:
$defs, $import)$nomenclature)$format)$compute)The goal is to guarantee deterministic behavior, strict version isolation,
and predictable inheritance semantics, even in the presence of external imports
and object-level inclusion.
Every reusable element in Okyline belongs to exactly one owner contract scope:
(scope) = (origin, $id, $version)
$deps, including the origin it resolved under (§E.2.6).This rule applies uniformly to all element kinds:
$defs$nomenclature$format$computeThe owner scope of an element never changes implicitly.
Two definitions of the same name, one imported from [email protected] (native) and one from globex:[email protected], belong to two distinct scopes and are never interchangeable, even when their contents are identical.
External owner scopes are introduced only through explicit $import imports ($import inside $defs, $nomenclature, $format, or $compute). $deps declarations do not create visibility by themselves.
Type and object definitions participate in a single global namespace per contract.
Accordingly:
$import is only valid inside the $defs block ($defs.$import). A $import block declared at the root of the schema document MUST cause schema loading to fail (E.4.3).$defs.$import contributes to the global type namespace:Available types = $defs (local entries) ∪ $defs.$import (imported aliases)
Any $import declared inside $nomenclature, $format, or $compute:
Any reference to a reusable name:
&TypeName($NOMENCLATURE)~$FORMAT~(%COMPUTE)is always resolved within the owner scope of the element that contains the reference.
References are:
For any element declared in the current contract:
"$nomenclature": {
"COUNTRY": "FR,DE,ES"
}or:
"$defs": {
"Address": {
"country|@ ($COUNTRY)": "FR"
}
}All references inside the element are resolved in the
current contract’s owner scope.
$import)When a definition is imported via $defs.$import:
"$deps": { "crm": "2.0.1" },
"$defs": { "$import": { "Customer": "&crm.Customer" } }the imported definition:
[email protected],$nomenclature, $format, $compute, nested $defs) strictly within the [email protected] scope.The consuming contract does not alter this behavior.
$ref) and Field InjectionWhen object-level inclusion is used:
"Order": {
"$ref": "&Customer",
"orderId|@": "ORD-1"
}the implementation performs field injection.
Injected fields retain their original owner scope.
Consequences:
[email protected] continues to resolve its nomenclatures, formats and computes in the [email protected] scope.This guarantees that importing a type preserves the exact behavior defined and tested in its original contract version.
$override rebinds a whole inherited field to the local scope.
Example:
"country | $override @ ($COUNTRY)": "FR"$amend rebinds only the blocks it writes; the blocks it inherits keep the source contract’s scope.Any field added locally (not inherited) is:
$nomenclature, $format, and $compute.When a contract imports shared resources explicitly:
"$deps": { "geo": "1.4.0" },
"$nomenclature": {
"$import": { "COUNTRY": "&geo.COUNTRY" }
}the imported name:
[email protected].Thus:
"country|@ ($COUNTRY)": "FR"resolves against geo.COUNTRY, not a local definition.
The same rule applies to $format.$import and $compute.$import.
Numeric precision of imported computes. When a compute is imported via $compute.$import (or inherited through a field of an imported type), the validator evaluates it with the source contract’s $decimalScale - not the consumer’s. The library author chose that precision for a domain reason (often financial); silently inheriting the consumer’s scale would cause incorrect rounding.
Opt-in. This scale switch happens only when the source contract has explicitly declared $decimalScale. A contract that does not declare one is treated as “no opinion” and inherits the active scale of the validation.
Example. A tax library at $decimalScale: 6 defines round(amount * 0.1965, 6). When imported by a consumer at $decimalScale: 2, the multiplication still runs at scale 6, preserving the 4 fractional digits needed for accurate fiscal rounding before final cent-level display.
The following behaviors are explicitly forbidden:
Any such behavior would break determinism and testability.
| Situation | Resolution Scope |
|---|---|
| Local definition | Current contract |
Imported type definition ($defs.$import) |
Source contract |
Injected field via $ref |
Source contract |
Overridden field ($override) |
Current contract |
| Local field addition | Current contract |
| Imported nomenclature / format / compute | Source contract |
$decimalScale for imported compute |
Source contract if defined |
Throughout this table, a “contract” is identified by (origin, $id, $version).
| Feature | Description |
|---|---|
$id |
Schema identifier for registry publication and external references |
$version |
Schema version following semver format |
$state |
Lifecycle state of the contract ("DRAFT", "DRAFT-FINAL" or "FINAL", default "DRAFT"); a contract binds only dependencies at least as strict as itself |
| Origin | Host-assigned namespace label of a registry entry; "" = native. Never declared by the contract |
$entries |
Validation entry points, defined in Annex D §D.2.5; an entry MAY name an imported alias |
$deps |
Declares external dependencies with version constraints (~, ^, exact) |
label:id |
Qualified dependency key - resolves only within that origin; never unifies with the unqualified form |
$import |
Creates local aliases for external definitions |
$public / $private |
Per-sub-block visibility - controls what is exposable to $import import |
&[label:]schemaId.Name |
External reference syntax (only in $import); the optional label: names the origin |
&Name |
Local reference syntax (in $oky, $defs) - resolves in $defs ∪ $import |
| Re-export | A $import alias is a valid import target - intermediate libs can re-export |
| Registry resolution | Highest admissible version (maturity + finality filters), with pre-release fallback |
| Transitive dependencies | Resolved by registry; cycles forbidden; multiple versions of the same $id MAY coexist, as may the same $id under different origins |
Conformance is declared per annex (Core, Conformance): Annex D alone provides internal composition ($defs, $ref, $override, $amend, $remove); Annex E adds external imports and versioning, and requires Annex D.
common v1.2.0{
"$id": "common",
"$version": "1.2.0",
"$title": "Common Definitions",
"$oky": null,
"$defs": {
"Email|~$Email~ {5,100}": "[email protected]",
"DateTime|~$DateTime~": "2025-01-01T00:00:00Z",
"Address": {
"street|@ {5,100}": "123 Main Street",
"city|@ {2,50}": "Paris",
"postalCode|@ {5,10}": "75001",
"country|@ {2}": "FR"
},
"Auditable": {
"createdAt|@ $ref": "&DateTime",
"updatedAt|@ $ref": "&DateTime"
}
}
}ecommerce v1.0.0{
"$id": "ecommerce",
"$version": "1.0.0",
"$title": "E-Commerce Order Schema",
"$oky": {
"order": {
"$ref": "&Auditable",
"orderId|@ # ~$OrderId~": "ORD-12345678",
"customerEmail|@": "&Email",
"status|@ ($ORDER_STATUS)": "PENDING",
"items|@ [1,100]": ["&OrderItem"],
"shippingAddress|@": "&Address",
"billingAddress": "&Address",
"total|@ (%OrderTotal)": 100.00,
"$requiredIf status('SHIPPED','DELIVERED')": ["trackingNumber"],
"trackingNumber|{10,50}": "TRACK123456"
}
},
"$defs": {
"$import": {
"Email": "&common.Email",
"Address": "&common.Address",
"Auditable": "&common.Auditable"
},
"OrderItem": {
"sku|@ # {5,20}": "SKU-12345",
"name|@ {2,200}": "Product Name",
"quantity|@ (1..1000)": 1,
"unitPrice|@ (>0)": 10.00
}
},
"$format": {
"OrderId": "^ORD-[0-9]{8}$"
},
"$compute": {
"ItemTotal": "unitPrice * quantity",
"OrderTotal": "total == sum(items, %ItemTotal)"
},
"$nomenclature": {
"ORDER_STATUS": "PENDING,CONFIRMED,SHIPPED,DELIVERED,CANCELLED"
},
"$deps": {
"common": "~1.2.0"
}
}common: ~1.2.0 → finds [email protected] (highest patch)$import aliases are bound:
&Email → [email protected].$defs.Email&Address → [email protected].$defs.Address&Auditable → [email protected].$defs.Auditable&Email, &Address, &Auditable as if they were local(origin, $id, $version). The origin is an opaque label assigned by the registry host, never declared by the contract; the empty label denotes a native contract. A registry serving a single publishing authority uses the empty origin everywhere and is unaffected.$deps key and a $import value accept an optional label: prefix - "globex:common.address", "&globex:common.address.Address". Qualified and unqualified forms resolve in disjoint spaces and never unify; the $deps key must match the reference verbatim.$state now filters resolution: a FINAL consumer selects only FINAL candidates, a DRAFT consumer selects freely. A drafts-only outcome fails distinctly from a constraint no-match. E.2.4 updated accordingly: the transitive consequence of FINAL is no longer out of scope for this annex.DRAFT-FINAL (E.2.4, E.6.6). A third $state: mutable like a DRAFT, resolving like a FINAL. The states are ordered DRAFT < DRAFT-FINAL < FINAL, and a contract admits only dependencies at least as strict as itself; a FINAL consumer fails rather than settle below a DRAFT-FINAL that ranks higher.$remove targets apply identically to imported definitions.(origin, $id, $version); contracts sharing an $id under different origins are unrelated (E.7.3).$entries moves to Annex D (D.2.5): it names local definitions and needs no external dependency; E.2.5 keeps a pointer.End of Annex E - External Imports and Versioning (Normative)