ALTITUDE · COMPANIONS PLATES READ: census-bricks.json · census-couplings.json SEE ALSO 3 4 

FIT VERDICTexploded, then seated: every coupling is a published stud, on core or on the brick below — and the server takes none of core’s

THE ALTITUDE ATLAS — DRAWING SETSHEET 2 / 14

the BRICK ASSEMBLY

ALTITUDE 2 — one baseplate, ten bricks, 89 authored files · an exploded LEGO assembly with every coupling named to the API call that makes it, brick and stud faces drawn opaque so nothing reads through a mass in front of it · REV J 2026-10-24: ui-router-registry takes schedule row 11, a compile-time part beside the lint plugin — types only, no stud, not drawn · source counted at origin/main @ 3f55e419 (2026-10-31)

SCALE — plan = whole studs (1 stud per 150 sloc, rounded up to the next standard brick shape) · height = one course per 3 authored files · the baseplate is not massed PARTS — 10 BRICKS, 2 PLATES 1 lit-ui-router x1 · 2×4 · 9 courses 2 navigation-location-plugin x1 · 1×2 · 1 course 3 lit-ui-router-mobx x1 · 1×1 · 2 courses 4 ui-router-server x1 · 2×4 · 7 courses 5 lit-ui-router-effect x1 · 1×2 · 2 courses 6 lit-ui-router-ssr x1 · 2×4 · 6 courses 7 lit-ui-router-signals x1 · 1×2 · 2 courses 8 ui-router-effect x1 · 1×1 · 1 course 9 ui-router-signals x1 · 1×1 · 2 courses 10 ui-router-mobx x1 · 1×1 · 1 course SPARE PARTS — STUD A, NOT IN THIS SET @uirouter/visualizer 7.2.1 @uirouter/sticky-states 1.5.1 @uirouter/dsr 1.2.0 @uirouter/rx 1.0.0 all four queue on stud A — sheet 4 3 lit-ui-router-mobx 1.4.0 5f · 144 sloc · 1×1 · 2 courses SEATS ON BRICK 1 — F · seekRouter() AND PEERS BRICK 10 — K · RouterStore.for() 7 lit-ui-router-signals 0.3.0 4f · 170 sloc · 1×2 · 2 courses SEATS ON BRICK 1 — F · seekRouter() AND PEERS BRICK 9 — J · RouterSignals.for() 5 lit-ui-router-effect 0.6.0 4f · 285 sloc · 1×2 · 2 courses SEATS ON BRICK 1 — F · seekRouter() AND PEERS BRICK 8 — I · routeRef() 1 lit-ui-router 1.19.0 25f · 3,262 sloc · 2×4 · 9 courses STUDS A · B · D extends UIRouter · carries F · G 2 ui-router-navigation- location-plugin 1.2.0 2f · 214 sloc · 1×2 · 1 course STUD A — the LOCATION SEAT a swap, never an addition 8 ui-router-effect 0.2.0 3f · 30 sloc · 1×1 · 1 course STUDS C · E · carries I — routeRef() 10 ui-router-mobx 0.2.0 3f · 117 sloc · 1×1 · 1 course C RAIL · carries K — RouterStore.for() 9 ui-router-signals 0.2.0 4f · 104 sloc · 1×1 · 2 courses STUDS C · E — on the plate itself carries J — RouterSignals.for() 6 lit-ui-router-ssr 0.7.0 18f · 1,485 sloc · 2×4 · 6 courses SEATS ON BRICK 1 — G · context-request AND ON BRICK 4 — H · createServerRouter() the bridge — a drop line onto each assembly 4 ui-router-server 0.7.1 21f · 3,006 sloc · 2×4 · 7 courses STUDS A′ · D′ — on a plate of its own carries H — createServerRouter() @uirouter/core 6.1.2 — THE BASEPLATE 80 files · 5,272 sloc · not massed a plate is ground: every brick peers it, and no brick may replace it A router.plugin(factory) one method, not eight slots: bricks queue on it D urlService .listen() · .sync() · .rules — brick 1 starts and syncs the client router C transitionService .onSuccess({}, …) — one hook per router from each of bricks 8, 9 and 10 E globals .current · .params · the last transition bricks 8, 9 and 10 mirror them, never write B stateRegistry .decorator(‘views’, litViewsBuilder) the one graft that renders Lit ui-router-server takes NO stud on this plate — @uirouter/core is an OPTIONAL peer for it THE SECOND PLATE — the same core, headless peerDependenciesMeta marks core optional; simulate.ts builds a headless UIRouter — servicesPlugin + serverLocationPlugin, the mount’s matcher config replayed — pooled per mount, prewarm builds them ahead; reached only through a lazy import(), so the default ‘matcher’ tier, the one brick 6 drives by default, ships with no plate at all bricks 3, 5, 6 and 7 seat on brick 1 and 6 on brick 4 too, and bricks 5, 7 and 3 peer studs I, J and K on bricks 8, 9 and 10 — every brick-to-brick joint is a seam the lower brick publishes THE FINISHED MODEL — the same parts seated in a landscape of two levels: three steps at the drawing’s own corner, then the whole from two more STEP 1 — brick 1 takes the rail, brick 2 the location seat, bricks 8, 9 and 10 on C and E STEP 2 — bricks 3, 5 and 7 seat on brick 1’s cap, stud F STEP 3 — brick 6 seats on G and on H: the bridge spans the no-DOM line THE WHOLE, FROM THE OPPOSITE CORNER — brick 6 is the bridge; the free rail seats stay ringed THE WHOLE, FROM THE SERVER SIDE — the optional peer in place: the headless plate, dashed, on its shelf under brick 4, and brick 6 seated on H TWO LEVELS OF GROUND the browser — the DOM, one course of ground under the client plate the server shelf — a request runtime with no DOM (node >=22.12; Connect, Vite, fetch and Hono adapters), standing two courses higher, because brick 4’s cap must meet brick 1’s for brick 6 to seat on both the headless plate is dashed on its shelf: an optional peer, reached by lazy import — the ground is real

STRUCTURE SCHEDULE — one row per brick · quantized plan · courses · the exact coupling, with its source

#packageversionfilesslocmouldstudscoupling, with its source
0@uirouter/core6.1.2805,272baseplate, not massed — a plate is groundA–E, its published APIevery brick on this sheet declares it as a peerDependency; the only joints from one brick to another are 3 → 1, 5 → 1, 5 → 8, 6 → 1, 6 → 4, 7 → 1, 7 → 9 and 3 → 10
1lit-ui-router1.19.0253,2622×4 · 9 coursesstuds A · B · D · carries F · Gclass UIRouterLit extends UIRouter — moulded, not snapped; everything lit-specific enters through published seams: this.plugin(servicesPlugin) · this.stateRegistry.decorator(‘views’, litViewsBuilder) · urlService.listen()/sync() · viewService._pluginapi._viewConfigFactory(‘lit’, …) (internal) [src/core.ts] and publishes two of its own: F UIRouterLitElement.seekRouter(host), the ui-router-context event; G lit-ui-router/context, the context-request protocol [src/ui-router.ts, src/context.ts]
2ui-router-navigation-location-plugin1.2.022141×2 · 1 coursestud A (LOCATION SEAT)navigationLocationPlugin = locationPluginFactory(‘vanilla.navigationLocation’, true, NavigationLocationService, BrowserLocationConfig) a router holds exactly one location plugin, so this brick SWAPS core’s pushStateLocation; it peers core only — it has never heard of Lit [src/index.ts]
3lit-ui-router-mobx1.4.051441×1 · 2 coursesseats on brick 1 (F) · peers brick 10 (K)RouterStore.for(router) comes from ui-router-mobx ^0.2.0, stud K on brick 10, which registers the one onSuccess per router — this brick holds no hook of its own; src/router-store.ts re-exports it [src/router-reaction-controller.ts, src/router-store.ts] RouterReactionController → requestRouter(host, { subscribe: true }) over stud G, then UIRouterLitElement.seekRouter(host), the ui-router-context event — stud F on brick 1 [src/router-reaction-controller.ts]
4ui-router-server0.7.1213,0062×4 · 7 coursesNO STUD on this plate · carries HpeerDependenciesMeta: { ‘@uirouter/core’: { optional: true } } — the ‘matcher’ tier is dependency-free pattern matching the ‘simulate’ tier reaches plates of its own behind a lazy import(): createHeadlessRouter(states, config) — new UIRouter() + plugin(servicesPlugin) + plugin(serverLocationPlugin), the mount’s strict / caseInsensitive / defaultSquashPolicy replayed [src/simulate.ts] a pool of them per mount, each held by one request at a time — prewarm builds them ahead, release() resets one for the next request [src/index.ts, src/router-pool.ts]
5lit-ui-router-effect0.6.042851×2 · 2 coursesseats on brick 1 (F) · peers brick 8 (I)routeRef(router) and snapshotRoute() come from ui-router-effect ^0.2.0, stud I on brick 8, which registers the one onSuccess per router — this brick holds no hook of its own [src/router-ref-controller.ts, src/index.ts] RouterRefController → getScopedRouter() under a server render, else requestRouter(host, { subscribe: true }) over stud G, then seekRouter(host) on stud F; RefController forks one Effect fiber per host · peers effect ^3.22.0 || ^4.0.2 [src/router-ref-controller.ts, src/ref-controller.ts]
6lit-ui-router-ssr0.7.0181,4852×4 · 6 coursesseats on brick 1 (G) AND on brick 4 (H) — the one part with a drop line onto each plate’s assemblyprerender() → createServerRouter({ mounts }).resolve(path) per path · provideRouter(root, router) + withRouterSync(router, …) around @lit-labs/ssr render() [src/prerender.ts] UiViewRenderer → getScopedRouter() + router.viewService.registerUIView(); withServedRender(UiView) extends brick 1’s view, which wakes on requestContext(adoptUiViewContext) [src/ui-view-renderer.ts, src/served-view.ts, src/register.ts] client half: hydrateRoot() → provideContext(container, adoptUiViewContext, adopt) + hydrate(); uiViewSlot() marks each view’s hole · peers @lit-labs/ssr ^4.1.0 and ssr-client ^1.1.7 — a render table, not a router plate [src/client.ts]
7lit-ui-router-signals0.3.041701×2 · 2 coursesseats on brick 1 (F) · peers brick 9 (J)RouterSignalController → getScopedRouter() under a server render, else requestRouter(host, { subscribe: true }) over stud G, then UIRouterLitElement.seekRouter(host), stud F on brick 1 [src/router-signal-controller.ts] RouterSignals.for(router) + watchSelection() from ui-router-signals ^0.2.0 — it holds no hook of its own; brick 9 registers the one onSuccess per router [src/router-signal-controller.ts, src/signal-controller.ts]
8ui-router-effect0.2.03301×1 · 1 coursestuds C · E on the plate · carries I · peers core and nothing in this reporouteRef(router) → router.transitionService.onSuccess({}, …), one SubscriptionRef per router; snapshotRoute() reads globals.current/.params/.successfulTransitions — no Lit host, no brick under it · peers effect ^3.22.0 || ^4.0.2 [src/route-ref.ts]
9ui-router-signals0.2.041041×1 · 2 coursesstuds C · E on the plate · carries J · peers core and nothing in this repoRouterSignals.for(router) → attach(): one transitionService.onSuccess({}, …) hook writes a Signal.State; route · current · params · transition are Signal.Computed over it — no Lit host, no brick under it · peers signal-polyfill ^0.2.0 [src/router-signals.ts]
10ui-router-mobx0.2.031171×1 · 1 coursethe C rail’s far seat · carries K · peers core and nothing in this repoRouterStore.for(router) → attach(): one transitionService.onSuccess({}, update) hook writes MobX observables; update() reads globals.current/.params/.successfulTransitions — no Lit host, no brick under it · peers mobx ^6.0.0 || ^7.0.0 [src/router-store.ts]
11ui-router-registry0.3.05362not a brickno stud on any router plate · peers core ^6.0.8 for its typesa types package: an app fills in interface Register by module augmentation, and StateName · ParamsOf · TypedStateService narrow to its states — it emits no JavaScript [src/registry.ts]
12eslint-plugin-lit-ui-router1.3.0121,481not a brickno stud on any router platea lint package: it plugs into ESLint/oxlint hosts, never into @uirouter/core — rows 11 and 12 are the two published parts of twelve that live on a different table — types and lint

TOTAL — 10 bricks drawn · 89 authored files · 8,817 sloc, standing on one 80-file, 5,272-line baseplate · counted at origin/main @ 3f55e419 (2026-10-31), same basis as sheets 3 and 4

STUD SCHEDULE — the published surface each brick seats on

studpublished onseamwho seats on it
Acore — the platerouter.plugin(factory)the back rail, eight seats — brick 1 registers servicesPlugin on it the red ring: ONE location plugin per router — brick 2 SWAPS core’s out
Bcore — the platestateRegistry.decorator(…)brick 1 — decorator(‘views’, litViewsBuilder) is the whole Lit graft
Ccore — the platetransitionService.onSuccess() — a front rail, a hook per callerbricks 8, 9 and 10 — one hook per router each; 8 and 10 seat on it
Dcore — the plateurlService.listen() / .sync() / .rulesbrick 1 starts the client; brick 4 replays rules on its own plate
Ecore — the plateglobals.current / .paramsbricks 8, 9 and 10 read them into observables — none writes them
Fa brick, for the ones above itUIRouterLitElement.seekRouter(host)brick 1’s ui-router-context event — bricks 3, 5 and 7 seat here
Ga brick, for the ones above itlit-ui-router/context — context-requestbrick 1’s provideRouter · withRouterSync · getScopedRouter, requestContext · provideContext — brick 6 seats here; 3, 5 and 7 ask here first
Ha brick, for the ones above itcreateServerRouter({ mounts }).resolve(path)brick 4’s verdict table — brick 6 prerenders through it
Ia brick, for the ones above itrouteRef(router) · snapshotRoute()brick 8’s SubscriptionRef — brick 5 forks it per host
Ja brick, for the ones above itRouterSignals.for(router) · watchSelection()brick 9’s signal state — brick 7 binds it to a Lit host
Ka brick, for the ones above itRouterStore.for(router)brick 10’s observable store — brick 3 reacts to it on a Lit host

TOTAL — 11 seams: 5 on core, 6 on the bricks

FIG. 2Every companion drawn here enters through a published stud — on @uirouter/core, or on a brick that publishes one of its own. Drawn exploded, the sheet answers the only question that matters about a plugin architecture: pull any brick off and what breaks? Only what stands on it or peers it — four bricks seat on lit-ui-router, one of them on the server too, three of them peering a brick on the plate — and the plate’s studs stay where they are.

GENERAL NOTES

Why bricks. The mechanism these packages share is a standardised coupling: each companion attaches through a published extension point — on @uirouter/core, or on a brick below it that publishes one — and any brick nothing stands on can be left in the box without disturbing the rest. That is a stud, and a stud is worth drawing. So this is an exploded isometric — the LEGO instruction manual's own idiom — with a numbered part per package, a drop line onto the exact stud it takes, and a parts callout. The exploded view seats nothing, because a seated assembly hides the undersides, and the undersides are the argument; the finished model under it is drawn seated for the other half of the reading — the shape the parts make — in three steps at the drawing's own corner and then whole from two more, on a landscape of two levels: browser ground under the client plate, and a server shelf under the headless one.

The plate is core, not this package. Sheet 4's finding decides it: every limb in the family declares @uirouter/core as a peer. lit-ui-router is therefore brick 1, not the ground — and the drawing is honest about the one place the metaphor strains: class UIRouterLit extends UIRouter is moulded onto the plate, not snapped to it. What the drawing then shows is that the graft is thin anyway. Everything Lit-specific arrives through two published seams — this.plugin(servicesPlugin) and this.stateRegistry.decorator('views', litViewsBuilder) — plus urlService.listen()/sync() to start the thing. Three calls, and one internal seam — viewService._pluginapi._viewConfigFactory('lit', …) — that core does not publish. That is the whole renderer coupling.

One stud is a seat, not a socket. router.plugin() is the back rail: a single method that anything may queue on, which is why the spare-parts box is drawn at all — @uirouter/visualizer, sticky-states, dsr and rx all register through it and none of them knows this repo exists. One seat on that rail is ringed red, because it behaves differently: a router holds exactly one location plugin, so ui-router-navigation-location-plugin is a swap for core's pushStateLocation, never an addition. That package peers @uirouter/core and nothing else — it has never heard of Lit, and would work identically under the React or Angular adapters.

Brick 1 publishes two studs of its own, and four bricks take them. lit-ui-router-mobx and lit-ui-router-effect both ask first over stud G, requestRouter(host, { subscribe: true }), so a placeholder router's upgrade reaches them, and seat through UIRouterLitElement.seekRouter(host) — stud F, a bubbling ui-router-context event, published precisely as the dependency-injection primitive for external reactivity systems — when no provider answers. Brick 3 holds no hook of its own: it takes RouterStore.for() from ui-router-mobx ^0.2.0, stud K on brick 10, and runs a MobX reaction per connected host over the store it returns. Brick 5 holds no hook of its own: it takes routeRef() and snapshotRoute() from ui-router-effect ^0.2.0, stud I on brick 8, and forks one Effect fiber per connected host over the SubscriptionRef it returns. Both observe; neither writes router state. 144 and 285 lines. lit-ui-router-ssr takes stud G alone: lit-ui-router/context, the community context-request protocol spoken without @lit/context. prerender() provides the router on the render root with provideRouter() and scopes each render with withRouterSync(), its UiViewRenderer reads that scope with getScopedRouter(), and on the client the served view it defines — withServedRender(UiView), brick 1's own view class extended — wakes on requestContext(adoptUiViewContext), which hydrateRoot() answers with provideContext().

The agnostic bricks. ui-router-effect (brick 8), ui-router-signals (brick 9) and ui-router-mobx (brick 10) peer @uirouter/core and nothing in this repo — no Lit, no brick 1: each holds one transitionService.onSuccess({}, …) hook per router and reads globals, into an Effect SubscriptionRef through routeRef(), a Signal.State through RouterSignals.for() or MobX observables through RouterStore.for(). So all three drop straight onto the plate, brick 8 onto stud C, brick 9 onto stud E and brick 10 onto the far seat of stud C's front rail — onSuccess() is one method that queues a hook per caller — 30, 104 and 117 lines with no brick under them. Each one-stud cap is ringed, because a Lit binding on brick 1's cap peers it: I, routeRef(), which brick 5 takes; J, RouterSignals.for(router) and watchSelection(), which lit-ui-router-signals (brick 7) takes at ^0.2.0; and K, RouterStore.for(router), which brick 3 takes at ^0.2.0. Bricks 5, 7 and 3 find their router over G and then F on brick 1, and stand nine courses above the plate, so they peer I, J and K without sitting on them: those joints are lettered at both ends, not dropped, and no binding holds a hook of its own.

The fourth brick has no stud on the client plate, and that is the design. ui-router-server declares @uirouter/core as an optional peer (peerDependenciesMeta); its default 'matcher' tier is dependency-free pattern matching and never loads core, and its 'simulate' tier reaches plates of its own behind a lazy import() — a pool of headless routers per mount, each a new UIRouter() with servicesPlugin and the package’s own serverLocationPlugin, the mount’s matcher config replayed onto it, built on first use or up front by prewarm, reset between requests and dropped after a failed transition, because core mutates registrations and no two requests may hold one at the same time. Drawing it over a dashed second plate is the only truthful placement: it is the same mould, a different assembly, and the tie back to the client plate is crossed out. Seated, that plate stands on a shelf of its own: server ground, a request runtime with no DOM (node >=22.12; Connect, Vite, fetch and Hono adapters), drawn two courses above the browser ground because brick 6 must meet brick 4's cap at the height of brick 1's. The plate on it is dashed — an optional peer, reached by lazy import — and the ground is not. It does carry a stud of its own, H — createServerRouter({ mounts }), whose resolve(path) returns a verdict.

Brick 6 is the bridge. lit-ui-router-ssr peers lit-ui-router ^1.19.0 and ui-router-server ^0.7.0, and it is the one part in the family that takes a drop line onto each of this sheet's two assemblies: G on brick 1, H on brick 4. prerender() compiles the mount table with createServerRouter(), asks it for a verdict per path, and turns shell verdicts into pages, redirects into host rules and the otherwise projection into the 404 document. It never reaches the second plate's studs itself — that plate is ui-router-server's to load or not. Its other two peers, @lit-labs/ssr ^4.1.0 and @lit-labs/ssr-client ^1.1.7, are a render table, not a router plate, so they take no stud here.

Massing, quantized. Continuous mass would have made these bricks unbuildable shapes, so the census is rounded to LEGO: plan is one stud per 150 sloc rounded up to the next standard shape (1×1, 1×2, 2×2, 2×3, 2×4), height is one course per three authored files. The result is legible and it is a finding — the three bricks that carry a renderer, a server and the prerender bridge between them are all 2×4s; the seven that plug the router into something are four 1×1 and three 1×2 tiles, 30, 104, 117, 144, 170, 214 and 285 lines. A plug that needed to be a 2×4 would be lit-ui-router's problem to absorb, not a package. The baseplate is deliberately not massed: 80 files and 5,272 lines of core is ground, and ground has no height.

Twelve published packages; ten bricks in this assembly. Every runtime companion is a brick, and every one is drawn. The remaining two packages are compile-time parts, and neither is a brick: they take no stud on any router plate. ui-router-registry 0.3.0 (5 files, 362 sloc) is types only — it peers @uirouter/core for the declarations it narrows, and an app's module augmentation of Register is the whole of its coupling, so nothing of it reaches a running router. eslint-plugin-lit-ui-router 1.3.0 (12 files, 1,481 sloc) couples to ESLint and oxlint, not to @uirouter/core. Every number on this sheet is read from www/atlas.lit-ui-router.dev/data/census-bricks.json — counted at origin/main @ 3f55e419 (2026-10-31).

KEY

a published package — plan ∝ quantized sloc, courses ∝ files
a stud — a published extension point, on core or on a brick
the location seat — exactly one per router, so it swaps
a ringed stud on a plate brick — a brick on the cap peers it, lettered at both ends, not dropped
drop line — the brick falls onto that stud
a second plate — optional peer, reached by lazy import
no coupling — the server takes no stud on this plate
spare part — same stud, not in this set (sheet 4)
ground — the browser, and a server shelf above the no-DOM line
PROJECTTHE ALTITUDE ATLAS
SHEET TITLEthe BRICK ASSEMBLY
SCALETEN PACKAGES
FORMBRICK ASSEMBLY
CLIENTlit-ui-router · simshanith
DATE / REV2026-10-30 · K
DRAWN BYFable (Claude, AI)
SHEET2 OF 14