Skip to content

Explore CalVer for MrDocs #1223

@alandefreitas

Description

@alandefreitas

We could consider moving MrDocs to Calendar Versioning (CalVer) for the project as a whole, and to reserve Semantic Versioning (SemVer) for the individual subsystems that actually have a contract to keep: the configuration file format, the XML/JSON output schemas, the plugin ABI, and the extension API.

This follows the pattern set by LLVM (see https://blog.llvm.org/2016/12/llvms-new-versioning-scheme.html), which MrDocs depends on, by Boost (our own ecosystem), and by a long list of end-user applications that have made the same move. References are collected throughout and listed at the end.

The short version of the argument is that MrDocs is an end-user application built on top of Clang/LLVM, with the same kind of internal API that LLVM has for various subsystems. As with most user-facing tools, promising a stable public API at the level of the whole tool (config, commands, library, extensions, and plugins at the same time) would be theater. A single SemVer number on the front of the project tries to answer a question ("Is this upgrade safe for code that depends on us?") that does not have one answer here, because MrDocs is not a single contract. It is a CLI tool, an embeddable library, a config format, several output formats, a template/addon system, and a plugin loader, each with a different audience and a different rate of change. CalVer would be on the outside, and SemVer on the parts that need it to let each user make decisions about the subsystems.

Where we are today: We currently version the project with SemVer. CMakeLists.txt declares:

project(
    MrDocs
    VERSION 0.8.0
    DESCRIPTION "C++ Documentation Tool"
)

plus a git-SHA build string. The CMake config even encodes a SemVer expectation for the future:

if (CMAKE_PROJECT_VERSION VERSION_LESS 1.0.0)
    set(compatibility_mode SameMajorVersion)
else ()
    set(compatibility_mode SameMinorVersion)
endif ()

So today the plan is implicitly "reach 1.0.0, then start honoring major/minor compatibility." This plan commits us to a promise we cannot keep, effectively keeping us stuck under v1.0.0. The moment we hit 1.0.0, we will either break it constantly (more likely) or freeze the project completely to avoid breaking it.

SemVer is a precise promise to people who depend on a stable interface: a major bump means "we broke something you call," a minor bump means "we added something, you are safe," a patch bump means "we fixed something, you are safe." That promise is valuable, but only when the thing being versioned has a well-defined interface that consumers link against.

MrDocs as a whole does not, because

  1. We inherit LLVM's instability. LLVM deliberately does not promise API or ABI stability, and bumps its major version every six months regardless of impact. The LLVM project says so directly in LLVM's New Versioning Scheme: essentially every release is API breaking, so by SemVer logic the major number "must" increment every time, so they just increment it on a schedule. Since our AST extraction sits directly on Clang internals, any LLVM bump can change what MrDocs sees. We cannot offer our users more stability than our own foundation offers us.

  2. Our real interface is its observable behavior, and we cannot freeze that. Every observable behavior will get depended on by someone. The "observable behavior" of Mr.Docs is the entire rendered output. We deliberately change that output constantly to improve it. Under SemVer, every commit is technically a breaking change. A version scheme that forces a major bump on nearly every commit is conveying no information.

  3. MrDocs is an application, not a library. The primary product is a tool you run, not the API you may also link to (and have semver for). The Python Packaging Authority makes exactly this distinction in its versioning guide: SemVer fits libraries with a deprecation contract, while projects with a regular time-based cadence are often better served by CalVer. SensioLabs puts it plainly in SemVer vs. CalVer: SemVer is the standard for libraries, CalVer is the better choice for applications.

Why CalVer: CalVer encodes the release date into the version number (for example, 26.6 for June 2026, or 2026.1 for the first release of 2026). It answers the question an application's version should answer: "how recent is this, and how does it sit on the release timeline?" That is the honest question for MrDocs, because what users actually want to know is whether they are on a recent build, not whether we promise not to change anchor names. It tells the truth. It frees the subsystems. It matches our release reality. It matches what our users already see.

SemVer for the subsystems: This is where SemVer is useful. It is something our current single-number scheme cannot give us. MrDocs has several interfaces for each main version. CalVer on the outside lets each inner interface carry its own SemVer, so each one can make and keep a promise appropriate to its audience. This is also similar to how other user-facing applications do things.

The subsystems that should be versioned independently over time:

  • Configuration file format.
  • Output schemas
  • Plugin ABI.
  • Extension / scripting API.
  • Embeddable library

Prior art: End-user applications and ecosystems that version on the calendar or on a time-based train rather than on SemVer compatibility:

  • LLVM / Clang, our own foundation. A new major every six months, incremented mechanically because nearly every release breaks an API. See LLVM's New Versioning Scheme. clang-format, clang-tidy, and friends inherit this number. If the tool we are built on treats its version as a release pointer rather than a compatibility promise, it is strange for us to claim more.
  • Boost, our ecosystem. Boost ships on a roughly quarterly time-based train (the release history and release schedule show fixed cutoff and release dates), and the leading 1. has been frozen for the entire life of the project while the minor increments every release. The number is effectively a release counter, not a SemVer contract. This is the example closest to home for a C++ Alliance project.
  • Python is in the middle of adopting CalVer for the language itself: PEP 2026 – Calendar versioning for Python. The core packaging tools already use it: pip (e.g. 26.0), Black, virtualenv, and PyPA's packaging library, per the CalVer users page.
  • Ubuntu has used CalVer since its first release in 2004 (4.10 is October 2004). The version literally is the date.
  • Docker switched from SemVer to CalVer in 2017 (Docker 17.03, YY.0M format), an explicit, public migration by a major application.
  • JetBrains IDEs (PyCharm, IntelliJ, CLion: 2026.1) and Unity use year-based versioning.
  • yt-dlp and similar tools use full date stamps (YYYY.MM.DD).

And the direct counterpoint worth naming: Doxygen, our closest competitor, has sat on 1.x.y for roughly twenty-five years (1.13.x at time of writing). Its major number is as meaningless as Boost's leading 1.: nobody reads a Doxygen version as a compatibility statement. The "SemVer-shaped but not actually SemVer" number is the failure mode I want us to avoid by being explicit about what we mean.

For the theory behind splitting the contracts:

Sketch of a concrete scheme: Nothing here is final. This is a starting point to argue about.

  • Project: YYYY.MINOR[.PATCH] (for example 2026.1, then 2026.2), where MINOR is the release within the year and PATCH is a bugfix on a release. We could also go YY.MM. We keep the git SHA build string we already embed.
  • Config format: a config-version field (SemVer) checked on load, with migration or a clear diagnostic on mismatch.
  • Output schemas: a SemVer schema-version attribute on the XML root and in the JSON output, bumped on its own clock.
  • Plugin ABI: a SemVer the loader verifies before loading a shared library.
  • Extension/scripting API and library headers: SemVer documented in their own headers/docs.

include/mrdocs/Version.hpp.in already gives us the build-metadata plumbing. This mostly changes what string we put in project(... VERSION ...) and adds small version fields to the subsystems that need them.

Open questions

  • What exact CalVer shape do we want: YYYY.MINOR or YY.MM? Do we want a patch component, and what triggers it?
  • The CMake find_package config currently leans on SameMajorVersion/SameMinorVersion compatibility for mrdocs-core. If the project number becomes a date, which compatibility mode do we use for the installed library, and does mrdocs-core get its own SemVer separate from the tool's CalVer?
  • Do we adopt independent subsystem versions all at once, or start with the highest-value ones (config format and output schema, which break external tooling most visibly) and add the rest later?
  • How do we communicate the change so users do not read a jump from 0.8.0 to 2026.x as "version 2026, a huge release"? A short policy doc plus the first release notes probably cover it.
  • Does the embeddable-library story (examples/library/) carry enough of a stability promise that it deserves SemVer now, or is it still too early to commit to anything there?
  • How does this interact with Explore reflection #1114 (reflection) and Explore mrdocs as compiler mode #1073 (mrdocs-as-compiler)? Both reshape the corpus and the serialized formats, which is exactly the kind of churn that motivates putting SemVer on the schemas rather than on the tool.

Metadata

Metadata

Assignees

No one assigned

    Labels

    choreMaintenance, research

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions