The page shell becomes a trait — 2026-09-18
DECISIONS D12 made the document shell a thin function over a separate
context struct:
page_or_fragment(req, render_list(store), Site("notes"), wrap)
It was designed as a trait and went to that form because on Mojo 1.0 an
application's conformance to a trait it imported from a .mojoc was
accepted and its witness table never emitted. That was never the package
boundary: a package compiled from a directory named other than the package
recorded its traits under the DIRECTORY's name, and every package here
runs mojo precompile src -o <name>.mojoc
(a-trait-and-a-directory-name). Mojo
1.1.0 fixed it and the pin moved the same day
(the-pin-moves-to-1-1-0). D12's retiring
condition read "page_or_fragment taking a PageShell and the apps moved
onto it". This is the record of that round.
The probe
check-mojoc-trait's mismatch arm already conformed an app struct to
m0_http.fragment.PageShell and called a generic helper, wrap_with,
with it. What it did not prove is the shape this round ships: the
conformance crossing the .mojoc as an argument to the real API, with
wrap called only on the document branch. So that was built first, as an
application compiled against the built package.
| case | answer |
|---|---|
Site(PageShell) with a field, document branch |
<title>n</title><p>x</p> |
the same, fragment branch (HX-Request: true) |
<p>x</p> |
Bare(PageShell) with NO fields, document branch |
<html><p>x</p></html> |
| the same, fragment branch | <p>x</p> |
status=404 through the document branch |
404 |
The fieldless arm is the one that decided the round's open question.
Replace, not overload
The thin form shipped in four releases (v1.1.0 through v1.4.0), so
keeping both was the safe option and was considered. It was rejected, and
the fieldless probe is why: the only thing the function form buys is a
shell that carries no context, and such a shell is a struct with no
fields, which conforms trivially and costs nothing. Two forms would have
meant a module docstring that had to say when to use which, with no honest
answer — and this tree has already paid for one duplicated function twice
(split_data_lines, whose copy kept a fixed trap for a release).
A Mojo signature is outside the served contract CHANGELOG states
(m0serve's flags and environment, the M0-Hold/M0-Channel headers,
m0pub.publish()), so this is not a SemVer break; it is a documented API
change and goes under Changed. The one call shape that changes is an
application's, and the migration is mechanical: the context struct
conforms to PageShell, the shell function becomes its wrap method, and
the argument goes.
What did not change
smoke-fragment-notes is the wire gate, and the point of the round was
that it should not move. It passes, which is the gate; what the gate does
not do is compare bytes, so that was done separately. Twenty-one request
shapes were driven against the application binary built at 60b205a and
again against the refactored one — every one of the app's NINE
page_or_fragment call sites, in both representations where it has two,
plus both page-forcing headers, the styled 404, the login page, the 401
fragment, OPTIONS and a bad method — and their bodies and headers, 42
files, compared byte for byte. Only the session cookie and the CSRF token
were normalised, both being derived from the issue time. All 42 are
identical.
Covering the writes took a second pass: reads alone reach five of the nine
call sites, and create, delete, and login and logout answered as
fragments are the four that need a POST with a valid token to reach at
all. A comparison that had stopped at the reads would have been a true
statement about a smaller thing than it sounded like.
The generic body is run, not merely compiled
Mojo checks a generic's body where it is instantiated, so a
page_or_fragment[S: PageShell] that test_fragment.mojo only compiles
would look tested and be nothing of the kind. The document branch was
therefore sabotaged to skip wrap altogether. Three of the nine tests
fail — test_page_or_fragment_wraps_only_without_the_header,
test_a_history_restore_is_a_page and test_a_boosted_request_is_a_page
— each naming the missing shell in the body it got. The branch is
exercised.
check-mojoc-trait keeps four arms
wrap_with existed only so the probe had a generic to instantiate.
Once page_or_fragment is that generic, it has no reason to exist and is
gone; the mismatch arm calls the real API instead. The arm is NOT
deleted — its failure message names D7 and D28 as well as D12, and it is
the tree's only standing evidence that the 1.1.0 fix is still there. It
was sabotaged once by hand after the re-point (the conformance's method
misnamed) and reported REGRESSED, exit 1, naming what it costs.
What this leaves
D7 is the last of the three moves the pin unblocked: Vocabulary opened
to an application-defined conformance, with a gate that an app outside
this repo can emit a third library's attributes. PageShell is now
exported from m0_http, which is what an application-defined Vocabulary
would need too.