Version manifests
Rendered from spec at76d46f3 (2026-10-25) · its source · Edit this page
Each file here is one release’s staged state — the single source
of truth for a version’s goals and progress. Named <version>.toml
(e.g. 0.2.0.toml). Written by stage-version, read by the tracker and the gate,
and finalised by execute-version.
Contract
version = "0.2.0" # the release this manifest describes; matches the tag v0.2.0
status = "staged" # planned → staged → in_progress → releasable → released → yanked
released_on = "2026-07-30" # UTC date the release was published; written at release (OPS-R57)
released_as = "0.2.1" # the tag that carried these goals, where it is not this version's own
withdrawn_because = "the installer shipped a broken pin" # why it was yanked; only on `yanked` (E5-R8)
repos = ["lemonfiber", "lemonfiber-media-stack"] # streams this version cuts; never "brand"
satisfied_in = ["lemonfiber", "lemonfiber-web"] # where the goal gate searches; omit to mean `repos`
goals = ["A2-R1", "A2-R6", "C1-R13"] # locked Accepted requirement IDs (OPS-R30)
[[prerelease]] # one per pre-release cut from this version (OPS-R65)
tag = "v0.2.0-pre.1" # never the version's own tag
cut_on = "2026-07-24" # UTC date it was tagged
unmet = [] # always empty: a pre-release with an unmet goal is refused (OPS-R66)
pins = { lemonfiber-media-stack = "fbdafe0eb229c5c5016decf00b8a460b488a4225" }
[pins] # written at execute (OPS-R35); absent while staged
lemonfiber-media-stack = "fbdafe0eb229c5c5016decf00b8a460b488a4225" # one line per embedded submodule
lemonfiber-web = "ed49b4224f91d4e53055910e27db5d1b697a2de3"
| Field | Meaning |
|---|---|
version |
Semver, matching the eventual v<version> tag. |
status |
The lifecycle state (OPS-R32); every transition is recorded here. |
released_on |
The UTC date the release was published, YYYY-MM-DD (OPS-R57). Written by the transition to released, from the publication that triggered it — never typed. Absent on every earlier status. |
repos |
The release streams this version cuts. brand releases on its own clock and is never listed. |
satisfied_in |
The repositories the goal gate searches for Spec: citations (OPS-R58). Omit it and the streams in repos are searched, which is right whenever the two coincide. They do not always. 0.10.0 cuts lemonfiber alone and its goals are satisfied across four repositories: ARCH-R55 is cited in lemonfiber-web, and ARCH-R58 and ARCH-R67 in the two SDKs, which are rules only an SDK can be held to. Searched where it cuts, its gate calls all three unmet for a reason that is not about the work. 0.9.0 avoided that by putting lemonfiber-web in repos, and bought a different problem: execute-version tags every stream it names, and lemonfiber-web publishes on a version tag, so the first release run through the train would have published a build nobody asked for. Naming a repository here does not tag it; only repos does. Write it by checking rather than from memory: 0.10.0’s list was drafted three times and was short each time — first missing the web surface, then the two SDKs, then the stack, and each omission read as a goal nobody had done. Run the gate over the manifest with a repository added and see whether any verdict changes; one that does belongs on the list. |
goals |
The locked set of Accepted requirement IDs the release must satisfy before it ships. |
blockers |
What blocks the release, one table per blocker: what it is and where it is being dealt with, a pull request or an issue by address (OPS-R44). The releasable transition, a pre-release and execute refuse while one is listed, naming each. Written and removed by hand, in the pull request that finds or resolves the blocker. Absent when nothing blocks. |
withdrawn_because |
Why a shipped release was withdrawn, in one plain sentence. Present only on a yanked manifest, and required there: a withdrawal recorded bare is the record losing the only thing anyone comes back to it for. The release is never removed — the manifest stays, its goals stay, and this says what went wrong (E5-R8, E5-R12). Written by the transition to yanked, which refuses without it. |
released_as |
The tag the goals actually shipped under, present only where it is not this version’s own. A minor whose release run fails part-way is finished by a patch, and the patch is the artefact people install; there is no manifest per patch, because a patch delivers no goals and a manifest for it would be another version the serial train must walk past. Written by the transition to released — never typed. |
prerelease |
One table per pre-release cut from this version (OPS-R65), in the order they were cut. A pre-release is not a lifecycle transition: status does not move, released_on is not written, and the version goes on being whatever it was. What the record holds is what nothing else can answer afterwards — the tag, the day and the pins the artefacts embedded — beside unmet, the goals it went out without. The pre-release lane writes unmet empty, because a pre-release with an unmet goal is refused (OPS-R66); a record that lists goals there, as 0.17.0’s does, names what that artefact went out without. The tag is never the version’s own: v0.2.0-pre.1 orders below v0.2.0, so the release tag stays free and nothing that compares tags ranks the pre-release above the release. Written by the pre-release lane; never typed. |
pins |
The exact submodule commits embedded, recorded at execute so the release is reproducible from this file alone. One line per submodule the tag declares, named for the repository rather than the path it is mounted at — lemonfiber-media-stack, not assets/media-stack. The list is enumerated from the tag’s own .gitmodules, not named in the workflow: release-finalize spelled out the one path that existed when it was written, and went on recording only that one after ADR-0012 added the web app, so 0.10.0 first shipped a record that did not say which build of the app went out with it. |
The no-stub rule
Minors (0.4.0, 1.3.0, …) are themed slices toward the next major; patches
(x.y.Z) are hotfixes; a major (X.0.0) carries the capability that justifies
the number. Whichever it is, a version MUST NOT ship a requirement it has not
finished — before the tag, every requirement this manifest’s goals name has to
be built, which OPS-R54 states in full.
The subject is what a version carries. A manifest may lock part of a feature and
most of them do: 24 of the 25 here lock part of at least one, and 0.1.0 locks two
of B1’s fifteen. So the catalogue answers where it can — a feature that is
built or shipped has every requirement finished, one that is planned has none
— and where a feature is building the trackers, each searched repository’s
status.toml (OPS-R74), are asked about each locked requirement
on its own. A catalogue calling a feature
untouched while the tracker ticks its requirements is refused either way; the two
records have to agree.
execute-version refuses on it, and just no-stubs <version> asks the same
question from clones under checkouts/. Ask it early. The rule was enforced by nothing until
0.15.0 was already staged, and what stood in for it was somebody reading six
catalogue files by hand on release day — which is how 0.14.0 came to lock five
features that still said building an hour before it went out.
It also refuses a version touching a feature with a requirement no manifest in
the train locks at all, naming each one. A requirement no version locks is one no
release gate reaches, and the run already looking at the feature holding it is the
only one placed to notice; a tag is cut only where the gate prints no error and no
warning (OPS-R70), so the answer is to lock it in a version.
just no-stubs <version> names them.
Rules
- A goal MUST be an
Acceptedrequirement; aDraftorWithdrawnone is rejected at staging (OPS-R30). - Once
status = "staged", thegoalsset is frozen — changing it needs review and a maintainer-channel notice (OPS-R31). - The file, not CI history, answers “where is this version”: read
status. - The file, not the forge, answers “when did it ship”: read
released_on(OPS-R57). - And what went out before it: read
prerelease. A record there is an artefact somebody can install and a verdict somebody can read; it is not a release, and no rule about a released manifest applies to it. - And what it shipped as: read
released_aswhere there is one,versionotherwise. A patch records the line it closed rather than a manifest of its own, so the train stays serial and the record still names the tag. - A comment says why a goal is in the version — what it needs, what needs
it, why it ships now — and never how far it has got. Where a goal stands is
its repository’s
status.toml, which the gate reads; a comment saying it too is a second record nothing compares with the work.check_manifest_comments.pyrefuses a sentence that names a requirement and says it is built, met, done or partial, or where it landed (OPS-R79). The words are narrow on purpose: what a thing is built on or built against, and a sentence that names no requirement, are other uses and pass. - A comment opening
# <n> goalsis checked against thegoalslist beneath it byjust integrity, andjust countsrewrites it. It is a number a person types into a data file nothing mechanical reads, so a scope change under OPS-R31 leaves it wrong with no run going red; stating it is optional and stating it wrongly is not.
See TEMPLATE.toml to start one, and staging.md for the lifecycle these files move through.