@@ -101,6 +101,16 @@ export interface ObjectQLPluginOptions {
101101 * Safe to leave unset: hydration tolerates a missing table.
102102 */
103103 hydrateMetadataFromDb ?: boolean ;
104+ /**
105+ * ADR-0076 Step 2 (#2462): when `false`, this plugin SKIPS its built-in
106+ * protocol assembly (the `protocol` service, the metadata-storage platform
107+ * objects, and the lightweight `analytics` fallback) — mount
108+ * `createMetadataProtocolPlugin()` from `@objectstack/metadata-protocol`
109+ * alongside to own them instead. Protocol CONSUMERS stay here either way
110+ * (DB hydration + authored hook/action rebind resolve `protocol` lazily).
111+ * Defaults to `true` (built-in assembly, fully backward compatible).
112+ */
113+ registerProtocol ?: boolean ;
104114 /**
105115 * ADR-0057 LifecycleService tuning. Lifecycle enforcement is a platform
106116 * primitive and defaults ON — objects without a `lifecycle` declaration are
@@ -134,6 +144,7 @@ export class ObjectQLPlugin implements Plugin {
134144 /** Serializes reload-time schema syncs so overlapping reloads can't race DDL. */
135145 private reloadSchemaSync : Promise < void > = Promise . resolve ( ) ;
136146 private hydrateMetadataFromDb = false ;
147+ private registerProtocol = true ;
137148 /**
138149 * Armed at the end of `start()` (AFTER the one-shot
139150 * {@link bridgeObjectsToMetadataService}, where that runs). From that
@@ -176,9 +187,43 @@ export class ObjectQLPlugin implements Plugin {
176187 ? opts . skipSchemaSync
177188 : process . env . OS_SKIP_SCHEMA_SYNC === '1' ;
178189 this . hydrateMetadataFromDb = opts . hydrateMetadataFromDb === true ;
190+ this . registerProtocol = opts . registerProtocol !== false ;
179191 this . lifecycleOptions = opts . lifecycle ;
180192 }
181193
194+ /**
195+ * Arm the authored hook/action rebind on protocol metadata mutations
196+ * (#2588, #2605). Shared by both assembly modes: called with the in-house
197+ * shim when `registerProtocol` is on, and lazily from `start()` against
198+ * whatever registered `protocol` (MetadataProtocolPlugin) otherwise.
199+ */
200+ private subscribeMetadataRebind ( ctx : PluginContext , protocol : unknown ) : void {
201+ if ( typeof ( protocol as any ) ?. onMetadataMutation !== 'function' ) return ;
202+ const unsubscribe = ( protocol as any ) . onMetadataMutation (
203+ ( evt : { type : string ; name : string ; state : string } ) => {
204+ if ( evt ?. state === 'draft' ) return ;
205+ if ( evt ?. type === 'hook' ) {
206+ void this . resyncAuthoredHooks ( ctx ) . catch ( ( e : any ) => {
207+ ctx . logger . warn ( '[ObjectQLPlugin] authored-hook rebind after mutation failed' , {
208+ hook : evt . name ,
209+ error : e ?. message ,
210+ } ) ;
211+ } ) ;
212+ } else if ( evt ?. type === 'action' || evt ?. type === 'object' ) {
213+ // `object` rows carry embedded `actions[]`, so an object edit can
214+ // add/remove an authored action too — re-sync on both.
215+ void this . resyncAuthoredActions ( ctx ) . catch ( ( e : any ) => {
216+ ctx . logger . warn ( '[ObjectQLPlugin] authored-action rebind after mutation failed' , {
217+ item : evt . name ,
218+ error : e ?. message ,
219+ } ) ;
220+ } ) ;
221+ }
222+ } ,
223+ ) ;
224+ this . metadataUnsubscribes . push ( unsubscribe ) ;
225+ }
226+
182227 init = async ( ctx : PluginContext ) => {
183228 if ( ! this . ql ) {
184229 // Pass kernel logger to engine to avoid creating a separate logger instance
@@ -217,6 +262,7 @@ export class ObjectQLPlugin implements Plugin {
217262 services : [ 'objectql' , 'data' , 'manifest' ] ,
218263 } ) ;
219264
265+ if ( this . registerProtocol ) {
220266 // Register the metadata-storage objects this engine's own protocol reads
221267 // and writes — `sys_metadata` (loadMetaFromDb / getMetaItems / saveMetaItem),
222268 // its history/audit siblings, and `sys_view_definition`. Doing it here
@@ -267,31 +313,7 @@ export class ObjectQLPlugin implements Plugin {
267313 // without a restart. Draft saves are skipped — drafts are not live by
268314 // design. Fire-and-forget: a resync failure is logged, never fails the
269315 // write.
270- if ( typeof ( protocolShim as any ) . onMetadataMutation === 'function' ) {
271- const unsubscribe = ( protocolShim as any ) . onMetadataMutation (
272- ( evt : { type : string ; name : string ; state : string } ) => {
273- if ( evt ?. state === 'draft' ) return ;
274- if ( evt ?. type === 'hook' ) {
275- void this . resyncAuthoredHooks ( ctx ) . catch ( ( e : any ) => {
276- ctx . logger . warn ( '[ObjectQLPlugin] authored-hook rebind after mutation failed' , {
277- hook : evt . name ,
278- error : e ?. message ,
279- } ) ;
280- } ) ;
281- } else if ( evt ?. type === 'action' || evt ?. type === 'object' ) {
282- // `object` rows carry embedded `actions[]`, so an object edit can
283- // add/remove an authored action too — re-sync on both.
284- void this . resyncAuthoredActions ( ctx ) . catch ( ( e : any ) => {
285- ctx . logger . warn ( '[ObjectQLPlugin] authored-action rebind after mutation failed' , {
286- item : evt . name ,
287- error : e ?. message ,
288- } ) ;
289- } ) ;
290- }
291- } ,
292- ) ;
293- this . metadataUnsubscribes . push ( unsubscribe ) ;
294- }
316+ this . subscribeMetadataRebind ( ctx , protocolShim ) ;
295317
296318 // Register an `analytics` service adapter that maps the dispatcher's
297319 // expected interface (query / getMeta / generateSql) onto the
@@ -349,6 +371,9 @@ export class ObjectQLPlugin implements Plugin {
349371 message : 'Analytics SQL generation not implemented by ObjectQL adapter' ,
350372 } ) ,
351373 } ) ;
374+ } else {
375+ ctx . logger . info ( 'registerProtocol=false — protocol assembly delegated to MetadataProtocolPlugin (ADR-0076 Step 2, #2462)' ) ;
376+ }
352377
353378 // ADR-0057: the platform-owned LifecycleService. Registered from the
354379 // engine plugin (not an opt-in capability) so every kernel that has data
@@ -375,6 +400,15 @@ export class ObjectQLPlugin implements Plugin {
375400 start = async ( ctx : PluginContext ) => {
376401 ctx . logger . info ( 'ObjectQL engine starting...' ) ;
377402
403+ // Delegated-assembly mode (ADR-0076 Step 2): the protocol was registered
404+ // by MetadataProtocolPlugin during init — arm the authored hook/action
405+ // rebind against it now that all inits ran. Graceful when absent.
406+ if ( ! this . registerProtocol ) {
407+ try {
408+ this . subscribeMetadataRebind ( ctx , ctx . getService ( 'protocol' ) ) ;
409+ } catch { /* no protocol registered — rebind not armed */ }
410+ }
411+
378412 // Sync from external metadata service (e.g. MetadataPlugin) if available
379413 try {
380414 const metadataService = ctx . getService ( 'metadata' ) as any ;
0 commit comments