Config drift detection & seed policy
Status: Accepted · Audience: Operator · Area: C — Trust & correctness
Purpose
Section titled “Purpose”Resolve the direct conflict between two things the product promises.
P6 says configuration should be reproducible: delete it, run seed, get a working stack back. F1 says the operator can customise anything.
These fight. Seeding is idempotent — it re-asserts lemonfiber’s view of configuration. So an operator who spends an evening tuning Sonarr’s quality profiles, then runs seed for an unrelated reason, silently loses that work.
That is a trust-destroying outcome, and it arises from two features that are each individually correct.
Behaviour
Section titled “Behaviour”lemonfiber records what it set
Section titled “lemonfiber records what it set”Every value written into a service is recorded — the field, the value, and when. That record is what makes drift detectable: without it, lemonfiber cannot distinguish a value the operator changed from one it set itself.
Three-way comparison
Section titled “Three-way comparison”Like a merge, drift detection compares three states:
| Source | Meaning |
|---|---|
| Expected | What lemonfiber last wrote |
| Actual | What the service currently holds |
| Desired | What lemonfiber would write now |
| Expected | Actual | Desired | Interpretation | Action |
|---|---|---|---|---|
| A | A | A | Unchanged | Nothing |
| A | A | B | lemonfiber’s intent changed | Apply B |
| A | B | A | Operator changed it | Preserve B |
| A | B | C | Both changed | Conflict — ask |
The third row is the whole point: a value differing from what lemonfiber wrote, where lemonfiber’s intent is unchanged, is an operator edit and must survive.
Operator edits win by default
Section titled “Operator edits win by default”Seeding never silently reverts a manual change. When lemonfiber would write something different from what it finds, it reports rather than overwrites.
Silent reversion is the worst possible behaviour — the operator’s work vanishes with no error, and they cannot tell whether they imagined making the change.
Conflicts are presented, not resolved
Section titled “Conflicts are presented, not resolved”Where both sides changed, the operator is shown both values and chooses. lemonfiber does not guess. Options are keep-mine, take-lemonfiber’s, and for structured values, merge where unambiguous.
Edits can be adopted
Section titled “Edits can be adopted”An operator happy with their change can promote it to lemonfiber’s expected state, so it stops reporting as drift and is preserved across future seeds and restores. This is how customisation becomes durable rather than perpetually flagged.
Drift is reported, not alarming
Section titled “Drift is reported, not alarming”Drift is normal and often intentional. It surfaces as information, not a failure — warning severity only when it breaks something, such as a root folder edited to a nonexistent path.
Materialised stack files are covered too
Section titled “Materialised stack files are covered too”The same logic applies to the compose file and stack configuration lemonfiber writes to disk. Local modifications are detected by content hash and are never silently overwritten on upgrade (ADR-0005); the operator is shown a diff.
States
Section titled “States”Per managed value:
| State | Meaning |
|---|---|
in-sync |
Actual matches expected |
drifted |
Actual differs; lemonfiber’s intent unchanged. Operator edit — preserved. |
stale |
Actual matches expected, but lemonfiber’s intent changed. Nobody’s edit is at stake, so it can be brought up to date on request (C3). |
conflicted |
Both changed. Requires a decision. |
adopted |
An operator edit promoted to expected state |
unmanaged |
Never written by lemonfiber; outside its scope entirely |
Edge cases
Section titled “Edge cases”| Situation | Behaviour |
|---|---|
| Service normalises a value on write | Compare semantically, not textually, or the value reports as permanently drifted. |
| Service regenerates its own API key | Not drift — detect and re-propagate (A7). |
| Operator edits a value lemonfiber must control for correctness | Report as conflicted with the consequence stated. Still don’t overwrite silently. |
| Drifted value breaks the stack | Warning severity with the breakage named, and remediation offered. |
| First seed after adopting an existing setup | Everything is unmanaged. Adopt what’s found as expected rather than reporting mass drift. |
| Service upgrade changes a schema | Detect the version change and re-baseline rather than reporting every field as drifted. |
| Value changed by another tool | Indistinguishable from an operator edit. Treat identically — preserve. |
| Operator wants a full reset to lemonfiber’s state | Supported as an explicit, confirmed action naming what will be lost. |
| Drift in a value containing a secret | Report that it drifted without displaying either value. |
| Very many drifted values | Summarise by service with detail on request. |
| Expected-state record lost | Report that drift cannot be assessed; offer to re-baseline from current state. |
| Operator edits the materialised compose file | Detect by hash, never overwrite on upgrade, show a diff. |
Acceptance criteria
Section titled “Acceptance criteria”| ID | Requirement |
|---|---|
| C9-R1 | lemonfiber MUST record every value it writes into a service, forming an expected-state baseline. |
| C9-R2 | Drift detection MUST compare expected, actual and desired state. |
| C9-R3 | Seeding MUST NOT silently overwrite a value that differs from the expected baseline. |
| C9-R4 | A value differing from expected, where lemonfiber’s intent is unchanged, MUST be preserved. |
| C9-R5 | Where both actual and desired have changed, lemonfiber MUST present the conflict and MUST NOT resolve it automatically. |
| C9-R6 | Operator edits MUST be promotable to expected state, surviving future seeds and restores. |
| C9-R7 | Drift MUST be reported informationally, escalating to warning only when it breaks functionality. |
| C9-R8 | Comparison MUST be semantic where a service normalises values on write. |
| C9-R9 | Adopting an existing setup MUST baseline from what is found rather than reporting mass drift. |
| C9-R10 | A service schema change MUST trigger re-baselining rather than mass drift reporting. |
| C9-R11 | Drift in a secret MUST be reported without displaying either value. |
| C9-R12 | A full reset to lemonfiber’s state MUST be available as an explicit, confirmed action naming what will be lost. |
| C9-R13 | Locally modified materialised stack files MUST be detected by content and MUST NOT be overwritten on upgrade without a diff and confirmation. |
| C9-R14 | Loss of the expected-state record MUST be reported, with re-baselining offered. |
| C9-R15 | A value the service still holds as lemonfiber wrote it, where lemonfiber’s intent has since moved on, MUST be bringable up to date on request rather than only reported. |
Related
Section titled “Related”- D1 Service auto-wiring — what writes the baseline
- F1 Customisation — the promise this protects
- A5 Migration — adopting an existing configuration
- E3 Backup & restore · C3 Auto-remediation
This page lives in another repository Rendered from lemonfiber/spec at 1d10402, 2026-09-09. Read the source of this page