docs(website): client extension slots, and the client half is one slice (phase 3, slice 2) #137

Merged
whitlocktech merged 2 commits from docs/module-client-slots into main 2026-08-11 21:54:09 +00:00
Member

Docs for website#138. Stacked on docs#136 — merge that one first and this diff reduces to its own two commits.

The slice table changes shape

Surveying the client half turned up two things, and one org-lead decision that makes them the same piece of work.

The client half does not divide into public and admin after all. Routes slice by area, but lib/useShardFeed.js and lib/shardEvents.js are imported by eight public pages and three admin views — and a shared leaf moves with its last consumer, the same rule the server half taught. Of the three ways out (the module vendors a copy for one slice; public keeps only the four pages that never touch the live feed; the two slices become one), the first two trade a real cost for a boundary that lasts one review. The client half is one slice.

Two separately-tabled items fold into it as a consequence, both improvements:

  • The nav rows keep their feature provider. The nine UO rows in the public header carry feature gates resolved by useShardFlags under namespace uo. Since resolution is by the registering module, rows that moved without their provider would resolve against a namespace nothing answers for — and everything fails open, so nine rows an operator may have disabled or gated to staff would advertise themselves again for the length of a slice. useShardFeatures.js already says as much in its own closing comment.
  • VendorSales was tabled with the public pages and has no public consumer at all. Its three are AdminCharacters, PlayerCharacters and core's own UserDetail.

Which is the next finding

UserDetail.jsx renders a core header, core's SecurityAdmin, then six UO sections. The server half already had somewhere to put that — Phase 2 PR 4 declared admin.users.detail and moved the six /admin/users/:id/shard/* routes behind it. The client never got the twin. SiteFooter's link to a URL the extraction deletes is the same gap one component over.

So slice 2 is core-only and adds rather than moves: client extension slots (new API §3.7), the site.footer.status and admin.users.detail slots, core filling both itself. MODULE_API_VERSION1.2.0, both halves.

Two decisions, settled with the org lead:

  • A slot is named for a PLACE, never for a meaning. Core supplies the position and the styling; the label, the target, the data and whether it renders at all are the module's. Typing a slot by its content would put game semantics back into core.
  • This slice inverts the merge order, once — a chunk cannot call registerExtension before the function exists. Harmless because the slice only adds, and core fills both slots with its own components under owner id core.

What the browser changed about the contract

The contract as first written had core branch on hasExtension to decide about its own decoration around a slot. That is right when nothing is installed and wrong when something is installed and fails: the slot is filled, so the separator renders, and the component then throws into the boundary and leaves the separator behind on its own. Core decorates through <Slot wrap> now, inside the boundary, and hasExtension is gone rather than kept as a trap for the next caller.

Every unit test passed both before and after — the §7.7 browser smoke is what saw it. Second time that smoke has caught something no test in either repo can.

Also recorded, neither a defect: core's own fill occupies a slot, so a module cannot fill either one until the client half deletes core's (worth stating — a module written against 1.2.0 today cannot use them); and core's UO sections on the user-detail page now fail to load, which is slice 1 removing the routes rather than anything this slice did.

AI disclosure

Written with Claude Code.

Co-Authored-By: Claude noreply@anthropic.com

Docs for `website#138`. **Stacked on docs#136** — merge that one first and this diff reduces to its own two commits. ## The slice table changes shape Surveying the client half turned up two things, and one org-lead decision that makes them the same piece of work. **The client half does not divide into public and admin after all.** Routes slice by area, but `lib/useShardFeed.js` and `lib/shardEvents.js` are imported by eight public pages *and* three admin views — and a shared leaf moves with its last consumer, the same rule the server half taught. Of the three ways out (the module vendors a copy for one slice; public keeps only the four pages that never touch the live feed; the two slices become one), the first two trade a real cost for a boundary that lasts one review. **The client half is one slice.** Two separately-tabled items fold into it as a consequence, both improvements: - **The nav rows keep their feature provider.** The nine UO rows in the public header carry `feature` gates resolved by `useShardFlags` under namespace `uo`. Since resolution is by the *registering* module, rows that moved without their provider would resolve against a namespace nothing answers for — and everything fails open, so nine rows an operator may have disabled or gated to staff would advertise themselves again for the length of a slice. `useShardFeatures.js` already says as much in its own closing comment. - **`VendorSales` was tabled with the public pages and has no public consumer at all.** Its three are `AdminCharacters`, `PlayerCharacters` and core's own `UserDetail`. ## Which is the next finding `UserDetail.jsx` renders a core header, core's `SecurityAdmin`, then six UO sections. The server half already had somewhere to put that — Phase 2 PR 4 declared `admin.users.detail` and moved the six `/admin/users/:id/shard/*` routes behind it. **The client never got the twin.** `SiteFooter`'s link to a URL the extraction deletes is the same gap one component over. So **slice 2 is core-only and adds rather than moves**: client extension slots (new **API §3.7**), the `site.footer.status` and `admin.users.detail` slots, core filling both itself. `MODULE_API_VERSION` → **1.2.0**, both halves. Two decisions, settled with the org lead: - **A slot is named for a PLACE, never for a meaning.** Core supplies the position and the styling; the label, the target, the data and whether it renders at all are the module's. Typing a slot by its content would put game semantics back into core. - **This slice inverts the merge order, once** — a chunk cannot call `registerExtension` before the function exists. Harmless because the slice only adds, and core fills both slots with its own components under owner id `core`. ## What the browser changed about the contract The contract as first written had core branch on `hasExtension` to decide about its own decoration around a slot. That is right when nothing is installed and **wrong when something is installed and fails**: the slot *is* filled, so the separator renders, and the component then throws into the boundary and leaves the separator behind on its own. Core decorates through `<Slot wrap>` now, inside the boundary, and `hasExtension` is gone rather than kept as a trap for the next caller. **Every unit test passed both before and after** — the §7.7 browser smoke is what saw it. Second time that smoke has caught something no test in either repo can. Also recorded, neither a defect: core's own fill occupies a slot, so a module cannot fill either one until the client half deletes core's (worth stating — a module written against 1.2.0 today cannot use them); and core's UO sections on the user-detail page now fail to load, which is slice 1 removing the routes rather than anything this slice did. ## AI disclosure Written with Claude Code. Co-Authored-By: Claude <noreply@anthropic.com>
wtclaude added 4 commits 2026-08-11 21:47:27 +00:00
Found before writing any of it. §2.7.1 split the server extraction five ways by
feature; it does not divide, because two contract rules compose.

A mount prefix is claimed whole -- ownedByCore probes the live tier router and
registerRoutes validates single-segment prefixes -- and /admin/shard is one
386-line router carrying 25 routes across atlas, clilocs, shard-ops, visibility,
market and account links. Meanwhile a model cannot be shared across the boundary
(§5.1), so it moves with the last route that consumes it. Take the closure and
every prefix is in it: /public/atlas holds shardAtlas with /admin/shard, which
holds shardState/shardEvents/shardMarket with /public/shard, which holds
uoLinkConfig with /admin/uo-link, and shardClilocs/shardLinks with /player/shard.

Landing any one of the old slices alone would either strand core importing
modules/uo/ -- what acceptance criterion 2 forbids -- or delete routes core is
still serving.

Giving the admin routes their own prefixes would divide it and is rejected: it
changes API URLs, which §1.2 promises not to do, and not hypothetically. The
shipped Android app calls POST /api/v1/admin/shard/kick, /ban, /unban,
/broadcast and the three /pages routes.

So the table is now six slices: 0 the skeleton (done), 1 the whole server half
as ONE PR per repo structured as five commits along the old slice lines, 2-4 the
client halves, 5 close the phase. The client half is unaffected and still slices
cleanly -- the registry takes routes per area, with no prefix atomicity and no
shared models, the same asymmetry that let the two halves be separated at all.

Co-Authored-By: Claude <noreply@anthropic.com>
The whole server half is out: 40 files, ~9,674 lines, 27 of 68 tables. The
acceptance criterion held exactly -- core's manifest goes 228 to 158 public
routes and the 70 that left reappear byte-identical once the module loads, with
routes.guards identical across all 228.

The contract grew to 1.1.0: ctx.activity.log, ctx.users.getById,
ctx.site.baseUrl, ctx.middleware.rateLimit + accountChangeLimiter, and a fourth
registry, registerPostHook. Each is documented with why it could not be
vendored, because that reasoning is the useful part -- an admin action a module
performs belongs in core's ONE audit log, a second rate-limit store is a limit
enforced by two counters, and core's CMS was calling a UO file directly.

§2.7.1 gains the slice record: the core.js port mechanism and its consequence
(require order is load-bearing), the vendoring line (pure leaf helpers may be
copied, security controls may not), the two core defects the extraction exposed,
the one deliberate behaviour change, and the one test that looked like it should
move and should not.

Co-Authored-By: Claude <noreply@anthropic.com>
Two findings from surveying the client half, and one org-lead decision that
turns them into the same piece of work.

The client half does not divide into public and admin after all. Routes slice by
area, but useShardFeed and shardEvents are imported by eight public pages AND
three admin views, and a shared leaf moves with its last consumer -- the same
rule the server half taught. Of the three ways out, two trade a real cost for a
boundary that lasts one review, so the client half is one slice. The nav rows
keep their feature provider as a consequence (rows without it fail open, so
disabled surfaces would advertise themselves again), and VendorSales turns out
to have no public consumer at all.

Its three consumers include core's own UserDetail, which renders six UO sections
with nowhere to put them: the server got declareSlot/registerExtension in phase
2 PR 4 and the client never got the twin. SiteFooter's link to a URL the
extraction deletes is the same gap one component over.

So slice 2 is core-only and adds rather than moves: client extension slots
(API 3.7), the site.footer.status and admin.users.detail slots, core filling
both itself. MODULE_API_VERSION goes to 1.2.0.

A slot is named for a PLACE, never for a meaning -- core supplies the position
and the styling, the module owns the label, the target, the data and whether it
renders at all. Typing a slot by its content would put game semantics back into
core, which is what this phase removes.

This is also the one slice where core merges before module-uo, because a chunk
cannot call registerExtension before the function exists. Harmless here: the
slice only adds, and core fills both slots with its own components under owner
id `core`, so the page is unchanged and the mechanism is proved before anything
moves.

Co-Authored-By: Claude <noreply@anthropic.com>
The contract as written had core branch on hasExtension to decide about its own
decoration around a slot. That is right when nothing is installed and wrong when
something is installed and fails: the slot IS filled, so the separator renders,
and the component then throws into the boundary and leaves the separator behind
on its own. Core decorates through <Slot wrap> now, inside the boundary, and
hasExtension is gone rather than kept as a trap for the next caller. Every unit
test passed both before and after -- the 7.7 browser smoke is what saw it.

Also recorded, neither a defect: core's own fill occupies a slot, so a module
cannot fill either one until the client half deletes core's (worth stating,
because a module written against 1.2.0 today cannot use them); and core's UO
sections on the user-detail page now fail to load, which is slice 1 removing the
routes rather than anything this slice did.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit fc05412070 into main 2026-08-11 21:54:09 +00:00
whitlocktech deleted branch docs/module-client-slots 2026-08-11 21:54:10 +00:00
Sign in to join this conversation.
No description provided.