feat: add module contracts and discovery
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
# 0005: Static module manifests and atomic discovery
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-07-12
|
||||
|
||||
## Context
|
||||
|
||||
Ekklesia modules must be installable by placing a package in `modules/` without editing Core or a
|
||||
central registry. Core must validate identity, compatibility, dependencies, capabilities, storage
|
||||
requirements, and entrypoints before it runs module behavior. Importing an executable TypeScript
|
||||
manifest would allow top-level module code to run before validation completed.
|
||||
|
||||
## Decision
|
||||
|
||||
Every module uses one inert, versioned `ekklesia.module.json` manifest. `@ekklesia/contracts` owns
|
||||
the authoritative Zod schema and serializable types. `@ekklesia/module-sdk` re-exports those public
|
||||
contracts and provides `defineModule` for typed programmatic validation without becoming a Core
|
||||
service locator.
|
||||
|
||||
Core scans immediate non-hidden directories under the configured modules directory. Discovery
|
||||
validates all manifests and package metadata, resolves entrypoints without loading them, checks
|
||||
platform and Module SDK compatibility, resolves module and capability dependencies, and produces a
|
||||
deterministic dependency-first order. Any error rejects the entire discovery result with sorted,
|
||||
structured issues; no module server code is partially loaded.
|
||||
|
||||
Storage requirements are declarative. A module may request one or more logical buckets in its
|
||||
manifest, but it cannot create provider clients or select physical bucket names. The later Core
|
||||
storage service will provision deployment-prefixed physical buckets and give the owning module
|
||||
scoped handles. Buckets remain private unless a future explicit storage contract says otherwise.
|
||||
|
||||
## Alternatives
|
||||
|
||||
- Executable `module.ts` manifests were rejected because discovery would cross the code-execution
|
||||
boundary before validation.
|
||||
- Embedding the full manifest in `package.json` was rejected because it couples the platform
|
||||
contract to package-manager metadata and makes the manifest harder to version independently.
|
||||
- A manually maintained import registry was rejected because every module installation would
|
||||
require a central source edit.
|
||||
- Letting modules configure SeaweedFS or S3 clients directly was rejected because Core owns
|
||||
credentials, provider selection, isolation, and lifecycle.
|
||||
|
||||
## Consequences
|
||||
|
||||
JSON manifests do not provide TypeScript inference by themselves, so validation commands and clear
|
||||
path-aware errors are part of the authoring workflow. A JSON Schema can be generated from the
|
||||
authoritative contract later for editor completion.
|
||||
|
||||
Discovery currently proves entrypoint existence but does not import server behavior, execute
|
||||
lifecycle hooks, provision requested buckets, or register routes, jobs, schemas, and migrations.
|
||||
Those operations begin only after a complete valid discovery result in later runtime slices.
|
||||
|
||||
## Migration implications
|
||||
|
||||
The manifest includes `manifestVersion: 1`. Future incompatible shapes require an explicit schema
|
||||
version and migration path rather than silently changing existing module interpretation.
|
||||
@@ -13,3 +13,4 @@ ADRs capture durable choices that affect more than one package or phase.
|
||||
- [0002: pnpm workspace and strict TypeScript](0002-pnpm-workspace-and-typescript.md) — Accepted
|
||||
- [0003: Development infrastructure](0003-development-infrastructure.md) — Accepted
|
||||
- [0004: Replace unmaintained MinIO baseline](0004-replace-unmaintained-minio.md) — Accepted
|
||||
- [0005: Static module manifests and atomic discovery](0005-static-module-manifests.md) — Accepted
|
||||
|
||||
@@ -56,11 +56,14 @@ surfaces.
|
||||
- Private objects use signed access and namespaced storage paths.
|
||||
- Every network service starts from least-privilege and local-only development defaults.
|
||||
|
||||
## Phase 1 status
|
||||
## Current implementation status
|
||||
|
||||
Implemented now: monorepo/tooling, strict TypeScript policy, repository-boundary validation,
|
||||
tests, base CI, PostgreSQL, and a development S3-compatible object store.
|
||||
Implemented now: the Phase 1 repository and infrastructure foundation plus public module contracts,
|
||||
the Module SDK manifest API, static versioned module manifests, automatic filesystem discovery,
|
||||
compatibility/dependency ordering, structured discovery failures, and a backend-only module
|
||||
template. Core can validate zero or more modules without importing their server behavior.
|
||||
|
||||
Target architecture, not yet implemented: Core runtime, module discovery and lifecycle, Drizzle
|
||||
schema aggregation/migrations, Better Auth, permissions, storage service, Module SDK, Surface
|
||||
SDK, capability/resource/action registries, API server, dashboard, Builder, and reference modules.
|
||||
Target architecture, not yet implemented: lifecycle execution and runtime registration, Drizzle
|
||||
schema aggregation/migrations, Better Auth, permissions, physical storage-bucket provisioning,
|
||||
Surface SDK, capability/resource/action registries, API server, dashboard, Builder, and reference
|
||||
feature modules.
|
||||
|
||||
@@ -18,6 +18,7 @@ pnpm install --frozen-lockfile
|
||||
pnpm format:check # formatting without mutation
|
||||
pnpm lint # Biome recommended rules
|
||||
pnpm boundaries # architectural directions, UI ownership, and cycles
|
||||
pnpm modules:validate # installed module manifests, compatibility, and dependency graph
|
||||
pnpm typecheck # strict TypeScript
|
||||
pnpm test # Vitest
|
||||
pnpm build # Turborepo package build graph
|
||||
@@ -28,8 +29,12 @@ The lint, typecheck, and test commands run both repository-root checks and every
|
||||
package-level Turborepo task. New packages add their own `lint`, `typecheck`, and `test` scripts;
|
||||
the root verification command discovers them automatically.
|
||||
|
||||
CI runs `pnpm ci`, which adds Compose configuration validation to `pnpm verify`. A separate CI job
|
||||
runs the live infrastructure smoke test so code-only local verification does not require Docker.
|
||||
`pnpm modules:validate` scans `modules/*` without a central registry. A missing or empty modules
|
||||
directory is valid and reports zero installed modules.
|
||||
|
||||
CI runs `pnpm ci:verify`, which adds Compose configuration validation to `pnpm verify`. A separate
|
||||
CI job runs the live infrastructure smoke test so code-only local verification does not require
|
||||
Docker. The explicit suffix avoids pnpm's built-in `ci` install command.
|
||||
|
||||
## Development infrastructure
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# Module development
|
||||
|
||||
An Ekklesia module is a backend capability package. It owns business behavior and data declarations;
|
||||
it never owns components, styles, layouts, renderers, navigation, or other UI.
|
||||
|
||||
## Required package shape
|
||||
|
||||
Each immediate child of `modules/` must contain:
|
||||
|
||||
```text
|
||||
modules/example/
|
||||
├── ekklesia.module.json
|
||||
├── package.json
|
||||
└── src/server/index.ts
|
||||
```
|
||||
|
||||
`package.json` must use ESM, a strict semantic version, a unique package name, and:
|
||||
|
||||
```json
|
||||
{
|
||||
"ekklesia": {
|
||||
"layer": "module"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Its version must match the module manifest version. Internal package dependencies use `workspace:`
|
||||
ranges and supported package entrypoints.
|
||||
|
||||
## Manifest
|
||||
|
||||
The canonical manifest is inert JSON so discovery can validate a module without executing it:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifestVersion": 1,
|
||||
"id": "calendar",
|
||||
"name": "Calendar",
|
||||
"version": "0.1.0",
|
||||
"compatibility": {
|
||||
"platform": "^0.1.0",
|
||||
"moduleSdk": "^0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"modules": {
|
||||
"required": {
|
||||
"people": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"optional": ["media.items"]
|
||||
}
|
||||
},
|
||||
"capabilities": ["calendar.events"],
|
||||
"permissions": ["calendar.events.read"],
|
||||
"storage": {
|
||||
"buckets": [
|
||||
{
|
||||
"id": "assets",
|
||||
"description": "Private event assets owned by Calendar."
|
||||
}
|
||||
]
|
||||
},
|
||||
"entrypoints": {
|
||||
"server": "./src/server/index.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Module, capability, and permission identifiers are lowercase and namespaced. Entrypoints must begin
|
||||
with `./`, stay inside the package, and exist. Unknown manifest properties are rejected, including
|
||||
presentation fields such as components or renderers.
|
||||
|
||||
Storage bucket IDs are logical names scoped to the module. Modules may request multiple buckets.
|
||||
Core will later map them to deployment-specific physical S3 buckets and provide scoped access;
|
||||
module code must not configure SeaweedFS or other S3 clients directly.
|
||||
|
||||
## Discovery behavior
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm modules:validate
|
||||
```
|
||||
|
||||
Discovery checks every installed module atomically. It rejects malformed manifests, duplicate IDs
|
||||
or package names, version mismatches, incompatible platform or SDK ranges, missing required modules
|
||||
or capabilities, incompatible installed dependencies, cycles, and missing or escaping entrypoints.
|
||||
Missing optional dependencies are allowed. Valid modules are ordered dependency-first with stable
|
||||
identifier sorting.
|
||||
|
||||
Discovery does not load server code. Lifecycle execution, route/job registration, Drizzle schema
|
||||
aggregation and migrations, and physical bucket provisioning are deliberately deferred.
|
||||
|
||||
## Start from the template
|
||||
|
||||
Copy `templates/module` to `modules/<your-module-id>` and follow its README. The template is part of
|
||||
the workspace and is validated by the same contracts and discovery implementation as installed
|
||||
modules.
|
||||
Reference in New Issue
Block a user