The Version Check That Ate Your Folders
A migration is supposed to move your furniture into the new house. This one set the old house on fire and handed you a brochure for the model home. The code that runs when you upgrade is the code that decides whether your settings survive the upgrade. On one fractal app, on one version bump, that code looked at every directory the user had configured and replaced all of it with nothing.
Operating Conditions
The program is a desktop fractal renderer. It keeps a saved-settings store the way most desktop apps do, a flat bag of keys and values that persists between runs. Under a "Paths" group it kept the directories the user had picked in Preferences. Where maps go. Where images go. Where meshes go. Where the background lives. The user sets these once, deliberately, because they have a folder layout they care about, and then they forget them, because that is the whole point of a preference. You configure it and it stays configured. That is the contract.
An upgrade arrives. Version number ticks up. The schema the program ships with has changed, so some defaults need to be refreshed so the new version behaves correctly. Routine. The kind of housekeeping that runs on every launch after every update and that nobody watches, because it is supposed to be boring.
It was not boring. A real user, handle astiglic, updated and reported "garbled folder utilities." The paths were gone. Not corrupted. Gone. Reset to defaults, every one, silently, on the way in.
Root Cause
The upgrade path called restoreDefaults() unconditionally on any version bump. Issue 510. Commit 101b2155.
That function did exactly what it says. It restored defaults. All of them. Including the entire Paths group, which was never a default in the meaningful sense. Those values were not factory settings the user had failed to change. They were the user's own config, sitting in the same store, wearing the same data type, indistinguishable to a function that only knows how to flatten everything back to the shipped state.
The intent was defensible on its face. "Reset preferences on upgrade so new schema defaults apply." There are settings where that is correct. A new default render quality, a new internal tuning constant, a key whose meaning changed between versions. For those, resetting is the migration. Wipe the stale value, let the new default take over, fine.
But the code drew no line between two categories that live in the same bag and must never be treated the same:
- Defaults the program ships with. Safe to reset. The program owns them.
- Config the user set on purpose. Never safe to silently destroy. The user owns it.
restoreDefaults() could not see the difference because nobody had taught it there was one. It treated a directory the user typed into a Preferences dialog with the exact same contempt as an internal constant. Same store, same call, same blast radius. The version check was supposed to migrate settings forward. Instead it used the upgrade as an excuse to clear the table, and the user's directories were sitting on the table.
Failure Modes
This is the shape of the thing, and it is worth naming because it repeats.
The reset, recovery, and migration paths are the most dangerous code in the program and the least tested. They only run on the unhappy path. Nobody exercises them in the demo. You do not upgrade across a version boundary in your happy-path test. You start clean, you click through the feature, it works, you ship. The migration code runs for the first time in anger on a real machine that has real state on it, weeks later, on someone else's directories. The first honest test of the upgrade path is production.
So the AI writes the forward happy path. The feature renders, the new default loads, the screenshot looks right. And it treats the version bump as a natural place to "clean up," because cleanup reads as responsible. Tidy. Good hygiene. Except destroy and tidy are the same gesture from the inside of restoreDefaults(), and the only thing distinguishing them is knowledge the code did not have: which of these values did a human choose on purpose.
"Reset on upgrade" is a destructive operation wearing the costume of routine housekeeping. It sounds like sweeping the floor. It is shredding the filing cabinet. The verb hides the blast radius, and the blast radius is the user's work.
I/O Protocols
The rule a migration has to obey is narrow and it is not optional.
A migration moves user data. It never deletes it. If you can carry a value forward, you carry it. If the value's format changed, you transform it and write the new shape. If you genuinely cannot migrate a value, you have two honest options and a forbidden one. You preserve it untouched, or you ask the user what to do. You do not silently write a default on top of it. Silent destruction of owned config is never the correct branch, no matter how stale you think the value is, because "stale" is your judgment about data that is not yours.
The line you have to draw in code, every time, before you write a single reset:
Which of these keys does the program own, and which did the user set on purpose? Reset the first group. Touch the second only to move it forward. If a value is in the second group and you cannot move it, leave it where it sits.
And the standing order for anything with restore, reset, clear, default, or wipe in its name:
A destructive operation gets more scrutiny than a feature, not less. A feature that breaks annoys the user. A reset that fires on the wrong group erases work the user cannot get back. The cost is asymmetric, so the review has to be asymmetric too. Read the blast radius before you read the logic.
The AI will not give you this for free. It scrutinizes the visible feature, because that is what the screenshot grades. The reset path produces no screenshot. It produces a quiet line that runs once, on a stranger's machine, over data the model never saw and has no reason to respect.
System Status
The fix is small. The version check has to distinguish the group it owns from the group the user owns, and only reset the first. One conditional, drawn at the right place. The damage was large and, for astiglic, unrecoverable. There is no undo for a path that was overwritten with a default. The old value is not in a trash can. It was never copied anywhere. It is just gone.
So when you let a machine write your upgrade path, do not read it the way you read a feature. Find every call that resets, restores, or clears. For each one, ask the only question that matters: does this touch anything the user set on purpose. If the answer is yes, or if the code cannot tell, that is not housekeeping. That is a delete with a polite name, and it deserves the same fear you would give any other delete that runs on data you cannot replace.
Where have you watched a "cleanup" step quietly destroy user-owned state because the code could not tell the difference between a default and a decision? Tell me the mechanism, not just the outage. The mechanism is the part that ships again next release.
Further Reading
- It Said Deployed: a delete that said "Deleted" and deleted nothing. The other half of trusting the verb over the state.
- The Reward Was the Bug: why the model polishes the visible feature and never the quiet path that only runs on a stranger's machine.




No comments yet