Kelvin Versioning
25 Feb 2020Long ago, in the distant past, Curtis introduced the idea of kelvin versioning in an informal blog post about Urbit. Imagining the idea of an ancient and long-frozen form of Martian computing, he described this versioning scheme as follows:
Some standards are extensible or versionable, but some are not. ASCII, for instance, is perma-frozen. So is IPv4 (its relationship to IPv6 is little more than nominal - if they were really the same protocol, they’d have the same ethertype). Moreover, many standards render themselves incompatible in practice through excessive enthusiasm for extensibility. They may not be perma-frozen, but they probably should be.
The true, Martian way to perma-freeze a system is what I call Kelvin versioning. In Kelvin versioning, releases count down by integer degrees Kelvin. At absolute zero, the system can no longer be changed. At 1K, one more modification is possible. And so on. For instance, Nock is at 9K. It might change, though it probably won’t. Nouns themselves are at 0K - it is impossible to imagine changing anything about those three sentences.
Understood in this way, kelvin versioning is very simple. One simply counts downwards, and at absolute zero (i.e. 0K) no other releases are legal. It is no more than a versioning scheme designed for abstract components that should eventually freeze.
Many years later, the Urbit blog described kelvin versioning once more in the post Towards a Frozen Operating System. This presented a significant refinement of the original scheme, introducing both recursive and so-called “telescoping” mechanics to it:
The right way for this trunk to approach absolute zero is to “telescope” its Kelvin versions. The rules of telescoping are simple:
If tool B sits on platform A, either both A and B must be at absolute zero, or B must be warmer than A.
Whenever the temperature of A (the platform) declines, the temperature of B (the tool) must also decline.
B must state the version of A it was developed against. A, when loading B, must state its own current version, and the warmest version of itself with which it’s backward-compatible.
Of course, if B itself is a platform on which some higher-level tool C depends, it must follow the same constraints recursively.
This is more or less a complete characterisation of kelvin versioning, but it’s still not quite precise enough. If one looks at other versioning schemes that try to communicate some specific semantic content (the most obvious example being semver), it’s obvious that they take great pains to be formal and precise about their mechanics.
Experience has demonstrated to me that such formality is necessary. Even the excerpt above has proven to be ambiguous or underspecified re: the details of various situations or corner cases that one might run into. These confusions can be resolved by a rigorous protocol specification, which, in this case isn’t very difficult to put together.
Kelvin versioning and its use in Urbit is the subject of the currently-evolving UP9, recent proposed updates to which have not yet been ratified. The following is my own personal take on and simple formal specification of kelvin versioning – I believe it resolves any major ambiguities that the original descriptions may have introduced.
Kelvin Versioning (Specification)
(The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.)
For any component A following kelvin versioning,
-
A’s version SHALL be a nonnegative integer.
-
A, at any specific version, MUST NOT be modified after release.
-
At version 0, new versions of A MUST NOT be released.
-
New releases of A MUST be assigned a new version, and this version MUST be strictly less than the previous one.
-
If A supports another component B that also follows kelvin versioning, then:
- Either both A and B MUST be at version 0, or B’s version MUST be strictly greater than A’s version.
- If a new version of A is released and that version supports B, then a new version of B MUST be released.
These rules apply recursively for any kelvin-versioned component C that is supported by B, and so on.
Examples
Examples are particularly useful here, so let me go through a few.
Let’s take the following four components, sitting in three layers, as a running example. Here’s our initial state:
A 10K
B 20K
C 21K
D 30K
So we have A at 10K supporting B at 20K. B in turn supports both C at 21K and D at 30K.
State 1
Imagine we have some patches lying around for D and want to release a new version of it. That’s easy to do; we push out a new version of D. In this case it will have version one less than 30, i.e. 29K:
A 10K
B 20K
C 21K
D 29K <-- cools from 30K to 29K
Easy peasy. This is the most trivial example.
The only possible point of confusion here is: well, what kind of change warrants a version decrement? And the answer is: any (released) change whatsoever. Anything with an associated kelvin version is immutable after being released at that version, analogous to how things are done in any other versioning scheme.
State 2
For a second example, imagine that we now have completed a major refactoring of A and want to release a new version of that.
Since A supports B, releasing a new version of A obligates us to release a new version of B as well. And since B supports both C and D, we are obligated, recursively, to release new versions of those to boot.
The total effect of a new A release is thus the following:
A 9K <-- cools from 10K to 9K
B 19K <-- cools from 20K to 19K
C 20K <-- cools from 21K to 20K
D 28K <-- cools from 29K to 28K
This demonstrates the recursive mechanic of kelvin versioning.
An interesting effect of the above mechanic, as described in Toward a Frozen Operating System is that anything that depends on (say) A, B, and C only needs to express its dependency on some version of C. Depending on C at e.g. 20K implicitly specifies a dependency on its supporting component, B, at 19K, and then A at 9K as well (since any change to A or B must also result in a change to C).
State 3
Now imagine that someone has contributed a performance enhancement to C, and we’d like to release a new version of that.
The interesting thing here is that we’re prohibited from releasing a new version of C. Recall our current state:
A 9K
B 19K
C 20K <-- one degree K warmer than B
D 28K
Releasing a new version of C would require us to cool it by at least one kelvin, resulting in the warmest possible version of 19K. But since its supporting component, B, is already at 19K, this would constitute an illegal state under kelvin versioning. A supporting component must always be strictly cooler than anything it supports, or be at absolute zero conjointly with anything it supports.
This illustrates the so-called telescoping mechanic of kelvin versioning – one is to imagine one of those handheld telescopes made of segments that flatten into each other when collapsed.
State 4
But now, say that we’re finally going to release our new API for B. We release a new version of B, this one at 18K, which obligates us to in turn release new versions of C and D:
A 9K
B 18K <-- cools from 19K to 18K
C 19K <-- cools from 20K to 19K
D 27K <-- cools from 28K to 27K
In particular, the new version of B gives us the necessary space to release a new version of C, and, indeed, obligates us to release a new version of it. In releasing C at 19K, presumably we’d include the performance enhancement that we were prohibited from releasing in State 3.
State 5
A final example that’s simple, but useful to illustrate explicitly, involves introducing a new component, or replacing a component entirely.
For example: say that we’ve decided to deprecate C and D and replace them with a single new component, E, supported by B. This is as easy as it sounds:
A 9K
B 18K
E 40K <-- initial release at 40K
We just swap in E at the desired initial kelvin version. The initial kelvin can be chosen arbitrarily; the only restriction is that it be warmer than the the component that supports it (or be at absolute zero conjointly with it).
It’s important to remember that, in this component-resolution of kelvin versioning, there is no notion of the “total temperature” of the stack. Some third party could write another component, F, supported by E, with initial version at 1000K, for example. It doesn’t introduce any extra burden or responsibility on the maintainers of components A through E.
Collective Kelvin Versioning
So – all that is well and good for what I’ll call the component-level mechanics of kelvin versioning. But it’s useful to touch on a related construct, that of collectively versioning a stack of kelvin-versioned components. This minor innovation on Curtis’s original idea was put together by myself and my colleague Philip Monk.
If you have a collection of kelvin-versioned things, e.g. the things in our initial state from the prior examples:
A 10K
B 20K
C 21K
D 30K
then you may want to release all these things, together, as some abstract thing. Notably, this happens in the case of the Urbit kernel, where the stack consists of a functional VM, an unapologetically amathematical purely functional programming language, special-purpose kernel modules, etc. It’s useful to be able to describe the whole kernel with a single version number.
To do this in a consistent way, you can select one component in your stack to serve as a primary index of sorts, and then capture everything it supports via a patch-like, monotonically decreasing “fractional temperature” suffix.
This is best illustrated via example. If we choose B as our primary index in the initial state above, for example, we could version the stack collectively as 20.9K. B provides the 20K, and everything it supports is just lumped into the “patch version” 9.
If we then consider the example given in State 1, i.e.:
A 10K
B 20K
C 21K
D 29K
in which D has cooled by a degree kelvin, then we can version this stack collectively as 20.8K. If we were to then release a new version of C at 20K, then we could release the stack collectively as 20.7K. And so on.
There is no strictly prescribed schedule as to how to decrease the fractional temperature, but the following schedule is recommended:
.9, .8, .7, .., .1, .01, .001, .0001, ..
Similarly, the fractional temperature should reset to .9 whenever the primary index cools. If we consider the State 2, for example, where a new release of A led to every other component in the stack cooling, we had this:
A 9K
B 19K
C 20K
D 28K
Note that B has cooled by a kelvin, so we would version this stack collectively as 19.9K. The primary index has decreased by a kelvin, and the fractional temperature has been reset to .9.
While I think examples illustrate this collective scheme most clearly, after my schpeel about the pitfalls of ambiguity it would be remiss of me not to include a more formal spec:
Collective Kelvin Versioning (Specification)
(The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.)
For a collection of kelvin-versioned components K:
-
K’s version SHALL be characterised by a primary index, chosen from a component in K, and and a real number in the interval [0, 1) (the “fractional temperature”), determined by all components that the primary index component supports.
The fractional temperature MAY be 0 only if the primary index’s version is 0.
-
K, at any particular version, MUST NOT be modified after release.
-
At primary index version 0 and fractional temperature 0, new versions of K MUST NOT be released.
-
New releases of K MUST be assigned a new version, and this version MUST be strictly less than the previous one.
-
When a new release of K includes new versions of any component supported by the primary index, but not a new version of the primary index proper, its fractional temperature MUST be less than the previous version.
Given constant primary index versions, fractional temperatures corresponding to new releases SHOULD decrease according to the following schedule:
.9, .8, .7, .., .1, .01, .001, .0001, ..
-
When a new release of K includes a new version of the primary index, the fractional temperature of SHOULD be reset to 9.
-
New versions of K MAY be indexed by components other than the primary index (i.e., K may be “reindexed” at any point). However, the new chosen component MUST either be colder than the primary index it replaces, or be at version 0 conjointly with the primary index it replaces.
Etc.
In my experience, the major concern in adopting a kelvin versioning scheme is that one will accidentally initialise everything with a set of temperatures (i.e. versions) that are too cold (i.e. too close to 0), and thus burn through too many version numbers too quickly on the path to freezing. To alleviate this, it helps to remember that one has an infinite number of release candidates available for every component at every temperature.
The convention around release candidates is just to prepend a suffix to the next release version along the lines of .rc1, .rc2, etc. One should feel comfortable using these liberally, iterating through release candidates as necessary before finally committing to a new version at a properly cooler temperature.
The applications that might want to adopt kelvin versioning are probably pretty limited, and may indeed even be restricted to the Urbit kernel itself (Urbit has been described by some as “that operating system with kernel that eventually reaches absolute zero under kelvin versioning”). Nonetheless: I believe this scheme to certainly be more than a mere marketing gimmick or what have you, and, at minimum, it makes for an interesting change of pace from semver.