From 20a6d6a8150241a80857cd8be275ea71bf1351c9 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Fri, 26 Jun 2026 00:41:02 +0800 Subject: [PATCH] fix(metadata): provision sys_metadata_commit for env kernels (ADR-0067) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-project (cloud) env kernels boot MetadataPlugin with registerSystemObjects:true, registering `queryableMetadataObjects` so ObjectQL boot-sync provisions their physical tables. The ADR-0067 commit log `sys_metadata_commit` was missing from that list — it lived only in ObjectQLPlugin's standalone `environmentId === undefined` path — so env DBs never got the table. Every AI build/apply calls publishPackageDrafts, which writes a commit row (op='apply'), hitting a hard `SqliteError: no such table: sys_metadata_commit` on the first build. The write is best-effort so the build still landed, but the error spammed logs and the commit timeline silently recorded nothing. Add SysMetadataCommitObject next to its sys_metadata_history sibling in queryableMetadataObjects so env kernels provision it at boot. Same class as the audit/messaging lazy-table provisioning fix. Verified before/after on a kernel booted with the env-kernel plugin triple (DriverPlugin('cloud') + ObjectQLPlugin(skipSchemaSync:false) + MetadataPlugin(registerSystemObjects:true)) over real SQLite: boot-sync goes 4 -> 5 objects and the writer's insert succeeds; without the change the exact reported error reproduces. Unit test in plugin.test.ts fails without the fix, passes with it. Co-Authored-By: Claude Opus 4.8 --- packages/metadata/src/plugin.test.ts | 57 ++++++++++++++++++++++++++++ packages/metadata/src/plugin.ts | 13 +++++++ 2 files changed, 70 insertions(+) diff --git a/packages/metadata/src/plugin.test.ts b/packages/metadata/src/plugin.test.ts index 0615acf241..359a9dd647 100644 --- a/packages/metadata/src/plugin.test.ts +++ b/packages/metadata/src/plugin.test.ts @@ -221,3 +221,60 @@ describe('MetadataPlugin._loadFromFileSystem — package provenance stamping', ( expect(item._provenance).toBeUndefined(); }); }); + +// ───────────────────────────────────────────────────────────────────────── +// ADR-0067 regression: the package-scoped commit log `sys_metadata_commit` +// must be registered among the queryable system objects so per-project +// (cloud) env kernels provision the table at boot. Every publish/apply +// writes a commit row via `publishPackageDrafts`; the object was omitted +// from `queryableMetadataObjects` (only the standalone ObjectQLPlugin +// `environmentId === undefined` path had it), so env builds logged +// `no such table: sys_metadata_commit` and the commit timeline recorded +// nothing. init() must register it via the manifest — next to its +// `sys_metadata_history` sibling — whenever registerSystemObjects is on. +// ───────────────────────────────────────────────────────────────────────── +describe('MetadataPlugin — system object provisioning (ADR-0067 commit log)', () => { + function fakeCtxWithManifest() { + const registered: any[] = []; + const manifest = { register: vi.fn((m: any) => registered.push(m)) }; + const ctx = { + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, + registerService: vi.fn(), + getService: vi.fn((name: string) => (name === 'manifest' ? manifest : undefined)), + } as any; + return { ctx, registered }; + } + + it('registers sys_metadata_commit alongside its history sibling (env kernel)', async () => { + const plugin = new MetadataPlugin({ + watch: false, + config: { bootstrap: 'lazy' }, + environmentId: 'proj_test', + // registerSystemObjects defaults to true → env-kernel provisioning path + }); + const { ctx, registered } = fakeCtxWithManifest(); + + await plugin.init(ctx); + + const names = registered.flatMap((m) => m.objects ?? []).map((o: any) => o.name); + // The bug: history present, commit absent → the table is never provisioned. + expect(names).toContain('sys_metadata_history'); + expect(names).toContain('sys_metadata_commit'); + }); + + it('registers NOTHING when registerSystemObjects=false (control-plane kernel)', async () => { + const plugin = new MetadataPlugin({ + watch: false, + config: { bootstrap: 'lazy' }, + environmentId: 'proj_test', + registerSystemObjects: false, + }); + const { ctx, registered } = fakeCtxWithManifest(); + + await plugin.init(ctx); + + // Control-plane owns these tables; per-ADR they must NOT leak into a + // kernel that opted out of system-object registration. + expect(registered).toHaveLength(0); + }); +}); diff --git a/packages/metadata/src/plugin.ts b/packages/metadata/src/plugin.ts index e3f22c8590..f627b7591c 100644 --- a/packages/metadata/src/plugin.ts +++ b/packages/metadata/src/plugin.ts @@ -11,6 +11,7 @@ import { applyProtection } from '@objectstack/spec/shared'; import { SysMetadataObject, SysMetadataHistoryObject, + SysMetadataCommitObject, SysMetadataAuditObject, SysViewDefinitionObject, } from '@objectstack/metadata-core'; @@ -28,9 +29,21 @@ import { // metadata write decisions — provisioned alongside the storage tables so // `_lock` enforcement always has a place to record decisions, even when // the deployment skipped @objectstack/plugin-audit. +// +// `SysMetadataCommitObject` (ADR-0067) is the package-scoped commit log that +// GROUPS a turn's `sys_metadata_history` events (the unit `revertCommit` +// operates on). It MUST be provisioned alongside its history sibling: every +// publish/apply writes a commit row, so a per-project (cloud) env kernel that +// omitted it hit `no such table: sys_metadata_commit` on the first AI build — +// the write is best-effort so the build still landed, but the error spammed +// logs and the commit timeline silently recorded nothing. Registered here +// (not only in the ObjectQLPlugin `environmentId === undefined` standalone +// path) so env kernels — the only place this table is written — get it. const queryableMetadataObjects = [ SysMetadataObject, SysMetadataHistoryObject, + // ADR-0067 commit log — sibling of sys_metadata_history (see note above). + SysMetadataCommitObject, SysMetadataAuditObject, // Runtime view storage (shared / personal). Must always be provisioned so // end-user view creation via the generic data API has a place to write —