Skip to main content

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​

ContextInputResolves to
Compile / publish (octo-ckc)Ranges in the source ckModel.yamlHighest published version satisfying the range — frozen into the compiled library as an exact pin
Tenant import / FixAllExact pins of the compiled libraryExactly the pinned versions — the tenant must hold them precisely
Blueprint apply (ckModelDependencies)Ranges in blueprint.yamlThe 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:

  1. Basic.Energy publishes a new version (say 1.3.0).
  2. EnergyCommunity still pins 1.2.x (compile-time freeze). It must be recompiled so the range re-resolves to 1.3.0, and republished with a version bump — the SemVer gate (OCTO-CK100) enforces the bump because the resolved dependency set changed.
  3. 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).
Blueprint manifest and seed data pin independently

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:

CodeMeaning
OCTO-CK100Declared 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-CK101Declared version is lower than the baseline (the newest published version of the same major) — downgrade, not allowed.
OCTO-CK102Catalog source unreachable — the baseline could not be determined. Never treated as a first publication.
OCTO-CK103A declared dependency range is satisfied by no published version (and by no sibling package validated earlier in the same run).
OCTO-CK104The 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-CK103 check — so same-commit dependency bumps across sibling models in one repository work.
  • Any OCTO-CK1xx finding makes the command exit non-zero (-6, which surfaces as 250 in most shells). Gate CI on exit 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:

  1. Same major. The baseline is the newest published version of the declared major. With EnergyCommunity 3.3.0 and 4.5.0 published, a declared 3.4.0 is compared with 3.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 declared 3.2.0 fails with OCTO-CK101 against 3.3.0.
  2. New major. When the declared major has no published version yet, the baseline is the newest version of the previous major line: 4.0.0 is compared with the newest 3.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.
  3. 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 ValidateVersion twice gives the same result. A published version equal to the declared one still counts: changing a model without raising its version fails with OCTO-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).

CodeMeaning
OCTO-CK200A 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-CK201The declared version is lower than the baseline of the same major.
OCTO-CK202The 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:

ValueBaseline fromDefault
Localthe local file-system catalog plus the cached remote catalogs; works without networkoutside CI (DebugL)
Remotethe GitHub catalogs only; the local catalog is ignoredwhen 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"
CodeMeaning
OCTO-CK203A change that needs an acknowledgement has no matching entry.
OCTO-CK204An 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 reason is mandatory and appears in the verdict, the build log and the CHANGELOG.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-CK200 even 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​

ChangeClassification
New type, attribute, or enum memberMINOR
Flagging an existing attribute isRuntimeState: trueMINOR
Changing an attribute's valueType from String to Secret (Secret Attributes)MINOR
Setting, clearing or changing a record's recordKeyMINOR
Removing or renaming a type/attribute, changing an attribute's type (except String → Secret)MAJOR
Description/metadata-only changesPATCH
Changing a type's displayNameRule / displayDescriptionRulePATCH

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):

ChangeClassification
New interface, new implements entry on a type, new methodMINOR
Interface removed, implements entry removed, method removedMAJOR
Optional interface attribute or association addedMINOR — 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 interfaceMAJOR — publish a new interface version (Named-2) next to the old one instead
Interface member made optionalMINOR
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 removedMAJOR — 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 clearedMINOR
Type association targetCkInterfaceId set or changed / clearedMAJOR / MINOR
visibility Public → Internal / Internal → PublicMAJOR / MINOR
derivable Any → Model / Model → AnyMAJOR / MINOR
Attribute-assignment access tightened (ReadWrite < ReadOnly < MethodOnly < Hidden) / relaxedMAJOR / MINOR, with an "access/security" note in the changelog
access tightened on an attribute marked securitySensitive: true in both versionsMINOR + requires acknowledge (security exception)
securitySensitive set or clearedMINOR
ckLanguage raised 1 → 2MINOR — but MAJOR unless every type and record declares derivable: Any, because the derivable default flips to Model (reported per element)
ckLanguage lowered 2 → 1MAJOR
Method description, parameter and error descriptions, interface descriptionPATCH

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.

Operating rule: shared catalogs

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 tenantDecisionLog
nothing, or an older versionimport (upgrade, migrations run as usual)INFO
the same versionnothing to do (pending migrations are retried)DEBUG
a newer version of the same majorskip — the tenant keeps its newer modelINFO downgrade prevented
a higher majorskip — the service keeps running against the newer modelWARN 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.Bot compiled against System-2.5.0) becomes ResolveFailed while the tenant holds System 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 stay Available.
  • 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 ResolveFailed model whose dependencies resolve again becomes Available (INFO), its collections and indexes are restored and the CK cache is reloaded;
  • an Available model that no longer resolves becomes ResolveFailed (WARN with the unmet dependency, e.g. exact pin System-2.5.0: installed System-2.6.0);
  • a model that still fails stays ResolveFailed without 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):

MetricLabelsMeaning
octo_ck_embedded_import_skipped_totalmodel, reason = newer_installed | newer_major_installedEmbedded imports skipped because the tenant holds a newer version
octo_ck_explicit_import_downgraded_totalmodelExplicit imports that installed an older version
octo_ck_model_revalidated_totalresult = recovered | still_failedRe-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​