docs: record the five Phase 8 prerequisites, and report contributions

Bring the living documentation in line with what the five passes actually
shipped, and correct the claims they superseded rather than leaving them to
mislead: the module UI context is six members, not five; module styling is
compiled CSS, not inline objects only; the strict manifest is version 7.

Also close a silent failure the new contribution mechanism introduces. A
contribution whose target capability nobody provides — or whose provider moved
outside its declared range — is not published, and the manifest is valid and
the artifact exists, so an operator sees a screen that renders nothing with no
error anywhere. `pnpm modules:validate` now lists each declared contribution
with its capability and range and says whether a compatible provider is
installed, and Doctor's discovery check reports the same per module. Both
derive from the publication registry the API serves from, so neither can
disagree with what a running platform would actually publish.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
ekklesia
2026-08-27 16:16:01 -05:00
co-authored by Claude Opus 5
parent ca3f37d5f9
commit 20d5128bad
10 changed files with 375 additions and 11 deletions
+81 -2
View File
@@ -228,7 +228,8 @@ takes a DOM element and returns an unmount function, so a module's framework and
independent.
The contract lives in `@ekklesia/module-ui-sdk`, a browser-safe shared package with no runtime
dependencies, consumed by module authors and hosts alike. Its host context is five members —
dependencies, consumed by module authors and hosts alike. Its host context was five members at that
checkpoint and is six today (ADR 0041) —
`moduleId`, `api`, `principal`, `permissions`, and `realtime`. Module UI reaches its own backend through
`api.request(path)` with a path relative to the module's own API root, never by constructing
Ekklesia's `/api/v1/modules/<module-id>` prefix, which is what keeps a standalone host possible.
@@ -244,7 +245,8 @@ it, and hosts perform their own dynamic import.
`src/ui/index.tsx` into the browser ESM artifact its manifest declares, and puts every bundler
option in one `tooling` package so module authors write no build configuration. React is bundled
rather than externalized, keeping `mount()` the ABI and a module artifact hostable by any host;
styling is inline style objects only, and a stylesheet import fails the build rather than emitting an
styling was inline style objects only at that checkpoint and is real compiled CSS today (ADR 0040),
and a stylesheet import then failed the build rather than emitting an
asset nothing declares. Building remains an authoring step — Core never builds, and discovery still
only validates the artifact a module shipped. At that checkpoint the Dashboard shell and UI runtime
were still future work; later Phase 6 passes supplied them. A design system, stylesheet loading
@@ -573,3 +575,80 @@ An authenticated result preserves the response, session, and mounted UI. Display
remains deferred and the display bundle gains no Dashboard login. Public Website routing,
production Dashboard asset hosting/DNS/TLS/proxy/CSP, enrollment and recovery, shared throttling,
and the missing production first-user creation path are also outside this pass.
**Phase 8 supplies the five platform prerequisites the first production feature module needs.** None
of it is Signage, and none of it names Signage: each capability is generic, and each is exercised by
something other than a feature module.
[ADR 0039](decisions/0039-shared-design-tokens-and-elements.md) adds the design layer the platform
never had. `@ekklesia/design-tokens` is one validated, enumerable registry of **semantic** tokens —
`background.raised`, `text.danger`, `state.hover-background` — where a token's category and
`--ekklesia-*` identifier are *derived* from its name and every value must be a bounded literal, so a
registry that constructs is one that can be emitted as CSS without escaping. That validation is
exactly what makes a future theme editor implementable rather than aspirational: the values it would
edit are already labelled, described, categorised, and enumerable. `@ekklesia/ui` is the small set of
elements a real module dashboard cannot be built without, every value a token reference, with real
`:focus-visible` treatment and status that never depends on colour alone. `pnpm boundaries` opens
exactly two shared roles to modules **by name**, and pins both design packages beneath everything
else. The Dashboard and display install the defaults at their document root; custom properties
inherit into every module artifact, so a module reads `var(--ekklesia-…)` with nothing to import at
run time and nothing for a host to forget to pass.
[ADR 0040](decisions/0040-module-stylesheets-and-artifact-isolation.md) reverses ADR 0023's
inline-styles-only rule, and supplies the three things that ADR said were missing. A module imports
plain `.css`; the canonical builder compiles it **into** the declared artifact as text, so there is
still exactly one file per role and no sidecar can exist — the failure ADR 0023 was protecting
against. Two rules are enforced with their own codes: a module stylesheet may reference
`--ekklesia-*` and never declare one, and every `@import` must resolve locally. The generic loader
then mounts **every** browser artifact inside its own open Shadow DOM root with its own stylesheet
installed, unconditionally, because a conditional isolation boundary is one a module can fall out of
by editing a source file. That root is a correctness boundary for *styles* and is never described as
a sandbox: it is `open` precisely because module code is trusted, same-realm code that could reach
`document` anyway. The browser mount ABI is versioned (`MODULE_UI_ABI_VERSION`, stamped by
`defineModuleUi`) with no dual acceptance; migration is one `pnpm build`.
[ADR 0041](decisions/0041-module-browser-contributions.md) lets one module contribute a browser
renderer to a capability another module provides — the browser half of the capability system Core has
had since ADR 0006. `manifestVersion: 7` declares a namespaced id, its own version, a target
capability, a compatible range, and one artifact. That is the complete list: there is no payload
schema, slot, ordering, or rendering hint anywhere in the platform, so Core cannot learn what a
"slide" is because the declaration cannot express it. Discovery resolves the artifact through the
identical loop every other compiled entrypoint uses; publication requires that *another* eligible
module provides the capability within the declared range, and a contribution that fails is absent
rather than broken. A consumer discovers through `ModuleUiHostContext.contributions` and mounts by
id — never by building a publication URL — and can only see contributions aimed at a capability it
itself provides. The input is opaque JSON validated for serializability and nothing else, so the
domain contract stays entirely between the two modules. A contributed renderer receives its own id
and that input: no API client, realtime, principal, configuration, database, storage, or credential,
and loading one grants its module nothing.
[ADR 0042](decisions/0042-bounded-streaming-request-bodies.md) closes the write path ADR 0031 left
open. ADR 0030 gave modules a bucket and ADR 0031 took Ekklesia out of the *read* path, but declined
signed write URLs — so bytes reach a bucket only through a module's own route, which was capped at
1 MiB. A route now declares `bodyMode`: `buffered` (the default, and what every existing route
meant) keeps that ceiling and materializes the body before the handler runs; `streamed` raises it to
32 MiB and counts bytes as the handler consumes them, holding nothing. The mode is explicit rather
than inferred from the size, because inferring it would silently change how an author's own handler
receives its body. 32 MiB is also the platform's absolute ceiling in every mode, sized for one
25 MiB image plus bounded overhead, and the unit is deliberately one file per request. The platform's
own limiter replaces the buffering dependency: a declared oversize is refused before authorization,
the buffered path now counts what actually arrived rather than trusting a header, and a streamed
overflow errors the stream, cancels the source, and replaces the response with the same sanitized
`413` even if the handler swallowed the error.
[ADR 0043](decisions/0043-display-offline-code-fallback.md) gives the display host-owned offline
resilience, scoped precisely to **the shell and the declared code**. A service worker scoped to
`/display/` caches the page, the host script, the module's display artifact, and every contributed
renderer that display mounted — and never the descriptor, `/api/v1/principal`, any API response, the
realtime stream, a pairing exchange, or any cross-origin request, which is what a signed
object-delivery URL is. The consequence is stated rather than papered over: a display cannot render
module content while offline, because mounting needs an authorization-bearing descriptor that is
never cached, so a revoked device stays revoked and cached code can never manufacture current
authorization. What it buys is that a kiosk rebooting mid-outage boots into Ekklesia's own offline
state and recovers by itself, and that reconnecting mounts with nothing left to download. Caching is
network-first everywhere, so a screen can never be trapped on obsolete code.
Still deliberately absent after Phase 8: a theme editor, a brand builder, or any second theme; a
module store or installer; a standalone host; signed write URLs, resumable uploads, or any body above
the absolute ceiling; module-owned offline *content*; and every real feature module, Signage
included. Nothing in this pass claims any of them exists.