# The scaffold's upgrade path — 2026-09-27 A design note from the engineering record. SPEC N42 is the row and D52 the decision. ## The question `m0 new` writes a project once, and nothing touches its files again. Taking a newer `m0` moved the CLI and the framework's source, and left everything the scaffold wrote as the older `m0` wrote it. Between `m0` 0.2.0 and 0.3.0, six of the ten files every template writes changed: `AGENTS.md`, `deploy/Dockerfile`, `deploy/README.md`, `deploy/fly.toml`, `.gitignore` and `pyproject.toml`. The Dockerfile is the one that bites. 0.3.0's installs `libsqlite3-0` in the runtime stage and makes `/app/data` for a volume; 0.2.0's does neither, so an application that upgrades and adopts `m0_sqlite` builds an image that cannot open its library. Nothing said so. The toolchain had a trap of its own. `m0` refuses any `mojo` but the one it was gated on and names the fix, `uv add --dev 'mojo==Y'`. With `max-core` installed that command cannot resolve: `max-core` 26.6.0 and `mojo` 1.1.0 each require `mojo-compiler==1.1.0` exactly (PyPI's metadata, read 2026-09-27), so moving `mojo` alone leaves two pins on two compilers. ## What was built **The doctor names what an upgrade would change.** `m0 doctor` compares the scaffold's own files with what the running `m0` writes for the project's name, and names those that differ. The scaffold's own files are every one `m0 new` writes except the README and `pyproject.toml`, which are the application's from its first edit. It reports and never fails. A difference is the application's own edit as often as an older `m0`'s file (`unotes` edited its Dockerfile, `.dockerignore` and `fly.toml` on its first deploy), and only the application can tell which. A file the application removed is not compared. The name comes from `pyproject.toml`, so a project moved to another directory still matches. The new `m0` is the only thing that knows what changed, and the doctor is the command an upgrade already runs, because the toolchain refusal sends you there. **The toolchain moves in one command.** With `max-core` installed, `mojo-gated`'s fix names both pins: `uv add --dev 'mojo==Y' 'max-core==Z'`. Without it, the sentence is unchanged. ## What was not built, and why There is no `m0 upgrade`. Rewriting a file safely needs a three-way merge: what the old `m0` wrote, what the application made of it, and what the new `m0` writes. The first is recorded nowhere, and recording it now would help only projects written from now on. A rewrite without it either discards the application's edits or refuses whenever there are any, which is most deploys. The retiring condition is in D52. No version stamp is written into the scaffold either. A stamp would make every upgrade report every file, whatever changed, which is the noise the content comparison avoids.