Moves Signage from the legacy module contract to Ekklesia 0.2.0's: manifest v8, Module Database ABI v1, the @ekklesia/module-database Drizzle adapter, plain schema metadata, and the curated module SDK. This is the first of the slices. Signage still builds inside the pinned platform workspace (ci/assemble.sh). No behaviour, schema or data change.
What changes
Manifest v8.manifestVersion: 8 and database: { abi: 1 }. Compatibility is now ^0.2.0 for both platform and module SDK, which is what CI tests against. Identity, permissions, configuration, storage, entrypoints and the display declaration are unchanged. The runtime declares the same ABI, because Core refuses a mismatch.
Database ABI v1.
createSignageStore(executor) now takes the platform's ABI v1 executor and binds moduleDrizzle to it once, so none of the 12 call sites change.
The four drizzle*.ts stores are now typed ModuleDrizzleDatabase / ModuleDrizzleTransaction instead of the removed legacy ModuleDatabase.
Query code is unchanged.
Schema metadata.src/schema/index.ts now exports only moduleDrizzleSchemaMetadata("signage", …): plain table names and foreign-key targets. The Drizzle table re-exports and defineModuleSchema are gone, so no Drizzle object reaches Core. The declarations in tables.ts are untouched (only its header comment changed).
Duplicate-image bug fix.isUniqueViolation used to check error.code === "23505", but Drizzle wraps every executor failure in DrizzleQueryError, with the real error on .cause. So the check never matched, and a duplicate-upload race returned a 500 instead of the content-hash conflict. It now reads findEkklesiaDatabaseError(error)?.sqlState === "23505".
Migrations. No new migration. 0000_initial.sql and its pinned checksum (fbbc448f…) are unchanged.
Version. Stays 0.1.2. By the repository's convention, versions move only in a chore: release signage X PR.
Temporary CI changes (deleted in the next PR)
ci/platform.env now pins v0.2.0 (1788d17), and ci/pnpm-lock.yaml is regenerated with ci/update-lock.sh.
The integration job is disabled (if: false). From 0.2.0 on, the platform image is generic (ADR 0048): the Dockerfile refuses a build context containing modules/, and the smoke installs modules only as Module Packages. Signage can't be packaged until its sharp dependency goes through the native linux-x64-glibc flow, which is out of scope here. Run against 0.2.0, the job would build an image without Signage and drive the template module's display instead. The job and ci/integration.sh stay as they are. The job comes back in the change that installs Signage as a Module Package.
Tests
New tests/drizzle-store.test.ts, driven by an ABI v1 executor double (tests/support/abi-executor.ts). The double enforces Core's rules: ABI-only parameters (a Date is refused), callback scopes, forgotten-await detection, revocation, and aborted transactions. The new tests cover:
all 13 store transactions: each is one BEGIN … COMMIT, with every statement inside the transaction and no savepoints (Signage has no nested transactions);
rollback when a removal or reorder is refused, and when a later statement fails;
encoding: instants go out as ISO text and JSON as text;
decoding of the raw forms Core sends: int8 text becomes a number, timestamptz text a Date (including a -05 offset), jsonb text a value (a JSON string stays a string);
the 23505 regression: the error arrives wrapped by Drizzle, is recognized as a conflict and rolled back, while 23503, a failure with no SQLSTATE, a refused parameter and a raw pg-shaped { code: "23505" } are not treated as duplicates.
I checked that the tests can fail: they catch both the old 23505 check and a forgotten await when each is reintroduced.
manifest.test.ts, schema.test.ts and acceptance.test.ts now cover manifest v8, database: { abi: 1 } in both manifest and runtime, and plain JSON-safe metadata. The schema entrypoint's only export is default, and the metadata pins every table and foreign-key target.
Verification
CI job commands, from a fresh ci/assemble.sh against v0.2.0: frozen install, lint, check-cleanup, typecheck, test (22 files, 261 tests), pnpm boundaries, pnpm modules:validate (1 module, 11 tables), pnpm migrations:validate and build all pass.
Live run, not committed (a module can't import Core): Signage's built store ran through Core 0.2.0's real createModuleDatabaseExecutor against disposable PostgreSQL 18.4. The server was set to America/New_York and the host to TZ=America/Chicago. All 22 checks passed:
every transaction flow and its rollback;
a real duplicate content hash, which arrives as 23505 through Drizzle and becomes SignageStoreConflict with nothing persisted;
23514 and 23503 not treated as duplicates;
int8 above 2³¹, exact instants, and the jsonb round trip.
Afterwards the persisted values were still jsonb objects, bigint, and exact timestamptz.
pnpm test:migrations:integration doesn't apply here: it uses the platform's own fixture modules, not workspace modules.
Not in this PR
Standalone dependencies: registry .npmrc, replacing workspace:*, a standalone lockfile, and deleting ci/assemble.sh, ci/platform.env and ci/pnpm-lock.yaml.
Native linux-x64-glibc packaging of sharp, and Module Package builds.
Moves Signage from the legacy module contract to Ekklesia 0.2.0's: manifest v8, Module Database ABI v1, the `@ekklesia/module-database` Drizzle adapter, plain schema metadata, and the curated module SDK. This is the first of the slices. Signage still builds inside the pinned platform workspace (`ci/assemble.sh`). No behaviour, schema or data change.
## What changes
- **Manifest v8.** `manifestVersion: 8` and `database: { abi: 1 }`. Compatibility is now `^0.2.0` for both platform and module SDK, which is what CI tests against. Identity, permissions, configuration, storage, entrypoints and the display declaration are unchanged. The runtime declares the same ABI, because Core refuses a mismatch.
- **Database ABI v1.**
- `createSignageStore(executor)` now takes the platform's ABI v1 executor and binds `moduleDrizzle` to it once, so none of the 12 call sites change.
- The four `drizzle*.ts` stores are now typed `ModuleDrizzleDatabase` / `ModuleDrizzleTransaction` instead of the removed legacy `ModuleDatabase`.
- Query code is unchanged.
- **Schema metadata.** `src/schema/index.ts` now exports only `moduleDrizzleSchemaMetadata("signage", …)`: plain table names and foreign-key targets. The Drizzle table re-exports and `defineModuleSchema` are gone, so no Drizzle object reaches Core. The declarations in `tables.ts` are untouched (only its header comment changed).
- **Duplicate-image bug fix.** `isUniqueViolation` used to check `error.code === "23505"`, but Drizzle wraps every executor failure in `DrizzleQueryError`, with the real error on `.cause`. So the check never matched, and a duplicate-upload race returned a 500 instead of the content-hash conflict. It now reads `findEkklesiaDatabaseError(error)?.sqlState === "23505"`.
- **Migrations.** No new migration. `0000_initial.sql` and its pinned checksum (`fbbc448f…`) are unchanged.
- **Version.** Stays 0.1.2. By the repository's convention, versions move only in a `chore: release signage X` PR.
## Temporary CI changes (deleted in the next PR)
- `ci/platform.env` now pins v0.2.0 (`1788d17`), and `ci/pnpm-lock.yaml` is regenerated with `ci/update-lock.sh`.
- **The `integration` job is disabled (`if: false`).** From 0.2.0 on, the platform image is generic (ADR 0048): the Dockerfile refuses a build context containing `modules/`, and the smoke installs modules only as Module Packages. Signage can't be packaged until its sharp dependency goes through the native linux-x64-glibc flow, which is out of scope here. Run against 0.2.0, the job would build an image without Signage and drive the template module's display instead. The job and `ci/integration.sh` stay as they are. The job comes back in the change that installs Signage as a Module Package.
## Tests
- New `tests/drizzle-store.test.ts`, driven by an ABI v1 executor double (`tests/support/abi-executor.ts`). The double enforces Core's rules: ABI-only parameters (a `Date` is refused), callback scopes, forgotten-`await` detection, revocation, and aborted transactions. The new tests cover:
- all **13** store transactions: each is one `BEGIN … COMMIT`, with every statement inside the transaction and no savepoints (Signage has no nested transactions);
- rollback when a removal or reorder is refused, and when a later statement fails;
- encoding: instants go out as ISO text and JSON as text;
- decoding of the raw forms Core sends: `int8` text becomes a number, `timestamptz` text a `Date` (including a `-05` offset), `jsonb` text a value (a JSON string stays a string);
- the 23505 regression: the error arrives wrapped by Drizzle, is recognized as a conflict and rolled back, while 23503, a failure with no SQLSTATE, a refused parameter and a raw pg-shaped `{ code: "23505" }` are **not** treated as duplicates.
- I checked that the tests can fail: they catch both the old 23505 check and a forgotten `await` when each is reintroduced.
- `manifest.test.ts`, `schema.test.ts` and `acceptance.test.ts` now cover manifest v8, `database: { abi: 1 }` in both manifest and runtime, and plain JSON-safe metadata. The schema entrypoint's only export is `default`, and the metadata pins every table and foreign-key target.
## Verification
- **CI job commands, from a fresh `ci/assemble.sh` against v0.2.0:** frozen install, lint, check-cleanup, typecheck, test (22 files, 261 tests), `pnpm boundaries`, `pnpm modules:validate` (1 module, 11 tables), `pnpm migrations:validate` and build all pass.
- **Live run, not committed (a module can't import Core):** Signage's built store ran through Core 0.2.0's real `createModuleDatabaseExecutor` against disposable PostgreSQL 18.4. The server was set to `America/New_York` and the host to `TZ=America/Chicago`. All 22 checks passed:
- every transaction flow and its rollback;
- a real duplicate content hash, which arrives as 23505 through Drizzle and becomes `SignageStoreConflict` with nothing persisted;
- 23514 and 23503 not treated as duplicates;
- `int8` above 2³¹, exact instants, and the jsonb round trip.
- Afterwards the persisted values were still `jsonb` objects, `bigint`, and exact `timestamptz`.
- `pnpm test:migrations:integration` doesn't apply here: it uses the platform's own fixture modules, not workspace modules.
## Not in this PR
- Standalone dependencies: registry `.npmrc`, replacing `workspace:*`, a standalone lockfile, and deleting `ci/assemble.sh`, `ci/platform.env` and `ci/pnpm-lock.yaml`.
- Native linux-x64-glibc packaging of sharp, and Module Package builds.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Signage moves from the legacy module contract to Ekklesia 0.2.0's, with no behaviour or schema
change:
- manifest version 8, `database: { abi: 1 }`, compatibility ^0.2.0 for platform and module SDK;
the runtime declares the same ABI, as Core requires;
- every store reaches PostgreSQL through the platform's Module Database ABI v1 executor, with
Signage's own Drizzle tables and the module-side adapter (`moduleDrizzle`) from
@ekklesia/module-database. createSignageStore takes the executor, so no call site changes;
- the schema entrypoint exports only plain ABI v1 metadata (table names and foreign-key targets),
derived from tables.ts with moduleDrizzleSchemaMetadata; no Drizzle object reaches Core;
- duplicate-image detection reads SQLSTATE 23505 from the stable Ekklesia database error, found
with findEkklesiaDatabaseError through Drizzle's DrizzleQueryError wrapping. The old
`error.code === "23505"` check never matched a wrapped error, so a duplicate-upload race
surfaced as a 500 instead of the content-hash conflict.
tables.ts declarations, the migration, and its pinned checksum are unchanged.
Tests: an ABI v1 executor double that enforces Core's scope, parameter, and abort rules drives all
13 store transactions, their rollbacks, raw int8/timestamptz/jsonb decoding, and the 23505
regression, including generic and driver-shaped errors that must not count as duplicates.
CI, temporarily: the platform pin moves to v0.2.0 (1788d17) with a regenerated ci/pnpm-lock.yaml.
The integration job is disabled: from 0.2.0 the platform image is generic (ADR 0048) and installs
modules only as Module Packages, which Signage cannot be until its sharp dependency goes through
the native package flow. The job and ci/integration.sh stay, and the job returns in that change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Moves Signage from the legacy module contract to Ekklesia 0.2.0's: manifest v8, Module Database ABI v1, the
@ekklesia/module-databaseDrizzle adapter, plain schema metadata, and the curated module SDK. This is the first of the slices. Signage still builds inside the pinned platform workspace (ci/assemble.sh). No behaviour, schema or data change.What changes
manifestVersion: 8anddatabase: { abi: 1 }. Compatibility is now^0.2.0for both platform and module SDK, which is what CI tests against. Identity, permissions, configuration, storage, entrypoints and the display declaration are unchanged. The runtime declares the same ABI, because Core refuses a mismatch.createSignageStore(executor)now takes the platform's ABI v1 executor and bindsmoduleDrizzleto it once, so none of the 12 call sites change.drizzle*.tsstores are now typedModuleDrizzleDatabase/ModuleDrizzleTransactioninstead of the removed legacyModuleDatabase.src/schema/index.tsnow exports onlymoduleDrizzleSchemaMetadata("signage", …): plain table names and foreign-key targets. The Drizzle table re-exports anddefineModuleSchemaare gone, so no Drizzle object reaches Core. The declarations intables.tsare untouched (only its header comment changed).isUniqueViolationused to checkerror.code === "23505", but Drizzle wraps every executor failure inDrizzleQueryError, with the real error on.cause. So the check never matched, and a duplicate-upload race returned a 500 instead of the content-hash conflict. It now readsfindEkklesiaDatabaseError(error)?.sqlState === "23505".0000_initial.sqland its pinned checksum (fbbc448f…) are unchanged.chore: release signage XPR.Temporary CI changes (deleted in the next PR)
ci/platform.envnow pins v0.2.0 (1788d17), andci/pnpm-lock.yamlis regenerated withci/update-lock.sh.integrationjob is disabled (if: false). From 0.2.0 on, the platform image is generic (ADR 0048): the Dockerfile refuses a build context containingmodules/, and the smoke installs modules only as Module Packages. Signage can't be packaged until its sharp dependency goes through the native linux-x64-glibc flow, which is out of scope here. Run against 0.2.0, the job would build an image without Signage and drive the template module's display instead. The job andci/integration.shstay as they are. The job comes back in the change that installs Signage as a Module Package.Tests
tests/drizzle-store.test.ts, driven by an ABI v1 executor double (tests/support/abi-executor.ts). The double enforces Core's rules: ABI-only parameters (aDateis refused), callback scopes, forgotten-awaitdetection, revocation, and aborted transactions. The new tests cover:BEGIN … COMMIT, with every statement inside the transaction and no savepoints (Signage has no nested transactions);int8text becomes a number,timestamptztext aDate(including a-05offset),jsonbtext a value (a JSON string stays a string);{ code: "23505" }are not treated as duplicates.awaitwhen each is reintroduced.manifest.test.ts,schema.test.tsandacceptance.test.tsnow cover manifest v8,database: { abi: 1 }in both manifest and runtime, and plain JSON-safe metadata. The schema entrypoint's only export isdefault, and the metadata pins every table and foreign-key target.Verification
ci/assemble.shagainst v0.2.0: frozen install, lint, check-cleanup, typecheck, test (22 files, 261 tests),pnpm boundaries,pnpm modules:validate(1 module, 11 tables),pnpm migrations:validateand build all pass.createModuleDatabaseExecutoragainst disposable PostgreSQL 18.4. The server was set toAmerica/New_Yorkand the host toTZ=America/Chicago. All 22 checks passed:SignageStoreConflictwith nothing persisted;int8above 2³¹, exact instants, and the jsonb round trip.jsonbobjects,bigint, and exacttimestamptz.pnpm test:migrations:integrationdoesn't apply here: it uses the platform's own fixture modules, not workspace modules.Not in this PR
.npmrc, replacingworkspace:*, a standalone lockfile, and deletingci/assemble.sh,ci/platform.envandci/pnpm-lock.yaml.🤖 Generated with Claude Code
Signage moves from the legacy module contract to Ekklesia 0.2.0's, with no behaviour or schema change: - manifest version 8, `database: { abi: 1 }`, compatibility ^0.2.0 for platform and module SDK; the runtime declares the same ABI, as Core requires; - every store reaches PostgreSQL through the platform's Module Database ABI v1 executor, with Signage's own Drizzle tables and the module-side adapter (`moduleDrizzle`) from @ekklesia/module-database. createSignageStore takes the executor, so no call site changes; - the schema entrypoint exports only plain ABI v1 metadata (table names and foreign-key targets), derived from tables.ts with moduleDrizzleSchemaMetadata; no Drizzle object reaches Core; - duplicate-image detection reads SQLSTATE 23505 from the stable Ekklesia database error, found with findEkklesiaDatabaseError through Drizzle's DrizzleQueryError wrapping. The old `error.code === "23505"` check never matched a wrapped error, so a duplicate-upload race surfaced as a 500 instead of the content-hash conflict. tables.ts declarations, the migration, and its pinned checksum are unchanged. Tests: an ABI v1 executor double that enforces Core's scope, parameter, and abort rules drives all 13 store transactions, their rollbacks, raw int8/timestamptz/jsonb decoding, and the 23505 regression, including generic and driver-shaped errors that must not count as duplicates. CI, temporarily: the platform pin moves to v0.2.0 (1788d17) with a regenerated ci/pnpm-lock.yaml. The integration job is disabled: from 0.2.0 the platform image is generic (ADR 0048) and installs modules only as Module Packages, which Signage cannot be until its sharp dependency goes through the native package flow. The job and ci/integration.sh stay, and the job returns in that change. Co-Authored-By: Claude Opus 5.5 <[email protected]>