# Decisions > Standing decisions about the application layer, one row each, with where each is recorded and what would retire it. One row per standing decision about the application layer, with what would retire it. The design notes argue each at length and are kept as written; this page is the index of which decisions stand, so a session that reads nothing else meets the constraint before re-proposing a piece. Rows have a permanent id (`D7`). Ids are never renumbered or reused: a retired or superseded row keeps its id and its line, and a new row takes the next number. A decision id and a [SPEC](https://m0serve.dev/docs/spec.md) row id are different tables — SPEC's section D is graceful shutdown, so `D9` there is a capability and `D9` here is a decision — and prose says which. | status | means | |---|---| | `standing` | in force; not to be re-proposed until the retiring condition holds | | `superseded by Dn` | replaced by the row named | | `retired YYYY-MM-DD` | the retiring condition was met on that date | **recorded in** names the note under `docs/notes/` that records the decision, or a CLAUDE.md heading written `CLAUDE.md: `. **retired by** is the condition that would reopen it; `—` says there is none foreseeable. `poe check-docs` fails when a `recorded in` does not resolve, a retiring condition is empty, an id is repeated or out of order, a status is not one of the three, or a `superseded by` names an id that is not here (`check_decisions_ledger` in `scripts/check_docs.py`; its `--selftest` reverts each rule against this page and insists the checker catches it). What no check can tell is whether a decision is still right; that is the retiring condition's job, and the reason each row has one. ## The application layer Seeded from the three notes that built the layer. Server decisions join as each is next revisited, one row at a time; until then CLAUDE.md's "properties of the design, not defects to fix in passing" list is where they are recorded. | id | decision | recorded in | status | retired by | |---|---|---|---|---| | D1 | HTML helpers, not a safety type: `String` stays the currency, `attr` and `text` escape, `raw` says so by name, and nothing forces an application to migrate | [a-fragment-that-names-itself](https://m0serve.dev/notes/a-fragment-that-names-itself.md) | standing | a decision to accept the migration to an auto-escaping newtype across every app | | D2 | No template language and no template files: an application's markup is Mojo, compiled with its context struct | [a-fragment-that-names-itself](https://m0serve.dev/notes/a-fragment-that-names-itself.md) | standing | nothing foreseeable | | D3 | No middleware and no decorators: a capturing closure is not `thin`, so there is nothing to put back in the table, and guards are early returns of `Optional[HTTPResponse]` | [a-fragment-that-names-itself](https://m0serve.dev/notes/a-fragment-that-names-itself.md) | standing | a language change that makes a capturing function value storable | | D4 | Route params are positional: origins are not spellable as struct parameters on the pinned toolchain, so a borrowing `RouteParams` cannot be written and an owning one allocates per name per request | [views-the-mojo-way](https://m0serve.dev/notes/views-the-mojo-way.md) | standing | origins spellable as struct parameters | | D5 | Routes are patterns, not function values: `thin` values are not `==`-comparable, so the pattern is the `comptime` constant given to both `add` and `url_for` | [a-fragment-that-names-itself](https://m0serve.dev/notes/a-fragment-that-names-itself.md) | standing | comparable function values | | D6 | `hx-*` against htmx 2.0.4, provisionally; `fx-*` and htmx 4 are deferred, and what weighs when the pin moves is recorded with the decision | [a-fragment-that-names-itself](https://m0serve.dev/notes/a-fragment-that-names-itself.md) | retired 2026-09-19 | met: the pin moved to htmx 4.0.0 — `Htmx` takes the sixth verb and spells the same three per-element attributes, `page_or_fragment` takes `HX-Request-Type` at its word and keeps the htmx 2 rule for a request without it, and the notes app's DELETE carries its CSRF token as a header (SPEC N22, D38); `fx-*` is not revived, `Vocabulary` being open to an application's own (D34) ([the-layer-moves-to-htmx-4](https://m0serve.dev/notes/the-layer-moves-to-htmx-4.md)) | | D7 | The frontend vocabulary is a type parameter (`Fragment[Htmx]`, `Fragment[Datastar]`) and its conformances live inside `html.mojo`, because on Mojo 1.0 an application could not conform to a trait defined in a `.mojoc` package | [one-renderer-two-transports](https://m0serve.dev/notes/one-renderer-two-transports.md) | retired 2026-09-18 | met: `Vocabulary` is open to an application's own conformance — `Html.open_kind()` and a defaulted `verbs()` are the surface, the verb check moved into the layer (D34) — and `poe check-app-vocabulary` builds an htmx 4 vocabulary from a directory outside this repository, asserts `hx-swap="outerMorph"` and `hx-query` in both tiers and reads the output with the vendored `hxlint`; `Htmx` and `Datastar` stay in `html.mojo` as the two the layer ships, with both apps' wire byte-identical ([a-vocabulary-an-application-defines](https://m0serve.dev/notes/a-vocabulary-an-application-defines.md)) | | D8 | One swap mode: a non-default mode belongs on the response beside `page_or_fragment`, where both libraries take a header, never on `swap` | [one-renderer-two-transports](https://m0serve.dev/notes/one-renderer-two-transports.md) | standing | an application that appends a row rather than replacing its list | | D9 | `page_or_fragment` reads four headers — `Datastar-Request`, `HX-Request`, `HX-History-Restore-Request`, `HX-Boosted` — and every answer's `Vary` names all four | [one-renderer-two-transports](https://m0serve.dev/notes/one-renderer-two-transports.md) | standing | — | | D10 | A Datastar URL carrying `'`, `\`, CR or LF is refused, not encoded: `url_for` encodes them, and an application building a query from request data must too | [one-renderer-two-transports](https://m0serve.dev/notes/one-renderer-two-transports.md) | standing | a query builder on `url_for` | | D11 | The mount prefix is a value threaded through state (`Mount`, used once for registration and once for `url_for`); `PoolContext.prefix` comes from the pool's own lane table, the one the loop routes by | [one-renderer-two-transports](https://m0serve.dev/notes/one-renderer-two-transports.md) | standing | — | | D12 | The page shell is a `thin` function over a context, not a trait; `PageShell` stays in `fragment.mojo`, now as the target the flipped `check-mojoc-trait` compiles | [a-fragment-that-names-itself](https://m0serve.dev/notes/a-fragment-that-names-itself.md) | retired 2026-09-18 | met: `page_or_fragment[S: PageShell]` takes the conformance and calls `wrap` only on the document branch, `apps/fragment_notes` is on it with its wire byte-identical, and `check-mojoc-trait`'s `mismatch` arm passes an app's shell through the real API ([the-page-shell-becomes-a-trait](https://m0serve.dev/notes/the-page-shell-becomes-a-trait.md)) | | D13 | `form(req)` is `Optional`, compares the media type whole, and keeps a decoding loop of its own rather than sharing `URI.parse`'s last-wins one | [a-fragment-that-names-itself](https://m0serve.dev/notes/a-fragment-that-names-itself.md) | standing | — | | D14 | `ViewService` does not conform to `PoolHandler`; a mounted application is a three-line struct in its mount module (`m0serve_mount`, SPEC N14) holding a `Views` table and its per-thread state | [one-renderer-two-transports](https://m0serve.dev/notes/one-renderer-two-transports.md) | standing | the trait boundary lifting — it is a directory-name mismatch, fixed on the Mojo nightly (2026-09-15; [a-trait-and-a-directory-name](https://m0serve.dev/notes/a-trait-and-a-directory-name.md)) — and an app whose state is a per-thread SQLite `Connection` | | D15 | No sessions and no CSRF: `wyhash64` is not a MAC, so an HMAC-SHA256 with published test vectors comes first, and whatever is built emits through `ResponseCookieJar.add_raw` | [a-login-on-the-notes-app](https://m0serve.dev/notes/a-login-on-the-notes-app.md) | retired 2026-09-12 | an application with a login (SPEC N13), built: `m0_http.session` signs with `m0_core.hmac`, emits through `add_raw`, and `apps/fragment_notes` is the application | | D16 | No `multipart/form-data`: a boundary scanner, per-part headers, a spill-to-disk policy and a size story the whole-body cap does not answer | [a-fragment-that-names-itself](https://m0serve.dev/notes/a-fragment-that-names-itself.md) | standing | an application that uploads a file | | D17 | No `HX-*` setter helpers: `resp.headers["HX-Trigger"] = x` is the API | [a-fragment-that-names-itself](https://m0serve.dev/notes/a-fragment-that-names-itself.md) | standing | three apps writing the same setter | | D18 | No streaming from a Mojo mount: `MojoPool` refuses a streaming response, because a stream begun on a pool thread has no producer the loop drains | [mojo-handler-pool](https://m0serve.dev/notes/mojo-handler-pool.md) | superseded by D22 | an application that needs it (SPEC N11), settled by the probe: a `MojoPool` thread sending the hold frame a WSGI pool thread sends — it can, 2026-09-11 | | D19 | The expression tier allocates per element and has no `join`; the builder is for a renderer that cares | [one-renderer-two-transports](https://m0serve.dev/notes/one-renderer-two-transports.md) | standing | storable closures, or a `List[String]` form of `el` that an application asks for | | D20 | Datastar is pinned at 1.0.3; re-check the version before any vocabulary work | [one-renderer-two-transports](https://m0serve.dev/notes/one-renderer-two-transports.md) | standing | the next Datastar release with a protocol change | | D21 | A Datastar field's action sends the signal store, not the field, so a field is bound and a form arm reads what the store carries; confirmed in Chromium 2026-09-12 (`poe browser-datastar-form`: the draft field's action posted `{"draft":"buy milk"}` as JSON, the rename form posted `text=buy+oat+milk` urlencoded) | [a-datastar-form-end-to-end](https://m0serve.dev/notes/a-datastar-form-end-to-end.md) | standing | a Datastar release whose field actions send the field alone — the browser run is where it would show | | D22 | A Mojo mount streams only as an `M0-Hold`: a pool thread takes the two headers a Django view returns, sends the loop the `h` frame a WSGI pool thread sends, and the loop drains the stream from its own registries; a streaming response that is not a hold is still refused, because a pool thread has no producer the loop can drain — a request-scoped generator is the executor's shape | [hold-from-a-mojo-mount](https://m0serve.dev/notes/hold-from-a-mojo-mount.md) | standing | a Mojo view that must produce its own body after the head | | D23 | A hold grant is the server's own dotted format — version, key id, expiry, channel, session binding, HMAC-SHA256 tag over the rest — signed with a dedicated `M0_GRANT_KEY` both sides read from the environment, bound to the session cookie by default, verified in constant time, with revocation bounded by its TTL; not Django's own signer, and no server-side grant store | [grant-verified-holds](https://m0serve.dev/notes/grant-verified-holds.md) | standing | a hold the server can close from the application side, which would let the TTL grow and the binding relax | | D24 | No session store: the cookie is stateless, so logout expires it in the CLIENT and a copy of the value works until its own expiry — revocation is the TTL and the key ring, where dropping a key ends every session signed under it at once. The CSRF token is stateless for the same reason, a MAC over the session's own tag | [a-login-on-the-notes-app](https://m0serve.dev/notes/a-login-on-the-notes-app.md) | standing | an application that must end a session before its expiry — a stolen cookie, or "sign out everywhere" | | D25 | No password KDF and no user table: one identity, its secret in the environment, compared as SHA-256 digests so `constant_time_equal` has two fixed-length inputs. Not a password hash, and not meant to be — there is nothing at rest to steal | [a-login-on-the-notes-app](https://m0serve.dev/notes/a-login-on-the-notes-app.md) | standing | a second user, or a password stored anywhere | | D26 | No worker-thread helper: an application that needs a cadence off the loop spawns its own `ThreadSet` and publishes through the `BroadcastBus` with `skip_worker = -1`, as `--pg-listen`'s listener and `apps/sim_loop` both do. A `Server.add_worker_thread(body, period)` would have to choose the cadence policy, the shutdown bound and the publish shape on the application's behalf, and one application is not evidence of which choices are right. The retiring condition was met on 2026-09-16, when `apps/blobs` hand-rolled the same block slots (`BLK_STOP`, `BLK_FDS`, `BLK_NFDS`) with different choices ([a-world-the-page-cannot-hold](https://m0serve.dev/notes/a-world-the-page-cannot-hold.md)), and the Mojo host's `Producer` trait replaced the helper the same day: D27 records the choices it makes | [periodic-work-off-the-loop](https://m0serve.dev/notes/periodic-work-off-the-loop.md) | retired 2026-09-16 | a second application hand-rolling the same block slots | | D27 | The Mojo host makes a producer's three choices once, for every application: the CADENCE is what `step` returns, the time from this step's scheduled start to the next, fixed-rate and never caught up after an overrun, a pause being a poll interval with no frame; the SHUTDOWN BOUND is the drain's own 5 s, after which the process leaves without the producer and names it; the PUBLISH SHAPE is every worker's channel with `skip_worker = -1`, through a `Publisher` that hides the descriptors and counts each refusal, and the ID SPACE is the pre-fork shared word (`Publisher.next_id`, since 2026-09-17), the one `DatastarStream` numbers from under `enable_bus`, handed out rather than stamped inside `publish` because the id sits in the frame body and the framing is the application's (`id:` first for `format_sse_event`, `event:` first for Datastar) — a producer numbering from a counter of its own restarted at 1 when the supervisor respawned worker 0, and every stream held on a sibling went silent for the pre-crash uptime. A step that raises ends the producer, named in the log, while the server keeps serving. `sim_loop`'s fixed rate and `blobs`' pause-and-idle cadence are both one `step` | [the-mojo-host](https://m0serve.dev/notes/the-mojo-host.md) | standing | an application whose producer needs what `step` cannot say: catching up, a longer bound, or a subset of the workers | | D28 | The host lives in the fork (`lightbug_http/host.mojo`), beside `mojo_pool.mojo`, because an application must conform to `AppHandler` and `Producer` and, on Mojo 1.0, a conformance to a trait in a `.mojoc` built from a directory named other than its package got no witness table; that added fork -> `m0_http` imports of `config`, `multiworker`, `prefork`, `signal`, `threads` and `views` (for `ViewsApp`). Retired when the code moved: `mojo_pool.mojo` into `m0_http` (`src/`), and the host into a source-resolved package of its own, `m0_host`, because `src/` cannot hold it — D33 | [the-mojo-host](https://m0serve.dev/notes/the-mojo-host.md) | retired 2026-09-18 | `poe check-mojoc-trait` failing, or `build-apps` refusing `apps/pool_spike`'s `PoolHandler` conformance: the witness-table bug is back and both files return to the fork ([the-host-leaves-the-fork](https://m0serve.dev/notes/the-host-leaves-the-fork.md)) | | D29 | Host v1 is an environment-configured prefork server of inline loops: `M0_THREADS`, `M0_BLOCKING_THREADS` and `M0_SPAWN_WORKERS` are refused with 78 rather than ignored, as is an `M0_WORKERS` above what the application declares it can serve (`max_workers`, for state that lives in one process), and there are no CLI flags and no `--doctor`. That last clause outlived the supersession and was retired on 2026-09-19, when the developer product needed a binary whose doctor a CLI can delegate to: SPEC E30–E31, [flags-and-a-doctor-for-the-host](https://m0serve.dev/notes/flags-and-a-doctor-for-the-host.md), and D37 for what the command line still refuses to be | [the-mojo-host](https://m0serve.dev/notes/the-mojo-host.md) | superseded by D31 | an application that needs a pool lane, loops on threads (plan Phase 4), exec'd workers, or a flag its operator cannot set in the environment — the first was met on 2026-09-17 by the ramp test, which needs a Mojo application's compute views off the loop on both hosts | | D30 | A `make` that raises is a refusal, not a crash: the host prints the error under `host:` and exits 78 (`EX_CONFIG`), which the supervisor never respawns, for the handler's `make` in any worker and the producer's on worker 0 alike. The producer is BUILT on the spawning thread, inside `ProducerThread.start`, before the server listens — not on its own thread, as D27's first form had it — so the refusal precedes the listening banner by construction rather than by racing it; the thread takes the built producer as its first act and owns it from there. One worker's refusal ends its siblings (`WorkerSupervisor` forwards SIGTERM on an `EX_CONFIG` exit), because a host with no worker 0 and no producer is not the configuration that was written down. Measured before the fix: five crashes and exit 1 under `M0_WORKERS=2`, the banner then the trace at one worker | [the-mojo-host](https://m0serve.dev/notes/the-mojo-host.md) | standing | an application whose `make` fails transiently and wants a retry rather than a refusal, or a producer that must be built on the thread that runs it | | D31 | The host serves `M0_BLOCKING_THREADS=N` as one GIL-free pool lane per worker (SPEC E26): N `MojoPool` threads behind the loop, each building the application's handler again through `PoolLane[H]` — the host's `PoolHandler` adapter, so an application conforms to `AppHandler` once and `HostContext.thread` says which instance a `make` is building — with `before_request` and the streaming hooks the loop instance's and every `func` a pool thread's; the host waits for every thread to report its handler built before it serves, and a `make` that raises on a pool thread is the same 78 the loop's gets (D30 on the lane); a stream begun in `func` is refused 409 from a pool thread, per request, as on a Mojo mount, and a hold from a pool thread (`set_hold_notify`) is not offered. `M0_SPAWN_WORKERS` stays refused with 78 (`M0_THREADS` did until D35), and there were still no CLI flags and no `--doctor` (until 2026-09-19: SPEC E30–E31, D37). No pool by default: `M0_BLOCKING_THREADS=0` is unset, and an application with a producer alone does not want one. The shutdown bounds overlap since the same day: the loop stamps the producer's stop word as its drain begins (`run_event_loop`'s `stop_addr`), the pool is pilled after the loop returns (a pill read beside a queued job strands it) and joined within what the drain left of the bound, floored at `POOL_JOIN_FLOOR_NS`, and the producer within the rest of its own — measured 5.05 s to leave with a 4 s request in flight beside a 60 s step, against 8.54 s in sequence | [the-ramp-test](https://m0serve.dev/notes/the-ramp-test.md) | standing | an application that needs exec'd workers, a hold taken on a pool thread, or a flag its operator cannot set in the environment | | D32 | A route's placement is a property of the route, declared where it is registered (SPEC N19): `add_read`/`add_write` with `on_loop=True` keep a view on the loop with the loop instance's state, `add_loop` stays the stateless form, and the default under a pool is the lane. `m0serve` does NOT honour either on a Mojo mount: its loop handler is `WSGIHandler`, which holds no Mojo table, so a mounted table's loop routes are answered on a pool thread by `dispatch`, bytes identical, one round trip later, with that thread's state — recorded rather than built, because the divergence is latency under a full lane, not a stall, and the lane's other threads answer — measured by `smoke-ramp`'s full-lane arm as 196 ms for `m0serve`'s `/x/now` against 0 ms for the host's with both lane threads busy, and 1 ms or less on both with one. The alternative was rejected for now, not forever: teach `WSGIHandler.before_request` a `thin` function pointer plus an address from one extra `MojoMount` built on the loop thread (`index=-1`), at the cost of a second handler instance per loop | [the-ramp-test](https://m0serve.dev/notes/the-ramp-test.md) | standing | a mounted route that must answer while its lane is saturated, measured by `smoke-ramp` rather than argued | | D33 | Nothing in `m0-http/src/` may reach `lightbug_http/event_loop.mojo`, at any depth: that file imports `m0_http.log`, which resolves through `m0_http.mojoc` — the file `build-http` is writing while it compiles `src/` — so the build fails with `invalid magic bytes` from a clean checkout and every one after, and a function-local import does not help, a precompile parsing every body it reaches (both spellings measured). So the Mojo host, which calls `run_event_loop`, is the package `m0_host` (`packages/m0-http/m0_host/`), above the fork and `m0_http` and imported by neither, resolved from source with no `.mojoc` of its own (a directory beside a `.mojoc` of its name shadows it), and compiled whole by `poe check-host-package` | [the-host-leaves-the-fork](https://m0serve.dev/notes/the-host-leaves-the-fork.md) | standing | `event_loop.mojo` no longer importing `m0_http` — its access log given another home — which CLAUDE.md's fork section currently rules out; or an `m0_host.mojoc` an application asks for, which means renaming the directory | | D34 | A `Vocabulary` conformance is written without an underscore, and that is the whole contract, because nothing behind a `.mojoc` is private (an app reads `h._open_kind` and imports `_check_verb`, both measured): the element kind is the accessor `Html.open_kind()` rather than a fifth argument to `swap`, `Html` being handed over `mut` and so never opaque to a vocabulary; the verb check is the layer's (`_swap[V]`, before `V.swap` runs) against the vocabulary's own `verbs()`, defaulted to the five, so a conformance that checks nothing still refuses a typo and htmx 4's sixth verb is one line; `_check_action_url` stays Datastar's; and the two built-in conformances are written against that surface alone | [a-vocabulary-an-application-defines](https://m0serve.dev/notes/a-vocabulary-an-application-defines.md) | standing | a library whose spelling needs something the surface lacks — an ancestor of the open element, or a per-element mode — shown by an application that had to reach for an underscore | | D35 | The Mojo host serves `M0_THREADS=N` as N loops on N threads of one process, and prefork stays what the documentation reaches for first, because the two measure the same (SPEC E27–E29): threads over workers on `apps/ramp`, the loop route and the compute route at 16 and 256 connections, ran 0.99–1.00x throughput on macOS at four, 0.95–1.03x on Linux aarch64 at two and 0.98–0.99x on Linux x86-64 at two (a GitHub runner, AMD EPYC, measured 2026-09-18 in host Phase 5), the tail equal within its own noise, and summed RSS 0.78–0.80x, 0.63–0.68x and 0.56–0.59x. The x86 run disagreed in one place and it is kept, not averaged: at 256 connections on the loop route threads served the same requests on more CPU, 0.86–0.94x workers' throughput per core in every round, which is prefork's side of the decision. Threads buy memory, one address space and the absence of `fork`; they cost crash isolation — there is no supervisor, so a loop that dies takes the process (exit 1, named) — and neither happens unasked, the host's default being one loop. `HostContext.worker`/`workers` count loops and `threaded` names the kind, so an application is served by either mode unchanged; `AppHandler.max_threads()` defaults to `max_workers()`, so state that lives in one handler refuses loops as it refuses workers; the two variables together are refused; no loop serves until every loop's `make` has returned; fan-out across loops rides the bus and accepts are shared across threads through the same `SCM_RIGHTS` channels, measured as needed (30 of 32 connections on one loop of four without them) | [loops-on-threads](https://m0serve.dev/notes/loops-on-threads.md) | standing | a measurement where THREADS win by more than their own round-to-round spread — macOS, Linux aarch64 and Linux x86-64 are measured, and the one consistent difference (x86, 256 connections, per core) favours workers — or an in-process ring measured against the bus's socketpairs. The default for a deploy on one shared vCPU is D36's: one loop | | D36 | The blobs demo's deploy serves ONE loop -- no `M0_WORKERS`, no `M0_THREADS` -- because on the machine it is billed for, one shared vCPU at 256 MB, neither N-loop mode buys anything measurable and both cost memory. `bench_blobs_modes.py` held 25 to 400 viewers against the demo's own image pinned to ONE core (a CPU quota alone lets two loops run on two cores at once, and measured unpinned they looked faster for exactly that reason), on linux/arm64 (colima, Apple M4) and on x86-64 (a GitHub runner, AMD EPYC 7763): every mode used the same CPU (0.92–1.11x one loop's), delivered every frame, and fanned out with the same spread; at a full core `/now`'s tail differed by at most 2 ms and in no consistent direction (at 400 viewers arm64 favoured two loops, 6.4–6.8 ms against 8.3, and x86 one, 0.66 ms against 0.98–1.07); RSS was 1.11–1.25x one loop's for two threads and 1.79–2.29x for two workers. Throttled to a shared-cpu-1x's baseline share (6.25 %), every mode still delivered every frame on the same CPU, its tail set by the quota's period (23–50 ms p99 in every mode). What one loop gives up is a supervisor: a crash takes the process, and the machine's restart plays its part. The world lives in the producer on worker 0 under prefork too, so a crash there loses it in every mode | [the-demo-in-its-own-image](https://m0serve.dev/notes/the-demo-in-its-own-image.md) | standing | a deploy on more than one vCPU, where N loops run at once; or a delay viewers can see growing with their number under one loop -- the fan-out's own spread, not a `/now` probe's tail | | D37 | The Mojo host's command line is CLOSED and its doctor stops before the bind. Closed: the flags are the fields `AppConfig` carries, plus `--doctor` and `--help`; an unknown flag or any positional is exit 2, so an application defines no flags of its own and keeps its settings in its own variables, checked in its `main` before `serve` — which is also what makes them refuse identically under the doctor without a hook. The doctor does not render those settings, does not build the handler (a `make` opens the application's database to report on a port) and does not try the address; both fail at run time with a named line. A count that cannot be served is a refusal (78) whichever way it arrived, never a usage error for the flag and a refusal for the variable, where m0serve answers its `--workers 0` with 2: the host had the env-side refusal first, and one rule gets one answer. And the report carries a format number, not a release, the version having exactly two homes (docs/RELEASING.md) | [flags-and-a-doctor-for-the-host](https://m0serve.dev/notes/flags-and-a-doctor-for-the-host.md) | standing | a second application that needs its own settings in the report (then a hook on `AppHandler`), or one that cannot express a setting as a variable; a `make` cheap and pure enough across real applications that building it under the doctor is safe to default; or the `m0` wheel needing the framework version from inside the binary rather than from its own metadata | | D38 | No request-header helper in the layer: a vocabulary spells a swap, and an app that needs a header on one — the notes app's `X-CSRF-Token` on a DELETE, htmx 4 leaving a DELETE no body and sending its fields in the URL — writes the library's own attribute by hand (`hx-headers`, the one `hx-` that app types). The two libraries put a header in different places, htmx in an attribute beside the swap and Datastar in an option INSIDE the action expression, which `Vocabulary.swap` has no argument for; one application is not evidence of the shared shape | [the-layer-moves-to-htmx-4](https://m0serve.dev/notes/the-layer-moves-to-htmx-4.md) | standing | a second application hand-rolling a request header on a swap, or the scaffold's template needing one. NOT met by the scaffold (2026-09-20, D44): both templates are sessionless, so neither write carries a token and neither types `hx-headers`. The second data point is the soak application, which will want a login — and hand-rolling `fragment_notes`' session and CSRF glue a second time is the trigger for this helper and for an auth template together | | D39 | An application outside the repository gets the framework as SOURCE in the `m0` wheel, gated on ONE exact mojo, with no override. Source and not `.mojoc`: a `.mojoc` is locked to the compiler that wrote it, so shipping one forces lockstep and every toolchain bump breaks installed wheels; the fork and `m0_host` are source-resolved anyway (D33); and a build after a real edit measures the same either way (fragment_notes, a changed literal: 11.7–12.1 s against the `.mojoc` chain, 12.0–12.8 s all-source). The pair is a table, `gated_mojo` in `m0/_build_info.json`, read from the root pin at build time and compared by string equality to the `mojo` installed in m0's own environment; a list by format and a singleton by rule, since a second entry needs a second CI leg. No `Requires-Dist` on mojo — 220 MB downloaded before `m0 new` writes a file, and a mismatch read as a resolver's prose instead of a sentence naming the fix. No override, because the repository's grammar is refuse, never warn-and-run, and every gate ran on that one compiler | [the-m0-wheel](https://m0serve.dev/notes/the-m0-wheel.md) | standing | a compiler that keeps `.mojoc` stable across versions, or a registry story that beats a wheel; for the override alone, a user blocked between a mojo patch release and an m0 release; for the singleton, an m0 release that must span two pins, which is a redesign and not a patch | | D40 | The `m0` CLI is stdlib Python with a CLOSED command line and a closed set of exit codes (0, 1 the tool failed, 2 the command line, 78 a refusal), and it runs the `mojo` in its own `sys.prefix`, never `PATH`'s. Python, because the build toolchain is a PyPI wheel inside a venv whatever the CLI is written in, so it adds nothing to what building already needs, and the shipped application still has no interpreter. Closed, D37's reason: `m0 build` takes no `-o` because `bin/server` and `dist/` are the contract the doctor and the image rely on. Its own prefix, because the pair check is made about the distribution installed beside m0, and a global `mojo` earlier on `PATH` would be a different compiler passing a check made about another | [the-m0-wheel](https://m0serve.dev/notes/the-m0-wheel.md) | standing | a self-bootstrapping toolchain story without a venv, or a second application needing a build the fixed paths cannot express — written down as what it could not express before any flag is added | | D41 | No pixi or conda package. The official Mojo distribution path publishes COMPILED `.mojoc`/`.mojopkg` conda packages with an exact `mojo-compiler` pin — the lockstep D39 avoids — and would be a second toolchain for a uv-based repository with nothing pulling for it. The sources in the wheel are the sources a conda package would carry, so opening the door later costs a build recipe, not a redesign | [the-m0-wheel](https://m0serve.dev/notes/the-m0-wheel.md) | standing | a user asking for pixi, or conda becoming the way Mojo users arrive | | D42 | The CLI does not wrap deploy. It wraps what the repository owns — the build, the relocate-and-bundle recipe, the checks — and leaves `fly deploy` to a runbook, where `--remote-only` is load-bearing (a local build emulates amd64 and the binary dies under QEMU) and is better read than hidden behind a flag that would have to track someone else's CLI | [the-m0-wheel](https://m0serve.dev/notes/the-m0-wheel.md) | standing | a second deploy target whose steps the runbook cannot state in a page | | D43 | `m0` is versioned apart from the repository: `0.1.0` first, tags `m0-v*`, its own release workflow. The two products have different stability — m0serve's 1.x is a served contract with a production consumer, and the Mojo API broke inside 1.x minors twice in one week (D12's page shell, htmx 4) and will again — so one shared number means either m0serve majors that mean nothing to its users or an `m0` whose SemVer lies. `0.x` is "preview" in the form tools read. Nothing is lost: `_build_info.json` records the framework version and the commit the wheel was cut from | [the-m0-wheel](https://m0serve.dev/notes/the-m0-wheel.md) | standing | the application-layer soak landing: then `1.0.0`, and whether to rejoin the repository's number | | D44 | The scaffold is two SESSIONLESS templates behind a closed `m0 new --template`, which takes `views` or `live`, and there is no `--ui` and no `--live`: `views` is the htmx 4 list (`fragment_notes`' shape without its login), `live` a producer pushing full-state Datastar frames (`blobs`' shape without its kernel). A `--ui` of three values times `--live` is six shapes of which two are proven on the wire; a closed set says what exists and grows by a VALUE, never by a flag per feature, and moving `views` to Datastar is one `comptime` line and a script tag, which the scaffold's AGENTS.md says. Sessionless because a scaffold that refuses to start until a password variable is set is a bad first minute, and with no ambient authority there is no token to carry (D38 stands). Templates are real files that compile unsubstituted — the name only inside string literals, TOML and Markdown — so substitution is `str.replace` and there is no template engine (D2's reason, applied to the tool) | [the-scaffold](https://m0serve.dev/notes/the-scaffold.md) | standing | a third shape proven on the wire in `apps/` and asked for; or the soak application's login being the second hand-rolled one, which makes an auth template (and D38's helper) the next value | | D45 | The Mojo stack's documentation is six pages whose URLs are permanent from the day they land: `/mojo/`, `/mojo/quickstart/`, `/mojo/host/`, `/mojo/views/`, `/mojo/deploy/`, `/mojo/ramp/`. Every scaffolded project's `AGENTS.md` sends its reader to the site, agents cache links, and a moved page reads to them as a page that never existed — so a page may be rewritten, split by adding a seventh, or emptied into a pointer, and its URL still answers. The word "preview" is said once, on the index. The quickstart's SOURCE is `packaging/m0/QUICKSTART.md` and not under `docs/`, because CI executes it and `test.yml` ignores `docs/**` and root-level Markdown: there, a pull request that edits only the page still runs the page. Its image step is display-only — half the gate's runners have no docker — and N31's gate builds that image instead. The section rides in the existing `llms-full.txt` (about 5,000 words of 34,000, measured) and gets no file of its own | [the-mojo-stack-pages](https://m0serve.dev/notes/the-mojo-stack-pages.md) | standing | never for a URL that has been published; the placement under `packaging/` when `test.yml` stops ignoring the page's path some other way; the shared `llms-full.txt` when the section outgrows a third of it |