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
+1 -1
View File
@@ -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
+3 -2
View File
@@ -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.
+6 -2
View File
@@ -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.
+1 -1
View File
@@ -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
+71
View File
@@ -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.