Contract: versioning & compatibility
Status: Accepted
Three things version independently and must stay compatible: the lemonfiber
binary, the stack it operates, and the manifest format between them.
Satisfies: E1-R1, E2-R9, E2-R11, F1-R9, F1-R12
The three versions
Section titled “The three versions”| Version | Owns | Scheme |
|---|---|---|
lemonfiber binary |
The tool | Semver |
stack_version |
The service set and forms | Semver |
schema_version |
The manifest format | Monotonic integer |
They are separate because they change for different reasons. Bumping Sonarr’s
pinned tag changes stack_version and nothing else. Adding a manifest field
changes schema_version. Fixing a TUI bug changes only the binary.
Why schema_version is an integer, not semver
Section titled “Why schema_version is an integer, not semver”Semver’s minor/patch distinction implies backwards-compatible change — and for a parsed format that distinction is unreliable in practice. A field addition is compatible only if every consumer ignores unknown fields, and a field becoming optional is compatible only in one direction.
A monotonic integer states the only thing that matters: can this parser read
this file? Yes or no. Each lemonfiber release declares the set it supports.
The compatibility check
Section titled “The compatibility check”flowchart TD load[Load manifest] --> sv{schema_version<br/>supported?} sv -->|No| refuse[Refuse — name both versions] sv -->|Yes| mcv{stack requires a<br/>newer lemonfiber?} mcv -->|Yes| refuse2[Refuse — name required version] mcv -->|No| validate[Validate contents] validate -->|violations| report[Report all, with locations] validate -->|clean| ok[Proceed]Three distinct refusals, three distinct messages. Collapsing them into “invalid manifest” would leave the operator guessing which of three unrelated problems they have.
Where skew is caught
Section titled “Where skew is caught”The embedded stack (ADR-0005) means the common case never reaches a user:
| Path | Caught |
|---|---|
| Embedded stack | Compile time — build.rs validates the submodule’s schema_version |
--stack-dir fork |
Load time, refused with both versions named |
| Manifest edited in place | Load time |
Turning version skew into a build failure is the strongest available mitigation for the main cost of the four-repo split. An incompatible pairing cannot ship.
Changing the schema
Section titled “Changing the schema”| Change | schema_version |
|---|---|
| Add an optional field | Unchanged |
| Add a field whose absence the reader must be able to trust | Increment |
| Add a required field | Increment |
| Remove or rename a field | Increment |
| Change a field’s type or meaning | Increment |
| Add a permitted enum value | Increment — older parsers reject unknown values |
| Add a service, profile or form | Unchanged — that’s stack_version |
Adding an enum value increments deliberately: a stricter parser rejecting an
unknown criticality is correct behaviour, and pretending otherwise produces a
failure that looks like corruption.
The absence row is subtler and was learned rather than designed. An optional field is usually compatible because a reader that ignores it behaves as it always did. It is not compatible when the reader draws a conclusion from the field being absent — an older manifest, which simply predates the field, is then indistinguishable from a newer one declaring the answer is “none”.
Before the first release candidate
Section titled “Before the first release candidate”None of the above binds yet.
schema_version protects readers, and until a release candidate ships there are
no readers to protect: the stack is embedded, both repositories move together,
and the only manifest any binary has ever seen is the one it was built against.
Incrementing now would buy nothing and cost a predecessor parser to maintain,
plus a compatibility window to test, for a generation nobody ran.
So until the first release candidate, the schema changes in place. Fields are
added, removed and given new meanings at schema_version = 1, and the pairing
stays honest because the build refuses a stack it cannot read
(ADR-0005) rather
than because a number moved.
From the first release candidate the table above binds, and the first breaking change after it increments.
This is written down because the alternative is doing it by accident. A required
field was already added at schema_version = 1 before this section existed —
correct in substance, undocumented in intent, and indistinguishable from an
oversight by anyone reading later.
Supported window
Section titled “Supported window”lemonfiber supports the current schema_version and one predecessor. That gives
one release cycle of overlap for anyone maintaining a fork, without carrying
parser variants indefinitely.
Dropping support is a breaking change for the binary and moves its major version.
Binary and configuration
Section titled “Binary and configuration”lemonfiber holds no irreversibly-migrating state, so downgrade is supported
(E2-R10) — unlike the *arr databases, where it isn’t
(E1).
Configuration written by a newer binary is refused rather than modified
(E2-R11). Silently downgrading a config file is how a downgrade-to-test becomes
an unrecoverable state.
The machine-readable output contract
Section titled “The machine-readable output contract”F1-R12 makes machine-readable output a stable interface, so it versions too:
{ "api_version": 1, "kind": "status", "data": { … } }Every payload carries api_version. Additive changes leave it alone; removing or
retyping a field increments it. Scripts can assert on it rather than
pattern-matching output shapes.
Requirements
Section titled “Requirements”| ID | Requirement |
|---|---|
| ARCH-R1 | The binary, stack content and manifest format MUST version independently. |
| ARCH-R2 | schema_version MUST be a monotonic integer, not semver. |
| ARCH-R3 | An unsupported schema_version MUST be refused with both the found and supported versions named. |
| ARCH-R4 | A stack declaring a min_cli_version above the running binary MUST be refused, naming the required version. |
| ARCH-R5 | Unsupported schema, insufficient binary version, and content violations MUST produce distinct messages. |
| ARCH-R6 | The embedded stack’s schema_version MUST be validated at build time. |
| ARCH-R7 | lemonfiber MUST support the current schema_version and exactly one predecessor. |
| ARCH-R8 | Adding a permitted enum value MUST increment schema_version. |
| ARCH-R9 | Machine-readable output MUST carry an api_version. |
| ARCH-R10 | Configuration written by a newer binary MUST be refused, never modified. |
| ARCH-R43 | Before the first release candidate schema_version MUST NOT increment; the schema changes in place, and the build-time refusal is what keeps a pairing honest. |
Related
Section titled “Related”- stack-manifest.md — the format being versioned
- ADR-0004 Four-repo split — the skew risk this mitigates
- ADR-0005 Embedded stack assets
- E1 · E2
This page lives in another repository Rendered from lemonfiber/spec at 1d10402, 2026-09-09. Read the source of this page