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 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: <heading>.
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 | 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 | 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 | 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 | 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 | 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 | 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) |
| 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 | 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) |
| 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 | 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 | 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 | 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 | 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 | 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) |
| 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 | 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 | 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) — 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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), and the Mojo host's Producer trait replaced the helper the same day: D27 records the choices it makes |
periodic-work-off-the-loop | 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 | 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 | 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) |
| 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, and D37 for what the command line still refuses to be |
the-mojo-host | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 |