Construction Kit Versioning Rules
CK model versions follow SemVer, and dependencies are declared as version ranges — but which concrete version a range resolves to depends on where the resolution happens. Getting this wrong is the most common cause of ResolveFailed models and failed blueprint installs, so the rules are spelled out here in one place.
Where Ranges Are Resolved — and to Which Version
| Context | Input | Resolves to |
|---|---|---|
Compile / publish (octo-ckc) | Ranges in the source ckModel.yaml | Highest published version satisfying the range — frozen into the compiled library as an exact pin |
| Tenant import / FixAll | Exact pins of the compiled library | Exactly the pinned versions — the tenant must hold them precisely |
Blueprint apply (ckModelDependencies) | Ranges in blueprint.yaml | The range's lower bound (floor) is the install target |
Two safeguards soften the blueprint floor rule:
- No downgrade: if the tenant already holds a model of the same name at a version at or above the floor, the import is skipped entirely.
- Service-managed redirect: for service-managed models (
System,System.*), the install target is redirected to the version the services ship, provided it satisfies the floor.
Practical consequence: a floor must be installable
Because the blueprint engine installs the floor on a fresh tenant, the floor version itself must be resolvable together with all other declared dependencies. A typical failure: EnergyCommunity-[4.0,5.0) next to Basic.Energy-[1.3,2.0) — if EnergyCommunity 4.0.0 was compiled (and therefore exactly pinned) against Basic.Energy 1.2.x, a fresh install tries 4.0.0, whose pin conflicts with the 1.3 floor, and fails with:
CK model 'EnergyCommunity-4.0.0' could not be installed; required CK model
dependencies may be missing or still importing
This message fires whenever, after the import attempt, no model of that name is in state Available — the underlying dependency-resolution error is swallowed by the parallel-startup import path, so this post-check is the only signal. The fix is to raise the floor to the first version that was compiled against the new dependency (EnergyCommunity-[4.1,5.0)).
The Cascade Rule
When a model's dependency moves, the model itself must be rebuilt, bumped, and republished — and every blueprint floor that points at it must move too.
Step by step, using Basic.Energy → EnergyCommunity as the example:
Basic.Energypublishes a new version (say1.3.0).EnergyCommunitystill pins1.2.x(compile-time freeze). It must be recompiled so the range re-resolves to1.3.0, and republished with a version bump — the SemVer gate (OCTO-CK100) enforces the bump because the resolved dependency set changed.- Any blueprint declaring
EnergyCommunity-[4.0,5.0)should raise its floor to the first rebuilt version, or fresh installs will target a version that is incompatible with the new dependency floor (see above).
A blueprint's blueprint.yaml (ckModelDependencies) and its seed-data file (dependencies: header in the runtime-model YAML) declare CK version constraints independently and are resolved by different code paths. When you move a floor, move it in both files — a lagging seed pin quietly reintroduces the incompatible pairing.
Upgrading an already-provisioned tenant follows the same order: publish the rebuilt dependent model to the catalog before upgrading the dependency on the tenant. Running FixAll after only the dependency was published upgrades the dependency but leaves the dependent at a version whose pins no longer resolve — the dependent flips to ResolveFailed and its types vanish from the tenant's GraphQL schema. Recovery: publish the rebuilt dependent, RefreshCatalogs, FixAll, then ClearCache.
The SemVer Gate (octo-ckc ValidateVersion)
CI validates every CK library against its published baseline before publishing. The gate classifies the model diff (major / minor / patch) and enforces a sufficient version bump:
| Code | Meaning |
|---|---|
OCTO-CK100 | Declared version does not satisfy the required bump over the published baseline — version too low. Also triggered when the resolved dependency set changed (see cascade rule). |
OCTO-CK101 | Declared version is lower than the baseline (the newest published version of the same major) — downgrade, not allowed. |
OCTO-CK102 | Catalog source unreachable — the baseline could not be determined. Never treated as a first publication. |
OCTO-CK103 | A declared dependency range is satisfied by no published version (and by no sibling package validated earlier in the same run). |
OCTO-CK104 | The diff requires a major bump but no migration script targets the declared version (error only with --requireMigrationForMajor, otherwise a warning). |
Notes:
- Sibling packages validated in the same run count as published for the
OCTO-CK103check — so same-commit dependency bumps across sibling models in one repository work. - Any
OCTO-CK1xxfinding makes the command exit non-zero (-6, which surfaces as250in most shells). Gate CI onexit code != 0, never on== 1. - A separate lint family (
OCTO-CK001/OCTO-CK002) validates runtime-state markers at build time and is unrelated to the SemVer gate.
Which version is the baseline
The gate compares a model with its baseline, decided per major line:
- Same major. The baseline is the newest published version of the declared major. With
EnergyCommunity3.3.0and4.5.0published, a declared3.4.0is compared with3.3.0. A maintenance release on an older major (while tenants still run 3.x and 4.x already exists) therefore passes the normal CI gate; no manual compile-and-publish is needed. A declared3.2.0fails withOCTO-CK101against3.3.0. - New major. When the declared major has no published version yet, the baseline is the newest version of the
previous major line:
4.0.0is compared with the newest3.x, so the migration check (OCTO-CK104) and the changelog still work. A major allows any change; if the changes would only need a minor bump, the report says so ("valid major bump without structural need"). Only a model that was never published is a first publication. - Never the model under test. Entries in the local file-system catalog at or above the declared version come
from earlier local builds, not from a publish. They are never the baseline and are listed as ignored in the
report, so running
ValidateVersiontwice gives the same result. A published version equal to the declared one still counts: changing a model without raising its version fails withOCTO-CK100.
--catalogName restricts the baseline lookup to one catalog; without it every readable catalog is asked. If a
catalog source is unreachable and no baseline of the declared major was found, the gate reports OCTO-CK102 instead of
falling back to an older major.
Compile gate
For models with ckLanguage: 2, the build itself is a gate: dotnet build (task CkCompile) compares the freshly
compiled model with its baseline before it publishes anything and fails for every change the declared version does
not cover. The gate never writes to a catalog, so an incompatible version leaves neither your machine nor a CI build.
It uses the same verdict as ValidateVersion (same baseline rules, same required level, same minimum version).
| Code | Meaning |
|---|---|
OCTO-CK200 | A change is not covered by the declared version. One error per change, naming the element, the change, the required level and the minimum version. |
OCTO-CK201 | The declared version is lower than the baseline of the same major. |
OCTO-CK202 | The baseline source was unreachable and no baseline is known: an error with Remote, a message only with Local. |
The MSBuild property OctoCkCompatibilityBaseline selects where the baseline comes from:
| Value | Baseline from | Default |
|---|---|---|
Local | the local file-system catalog plus the cached remote catalogs; works without network | outside CI (DebugL) |
Remote | the GitHub catalogs only; the local catalog is ignored | when TF_BUILD or ContinuousIntegrationBuild is true |
Setting the property overrides the default. In Local mode, entries of the local catalog at or above the declared
version are never the baseline (they come from earlier builds of the same model), so building twice, or for two
target frameworks, gives the same result and a breaking edit at an unchanged version fails again on the next build.
Every build logs one line naming the model, the declared version, the baseline (with its catalog, and "local, not
published" where applicable) and the required level. Models with ckLanguage 1 only get this line; their gate remains
ValidateVersion in CI.
Acknowledging changes
Two kinds of change are accepted only when the publisher says so explicitly: tightening access on a
security-sensitive attribute
(minor instead of major) and a unique index on a stable base (major). Without an acknowledgement the build fails with
OCTO-CK203; the message contains the change key and an example entry. Add it to ckModel.yaml (ckLanguage: 2):
compatibility:
acknowledge:
- change: "RecordAttribute:Login-1/Secret#Modified:access"
reason: "Close accepted risk R13: password hash readable via GraphQL"
| Code | Meaning |
|---|---|
OCTO-CK203 | A change that needs an acknowledgement has no matching entry. |
OCTO-CK204 | An entry matches no change that needs one in this release (stale, or it names an ordinary change). Remove it. |
- The key is stable and free of model version numbers:
{ElementKind}:{ElementId}#{ChangeKind}, with:{property}for a modification. Copy it from the error message; it is compared exactly, wildcards are not allowed. - The
reasonis mandatory and appears in the verdict, the build log and theCHANGELOG.md("Acknowledged changes"). - An acknowledgement never lowers the required bump: a unique index on a stable base still needs a major version, and an
ordinary breaking change still fails with
OCTO-CK200even if it is listed. - It is valid for one release only: the next release must not carry the entry (
OCTO-CK204). - The acknowledgements are part of the compiled model, so the publish check can verify them without the source.
Writing the minimum version into ckModel.yaml
The build never edits ckModel.yaml. When the compile gate or ValidateVersion reports that the declared version is too
low, one explicit command writes the minimum valid version:
octo-ckc -c ValidateVersion -p src/ConstructionKit --apply
It computes the minimum with the same verdict as the gates, replaces only the version in the modelId line (comments and
formatting stay as they are), prints 2.5.0 → 3.0.0 and validates the package again. Nothing is written when the version is
already valid, on a downgrade (OCTO-CK101/OCTO-CK201), when an acknowledgement is missing or stale (OCTO-CK203/OCTO-CK204)
or when the model does not compile. Pass several -p paths in dependency order: a dependent is validated against the sibling's
new version. If a dependency has a newer major than your range allows, the command reports
not reconciled automatically: range … excludes … and leaves the range untouched; update the range and run it again.
Dry run of a base model against its dependents
Before you publish the next version of a base model (System, Basic, …), check what it does to every model that depends on
it. Nothing is published, registered or written:
octo-ckc -c ValidateCascade -p src/ConstructionKit -o cascade.md
The command compiles the candidate in memory, finds the dependents in the local and cached catalogs (newest version per major
line, transitive dependents included) and prints one verdict each: Compatible, NeedsRepin (an exact-pinned model that must be
recompiled and republished, with the bump level), Breaks (with the element or member that no longer binds, a floor that is not
met, or a name collision with a member of a derived type) and NotInRange (the dependent's range or pin does not cover the
candidate's major). The exit code is non-zero when at least one dependent breaks. The same catalog options as ValidateVersion
apply (-cn, -rf, -lce, -lcr).
Candidate: System-2.3.0
Baseline: System-2.2.0 (LocalFileSystemCatalog, local, not published)
Candidate change level: MINOR (minimum version 2.3.0, declared 2.3.0: ok)
Breaks PlantE-1.0.0 (exact pins)
PlantE-1.0.0 uses System-2.2.0/Description-1, which System-2.3.0 does not define (attribute removed or re-identified)
Compatible LineR-1.0.0 (range-retaining)
no references into the candidate
2 dependent(s): 1 Breaks, 0 NeedsRepin, 1 Compatible, 0 NotInRange
The dry run proves that the referenced surface still binds. It does not prove behavioural changes (defaults, display rules, non-unique indexes) or the compatibility of existing runtime data.
Change classification examples
| Change | Classification |
|---|---|
| New type, attribute, or enum member | MINOR |
Flagging an existing attribute isRuntimeState: true | MINOR |
Changing an attribute's valueType from String to Secret (Secret Attributes) | MINOR |
Setting, clearing or changing a record's recordKey | MINOR |
Removing or renaming a type/attribute, changing an attribute's type (except String → Secret) | MAJOR |
| Description/metadata-only changes | PATCH |
Changing a type's displayNameRule / displayDescriptionRule | PATCH |
CK Language 2 Changes
For models with ckLanguage: 2 the gate classifies the new elements and modifiers as follows (values are compared as resolved, so an omitted modifier counts as its default):
| Change | Classification |
|---|---|
New interface, new implements entry on a type, new method | MINOR |
Interface removed, implements entry removed, method removed | MAJOR |
| Optional interface attribute or association added | MINOR — the interface grows within its version. An optional attribute member is MINOR only with an attribute definition that is new in this release (or was internal); reusing a definition that was already public is MAJOR, because a type of another model may already assign it as Hidden or under another name |
Required interface member added, member removed or renamed, member attribute changed, association target or multiplicity changed, member made required; interface extends entry added or removed; method added to an interface | MAJOR — publish a new interface version (Named-2) next to the old one instead |
| Interface member made optional | MINOR |
Method: optional parameter added, parameter made optional, idempotent false → true, timeout changed, sensitive changed, authorization loosened (role added — also to an empty list —, scope removed, allowSelf set, a block added that only grants roles) | MINOR (on an interface method the parameter and sensitive changes are MAJOR, see below); looser authorization is listed under "Behavioural changes" as security: method access widened |
Authorization semantics: roles are any-of (one listed role suffices; role names compare case-insensitively, Admin = admin), scopes are all-of (every listed scope is required in addition to octo_api), and authorization is default-deny — a method without an authorization block, or with empty roles, can only be invoked by administrators. | |
Interface method: any change of its invocation contract — kind, a parameter added or removed, a parameter's type, optionality or sensitive, the result, an error code added or removed | MAJOR — a type that redeclares the method keeps the old contract and stops compiling (message 122); descriptions, authorization and execution are not part of the contract |
Method: required parameter added, parameter removed, renamed or retyped, parameter made required, result changed, error code added or removed, kind changed, idempotent true → false, authorization tightened (role removed or roles emptied, scope added, allowSelf cleared, authorization block removed) | MAJOR — publish a new method version (ChangePassword-2) instead |
Interface deprecated set or cleared | MINOR |
Type association targetCkInterfaceId set or changed / cleared | MAJOR / MINOR |
visibility Public → Internal / Internal → Public | MAJOR / MINOR |
derivable Any → Model / Model → Any | MAJOR / MINOR |
Attribute-assignment access tightened (ReadWrite < ReadOnly < MethodOnly < Hidden) / relaxed | MAJOR / MINOR, with an "access/security" note in the changelog |
access tightened on an attribute marked securitySensitive: true in both versions | MINOR + requires acknowledge (security exception) |
securitySensitive set or cleared | MINOR |
ckLanguage raised 1 → 2 | MINOR — but MAJOR unless every type and record declares derivable: Any, because the derivable default flips to Model (reported per element) |
ckLanguage lowered 2 → 1 | MAJOR |
Method description, parameter and error descriptions, interface description | PATCH |
Internal elements are outside the compatibility surface. Other models can never reference an element declared
visibility: Internal, so changing it — or any member of an internal type, record, enum, interface or method — is at
most MINOR (descriptions stay PATCH). This holds only when the element is internal in both versions: removing an
element that was public, making a public element internal, or making an element public and changing it in the same
release follows the public rules. Example: an internal helper type may lose an attribute in a minor release; a public
one may not.
This is only sound because no public element may reach an internal one: in a ckLanguage: 2 model the compiler
rejects a public type, attribute, record, association role, interface or public method that references an internal
element of its own model (base type, implemented interface, assigned attribute, association role or target, value or
parameter record/enum, extends, interface member), a public interface with an internal method, and a type that
redeclares a public interface's method as internal — message 129 ("inconsistent visibility"). Make the referenced
element public, or the referrer internal. Internal → public, internal → internal and internal methods on public types
stay allowed.
Behavioural changes. Changes that keep the schema compatible but change runtime behaviour — default values, display
rules, auto-complete and auto-increment settings, change streams, non-unique indexes, method timeouts — keep their level
and are listed in their own section "Behavioural changes" in the verdict and in CHANGELOG.md, together with changes
that require an acknowledge. A unique index on a stable base (a type other models may derive from: public, not final,
derivable: Any; in CK language 1 System/Entity and System/Configuration) stays MAJOR and also requires an
acknowledge, because every derived type in every dependent model gets the index. One default change is not behavioural
but breaking: removing the default values of an attribute definition that is assigned as required, or that is public
(another model may assign it as required), is MAJOR — otherwise a required attribute without default would arrive in two
minor releases.
In CK language 2 an index is matched by its field paths (case-insensitive): changing only the case of a path is no
change, Unique → UniqueNotDeleted or unique → non-unique is MINOR, and UniqueNotDeleted → Unique or making an
index unique is MAJOR.
Add an optional member or publish X-2? Add the member to Named-1 when implementors need not provide it
(isOptional: true). When every implementor must provide it, or an existing member changes, publish Named-2 next to
Named-1 and let implementors move at their own pace.
Do not publish a ckLanguage: 2 or range-retaining version of a base model (for example System or Basic) into a shared catalog — the GitHub catalogs (octo-ckc) or any catalog other teams compile against — until every service that consumes that catalog runs the CK v2 engine. After one such publish every dependent compiled against the catalog inherits the base's minEngineVersion and is published under ck-models/v3/, which older engines do not read. See CK language 2.
For range-retaining models the declared ranges (dependencyRanges) are compared
per dependency: a raised or lowered floor and a range widened or narrowed within the same major are MINOR — unless the
new range or floor excludes the dependency version the previous release resolved, which is MAJOR because a tenant that
installed the previous release with that version would need a downgrade — a range or floor moving to another major is MAJOR, a new range dependency is MINOR and a removed one MAJOR. Switching a model from
exact pins to range retention (or back) is MINOR — unless switching back pins a version other than the one the previous
release resolved, which is MAJOR. A raised floor counts as excluding the previous resolution once it goes beyond it. Every declared dependency also carries
usedSurface, the elements and members of the dependency the
model uses, with a sha256 usedSurfaceHash; it is not classified on its own. The exact dependencies closure is still diffed as before; dropping
the "resolved dependency changed → MINOR" rule for range-retaining models comes with a later release (F2.4).
Embedded Models: Downgrade Guard and Re-validation
Services embed the CK models they need (System, System.StreamData, System.Identity, service-managed models such as System.UI) and import them into a tenant when the tenant is resolved or set up. During a rolling update, or when services of different versions run side by side, a service may embed an older version than the tenant already holds. Every embedded/startup import therefore goes through a downgrade guard that compares the installed model by name:
| Installed in the tenant | Decision | Log |
|---|---|---|
| nothing, or an older version | import (upgrade, migrations run as usual) | INFO |
| the same version | nothing to do (pending migrations are retried) | DEBUG |
| a newer version of the same major | skip — the tenant keeps its newer model | INFO downgrade prevented |
| a higher major | skip — the service keeps running against the newer model | WARN this service is too old for the tenant |
A skipped import sends no tenant-update notification, runs no migration and leaves the CK cache alone. Each skip is logged once per process (repeats at DEBUG). The decision is repeated under the model import lock, so two services starting in parallel cannot downgrade each other.
Consequences:
- An older service runs against the tenant's newer
System— runtime code works with version-less ids, and the CK cache is built from what is installed. - Its own exact-pinned service model (for example a
System.Botcompiled againstSystem-2.5.0) becomesResolveFailedwhile the tenant holdsSystem 2.6.0, and its types disappear from the tenant's schema until the service is upgraded. In production all services are released together, so this window is the rollout. Range-retaining service models stayAvailable. - Blueprint installs use the same guard for their
ckModelDependencies(see above).
Explicit imports may downgrade. ImportCk through octo-cli or the API is an operator decision and is not guarded: it may install an older version, logged as WARN Explicit downgrade of CK model ... from X to Y. If a running service embeds a newer version of that model, the next tenant resolve upgrades it back.
Re-validation of ResolveFailed models
At the end of every import into a tenant the engine re-validates all Available and ResolveFailed models:
- a
ResolveFailedmodel whose dependencies resolve again becomesAvailable(INFO), its collections and indexes are restored and the CK cache is reloaded; - an
Availablemodel that no longer resolves becomesResolveFailed(WARN with the unmet dependency, e.g.exact pin System-2.5.0: installed System-2.6.0); - a model that still fails stays
ResolveFailedwithout further log lines.
An embedded import that finds its model already installed in state ResolveFailed triggers the re-validation as well, so a restart heals such a model even without an import. Previously a ResolveFailed model only recovered when it was re-imported.
Metrics
The guard and the re-validation emit counters on the meter Meshmakers.Octo.MongoDb (Prometheus names, no tenant label — the tenant is in the log line):
| Metric | Labels | Meaning |
|---|---|---|
octo_ck_embedded_import_skipped_total | model, reason = newer_installed | newer_major_installed | Embedded imports skipped because the tenant holds a newer version |
octo_ck_explicit_import_downgraded_total | model | Explicit imports that installed an older version |
octo_ck_model_revalidated_total | result = recovered | still_failed | Re-validation outcomes of ResolveFailed models |
A rising newer_major_installed count means a service is too old for a tenant and should be upgraded.
See Also
- Library Management — catalogs, compatibility checks, import pipeline
- Blueprints —
ckModelDependencies, force re-apply semantics - CK Model Migrations — migration scripts for major changes
- CK Language 2 — interfaces, access modifiers, method definitions, range retention