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:
co-authored by
Claude Opus 5
parent
ca3f37d5f9
commit
20d5128bad
@@ -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.
|
||||
|
||||
@@ -1232,7 +1232,7 @@ its role-specific URL only from the protected descriptor below.
|
||||
|
||||
## Module display host
|
||||
|
||||
Manifest version 6 adds one independently optional display declaration
|
||||
The manifest carries one independently optional display declaration
|
||||
([ADR 0033](../architecture/decisions/0033-display-kiosk-host-contract.md)):
|
||||
|
||||
```json
|
||||
|
||||
@@ -137,7 +137,8 @@ enforces those permissions while the module is disabled, because it mounts no ro
|
||||
|
||||
## Module permission composition
|
||||
|
||||
A module manifest's `permissions` array (`manifestVersion: 4`,
|
||||
A module manifest's `permissions` array (introduced in `manifestVersion: 4`; the current strict
|
||||
manifest is version 7 — see
|
||||
[Module development](../module-development/README.md#canonical-manifest)) declares permissions in
|
||||
the exact same `PermissionDeclaration` shape — but declaring one there only proves it is
|
||||
well-formed and correctly namespaced under the module's own id; it does not, by itself, register
|
||||
@@ -667,7 +668,7 @@ module database usage), and exactly one `pool.end()` call regardless of how many
|
||||
- `packages/contracts/tests/authorization.test.ts` — `PermissionDeclaration` shape validation:
|
||||
every baseline, unknown-property/missing-baseline/invalid-baseline/blank-or-oversized-description
|
||||
rejection, malformed permission id rejection.
|
||||
- `packages/contracts/tests/module-manifest.test.ts` — `manifestVersion: 4` acceptance and version
|
||||
- `packages/contracts/tests/module-manifest.test.ts` — exact-version acceptance and version
|
||||
2/string-array rejection, per-declaration validation (mirroring the contracts-level cases above),
|
||||
module-namespace enforcement, duplicate permission id at its exact `permissions[index].id` issue
|
||||
path, and default-empty-list isolation across separate parses.
|
||||
|
||||
@@ -37,7 +37,10 @@ the root verification command discovers them automatically.
|
||||
|
||||
`pnpm modules:validate` builds the workspace, then scans `modules/*` without a central registry. A
|
||||
missing or empty modules directory is valid and reports zero installed modules. Installed modules
|
||||
must produce the compiled JavaScript entrypoints declared by their static manifests. Optional
|
||||
must produce the compiled JavaScript entrypoints declared by their static manifests. Each module's
|
||||
declared browser contributions are listed with their target capability and range, and marked
|
||||
according to whether a compatible provider is installed — a contribution nobody provides is otherwise
|
||||
silent, since the manifest is valid and the artifact exists while nothing renders (ADR 0041). Optional
|
||||
schema entrypoints are loaded and validated for table ownership, namespacing, duplicates, and
|
||||
foreign-key isolation; this command does not connect to PostgreSQL or run migrations.
|
||||
|
||||
@@ -82,7 +85,8 @@ not a broken deployment).
|
||||
Doctor reports a fixed, deterministic set of checks:
|
||||
|
||||
- The active Node.js version against the repository's declared engine range.
|
||||
- Static module discovery and dependency-first ordering.
|
||||
- Static module discovery and dependency-first ordering. A module that declares browser
|
||||
contributions also reports how many of them have a compatible provider among the installed set.
|
||||
- Schema discovery, including validated module and table counts. This imports compiled schema
|
||||
definitions, which the module contract requires to perform no configuration reads, I/O, or
|
||||
database connection.
|
||||
|
||||
@@ -201,7 +201,7 @@ separate deployment and threat-model decision.
|
||||
|
||||
### A display page is authorized; its artifact bytes are not authority
|
||||
|
||||
Manifest version 6 binds a display artifact to one permission declared by the same module
|
||||
The manifest binds a display artifact to one permission declared by the same module
|
||||
([ADR 0033](../architecture/decisions/0033-display-kiosk-host-contract.md)). The declaration rejects
|
||||
a `public` baseline: the current display host requires an ordinary authenticated cookie principal,
|
||||
and public/device identity is not approximated with an anonymous exception. Both the stable
|
||||
|
||||
@@ -69,6 +69,77 @@ protocol-relative, backslash-prefixed, and scheme-bearing values cannot become n
|
||||
Modularity checks keep authentication out of the module UI SDK/context and Dashboard login code out
|
||||
of the separate display bundle.
|
||||
|
||||
Phase 8 adds evidence for the five platform prerequisites.
|
||||
|
||||
**Design tokens and shared elements** (ADR 0039). The token registry's tests prove it is
|
||||
deterministic and enumerable, that every token derives a unique `--ekklesia-*` identifier from its
|
||||
name and carries a label, description, and category, that each status family pairs a colour with a
|
||||
non-colour symbol, and that a duplicate name, an unknown category, an invalid or single-segment name,
|
||||
and a value carrying a declaration terminator, a rule block, a comment, an at-rule, `url(`,
|
||||
`!important`, an escape, a control character, or markup are each rejected — with an error that never
|
||||
echoes the rejected declaration. The elements' tests assert accessible structure (label/hint/error
|
||||
association, icon-button naming, tab roles and arrow-key movement, announced error and loading
|
||||
states), that status never depends on colour alone, that a dialog renders in place rather than
|
||||
portalling out of an isolated root, and — mechanically — that the stylesheet and every component
|
||||
source contain no literal colour, font stack, or theme value and reference only registered tokens.
|
||||
Both packages compile module-shaped source under the exact browser project a module uses, where
|
||||
`process.env` must fail to typecheck, so the browser boundary is measured rather than asserted.
|
||||
|
||||
**Module stylesheets and artifact isolation** (ADR 0040). The builder's tests prove a plain
|
||||
stylesheet is compiled into the declared artifact with no `.css` emitted beside it, that a local
|
||||
`@import` is inlined, and that an unresolved `@import`, a stylesheet declaring an `--ekklesia-*`
|
||||
variable, and a non-CSS dialect are each rejected with their own sanitized code — while a private
|
||||
alias derived from a registered token is accepted. The loader's tests prove every artifact mounts
|
||||
inside an open shadow root, that the artifact receives that root rather than the host's element, that
|
||||
exactly one `<style>` is installed per mount and never accumulates across remounts, that the host's
|
||||
base rules precede the artifact's so a display can override `:host`, that unmount removes the whole
|
||||
root even when the module's cleanup throws, that a failed mount leaves neither DOM nor styles behind,
|
||||
that an environment without Shadow DOM fails closed rather than mounting unisolated, and that an
|
||||
artifact built against the previous ABI is refused with `MODULE_UI_ABI_UNSUPPORTED`. The template's
|
||||
suite builds real React artifacts through the canonical pipeline and asserts the compiled stylesheet
|
||||
travels inside the one declared file.
|
||||
|
||||
**Browser contributions** (ADR 0041). Manifest tests cover namespacing, the single local segment,
|
||||
duplicate ids and artifact paths, collision with an entrypoint, and a contribution targeting a
|
||||
capability its own module provides. Discovery tests prove a contribution artifact inherits the exact
|
||||
path, symlink, containment, extension, and file-kind protections every other compiled entrypoint has,
|
||||
including two contributions linked to one file. Publication tests prove a contribution is published
|
||||
only when another eligible module provides its capability *within the declared range*, that the
|
||||
contributing module never sees its own contribution, that a lookup fails closed on an unknown,
|
||||
mismatched, or non-string identity, and that the published record carries no domain vocabulary at
|
||||
all. Route tests prove the artifact is served byte-for-byte at a URL that cannot select a neighbour,
|
||||
and that an unknown id, another module's id, a traversal, a source map, a wrong version, an absent
|
||||
provider, and an incompatible provider all collapse into one indistinguishable `404`. The loader's
|
||||
tests cover mount, update-without-remount, a renderer that declares no `update`, deterministic and
|
||||
idempotent unmount, a sanitized update failure, and input that is not genuinely JSON — a `Date`, a
|
||||
function, `NaN`, `undefined`, and a cycle — refused *before* anything is imported and without ever
|
||||
echoing the consumer's data. The flagship shell fixture drives the whole relationship: a `signage`
|
||||
module discovers, mounts, updates, and unmounts an `announcements` React renderer with neither module
|
||||
importing the other, the consumer seeing identity but never a URL, the host resolving publication,
|
||||
and a contribution id the server withheld refused without a fetch.
|
||||
|
||||
**Bounded streaming uploads** (ADR 0042). Contract tests pin the two ceilings, that `maxBodyBytes` is
|
||||
checked against the ceiling the declared mode selects, that 32 MiB holds a 25 MiB image and one byte
|
||||
more is refused, and that no mode admits an unbounded body. HTTP tests cover exact-boundary success,
|
||||
a declared `Content-Length` refused before the handler runs, a misleading smaller `Content-Length`
|
||||
that cannot smuggle a megabyte past the limit, a chunked overflow, a `413` that still wins when the
|
||||
handler swallowed the stream error, proof that the body is genuinely not buffered (a handler sees the
|
||||
first chunk before the last is produced), an aborted upload surfacing as an ordinary failure rather
|
||||
than a `413`, an unread body being released, authorization ordering in both directions, and a
|
||||
rejection that names no limit, size, or body content. The live suite uploads a real 25 MiB body over
|
||||
a real socket end to end and proves the server is healthy immediately afterwards.
|
||||
|
||||
**Display offline fallback** (ADR 0043). The worker's caching logic is exercised directly, because it
|
||||
touches no global: first online load, offline reload after one successful visit, a missing uncached
|
||||
artifact failing honestly, contributed-renderer fallback, version upgrade with pruning that keeps
|
||||
every artifact of the current version and never touches another module's, superseded cache
|
||||
generations removed while unrelated ones survive, network-first behaviour picking up rebuilt bytes,
|
||||
and an explicit inventory proving the descriptor, principal, module API, realtime stream, pairing
|
||||
exchange, auth routes, cross-origin signed object URLs, and the worker itself are **never** stored —
|
||||
so a revoked device's refusal is what a reconnected display sees. Page-level tests cover the quiet
|
||||
offline indicator, a cache fallback reported while the browser still claims to be online, automatic
|
||||
re-resolution when the network returns, and a revoked device dropping back to pairing on reconnect.
|
||||
|
||||
`pnpm verify` is intentionally Docker-free. It never plans or applies migrations against a live
|
||||
database.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user