Docs / Architecture / Versioning
Explanation For everyone

Versioning

One number names the application and its documentation together, because they ship as one thing: the pages a reader opens at /docs are always the pages of the version that is running, never a newer or older set.

One number, read from one file

VERSION, at the root of the repository, is the only place the number is written. version.current() reads it once — a deploy restarts the process, so reading it again would only ever find the same answer — and the sidebar, the CLI, the API and the documentation header all print it from there, so there is no second copy to drift out of step with the first.

version.build() is a different question: which commit is actually running, from DPF_BUILD, else a BUILD file the deploy writes, else git rev-parse --short HEAD. The version says what was released; the build says exactly what is deployed, and between two releases those are not the same fact — a restart, a hotfix rebuilt from the same tag, or a box that is a commit behind the one that was meant to go out all show the same version and a different build. version.label() prints both together, 0.1.0 · b325fcd, for exactly that reason.

What MAJOR, MINOR and PATCH mean here

This application has no library that code calls, so "breaking" never means a function signature changed — it means something a user, an operator or an integration was depending on stopped being true.

  • MAJOR — a setting, a CLI command or an API field is removed or changes meaning, the workflow file's format changes so an existing file no longer loads, or a database change needs a manual step. Something that already depended on the old behaviour now has to change.
  • MINOR — something is added: a step, a setting, an endpoint, an expectation kind. Nothing that already worked stops working.
  • PATCH — a fix, with no change to what anybody already relies on.
  • Before 1.0.0, semantic versioning allows a breaking change to raise MINOR instead of MAJOR, and this factory uses that allowance rather than pretending to a stability it has not earned yet. A reader of the changelog should assume that rule is in force until the first 1.0.0, not the convention that applies afterwards.

A data contract has a semantic version of its own, too: v0.1 when Source ends, 0.x while it is agreed, 1.0.0 when it first goes live, and after that computed from what changed, so a removed column is a major version of that product's promise. It is a different number for a different thing, and unrelated to the factory's.

The API keeps its own number

/api/v1 carries a version of its own (api_v1.VERSION, 1.0 today) that is reported alongside the application's, not instead of it. It moves only when a system reading the API would have to change something to keep working — which is rarer than every change to the application — and the path itself, v1, is the part of that promise a caller can see without reading a response body at all.

Cutting a release

python -m dpf release major|minor|patch — or an exact MAJOR.MINOR.PATCH — is the one path to a new version, and it refuses to take it on trust. It reads Unreleased in the changelog, and if there is nothing written there it stops: "a version number has to be able to say what it means." Where there is something, it moves those lines into a new section dated today, writes the number into VERSION, keeps the comparison links at the foot of the file in step, and prints the two git commands — commit, then tag v<number> — rather than running them, because a release is the one commit somebody should read before it exists. See Release a version.

Keeping the documentation honest about all of this

The documentation is reviewed in the same pull request as the code it describes, and five mechanisms hold it to that rather than trusting it:

  • Generated sections. A block between <!-- generated: NAME --> and <!-- /generated --> is rewritten from the code it describes — settings, the CLI, API endpoints, audit codes, permissions, the workflow and its drawing — whenever a page is served or python -m dpf docs build runs, so the copy on GitHub and the copy the application renders cannot disagree.
  • Review stamps. A page's front matter names what it covers: the files that would make it wrong if they changed. fingerprint is a hash of those files' bytes at the moment somebody last read the page against them. Change a covered file and the fingerprint stops matching, which fails python -m dpf docs check and names the page until it is read again and re-stamped with python -m dpf docs stamp PAGE. Stamping without reading is the one way to defeat this, and nothing stops it mechanically — a check that can be lied to is still worth having, because most people are not looking for the lie.
  • Links and navigation. Every relative link has to resolve to a page listed in nav.yaml, every #anchor to a heading that exists on the page it names, and every file under docs/ has to be listed exactly once.
  • Environment-variable drift. A variable the code reads that the settings registry has never heard of fails its own test, and so does the opposite — documented, and read by nothing. The CLI and the API cannot drift the same way, because their reference pages are generated rather than written by hand.
  • Drawings. A drawing is a few lines of YAML in its page, drawn when the page is served (dpf/docs_diagrams.py). A box names the code it stands for, and the check fails the day that path is renamed or deleted; a drawing that cannot be laid out — two boxes in one place, a label longer than its line — fails it too. The layers drawing on Components is held to the real imports by tests/test_architecture.py.

python -m dpf docs check runs everything the test suite checks, faster, and says what to do about each problem it finds.

Documentation for version 0.2.0 Reviewed 2026-10-04 Covers dpf/version.py, dpf/release.py, dpf/docs.py, dpf/docs_diagrams.py