feat: establish the module product contract
Ekklesia's module model has been backend-only since ADR 0001: a module owns domain behavior and data, and every presentation concern belongs to a surface. That prevents a module author — the person who knows the domain — from shipping the screen that makes their module useful, and it guarantees a module and its own UI drift apart. It also pushes toward two codebases for one domain the moment a module needs to run standalone. ADR 0021 replaces that rule. A module is an independently versioned product that owns its domain behavior, data, permissions, routes, migrations, and its own UI, hostable inside Ekklesia and — from the same implementation — by a later standalone host. Core keeps the containing application shell and every platform policy: authentication, authorization enforcement, platform context, top-level routing, navigation, global theme, error boundaries, and trusted-origin/CORS. The module graph stays flat: modules are peers, genuine module and capability dependencies remain the only relationships, and Dashboard folders are presentation configuration an administrator can reorganize, never module architecture. This pass implements the static contract only. `manifestVersion` moves to 4 and `entrypoints` gains one optional field, `ui`. Version 3's `entrypoints` is strict and rejects `ui`, so this is an incompatible shape change and version 3 is now rejected outright rather than reinterpreted, exactly as version 1 was by version 2 and version 2 by version 3. Every field version 4 adds is optional, so upgrading a backend-only manifest is a one-character edit. Discovery resolves and validates `ui` under the same rules as `server` and `schema` — package-relative, inside the package before and after symlink resolution, a real file, compiled ESM `.js`/`.mjs`, so source TypeScript is rejected rather than assumed to be compiled later. Discovery stays inert: nothing imports, evaluates, mounts, or renders a UI entrypoint, and a test proves a throwing artifact still discovers successfully. This follows `entrypoints.jobs`, declared and resolved since ADR 0006 with no runtime consumer. The name is `ui`, not `dashboard`, because a module's UI must be hostable by Ekklesia's shell and by a standalone host, so the contract cannot be named after one of them; and not `surface`, because `surfaces/*` is a separate ownership layer. No widget, route, icon, folder, menu, layout, or standalone-metadata field is added, and tests assert the manifest still rejects every unknown entrypoint key and offers no hierarchy field. Deliberately unchanged: `scripts/check-boundaries.ts` still enforces `modules-do-not-own-ui`. Relaxing that guard depends entirely on the UI authoring model a later pass chooses, and weakening a working protection before that decision exists would trade real coverage for nothing. A module may therefore declare `ui` today only for a framework-neutral compiled artifact, and the docs say so rather than implying otherwise. Restart-based discovery, explicit operator-invoked migrations, and the trusted compiled-module model are untouched — a module that ships UI is exactly as trusted as one that does not. No Dashboard shell, UI runtime, renderer, framework choice, navigation registry, grouping system, standalone host, installer, hot-loading, or feature module is added by this pass. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
94d26cffa0
commit
44ba259f58
@@ -11,6 +11,16 @@ would couple business behavior to a particular presentation framework.
|
||||
|
||||
## Decision
|
||||
|
||||
> **Partial correction — see [ADR 0021](0021-modules-as-hostable-products.md).**
|
||||
> The rule below that a module exposes "data and behavior only" and that "every renderer and UI
|
||||
> concern belongs to a surface" was superseded by ADR 0021: a module may own its own UI and ship it
|
||||
> as part of an independently versioned product, hostable by Ekklesia and, later, by a standalone
|
||||
> host. The four ownership layers themselves, Core's feature-agnostic role, the prohibition on Core
|
||||
> importing a module, and every other boundary in this ADR remain accepted and unchanged — Core
|
||||
> still owns the containing application shell and all platform policy. This section is left as
|
||||
> originally accepted, as the historical record.
|
||||
|
||||
|
||||
Adopt four explicit ownership layers: Core orchestration, backend Modules, presentation Surfaces,
|
||||
and declarative Surface Builder configuration. Begin as a modular monolith. Modules expose data
|
||||
and behavior only; every renderer and UI concern belongs to a surface.
|
||||
|
||||
+7
@@ -50,6 +50,13 @@ server entrypoint, job, or lifecycle hook to discover its permissions; only the
|
||||
|
||||
### Manifest version 3 replaces version 2's bare permission strings
|
||||
|
||||
> **Partial correction — see [ADR 0021](0021-modules-as-hostable-products.md).**
|
||||
> `manifestVersion: 3` was superseded by ADR 0021's `manifestVersion: 4`, which adds the optional
|
||||
> `entrypoints.ui` field. The permission-declaration shape this section establishes, the reserved
|
||||
> `core.*` namespace, the composed platform registry, and the inert-manifest discovery model are
|
||||
> carried forward into version 4 unchanged; only the accepted version literal moved. The
|
||||
> version-rejection rule described below applies identically to version 3 under version 4.
|
||||
|
||||
Representing a permission as a complete declaration instead of a bare identifier string is an
|
||||
incompatible manifest shape change, so the canonical manifest moves to `manifestVersion: 3`
|
||||
(`z.literal(3)`). Version 2 — and any other version — fails validation outright; there is no
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
# 0021: Modules as independently hostable products
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-08-21
|
||||
- Extends: [0006](0006-versioned-capabilities-and-compiled-module-runtime.md),
|
||||
[0014](0014-module-permission-declarations-and-platform-registry.md)
|
||||
- Supersedes (partial): [0001](0001-four-layer-platform-boundary.md) — only its rule that a module
|
||||
never owns UI and that `surfaces/*` owns all presentation. The four ownership layers, Core's
|
||||
feature-agnostic role, and the prohibition on Core importing a module remain accepted and
|
||||
unchanged.
|
||||
- Supersedes (partial): [0014](0014-module-permission-declarations-and-platform-registry.md) — only
|
||||
its `manifestVersion: 3` literal. Permission declarations, the composed platform registry, the
|
||||
reserved `core.*` namespace, and the inert-manifest discovery model remain accepted and unchanged.
|
||||
|
||||
## Context
|
||||
|
||||
Every module contract this repository has accepted so far — static manifests (ADR 0005/0006),
|
||||
module-owned schemas and migrations (ADR 0007/0008/0009), permission declarations (ADR 0014),
|
||||
framework-neutral HTTP routes (ADR 0015), and typed database access (ADR 0016) — describes a
|
||||
*backend capability*. ADR 0001 placed all presentation in `surfaces/*` and forbade a module from
|
||||
owning any UI.
|
||||
|
||||
That split has a cost the platform can no longer absorb. A module like Announcements owns its
|
||||
domain model, its permissions, its storage, its routes, and its migrations, but cannot own the one
|
||||
screen that makes it useful. Under ADR 0001, an announcement editor must be written independently
|
||||
by whoever builds a surface, against a module the surface author does not own and cannot see the
|
||||
internals of. Every module then needs a second, separately maintained implementation of its own
|
||||
product experience, and the module's author — the person who actually knows the domain — is
|
||||
structurally prevented from shipping it.
|
||||
|
||||
The same pressure appears from the other direction. A church that wants only announcements should
|
||||
eventually be able to run that module as a standalone application. If Ekklesia-hosted and
|
||||
standalone experiences are two codebases, they diverge immediately: two editors, two validation
|
||||
rules, two permission interpretations, two release cadences, one domain.
|
||||
|
||||
Neither problem is solved by more surface tooling. Both are the same missing idea: a module is a
|
||||
*product*, not just a backend capability, and a product includes its own presentation.
|
||||
|
||||
## Decision
|
||||
|
||||
### Modules are independently versioned, first-class products
|
||||
|
||||
A module owns its domain behavior, data, migrations, permissions, capabilities, HTTP routes, and —
|
||||
new in this ADR — its own UI. It remains an independently versioned package with its own semantic
|
||||
version, its own capability versions, and its own compatibility ranges, exactly as ADR 0006
|
||||
established. Nothing about becoming a product changes how a module is versioned, discovered,
|
||||
ordered, or validated.
|
||||
|
||||
### Modules may own UI; Core owns the host shell and platform policy
|
||||
|
||||
A module may ship its own management screen, its feature-specific presentation behavior, and
|
||||
reusable UI belonging to its feature. It does **not** own the application that contains it.
|
||||
|
||||
When a module's UI runs inside Ekklesia, Core and the host application remain solely responsible
|
||||
for authentication and session establishment, authorization enforcement, organization/platform
|
||||
context, the containing application shell, top-level routing and mount boundaries, module
|
||||
discovery, the platform navigation container, the global theme and design-token contract, platform
|
||||
error boundaries, cross-module coordination contracts, trusted-origin/CORS and other security
|
||||
policy, and every platform service exposed through a deliberate contract. A module UI entrypoint
|
||||
consumes those; it never redefines, replaces, or bypasses them. This mirrors the boundary the
|
||||
backend already has: ADR 0015 lets a module mount routes, but the platform still owns method
|
||||
vocabulary, CORS preflight, body ceilings, and permission enforcement.
|
||||
|
||||
### The module hierarchy is flat
|
||||
|
||||
There is no architectural parent/child relationship between modules, and none is being introduced.
|
||||
`video`, `livestream`, `announcements`, `calendar`, `roster`, and `signage` are peers. A module that
|
||||
ships UI gains no containment relationship over any other module.
|
||||
|
||||
The only relationships a module may express are the two that already exist and remain fully
|
||||
supported: a genuine module dependency and a genuine capability dependency, both version-ranged and
|
||||
both resolved by discovery into a dependency-ordered graph (ADR 0006). Those express *requirement*,
|
||||
not *ownership*, and they are the correct mechanism when one module truly cannot function without
|
||||
another. Conceptual relatedness is not a dependency and must never be modelled as one.
|
||||
|
||||
### Dashboard grouping is presentation configuration, not module architecture
|
||||
|
||||
A Dashboard may eventually present independent modules inside folders or groups — `Media` holding
|
||||
Video and Livestream, or `Sunday Production` holding Livestream, Signage, and Video. Those groupings
|
||||
belong to Dashboard/surface configuration. They are not module ownership, not dependency, and not
|
||||
part of the module contract.
|
||||
|
||||
A module may eventually offer presentation metadata such as a *suggested* default grouping, but it
|
||||
must never own the folder it appears in, and correctness must never depend on that hierarchy. An
|
||||
administrator must be able to reorganize every module without any module changing. Accordingly, the
|
||||
manifest deliberately declares no group, folder, parent, menu, navigation, icon, or ordering field,
|
||||
and none may be added as a hierarchy mechanism.
|
||||
|
||||
### One module implementation, many hosts
|
||||
|
||||
Standalone support is explicitly *not* a second application. The intended shape is one module
|
||||
implementation consumed by thin host-specific composition:
|
||||
|
||||
```text
|
||||
Announcements module
|
||||
/ \
|
||||
Ekklesia host Standalone host
|
||||
```
|
||||
|
||||
Domain behavior, APIs, schema, migrations, permissions, UI, and feature logic are the module's and
|
||||
are reusable by both. Host-specific composition stays thin. This ADR does not implement a standalone
|
||||
host; it forbids designing the public contract in a way that makes one impossible. Concretely: a
|
||||
module's UI entrypoint must not be fundamentally dependent on a private Ekklesia dashboard
|
||||
application or on Core internals, and the contract must not be named after, or shaped around, one
|
||||
particular host.
|
||||
|
||||
### `entrypoints.ui` is the smallest primitive that establishes this
|
||||
|
||||
The manifest gains exactly one new optional field: `entrypoints.ui`, a package-relative path to a
|
||||
compiled artifact, validated identically to every other entrypoint. That is the whole new contract.
|
||||
|
||||
The name is deliberate. It is not `dashboard`, because a module's UI must be hostable by Ekklesia's
|
||||
shell *and* by a standalone host, so the contract cannot be named after one of them. It is not
|
||||
`surface`, because `surfaces/*` is a separate ownership layer (ADR 0001) and reusing that word for a
|
||||
module-owned artifact would conflate two different things. `ui` is host-neutral, platform-neutral,
|
||||
and already this repository's own vocabulary for the concept.
|
||||
|
||||
No widget, route, icon, folder, menu, layout, page-builder, or standalone-metadata field is added.
|
||||
Those contracts, if they are ever needed, evolve from this primitive under a later manifest version.
|
||||
|
||||
### The UI entrypoint is a compiled artifact, statically validated, and never executed by discovery
|
||||
|
||||
`entrypoints.ui` obeys exactly the rules ADR 0006 established for `server` and `schema`: it must be
|
||||
package-relative, must stay inside the module package (checked before and after symlink resolution),
|
||||
must resolve to a real file, and must be compiled ESM JavaScript ending in `.js` or `.mjs`. Source
|
||||
TypeScript is rejected at discovery rather than assumed to be compiled later.
|
||||
|
||||
Discovery remains completely inert. Core parses the manifest as static JSON, resolves the UI path,
|
||||
and stores it. It does not import, evaluate, render, mount, or otherwise execute a UI entrypoint —
|
||||
and nothing in this repository does yet. This follows the precedent already set by `entrypoints.jobs`,
|
||||
which has been declared and resolved since ADR 0006 with no runtime consumer.
|
||||
|
||||
### Manifest version 4 replaces version 3
|
||||
|
||||
Version 3's `entrypoints` object is `.strict()` and rejects `ui` outright, so accepting a UI
|
||||
entrypoint is an incompatible shape change. The canonical manifest moves to `manifestVersion: 4`
|
||||
(`z.literal(MODULE_MANIFEST_VERSION)`), and version 3 — like every other version — fails validation.
|
||||
There is no compatibility adapter, no automatic conversion, and no dual acceptance, exactly as
|
||||
version 1 was rejected by version 2 (ADR 0006) and version 2 by version 3 (ADR 0014). One manifest
|
||||
document can therefore never be interpreted as two different versions.
|
||||
|
||||
Backward compatibility is deliberate rather than automatic: because every field version 4 adds is
|
||||
optional, upgrading a backend-only version 3 manifest is a one-character edit and changes nothing
|
||||
else. A backend-only module remains completely valid and completely supported — declaring UI is a
|
||||
choice, never a requirement.
|
||||
|
||||
### Restart-based discovery remains correct
|
||||
|
||||
Installed module inventory is composed at startup and changes take effect on the next restart. This
|
||||
ADR adds no hot installation, no runtime unloading, no filesystem watching, and no dynamic mutation
|
||||
of the running registry. A future module-management UI does not imply hot-loading, and module UI
|
||||
does not change the reasoning ADR 0014 recorded for the static startup snapshot.
|
||||
|
||||
### Migrations remain explicit
|
||||
|
||||
Module installation and API startup never apply migrations. Modules continue to own their Drizzle
|
||||
schema and migration history, applied through Core's existing validated, operator-invoked workflow
|
||||
under an advisory lock and one transaction (ADR 0008/0009/0020). Destructive-migration approval,
|
||||
checksums, planning, locking, and transactional behavior are untouched by this ADR.
|
||||
|
||||
### The trusted compiled-module security model is unchanged
|
||||
|
||||
Installed modules remain trusted compiled code running inside the API process and sharing its
|
||||
PostgreSQL authority. Manifest validation, typed facades, route restrictions, table naming, and
|
||||
frozen contexts remain correctness and coordination boundaries — not a sandbox. **Adding a UI
|
||||
entrypoint grants no isolation and implies none.** A module that ships UI is exactly as trusted as
|
||||
one that does not, and an operator installing either is deploying trusted code. See
|
||||
[Security model and deployment trust boundaries](../../development/security.md).
|
||||
|
||||
## Alternatives
|
||||
|
||||
- **Surfaces own all UI (the status quo, ADR 0001).** Rejected because it forces every module's
|
||||
product experience to be re-implemented by someone who does not own the domain, and guarantees
|
||||
that a module and its own screen drift apart. It is the specific constraint this ADR exists to
|
||||
remove.
|
||||
- **Parent modules own child modules** (`media` containing `video` and `livestream`). Rejected
|
||||
because it encodes one organization's conceptual taxonomy as an architectural dependency. It
|
||||
makes an administrator's presentation preference a code change, forces artificial dependencies
|
||||
between modules that do not actually require one another, prevents a "child" from being installed
|
||||
or versioned alone, and conflates grouping with dependency resolution. Flat peers plus genuine
|
||||
dependencies express everything real, and Dashboard configuration expresses the rest.
|
||||
- **Separate standalone and Ekklesia implementations.** Rejected because two codebases for one
|
||||
domain diverge immediately in validation, permissions, and behavior, and double the maintenance
|
||||
and audit surface for every module forever. One implementation with thin host composition is the
|
||||
entire point of making a module a product.
|
||||
- **Hot-load modules immediately.** Rejected for this phase. It introduces concurrent-mutation and
|
||||
partial-update hazards across the registry, the permission registry, the route table, and now a
|
||||
UI surface, none of which this phase needs to solve. ESM evaluation cannot be rolled back or
|
||||
unloaded (ADR 0006), so hot replacement is a substantially harder problem than it appears. A
|
||||
restart remains sufficient for the current deployment model.
|
||||
- **Automatic migrations on install or startup.** Rejected, and rejected consistently with every
|
||||
prior ADR: a module gaining UI does not make schema change safe to perform implicitly. Automatic
|
||||
application would defeat destructive approval, remove the operator's review step, and make startup
|
||||
a mutating database operation.
|
||||
- **Extending version 3 in place with a new optional field.** Rejected because version 3's
|
||||
`entrypoints` is strict: silently widening it would change what an existing declared version means
|
||||
and make one document ambiguously readable as two versions. An explicit version bump keeps
|
||||
rejection behavior clear and validation deterministic.
|
||||
- **A richer first UI contract** (mount routes, widgets, icons, navigation entries, grouping hints,
|
||||
standalone metadata). Rejected as premature. None of it can be designed honestly before a host
|
||||
exists to consume it, and every field added now would be a guess frozen into a strict public
|
||||
contract.
|
||||
- **Naming the entrypoint `dashboard`.** Rejected because it names one prospective host and would
|
||||
bias the contract against standalone reuse — the exact coupling this ADR is trying to avoid.
|
||||
|
||||
## Consequences
|
||||
|
||||
A module author can now express the complete product they own, and the platform has one contract for
|
||||
discovering that a module offers presentation at all. Backend-only modules are unaffected beyond a
|
||||
one-character manifest version bump. The dependency graph stays flat and meaningful, and Dashboard
|
||||
organization stays an administrator's decision rather than an architectural one.
|
||||
|
||||
The cost is a declared contract with no consumer: `entrypoints.ui` can be validated but not yet
|
||||
hosted, exactly like `entrypoints.jobs`. That is deliberate — the static contract is established
|
||||
first so the runtime design in a later pass has a fixed, tested boundary to build against — but
|
||||
until that pass lands, declaring `ui` accomplishes nothing observable at runtime.
|
||||
|
||||
This ADR also creates a real, tracked tension with an enforced repository rule. `pnpm boundaries`
|
||||
(`scripts/check-boundaries.ts`, rule `modules-do-not-own-ui`) rejects UI *source* extensions
|
||||
(`.tsx`, `.vue`, `.css`, `.svg`, and others) and UI *framework dependencies* inside a module
|
||||
package. That rule is intentionally left enforced by this ADR: whether and how to relax it depends
|
||||
entirely on the UI authoring contract a later pass chooses, and weakening a working guard before
|
||||
that decision exists would trade a real protection for nothing. A module may therefore declare
|
||||
`entrypoints.ui` today only for a framework-neutral compiled artifact, and choosing the module UI
|
||||
authoring model — including exactly which parts of that deny list are relaxed, and what replaces
|
||||
them — is the first question the next pass must answer.
|
||||
|
||||
## Deferred
|
||||
|
||||
None of the following is implemented, designed, or promised by this ADR: the Dashboard application
|
||||
shell; a UI component library or design-token implementation; any React/Vue/Svelte or other
|
||||
framework choice; a module UI runtime, renderer, loader, or mount mechanism; navigation registration;
|
||||
Dashboard folders, groups, or grouping metadata; a standalone application host; a module-management
|
||||
UI; a module installer or marketplace; hot-loading or runtime unloading; automatic migration
|
||||
application; and any real feature module. The `entrypoints.ui` field is a static declaration only.
|
||||
@@ -29,3 +29,4 @@ ADRs capture durable choices that affect more than one package or phase.
|
||||
- [0018: Identity session resolution, canonical public origins, and PostgreSQL transport policy](0018-identity-session-resolution-and-database-transport.md) — Accepted
|
||||
- [0019: Durable authorization history and grant management](0019-durable-authorization-history-and-grant-management.md) — Accepted
|
||||
- [0020: Module runtime and migration execution hardening](0020-module-runtime-and-migration-execution-hardening.md) — Accepted
|
||||
- [0021: Modules as independently hostable products](0021-modules-as-hostable-products.md) — Accepted
|
||||
|
||||
@@ -4,10 +4,12 @@ Ekklesia is a long-lived platform from which churches can compose many applicati
|
||||
experiences. It begins as a modular monolith so contracts can stabilize before distribution is
|
||||
introduced.
|
||||
|
||||
> **Trust boundary.** Installed modules are trusted compiled backend code, not sandboxed
|
||||
> plugins: a module runs arbitrary JavaScript in the API process and shares its PostgreSQL
|
||||
> role. The layering described below is a correctness and ownership boundary, not hostile-code
|
||||
> isolation. See [Security model and deployment trust boundaries](../development/security.md).
|
||||
> **Trust boundary.** Installed modules are trusted compiled code, not sandboxed plugins: a module
|
||||
> runs arbitrary JavaScript in the API process and shares its PostgreSQL role. The layering
|
||||
> described below is a correctness and ownership boundary, not hostile-code isolation. A module that
|
||||
> ships its own UI ([ADR 0021](decisions/0021-modules-as-hostable-products.md)) is exactly as
|
||||
> trusted as one that does not. See
|
||||
> [Security model and deployment trust boundaries](../development/security.md).
|
||||
|
||||
## Dependency model
|
||||
|
||||
@@ -27,16 +29,26 @@ The four ownership layers are:
|
||||
|
||||
1. **Core** orchestrates generic platform infrastructure: startup, identity, permissions,
|
||||
configuration, database connectivity, storage, events, jobs, module discovery, and lifecycle.
|
||||
2. **Modules** own backend domain behavior, data schemas, resources, actions, routes, events,
|
||||
permissions, and jobs.
|
||||
3. **Surfaces** own every presentation concern: components, renderers, layout, navigation,
|
||||
accessibility, interaction, branding, and client state.
|
||||
2. **Modules** own domain behavior, data schemas, resources, actions, routes, events, permissions,
|
||||
jobs — and, per [ADR 0021](decisions/0021-modules-as-hostable-products.md), their own UI. A module
|
||||
is an independently versioned product, not only a backend capability. It does not own the
|
||||
containing application shell or any platform policy.
|
||||
3. **Surfaces** own the presentation concerns a module does not: the containing application shell,
|
||||
layout, navigation, accessibility, interaction, branding, and client state — including the shell
|
||||
that hosts a module's own UI.
|
||||
4. **Surface Builder** stores validated declarative composition. It selects capabilities and
|
||||
surface-owned renderers but contains neither executable code nor module business logic.
|
||||
|
||||
Modules do not own UI. For example, Calendar data, actions, and permissions will live in
|
||||
`modules/calendar`; Calendar renderers for a dashboard or public website will live in those
|
||||
surfaces.
|
||||
A module owns its own product experience. Calendar data, actions, permissions, and its own
|
||||
management UI live in `modules/calendar`; the dashboard or public-website shell that hosts it —
|
||||
navigation, theme, layout, and platform chrome — lives in a surface. The module supplies the screen
|
||||
for its feature; the surface supplies the application around it.
|
||||
|
||||
Modules are flat peers. `video`, `livestream`, `announcements`, and `calendar` are independent
|
||||
first-class modules with no parent/child relationship. A Dashboard may group them into folders such
|
||||
as `Media` or `Sunday Production`, but those groups are presentation configuration an administrator
|
||||
can reorganize, never module ownership and never a dependency. Genuine module and capability
|
||||
dependencies remain the only way one module may require another.
|
||||
|
||||
## Stable seams
|
||||
|
||||
@@ -151,7 +163,21 @@ blocks Phase 6. Read
|
||||
does and does not protect against, and the repository root
|
||||
[README](../../README.md#current-status) for the current status of every phase.
|
||||
|
||||
The module platform's backend foundations are now in place and Phase 6 Surface work is unblocked.
|
||||
**Phase 6A establishes the module product contract.**
|
||||
[ADR 0021](decisions/0021-modules-as-hostable-products.md) replaces the backend-only module model:
|
||||
a module is an independently versioned product that may own its own UI, hostable inside Ekklesia and
|
||||
— later, from the same implementation — as a standalone application. Core keeps the host shell and
|
||||
every platform policy, the module graph stays flat, and Dashboard grouping stays presentation
|
||||
configuration.
|
||||
|
||||
What this pass actually implements is the **static contract only**: `manifestVersion: 4` adds one
|
||||
optional `entrypoints.ui` field, validated during discovery exactly like the server and schema
|
||||
entrypoints — package-relative, inside the package, compiled ESM. Discovery remains inert; nothing
|
||||
imports, mounts, or renders that entrypoint. The Dashboard shell, the UI runtime, the standalone
|
||||
host, module UI authoring (including the still-enforced `modules-do-not-own-ui` boundary rule), and
|
||||
every feature module remain future work. Restart-based discovery, explicit operator-invoked
|
||||
migrations, and the trusted compiled-module model are all unchanged.
|
||||
|
||||
Target architecture not yet implemented includes production approval-provider
|
||||
composition, physical storage-bucket provisioning, jobs/workflow automation, richer public resource
|
||||
and action contracts, a Dashboard, a Builder, and reference feature modules; none of these are
|
||||
|
||||
@@ -7,7 +7,7 @@ These rules map platform ownership to filesystem and package boundaries. The exe
|
||||
| --- | --- | --- |
|
||||
| `apps/*` | Thin deployable composition roots | Public Core APIs, generated bootstrap, or a surface as required by the app |
|
||||
| `core` | Feature-agnostic platform implementations | Public contracts and external infrastructure libraries |
|
||||
| `modules/*` | Backend domain capability and data | Module SDK, public contracts, and external backend libraries |
|
||||
| `modules/*` | Domain capability, data, and (per ADR 0021) the module's own UI | Module SDK, public contracts, and external backend libraries |
|
||||
| `surfaces/*` | UI, renderers, navigation, and interaction | Surface SDK, API/auth clients, public contracts, and design packages |
|
||||
| `builders/*` | Declarative surface composition tooling | Builder/surface SDKs and public contracts |
|
||||
| `packages/*` | Explicitly scoped SDKs, contracts, clients, design, testing, or tooling | Only dependencies appropriate to the package's declared level |
|
||||
@@ -19,7 +19,12 @@ These rules map platform ownership to filesystem and package boundaries. The exe
|
||||
- A module never imports Core implementations, another module, a surface, Builder code, or an app.
|
||||
- A module cannot contain presentation source, including JSX/TSX, single-file components,
|
||||
Astro/Marko/MDX/HTML views, stylesheets, or SVG presentation assets, and cannot depend on UI
|
||||
frameworks or shared UI-role packages. Modules expose data and behavior only.
|
||||
frameworks or shared UI-role packages. **This rule is still enforced, but it is no longer the
|
||||
architectural direction.** [ADR 0021](decisions/0021-modules-as-hostable-products.md) establishes
|
||||
that a module may own its own UI and declare a compiled `entrypoints.ui` artifact. The deny list
|
||||
above stays in force until a later Phase 6 pass chooses the module UI authoring model and decides
|
||||
what replaces it; until then a module UI must be a framework-neutral compiled artifact. Core still
|
||||
owns the containing application shell and all platform policy.
|
||||
- A surface never imports Core or a feature module. It uses public API/client contracts.
|
||||
- Separate surfaces do not import each other's internals.
|
||||
- Builder code composes contracts and surface renderer identifiers; it does not import module or
|
||||
@@ -39,7 +44,8 @@ The boundary test suite includes intentionally invalid fixture graphs. This prov
|
||||
rejects every currently machine-checkable repository rule even before production packages fill
|
||||
every directory. The UI guard rejects component, view, template, stylesheet, shared UI-role, and
|
||||
common UI-framework dependencies inside modules. This deny list is defense in depth; package
|
||||
review remains required when adopting a new frontend technology.
|
||||
review remains required when adopting a new frontend technology — and revisiting the guard itself is
|
||||
an explicit prerequisite of the module UI authoring pass (ADR 0021).
|
||||
|
||||
The Phase 3 schema validator complements source boundaries by checking table ownership, foreign-key
|
||||
targets, and physical table namespaces in compiled Drizzle metadata. Offline migration validation
|
||||
|
||||
Reference in New Issue
Block a user