feat: standalone module-build readiness for the public @ekklesia packages (ADR 0053) #79

Merged
Zayin_Admin merged 2 commits from feat/standalone-module-build into main 2026-10-08 12:48:07 +00:00
Member

Implements the standalone module-build readiness slice of ADR 0053. The public @ekklesia/* module-development packages can now be used from a module repository outside this monorepo. Nothing is published: no registry credentials, no Signage changes, and no change to the Module Package format. ADR 0053 stays Proposed.

What changes

Curated @ekklesia/module-sdk

  • The SDK exposes an explicit author API instead of re-exporting the @ekklesia/contracts barrel. That covers runtime definition, HTTP routes, principals, configuration, storage, realtime, and manifest/migration validation for a module's own tests.
  • It does not expose identity or authorization internals, Drizzle, or the legacy ModuleDatabase type. A module with no database sees services.database as never.
  • defineModuleRuntime now returns the SDK's own named interfaces. Before, a module that emits declarations failed with TS2883, because the inferred type reached into @ekklesia/contracts. The standalone fixture found this.
  • defineModuleSchema is removed. createModuleTable moves to @ekklesia/module-database/drizzle, and defineModuleSchemaMetadata is new in @ekklesia/module-database for modules that use no ORM.

Drizzle

  • drizzle-orm is no longer a dependency or peer of module-sdk or contracts. It stays public only as an optional peer of @ekklesia/module-database/drizzle.
  • @ekklesia/contracts contains no Drizzle import or declaration reference (aaa7ebf):
    • The legacy facade's types (ModuleDatabase, LegacyModuleRuntimeServices, AnyModuleRuntimeServices) moved to core/src/modules/database.ts, next to the facade Core builds. The facade itself is unchanged.
    • Contracts' runtime, route and services generics now take a plain database parameter that defaults to the ABI v1 executor. The legacy definition's database is opaque until Core specializes it.
  • The legacy Drizzle schema validator (ADR 0007) moved from contracts into Core: core/src/modules/legacy-schema-definition.ts, next to Core's own Drizzle dependency. Core's legacy runtime support is unchanged.
    • pnpm boundaries caught the reason: Core loads that validator at run time, so it cannot sit behind an optional peer in the production image.
    • The verifier's probe imports the validator as a .ts file, so the probe's source still runs directly under Node in tests. Core enables rewriteRelativeImportExtensions, which rewrites that import to .js in the build.

React

  • @ekklesia/ui takes react as a peer plus a devDependency. ADR 0023's bundling is unchanged.
  • runtime-imports-are-production-dependencies allows exactly this one consumer-selected peer, next to module-database's Drizzle peer. @ekklesia/ui is only ever bundled into browser artifacts; no production Node.js process loads it.
  • No public package imports react-dom.

Publishability

  • The nine public packages carry "ekklesia": { "distribution": "module-development" }. Their monorepo-root prepack scripts are removed.
  • typescript-config is now publishable. module-ui-host is now private.
  • module-pack carries its own copy of LICENSE, identical to the repository's, and a test keeps them equal. pnpm adds the workspace LICENSE only when no packed path matches /LICEN[CS]E/i, and module-pack's licenses/ directory matches.

Packager

  • The Node.js range comes from the module's own engines.node. A missing range fails with MODULE_PACK_RUNTIME_UNKNOWN. The packager no longer reads pnpm-workspace.yaml, and no CLI override was needed.
  • An Ekklesia package is recognised by an @ekklesia/ name and the distribution marker, never by its path. Registry-installed packages are therefore recorded under incorporates, still checked against moduleSdk, and never credited as third-party. A package that only borrows the scope, or only the marker, is third-party.

Release-check tooling

  • pnpm packages:pack (scripts/pack-public-packages.ts) removes each public package's dist, rebuilds without the Turbo cache, packs, and inspects each tarball. Allowed content:

    • only files entries, package.json, and LICENSE;
    • built output only where a source file produced it (stale output fails).

    The packed manifest must have:

    • no workspace: range, no private field, and no install-time lifecycle script;
    • the distribution marker;
    • the coordinated version, with every installed @ekklesia dependency on that exact version;
    • no reference to a private package.

    No file may name a local path.

  • pnpm test:standalone-module (scripts/test-standalone-module.sh, new CI job standalone-module) installs the packed tarballs into tests/fixtures/standalone-module, copied outside the repository, then proves it end to end (details below).

Docs

  • docs/module-development/README.md gains "Developing a module in its own repository". It covers:

    • dependencies vs devDependencies, with the exact module-pack pin;
    • engines.node;
    • allowBuilds: esbuild;
    • not cloning into modules/;
    • contracts is not a direct dependency.

    It is marked "registry publication is the next slice".

  • The schema and legacy-database sections, repository-boundaries.md, and the README status are updated to match.

Evidence

  • pnpm verify passes. 2,555 tests ran, including:

    • new public-package boundary tests;
    • packager tests for engines.node, registry-style node_modules identity, the moduleSdk check, and the scope squatter;
    • an SDK named-type test;
    • Core's legacy schema validator tests.
  • pnpm test:migrations:integration passes: 396 tests, because Core's schema-discovery imports changed.

  • pnpm packages:pack packs all 9 tarballs and every inspection passes.

  • pnpm test:standalone-module: the fixture is an ABI v1 module with no ORM, using sql and defineModuleSchemaMetadata, plus a React UI using @ekklesia/ui and design tokens. It extends @ekklesia/typescript-config, declares engines.node, and pins module-pack exactly. From local tarballs, with no workspace link and no Ekklesia registry, it:

    • installs with no drizzle-orm present and exactly one react, shared by @ekklesia/ui;
    • typechecks with skipLibCheck: false, so every published declaration is checked, and builds, including the UI build. Against the earlier contracts tarball, the same check fails with TS2307 on drizzle-orm/node-postgres;
    • packs with no workspace file present;
    • verifies with Core's verifier;
    • records incorporates for all six bundled @ekklesia packages at 0.1.2;
    • credits no @ekklesia package and exactly one React in its notices, and leaks no local path.

    Two runs from different temporary directories produced the same archive sha256.

Breaking for existing modules

  • A module that imports defineModuleSchema, createModuleTable, or contracts internals from @ekklesia/module-sdk, or that relies on the SDK typing the legacy Drizzle facade, must move to the APIs above. Core still runs legacy modules unchanged.
  • The Signage migration plan already moves it to ABI v1. Signage is untouched here.

Not in this PR

  • Registry provisioning and publication.
  • The release workflow that publishes after the tarball smoke.
  • Signing and provenance.

These are the reasons ADR 0053 remains Proposed.

🤖 Generated with Claude Code

Implements the standalone module-build readiness slice of [ADR 0053](docs/architecture/decisions/0053-standalone-module-development-packages.md). The public `@ekklesia/*` module-development packages can now be used from a module repository outside this monorepo. **Nothing is published**: no registry credentials, no Signage changes, and no change to the Module Package format. ADR 0053 stays **Proposed**. ## What changes **Curated `@ekklesia/module-sdk`** - The SDK exposes an explicit author API instead of re-exporting the `@ekklesia/contracts` barrel. That covers runtime definition, HTTP routes, principals, configuration, storage, realtime, and manifest/migration validation for a module's own tests. - It does not expose identity or authorization internals, Drizzle, or the legacy `ModuleDatabase` type. A module with no database sees `services.database` as `never`. - `defineModuleRuntime` now returns the SDK's own named interfaces. Before, a module that emits declarations failed with TS2883, because the inferred type reached into `@ekklesia/contracts`. The standalone fixture found this. - `defineModuleSchema` is removed. `createModuleTable` moves to `@ekklesia/module-database/drizzle`, and `defineModuleSchemaMetadata` is new in `@ekklesia/module-database` for modules that use no ORM. **Drizzle** - `drizzle-orm` is no longer a dependency or peer of `module-sdk` or `contracts`. It stays public only as an optional peer of `@ekklesia/module-database/drizzle`. - `@ekklesia/contracts` contains no Drizzle import or declaration reference (aaa7ebf): - The legacy facade's types (`ModuleDatabase`, `LegacyModuleRuntimeServices`, `AnyModuleRuntimeServices`) moved to `core/src/modules/database.ts`, next to the facade Core builds. The facade itself is unchanged. - Contracts' runtime, route and services generics now take a plain database parameter that defaults to the ABI v1 executor. The legacy definition's database is opaque until Core specializes it. - The legacy Drizzle schema validator (ADR 0007) moved from `contracts` into Core: `core/src/modules/legacy-schema-definition.ts`, next to Core's own Drizzle dependency. Core's legacy runtime support is unchanged. - `pnpm boundaries` caught the reason: Core loads that validator at run time, so it cannot sit behind an optional peer in the production image. - The verifier's probe imports the validator as a `.ts` file, so the probe's source still runs directly under Node in tests. Core enables `rewriteRelativeImportExtensions`, which rewrites that import to `.js` in the build. **React** - `@ekklesia/ui` takes `react` as a peer plus a devDependency. ADR 0023's bundling is unchanged. - `runtime-imports-are-production-dependencies` allows exactly this one consumer-selected peer, next to `module-database`'s Drizzle peer. `@ekklesia/ui` is only ever bundled into browser artifacts; no production Node.js process loads it. - No public package imports `react-dom`. **Publishability** - The nine public packages carry `"ekklesia": { "distribution": "module-development" }`. Their monorepo-root `prepack` scripts are removed. - `typescript-config` is now publishable. `module-ui-host` is now `private`. - `module-pack` carries its own copy of `LICENSE`, identical to the repository's, and a test keeps them equal. pnpm adds the workspace LICENSE only when no packed path matches `/LICEN[CS]E/i`, and module-pack's `licenses/` directory matches. **Packager** - The Node.js range comes from the module's own `engines.node`. A missing range fails with `MODULE_PACK_RUNTIME_UNKNOWN`. The packager no longer reads `pnpm-workspace.yaml`, and no CLI override was needed. - An Ekklesia package is recognised by an `@ekklesia/` name **and** the distribution marker, never by its path. Registry-installed packages are therefore recorded under `incorporates`, still checked against `moduleSdk`, and never credited as third-party. A package that only borrows the scope, or only the marker, is third-party. **Release-check tooling** - `pnpm packages:pack` (`scripts/pack-public-packages.ts`) removes each public package's `dist`, rebuilds without the Turbo cache, packs, and inspects each tarball. Allowed content: - only `files` entries, `package.json`, and `LICENSE`; - built output only where a source file produced it (stale output fails). The packed manifest must have: - no `workspace:` range, no `private` field, and no install-time lifecycle script; - the distribution marker; - the coordinated version, with every installed `@ekklesia` dependency on that exact version; - no reference to a private package. No file may name a local path. - `pnpm test:standalone-module` (`scripts/test-standalone-module.sh`, new CI job `standalone-module`) installs the packed tarballs into `tests/fixtures/standalone-module`, copied outside the repository, then proves it end to end (details below). **Docs** - `docs/module-development/README.md` gains "Developing a module in its own repository". It covers: - dependencies vs devDependencies, with the exact `module-pack` pin; - `engines.node`; - `allowBuilds: esbuild`; - not cloning into `modules/`; - `contracts` is not a direct dependency. It is marked "registry publication is the next slice". - The schema and legacy-database sections, `repository-boundaries.md`, and the README status are updated to match. ## Evidence - `pnpm verify` passes. 2,555 tests ran, including: - new public-package boundary tests; - packager tests for `engines.node`, registry-style `node_modules` identity, the `moduleSdk` check, and the scope squatter; - an SDK named-type test; - Core's legacy schema validator tests. - `pnpm test:migrations:integration` passes: 396 tests, because Core's schema-discovery imports changed. - `pnpm packages:pack` packs all 9 tarballs and every inspection passes. - `pnpm test:standalone-module`: the fixture is an ABI v1 module with no ORM, using `sql` and `defineModuleSchemaMetadata`, plus a React UI using `@ekklesia/ui` and design tokens. It extends `@ekklesia/typescript-config`, declares `engines.node`, and pins `module-pack` exactly. From local tarballs, with no workspace link and no Ekklesia registry, it: - installs with no `drizzle-orm` present and exactly one `react`, shared by `@ekklesia/ui`; - typechecks with `skipLibCheck: false`, so every published declaration is checked, and builds, including the UI build. Against the earlier contracts tarball, the same check fails with TS2307 on `drizzle-orm/node-postgres`; - packs with no workspace file present; - verifies with Core's verifier; - records `incorporates` for all six bundled `@ekklesia` packages at 0.1.2; - credits no `@ekklesia` package and exactly one React in its notices, and leaks no local path. Two runs from different temporary directories produced the same archive sha256. ## Breaking for existing modules - A module that imports `defineModuleSchema`, `createModuleTable`, or contracts internals from `@ekklesia/module-sdk`, or that relies on the SDK typing the legacy Drizzle facade, must move to the APIs above. Core still *runs* legacy modules unchanged. - The Signage migration plan already moves it to ABI v1. Signage is untouched here. ## Not in this PR - Registry provisioning and publication. - The release workflow that publishes after the tarball smoke. - Signing and provenance. These are the reasons ADR 0053 remains Proposed. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ekklesia-bot added 1 commit 2026-10-08 12:29:25 +00:00
feat: standalone module-build readiness for the public @ekklesia packages (ADR 0053)
CI / verify (pull_request) Successful in 1m2s
CI / infrastructure-smoke (pull_request) Successful in 28s
CI / migration-smoke (pull_request) Successful in 1m52s
CI / standalone-module (pull_request) Successful in 26s
CI / image-build (pull_request) Successful in 30s
CI / deploy-smoke (same-origin) (pull_request) Successful in 50s
CI / deploy-smoke (split) (pull_request) Successful in 1m42s
89d5d0f2da
Makes the public module-development package set usable from a module
repository outside the monorepo. Nothing is published.

- Curated @ekklesia/module-sdk: an explicit author API, no contracts
  barrel, no identity/authorization internals, no Drizzle and no legacy
  ModuleDatabase type. defineModuleRuntime returns the SDK's own named
  interfaces so a module's emitted declarations never reach into
  @ekklesia/contracts. defineModuleSchema is gone; createModuleTable and
  defineModuleSchemaMetadata live in @ekklesia/module-database.
- Drizzle: no longer a dependency of module-sdk or contracts. Core keeps
  the legacy Drizzle schema validator (moved to core/src/modules) and
  its legacy runtime support; drizzle-orm is an optional peer only.
- React is a peer (plus devDependency) of @ekklesia/ui; the boundary
  checker allows exactly that consumer-selected peer.
- @ekklesia/typescript-config is publishable; module-ui-host is private.
  Every public package carries "ekklesia.distribution":
  "module-development" and no monorepo-root prepack.
- module-pack reads the Node.js range from the module's own engines.node
  and recognizes Ekklesia packages by name plus that marker, so
  registry-installed packages are incorporated, SDK-compatibility
  checked, and never credited as third-party.
- scripts/pack-public-packages.ts (pnpm packages:pack) rebuilds, packs
  and inspects the nine public tarballs; scripts/test-standalone-module.sh
  (pnpm test:standalone-module, new CI job) installs them into a fixture
  module outside the repository and proves it typechecks, builds,
  packages, and verifies.
- Public-package boundary tests and module-development docs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Zayin_Admin added 1 commit 2026-10-08 12:40:46 +00:00
fix: keep Drizzle out of every published declaration (ADR 0053)
CI / verify (pull_request) Successful in 1m2s
CI / infrastructure-smoke (pull_request) Successful in 37s
CI / migration-smoke (pull_request) Successful in 1m52s
CI / standalone-module (pull_request) Successful in 26s
CI / image-build (pull_request) Successful in 28s
CI / deploy-smoke (same-origin) (pull_request) Successful in 43s
CI / deploy-smoke (split) (pull_request) Successful in 1m30s
aaa7ebfd84
@ekklesia/contracts still emitted a type-only import of
drizzle-orm/node-postgres for the legacy ModuleDatabase facade, so a
standalone module type-checked only because skipLibCheck hid it.

- ModuleDatabase, LegacyModuleRuntimeServices and
  AnyModuleRuntimeServices move to core/src/modules/database.ts, beside
  the facade Core builds; the facade itself is unchanged.
- contracts' runtime, route, and services generics no longer name an
  ORM: the database type is a plain parameter defaulting to the ABI v1
  executor, and the legacy definition's database is opaque until Core
  specializes it. Core and apps/api name the legacy types from Core.
- contracts drops its drizzle-orm optional peer and devDependency;
  Drizzle stays public only through @ekklesia/module-database/drizzle.
- The facade's type-level test moves from contracts to Core.
- Regression proof: the standalone fixture type-checks with
  skipLibCheck: false against the packed tarballs, with no drizzle-orm
  installed (the smoke asserts both). Against the previous contracts
  tarball the same check fails with TS2307 on drizzle-orm/node-postgres.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Zayin_Admin merged commit 7ea2d7883f into main 2026-10-08 12:48:07 +00:00
Zayin_Admin deleted branch feat/standalone-module-build 2026-10-08 12:48:07 +00:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Zayin/Ekklesia#79