Zillowe FoundationZillowe Documentation

Precedence and SemVer

How ZFVM versions are ordered, and how they project onto SemVer.

Ordering Within a Branch

ZFVM defines precedence only between versions on the same branch. Compare the core version first, and use the status only to break ties:

use zfvm::Version;

fn main() -> zfvm::Result<()> {
    let older = Version::parse("Dev Beta 1.2.0")?;
    let newer = Version::parse("Dev RC 1.2.0")?;

    assert!(older.is_precedent_of(&newer)?);

    Ok(())
}

Two consequences surprise people, and both follow from comparing the core first:

  • A release candidate of a higher core version outranks a release of a lower one. Dev RC 1.2.0 follows Dev Release 1.1.0.
  • A higher status never rescues a lower core version. Dev Release 1.1.0 does not follow Dev RC 1.2.0.

The build component never affects precedence. It exists for traceability only:

use zfvm::Version;

fn main() -> zfvm::Result<()> {
    let plain = Version::parse("Dev Beta 1.2.0")?;
    let built = Version::parse("Dev Beta 1.2.0 456")?;

    assert_eq!(
        plain.precedence_cmp(&built)?,
        std::cmp::Ordering::Equal
    );

    Ok(())
}

Cross-Branch Comparison Is an Error

A build on Dev is not meaningfully greater than or less than a build on Pub. ZFVM leaves this undefined rather than inventing an ordering, and the API reports it as an error:

use zfvm::Version;

fn main() {
    let dev = Version::parse("Dev Release 1.0.0").unwrap();
    let prod = Version::parse("Prod Release 1.0.0").unwrap();

    assert!(dev.precedence_cmp(&prod).is_err());
}

This is why Version does not implement Ord. The trait has no way to signal that two values are not comparable, so implementing it would force a total order that the specification does not define. Use precedence_cmp or is_precedent_of.

Deployment decisions that span branches belong to the deployment system, not to ZFVM.

Why Ord Is Missing

It is tempting to derive Ord on Branch and Status and implement it on Version. That would compile, and it would be wrong: it would silently answer the cross-branch question that ZFVM leaves undefined. A caller sorting releases from several branches would get a plausible-looking order with no specification behind it.

The Status enum does derive Ord, and its variant order matches the specification's ladder, so status.cmp(&other) is already correct. Branch also derives Ord, but no comparison across branches is meaningful, so nothing in the crate performs one outside of the documented vocabulary helpers.

The SemVer Projection

The canonical form is not a valid SemVer string. to_semver projects one onto the other: the status becomes a numeric pre-release identifier, and the build becomes build metadata.

use zfvm::Version;

fn main() -> zfvm::Result<()> {
    assert_eq!(
        Version::parse("Prod Release 2.0.0")?.to_semver(),
        "2.0.0"
    );
    assert_eq!(
        Version::parse("Dev Beta 1.2.3")?.to_semver(),
        "1.2.3-2"
    );
    assert_eq!(
        Version::parse("Dev Beta 1.2.3 456")?.to_semver(),
        "1.2.3-2+456"
    );
    assert_eq!(
        Version::parse("Pub Alpha 0.8.0 a7d3c1f")?.to_semver(),
        "0.8.0-1+a7d3c1f"
    );

    Ok(())
}

The branch has no SemVer representation and is omitted, so two versions on different branches can project to the same string. A consumer that needs the branch must store it separately.

Why the Status Becomes a Number

A readable projection would be 1.2.3-beta, which looks friendlier. It silently breaks precedence, though. SemVer compares pre-release identifiers numerically first and lexically second, so -alpha and -beta happen to sort in the right order by luck of the alphabet, but -pre-alpha sorts after -beta because p follows b.

The ordering ZFVM needs is Pre-Alpha < Alpha < Beta < RC < Release, which no purely alphabetic mapping can express. The numeric ordinal removes the luck:

StatusOrdinalProjects to
Pre-Alpha0-0
Alpha1-1
Beta2-2
RC3-3
Release4none

Using ordinals guarantees that SemVer precedence reproduces ZFVM precedence:

use semver::Version as Semver;
use zfvm::{Status, Version};

fn main() -> zfvm::Result<()> {
    let mut previous: Option<Semver> = None;

    for status in Status::ALL {
        let projected = Version::parse(&format!("Dev {status} 1.2.3"))?
            .to_semver_version()?;

        if let Some(prev) = previous {
            assert!(prev < projected, "{status} out of order");
        }
        previous = Some(projected);
    }

    Ok(())
}

The tradeoff is that -2 is less legible than -beta in a registry listing. The specification accepts that cost, because a projection whose ordering disagrees with the source scheme is worse than an ugly one.

The Underscore Substitution

_ is legal in a ZFVM build identifier but not in SemVer, which permits only [0-9A-Za-z-]. The projection replaces each _ with a -:

use zfvm::Version;

fn main() -> zfvm::Result<()> {
    assert_eq!(
        Version::parse("Spec Beta 1.4.2 nightly_build")?.to_semver(),
        "1.4.2-2+nightly-build"
    );
    Ok(())
}

This is why the build charset is narrow to begin with. It keeps the identifier usable in container tags, file names, and URL segments without escaping, and the single substitution is the only translation needed.

Do Not Mix Projection Conventions

SemVer gives numeric pre-release identifiers lower precedence than non-numeric ones. So 1.2.3-2 sorts below 1.2.3-beta, even though ZFVM assigns Beta the ordinal 2. A consumer comparing a projected string against a hand-written readable alias gets a silently wrong answer.

Project every version the same way, or the comparison is meaningless.

The Projection Is Not Round-Trippable

Neither the branch nor the original status can be recovered from a projected string. A tool that stores only the projection has discarded information ZFVM treats as significant, so store both when you need to reconstruct the version.

This is also why the branch cannot be smuggled into build metadata: SemVer ignores metadata when comparing, so anything placed there is invisible to ordering. Debian hit the same wall and introduced an epoch prefix to work around it. ZFVM sidesteps the problem because the branch occupies a visible prefix.


A software organization

2026 © All Rights Reserved.

  • All the content is available under CC BY-SA 4.0, expect where otherwise stated.

Last updated on