Skip to content

Commit 20a6d6a

Browse files
os-zhuangclaude
andcommitted
fix(metadata): provision sys_metadata_commit for env kernels (ADR-0067)
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 <noreply@anthropic.com>
1 parent a6f0b7f commit 20a6d6a

2 files changed

Lines changed: 70 additions & 0 deletions

File tree

packages/metadata/src/plugin.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,60 @@ describe('MetadataPlugin._loadFromFileSystem — package provenance stamping', (
221221
expect(item._provenance).toBeUndefined();
222222
});
223223
});
224+
225+
// ─────────────────────────────────────────────────────────────────────────
226+
// ADR-0067 regression: the package-scoped commit log `sys_metadata_commit`
227+
// must be registered among the queryable system objects so per-project
228+
// (cloud) env kernels provision the table at boot. Every publish/apply
229+
// writes a commit row via `publishPackageDrafts`; the object was omitted
230+
// from `queryableMetadataObjects` (only the standalone ObjectQLPlugin
231+
// `environmentId === undefined` path had it), so env builds logged
232+
// `no such table: sys_metadata_commit` and the commit timeline recorded
233+
// nothing. init() must register it via the manifest — next to its
234+
// `sys_metadata_history` sibling — whenever registerSystemObjects is on.
235+
// ─────────────────────────────────────────────────────────────────────────
236+
describe('MetadataPlugin — system object provisioning (ADR-0067 commit log)', () => {
237+
function fakeCtxWithManifest() {
238+
const registered: any[] = [];
239+
const manifest = { register: vi.fn((m: any) => registered.push(m)) };
240+
const ctx = {
241+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
242+
registerService: vi.fn(),
243+
getService: vi.fn((name: string) => (name === 'manifest' ? manifest : undefined)),
244+
} as any;
245+
return { ctx, registered };
246+
}
247+
248+
it('registers sys_metadata_commit alongside its history sibling (env kernel)', async () => {
249+
const plugin = new MetadataPlugin({
250+
watch: false,
251+
config: { bootstrap: 'lazy' },
252+
environmentId: 'proj_test',
253+
// registerSystemObjects defaults to true → env-kernel provisioning path
254+
});
255+
const { ctx, registered } = fakeCtxWithManifest();
256+
257+
await plugin.init(ctx);
258+
259+
const names = registered.flatMap((m) => m.objects ?? []).map((o: any) => o.name);
260+
// The bug: history present, commit absent → the table is never provisioned.
261+
expect(names).toContain('sys_metadata_history');
262+
expect(names).toContain('sys_metadata_commit');
263+
});
264+
265+
it('registers NOTHING when registerSystemObjects=false (control-plane kernel)', async () => {
266+
const plugin = new MetadataPlugin({
267+
watch: false,
268+
config: { bootstrap: 'lazy' },
269+
environmentId: 'proj_test',
270+
registerSystemObjects: false,
271+
});
272+
const { ctx, registered } = fakeCtxWithManifest();
273+
274+
await plugin.init(ctx);
275+
276+
// Control-plane owns these tables; per-ADR they must NOT leak into a
277+
// kernel that opted out of system-object registration.
278+
expect(registered).toHaveLength(0);
279+
});
280+
});

packages/metadata/src/plugin.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { applyProtection } from '@objectstack/spec/shared';
1111
import {
1212
SysMetadataObject,
1313
SysMetadataHistoryObject,
14+
SysMetadataCommitObject,
1415
SysMetadataAuditObject,
1516
SysViewDefinitionObject,
1617
} from '@objectstack/metadata-core';
@@ -28,9 +29,21 @@ import {
2829
// metadata write decisions — provisioned alongside the storage tables so
2930
// `_lock` enforcement always has a place to record decisions, even when
3031
// the deployment skipped @objectstack/plugin-audit.
32+
//
33+
// `SysMetadataCommitObject` (ADR-0067) is the package-scoped commit log that
34+
// GROUPS a turn's `sys_metadata_history` events (the unit `revertCommit`
35+
// operates on). It MUST be provisioned alongside its history sibling: every
36+
// publish/apply writes a commit row, so a per-project (cloud) env kernel that
37+
// omitted it hit `no such table: sys_metadata_commit` on the first AI build —
38+
// the write is best-effort so the build still landed, but the error spammed
39+
// logs and the commit timeline silently recorded nothing. Registered here
40+
// (not only in the ObjectQLPlugin `environmentId === undefined` standalone
41+
// path) so env kernels — the only place this table is written — get it.
3142
const queryableMetadataObjects = [
3243
SysMetadataObject,
3344
SysMetadataHistoryObject,
45+
// ADR-0067 commit log — sibling of sys_metadata_history (see note above).
46+
SysMetadataCommitObject,
3447
SysMetadataAuditObject,
3548
// Runtime view storage (shared / personal). Must always be provisioned so
3649
// end-user view creation via the generic data API has a place to write —

0 commit comments

Comments
 (0)