feat: add the minimal operational module lifecycle
Ekklesia had no notion of a module being on or off: the accepted module set was exactly "whatever discovery found on disk", and `ModuleLifecycleStage` named nine states nothing produced or consumed. This replaces that vocabulary with two axes that real code produces — persistent, administrator-owned configuration (`enabled`/`disabled`) and per-boot runtime observation (`running`/`disabled`/`blocked`) — and wires them through startup. - One Core table (`ekklesia_core_module_state`, migration 0004) holding only `module_id` and `enabled`. A module with no record is enabled by default, so deploying a module is still sufficient to run it and discovery never needs an initialization write. A record for an undeployed module is retained and ignored, so redeploying restores the decision. - `resolveModuleEligibility` is pure: a disabled module transitively blocks every module that *requires* it (module and capability edges alike) with a structured reason naming the provider; optional dependencies never block; and a missing, incompatible, or cyclic dependency stays discovery's concern. - `startApi` starts only the eligible set and builds the UI publication registry from that same list, so routes, artifacts, browser inventory, and Dashboard navigation all follow one authoritative set. A disabled module is never imported, so there is no per-request enablement check anywhere. - Startup stays all-or-nothing, deliberately; there is consequently no `failed` runtime state. `pnpm modules:enable`/`modules:disable` is the out-of-band recovery path for a module that breaks startup. - Changes take effect at the next start, and the read model reports configuration, runtime, and `restartRequired` separately rather than claiming a module stopped when it has not. - Migration participation and permission composition still span the deployed and discovered inventories respectively, unchanged. - New restricted platform permission `core.modules.lifecycle.manage` gates both reading and changing enablement, at a sibling prefix (`/api/v1/module-lifecycle`) that cannot collide with the module route namespace. - `pnpm doctor` gains a `modules.lifecycle` check so "why is this module unavailable?" is answerable without reading logs. See ADR 0028. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
db67cf7700
commit
7729c99e24
@@ -9,13 +9,18 @@ import {
|
||||
} from "@ekklesia/core/dashboard";
|
||||
import { createValidatedPostgresPool, type PostgresPoolOptions } from "@ekklesia/core/database";
|
||||
import { createIdentityDrizzleView, type IdentityDrizzleDatabase } from "@ekklesia/core/identity";
|
||||
import { createModuleDatabaseView } from "@ekklesia/core/modules";
|
||||
import {
|
||||
createModuleDatabaseView,
|
||||
createModuleLifecycleView,
|
||||
type ModuleLifecycleDrizzleDatabase,
|
||||
} from "@ekklesia/core/modules";
|
||||
|
||||
export interface ApiDatabaseHandle {
|
||||
readonly authorization: AuthorizationDrizzleDatabase;
|
||||
readonly dashboard: DashboardDrizzleDatabase;
|
||||
readonly identity: IdentityDrizzleDatabase;
|
||||
readonly module: ModuleDatabase;
|
||||
readonly moduleLifecycle: ModuleLifecycleDrizzleDatabase;
|
||||
/** Closes the one shared pool exactly once, regardless of how many typed views were created
|
||||
* over it. Safe to call from both a startup-failure cleanup path and normal shutdown. */
|
||||
readonly close: () => Promise<void>;
|
||||
@@ -24,9 +29,10 @@ export interface ApiDatabaseHandle {
|
||||
/**
|
||||
* Composes the one physical PostgreSQL pool this process uses for Core's identity and
|
||||
* authorization schemas plus the module database facade: one validated, timeout-bounded
|
||||
* `pg.Pool` (`createValidatedPostgresPool`, `@ekklesia/core/database`), wrapped by three
|
||||
* independent typed Drizzle views (`createIdentityDrizzleView`, `createAuthorizationDrizzleView`,
|
||||
* `createModuleDatabaseView`). No domain owns or closes the pool in this composition — `close()`
|
||||
* `pg.Pool` (`createValidatedPostgresPool`, `@ekklesia/core/database`), wrapped by independent
|
||||
* typed Drizzle views (`createIdentityDrizzleView`, `createAuthorizationDrizzleView`,
|
||||
* `createDashboardDrizzleView`, `createModuleLifecycleView`, `createModuleDatabaseView`). No domain
|
||||
* owns or closes the pool in this composition — `close()`
|
||||
* here is the only thing that ever calls `pool.end()`. `createIdentityDrizzleDatabase`/
|
||||
* `createAuthorizationDrizzleDatabase` remain available, unaffected, and independently testable
|
||||
* for callers that only need one domain.
|
||||
@@ -43,5 +49,6 @@ export function createApiDatabase(
|
||||
dashboard: createDashboardDrizzleView(pool),
|
||||
identity: createIdentityDrizzleView(pool),
|
||||
module: createModuleDatabaseView(pool),
|
||||
moduleLifecycle: createModuleLifecycleView(pool),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user