m0serve Realtime from a synchronous Python app

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.