@objectstack/metadata-protocol 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,4855 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+ var _chunkJRNTUZG6cjs = require('./chunk-JRNTUZG6.cjs');
4
+
5
+
6
+ var _chunk7LOFAEHAcjs = require('./chunk-7LOFAEHA.cjs');
7
+
8
+ // src/protocol.ts
9
+ var _types = require('@objectstack/types');
10
+
11
+ // src/sys-metadata-repository.ts
12
+ var _metadatacore = require('@objectstack/metadata-core');
13
+
14
+ var _kernel = require('@objectstack/spec/kernel');
15
+ var _shared = require('@objectstack/spec/shared');
16
+ var OVERLAY_ALLOWED_TYPES = new Set(
17
+ _kernel.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowOrgOverride).map((e) => e.type)
18
+ );
19
+ var STATIC_REGISTRY_TYPES = new Set(
20
+ _kernel.DEFAULT_METADATA_TYPE_REGISTRY.map((e) => e.type)
21
+ );
22
+ var RUNTIME_CREATE_ALLOWED_TYPES = new Set(
23
+ _kernel.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowRuntimeCreate).map((e) => e.type)
24
+ );
25
+ var _envWritableMetadataTypes = null;
26
+ function envWritableMetadataTypes() {
27
+ if (_envWritableMetadataTypes !== null) return _envWritableMetadataTypes;
28
+ const raw = _types.readEnvWithDeprecation.call(void 0, "OS_METADATA_WRITABLE", []) || "";
29
+ const set = /* @__PURE__ */ new Set();
30
+ for (const tok of raw.split(",")) {
31
+ const t = tok.trim();
32
+ if (!t) continue;
33
+ const singular = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[t], () => ( t));
34
+ set.add(singular);
35
+ const plural = _shared.SINGULAR_TO_PLURAL[singular];
36
+ if (plural) set.add(plural);
37
+ }
38
+ _envWritableMetadataTypes = set;
39
+ return set;
40
+ }
41
+ function resetEnvWritableMetadataTypes() {
42
+ _envWritableMetadataTypes = null;
43
+ }
44
+ var SysMetadataRepository = class {
45
+ constructor(opts) {
46
+ /**
47
+ * Local seq counter for in-memory watch() event broadcasts. Mirrors
48
+ * the durable `event_seq` we write into `sys_metadata_history` on
49
+ * each successful put/delete — assigned AFTER the transaction commits
50
+ * so we never broadcast events that got rolled back.
51
+ */
52
+ this.seqCounter = 0;
53
+ this.watchers = /* @__PURE__ */ new Set();
54
+ this.closed = false;
55
+ /** Table name for the durable event log. */
56
+ this.historyTable = "sys_metadata_history";
57
+ this.engine = opts.engine;
58
+ this.organizationId = _nullishCoalesce(opts.organizationId, () => ( null));
59
+ this.orgLabel = _nullishCoalesce(opts.orgLabel, () => ( (_nullishCoalesce(opts.organizationId, () => ( "system")))));
60
+ }
61
+ /**
62
+ * Run `cb` inside `engine.transaction(...)` if the engine supports it,
63
+ * otherwise fall through to a direct call. Matches the real
64
+ * `ObjectQL.transaction` semantics — in-memory drivers (and our test
65
+ * fakes) get no rollback, which is acceptable because production
66
+ * always runs on a SQL driver with real ACID.
67
+ */
68
+ async withTxn(cb) {
69
+ if (typeof this.engine.transaction === "function") {
70
+ return this.engine.transaction(cb);
71
+ }
72
+ return cb(void 0);
73
+ }
74
+ /**
75
+ * Read the current overlay row. Returns null if no row exists —
76
+ * callers (e.g. LayeredRepository) fall through to lower layers.
77
+ *
78
+ * `opts.state` selects which lifecycle row to read: defaults to the
79
+ * live published row (`'active'`). Pass `'draft'` to read the pending
80
+ * unpublished revision (if any).
81
+ */
82
+ async get(ref, opts) {
83
+ this.assertOpen();
84
+ const state = _nullishCoalesce(_optionalChain([opts, 'optionalAccess', _ => _.state]), () => ( "active"));
85
+ const row = await this.engine.findOne("sys_metadata", {
86
+ where: this.whereFor(ref, state, opts && "packageId" in opts ? _nullishCoalesce(opts.packageId, () => ( null)) : void 0)
87
+ });
88
+ if (!row) return null;
89
+ return this.rowToItem(ref, row);
90
+ }
91
+ /**
92
+ * Resolve a historical version by content hash (ADR-0009).
93
+ *
94
+ * Looks up `sys_metadata_history` by `(organization_id, type, name,
95
+ * checksum)`. Returns null if no row matches. `executionPinned` types
96
+ * are guaranteed to find their body here because history GC skips
97
+ * them.
98
+ */
99
+ async getByHash(ref, hash) {
100
+ this.assertOpen();
101
+ const full = this.fullRef(ref);
102
+ const row = await this.engine.findOne(this.historyTable, {
103
+ where: {
104
+ organization_id: this.organizationId,
105
+ type: full.type,
106
+ name: full.name,
107
+ checksum: hash
108
+ }
109
+ });
110
+ if (!row) return null;
111
+ const rawBody = row.metadata;
112
+ if (rawBody === null || rawBody === void 0) {
113
+ return null;
114
+ }
115
+ const body = typeof rawBody === "string" ? JSON.parse(rawBody) : rawBody;
116
+ return {
117
+ ref: { ...full, version: void 0 },
118
+ body,
119
+ hash,
120
+ parentHash: _nullishCoalesce(row.previous_checksum, () => ( null)),
121
+ authoredBy: _nullishCoalesce(row.recorded_by, () => ( "unknown")),
122
+ authoredAt: _nullishCoalesce(row.recorded_at, () => ( (/* @__PURE__ */ new Date(0)).toISOString())),
123
+ message: _nullishCoalesce(row.change_note, () => ( void 0)),
124
+ seq: _nullishCoalesce(row.event_seq, () => ( 0))
125
+ };
126
+ }
127
+ async put(ref, spec, opts) {
128
+ this.assertOpen();
129
+ this.assertAllowed(ref.type, opts.intent);
130
+ const state = _nullishCoalesce(opts.state, () => ( "active"));
131
+ const body = _nullishCoalesce(spec, () => ( {}));
132
+ const hash = _metadatacore.hashSpec.call(void 0, body);
133
+ const result = await this.withTxn(async (ctx) => {
134
+ const existing = await this.engine.findOne("sys_metadata", {
135
+ where: this.whereFor(ref, state, _nullishCoalesce(opts.packageId, () => ( null))),
136
+ context: ctx
137
+ });
138
+ const existingHash = _nullishCoalesce(_optionalChain([existing, 'optionalAccess', _2 => _2.checksum]), () => ( null));
139
+ if (opts.parentVersion !== existingHash) {
140
+ throw new (0, _metadatacore.ConflictError)(this.fullRef(ref), opts.parentVersion, existingHash);
141
+ }
142
+ if (existing && existingHash === hash) {
143
+ const item2 = this.rowToItem(ref, existing);
144
+ return { skipped: true, version: hash, seq: item2.seq, item: item2 };
145
+ }
146
+ const now = (/* @__PURE__ */ new Date()).toISOString();
147
+ const baseOp = existing ? "update" : "create";
148
+ const op = _nullishCoalesce(opts.opType, () => ( baseOp));
149
+ const version = await this.nextItemVersion(ref, ctx);
150
+ const eventSeq = await this.nextEventSeq(ctx);
151
+ const parentRowData = {
152
+ type: ref.type,
153
+ name: ref.name,
154
+ organization_id: this.organizationId,
155
+ metadata: JSON.stringify(body),
156
+ checksum: hash,
157
+ state,
158
+ version,
159
+ updated_at: now
160
+ };
161
+ if (existing) {
162
+ const existingPkg = _nullishCoalesce(existing.package_id, () => ( null));
163
+ parentRowData.package_id = _nullishCoalesce(_nullishCoalesce(existingPkg, () => ( opts.packageId)), () => ( null));
164
+ } else {
165
+ parentRowData.package_id = _nullishCoalesce(opts.packageId, () => ( null));
166
+ }
167
+ if (existing) {
168
+ const existingId = existing.id;
169
+ if (existingId === void 0) {
170
+ throw new Error(
171
+ `SysMetadataRepository.put: existing row for ${ref.type}/${ref.name} has no id column`
172
+ );
173
+ }
174
+ await this.engine.update("sys_metadata", parentRowData, {
175
+ where: { id: existingId },
176
+ context: ctx
177
+ });
178
+ } else {
179
+ parentRowData.created_at = now;
180
+ await this.engine.insert("sys_metadata", parentRowData, { context: ctx });
181
+ }
182
+ await this.engine.insert(
183
+ this.historyTable,
184
+ {
185
+ id: this.uuid(),
186
+ event_seq: eventSeq,
187
+ type: ref.type,
188
+ name: ref.name,
189
+ version,
190
+ operation_type: op,
191
+ metadata: JSON.stringify(body),
192
+ checksum: hash,
193
+ previous_checksum: existingHash,
194
+ change_note: opts.message,
195
+ source: _nullishCoalesce(opts.source, () => ( "sys-metadata-repo")),
196
+ organization_id: this.organizationId,
197
+ recorded_by: opts.actor,
198
+ recorded_at: now
199
+ },
200
+ { context: ctx }
201
+ );
202
+ const item = {
203
+ ref: this.fullRef(ref),
204
+ body,
205
+ hash,
206
+ parentHash: existingHash,
207
+ authoredBy: opts.actor,
208
+ authoredAt: now,
209
+ message: opts.message,
210
+ seq: eventSeq
211
+ };
212
+ return {
213
+ skipped: false,
214
+ version: hash,
215
+ seq: eventSeq,
216
+ item,
217
+ op,
218
+ existingHash,
219
+ now,
220
+ source: _nullishCoalesce(opts.source, () => ( "sys-metadata-repo")),
221
+ message: opts.message,
222
+ actor: opts.actor
223
+ };
224
+ });
225
+ if (result.skipped) {
226
+ return { version: result.version, seq: result.seq, item: result.item };
227
+ }
228
+ this.seqCounter = result.seq;
229
+ if (state === "active") {
230
+ this.broadcast({
231
+ seq: result.seq,
232
+ op: result.op,
233
+ ref: this.fullRef(ref),
234
+ hash: result.version,
235
+ parentHash: result.existingHash,
236
+ actor: result.actor,
237
+ message: result.message,
238
+ ts: result.now,
239
+ source: result.source
240
+ });
241
+ }
242
+ return { version: result.version, seq: result.seq, item: result.item };
243
+ }
244
+ async delete(ref, opts) {
245
+ this.assertOpen();
246
+ this.assertAllowed(ref.type, opts.intent);
247
+ const state = _nullishCoalesce(opts.state, () => ( "active"));
248
+ const result = await this.withTxn(async (ctx) => {
249
+ const existing = await this.engine.findOne("sys_metadata", {
250
+ where: this.whereFor(ref, state),
251
+ context: ctx
252
+ });
253
+ if (!existing) {
254
+ throw new (0, _metadatacore.ConflictError)(this.fullRef(ref), opts.parentVersion, null);
255
+ }
256
+ const existingHash = _nullishCoalesce(existing.checksum, () => ( null));
257
+ if (opts.parentVersion !== existingHash) {
258
+ throw new (0, _metadatacore.ConflictError)(this.fullRef(ref), opts.parentVersion, existingHash);
259
+ }
260
+ const existingId = existing.id;
261
+ if (existingId === void 0) {
262
+ throw new Error(
263
+ `SysMetadataRepository.delete: existing row for ${ref.type}/${ref.name} has no id column`
264
+ );
265
+ }
266
+ const now = (/* @__PURE__ */ new Date()).toISOString();
267
+ let version = 0;
268
+ let eventSeq = 0;
269
+ if (state === "active") {
270
+ version = await this.nextItemVersion(ref, ctx);
271
+ eventSeq = await this.nextEventSeq(ctx);
272
+ }
273
+ await this.engine.delete("sys_metadata", {
274
+ where: { id: existingId },
275
+ context: ctx
276
+ });
277
+ if (state === "active") {
278
+ await this.engine.insert(
279
+ this.historyTable,
280
+ {
281
+ id: this.uuid(),
282
+ event_seq: eventSeq,
283
+ type: ref.type,
284
+ name: ref.name,
285
+ version,
286
+ operation_type: "delete",
287
+ metadata: null,
288
+ checksum: null,
289
+ previous_checksum: existingHash,
290
+ change_note: opts.message,
291
+ source: _nullishCoalesce(opts.source, () => ( "sys-metadata-repo")),
292
+ organization_id: this.organizationId,
293
+ recorded_by: opts.actor,
294
+ recorded_at: now
295
+ },
296
+ { context: ctx }
297
+ );
298
+ }
299
+ return {
300
+ eventSeq,
301
+ existingHash,
302
+ now,
303
+ source: _nullishCoalesce(opts.source, () => ( "sys-metadata-repo")),
304
+ message: opts.message,
305
+ actor: opts.actor
306
+ };
307
+ });
308
+ if (state === "active") {
309
+ this.seqCounter = result.eventSeq;
310
+ this.broadcast({
311
+ seq: result.eventSeq,
312
+ op: "delete",
313
+ ref: this.fullRef(ref),
314
+ hash: null,
315
+ parentHash: result.existingHash,
316
+ actor: result.actor,
317
+ message: result.message,
318
+ ts: result.now,
319
+ source: result.source
320
+ });
321
+ }
322
+ return { seq: result.eventSeq };
323
+ }
324
+ /**
325
+ * Promote the pending draft row for `ref` into the live (`active`)
326
+ * overlay. Atomic: reads the draft inside the same transaction, runs
327
+ * the canonical `put` to upsert the active row (which appends a
328
+ * history event with `operation_type='publish'`), then deletes the
329
+ * draft row.
330
+ *
331
+ * Errors if no draft exists (callers should 404). The active row's
332
+ * `parentVersion` is computed from the current active hash so this
333
+ * also surfaces optimistic-lock conflicts when something else has
334
+ * published in between (e.g. another admin reverted to an older
335
+ * version since the draft was authored).
336
+ */
337
+ async promoteDraft(ref, opts) {
338
+ this.assertOpen();
339
+ const draftRow = await this.engine.findOne("sys_metadata", {
340
+ where: this.whereFor(ref, "draft")
341
+ });
342
+ if (!draftRow) {
343
+ const err = new Error(
344
+ `[no_draft] No pending draft exists for ${ref.type}/${ref.name} \u2014 nothing to publish.`
345
+ );
346
+ err.code = "no_draft";
347
+ err.status = 404;
348
+ throw err;
349
+ }
350
+ const draftPackageId = _nullishCoalesce(draftRow.package_id, () => ( null));
351
+ const draft = this.rowToItem(ref, draftRow);
352
+ const currentActive = await this.get(ref, { state: "active", packageId: draftPackageId });
353
+ const result = await this.put(ref, draft.body, {
354
+ parentVersion: _nullishCoalesce(_optionalChain([currentActive, 'optionalAccess', _3 => _3.hash]), () => ( null)),
355
+ actor: opts.actor,
356
+ source: _nullishCoalesce(opts.source, () => ( "sys-metadata-repo.publish")),
357
+ message: _nullishCoalesce(opts.message, () => ( `publish draft (hash ${draft.hash})`)),
358
+ intent: _nullishCoalesce(opts.intent, () => ( "override-artifact")),
359
+ state: "active",
360
+ opType: "publish",
361
+ packageId: draftPackageId
362
+ });
363
+ try {
364
+ await this.delete(ref, {
365
+ parentVersion: draft.hash,
366
+ actor: opts.actor,
367
+ source: _nullishCoalesce(opts.source, () => ( "sys-metadata-repo.publish")),
368
+ intent: _nullishCoalesce(opts.intent, () => ( "override-artifact")),
369
+ state: "draft"
370
+ });
371
+ } catch (e2) {
372
+ }
373
+ return result;
374
+ }
375
+ /**
376
+ * Restore the body recorded in history at `targetVersion` (per-org
377
+ * lineage counter) as the new active row. Writes a history event
378
+ * with `operation_type='revert'` so the audit trail captures the
379
+ * intent. Does NOT touch any draft row.
380
+ *
381
+ * Throws `[version_not_found]` (404) if the target version row is
382
+ * missing or is a delete tombstone (no body to restore).
383
+ */
384
+ async restoreVersion(ref, targetVersion, opts) {
385
+ this.assertOpen();
386
+ const full = this.fullRef(ref);
387
+ const row = await this.engine.findOne(this.historyTable, {
388
+ where: {
389
+ organization_id: this.organizationId,
390
+ type: full.type,
391
+ name: full.name,
392
+ version: targetVersion
393
+ }
394
+ });
395
+ if (!row) {
396
+ const err = new Error(
397
+ `[version_not_found] No history row at version ${targetVersion} for ${ref.type}/${ref.name}.`
398
+ );
399
+ err.code = "version_not_found";
400
+ err.status = 404;
401
+ throw err;
402
+ }
403
+ const raw = row.metadata;
404
+ if (raw === null || raw === void 0) {
405
+ const err = new Error(
406
+ `[version_not_restorable] Version ${targetVersion} for ${ref.type}/${ref.name} is a delete tombstone \u2014 nothing to restore.`
407
+ );
408
+ err.code = "version_not_restorable";
409
+ err.status = 409;
410
+ throw err;
411
+ }
412
+ const body = typeof raw === "string" ? JSON.parse(raw) : raw;
413
+ const currentActive = await this.get(ref, { state: "active" });
414
+ return this.put(ref, body, {
415
+ parentVersion: _nullishCoalesce(_optionalChain([currentActive, 'optionalAccess', _4 => _4.hash]), () => ( null)),
416
+ actor: opts.actor,
417
+ source: _nullishCoalesce(opts.source, () => ( "sys-metadata-repo.revert")),
418
+ message: _nullishCoalesce(opts.message, () => ( `revert to version ${targetVersion}`)),
419
+ intent: _nullishCoalesce(opts.intent, () => ( "override-artifact")),
420
+ state: "active",
421
+ opType: "revert"
422
+ });
423
+ }
424
+ async *list(filter) {
425
+ this.assertOpen();
426
+ const where = {
427
+ organization_id: this.organizationId,
428
+ state: "active"
429
+ };
430
+ if (filter.type) where.type = filter.type;
431
+ const rows = await this.engine.find("sys_metadata", {
432
+ where,
433
+ limit: filter.limit
434
+ });
435
+ for (const row of rows) {
436
+ if (filter.nameContains && !String(row.name).includes(filter.nameContains)) continue;
437
+ const item = this.rowToItem(
438
+ { ...this.fullRef({ type: row.type, name: row.name }) },
439
+ row
440
+ );
441
+ const { body, ...header } = item;
442
+ yield header;
443
+ }
444
+ }
445
+ /**
446
+ * List pending DRAFT rows (ADR-0033) for this org, optionally narrowed by
447
+ * `type` and/or `packageId`. Unlike {@link list} (which is hard-scoped to
448
+ * `state='active'`), this reads `state='draft'` so the console can surface
449
+ * what an AI authored but a human hasn't published yet. Returns a light
450
+ * header projection (no body) suitable for a "pending changes" list.
451
+ */
452
+ async listDrafts(filter) {
453
+ this.assertOpen();
454
+ const where = { state: "draft" };
455
+ if (this.organizationId != null) {
456
+ where.$or = [
457
+ { organization_id: this.organizationId },
458
+ { organization_id: null }
459
+ ];
460
+ } else {
461
+ where.organization_id = null;
462
+ }
463
+ if (_optionalChain([filter, 'optionalAccess', _5 => _5.type])) where.type = filter.type;
464
+ if (_optionalChain([filter, 'optionalAccess', _6 => _6.packageId])) where.package_id = filter.packageId;
465
+ const rows = await this.engine.find("sys_metadata", { where });
466
+ return rows.map((row) => ({
467
+ type: row.type,
468
+ name: row.name,
469
+ packageId: _nullishCoalesce(row.package_id, () => ( null)),
470
+ updatedAt: _nullishCoalesce(_nullishCoalesce(row.updated_at, () => ( row.created_at)), () => ( null)),
471
+ updatedBy: _nullishCoalesce(_nullishCoalesce(row.updated_by, () => ( row.created_by)), () => ( null))
472
+ }));
473
+ }
474
+ /**
475
+ * Yield every history event for `(org, type?, name?)` from the
476
+ * durable log, ordered by per-(type,name) `version` ascending. When
477
+ * `filter.type`/`filter.name` are unset the consumer gets the full
478
+ * org-scoped event stream — still ordered by version within each
479
+ * (type,name) bucket, then by `recorded_at` across buckets (we sort
480
+ * client-side because the test engine doesn't honor `orderBy`).
481
+ */
482
+ async *history(ref, opts) {
483
+ this.assertOpen();
484
+ const full = this.fullRef(ref);
485
+ const where = {
486
+ organization_id: this.organizationId,
487
+ type: full.type,
488
+ name: full.name
489
+ };
490
+ const rows = await this.engine.find(this.historyTable, { where });
491
+ rows.sort((a, b) => {
492
+ const va = typeof a.event_seq === "number" ? a.event_seq : 0;
493
+ const vb = typeof b.event_seq === "number" ? b.event_seq : 0;
494
+ return va - vb;
495
+ });
496
+ let yielded = 0;
497
+ for (const row of rows) {
498
+ if (_optionalChain([opts, 'optionalAccess', _7 => _7.sinceSeq]) !== void 0 && (_nullishCoalesce(row.event_seq, () => ( 0))) <= opts.sinceSeq) continue;
499
+ if (_optionalChain([opts, 'optionalAccess', _8 => _8.limit]) !== void 0 && yielded >= opts.limit) break;
500
+ yielded++;
501
+ yield {
502
+ seq: _nullishCoalesce(row.event_seq, () => ( 0)),
503
+ op: _nullishCoalesce(row.operation_type, () => ( "update")),
504
+ ref: full,
505
+ hash: _nullishCoalesce(row.checksum, () => ( null)),
506
+ parentHash: _nullishCoalesce(row.previous_checksum, () => ( null)),
507
+ version: typeof row.version === "number" ? row.version : void 0,
508
+ actor: _nullishCoalesce(row.recorded_by, () => ( "unknown")),
509
+ message: _nullishCoalesce(row.change_note, () => ( void 0)),
510
+ ts: _nullishCoalesce(row.recorded_at, () => ( (/* @__PURE__ */ new Date(0)).toISOString())),
511
+ source: _nullishCoalesce(row.source, () => ( "sys-metadata-repo"))
512
+ };
513
+ }
514
+ }
515
+ /**
516
+ * Live event stream. Fires for every successful put/delete on THIS
517
+ * instance — cross-replica fan-out is M1. Manual AsyncIterator (not
518
+ * an async generator) so we can deterministically tear down via
519
+ * `iter.return()`, matching the pattern used by InMemoryRepository.
520
+ */
521
+ watch(filter, since) {
522
+ const self = this;
523
+ return {
524
+ [Symbol.asyncIterator]: () => {
525
+ const queue = [];
526
+ let pendingResolve = null;
527
+ let stopped = false;
528
+ const dispatch = (evt) => {
529
+ if (stopped) return;
530
+ if (!self.matchesFilter(evt, filter)) return;
531
+ if (since !== void 0 && evt.seq <= since) return;
532
+ if (pendingResolve) {
533
+ const r = pendingResolve;
534
+ pendingResolve = null;
535
+ r({ value: evt, done: false });
536
+ } else {
537
+ queue.push(evt);
538
+ }
539
+ };
540
+ self.watchers.add(dispatch);
541
+ return {
542
+ next() {
543
+ if (stopped) return Promise.resolve({ value: void 0, done: true });
544
+ const buffered = queue.shift();
545
+ if (buffered) return Promise.resolve({ value: buffered, done: false });
546
+ return new Promise((resolve) => {
547
+ pendingResolve = resolve;
548
+ });
549
+ },
550
+ return() {
551
+ stopped = true;
552
+ self.watchers.delete(dispatch);
553
+ if (pendingResolve) {
554
+ const r = pendingResolve;
555
+ pendingResolve = null;
556
+ r({ value: void 0, done: true });
557
+ }
558
+ return Promise.resolve({ value: void 0, done: true });
559
+ }
560
+ };
561
+ }
562
+ };
563
+ }
564
+ /** Shut down all watch iterators. */
565
+ close() {
566
+ this.closed = true;
567
+ const snapshot = Array.from(this.watchers);
568
+ for (const w of snapshot) {
569
+ try {
570
+ w({
571
+ seq: -1,
572
+ op: "delete",
573
+ ref: { org: "", type: "view", name: "_close" },
574
+ hash: null,
575
+ parentHash: null,
576
+ actor: "system",
577
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
578
+ source: "sys-metadata-repo-close"
579
+ });
580
+ } catch (e3) {
581
+ }
582
+ }
583
+ this.watchers.clear();
584
+ }
585
+ // ── helpers ─────────────────────────────────────────────────────────
586
+ assertOpen() {
587
+ if (this.closed) throw new Error("SysMetadataRepository is closed");
588
+ }
589
+ /**
590
+ * Defense-in-depth authorization gate.
591
+ *
592
+ * `intent` defaults to `'override-artifact'` (the historical strict
593
+ * behavior). The protocol layer passes `'runtime-only'` after it has
594
+ * verified — via the schema registry — that no artifact item exists
595
+ * at `(type, name)`. In that case we accept types with
596
+ * `allowRuntimeCreate: true`, even when `allowOrgOverride` is false.
597
+ *
598
+ * The env-var escape hatch (`OS_METADATA_WRITABLE`) still
599
+ * applies to BOTH intents, so operators can opt into artifact
600
+ * overrides at runtime for emergency fixes.
601
+ */
602
+ assertAllowed(type, intent = "override-artifact") {
603
+ const singular = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[type], () => ( type));
604
+ const allowedByRegistry = OVERLAY_ALLOWED_TYPES.has(singular) || OVERLAY_ALLOWED_TYPES.has(type);
605
+ if (allowedByRegistry) return;
606
+ if (intent === "runtime-only") {
607
+ if (RUNTIME_CREATE_ALLOWED_TYPES.has(singular) || RUNTIME_CREATE_ALLOWED_TYPES.has(type)) {
608
+ return;
609
+ }
610
+ if (!STATIC_REGISTRY_TYPES.has(singular) && !STATIC_REGISTRY_TYPES.has(type)) {
611
+ return;
612
+ }
613
+ }
614
+ const env = envWritableMetadataTypes();
615
+ if (env.has(singular) || env.has(type)) return;
616
+ const allowed = [
617
+ ...OVERLAY_ALLOWED_TYPES,
618
+ ...envWritableMetadataTypes()
619
+ ];
620
+ const code = intent === "runtime-only" ? "not_creatable" : "not_overridable";
621
+ const detail = intent === "runtime-only" ? `'${type}' has neither allowOrgOverride nor allowRuntimeCreate in the registry. ` : `'${type}' is not allowOrgOverride in the registry. `;
622
+ const err = new Error(
623
+ `[${code}] ${detail}Overlay-allowed: ${Array.from(new Set(allowed)).join(", ") || "(none)"}. Set OS_METADATA_WRITABLE to enable additional types at runtime.`
624
+ );
625
+ err.code = code;
626
+ err.status = 403;
627
+ throw err;
628
+ }
629
+ whereFor(ref, state = "active", packageId) {
630
+ const where = {
631
+ type: ref.type,
632
+ name: ref.name,
633
+ organization_id: this.organizationId,
634
+ state
635
+ };
636
+ if (packageId !== void 0) where.package_id = packageId;
637
+ return where;
638
+ }
639
+ fullRef(ref) {
640
+ return {
641
+ org: this.orgLabel,
642
+ type: ref.type,
643
+ name: ref.name
644
+ };
645
+ }
646
+ rowToItem(ref, row) {
647
+ const body = typeof row.metadata === "string" ? JSON.parse(row.metadata) : _nullishCoalesce(row.metadata, () => ( {}));
648
+ const hash = _nullishCoalesce(row.checksum, () => ( _metadatacore.hashSpec.call(void 0, body)));
649
+ return {
650
+ ref: this.fullRef(ref),
651
+ body,
652
+ hash,
653
+ parentHash: null,
654
+ authoredBy: _nullishCoalesce(_nullishCoalesce(row.updated_by, () => ( row.created_by)), () => ( "unknown")),
655
+ authoredAt: _nullishCoalesce(_nullishCoalesce(row.updated_at, () => ( row.created_at)), () => ( (/* @__PURE__ */ new Date()).toISOString())),
656
+ message: void 0,
657
+ seq: this.seqCounter
658
+ };
659
+ }
660
+ broadcast(evt) {
661
+ for (const w of Array.from(this.watchers)) {
662
+ try {
663
+ w(evt);
664
+ } catch (e4) {
665
+ }
666
+ }
667
+ }
668
+ matchesFilter(evt, filter) {
669
+ if (filter.type && evt.ref.type !== filter.type) return false;
670
+ if (filter.name && evt.ref.name !== filter.name) return false;
671
+ if (filter.org && evt.ref.org !== filter.org) return false;
672
+ return true;
673
+ }
674
+ /**
675
+ * Per-org monotonic event sequence. Reads `MAX(event_seq) + 1` from
676
+ * `sys_metadata_history` scoped by `organization_id`. MUST be called
677
+ * inside a transaction (the only caller is the put/delete txn body) —
678
+ * concurrent writers in the same org race otherwise.
679
+ */
680
+ async nextEventSeq(ctx) {
681
+ try {
682
+ const rows = await this.engine.find(this.historyTable, {
683
+ where: { organization_id: this.organizationId },
684
+ context: ctx
685
+ });
686
+ let max = 0;
687
+ for (const row of rows) {
688
+ const v = typeof row.event_seq === "number" ? row.event_seq : 0;
689
+ if (v > max) max = v;
690
+ }
691
+ return max + 1;
692
+ } catch (e5) {
693
+ return 1;
694
+ }
695
+ }
696
+ /**
697
+ * Per-(org,type,name) lineage counter. Reads from history (not from
698
+ * `sys_metadata.version`) so delete + recreate continues incrementing
699
+ * instead of restarting at 1.
700
+ */
701
+ async nextItemVersion(ref, ctx) {
702
+ try {
703
+ const rows = await this.engine.find(this.historyTable, {
704
+ where: {
705
+ organization_id: this.organizationId,
706
+ type: ref.type,
707
+ name: ref.name
708
+ },
709
+ context: ctx
710
+ });
711
+ let max = 0;
712
+ for (const row of rows) {
713
+ const v = typeof row.version === "number" ? row.version : 0;
714
+ if (v > max) max = v;
715
+ }
716
+ return max + 1;
717
+ } catch (e6) {
718
+ return 1;
719
+ }
720
+ }
721
+ /** Lightweight UUID-ish id for history rows; sufficient for an audit log. */
722
+ uuid() {
723
+ if (typeof _optionalChain([globalThis, 'access', _9 => _9.crypto, 'optionalAccess', _10 => _10.randomUUID]) === "function") {
724
+ return globalThis.crypto.randomUUID();
725
+ }
726
+ return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
727
+ }
728
+ };
729
+
730
+ // src/protocol.ts
731
+
732
+ var _data = require('@objectstack/spec/data');
733
+
734
+ var _ui = require('@objectstack/spec/ui');
735
+ var _system = require('@objectstack/spec/system');
736
+
737
+
738
+
739
+
740
+
741
+
742
+
743
+ var _zod = require('zod');
744
+
745
+ // src/metadata-diagnostics.ts
746
+
747
+
748
+ function computeMetadataDiagnostics(type, item) {
749
+ const singular = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[type], () => ( type));
750
+ const schema = _kernel.getMetadataTypeSchema.call(void 0, singular);
751
+ if (!schema) return void 0;
752
+ if (item === null || item === void 0 || typeof item !== "object") {
753
+ return {
754
+ valid: false,
755
+ errors: [{
756
+ path: "",
757
+ message: "Metadata document must be a non-null object",
758
+ code: "invalid_type"
759
+ }]
760
+ };
761
+ }
762
+ const candidate = "_diagnostics" in item ? stripDiagnostics(item) : item;
763
+ const parsed = schema.safeParse(candidate);
764
+ if (parsed.success) {
765
+ return { valid: true };
766
+ }
767
+ const errors = parsed.error.issues.map((issue) => ({
768
+ path: issue.path.map(String).join("."),
769
+ message: issue.message,
770
+ code: issue.code
771
+ }));
772
+ return { valid: false, errors };
773
+ }
774
+ function stripDiagnostics(item) {
775
+ const { _diagnostics: _drop, ...rest } = item;
776
+ void _drop;
777
+ return rest;
778
+ }
779
+ function decorateMetadataItem(type, item) {
780
+ if (!item || typeof item !== "object") return item;
781
+ const diagnostics = computeMetadataDiagnostics(type, item);
782
+ if (!diagnostics) return item;
783
+ return { ...item, _diagnostics: diagnostics };
784
+ }
785
+ function decorateMetadataItems(type, items) {
786
+ if (!Array.isArray(items)) return items;
787
+ return items.map((item) => decorateMetadataItem(type, item));
788
+ }
789
+ function fieldMap(objectDef) {
790
+ const map = /* @__PURE__ */ new Map();
791
+ const fields = _optionalChain([objectDef, 'optionalAccess', _11 => _11.fields]);
792
+ if (Array.isArray(fields)) {
793
+ for (const f of fields) if (_optionalChain([f, 'optionalAccess', _12 => _12.name])) map.set(f.name, f);
794
+ } else if (fields && typeof fields === "object") {
795
+ for (const [name, f] of Object.entries(fields)) map.set(name, _nullishCoalesce(f, () => ( {})));
796
+ }
797
+ return map;
798
+ }
799
+ function computeViewReferenceDiagnostics(view, objectDef) {
800
+ const fields = fieldMap(objectDef);
801
+ const errors = [];
802
+ const requireField = (name, path) => {
803
+ if (typeof name !== "string" || !name) return;
804
+ if (!fields.has(name)) {
805
+ errors.push({
806
+ path,
807
+ message: `Field "${name}" does not exist on the source object`,
808
+ code: "reference_not_found"
809
+ });
810
+ }
811
+ };
812
+ const userFilters = _optionalChain([view, 'optionalAccess', _13 => _13.userFilters]);
813
+ _optionalChain([userFilters, 'optionalAccess', _14 => _14.fields, 'optionalAccess', _15 => _15.forEach, 'call', _16 => _16((f, i) => requireField(_optionalChain([f, 'optionalAccess', _17 => _17.field]), `userFilters.fields.${i}.field`))]);
814
+ _optionalChain([userFilters, 'optionalAccess', _18 => _18.tabs, 'optionalAccess', _19 => _19.forEach, 'call', _20 => _20((t, i) => _optionalChain([t, 'optionalAccess', _21 => _21.filter, 'optionalAccess', _22 => _22.forEach, 'call', _23 => _23((r, j) => requireField(_optionalChain([r, 'optionalAccess', _24 => _24.field]), `userFilters.tabs.${i}.filter.${j}.field`))]))]);
815
+ _optionalChain([view, 'optionalAccess', _25 => _25.tabs, 'optionalAccess', _26 => _26.forEach, 'call', _27 => _27((t, i) => _optionalChain([t, 'optionalAccess', _28 => _28.filter, 'optionalAccess', _29 => _29.forEach, 'call', _30 => _30((r, j) => requireField(_optionalChain([r, 'optionalAccess', _31 => _31.field]), `tabs.${i}.filter.${j}.field`))]))]);
816
+ _optionalChain([view, 'optionalAccess', _32 => _32.filterableFields, 'optionalAccess', _33 => _33.forEach, 'call', _34 => _34((f, i) => requireField(f, `filterableFields.${i}`))]);
817
+ const kanban = _optionalChain([view, 'optionalAccess', _35 => _35.kanban]);
818
+ if (_optionalChain([kanban, 'optionalAccess', _36 => _36.groupByField])) {
819
+ requireField(kanban.groupByField, "kanban.groupByField");
820
+ const def = fields.get(kanban.groupByField);
821
+ if (def && def.type && !["select", "multi-select", "boolean", "lookup", "master_detail", "user"].includes(def.type)) {
822
+ errors.push({
823
+ path: "kanban.groupByField",
824
+ message: `Field "${kanban.groupByField}" (type "${def.type}") cannot group a kanban \u2014 use a select-like field`,
825
+ code: "invalid_binding"
826
+ });
827
+ }
828
+ }
829
+ return errors.length ? { valid: false, errors } : { valid: true };
830
+ }
831
+
832
+ // src/protocol.ts
833
+ var TYPE_TO_FORM = _system.METADATA_FORM_REGISTRY;
834
+ var _jsonSchemaCache = /* @__PURE__ */ new WeakMap();
835
+ function toJsonSchemaSafe(schema) {
836
+ const cached = _jsonSchemaCache.get(schema);
837
+ if (cached !== void 0) return _nullishCoalesce(cached, () => ( void 0));
838
+ try {
839
+ const result = _zod.z.toJSONSchema(schema, { unrepresentable: "any" });
840
+ _jsonSchemaCache.set(schema, result);
841
+ return result;
842
+ } catch (e7) {
843
+ _jsonSchemaCache.set(schema, null);
844
+ return void 0;
845
+ }
846
+ }
847
+ var HAND_CRAFTED_SCHEMAS = {
848
+ object: {
849
+ type: "object",
850
+ properties: {
851
+ name: { type: "string" },
852
+ label: { type: "string" },
853
+ pluralLabel: { type: "string" },
854
+ icon: { type: "string" },
855
+ description: { type: "string" },
856
+ tags: { type: "array", items: { type: "string" } },
857
+ active: { type: "boolean", default: true },
858
+ isSystem: { type: "boolean", default: false },
859
+ abstract: { type: "boolean", default: false },
860
+ datasource: { type: "string" },
861
+ fields: {
862
+ // Canonical Object.fields is a name-keyed map
863
+ // (Record<string, FieldDefinition>) — insertion order is
864
+ // display order. The SchemaForm engine recognises
865
+ // `additionalProperties` as a Record and dispatches to
866
+ // the `record` form-field renderer (ADR-0007). The form
867
+ // layout in `object.form.ts` declares `type: 'record'`
868
+ // so the inner `additionalProperties` schema is used to
869
+ // shape each value.
870
+ type: "object",
871
+ default: {},
872
+ additionalProperties: {
873
+ type: "object",
874
+ properties: {
875
+ name: { type: "string" },
876
+ label: { type: "string" },
877
+ type: { type: "string" },
878
+ required: { type: "boolean", default: false },
879
+ unique: { type: "boolean", default: false },
880
+ defaultValue: {},
881
+ description: { type: "string" }
882
+ },
883
+ required: ["type"]
884
+ }
885
+ },
886
+ capabilities: { type: "object", additionalProperties: true }
887
+ },
888
+ required: ["name"],
889
+ additionalProperties: true
890
+ },
891
+ action: {
892
+ type: "object",
893
+ properties: {
894
+ name: { type: "string" },
895
+ label: { type: "string" },
896
+ objectName: { type: "string" },
897
+ icon: { type: "string" },
898
+ type: { type: "string", enum: ["url", "flow", "api", "script"] },
899
+ variant: { type: "string", enum: ["primary", "secondary", "danger", "ghost", "outline"] },
900
+ target: { type: "string" },
901
+ method: { type: "string", enum: ["GET", "POST", "PUT", "PATCH", "DELETE"] },
902
+ body: {
903
+ type: "array",
904
+ default: [],
905
+ items: {
906
+ type: "object",
907
+ properties: {
908
+ line: { type: "string" }
909
+ }
910
+ }
911
+ },
912
+ params: {
913
+ type: "array",
914
+ default: [],
915
+ items: {
916
+ type: "object",
917
+ properties: {
918
+ name: { type: "string" },
919
+ label: { type: "string" },
920
+ type: { type: "string" },
921
+ required: { type: "boolean", default: false }
922
+ },
923
+ required: ["name"]
924
+ }
925
+ },
926
+ confirmText: { type: "string" },
927
+ successMessage: { type: "string" },
928
+ refreshAfter: { type: "boolean", default: true },
929
+ locations: {
930
+ type: "array",
931
+ default: [],
932
+ items: {
933
+ type: "object",
934
+ properties: {
935
+ location: { type: "string" }
936
+ }
937
+ }
938
+ },
939
+ component: { type: "string" },
940
+ visible: { type: "string" },
941
+ disabled: { type: "string" },
942
+ shortcut: { type: "string" },
943
+ bulkEnabled: { type: "boolean", default: false },
944
+ aiExposed: { type: "boolean", default: false },
945
+ recordIdParam: { type: "string" },
946
+ recordIdField: { type: "string" },
947
+ bodyShape: { type: "string", enum: ["flat", "nested"] }
948
+ },
949
+ required: ["name", "label", "type"],
950
+ additionalProperties: true
951
+ },
952
+ // Validation rules live inside `object.validations[]`. The canonical
953
+ // ValidationRuleSchema is a discriminated union of 9 variants; the
954
+ // generic SchemaForm renderer treats unions as opaque JSON, so we
955
+ // ship a *flat* form-friendly schema covering the common base
956
+ // properties plus every variant-specific field as optional. Save-time
957
+ // validation is unaffected — the union schema is still authoritative
958
+ // at write time.
959
+ validation: {
960
+ type: "object",
961
+ properties: {
962
+ // --- Base fields (all variants) ---
963
+ name: { type: "string", description: "Unique rule name (snake_case)" },
964
+ label: { type: "string" },
965
+ description: { type: "string" },
966
+ type: {
967
+ type: "string",
968
+ enum: [
969
+ "script",
970
+ "unique",
971
+ "state_machine",
972
+ "format",
973
+ "cross_field",
974
+ "json",
975
+ "async",
976
+ "custom",
977
+ "conditional"
978
+ ],
979
+ default: "script",
980
+ description: "Validation variant"
981
+ },
982
+ active: { type: "boolean", default: true },
983
+ events: {
984
+ type: "array",
985
+ items: { type: "string", enum: ["insert", "update", "delete"] },
986
+ default: ["insert", "update"]
987
+ },
988
+ priority: { type: "number", default: 100, minimum: 0, maximum: 9999 },
989
+ severity: {
990
+ type: "string",
991
+ enum: ["error", "warning", "info"],
992
+ default: "error"
993
+ },
994
+ message: { type: "string" },
995
+ tags: { type: "array", items: { type: "string" } },
996
+ // --- Variant-specific (all optional, gated by `type`) ---
997
+ condition: {
998
+ type: "string",
999
+ description: "CEL predicate (type=script). True \u21D2 validation fails."
1000
+ },
1001
+ fields: {
1002
+ type: "array",
1003
+ items: { type: "string" },
1004
+ description: "Fields (type=unique / cross_field)."
1005
+ },
1006
+ scope: { type: "string", description: "CEL scope predicate (type=unique)." },
1007
+ caseSensitive: { type: "boolean", default: true },
1008
+ field: { type: "string", description: "Single field (type=state_machine / format)." },
1009
+ transitions: {
1010
+ type: "object",
1011
+ additionalProperties: { type: "array", items: { type: "string" } },
1012
+ description: "Map { OldState: [AllowedNewStates] } (type=state_machine)."
1013
+ },
1014
+ regex: { type: "string", description: "Regex (type=format)." },
1015
+ format: {
1016
+ type: "string",
1017
+ enum: ["email", "url", "phone", "json"],
1018
+ description: "Built-in format (type=format)."
1019
+ },
1020
+ url: { type: "string", description: "Endpoint URL (type=async)." },
1021
+ handler: { type: "string", description: "Handler reference (type=custom)." },
1022
+ when: { type: "string", description: "Outer condition (type=conditional)." }
1023
+ },
1024
+ required: ["name", "type", "message"],
1025
+ additionalProperties: true
1026
+ }
1027
+ };
1028
+ function resolveOverlaySchema(type, _item) {
1029
+ const singular = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[type], () => ( type));
1030
+ return _nullishCoalesce(_kernel.getMetadataTypeSchema.call(void 0, singular), () => ( null));
1031
+ }
1032
+ function normalizeViewMetadata(type, item, saveName) {
1033
+ const singular = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[type], () => ( type));
1034
+ if (singular !== "view") return item;
1035
+ if (!item || typeof item !== "object" || Array.isArray(item)) return item;
1036
+ const it = item;
1037
+ return it.name ? it : { ...it, name: saveName };
1038
+ }
1039
+ function mergeArtifactProtection(item, artifactItem) {
1040
+ if (item === void 0 || item === null) return item;
1041
+ if (artifactItem === void 0 || artifactItem === null) return item;
1042
+ const a = artifactItem;
1043
+ if (typeof a !== "object") return item;
1044
+ const out = { ...item };
1045
+ if (a._lock !== void 0) out._lock = a._lock;
1046
+ if (a._lockReason !== void 0) out._lockReason = a._lockReason;
1047
+ if (a._lockDocsUrl !== void 0) out._lockDocsUrl = a._lockDocsUrl;
1048
+ if (a._lockSource !== void 0) out._lockSource = a._lockSource;
1049
+ if (a._packageId !== void 0) out._packageId = a._packageId;
1050
+ if (a._packageVersion !== void 0) out._packageVersion = a._packageVersion;
1051
+ if (a._provenance !== void 0) out._provenance = a._provenance;
1052
+ return out;
1053
+ }
1054
+ function simpleHash(str) {
1055
+ let hash = 0;
1056
+ for (let i = 0; i < str.length; i++) {
1057
+ const char = str.charCodeAt(i);
1058
+ hash = (hash << 5) - hash + char;
1059
+ hash = hash & hash;
1060
+ }
1061
+ return Math.abs(hash).toString(16);
1062
+ }
1063
+ var ConcurrentUpdateError = class extends Error {
1064
+ constructor(opts) {
1065
+ super(_nullishCoalesce(opts.message, () => ( "Record was modified by another user")));
1066
+ this.code = "CONCURRENT_UPDATE";
1067
+ this.status = 409;
1068
+ this.name = "ConcurrentUpdateError";
1069
+ this.currentVersion = opts.currentVersion;
1070
+ this.currentRecord = opts.currentRecord;
1071
+ }
1072
+ };
1073
+ function normaliseVersionToken(v) {
1074
+ if (v === null || v === void 0) return null;
1075
+ const s = String(v).trim();
1076
+ if (!s) return null;
1077
+ if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
1078
+ return s.slice(1, -1);
1079
+ }
1080
+ return s;
1081
+ }
1082
+ var CLONE_STRIP_FIELDS = [
1083
+ "id",
1084
+ "created_at",
1085
+ "created_by",
1086
+ "updated_at",
1087
+ "updated_by"
1088
+ ];
1089
+ var SERVICE_CONFIG = {
1090
+ auth: { route: "/api/v1/auth", plugin: "plugin-auth" },
1091
+ automation: { route: "/api/v1/automation", plugin: "plugin-automation" },
1092
+ cache: { route: "/api/v1/cache", plugin: "plugin-redis" },
1093
+ queue: { route: "/api/v1/queue", plugin: "plugin-bullmq" },
1094
+ job: { route: "/api/v1/jobs", plugin: "job-scheduler" },
1095
+ ui: { route: "/api/v1/ui", plugin: "ui-plugin" },
1096
+ workflow: { route: "/api/v1/workflow", plugin: "plugin-workflow" },
1097
+ realtime: { route: "/api/v1/realtime", plugin: "plugin-realtime" },
1098
+ notification: { route: "/api/v1/notifications", plugin: "plugin-notifications" },
1099
+ ai: { route: "/api/v1/ai", plugin: "plugin-ai" },
1100
+ i18n: { route: "/api/v1/i18n", plugin: "service-i18n" },
1101
+ graphql: { route: "/graphql", plugin: "plugin-graphql" },
1102
+ // GraphQL uses /graphql by convention (not versioned REST)
1103
+ "file-storage": { route: "/api/v1/storage", plugin: "plugin-storage" },
1104
+ search: { route: "/api/v1/search", plugin: "plugin-search" }
1105
+ };
1106
+ var REFERENCE_PATHS = {
1107
+ object: [
1108
+ { fromType: "view", paths: ["object", "objectName"], kind: "view" },
1109
+ { fromType: "dashboard", paths: ["widgets[].object", "widgets[].objectName"], kind: "dashboard widget" },
1110
+ { fromType: "flow", paths: ["object", "context.object", "trigger.object", "targetObject"], kind: "flow" },
1111
+ { fromType: "workflow", paths: ["object", "targetObject"], kind: "workflow" },
1112
+ { fromType: "permission", paths: ["objects[].name", "objects[].object"], kind: "permission" },
1113
+ { fromType: "app", paths: ["navItems[].objectName", "navItems[].object", "tabs[].objectName", "tabs[].object"], kind: "app nav" },
1114
+ { fromType: "page", paths: ["object", "objectName"], kind: "page" },
1115
+ { fromType: "report", paths: ["object", "objectName"], kind: "report" },
1116
+ { fromType: "action", paths: ["object", "objectName"], kind: "action" },
1117
+ { fromType: "validation", paths: ["object", "objectName"], kind: "validation" },
1118
+ { fromType: "hook", paths: ["object", "objectName"], kind: "hook" },
1119
+ { fromType: "object", paths: ["fields[].referenceTo", "fields{}.referenceTo", "fields{}.reference"], kind: "field reference" }
1120
+ ],
1121
+ view: [
1122
+ { fromType: "dashboard", paths: ["widgets[].view", "widgets[].viewName"], kind: "dashboard widget" },
1123
+ { fromType: "app", paths: ["navItems[].viewName", "tabs[].viewName"], kind: "app nav" },
1124
+ { fromType: "page", paths: ["viewName"], kind: "page" }
1125
+ ],
1126
+ tool: [
1127
+ { fromType: "agent", paths: ["tools[]", "tools[].name"], kind: "agent tool" }
1128
+ ],
1129
+ skill: [
1130
+ { fromType: "agent", paths: ["skills[]", "skills[].name"], kind: "agent skill" }
1131
+ ],
1132
+ flow: [
1133
+ { fromType: "app", paths: ["navItems[].flowName", "tabs[].flowName"], kind: "app nav" }
1134
+ ],
1135
+ dashboard: [
1136
+ { fromType: "app", paths: ["navItems[].dashboardName", "tabs[].dashboardName"], kind: "app nav" }
1137
+ ],
1138
+ page: [
1139
+ { fromType: "app", paths: ["navItems[].pageName", "tabs[].pageName"], kind: "app nav" }
1140
+ ]
1141
+ };
1142
+ function extractPathValues(item, path) {
1143
+ if (!item || typeof item !== "object") return [];
1144
+ const segments = path.split(".");
1145
+ let current = [item];
1146
+ for (const rawSeg of segments) {
1147
+ let kind = "value";
1148
+ let seg = rawSeg;
1149
+ if (seg.endsWith("[]")) {
1150
+ kind = "array";
1151
+ seg = seg.slice(0, -2);
1152
+ } else if (seg.endsWith("{}")) {
1153
+ kind = "record";
1154
+ seg = seg.slice(0, -2);
1155
+ }
1156
+ const next = [];
1157
+ for (const node of current) {
1158
+ if (!node || typeof node !== "object") continue;
1159
+ let value;
1160
+ if (seg === "") {
1161
+ value = node;
1162
+ } else {
1163
+ value = node[seg];
1164
+ }
1165
+ if (value === void 0 || value === null) continue;
1166
+ if (kind === "array") {
1167
+ if (Array.isArray(value)) {
1168
+ for (const v of value) next.push(v);
1169
+ }
1170
+ } else if (kind === "record") {
1171
+ if (Array.isArray(value)) {
1172
+ for (const v of value) next.push(v);
1173
+ } else if (typeof value === "object") {
1174
+ for (const v of Object.values(value)) next.push(v);
1175
+ }
1176
+ } else {
1177
+ next.push(value);
1178
+ }
1179
+ }
1180
+ current = next;
1181
+ if (current.length === 0) return [];
1182
+ }
1183
+ const out = [];
1184
+ for (const v of current) {
1185
+ if (typeof v === "string" && v.length > 0) out.push(v);
1186
+ else if (v && typeof v === "object" && "name" in v && typeof v.name === "string") {
1187
+ out.push(v.name);
1188
+ }
1189
+ }
1190
+ return out;
1191
+ }
1192
+ function diffShallow(from, to) {
1193
+ const added = [];
1194
+ const removed = [];
1195
+ const changed = [];
1196
+ const fromKeys = new Set(Object.keys(_nullishCoalesce(from, () => ( {}))));
1197
+ const toKeys = new Set(Object.keys(_nullishCoalesce(to, () => ( {}))));
1198
+ for (const k of toKeys) {
1199
+ if (!fromKeys.has(k)) {
1200
+ added.push({ path: k, value: to[k] });
1201
+ } else {
1202
+ const a = from[k];
1203
+ const b = to[k];
1204
+ const aStr = JSON.stringify(a);
1205
+ const bStr = JSON.stringify(b);
1206
+ if (aStr !== bStr) {
1207
+ changed.push({ path: k, from: a, to: b });
1208
+ }
1209
+ }
1210
+ }
1211
+ for (const k of fromKeys) {
1212
+ if (!toKeys.has(k)) {
1213
+ removed.push({ path: k, value: from[k] });
1214
+ }
1215
+ }
1216
+ return { added, removed, changed };
1217
+ }
1218
+ function detectDestructiveObjectChanges(prev, next) {
1219
+ if (!prev || typeof prev !== "object" || !next || typeof next !== "object") return [];
1220
+ const prevFields = prev.fields && typeof prev.fields === "object" ? prev.fields : {};
1221
+ const nextFields = next.fields && typeof next.fields === "object" ? next.fields : {};
1222
+ const issues = [];
1223
+ for (const fname of Object.keys(prevFields)) {
1224
+ if (_optionalChain([prevFields, 'access', _37 => _37[fname], 'optionalAccess', _38 => _38.system])) continue;
1225
+ if (!(fname in nextFields)) {
1226
+ issues.push({
1227
+ code: "field_removed",
1228
+ field: fname,
1229
+ message: `Field '${fname}' removed \u2014 existing data in this column will become inaccessible.`
1230
+ });
1231
+ }
1232
+ }
1233
+ const TYPE_COMPATIBILITY = {
1234
+ text: /* @__PURE__ */ new Set(["textarea", "markdown", "html", "code"]),
1235
+ number: /* @__PURE__ */ new Set([]),
1236
+ boolean: /* @__PURE__ */ new Set([]),
1237
+ date: /* @__PURE__ */ new Set(["datetime"]),
1238
+ datetime: /* @__PURE__ */ new Set(["date"])
1239
+ };
1240
+ for (const fname of Object.keys(nextFields)) {
1241
+ const prevField = prevFields[fname];
1242
+ const nextField = nextFields[fname];
1243
+ if (!prevField) continue;
1244
+ const prevType = prevField.type;
1245
+ const nextType = nextField.type;
1246
+ if (prevType && nextType && prevType !== nextType) {
1247
+ const compatible = _optionalChain([TYPE_COMPATIBILITY, 'access', _39 => _39[prevType], 'optionalAccess', _40 => _40.has, 'call', _41 => _41(nextType)]);
1248
+ if (!compatible) {
1249
+ issues.push({
1250
+ code: "field_type_change",
1251
+ field: fname,
1252
+ message: `Field '${fname}' type changed from '${prevType}' to '${nextType}' \u2014 existing values may not convert cleanly.`
1253
+ });
1254
+ }
1255
+ }
1256
+ if (!prevField.required && nextField.required && nextField.defaultValue === void 0) {
1257
+ issues.push({
1258
+ code: "field_required_no_default",
1259
+ field: fname,
1260
+ message: `Field '${fname}' is now required but has no default value \u2014 existing rows with null values may fail validation.`
1261
+ });
1262
+ }
1263
+ }
1264
+ return issues;
1265
+ }
1266
+ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementation {
1267
+ constructor(engine, getServicesRegistry, getFeedService, environmentId) {
1268
+ /**
1269
+ * Lazily-instantiated SysMetadataRepository per organization. Keyed by
1270
+ * `${organizationId ?? '__env__'}`. Repositories are stateful — they
1271
+ * carry the per-org `seqCounter` and watch subscribers — so we cache
1272
+ * them rather than constructing one per call.
1273
+ */
1274
+ this.overlayRepos = /* @__PURE__ */ new Map();
1275
+ /**
1276
+ * One-time guard for ensuring the overlay-uniqueness UNIQUE INDEX exists
1277
+ * on `sys_metadata`. ADR-0005: scopes overlays by
1278
+ * `(type, name, organization_id, environment_id, scope)` for active rows only.
1279
+ * Idempotent SQL — safe to attempt on every protocol instance.
1280
+ *
1281
+ * Inlined here (rather than importing from @objectstack/metadata/migrations)
1282
+ * to avoid a circular dependency: metadata already depends on objectql.
1283
+ */
1284
+ this.overlayIndexEnsured = false;
1285
+ this.engine = engine;
1286
+ this.getServicesRegistry = getServicesRegistry;
1287
+ this.getFeedService = getFeedService;
1288
+ this.environmentId = environmentId;
1289
+ }
1290
+ /**
1291
+ * Lazily obtain a SysMetadataRepository for the given organization.
1292
+ * Env-wide overlays (organizationId == null) share a singleton under
1293
+ * the `__env__` key.
1294
+ */
1295
+ getOverlayRepo(organizationId) {
1296
+ const key = _nullishCoalesce(organizationId, () => ( "__env__"));
1297
+ let repo = this.overlayRepos.get(key);
1298
+ if (!repo) {
1299
+ repo = new SysMetadataRepository({
1300
+ engine: this.engine,
1301
+ organizationId,
1302
+ orgLabel: _nullishCoalesce(organizationId, () => ( "env"))
1303
+ });
1304
+ this.overlayRepos.set(key, repo);
1305
+ }
1306
+ return repo;
1307
+ }
1308
+ async ensureOverlayIndex() {
1309
+ if (this.overlayIndexEnsured) return;
1310
+ this.overlayIndexEnsured = true;
1311
+ try {
1312
+ const engineAny = this.engine;
1313
+ let driver = _nullishCoalesce(_optionalChain([engineAny, 'optionalAccess', _42 => _42.driver]), () => ( _optionalChain([engineAny, 'optionalAccess', _43 => _43.getDriver, 'optionalCall', _44 => _44()])));
1314
+ if (!driver && _optionalChain([engineAny, 'optionalAccess', _45 => _45.drivers]) instanceof Map) {
1315
+ for (const candidate of engineAny.drivers.values()) {
1316
+ if (candidate && (typeof candidate.raw === "function" || typeof candidate.execute === "function")) {
1317
+ driver = candidate;
1318
+ break;
1319
+ }
1320
+ }
1321
+ }
1322
+ if (!driver) return;
1323
+ const exec = async (sql) => {
1324
+ if (typeof driver.raw === "function") {
1325
+ await driver.raw(sql);
1326
+ } else if (typeof driver.execute === "function") {
1327
+ await driver.execute(sql);
1328
+ } else {
1329
+ throw new Error("driver has neither raw nor execute");
1330
+ }
1331
+ };
1332
+ try {
1333
+ await exec("DROP INDEX IF EXISTS idx_sys_metadata_overlay_active");
1334
+ } catch (e8) {
1335
+ }
1336
+ const partialSql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_sys_metadata_overlay_active ON sys_metadata (type, name, organization_id, COALESCE(package_id, '')) WHERE state = 'active'";
1337
+ const fallbackSql = "CREATE INDEX IF NOT EXISTS idx_sys_metadata_overlay_active ON sys_metadata (type, name, organization_id, package_id)";
1338
+ try {
1339
+ await exec(partialSql);
1340
+ } catch (err) {
1341
+ const msg = err instanceof Error ? err.message : String(err);
1342
+ if (/partial|where clause|syntax/i.test(msg)) {
1343
+ try {
1344
+ await exec(fallbackSql);
1345
+ } catch (e9) {
1346
+ }
1347
+ }
1348
+ }
1349
+ try {
1350
+ await exec("DROP INDEX IF EXISTS idx_sys_metadata_overlay_draft");
1351
+ } catch (e10) {
1352
+ }
1353
+ const draftPartialSql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_sys_metadata_overlay_draft ON sys_metadata (type, name, organization_id, COALESCE(package_id, '')) WHERE state = 'draft'";
1354
+ try {
1355
+ await exec(draftPartialSql);
1356
+ } catch (err) {
1357
+ const msg = err instanceof Error ? err.message : String(err);
1358
+ if (/partial|where clause|syntax/i.test(msg)) {
1359
+ try {
1360
+ await exec(
1361
+ "CREATE INDEX IF NOT EXISTS idx_sys_metadata_overlay_draft ON sys_metadata (type, name, organization_id, package_id)"
1362
+ );
1363
+ } catch (e11) {
1364
+ }
1365
+ }
1366
+ }
1367
+ } catch (e12) {
1368
+ }
1369
+ }
1370
+ /**
1371
+ * Exposes the project scope the protocol is bound to. Consumers like
1372
+ * the HTTP dispatcher use this to decide whether to trust the process-
1373
+ * wide SchemaRegistry or whether they must route a read through the
1374
+ * protocol's environment_id-filtered lookup.
1375
+ */
1376
+ getProjectId() {
1377
+ return this.environmentId;
1378
+ }
1379
+ requireFeedService() {
1380
+ const svc = _optionalChain([this, 'access', _46 => _46.getFeedService, 'optionalCall', _47 => _47()]);
1381
+ if (!svc) {
1382
+ throw new Error("Feed service not available. Install and register service-feed to enable feed operations.");
1383
+ }
1384
+ return svc;
1385
+ }
1386
+ async getDiscovery() {
1387
+ const registeredServices = this.getServicesRegistry ? this.getServicesRegistry() : /* @__PURE__ */ new Map();
1388
+ const services = {
1389
+ // --- Kernel-provided (objectql is an example kernel implementation) ---
1390
+ metadata: { enabled: true, status: "available", route: "/api/v1/meta", provider: "objectql" },
1391
+ data: { enabled: true, status: "available", route: "/api/v1/data", provider: "objectql" },
1392
+ analytics: { enabled: true, status: "available", route: "/api/v1/analytics", provider: "objectql" }
1393
+ };
1394
+ for (const [serviceName, config] of Object.entries(SERVICE_CONFIG)) {
1395
+ if (registeredServices.has(serviceName)) {
1396
+ services[serviceName] = {
1397
+ enabled: true,
1398
+ status: "available",
1399
+ route: config.route,
1400
+ provider: config.plugin
1401
+ };
1402
+ } else {
1403
+ services[serviceName] = {
1404
+ enabled: false,
1405
+ status: "unavailable",
1406
+ message: `Install ${config.plugin} to enable`
1407
+ };
1408
+ }
1409
+ }
1410
+ const serviceToRouteKey = {
1411
+ auth: "auth",
1412
+ automation: "automation",
1413
+ ui: "ui",
1414
+ workflow: "workflow",
1415
+ realtime: "realtime",
1416
+ notification: "notifications",
1417
+ ai: "ai",
1418
+ i18n: "i18n",
1419
+ graphql: "graphql",
1420
+ "file-storage": "storage"
1421
+ };
1422
+ const optionalRoutes = {
1423
+ analytics: "/api/v1/analytics"
1424
+ };
1425
+ for (const [serviceName, config] of Object.entries(SERVICE_CONFIG)) {
1426
+ if (registeredServices.has(serviceName)) {
1427
+ const routeKey = serviceToRouteKey[serviceName];
1428
+ if (routeKey) {
1429
+ optionalRoutes[routeKey] = config.route;
1430
+ }
1431
+ }
1432
+ }
1433
+ if (registeredServices.has("feed")) {
1434
+ services["feed"] = {
1435
+ enabled: true,
1436
+ status: "available",
1437
+ route: "/api/v1/data",
1438
+ provider: "service-feed"
1439
+ };
1440
+ } else {
1441
+ services["feed"] = {
1442
+ enabled: false,
1443
+ status: "unavailable",
1444
+ message: "Install service-feed to enable"
1445
+ };
1446
+ }
1447
+ const routes = {
1448
+ data: "/api/v1/data",
1449
+ metadata: "/api/v1/meta",
1450
+ ...optionalRoutes
1451
+ };
1452
+ const wellKnown = {
1453
+ feed: registeredServices.has("feed"),
1454
+ comments: registeredServices.has("feed"),
1455
+ automation: registeredServices.has("automation"),
1456
+ cron: registeredServices.has("job"),
1457
+ search: registeredServices.has("search"),
1458
+ export: registeredServices.has("automation") || registeredServices.has("queue"),
1459
+ chunkedUpload: registeredServices.has("file-storage")
1460
+ };
1461
+ const capabilities = {};
1462
+ for (const [key, enabled] of Object.entries(wellKnown)) {
1463
+ capabilities[key] = { enabled };
1464
+ }
1465
+ return {
1466
+ version: "1.0",
1467
+ apiName: "ObjectStack API",
1468
+ routes,
1469
+ services,
1470
+ capabilities
1471
+ };
1472
+ }
1473
+ async getMetaTypes() {
1474
+ const schemaTypes = this.engine.registry.getRegisteredTypes();
1475
+ let runtimeTypes = [];
1476
+ try {
1477
+ const services = _optionalChain([this, 'access', _48 => _48.getServicesRegistry, 'optionalCall', _49 => _49()]);
1478
+ const metadataService = _optionalChain([services, 'optionalAccess', _50 => _50.get, 'call', _51 => _51("metadata")]);
1479
+ if (metadataService && typeof metadataService.getRegisteredTypes === "function") {
1480
+ runtimeTypes = await metadataService.getRegisteredTypes();
1481
+ }
1482
+ } catch (e13) {
1483
+ }
1484
+ const allTypes = Array.from(/* @__PURE__ */ new Set([...schemaTypes, ...runtimeTypes]));
1485
+ const writableOverrides = _ObjectStackProtocolImplementation.envWritableTypes();
1486
+ const registryByType = new Map(
1487
+ _kernel.DEFAULT_METADATA_TYPE_REGISTRY.map((e) => [e.type, e])
1488
+ );
1489
+ const entries = allTypes.map((type) => {
1490
+ const singular = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[type], () => ( type));
1491
+ const zodSchema = _kernel.getMetadataTypeSchema.call(void 0, singular);
1492
+ const schema = _nullishCoalesce((zodSchema ? toJsonSchemaSafe(zodSchema) : void 0), () => ( HAND_CRAFTED_SCHEMAS[singular]));
1493
+ const form = TYPE_TO_FORM[singular];
1494
+ const createSeed = _kernel.getMetadataCreateSeed.call(void 0, singular);
1495
+ const typeActions = _kernel.getMetadataTypeActions.call(void 0, singular);
1496
+ const base = registryByType.get(singular);
1497
+ if (base) {
1498
+ const isEnvOverridden = writableOverrides.has(singular);
1499
+ return {
1500
+ ...base,
1501
+ type: singular,
1502
+ schemaId: singular,
1503
+ // API client expects schemaId field
1504
+ allowOrgOverride: base.allowOrgOverride || isEnvOverridden,
1505
+ overrideSource: isEnvOverridden && !base.allowOrgOverride ? "env" : "registry",
1506
+ schema,
1507
+ form,
1508
+ ...createSeed !== void 0 ? { createSeed } : {},
1509
+ // Override the spread `base.actions` with the merged view
1510
+ // (declarative + plugin-registered). Omit when empty to
1511
+ // preserve the prior "no actions key" response shape.
1512
+ ...typeActions.length ? { actions: typeActions } : {}
1513
+ };
1514
+ }
1515
+ return {
1516
+ type: singular,
1517
+ schemaId: singular,
1518
+ // API client expects schemaId field
1519
+ label: singular,
1520
+ description: void 0,
1521
+ filePatterns: [],
1522
+ supportsOverlay: false,
1523
+ allowOrgOverride: writableOverrides.has(singular),
1524
+ allowRuntimeCreate: true,
1525
+ supportsVersioning: false,
1526
+ executionPinned: false,
1527
+ loadOrder: 1e3,
1528
+ domain: "system",
1529
+ overrideSource: writableOverrides.has(singular) ? "env" : "registry",
1530
+ schema,
1531
+ form,
1532
+ ...createSeed !== void 0 ? { createSeed } : {},
1533
+ // Plugin-registered actions on a type with no registry entry.
1534
+ ...typeActions.length ? { actions: typeActions } : {}
1535
+ };
1536
+ }).sort((a, b) => {
1537
+ if (a.domain !== b.domain) return a.domain.localeCompare(b.domain);
1538
+ return a.type.localeCompare(b.type);
1539
+ });
1540
+ return { types: allTypes, entries };
1541
+ }
1542
+ /**
1543
+ * Sweep all (or filtered) metadata types and report entries that
1544
+ * fail spec validation. Powers the Studio governance view
1545
+ * (`GET /api/v1/meta/diagnostics`) and `os doctor`-style CLI
1546
+ * checks.
1547
+ *
1548
+ * `severity` defaults to `'error'` — only entries with at least
1549
+ * one Zod error issue are returned. `'warning'` includes
1550
+ * everything we surface (warnings are reserved for a future lint
1551
+ * layer on top of spec validation).
1552
+ *
1553
+ * `type` may be either a singular (`'view'`) or plural (`'views'`)
1554
+ * identifier; the underlying `getMetaItems` already normalises.
1555
+ *
1556
+ * Implementation note: leverages the `_diagnostics` already
1557
+ * decorated onto items by `getMetaItems()` to avoid running
1558
+ * `safeParse()` twice. For types whose schema is unregistered we
1559
+ * skip silently (they cannot be validated and should not appear
1560
+ * as "valid" either — they are simply opaque to this report).
1561
+ */
1562
+ async getMetaDiagnostics(request = {}) {
1563
+ const includeWarnings = request.severity === "warning";
1564
+ const targetTypes = request.type ? [request.type] : _kernel.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => _kernel.getMetadataTypeSchema.call(void 0, e.type)).map((e) => e.type);
1565
+ const entries = [];
1566
+ const stats = {};
1567
+ let scannedItems = 0;
1568
+ for (const t of targetTypes) {
1569
+ let listed;
1570
+ try {
1571
+ listed = await this.getMetaItems({
1572
+ type: t,
1573
+ organizationId: request.organizationId,
1574
+ packageId: request.packageId
1575
+ });
1576
+ } catch (e14) {
1577
+ continue;
1578
+ }
1579
+ const items = Array.isArray(_optionalChain([listed, 'optionalAccess', _52 => _52.items])) ? listed.items : Array.isArray(listed) ? listed : [];
1580
+ const pkgSet = /* @__PURE__ */ new Set();
1581
+ let lockedCount = 0;
1582
+ for (const item of items) {
1583
+ scannedItems += 1;
1584
+ const pkg = _nullishCoalesce(_optionalChain([item, 'optionalAccess', _53 => _53._packageId]), () => ( null));
1585
+ if (pkg) pkgSet.add(pkg);
1586
+ const lock = _optionalChain([item, 'optionalAccess', _54 => _54._lock]);
1587
+ if (lock && lock !== "none") lockedCount += 1;
1588
+ const diag = _nullishCoalesce(_optionalChain([item, 'optionalAccess', _55 => _55._diagnostics]), () => ( computeMetadataDiagnostics(t, item)));
1589
+ if (!diag) continue;
1590
+ if (diag.valid && !includeWarnings) continue;
1591
+ if (diag.valid && includeWarnings && !_optionalChain([diag, 'access', _56 => _56.warnings, 'optionalAccess', _57 => _57.length])) continue;
1592
+ entries.push({
1593
+ type: t,
1594
+ name: typeof _optionalChain([item, 'optionalAccess', _58 => _58.name]) === "string" ? item.name : "<unknown>",
1595
+ diagnostics: diag
1596
+ });
1597
+ }
1598
+ stats[t] = { count: items.length, locked: lockedCount, packages: [...pkgSet].sort() };
1599
+ }
1600
+ return {
1601
+ entries,
1602
+ total: entries.length,
1603
+ scannedTypes: targetTypes.length,
1604
+ scannedItems,
1605
+ stats
1606
+ };
1607
+ }
1608
+ async getMetaItems(request) {
1609
+ const { packageId } = request;
1610
+ let items = [];
1611
+ if (this.environmentId === void 0) {
1612
+ items = [...this.engine.registry.listItems(request.type, packageId)];
1613
+ if (items.length === 0) {
1614
+ const alt = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( _shared.SINGULAR_TO_PLURAL[request.type]));
1615
+ if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
1616
+ }
1617
+ } else {
1618
+ items = [...this.engine.registry.listItems(request.type, packageId)];
1619
+ if (items.length === 0) {
1620
+ const alt = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( _shared.SINGULAR_TO_PLURAL[request.type]));
1621
+ if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
1622
+ }
1623
+ }
1624
+ try {
1625
+ const orgId = request.organizationId;
1626
+ const queryByOrg = async (oid) => {
1627
+ const whereClause = {
1628
+ type: request.type,
1629
+ state: "active",
1630
+ organization_id: oid
1631
+ };
1632
+ if (packageId) whereClause.package_id = packageId;
1633
+ let rs = await this.engine.find("sys_metadata", { where: whereClause });
1634
+ if (!rs || rs.length === 0) {
1635
+ const alt = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( _shared.SINGULAR_TO_PLURAL[request.type]));
1636
+ if (alt) {
1637
+ const altWhere = { type: alt, state: "active", organization_id: oid };
1638
+ if (packageId) altWhere.package_id = packageId;
1639
+ rs = await this.engine.find("sys_metadata", { where: altWhere });
1640
+ }
1641
+ }
1642
+ return _nullishCoalesce(rs, () => ( []));
1643
+ };
1644
+ const envWideRecords = await queryByOrg(null);
1645
+ const orgRecords = orgId ? await queryByOrg(orgId) : [];
1646
+ const mergedMap = /* @__PURE__ */ new Map();
1647
+ for (const r of envWideRecords) mergedMap.set(r.name, r);
1648
+ for (const r of orgRecords) mergedMap.set(r.name, r);
1649
+ const records = Array.from(mergedMap.values());
1650
+ if (records && records.length > 0) {
1651
+ const byName = /* @__PURE__ */ new Map();
1652
+ for (const existing of items) {
1653
+ const entry = existing;
1654
+ if (entry && typeof entry === "object" && "name" in entry) {
1655
+ byName.set(entry.name, entry);
1656
+ }
1657
+ }
1658
+ for (const record of records) {
1659
+ const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
1660
+ if (data && typeof data === "object" && "name" in data) {
1661
+ const recPkg = _nullishCoalesce(record.package_id, () => ( void 0));
1662
+ if (recPkg && data._packageId === void 0) {
1663
+ data._packageId = recPkg;
1664
+ }
1665
+ byName.set(data.name, data);
1666
+ }
1667
+ if (this.environmentId === void 0 && data && typeof data === "object") {
1668
+ const artifact = this.lookupArtifactItem(request.type, data.name);
1669
+ this.engine.registry.registerItem(
1670
+ request.type,
1671
+ mergeArtifactProtection(data, artifact),
1672
+ "name"
1673
+ );
1674
+ }
1675
+ }
1676
+ items = Array.from(byName.values());
1677
+ }
1678
+ } catch (e15) {
1679
+ }
1680
+ if (request.previewDrafts) {
1681
+ try {
1682
+ const orgId = request.organizationId;
1683
+ const queryDrafts = async (oid) => {
1684
+ const whereClause = { type: request.type, state: "draft", organization_id: oid };
1685
+ if (packageId) whereClause.package_id = packageId;
1686
+ let rs = await this.engine.find("sys_metadata", { where: whereClause });
1687
+ if (!rs || rs.length === 0) {
1688
+ const alt = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( _shared.SINGULAR_TO_PLURAL[request.type]));
1689
+ if (alt) {
1690
+ const altWhere = { type: alt, state: "draft", organization_id: oid };
1691
+ if (packageId) altWhere.package_id = packageId;
1692
+ rs = await this.engine.find("sys_metadata", { where: altWhere });
1693
+ }
1694
+ }
1695
+ return _nullishCoalesce(rs, () => ( []));
1696
+ };
1697
+ const draftRecords = [...await queryDrafts(null), ...orgId ? await queryDrafts(orgId) : []];
1698
+ if (draftRecords.length > 0) {
1699
+ const byName = /* @__PURE__ */ new Map();
1700
+ for (const existing of items) {
1701
+ const entry = existing;
1702
+ if (entry && typeof entry === "object" && "name" in entry) byName.set(entry.name, entry);
1703
+ }
1704
+ for (const record of draftRecords) {
1705
+ const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
1706
+ if (data && typeof data === "object" && "name" in data) {
1707
+ const recPkg = _nullishCoalesce(record.package_id, () => ( void 0));
1708
+ if (recPkg && data._packageId === void 0) data._packageId = recPkg;
1709
+ data._draft = true;
1710
+ byName.set(data.name, data);
1711
+ }
1712
+ }
1713
+ items = Array.from(byName.values());
1714
+ }
1715
+ } catch (e16) {
1716
+ }
1717
+ }
1718
+ try {
1719
+ const services = _optionalChain([this, 'access', _59 => _59.getServicesRegistry, 'optionalCall', _60 => _60()]);
1720
+ const metadataService = _optionalChain([services, 'optionalAccess', _61 => _61.get, 'call', _62 => _62("metadata")]);
1721
+ if (metadataService && typeof metadataService.list === "function") {
1722
+ let runtimeItems = await metadataService.list(request.type);
1723
+ if (packageId && runtimeItems && runtimeItems.length > 0) {
1724
+ runtimeItems = runtimeItems.filter((item) => _optionalChain([item, 'optionalAccess', _63 => _63._packageId]) === packageId);
1725
+ }
1726
+ if (runtimeItems && runtimeItems.length > 0) {
1727
+ const itemMap = /* @__PURE__ */ new Map();
1728
+ for (const item of items) {
1729
+ const entry = item;
1730
+ if (entry && typeof entry === "object" && "name" in entry) {
1731
+ itemMap.set(entry.name, entry);
1732
+ }
1733
+ }
1734
+ for (const item of runtimeItems) {
1735
+ const entry = item;
1736
+ if (entry && typeof entry === "object" && "name" in entry) {
1737
+ if (!itemMap.has(entry.name)) {
1738
+ itemMap.set(entry.name, entry);
1739
+ }
1740
+ }
1741
+ }
1742
+ items = Array.from(itemMap.values());
1743
+ }
1744
+ }
1745
+ } catch (e17) {
1746
+ }
1747
+ if (request.type !== "package" && request.type !== "object" && request.type !== "objects") {
1748
+ items = items.filter(
1749
+ (it) => !this.engine.registry.isPackageDisabled(_optionalChain([it, 'optionalAccess', _64 => _64._packageId]))
1750
+ );
1751
+ }
1752
+ if (request.type === "view" || request.type === "views") {
1753
+ items = items.filter((it) => !_ui.isAggregatedViewContainer.call(void 0, it));
1754
+ }
1755
+ if (request.type === "app" || request.type === "apps") {
1756
+ items = items.map((app) => this.engine.registry.applyNavContributions(app));
1757
+ }
1758
+ return {
1759
+ type: request.type,
1760
+ items: decorateMetadataItems(
1761
+ request.type,
1762
+ items.map((it) => {
1763
+ const a = this.lookupArtifactItem(
1764
+ request.type,
1765
+ _optionalChain([it, 'optionalAccess', _65 => _65.name]),
1766
+ _nullishCoalesce(packageId, () => ( _optionalChain([it, 'optionalAccess', _66 => _66._packageId])))
1767
+ );
1768
+ return mergeArtifactProtection(it, a);
1769
+ })
1770
+ )
1771
+ };
1772
+ }
1773
+ async getMetaItem(request) {
1774
+ let item;
1775
+ const orgId = request.organizationId;
1776
+ const readState = request.state === "draft" ? "draft" : "active";
1777
+ if (request.previewDrafts && readState !== "draft") {
1778
+ try {
1779
+ const findDraft = async (oid) => {
1780
+ const lookup = async (t) => {
1781
+ const base = {
1782
+ type: t,
1783
+ name: request.name,
1784
+ state: "draft",
1785
+ organization_id: oid
1786
+ };
1787
+ if (request.packageId) {
1788
+ const scoped = await this.engine.findOne("sys_metadata", {
1789
+ where: { ...base, package_id: request.packageId }
1790
+ });
1791
+ if (scoped) return scoped;
1792
+ return await this.engine.findOne("sys_metadata", {
1793
+ where: { ...base, package_id: null }
1794
+ });
1795
+ }
1796
+ return await this.engine.findOne("sys_metadata", { where: base });
1797
+ };
1798
+ const rec = await lookup(request.type);
1799
+ if (rec) return rec;
1800
+ const alt = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( _shared.SINGULAR_TO_PLURAL[request.type]));
1801
+ if (alt) return await lookup(alt);
1802
+ return void 0;
1803
+ };
1804
+ const draftRec = await _asyncNullishCoalesce((orgId ? await findDraft(orgId) : void 0), async () => ( await findDraft(null)));
1805
+ if (draftRec) {
1806
+ const draftItem = typeof draftRec.metadata === "string" ? JSON.parse(draftRec.metadata) : draftRec.metadata;
1807
+ if (draftItem && typeof draftItem === "object") {
1808
+ const recPkg = _nullishCoalesce(draftRec.package_id, () => ( void 0));
1809
+ if (recPkg && draftItem._packageId === void 0) draftItem._packageId = recPkg;
1810
+ draftItem._draft = true;
1811
+ }
1812
+ return { type: request.type, name: request.name, item: decorateMetadataItem(request.type, draftItem) };
1813
+ }
1814
+ } catch (e18) {
1815
+ }
1816
+ }
1817
+ try {
1818
+ const findOverlay = async (oid) => {
1819
+ const lookup = async (t) => {
1820
+ const base = {
1821
+ type: t,
1822
+ name: request.name,
1823
+ state: readState,
1824
+ organization_id: oid
1825
+ };
1826
+ if (request.packageId) {
1827
+ const scoped = await this.engine.findOne("sys_metadata", {
1828
+ where: { ...base, package_id: request.packageId }
1829
+ });
1830
+ if (scoped) return scoped;
1831
+ return await this.engine.findOne("sys_metadata", {
1832
+ where: { ...base, package_id: null }
1833
+ });
1834
+ }
1835
+ return await this.engine.findOne("sys_metadata", { where: base });
1836
+ };
1837
+ const rec = await lookup(request.type);
1838
+ if (rec) return rec;
1839
+ const alt = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( _shared.SINGULAR_TO_PLURAL[request.type]));
1840
+ if (alt) return await lookup(alt);
1841
+ return void 0;
1842
+ };
1843
+ const record = await _asyncNullishCoalesce((orgId ? await findOverlay(orgId) : void 0), async () => ( await findOverlay(null)));
1844
+ if (record) {
1845
+ item = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
1846
+ const recPkg = _nullishCoalesce(record.package_id, () => ( void 0));
1847
+ if (recPkg && item && typeof item === "object" && item._packageId === void 0) {
1848
+ item._packageId = recPkg;
1849
+ }
1850
+ }
1851
+ } catch (e19) {
1852
+ }
1853
+ if (readState === "draft") {
1854
+ if (item === void 0) {
1855
+ const err = new Error(
1856
+ `[no_draft] No pending draft exists for ${request.type}/${request.name}.`
1857
+ );
1858
+ err.code = "no_draft";
1859
+ err.status = 404;
1860
+ throw err;
1861
+ }
1862
+ return { type: request.type, name: request.name, item: decorateMetadataItem(request.type, item) };
1863
+ }
1864
+ if (item === void 0) {
1865
+ try {
1866
+ const services = _optionalChain([this, 'access', _67 => _67.getServicesRegistry, 'optionalCall', _68 => _68()]);
1867
+ const metadataService = _optionalChain([services, 'optionalAccess', _69 => _69.get, 'call', _70 => _70("metadata")]);
1868
+ if (metadataService && typeof metadataService.get === "function") {
1869
+ const fromService = await metadataService.get(request.type, request.name, request.packageId);
1870
+ if (fromService !== void 0 && fromService !== null) {
1871
+ item = fromService;
1872
+ } else {
1873
+ const alt = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( _shared.SINGULAR_TO_PLURAL[request.type]));
1874
+ if (alt) {
1875
+ const altFromService = await metadataService.get(alt, request.name, request.packageId);
1876
+ if (altFromService !== void 0 && altFromService !== null) {
1877
+ item = altFromService;
1878
+ }
1879
+ }
1880
+ }
1881
+ }
1882
+ } catch (e20) {
1883
+ }
1884
+ }
1885
+ if (item === void 0) {
1886
+ item = this.engine.registry.getItem(request.type, request.name, request.packageId);
1887
+ if (item === void 0) {
1888
+ const alt = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( _shared.SINGULAR_TO_PLURAL[request.type]));
1889
+ if (alt) item = this.engine.registry.getItem(alt, request.name, request.packageId);
1890
+ }
1891
+ }
1892
+ if ((request.type === "app" || request.type === "apps") && item) {
1893
+ item = this.engine.registry.applyNavContributions(item);
1894
+ }
1895
+ const artifactItem = this.lookupArtifactItem(request.type, request.name, request.packageId);
1896
+ let decorated = decorateMetadataItem(
1897
+ request.type,
1898
+ mergeArtifactProtection(item, artifactItem)
1899
+ );
1900
+ if ((request.type === "view" || request.type === "views") && decorated && typeof decorated === "object") {
1901
+ try {
1902
+ const viewDoc = decorated;
1903
+ const sourceObject = _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_optionalChain([viewDoc, 'optionalAccess', _71 => _71.object]), () => ( _optionalChain([viewDoc, 'optionalAccess', _72 => _72.data, 'optionalAccess', _73 => _73.object]))), () => ( _optionalChain([viewDoc, 'optionalAccess', _74 => _74.objectName]))), () => ( _optionalChain([viewDoc, 'optionalAccess', _75 => _75.list, 'optionalAccess', _76 => _76.data, 'optionalAccess', _77 => _77.object])));
1904
+ const objectDef = typeof sourceObject === "string" ? this.engine.registry.getObject(sourceObject) : void 0;
1905
+ if (objectDef) {
1906
+ const refs = computeViewReferenceDiagnostics(viewDoc, objectDef);
1907
+ if (!refs.valid) {
1908
+ const prior = viewDoc._diagnostics;
1909
+ decorated = {
1910
+ ...viewDoc,
1911
+ _diagnostics: {
1912
+ valid: false,
1913
+ errors: [
1914
+ ...prior && prior.valid === false && Array.isArray(prior.errors) ? prior.errors : [],
1915
+ ..._nullishCoalesce(refs.errors, () => ( []))
1916
+ ]
1917
+ }
1918
+ };
1919
+ }
1920
+ }
1921
+ } catch (e21) {
1922
+ }
1923
+ }
1924
+ const artifactBacked = this.isArtifactBacked(request.type, request.name);
1925
+ const lockState = _kernel.resolveLockState.call(void 0, decorated, artifactBacked);
1926
+ return {
1927
+ type: request.type,
1928
+ name: request.name,
1929
+ item: decorated,
1930
+ lock: lockState.lock,
1931
+ ...lockState.lockReason !== void 0 ? { lockReason: lockState.lockReason } : {},
1932
+ ...lockState.lockSource !== void 0 ? { lockSource: lockState.lockSource } : {},
1933
+ ...lockState.lockDocsUrl !== void 0 ? { lockDocsUrl: lockState.lockDocsUrl } : {},
1934
+ ...lockState.provenance !== void 0 ? { provenance: lockState.provenance } : {},
1935
+ ...lockState.packageId !== void 0 ? { packageId: lockState.packageId } : {},
1936
+ ...lockState.packageVersion !== void 0 ? { packageVersion: lockState.packageVersion } : {},
1937
+ editable: lockState.editable,
1938
+ deletable: lockState.deletable,
1939
+ resettable: lockState.resettable
1940
+ };
1941
+ }
1942
+ /**
1943
+ * Phase 3a-layered-get: return the 3 layers of a metadata item
1944
+ * separately — `code` (artifact-loaded baseline), `overlay` (per-org
1945
+ * customisation row, if any), and `effective` (what `getMetaItem`
1946
+ * would return, i.e. overlay-wins merge).
1947
+ *
1948
+ * Drives the "Code default vs Overlay vs Effective" diff tab in the
1949
+ * generic Metadata Resource Edit page. Admins can see exactly what
1950
+ * was customised and reset selectively.
1951
+ *
1952
+ * `code` is null if no artifact baseline exists; `overlay` is null if
1953
+ * no sys_metadata row exists for the requested scope; `effective` is
1954
+ * never null when either layer exists.
1955
+ */
1956
+ async getMetaItemLayered(request) {
1957
+ const orgId = request.organizationId;
1958
+ let code = null;
1959
+ try {
1960
+ const services = _optionalChain([this, 'access', _78 => _78.getServicesRegistry, 'optionalCall', _79 => _79()]);
1961
+ const metadataService = _optionalChain([services, 'optionalAccess', _80 => _80.get, 'call', _81 => _81("metadata")]);
1962
+ if (metadataService && typeof metadataService.get === "function") {
1963
+ let fromService = await metadataService.get(request.type, request.name, request.packageId);
1964
+ if (fromService === void 0 || fromService === null) {
1965
+ const alt = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( _shared.SINGULAR_TO_PLURAL[request.type]));
1966
+ if (alt) fromService = await metadataService.get(alt, request.name, request.packageId);
1967
+ }
1968
+ if (fromService !== void 0 && fromService !== null) code = fromService;
1969
+ }
1970
+ } catch (e22) {
1971
+ }
1972
+ if (code === null) {
1973
+ let regItem = _nullishCoalesce(this.lookupArtifactItem(request.type, request.name, request.packageId), () => ( this.engine.registry.getItem(request.type, request.name, request.packageId)));
1974
+ if (regItem === void 0) {
1975
+ const alt = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( _shared.SINGULAR_TO_PLURAL[request.type]));
1976
+ if (alt) regItem = this.engine.registry.getItem(alt, request.name, request.packageId);
1977
+ }
1978
+ if (regItem !== void 0) code = regItem;
1979
+ }
1980
+ let overlay = null;
1981
+ let overlayScope = null;
1982
+ try {
1983
+ const findOverlay = async (oid) => {
1984
+ const lookup = async (t) => {
1985
+ const base = {
1986
+ type: t,
1987
+ name: request.name,
1988
+ state: "active",
1989
+ organization_id: oid
1990
+ };
1991
+ if (request.packageId) {
1992
+ const scoped = await this.engine.findOne("sys_metadata", {
1993
+ where: { ...base, package_id: request.packageId }
1994
+ });
1995
+ if (scoped) return scoped;
1996
+ return await this.engine.findOne("sys_metadata", {
1997
+ where: { ...base, package_id: null }
1998
+ });
1999
+ }
2000
+ return await this.engine.findOne("sys_metadata", { where: base });
2001
+ };
2002
+ let rec = await lookup(request.type);
2003
+ if (!rec) {
2004
+ const alt = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( _shared.SINGULAR_TO_PLURAL[request.type]));
2005
+ if (alt) rec = await lookup(alt);
2006
+ }
2007
+ return rec;
2008
+ };
2009
+ if (orgId) {
2010
+ const rec = await findOverlay(orgId);
2011
+ if (rec) {
2012
+ overlay = typeof rec.metadata === "string" ? JSON.parse(rec.metadata) : rec.metadata;
2013
+ overlayScope = "org";
2014
+ }
2015
+ }
2016
+ if (overlay === null) {
2017
+ const rec = await findOverlay(null);
2018
+ if (rec) {
2019
+ overlay = typeof rec.metadata === "string" ? JSON.parse(rec.metadata) : rec.metadata;
2020
+ overlayScope = "env";
2021
+ }
2022
+ }
2023
+ } catch (e23) {
2024
+ }
2025
+ const effective = _nullishCoalesce(overlay, () => ( code));
2026
+ const _diagnostics = effective !== null && effective !== void 0 ? computeMetadataDiagnostics(request.type, effective) : void 0;
2027
+ const artifactBacked = this.isArtifactBacked(request.type, request.name);
2028
+ const lockSource = _nullishCoalesce(_nullishCoalesce(code, () => ( overlay)), () => ( {}));
2029
+ const lockState = _kernel.resolveLockState.call(void 0, lockSource, artifactBacked);
2030
+ return {
2031
+ type: request.type,
2032
+ name: request.name,
2033
+ code,
2034
+ overlay,
2035
+ overlayScope,
2036
+ effective,
2037
+ ..._diagnostics ? { _diagnostics } : {},
2038
+ lock: lockState.lock,
2039
+ ...lockState.lockReason !== void 0 ? { lockReason: lockState.lockReason } : {},
2040
+ ...lockState.lockSource !== void 0 ? { lockSource: lockState.lockSource } : {},
2041
+ ...lockState.lockDocsUrl !== void 0 ? { lockDocsUrl: lockState.lockDocsUrl } : {},
2042
+ ...lockState.provenance !== void 0 ? { provenance: lockState.provenance } : {},
2043
+ ...lockState.packageId !== void 0 ? { packageId: lockState.packageId } : {},
2044
+ ...lockState.packageVersion !== void 0 ? { packageVersion: lockState.packageVersion } : {},
2045
+ editable: lockState.editable,
2046
+ deletable: lockState.deletable,
2047
+ resettable: lockState.resettable
2048
+ };
2049
+ }
2050
+ /**
2051
+ * ADR-0010 §3.6 / Phase 4.1 — read the metadata-protection audit log
2052
+ * for a single item. Returns the most-recent rows of
2053
+ * `sys_metadata_audit` for this (type, name) tuple, sorted newest
2054
+ * first. Refused (`denied`) and forced (`forced`) writes both appear
2055
+ * here — they never reach the `history` endpoint, which only tracks
2056
+ * successful body snapshots.
2057
+ *
2058
+ * The table is provisioned by `platform-objects` and is the
2059
+ * compliance surface for the lock-enforcement story. When the
2060
+ * environment has not yet provisioned the table (legacy install
2061
+ * prior to ADR-0010) the call returns `{ events: [] }` instead of
2062
+ * raising, keeping the Studio tab harmless.
2063
+ */
2064
+ async auditMetaItem(request) {
2065
+ const singular = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( request.type));
2066
+ const limit = Math.min(
2067
+ Math.max(1, _nullishCoalesce(request.limit, () => ( 100))),
2068
+ 500
2069
+ );
2070
+ try {
2071
+ const where = {
2072
+ type: singular,
2073
+ name: request.name
2074
+ };
2075
+ const rows = await this.engine.find("sys_metadata_audit", {
2076
+ where,
2077
+ orderBy: [{ field: "occurred_at", direction: "desc" }],
2078
+ limit
2079
+ });
2080
+ const events = (Array.isArray(rows) ? rows : []).map((r) => ({
2081
+ id: r.id,
2082
+ occurredAt: typeof r.occurred_at === "string" ? r.occurred_at : r.occurred_at instanceof Date ? r.occurred_at.toISOString() : String(_nullishCoalesce(r.occurred_at, () => ( ""))),
2083
+ actor: String(_nullishCoalesce(r.actor, () => ( "system"))),
2084
+ source: _nullishCoalesce(r.source, () => ( null)),
2085
+ operation: r.operation,
2086
+ outcome: r.outcome,
2087
+ code: String(_nullishCoalesce(r.code, () => ( ""))),
2088
+ lockState: _nullishCoalesce(r.lock_state, () => ( null)),
2089
+ lockOverridden: Boolean(r.lock_overridden),
2090
+ requestId: _nullishCoalesce(r.request_id, () => ( null)),
2091
+ note: _nullishCoalesce(r.note, () => ( null))
2092
+ }));
2093
+ return { events };
2094
+ } catch (err) {
2095
+ console.warn(
2096
+ `[Protocol] auditMetaItem read failed for ${request.type}/${request.name}: ${_nullishCoalesce(_optionalChain([err, 'optionalAccess', _82 => _82.message]), () => ( err))}`
2097
+ );
2098
+ return { events: [] };
2099
+ }
2100
+ }
2101
+ async getUiView(request) {
2102
+ const schema = this.engine.registry.getObject(request.object);
2103
+ if (!schema) throw new Error(`Object ${request.object} not found`);
2104
+ const fields = schema.fields || {};
2105
+ const fieldKeys = Object.keys(fields);
2106
+ if (request.type === "list") {
2107
+ const priorityFields = ["name", "title", "label", "subject", "email", "status", "type", "category", "created_at"];
2108
+ let columns = fieldKeys.filter((k) => priorityFields.includes(k));
2109
+ if (columns.length < 5) {
2110
+ const remaining = fieldKeys.filter((k) => !columns.includes(k) && k !== "id" && !fields[k].hidden);
2111
+ columns = [...columns, ...remaining.slice(0, 5 - columns.length)];
2112
+ }
2113
+ return {
2114
+ list: {
2115
+ type: "grid",
2116
+ object: request.object,
2117
+ label: schema.label || schema.name,
2118
+ columns: columns.map((f) => ({
2119
+ field: f,
2120
+ label: _optionalChain([fields, 'access', _83 => _83[f], 'optionalAccess', _84 => _84.label]) || f,
2121
+ sortable: true
2122
+ })),
2123
+ sort: fields["created_at"] ? [{ field: "created_at", order: "desc" }] : void 0,
2124
+ searchableFields: columns.slice(0, 3)
2125
+ // Make first few textual columns searchable
2126
+ }
2127
+ };
2128
+ } else {
2129
+ const formFields = fieldKeys.filter((k) => k !== "id" && k !== "created_at" && k !== "updated_at" && !fields[k].hidden).map((f) => ({
2130
+ field: f,
2131
+ label: _optionalChain([fields, 'access', _85 => _85[f], 'optionalAccess', _86 => _86.label]),
2132
+ required: _optionalChain([fields, 'access', _87 => _87[f], 'optionalAccess', _88 => _88.required]),
2133
+ readonly: _optionalChain([fields, 'access', _89 => _89[f], 'optionalAccess', _90 => _90.readonly]),
2134
+ type: _optionalChain([fields, 'access', _91 => _91[f], 'optionalAccess', _92 => _92.type]),
2135
+ // Default to 2 columns for most, 1 for textareas
2136
+ colSpan: _optionalChain([fields, 'access', _93 => _93[f], 'optionalAccess', _94 => _94.type]) === "textarea" || _optionalChain([fields, 'access', _95 => _95[f], 'optionalAccess', _96 => _96.type]) === "html" ? 2 : 1
2137
+ }));
2138
+ return {
2139
+ form: {
2140
+ type: "simple",
2141
+ object: request.object,
2142
+ label: `Edit ${schema.label || schema.name}`,
2143
+ sections: [
2144
+ {
2145
+ label: "General Information",
2146
+ columns: 2,
2147
+ collapsible: false,
2148
+ collapsed: false,
2149
+ fields: formFields
2150
+ }
2151
+ ]
2152
+ }
2153
+ };
2154
+ }
2155
+ }
2156
+ async findData(request) {
2157
+ const options = { ...request.query };
2158
+ if (request.context !== void 0) {
2159
+ options.context = request.context;
2160
+ }
2161
+ for (const [dollar, bare] of [
2162
+ ["$top", "top"],
2163
+ ["$skip", "skip"],
2164
+ ["$orderby", "orderBy"],
2165
+ ["$select", "select"],
2166
+ ["$count", "count"],
2167
+ ["$search", "search"],
2168
+ ["$searchFields", "searchFields"]
2169
+ ]) {
2170
+ if (options[dollar] != null && options[bare] == null) {
2171
+ options[bare] = options[dollar];
2172
+ }
2173
+ delete options[dollar];
2174
+ }
2175
+ if (options.top != null) {
2176
+ options.limit = Number(options.top);
2177
+ delete options.top;
2178
+ }
2179
+ if (options.skip != null) {
2180
+ options.offset = Number(options.skip);
2181
+ delete options.skip;
2182
+ }
2183
+ if (options.limit != null) options.limit = Number(options.limit);
2184
+ if (options.offset != null) options.offset = Number(options.offset);
2185
+ if (typeof options.select === "string") {
2186
+ options.fields = options.select.split(",").map((s) => s.trim()).filter(Boolean);
2187
+ } else if (Array.isArray(options.select)) {
2188
+ options.fields = options.select;
2189
+ }
2190
+ if (options.select !== void 0) delete options.select;
2191
+ const sortValue = _nullishCoalesce(options.orderBy, () => ( options.sort));
2192
+ if (typeof sortValue === "string") {
2193
+ const parsed = sortValue.split(",").map((part) => {
2194
+ const trimmed = part.trim();
2195
+ if (trimmed.startsWith("-")) {
2196
+ return { field: trimmed.slice(1), order: "desc" };
2197
+ }
2198
+ const [field, order] = trimmed.split(/\s+/);
2199
+ return { field, order: _optionalChain([order, 'optionalAccess', _97 => _97.toLowerCase, 'call', _98 => _98()]) === "desc" ? "desc" : "asc" };
2200
+ }).filter((s) => s.field);
2201
+ options.orderBy = parsed;
2202
+ } else if (Array.isArray(sortValue)) {
2203
+ options.orderBy = sortValue;
2204
+ }
2205
+ delete options.sort;
2206
+ const filterValue = _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(options.filter, () => ( options.filters)), () => ( options.$filter)), () => ( options.where));
2207
+ delete options.filter;
2208
+ delete options.filters;
2209
+ delete options.$filter;
2210
+ if (filterValue !== void 0) {
2211
+ let parsedFilter = filterValue;
2212
+ if (typeof parsedFilter === "string") {
2213
+ try {
2214
+ parsedFilter = JSON.parse(parsedFilter);
2215
+ } catch (e24) {
2216
+ }
2217
+ }
2218
+ if (_data.isFilterAST.call(void 0, parsedFilter)) {
2219
+ parsedFilter = _data.parseFilterAST.call(void 0, parsedFilter);
2220
+ }
2221
+ options.where = parsedFilter;
2222
+ }
2223
+ const populateValue = options.populate;
2224
+ const expandValue = _nullishCoalesce(options.$expand, () => ( options.expand));
2225
+ const expandNames = [];
2226
+ if (typeof populateValue === "string") {
2227
+ expandNames.push(...populateValue.split(",").map((s) => s.trim()).filter(Boolean));
2228
+ } else if (Array.isArray(populateValue)) {
2229
+ expandNames.push(...populateValue);
2230
+ }
2231
+ if (!expandNames.length && expandValue) {
2232
+ if (typeof expandValue === "string") {
2233
+ expandNames.push(...expandValue.split(",").map((s) => s.trim()).filter(Boolean));
2234
+ } else if (Array.isArray(expandValue)) {
2235
+ expandNames.push(...expandValue);
2236
+ }
2237
+ }
2238
+ delete options.populate;
2239
+ delete options.$expand;
2240
+ if (typeof options.expand !== "object" || options.expand === null) {
2241
+ delete options.expand;
2242
+ }
2243
+ if (expandNames.length > 0 && !options.expand) {
2244
+ options.expand = {};
2245
+ for (const rel of expandNames) {
2246
+ options.expand[rel] = { object: rel };
2247
+ }
2248
+ }
2249
+ for (const key of ["distinct", "count"]) {
2250
+ if (options[key] === "true") options[key] = true;
2251
+ else if (options[key] === "false") options[key] = false;
2252
+ }
2253
+ const knownParams = /* @__PURE__ */ new Set([
2254
+ "top",
2255
+ "limit",
2256
+ "offset",
2257
+ "orderBy",
2258
+ "fields",
2259
+ "where",
2260
+ "expand",
2261
+ "distinct",
2262
+ "count",
2263
+ "aggregations",
2264
+ "groupBy",
2265
+ "search",
2266
+ "searchFields",
2267
+ "context",
2268
+ "cursor"
2269
+ ]);
2270
+ if (!options.where) {
2271
+ const implicitFilters = {};
2272
+ for (const key of Object.keys(options)) {
2273
+ if (!knownParams.has(key)) {
2274
+ implicitFilters[key] = options[key];
2275
+ delete options[key];
2276
+ }
2277
+ }
2278
+ if (Object.keys(implicitFilters).length > 0) {
2279
+ options.where = implicitFilters;
2280
+ }
2281
+ }
2282
+ const hasGroupBy = Array.isArray(options.groupBy) && options.groupBy.length > 0;
2283
+ const hasAggregations = Array.isArray(options.aggregations) && options.aggregations.length > 0;
2284
+ if (hasGroupBy || hasAggregations) {
2285
+ const records2 = await this.engine.aggregate(request.object, {
2286
+ where: options.where,
2287
+ groupBy: options.groupBy,
2288
+ aggregations: options.aggregations,
2289
+ context: options.context
2290
+ });
2291
+ const limited = typeof options.limit === "number" && options.limit > 0 ? records2.slice(0, options.limit) : records2;
2292
+ return {
2293
+ object: request.object,
2294
+ records: limited,
2295
+ total: records2.length,
2296
+ hasMore: limited.length < records2.length
2297
+ };
2298
+ }
2299
+ const records = await this.engine.find(request.object, options);
2300
+ const pageLimit = typeof options.limit === "number" && options.limit > 0 ? options.limit : void 0;
2301
+ const pageOffset = typeof options.offset === "number" && options.offset > 0 ? options.offset : 0;
2302
+ let total = records.length;
2303
+ let hasMore = false;
2304
+ if (pageLimit !== void 0) {
2305
+ const countable = options.search == null && options.distinct == null;
2306
+ if (countable) {
2307
+ try {
2308
+ total = await this.engine.count(request.object, {
2309
+ where: options.where,
2310
+ context: options.context
2311
+ });
2312
+ } catch (e25) {
2313
+ total = pageOffset + records.length;
2314
+ }
2315
+ hasMore = pageOffset + records.length < total;
2316
+ } else {
2317
+ hasMore = records.length === pageLimit;
2318
+ total = pageOffset + records.length + (hasMore ? 1 : 0);
2319
+ }
2320
+ }
2321
+ return {
2322
+ object: request.object,
2323
+ records,
2324
+ total,
2325
+ hasMore
2326
+ };
2327
+ }
2328
+ async getData(request) {
2329
+ const queryOptions = {
2330
+ where: { id: request.id }
2331
+ };
2332
+ if (request.context !== void 0) {
2333
+ queryOptions.context = request.context;
2334
+ }
2335
+ if (request.select) {
2336
+ queryOptions.fields = typeof request.select === "string" ? request.select.split(",").map((s) => s.trim()).filter(Boolean) : request.select;
2337
+ }
2338
+ if (request.expand) {
2339
+ const expandNames = typeof request.expand === "string" ? request.expand.split(",").map((s) => s.trim()).filter(Boolean) : request.expand;
2340
+ queryOptions.expand = {};
2341
+ for (const rel of expandNames) {
2342
+ queryOptions.expand[rel] = { object: rel };
2343
+ }
2344
+ }
2345
+ const result = await this.engine.findOne(request.object, queryOptions);
2346
+ if (result) {
2347
+ return {
2348
+ object: request.object,
2349
+ id: request.id,
2350
+ record: result
2351
+ };
2352
+ }
2353
+ const err = new Error(`Record ${request.id} not found in ${request.object}`);
2354
+ err.code = "RECORD_NOT_FOUND";
2355
+ err.status = 404;
2356
+ err.object = request.object;
2357
+ throw err;
2358
+ }
2359
+ async createData(request) {
2360
+ const result = await this.engine.insert(
2361
+ request.object,
2362
+ request.data,
2363
+ request.context !== void 0 ? { context: request.context } : void 0
2364
+ );
2365
+ return {
2366
+ object: request.object,
2367
+ id: result.id,
2368
+ record: result
2369
+ };
2370
+ }
2371
+ /**
2372
+ * Clone a record — read the source, drop engine-owned columns, and
2373
+ * insert a fresh copy. Gated by the object's `enable.clone` capability
2374
+ * (default `true`; only an explicit `enable.clone === false` disables it).
2375
+ *
2376
+ * Shallow by design: it duplicates the record's own scalar/business field
2377
+ * values, not its related child records. The insert path re-stamps audit
2378
+ * columns, regenerates `autonumber` fields, and recomputes derived
2379
+ * (`formula`/`summary`) fields, so the copy is a valid new row rather than
2380
+ * a byte-identical twin. Caller-supplied `overrides` are applied last and
2381
+ * win over the copied values — the natural place to set a new `name`,
2382
+ * clear a unique field, or reset status before insert.
2383
+ */
2384
+ async cloneData(request) {
2385
+ const schema = this.engine.registry.getObject(request.object);
2386
+ if (!schema) {
2387
+ const err = new Error(`Object '${request.object}' not found`);
2388
+ err.code = "OBJECT_NOT_FOUND";
2389
+ err.status = 404;
2390
+ err.object = request.object;
2391
+ throw err;
2392
+ }
2393
+ if (_optionalChain([schema, 'access', _99 => _99.enable, 'optionalAccess', _100 => _100.clone]) === false) {
2394
+ const err = new Error(`Cloning is disabled for object '${request.object}'`);
2395
+ err.code = "CLONE_DISABLED";
2396
+ err.status = 403;
2397
+ err.object = request.object;
2398
+ throw err;
2399
+ }
2400
+ const ctx = request.context;
2401
+ const ctxOpt = ctx !== void 0 ? { context: ctx } : void 0;
2402
+ const source = await this.engine.findOne(
2403
+ request.object,
2404
+ { where: { id: request.id }, ...ctxOpt }
2405
+ );
2406
+ if (!source) {
2407
+ const err = new Error(`Record ${request.id} not found in ${request.object}`);
2408
+ err.code = "RECORD_NOT_FOUND";
2409
+ err.status = 404;
2410
+ err.object = request.object;
2411
+ throw err;
2412
+ }
2413
+ const data = { ...source };
2414
+ for (const f of CLONE_STRIP_FIELDS) delete data[f];
2415
+ const fields = schema.fields || {};
2416
+ for (const [name, def] of Object.entries(fields)) {
2417
+ if (!def) continue;
2418
+ if (def.system === true || def.type === "autonumber" || def.type === "formula" || def.type === "summary") {
2419
+ delete data[name];
2420
+ }
2421
+ }
2422
+ if (request.overrides && typeof request.overrides === "object") {
2423
+ Object.assign(data, request.overrides);
2424
+ }
2425
+ const result = await this.engine.insert(request.object, data, ctxOpt);
2426
+ return {
2427
+ object: request.object,
2428
+ id: result.id,
2429
+ sourceId: request.id,
2430
+ record: result
2431
+ };
2432
+ }
2433
+ async updateData(request) {
2434
+ await this.assertVersionMatch(request.object, request.id, request.expectedVersion, request.context);
2435
+ const opts = { where: { id: request.id } };
2436
+ if (request.context !== void 0) opts.context = request.context;
2437
+ const result = await this.engine.update(request.object, request.data, opts);
2438
+ return {
2439
+ object: request.object,
2440
+ id: request.id,
2441
+ record: result
2442
+ };
2443
+ }
2444
+ async deleteData(request) {
2445
+ await this.assertVersionMatch(request.object, request.id, request.expectedVersion, request.context);
2446
+ const opts = { where: { id: request.id } };
2447
+ if (request.context !== void 0) opts.context = request.context;
2448
+ await this.engine.delete(request.object, opts);
2449
+ return {
2450
+ object: request.object,
2451
+ id: request.id,
2452
+ success: true
2453
+ };
2454
+ }
2455
+ /**
2456
+ * Optimistic Concurrency Control gate shared by updateData/deleteData.
2457
+ *
2458
+ * When the caller passes a non-empty `expectedVersion` token (typically
2459
+ * the `updated_at` value they read), this fetches the current record
2460
+ * and compares its `updated_at` against the token. Mismatch → throw
2461
+ * `ConcurrentUpdateError` which the REST layer maps to 409.
2462
+ *
2463
+ * Behaviour:
2464
+ * - Empty/missing token → no check (opt-in semantics; existing callers
2465
+ * that haven't yet adopted OCC are unaffected).
2466
+ * - Record not found → no check; downstream `engine.update` will
2467
+ * surface the usual `RECORD_NOT_FOUND` 404. We intentionally do not
2468
+ * treat "missing record" as a concurrency conflict.
2469
+ * - Record has no `updated_at` field (timestamps disabled) → no check.
2470
+ * Logging would be noisy here; OCC is opt-in and the absence of a
2471
+ * version column is an explicit "this object doesn't support OCC"
2472
+ * signal.
2473
+ */
2474
+ async assertVersionMatch(object, id, expectedVersion, context) {
2475
+ const expected = normaliseVersionToken(expectedVersion);
2476
+ if (!expected) return;
2477
+ const findOpts = { where: { id } };
2478
+ if (context !== void 0) findOpts.context = context;
2479
+ const current = await this.engine.findOne(object, findOpts);
2480
+ if (!current) return;
2481
+ const currentVersion = normaliseVersionToken(current.updated_at);
2482
+ if (!currentVersion) return;
2483
+ if (currentVersion !== expected) {
2484
+ throw new ConcurrentUpdateError({
2485
+ currentVersion,
2486
+ currentRecord: current,
2487
+ message: `Record ${object}/${id} was modified by another user (current version ${currentVersion}, expected ${expected})`
2488
+ });
2489
+ }
2490
+ }
2491
+ // ==========================================
2492
+ // Global Search (M10.5)
2493
+ // ==========================================
2494
+ /**
2495
+ * Cross-object substring search across all registered objects that opt in
2496
+ * via `enable.searchable !== false` and `enable.apiEnabled !== false`.
2497
+ * Searches text-like fields (text/textarea/email/url/phone/markdown/html/string)
2498
+ * whose `searchable: true` flag is set, falling back to the object's
2499
+ * `displayNameField` (or `name`) when no fields are explicitly searchable.
2500
+ *
2501
+ * The query is split into whitespace-separated terms; each term must match
2502
+ * (case-insensitive LIKE) at least one searchable field. RBAC/RLS is
2503
+ * enforced by forwarding the caller's `context` to `engine.find` so users
2504
+ * only see records they are entitled to read.
2505
+ */
2506
+ async searchAll(request) {
2507
+ const q = (_nullishCoalesce(request.q, () => ( ""))).trim();
2508
+ if (!q) {
2509
+ return { query: "", hits: [], totalObjects: 0, totalHits: 0, truncated: false };
2510
+ }
2511
+ const overallLimit = Math.max(1, Math.min(100, Number(_nullishCoalesce(request.limit, () => ( 20)))));
2512
+ const perObject = Math.max(1, Math.min(25, Number(_nullishCoalesce(request.perObject, () => ( 5)))));
2513
+ const objectsFilter = request.objects && request.objects.length ? new Set(request.objects) : null;
2514
+ const terms = q.split(/\s+/).filter(Boolean).slice(0, 8);
2515
+ const allObjects = _nullishCoalesce(_optionalChain([this, 'access', _101 => _101.engine, 'access', _102 => _102.registry, 'optionalAccess', _103 => _103.getAllObjects, 'optionalCall', _104 => _104()]), () => ( []));
2516
+ const hits = [];
2517
+ let objectsScanned = 0;
2518
+ for (const obj of allObjects) {
2519
+ if (hits.length >= overallLimit) break;
2520
+ if (!_optionalChain([obj, 'optionalAccess', _105 => _105.name])) continue;
2521
+ if (objectsFilter && !objectsFilter.has(obj.name)) continue;
2522
+ const enable = _nullishCoalesce(obj.enable, () => ( {}));
2523
+ if (enable.searchable === false) continue;
2524
+ if (enable.apiEnabled === false) continue;
2525
+ if (obj.name.startsWith("sys_audit_log") || obj.name.startsWith("sys_activity") || obj.name.startsWith("sys_session") || obj.name.startsWith("sys_presence") || obj.name.startsWith("sys_metadata") || obj.name.startsWith("sys_account")) {
2526
+ continue;
2527
+ }
2528
+ const fieldsRaw = obj.fields;
2529
+ const fields = Array.isArray(fieldsRaw) ? fieldsRaw : fieldsRaw && typeof fieldsRaw === "object" ? Object.entries(fieldsRaw).map(([name, f]) => ({ name, ...f || {} })) : [];
2530
+ const TEXT_TYPES = /* @__PURE__ */ new Set(["text", "textarea", "string", "email", "url", "phone", "markdown", "html"]);
2531
+ const fieldByName = new Map(fields.map((f) => [f.name, f]));
2532
+ const hasField = (n) => fieldByName.has(n);
2533
+ const titleFormatSource = obj.titleFormat && (obj.titleFormat.source || obj.titleFormat) || void 0;
2534
+ const renderTitle = (row) => {
2535
+ if (typeof titleFormatSource === "string") {
2536
+ let allResolved = true;
2537
+ const rendered = titleFormatSource.replace(/\{\{?\s*([a-zA-Z0-9_.]+)\s*\}?\}/g, (_m, key) => {
2538
+ const v = row[key];
2539
+ if (v == null || v === "") {
2540
+ allResolved = false;
2541
+ return "";
2542
+ }
2543
+ return String(v);
2544
+ }).trim();
2545
+ if (rendered && allResolved) return rendered;
2546
+ if (rendered) return rendered.replace(/\s+-\s+$/, "").replace(/^\s+-\s+/, "").trim() || row.id;
2547
+ }
2548
+ const candidates = [
2549
+ obj.displayNameField,
2550
+ "name",
2551
+ "full_name",
2552
+ "title",
2553
+ "subject",
2554
+ "label",
2555
+ "company"
2556
+ ].filter((c) => typeof c === "string" && hasField(c));
2557
+ for (const c of candidates) {
2558
+ const v = row[c];
2559
+ if (v != null && String(v).trim()) return String(v);
2560
+ }
2561
+ const fn = row.first_name, ln = row.last_name;
2562
+ if (fn || ln) return `${_nullishCoalesce(fn, () => ( ""))} ${_nullishCoalesce(ln, () => ( ""))}`.trim();
2563
+ return String(row.id);
2564
+ };
2565
+ const titleFieldName = obj.displayNameField || (hasField("name") ? "name" : void 0) || (hasField("title") ? "title" : void 0) || _optionalChain([fields, 'access', _106 => _106.find, 'call', _107 => _107((f) => TEXT_TYPES.has(f.type)), 'optionalAccess', _108 => _108.name]);
2566
+ let searchableFields = fields.filter((f) => f && TEXT_TYPES.has(f.type) && f.searchable === true).map((f) => f.name);
2567
+ if (searchableFields.length === 0 && titleFieldName) {
2568
+ searchableFields = [titleFieldName];
2569
+ }
2570
+ if (searchableFields.length === 0) continue;
2571
+ objectsScanned++;
2572
+ const andClauses = terms.map((term) => ({
2573
+ $or: searchableFields.map((f) => ({ [f]: { $contains: term } }))
2574
+ }));
2575
+ const where = andClauses.length === 1 ? andClauses[0] : { $and: andClauses };
2576
+ try {
2577
+ const opts = {
2578
+ where,
2579
+ limit: perObject,
2580
+ orderBy: [{ field: "updated_at", direction: "desc" }]
2581
+ };
2582
+ if (request.context !== void 0) opts.context = request.context;
2583
+ const rows = await this.engine.find(obj.name, opts);
2584
+ for (const row of rows || []) {
2585
+ if (hits.length >= overallLimit) break;
2586
+ const title = renderTitle(row);
2587
+ let snippet;
2588
+ for (const f of searchableFields) {
2589
+ const v = row[f];
2590
+ if (typeof v === "string" && v) {
2591
+ const lc = v.toLowerCase();
2592
+ const idx = terms.map((t) => lc.indexOf(t.toLowerCase())).find((i) => i >= 0);
2593
+ if (idx != null && idx >= 0) {
2594
+ const start = Math.max(0, idx - 30);
2595
+ const end = Math.min(v.length, idx + 90);
2596
+ snippet = (start > 0 ? "\u2026" : "") + v.slice(start, end) + (end < v.length ? "\u2026" : "");
2597
+ break;
2598
+ }
2599
+ }
2600
+ }
2601
+ hits.push({
2602
+ object: obj.name,
2603
+ id: row.id,
2604
+ title,
2605
+ snippet,
2606
+ record: row
2607
+ });
2608
+ }
2609
+ } catch (e26) {
2610
+ continue;
2611
+ }
2612
+ }
2613
+ return {
2614
+ query: q,
2615
+ hits,
2616
+ totalObjects: objectsScanned,
2617
+ totalHits: hits.length,
2618
+ truncated: hits.length >= overallLimit
2619
+ };
2620
+ }
2621
+ // ==========================================
2622
+ // Metadata Caching
2623
+ // ==========================================
2624
+ async getMetaItemCached(request) {
2625
+ try {
2626
+ const result = await this.getMetaItem({ type: request.type, name: request.name });
2627
+ const item = _optionalChain([result, 'optionalAccess', _109 => _109.item]);
2628
+ if (!item) {
2629
+ throw new Error(`Metadata item ${request.type}/${request.name} not found`);
2630
+ }
2631
+ const content = JSON.stringify(item);
2632
+ const hash = simpleHash(request.locale ? `${request.locale}\0${content}` : content);
2633
+ const etag = { value: hash, weak: false };
2634
+ if (_optionalChain([request, 'access', _110 => _110.cacheRequest, 'optionalAccess', _111 => _111.ifNoneMatch])) {
2635
+ const clientEtag = request.cacheRequest.ifNoneMatch.replace(/^"(.*)"$/, "$1").replace(/^W\/"(.*)"$/, "$1");
2636
+ if (clientEtag === hash) {
2637
+ return {
2638
+ notModified: true,
2639
+ etag
2640
+ };
2641
+ }
2642
+ }
2643
+ return {
2644
+ data: item,
2645
+ etag,
2646
+ lastModified: (/* @__PURE__ */ new Date()).toISOString(),
2647
+ cacheControl: {
2648
+ // Metadata is invalidated by publish, so freshness must be
2649
+ // gated by the ETag validator — not a TTL. `no-cache` lets
2650
+ // clients store the body but forces an `If-None-Match`
2651
+ // revalidation on every use: a cheap 304 when unchanged,
2652
+ // fresh fields the instant a publish bumps the ETag. The old
2653
+ // `max-age=3600` pinned the schema for up to an hour, so the
2654
+ // AI-build "New" form kept rendering pre-publish fields until
2655
+ // the TTL lapsed (no revalidation in between). `private` also
2656
+ // keeps per-tenant metadata out of shared CDN/proxy caches.
2657
+ directives: ["private", "no-cache"]
2658
+ },
2659
+ notModified: false
2660
+ };
2661
+ } catch (error) {
2662
+ throw error;
2663
+ }
2664
+ }
2665
+ // ==========================================
2666
+ // Batch Operations
2667
+ // ==========================================
2668
+ async batchData(request) {
2669
+ const { object, request: batchReq } = request;
2670
+ const { operation, records, options } = batchReq;
2671
+ const results = [];
2672
+ let succeeded = 0;
2673
+ let failed = 0;
2674
+ for (const record of records) {
2675
+ try {
2676
+ switch (operation) {
2677
+ case "create": {
2678
+ const created = await this.engine.insert(object, record.data || record);
2679
+ results.push({ id: created.id, success: true, record: created });
2680
+ succeeded++;
2681
+ break;
2682
+ }
2683
+ case "update": {
2684
+ if (!record.id) throw new Error("Record id is required for update");
2685
+ const updated = await this.engine.update(object, record.data || {}, { where: { id: record.id } });
2686
+ results.push({ id: record.id, success: true, record: updated });
2687
+ succeeded++;
2688
+ break;
2689
+ }
2690
+ case "upsert": {
2691
+ if (record.id) {
2692
+ try {
2693
+ const existing = await this.engine.findOne(object, { where: { id: record.id } });
2694
+ if (existing) {
2695
+ const updated = await this.engine.update(object, record.data || {}, { where: { id: record.id } });
2696
+ results.push({ id: record.id, success: true, record: updated });
2697
+ } else {
2698
+ const created = await this.engine.insert(object, { id: record.id, ...record.data || {} });
2699
+ results.push({ id: created.id, success: true, record: created });
2700
+ }
2701
+ } catch (e27) {
2702
+ const created = await this.engine.insert(object, { id: record.id, ...record.data || {} });
2703
+ results.push({ id: created.id, success: true, record: created });
2704
+ }
2705
+ } else {
2706
+ const created = await this.engine.insert(object, record.data || record);
2707
+ results.push({ id: created.id, success: true, record: created });
2708
+ }
2709
+ succeeded++;
2710
+ break;
2711
+ }
2712
+ case "delete": {
2713
+ if (!record.id) throw new Error("Record id is required for delete");
2714
+ await this.engine.delete(object, { where: { id: record.id } });
2715
+ results.push({ id: record.id, success: true });
2716
+ succeeded++;
2717
+ break;
2718
+ }
2719
+ default:
2720
+ results.push({ id: record.id, success: false, error: `Unknown operation: ${operation}` });
2721
+ failed++;
2722
+ }
2723
+ } catch (err) {
2724
+ results.push({ id: record.id, success: false, error: err.message });
2725
+ failed++;
2726
+ if (_optionalChain([options, 'optionalAccess', _112 => _112.atomic])) {
2727
+ break;
2728
+ }
2729
+ if (!_optionalChain([options, 'optionalAccess', _113 => _113.continueOnError])) {
2730
+ break;
2731
+ }
2732
+ }
2733
+ }
2734
+ return {
2735
+ success: failed === 0,
2736
+ operation,
2737
+ total: records.length,
2738
+ succeeded,
2739
+ failed,
2740
+ results: _optionalChain([options, 'optionalAccess', _114 => _114.returnRecords]) !== false ? results : results.map((r) => ({ id: r.id, success: r.success, error: r.error }))
2741
+ };
2742
+ }
2743
+ async createManyData(request) {
2744
+ const records = await this.engine.insert(request.object, request.records);
2745
+ return {
2746
+ object: request.object,
2747
+ records,
2748
+ count: records.length
2749
+ };
2750
+ }
2751
+ async updateManyData(request) {
2752
+ const { object, records, options } = request;
2753
+ const results = [];
2754
+ let succeeded = 0;
2755
+ let failed = 0;
2756
+ for (const record of records) {
2757
+ try {
2758
+ const updated = await this.engine.update(object, record.data, { where: { id: record.id } });
2759
+ results.push({ id: record.id, success: true, record: updated });
2760
+ succeeded++;
2761
+ } catch (err) {
2762
+ results.push({ id: record.id, success: false, error: err.message });
2763
+ failed++;
2764
+ if (!_optionalChain([options, 'optionalAccess', _115 => _115.continueOnError])) {
2765
+ break;
2766
+ }
2767
+ }
2768
+ }
2769
+ return {
2770
+ success: failed === 0,
2771
+ operation: "update",
2772
+ total: records.length,
2773
+ succeeded,
2774
+ failed,
2775
+ results
2776
+ };
2777
+ }
2778
+ async analyticsQuery(request) {
2779
+ const { query, cube } = request;
2780
+ const object = cube;
2781
+ const groupBy = query.dimensions || [];
2782
+ const aggregations = [];
2783
+ if (query.measures) {
2784
+ for (const measure of query.measures) {
2785
+ if (measure === "count" || measure === "count_all") {
2786
+ aggregations.push({ field: "*", method: "count", alias: "count" });
2787
+ } else if (measure.includes(".")) {
2788
+ const [field, method] = measure.split(".");
2789
+ aggregations.push({ field, method, alias: `${field}_${method}` });
2790
+ } else {
2791
+ aggregations.push({ field: measure, method: "sum", alias: measure });
2792
+ }
2793
+ }
2794
+ }
2795
+ let filter = void 0;
2796
+ if (query.filters && query.filters.length > 0) {
2797
+ const conditions = query.filters.map((f) => {
2798
+ const op = this.mapAnalyticsOperator(f.operator);
2799
+ if (f.values && f.values.length === 1) {
2800
+ return { [f.member]: { [op]: f.values[0] } };
2801
+ } else if (f.values && f.values.length > 1) {
2802
+ return { [f.member]: { $in: f.values } };
2803
+ }
2804
+ return { [f.member]: { [op]: true } };
2805
+ });
2806
+ filter = conditions.length === 1 ? conditions[0] : { $and: conditions };
2807
+ }
2808
+ const rows = await this.engine.aggregate(object, {
2809
+ where: filter,
2810
+ groupBy: groupBy.length > 0 ? groupBy : void 0,
2811
+ aggregations: aggregations.length > 0 ? aggregations.map((a) => ({ function: a.method, field: a.field, alias: a.alias })) : [{ function: "count", alias: "count" }]
2812
+ });
2813
+ const fields = [
2814
+ ...groupBy.map((d) => ({ name: d, type: "string" })),
2815
+ ...aggregations.map((a) => ({ name: a.alias, type: "number" }))
2816
+ ];
2817
+ return {
2818
+ success: true,
2819
+ data: {
2820
+ rows,
2821
+ fields
2822
+ }
2823
+ };
2824
+ }
2825
+ async getAnalyticsMeta(request) {
2826
+ const objects = this.engine.registry.listItems("object");
2827
+ const cubeFilter = _optionalChain([request, 'optionalAccess', _116 => _116.cube]);
2828
+ const cubes = [];
2829
+ for (const obj of objects) {
2830
+ const schema = obj;
2831
+ if (cubeFilter && schema.name !== cubeFilter) continue;
2832
+ const measures = {};
2833
+ const dimensions = {};
2834
+ const fields = schema.fields || {};
2835
+ measures["count"] = {
2836
+ name: "count",
2837
+ label: "Count",
2838
+ type: "count",
2839
+ sql: "*"
2840
+ };
2841
+ for (const [fieldName, fieldDef] of Object.entries(fields)) {
2842
+ const fd = fieldDef;
2843
+ const fieldType = fd.type || "text";
2844
+ if (["number", "currency", "percent"].includes(fieldType)) {
2845
+ measures[`${fieldName}_sum`] = {
2846
+ name: `${fieldName}_sum`,
2847
+ label: `${fd.label || fieldName} (Sum)`,
2848
+ type: "sum",
2849
+ sql: fieldName
2850
+ };
2851
+ measures[`${fieldName}_avg`] = {
2852
+ name: `${fieldName}_avg`,
2853
+ label: `${fd.label || fieldName} (Avg)`,
2854
+ type: "avg",
2855
+ sql: fieldName
2856
+ };
2857
+ dimensions[fieldName] = {
2858
+ name: fieldName,
2859
+ label: fd.label || fieldName,
2860
+ type: "number",
2861
+ sql: fieldName
2862
+ };
2863
+ } else if (["date", "datetime"].includes(fieldType)) {
2864
+ dimensions[fieldName] = {
2865
+ name: fieldName,
2866
+ label: fd.label || fieldName,
2867
+ type: "time",
2868
+ sql: fieldName,
2869
+ granularities: ["day", "week", "month", "quarter", "year"]
2870
+ };
2871
+ } else if (["boolean"].includes(fieldType)) {
2872
+ dimensions[fieldName] = {
2873
+ name: fieldName,
2874
+ label: fd.label || fieldName,
2875
+ type: "boolean",
2876
+ sql: fieldName
2877
+ };
2878
+ } else {
2879
+ dimensions[fieldName] = {
2880
+ name: fieldName,
2881
+ label: fd.label || fieldName,
2882
+ type: "string",
2883
+ sql: fieldName
2884
+ };
2885
+ }
2886
+ }
2887
+ cubes.push({
2888
+ name: schema.name,
2889
+ title: schema.label || schema.name,
2890
+ description: schema.description,
2891
+ sql: schema.name,
2892
+ measures,
2893
+ dimensions,
2894
+ public: true
2895
+ });
2896
+ }
2897
+ return {
2898
+ success: true,
2899
+ data: { cubes }
2900
+ };
2901
+ }
2902
+ mapAnalyticsOperator(op) {
2903
+ const map = {
2904
+ equals: "$eq",
2905
+ notEquals: "$ne",
2906
+ contains: "$contains",
2907
+ notContains: "$notContains",
2908
+ gt: "$gt",
2909
+ gte: "$gte",
2910
+ lt: "$lt",
2911
+ lte: "$lte",
2912
+ set: "$ne",
2913
+ notSet: "$eq"
2914
+ };
2915
+ return map[op] || "$eq";
2916
+ }
2917
+ async triggerAutomation(_request) {
2918
+ throw new Error('triggerAutomation requires plugin-automation service. Install and register a plugin that provides the "automation" service.');
2919
+ }
2920
+ async deleteManyData(request) {
2921
+ return this.engine.delete(request.object, {
2922
+ where: { id: { $in: request.ids } },
2923
+ ...request.options
2924
+ });
2925
+ }
2926
+ static envWritableTypes() {
2927
+ if (this._envWritableTypes !== null) return this._envWritableTypes;
2928
+ const raw = _types.readEnvWithDeprecation.call(void 0, "OS_METADATA_WRITABLE", "OBJECTSTACK_METADATA_WRITABLE") || "";
2929
+ const set = /* @__PURE__ */ new Set();
2930
+ for (const tok of raw.split(",")) {
2931
+ const t = tok.trim();
2932
+ if (!t) continue;
2933
+ const singular = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[t], () => ( t));
2934
+ set.add(singular);
2935
+ const plural = _shared.SINGULAR_TO_PLURAL[singular];
2936
+ if (plural) set.add(plural);
2937
+ }
2938
+ this._envWritableTypes = set;
2939
+ return set;
2940
+ }
2941
+ /** Test hook — clear the memoised env-writable cache. */
2942
+ static resetEnvWritableCache() {
2943
+ this._envWritableTypes = null;
2944
+ }
2945
+ /** Normalize plural→singular before consulting the allow-list. */
2946
+ static isOverlayAllowed(type) {
2947
+ const singular = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[type], () => ( type));
2948
+ if (this.OVERLAY_ALLOWED_TYPES.has(singular) || this.OVERLAY_ALLOWED_TYPES.has(type)) {
2949
+ return true;
2950
+ }
2951
+ const env = this.envWritableTypes();
2952
+ return env.has(singular) || env.has(type);
2953
+ }
2954
+ /** Does this type permit creating brand-new (artifact-free) items? */
2955
+ static isRuntimeCreateAllowed(type) {
2956
+ const singular = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[type], () => ( type));
2957
+ if (this.RUNTIME_CREATE_ALLOWED_TYPES.has(singular) || this.RUNTIME_CREATE_ALLOWED_TYPES.has(type)) {
2958
+ return true;
2959
+ }
2960
+ if (!this.STATIC_REGISTRY_TYPES.has(singular) && !this.STATIC_REGISTRY_TYPES.has(type)) {
2961
+ return true;
2962
+ }
2963
+ return false;
2964
+ }
2965
+ /**
2966
+ * Does an artifact (npm-package-loaded) item exist at `(type, name)`?
2967
+ *
2968
+ * The schema registry's `_packageId` tag is set only when
2969
+ * `registerItem(..., packageId)` is called with a truthy packageId
2970
+ * — and only artifact loaders do that. DB-rehydrated items
2971
+ * (sys_metadata rows registered back into the registry by
2972
+ * `getMetaItems` / `loadMetaFromDb`) call `registerItem` without a
2973
+ * packageId, so they carry no `_packageId` and are correctly
2974
+ * excluded here.
2975
+ *
2976
+ * Used by the two-tier authorization model to distinguish
2977
+ * "overlaying a packaged item" (requires `allowOrgOverride`) from
2978
+ * "authoring a DB-only item" (requires only `allowRuntimeCreate`).
2979
+ */
2980
+ isArtifactBacked(type, name) {
2981
+ return this.lookupArtifactItem(type, name) !== void 0;
2982
+ }
2983
+ // ───────────────────────────────────────────────────────────────────
2984
+ // ADR-0010 — metadata protection (Phase 1: L3 item-level lock)
2985
+ // ───────────────────────────────────────────────────────────────────
2986
+ /**
2987
+ * Look up an item from the artifact registry across both the requested
2988
+ * type and its singular/plural twin. Returns `undefined` when the
2989
+ * registry is unavailable or the item is not artifact-backed.
2990
+ */
2991
+ lookupArtifactItem(type, name, currentPackageId) {
2992
+ const registry = _optionalChain([this, 'access', _117 => _117.engine, 'optionalAccess', _118 => _118.registry]);
2993
+ if (!registry) return void 0;
2994
+ const singular = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[type], () => ( type));
2995
+ if (typeof registry.getArtifactItem === "function") {
2996
+ return _nullishCoalesce(registry.getArtifactItem(singular, name, currentPackageId), () => ( registry.getArtifactItem(type, name, currentPackageId)));
2997
+ }
2998
+ if (typeof registry.getItem !== "function") return void 0;
2999
+ const item = _nullishCoalesce(registry.getItem(singular, name, currentPackageId), () => ( registry.getItem(type, name, currentPackageId)));
3000
+ if (!item || !item._packageId || item._packageId === "sys_metadata") {
3001
+ return void 0;
3002
+ }
3003
+ return item;
3004
+ }
3005
+ /**
3006
+ * True when `packageId` is a **writable base** — a DB-backed package an
3007
+ * org or the AI may author *new* metadata into (ADR-0070 D2). The two
3008
+ * read-only kinds return `false`:
3009
+ *
3010
+ * • **Booted code packages** — they register a manifest into the engine
3011
+ * at startup (`registerApp` → `engine.manifests`); their items are
3012
+ * code-shipped artifacts. Only `allowOrgOverride` overlays are allowed
3013
+ * (ADR-0005), never fresh authored items.
3014
+ * • **Installed / platform packages** — manifest `scope` is `system` or
3015
+ * `cloud` (marketplace / platform-delivered).
3016
+ *
3017
+ * A project-scoped DB package, or a bare ADR-0048 *authoring-workspace* id
3018
+ * with no registered manifest, is writable.
3019
+ *
3020
+ * NOTE: the code-package signal is the engine manifest map ONLY — we
3021
+ * deliberately do NOT fall back to "owns ≥1 registered object" (the old
3022
+ * `isLoadedPackage` heuristic). A writable base accrues registered objects
3023
+ * once its drafts publish, and that must never flip the base to read-only
3024
+ * — that is the exact #2252 read-only-after-publish trap this ADR removes.
3025
+ */
3026
+ isWritablePackage(packageId) {
3027
+ if (!packageId) return false;
3028
+ const engine = this.engine;
3029
+ if (_optionalChain([engine, 'optionalAccess', _119 => _119.manifests, 'optionalAccess', _120 => _120.has, 'optionalCall', _121 => _121(packageId)])) return false;
3030
+ const scope = _optionalChain([engine, 'optionalAccess', _122 => _122.registry, 'optionalAccess', _123 => _123.getPackage, 'optionalCall', _124 => _124(packageId), 'optionalAccess', _125 => _125.manifest, 'optionalAccess', _126 => _126.scope]);
3031
+ if (scope === "system" || scope === "cloud") return false;
3032
+ return true;
3033
+ }
3034
+ /**
3035
+ * Resolve the effective `_lock` for an item by consulting the
3036
+ * artifact registry first, then the persisted overlay row. Artifact
3037
+ * always wins — by design, an overlay cannot loosen a packaged
3038
+ * lock (ADR-0010 §3.3).
3039
+ *
3040
+ * Returns `'none'` when nothing is locked, which is the common
3041
+ * case. Safe to call when `environmentId` is undefined (control-
3042
+ * plane bootstrap) — the lock check is only meaningful in tenant
3043
+ * scope and the caller is expected to also gate on `environmentId`.
3044
+ */
3045
+ async getEffectiveLock(type, name, organizationId) {
3046
+ const artifactItem = this.lookupArtifactItem(type, name);
3047
+ if (artifactItem) {
3048
+ const p = _kernel.extractProtection.call(void 0, artifactItem);
3049
+ if (p.lock !== "none") {
3050
+ return { lock: p.lock, lockReason: p.lockReason, lockSource: "artifact" };
3051
+ }
3052
+ }
3053
+ try {
3054
+ const where = {
3055
+ type,
3056
+ name,
3057
+ state: "active",
3058
+ organization_id: _nullishCoalesce(organizationId, () => ( null))
3059
+ };
3060
+ const row = await this.engine.findOne("sys_metadata", { where });
3061
+ if (row) {
3062
+ const body = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
3063
+ const p = _kernel.extractProtection.call(void 0, body);
3064
+ if (p.lock !== "none") {
3065
+ return { lock: p.lock, lockReason: p.lockReason, lockSource: "overlay" };
3066
+ }
3067
+ }
3068
+ } catch (e28) {
3069
+ }
3070
+ return { lock: "none", lockReason: void 0, lockSource: void 0 };
3071
+ }
3072
+ /**
3073
+ * Best-effort audit-row writer (ADR-0010 §3.6). Failures here are
3074
+ * logged but never block the underlying decision: an environment
3075
+ * without the audit table provisioned (legacy installs before this
3076
+ * ADR landed) still answers normal API calls, just without the
3077
+ * compliance trail. Phase 2 will make the audit table a hard
3078
+ * dependency.
3079
+ */
3080
+ async recordMetadataAudit(entry) {
3081
+ try {
3082
+ await this.engine.insert("sys_metadata_audit", {
3083
+ occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
3084
+ actor: _nullishCoalesce(entry.actor, () => ( "system")),
3085
+ source: _nullishCoalesce(entry.source, () => ( "protocol")),
3086
+ type: _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[entry.type], () => ( entry.type)),
3087
+ name: entry.name,
3088
+ organization_id: _nullishCoalesce(entry.organizationId, () => ( null)),
3089
+ operation: entry.operation,
3090
+ outcome: entry.outcome,
3091
+ code: entry.code,
3092
+ lock_state: _nullishCoalesce(entry.lockState, () => ( "none")),
3093
+ lock_overridden: _nullishCoalesce(entry.lockOverridden, () => ( false)),
3094
+ request_id: _nullishCoalesce(entry.requestId, () => ( null)),
3095
+ note: _nullishCoalesce(entry.note, () => ( null))
3096
+ });
3097
+ } catch (err) {
3098
+ console.warn(
3099
+ `[Protocol] sys_metadata_audit write failed for ${entry.type}/${entry.name}: ${_nullishCoalesce(_optionalChain([err, 'optionalAccess', _127 => _127.message]), () => ( err))}`
3100
+ );
3101
+ }
3102
+ }
3103
+ /**
3104
+ * Phase 1 L3 enforcement for write operations (save / publish /
3105
+ * rollback). Returns null on allow. Returns the structured `Error`
3106
+ * the caller should `throw` on deny — also records the denial in
3107
+ * the audit log so refused attempts are visible in compliance
3108
+ * reports (refused writes never reach sys_metadata_history).
3109
+ */
3110
+ async assertLockAllowsWrite(args) {
3111
+ if (this.environmentId === void 0) return null;
3112
+ const state = await this.getEffectiveLock(args.type, args.name, _nullishCoalesce(args.organizationId, () => ( null)));
3113
+ const refusal = _kernel.evaluateLockForWrite.call(void 0, state.lock);
3114
+ if (!refusal) return null;
3115
+ const reason = _nullishCoalesce(state.lockReason, () => ( refusal.reason));
3116
+ const err = new Error(
3117
+ `[item_locked] ${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ""}). ${reason} \u2014 See ADR-0010 \xA73.3.`
3118
+ );
3119
+ err.code = "item_locked";
3120
+ err.status = 403;
3121
+ err.lock = state.lock;
3122
+ err.lockReason = reason;
3123
+ await this.recordMetadataAudit({
3124
+ type: args.type,
3125
+ name: args.name,
3126
+ organizationId: _nullishCoalesce(args.organizationId, () => ( null)),
3127
+ operation: args.operation,
3128
+ outcome: "denied",
3129
+ code: "item_locked",
3130
+ lockState: state.lock,
3131
+ actor: args.actor,
3132
+ source: _nullishCoalesce(args.source, () => ( `protocol.${args.operation}MetaItem`)),
3133
+ requestId: args.requestId,
3134
+ note: reason
3135
+ });
3136
+ return err;
3137
+ }
3138
+ /** Counterpart of {@link assertLockAllowsWrite} for delete. */
3139
+ async assertLockAllowsDelete(args) {
3140
+ if (this.environmentId === void 0) return null;
3141
+ const state = await this.getEffectiveLock(args.type, args.name, _nullishCoalesce(args.organizationId, () => ( null)));
3142
+ const refusal = _kernel.evaluateLockForDelete.call(void 0, state.lock);
3143
+ if (!refusal) return null;
3144
+ const reason = _nullishCoalesce(state.lockReason, () => ( refusal.reason));
3145
+ const err = new Error(
3146
+ `[item_locked] ${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ""}). ${reason} \u2014 See ADR-0010 \xA73.3.`
3147
+ );
3148
+ err.code = "item_locked";
3149
+ err.status = 403;
3150
+ err.lock = state.lock;
3151
+ err.lockReason = reason;
3152
+ await this.recordMetadataAudit({
3153
+ type: args.type,
3154
+ name: args.name,
3155
+ organizationId: _nullishCoalesce(args.organizationId, () => ( null)),
3156
+ operation: "delete",
3157
+ outcome: "denied",
3158
+ code: "item_locked",
3159
+ lockState: state.lock,
3160
+ actor: args.actor,
3161
+ source: _nullishCoalesce(args.source, () => ( "protocol.deleteMetaItem")),
3162
+ requestId: args.requestId,
3163
+ note: reason
3164
+ });
3165
+ return err;
3166
+ }
3167
+ /**
3168
+ * Mirror an object-type overlay write into the in-memory engine
3169
+ * registry so subsequent CRUD finds the new schema. Idempotent and
3170
+ * safe to call after a successful persistence call. For the legacy
3171
+ * write path this is invoked BEFORE persistence (historical behavior
3172
+ * preserved); for the PR-10d.3 repository path it is invoked only
3173
+ * AFTER `put()` resolves successfully, so a failed write — DB error,
3174
+ * optimistic-lock conflict, validation failure — never leaks a
3175
+ * stale schema into the registry.
3176
+ */
3177
+ applyObjectRegistryMutation(request) {
3178
+ if (request.type !== "object" && request.type !== "objects") return;
3179
+ this.engine.registry.registerItem(request.type, request.item, "name");
3180
+ try {
3181
+ this.engine.registry.registerObject(request.item, "sys_metadata");
3182
+ } catch (err) {
3183
+ console.warn(
3184
+ `[Protocol] registerObject failed for ${request.name}: ${_nullishCoalesce(_optionalChain([err, 'optionalAccess', _128 => _128.message]), () => ( err))}`
3185
+ );
3186
+ }
3187
+ }
3188
+ /**
3189
+ * Heal the in-memory registry after a metadata reset (overlay-row
3190
+ * delete) on control-plane kernels. Two layers:
3191
+ *
3192
+ * 1. Drop the plain-key runtime shadow so the packaged artifact
3193
+ * (registered under `<packageId>:<name>`) becomes the visible
3194
+ * value again. The shadow is written by the overlay-hydration
3195
+ * paths (`getMetaItems` / `loadMetaFromDb`) and — pre-fix —
3196
+ * survived the reset until restart, leaving stale overlay
3197
+ * content (and a stripped `_lock` envelope) in every
3198
+ * registry-direct read (ADR-0010 §3.3).
3199
+ * 2. When no composite-key artifact exists, fall back to the
3200
+ * MetadataService baseline (FilesystemLoader-sourced types) and
3201
+ * re-register it, preserving the historical refresh behaviour
3202
+ * for items the SchemaRegistry never held as artifacts.
3203
+ *
3204
+ * Best-effort: a failure must never block the delete that already
3205
+ * succeeded; the next full reload fixes the registry anyway.
3206
+ */
3207
+ async restoreArtifactRegistryView(type, name) {
3208
+ try {
3209
+ const registry = this.engine.registry;
3210
+ let healed = false;
3211
+ if (typeof registry.removeRuntimeShadow === "function") {
3212
+ const singular = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[type], () => ( type));
3213
+ healed = registry.removeRuntimeShadow(singular, name);
3214
+ if (type !== singular) {
3215
+ healed = registry.removeRuntimeShadow(type, name) || healed;
3216
+ }
3217
+ }
3218
+ if (healed) return;
3219
+ if (this.environmentId !== void 0) return;
3220
+ const services = _optionalChain([this, 'access', _129 => _129.getServicesRegistry, 'optionalCall', _130 => _130()]);
3221
+ const metadataService = _optionalChain([services, 'optionalAccess', _131 => _131.get, 'call', _132 => _132("metadata")]);
3222
+ if (metadataService && typeof metadataService.get === "function") {
3223
+ const artifactItem = await metadataService.get(type, name);
3224
+ if (artifactItem !== void 0) {
3225
+ this.engine.registry.registerItem(type, artifactItem, "name");
3226
+ }
3227
+ }
3228
+ } catch (e29) {
3229
+ }
3230
+ }
3231
+ /**
3232
+ * Ensure a just-PUBLISHED object's physical table exists so it is usable
3233
+ * for data CRUD immediately — without a server restart. Registering the
3234
+ * object (above) only updates the in-memory registry; the table is created
3235
+ * by the driver's schema sync, which otherwise only runs at boot. Without
3236
+ * this, inserting into a freshly-published object fails with "no such
3237
+ * table" (surfaced as `object_not_found`) until the next restart.
3238
+ * Best-effort + non-fatal: drivers without DDL (or read-only datasources)
3239
+ * simply no-op, and a sync failure must not abort the publish.
3240
+ */
3241
+ async ensureObjectStorage(type, name) {
3242
+ if (type !== "object" && type !== "objects") return;
3243
+ try {
3244
+ await this.engine.syncObjectSchema(name);
3245
+ } catch (err) {
3246
+ console.warn(`[Protocol] table sync failed for object '${name}': ${_nullishCoalesce(_optionalChain([err, 'optionalAccess', _133 => _133.message]), () => ( err))}`);
3247
+ }
3248
+ }
3249
+ /**
3250
+ * Inverse of {@link ensureObjectStorage}: drop an object's physical table.
3251
+ * DESTRUCTIVE — deletes the table and all its rows. Only invoked when a
3252
+ * delete explicitly opts into storage teardown (see {@link deleteMetaItem}'s
3253
+ * `dropStorage`), so publishing an object solely to preview it can be undone
3254
+ * without leaving an orphan table. Best-effort: a failure is logged, not
3255
+ * thrown — the metadata delete already succeeded, and a stray table is
3256
+ * reclaimed by the next sync/drop rather than blocking the delete.
3257
+ */
3258
+ async dropObjectStorage(type, name) {
3259
+ if (type !== "object" && type !== "objects") return;
3260
+ try {
3261
+ await this.engine.dropObjectSchema(name);
3262
+ } catch (err) {
3263
+ console.warn(`[Protocol] table drop failed for object '${name}': ${_nullishCoalesce(_optionalChain([err, 'optionalAccess', _134 => _134.message]), () => ( err))}`);
3264
+ }
3265
+ }
3266
+ /**
3267
+ * Guard for storage teardown on delete. Drops a physical table only when
3268
+ * the caller opted in AND it is safe: object types only (others have no
3269
+ * table), active state only (drafts were never materialised), and never a
3270
+ * `sys_`-prefixed platform table.
3271
+ */
3272
+ shouldDropStorage(type, name, dropStorage, state) {
3273
+ if (!dropStorage) return false;
3274
+ const singular = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[type], () => ( type));
3275
+ if (singular !== "object") return false;
3276
+ if (state !== "active") return false;
3277
+ if (name.startsWith("sys_")) return false;
3278
+ return true;
3279
+ }
3280
+ async saveMetaItem(request) {
3281
+ if (!request.item) {
3282
+ throw new Error("Item data is required");
3283
+ }
3284
+ const mode = request.mode === "draft" ? "draft" : "publish";
3285
+ if (this.environmentId !== void 0) {
3286
+ const overlayAllowed = _ObjectStackProtocolImplementation.isOverlayAllowed(request.type);
3287
+ const runtimeCreateAllowed = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(request.type);
3288
+ const artifactBacked = this.isArtifactBacked(request.type, request.name);
3289
+ if (artifactBacked && !overlayAllowed) {
3290
+ const err = new Error(
3291
+ `[not_overridable] Metadata item '${request.type}/${request.name}' is provided by a code package and the type has not opted into per-org overlay writes (allowOrgOverride=false). Edit the source artifact and redeploy, or set OS_METADATA_WRITABLE to grant a runtime escape hatch. See docs/adr/0005-metadata-customization-overlay.md.`
3292
+ );
3293
+ err.code = "not_overridable";
3294
+ err.status = 403;
3295
+ throw err;
3296
+ }
3297
+ if (!artifactBacked && !overlayAllowed && !runtimeCreateAllowed) {
3298
+ const err = new Error(
3299
+ `[not_creatable] Metadata type '${request.type}' does not allow runtime creation (allowRuntimeCreate=false, allowOrgOverride=false). New items of this type must be defined in source code.`
3300
+ );
3301
+ err.code = "not_creatable";
3302
+ err.status = 403;
3303
+ throw err;
3304
+ }
3305
+ const lockErr = await this.assertLockAllowsWrite({
3306
+ type: request.type,
3307
+ name: request.name,
3308
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3309
+ operation: "save",
3310
+ ...request.actor ? { actor: request.actor } : {},
3311
+ source: "protocol.saveMetaItem"
3312
+ });
3313
+ if (lockErr) throw lockErr;
3314
+ }
3315
+ const singularType = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( request.type));
3316
+ if (!request.force && (singularType === "object" || singularType === "field")) {
3317
+ try {
3318
+ const existing = await this.getMetaItem({
3319
+ type: request.type,
3320
+ name: request.name,
3321
+ ...request.organizationId ? { organizationId: request.organizationId } : {}
3322
+ });
3323
+ const prev = _optionalChain([existing, 'optionalAccess', _135 => _135.item]);
3324
+ if (prev) {
3325
+ const issues = detectDestructiveObjectChanges(prev, request.item);
3326
+ if (issues.length > 0) {
3327
+ const summary = issues.slice(0, 3).map((i) => i.message).join("; ");
3328
+ const err = new Error(
3329
+ `[destructive_change] ${request.type}/${request.name} would drop or transform existing data: ${summary}` + (issues.length > 3 ? ` (+${issues.length - 3} more)` : "") + ` \u2014 re-submit with ?force=true to proceed.`
3330
+ );
3331
+ err.code = "destructive_change";
3332
+ err.status = 409;
3333
+ err.issues = issues;
3334
+ throw err;
3335
+ }
3336
+ }
3337
+ } catch (err) {
3338
+ if (_optionalChain([err, 'optionalAccess', _136 => _136.code]) === "destructive_change") throw err;
3339
+ }
3340
+ }
3341
+ {
3342
+ const it = request.item;
3343
+ const looksLikeLayeredEnvelope = it && typeof it === "object" && !Array.isArray(it) && "code" in it && "overlay" in it && "overlayScope" in it && "effective" in it;
3344
+ if (looksLikeLayeredEnvelope) {
3345
+ const err = new Error(
3346
+ `[invalid_metadata] ${request.type}/${request.name}: the request body is a layered read envelope ({ code, overlay, overlayScope, effective }), not a metadata body. Unwrap and send the effective/overlay document instead \u2014 the layered shape is read-only (GET ?layers=true) and must never be persisted.`
3347
+ );
3348
+ err.code = "invalid_metadata";
3349
+ err.status = 422;
3350
+ throw err;
3351
+ }
3352
+ }
3353
+ request.item = normalizeViewMetadata(request.type, request.item, request.name);
3354
+ {
3355
+ const schema = resolveOverlaySchema(request.type, request.item);
3356
+ if (schema) {
3357
+ const parsed = schema.safeParse(request.item);
3358
+ if (!parsed.success) {
3359
+ const issues = parsed.error.issues.map((i) => ({
3360
+ path: i.path.join("."),
3361
+ message: i.message,
3362
+ code: i.code
3363
+ }));
3364
+ const summary = issues.slice(0, 3).map((i) => `${i.path || "<root>"}: ${i.message}`).join("; ");
3365
+ const err = new Error(
3366
+ `[invalid_metadata] ${request.type}/${request.name} failed spec validation: ${summary}` + (issues.length > 3 ? ` (+${issues.length - 3} more)` : "")
3367
+ );
3368
+ err.code = "invalid_metadata";
3369
+ err.status = 422;
3370
+ err.issues = issues;
3371
+ throw err;
3372
+ }
3373
+ }
3374
+ }
3375
+ await this.ensureOverlayIndex();
3376
+ const singularTypeForRepo = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( request.type));
3377
+ const overlayAllowedForRepo = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
3378
+ const runtimeCreateAllowedForRepo = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularTypeForRepo);
3379
+ const useRepoPath = overlayAllowedForRepo || runtimeCreateAllowedForRepo;
3380
+ if (useRepoPath) {
3381
+ const artifactBacked = this.isArtifactBacked(singularTypeForRepo, request.name);
3382
+ const intent = artifactBacked ? "override-artifact" : "runtime-only";
3383
+ if (intent === "runtime-only" && request.packageId != null && !this.isWritablePackage(request.packageId)) {
3384
+ const err = new Error(
3385
+ `[writable_package_required] Cannot author ${singularTypeForRepo}/${request.name} into '${request.packageId}': it is a read-only code/installed package, not a writable base. Create or select a writable base (package) first, then retry. See docs/adr/0070-package-first-authoring.md.`
3386
+ );
3387
+ err.code = "writable_package_required";
3388
+ err.status = 422;
3389
+ err.packageId = request.packageId;
3390
+ throw err;
3391
+ }
3392
+ const orgId = _nullishCoalesce(request.organizationId, () => ( null));
3393
+ const repo = this.getOverlayRepo(orgId);
3394
+ const ref = {
3395
+ type: singularTypeForRepo,
3396
+ name: request.name,
3397
+ org: _nullishCoalesce(orgId, () => ( "env"))
3398
+ };
3399
+ let parentVersion;
3400
+ if (request.parentVersion !== void 0) {
3401
+ parentVersion = request.parentVersion;
3402
+ } else {
3403
+ const current = await repo.get(ref, {
3404
+ state: mode === "draft" ? "draft" : "active",
3405
+ packageId: _nullishCoalesce(request.packageId, () => ( null))
3406
+ });
3407
+ parentVersion = _nullishCoalesce(_optionalChain([current, 'optionalAccess', _137 => _137.hash]), () => ( null));
3408
+ }
3409
+ try {
3410
+ const result = await repo.put(ref, request.item, {
3411
+ parentVersion,
3412
+ actor: _nullishCoalesce(request.actor, () => ( "system")),
3413
+ source: "protocol.saveMetaItem",
3414
+ intent,
3415
+ state: mode === "draft" ? "draft" : "active",
3416
+ ...request.packageId !== void 0 ? { packageId: request.packageId } : {}
3417
+ });
3418
+ if (mode === "publish") {
3419
+ this.applyObjectRegistryMutation(request);
3420
+ await this.ensureObjectStorage(request.type, request.name);
3421
+ }
3422
+ await this.recordMetadataAudit({
3423
+ type: request.type,
3424
+ name: request.name,
3425
+ organizationId: orgId,
3426
+ operation: "save",
3427
+ outcome: "allowed",
3428
+ code: "ok",
3429
+ ...request.actor ? { actor: request.actor } : {},
3430
+ source: "protocol.saveMetaItem",
3431
+ note: mode === "draft" ? "draft" : "active"
3432
+ });
3433
+ return {
3434
+ success: true,
3435
+ version: result.version,
3436
+ seq: result.seq,
3437
+ state: mode === "draft" ? "draft" : "active",
3438
+ message: orgId ? `Saved customization overlay (org=${orgId}, state=${mode === "draft" ? "draft" : "active"}) \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]` : `Saved customization overlay (env-wide, state=${mode === "draft" ? "draft" : "active"}) \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
3439
+ };
3440
+ } catch (err) {
3441
+ if (err instanceof _metadatacore.ConflictError) {
3442
+ const conflict = new Error(
3443
+ `[metadata_conflict] ${request.type}/${request.name} has been modified since you loaded it. Expected parent ${_nullishCoalesce(err.expectedParent, () => ( "null"))} but current is ${_nullishCoalesce(err.actualHead, () => ( "null"))}.`
3444
+ );
3445
+ conflict.code = "metadata_conflict";
3446
+ conflict.status = 409;
3447
+ conflict.expectedParent = err.expectedParent;
3448
+ conflict.actualHead = err.actualHead;
3449
+ throw conflict;
3450
+ }
3451
+ throw err;
3452
+ }
3453
+ }
3454
+ this.applyObjectRegistryMutation(request);
3455
+ try {
3456
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3457
+ const orgId = _nullishCoalesce(request.organizationId, () => ( null));
3458
+ const scopedWhere = {
3459
+ type: request.type,
3460
+ name: request.name,
3461
+ organization_id: orgId,
3462
+ state: "active"
3463
+ };
3464
+ const existing = await this.engine.findOne("sys_metadata", {
3465
+ where: scopedWhere
3466
+ });
3467
+ if (existing) {
3468
+ const updateRow = {
3469
+ metadata: JSON.stringify(request.item),
3470
+ updated_at: now,
3471
+ version: (existing.version || 0) + 1,
3472
+ state: "active"
3473
+ };
3474
+ const existingPkg = _nullishCoalesce(existing.package_id, () => ( null));
3475
+ const nextPkg = _nullishCoalesce(_nullishCoalesce(existingPkg, () => ( request.packageId)), () => ( null));
3476
+ if (nextPkg !== null) updateRow.package_id = nextPkg;
3477
+ await this.engine.update("sys_metadata", updateRow, {
3478
+ where: { id: existing.id }
3479
+ });
3480
+ } else {
3481
+ const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `meta_${Date.now()}_${Math.random().toString(36).slice(2)}`;
3482
+ const row = {
3483
+ id,
3484
+ name: request.name,
3485
+ type: request.type,
3486
+ // `scope` enum is ['system','platform','user']; per-org
3487
+ // overlays use 'platform' as the informational tag. The
3488
+ // authoritative isolation key is `organization_id`.
3489
+ scope: "platform",
3490
+ metadata: JSON.stringify(request.item),
3491
+ state: "active",
3492
+ version: 1,
3493
+ created_at: now,
3494
+ updated_at: now,
3495
+ organization_id: orgId
3496
+ };
3497
+ if (request.packageId) row.package_id = request.packageId;
3498
+ await this.engine.insert("sys_metadata", row);
3499
+ }
3500
+ return {
3501
+ success: true,
3502
+ message: orgId ? `Saved customization overlay (org=${orgId}) \u2014 type=${request.type}, name=${request.name}` : `Saved customization overlay (env-wide) \u2014 type=${request.type}, name=${request.name}`
3503
+ };
3504
+ } catch (dbError) {
3505
+ console.error(
3506
+ `[Protocol] sys_metadata persistence failed for ${request.type}/${request.name}: ${dbError.message}`
3507
+ );
3508
+ const err = new Error(
3509
+ `Failed to persist customization overlay to sys_metadata: ${dbError.message}. In-memory registry was updated but will be lost on restart.`
3510
+ );
3511
+ err.code = "overlay_persistence_failed";
3512
+ err.status = 500;
3513
+ throw err;
3514
+ }
3515
+ }
3516
+ /**
3517
+ * Yield the durable change-log for a single metadata item — every
3518
+ * put/delete recorded in `sys_metadata_history` for `(org, type, name)`,
3519
+ * in event_seq order. Powers the Studio "History" tab and any
3520
+ * client-side audit timeline.
3521
+ *
3522
+ * Returns `[]` for non-overlay-allowed types (the legacy raw-engine
3523
+ * path doesn't record history) instead of throwing — callers can treat
3524
+ * "no history" uniformly.
3525
+ */
3526
+ async historyMetaItem(request) {
3527
+ const singularType = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( request.type));
3528
+ if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
3529
+ return { events: [] };
3530
+ }
3531
+ const orgId = _nullishCoalesce(request.organizationId, () => ( null));
3532
+ const repo = this.getOverlayRepo(orgId);
3533
+ const ref = {
3534
+ type: singularType,
3535
+ name: request.name,
3536
+ org: _nullishCoalesce(orgId, () => ( "env"))
3537
+ };
3538
+ const events = [];
3539
+ const opts = {};
3540
+ if (request.sinceSeq !== void 0) opts.sinceSeq = request.sinceSeq;
3541
+ if (request.limit !== void 0) opts.limit = request.limit;
3542
+ for await (const ev of repo.history(ref, opts)) events.push(ev);
3543
+ return { events };
3544
+ }
3545
+ /**
3546
+ * Promote the pending draft overlay to the live (`active`) row.
3547
+ * Records a history event with `op='publish'`. 404 (`[no_draft]`)
3548
+ * when there is nothing to publish.
3549
+ */
3550
+ async publishMetaItem(request) {
3551
+ const singularType = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( request.type));
3552
+ if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
3553
+ const err = new Error(
3554
+ `[not_overridable] Metadata type '${request.type}' is not draftable \u2014 no overlay/runtime-create permission.`
3555
+ );
3556
+ err.code = "not_overridable";
3557
+ err.status = 403;
3558
+ throw err;
3559
+ }
3560
+ const _publishLockErr = await this.assertLockAllowsWrite({
3561
+ type: request.type,
3562
+ name: request.name,
3563
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3564
+ operation: "publish",
3565
+ ...request.actor ? { actor: request.actor } : {},
3566
+ source: "protocol.publishMetaItem"
3567
+ });
3568
+ if (_publishLockErr) throw _publishLockErr;
3569
+ await this.ensureOverlayIndex();
3570
+ const orgId = _nullishCoalesce(request.organizationId, () => ( null));
3571
+ const repo = this.getOverlayRepo(orgId);
3572
+ const artifactBacked = this.isArtifactBacked(singularType, request.name);
3573
+ const intent = artifactBacked ? "override-artifact" : "runtime-only";
3574
+ const ref = {
3575
+ type: singularType,
3576
+ name: request.name,
3577
+ org: _nullishCoalesce(orgId, () => ( "env"))
3578
+ };
3579
+ try {
3580
+ const result = await repo.promoteDraft(ref, {
3581
+ actor: _nullishCoalesce(request.actor, () => ( "system")),
3582
+ source: "protocol.publishMetaItem",
3583
+ ...request.message ? { message: request.message } : {},
3584
+ intent
3585
+ });
3586
+ this.applyObjectRegistryMutation({
3587
+ type: request.type,
3588
+ name: request.name,
3589
+ item: result.item.body
3590
+ });
3591
+ await this.ensureObjectStorage(request.type, request.name);
3592
+ const response = {
3593
+ success: true,
3594
+ version: result.version,
3595
+ seq: result.seq,
3596
+ message: `Published draft \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
3597
+ };
3598
+ if (singularType === "seed" && !request._skipSeedApply) {
3599
+ response.seedApplied = await this.applySeedBodies([result.item.body], orgId);
3600
+ }
3601
+ return response;
3602
+ } catch (err) {
3603
+ if (err instanceof _metadatacore.ConflictError) {
3604
+ const conflict = new Error(
3605
+ `[metadata_conflict] ${request.type}/${request.name} published row advanced while you held the draft. Expected parent ${_nullishCoalesce(err.expectedParent, () => ( "null"))} but current is ${_nullishCoalesce(err.actualHead, () => ( "null"))}.`
3606
+ );
3607
+ conflict.code = "metadata_conflict";
3608
+ conflict.status = 409;
3609
+ conflict.expectedParent = err.expectedParent;
3610
+ conflict.actualHead = err.actualHead;
3611
+ throw conflict;
3612
+ }
3613
+ throw err;
3614
+ }
3615
+ }
3616
+ /**
3617
+ * Materialize published `seed` bodies into data rows via the SeedLoaderService
3618
+ * (externalId-keyed upsert, multi-pass for cross-seed references). Passing ALL
3619
+ * of a publish's seed bodies in ONE call lets a child seed reference a parent
3620
+ * seed's rows regardless of publish order. Best-effort: any failure is
3621
+ * returned, never thrown — publishing metadata must not be blocked by a data
3622
+ * problem, but the caller surfaces `seedApplied` so the failure is LOUD.
3623
+ */
3624
+ async applySeedBodies(bodies, organizationId) {
3625
+ try {
3626
+ const seeds = bodies.filter(
3627
+ (b) => b && typeof b.object === "string" && Array.isArray(b.records)
3628
+ );
3629
+ if (seeds.length === 0) {
3630
+ return { success: false, inserted: 0, updated: 0, error: "seed apply: no readable seed bodies" };
3631
+ }
3632
+ const { SeedLoaderService: SeedLoaderService2 } = await Promise.resolve().then(() => _interopRequireWildcard(require("./seed-loader-IFRY33L4.cjs")));
3633
+ const { SeedLoaderRequestSchema } = await Promise.resolve().then(() => _interopRequireWildcard(require("@objectstack/spec/data")));
3634
+ const metadataAdapter = {
3635
+ getObject: async (name) => {
3636
+ const wrapper = await this.getMetaItem({
3637
+ type: "object",
3638
+ name,
3639
+ ...organizationId ? { organizationId } : {}
3640
+ });
3641
+ return _nullishCoalesce(_nullishCoalesce(_optionalChain([wrapper, 'optionalAccess', _138 => _138.item]), () => ( wrapper)), () => ( null));
3642
+ }
3643
+ };
3644
+ const loader = new SeedLoaderService2(
3645
+ this.engine,
3646
+ metadataAdapter,
3647
+ console
3648
+ );
3649
+ const request = SeedLoaderRequestSchema.parse({
3650
+ seeds,
3651
+ config: {
3652
+ defaultMode: "upsert",
3653
+ multiPass: true,
3654
+ ...organizationId ? { organizationId } : {}
3655
+ }
3656
+ });
3657
+ const r = await loader.load(request);
3658
+ return {
3659
+ success: r.success,
3660
+ inserted: r.summary.totalInserted,
3661
+ updated: r.summary.totalUpdated,
3662
+ ..._optionalChain([r, 'access', _139 => _139.errors, 'optionalAccess', _140 => _140.length]) ? { errors: r.errors } : {}
3663
+ };
3664
+ } catch (e) {
3665
+ return { success: false, inserted: 0, updated: 0, error: _nullishCoalesce(_optionalChain([e, 'optionalAccess', _141 => _141.message]), () => ( "seed apply failed")) };
3666
+ }
3667
+ }
3668
+ /**
3669
+ * List pending DRAFT metadata (ADR-0033) for the org, optionally narrowed
3670
+ * by `packageId` and/or `type`. The list reads of `getMetaItems` only see
3671
+ * the ACTIVE registry; this exposes what an AI authored but a human hasn't
3672
+ * published yet, so the console can show a "pending changes" surface and a
3673
+ * just-built app package isn't displayed as empty. No body is returned.
3674
+ */
3675
+ async listDrafts(request) {
3676
+ await this.ensureOverlayIndex();
3677
+ const orgId = _nullishCoalesce(_optionalChain([request, 'optionalAccess', _142 => _142.organizationId]), () => ( null));
3678
+ const repo = this.getOverlayRepo(orgId);
3679
+ const drafts = await repo.listDrafts({
3680
+ ..._optionalChain([request, 'optionalAccess', _143 => _143.type]) ? { type: _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( request.type)) } : {},
3681
+ ..._optionalChain([request, 'optionalAccess', _144 => _144.packageId]) ? { packageId: request.packageId } : {}
3682
+ });
3683
+ return { drafts };
3684
+ }
3685
+ /**
3686
+ * Publish every pending DRAFT bound to a package in one shot (ADR-0033) —
3687
+ * the "publish whole app" action. Promotes each draft→active by reusing the
3688
+ * per-item {@link publishMetaItem} primitive (which runs the overridable /
3689
+ * lock guards and refreshes the runtime registry), so this needs NO
3690
+ * `metadata` service (unlike `MetadataService.publishPackage`, which reads
3691
+ * the in-memory registry and 503s when that service is absent). Per-item
3692
+ * failures are collected and do NOT abort the rest.
3693
+ */
3694
+ async publishPackageDrafts(request) {
3695
+ await this.ensureOverlayIndex();
3696
+ const orgId = _nullishCoalesce(request.organizationId, () => ( null));
3697
+ const repo = this.getOverlayRepo(orgId);
3698
+ const drafts = await repo.listDrafts({ packageId: request.packageId });
3699
+ const published = [];
3700
+ const failed = [];
3701
+ const ordered = [
3702
+ ...drafts.filter((d) => d.type !== "seed"),
3703
+ ...drafts.filter((d) => d.type === "seed")
3704
+ ];
3705
+ const seedBodies = [];
3706
+ const commitItems = [];
3707
+ for (const d of ordered) {
3708
+ try {
3709
+ const activeRow = await this.engine.findOne("sys_metadata", {
3710
+ where: { organization_id: orgId, type: d.type, name: d.name, state: "active" }
3711
+ });
3712
+ commitItems.push({
3713
+ type: d.type,
3714
+ name: d.name,
3715
+ existedBefore: !!activeRow,
3716
+ prevVersion: activeRow && typeof activeRow.version === "number" ? activeRow.version : null
3717
+ });
3718
+ } catch (e30) {
3719
+ commitItems.push({ type: d.type, name: d.name, existedBefore: false, prevVersion: null });
3720
+ }
3721
+ }
3722
+ const publishedSeqs = [];
3723
+ for (const d of ordered) {
3724
+ try {
3725
+ if (d.type === "seed") {
3726
+ const ref = { type: d.type, name: d.name, org: _nullishCoalesce(orgId, () => ( "env")) };
3727
+ const draft = await repo.get(ref, { state: "draft" });
3728
+ if (_optionalChain([draft, 'optionalAccess', _145 => _145.body])) seedBodies.push(draft.body);
3729
+ }
3730
+ const r = await this.publishMetaItem({
3731
+ type: d.type,
3732
+ name: d.name,
3733
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3734
+ ...request.actor ? { actor: request.actor } : {},
3735
+ message: `publish app package '${request.packageId}'`,
3736
+ _skipSeedApply: true
3737
+ });
3738
+ published.push({ type: d.type, name: d.name, version: r.version });
3739
+ if (typeof r.seq === "number") publishedSeqs.push(r.seq);
3740
+ } catch (e) {
3741
+ failed.push({
3742
+ type: d.type,
3743
+ name: d.name,
3744
+ error: _nullishCoalesce(_optionalChain([e, 'optionalAccess', _146 => _146.message]), () => ( "publish failed")),
3745
+ ..._optionalChain([e, 'optionalAccess', _147 => _147.code]) ? { code: e.code } : {}
3746
+ });
3747
+ }
3748
+ }
3749
+ const seedApplied = seedBodies.length > 0 ? await this.applySeedBodies(seedBodies, orgId) : void 0;
3750
+ let probes;
3751
+ if (published.length > 0) {
3752
+ try {
3753
+ const { runBuildProbes: runBuildProbes2 } = await Promise.resolve().then(() => _interopRequireWildcard(require("./build-probes-I3227FYL.cjs")));
3754
+ const analytics = _optionalChain([this, 'access', _148 => _148.getServicesRegistry, 'optionalCall', _149 => _149(), 'access', _150 => _150.get, 'call', _151 => _151("analytics")]);
3755
+ probes = await runBuildProbes2({
3756
+ engine: this.engine,
3757
+ getItem: async (type, name) => {
3758
+ const wrapper = await this.getMetaItem({
3759
+ type,
3760
+ name,
3761
+ ...orgId ? { organizationId: orgId } : {}
3762
+ });
3763
+ return _nullishCoalesce(_nullishCoalesce(_optionalChain([wrapper, 'optionalAccess', _152 => _152.item]), () => ( wrapper)), () => ( void 0));
3764
+ },
3765
+ published,
3766
+ ...analytics && typeof analytics.queryDataset === "function" ? { analytics } : {},
3767
+ organizationId: orgId
3768
+ });
3769
+ } catch (e31) {
3770
+ probes = void 0;
3771
+ }
3772
+ }
3773
+ let commit = null;
3774
+ if (published.length > 0) {
3775
+ const publishedKeys = new Set(published.map((p) => `${p.type}/${p.name}`));
3776
+ commit = await this.recordPackageCommit({
3777
+ orgId,
3778
+ packageId: request.packageId,
3779
+ operation: "apply",
3780
+ ...request.message ? { message: request.message } : {},
3781
+ ...request.actor ? { actor: request.actor } : {},
3782
+ ...request.aiModel ? { aiModel: request.aiModel } : {},
3783
+ items: commitItems.filter((it) => publishedKeys.has(`${it.type}/${it.name}`)),
3784
+ ...publishedSeqs.length ? { eventSeqStart: Math.min(...publishedSeqs), eventSeqEnd: Math.max(...publishedSeqs) } : {}
3785
+ });
3786
+ }
3787
+ return {
3788
+ success: failed.length === 0 && published.length > 0,
3789
+ publishedCount: published.length,
3790
+ failedCount: failed.length,
3791
+ published,
3792
+ failed,
3793
+ ...seedApplied ? { seedApplied } : {},
3794
+ ...probes ? { probes } : {},
3795
+ ...commit ? { commitId: commit.commitId } : {}
3796
+ };
3797
+ }
3798
+ /**
3799
+ * Discard every pending DRAFT bound to a package — the NON-destructive
3800
+ * inverse of {@link publishPackageDrafts}. Drops only `state='draft'` rows
3801
+ * (via the per-item delete primitive), reverting the package to its last
3802
+ * published baseline; active/published metadata and physical tables are
3803
+ * left untouched.
3804
+ *
3805
+ * Use case: "I edited this app for a while and it turned out worse than
3806
+ * before — abandon all my changes." Routes through the sys_metadata path
3807
+ * (no metadata-service dependency, unlike `POST /packages/:id/revert`).
3808
+ */
3809
+ async discardPackageDrafts(request) {
3810
+ await this.ensureOverlayIndex();
3811
+ const orgId = _nullishCoalesce(request.organizationId, () => ( null));
3812
+ const repo = this.getOverlayRepo(orgId);
3813
+ const drafts = await repo.listDrafts({ packageId: request.packageId });
3814
+ const discarded = [];
3815
+ const failed = [];
3816
+ for (const d of drafts) {
3817
+ try {
3818
+ await this.deleteMetaItem({
3819
+ type: d.type,
3820
+ name: d.name,
3821
+ state: "draft",
3822
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3823
+ ...request.actor ? { actor: request.actor } : {}
3824
+ });
3825
+ discarded.push({ type: d.type, name: d.name });
3826
+ } catch (e) {
3827
+ failed.push({
3828
+ type: d.type,
3829
+ name: d.name,
3830
+ error: _nullishCoalesce(_optionalChain([e, 'optionalAccess', _153 => _153.message]), () => ( "discard failed")),
3831
+ ..._optionalChain([e, 'optionalAccess', _154 => _154.code]) ? { code: e.code } : {}
3832
+ });
3833
+ }
3834
+ }
3835
+ return {
3836
+ success: failed.length === 0 && discarded.length > 0,
3837
+ discardedCount: discarded.length,
3838
+ failedCount: failed.length,
3839
+ discarded,
3840
+ failed
3841
+ };
3842
+ }
3843
+ /**
3844
+ * Delete an ENTIRE package: every `sys_metadata` row bound to it (active
3845
+ * AND draft) and — by default — the physical table of each object it
3846
+ * defined. DESTRUCTIVE: removes the app and its data. Use case: "I don't
3847
+ * want this package anymore."
3848
+ *
3849
+ * Set `keepData: true` to remove the metadata but preserve object tables.
3850
+ * The `sys_`-table guard in {@link deleteMetaItem} still applies, so
3851
+ * platform storage is never dropped. Drafts are removed before active rows
3852
+ * so each object's table is torn down once. Per-item failures are collected
3853
+ * without aborting the rest.
3854
+ */
3855
+ async deletePackage(request) {
3856
+ const where = { package_id: request.packageId };
3857
+ if (request.organizationId) where.organization_id = request.organizationId;
3858
+ const rows = await this.engine.find("sys_metadata", { where });
3859
+ const dropStorage = request.keepData !== true;
3860
+ const ordered = [...rows].sort((a, b) => (a.state === "draft" ? 0 : 1) - (b.state === "draft" ? 0 : 1));
3861
+ const deleted = [];
3862
+ const failed = [];
3863
+ for (const row of ordered) {
3864
+ const state = row.state === "draft" ? "draft" : "active";
3865
+ try {
3866
+ await this.deleteMetaItem({
3867
+ type: row.type,
3868
+ name: row.name,
3869
+ state,
3870
+ ...row.organization_id ? { organizationId: row.organization_id } : {},
3871
+ ...request.actor ? { actor: request.actor } : {},
3872
+ ...dropStorage ? { dropStorage: true } : {}
3873
+ });
3874
+ deleted.push({ type: row.type, name: row.name, state });
3875
+ } catch (e) {
3876
+ failed.push({
3877
+ type: row.type,
3878
+ name: row.name,
3879
+ error: _nullishCoalesce(_optionalChain([e, 'optionalAccess', _155 => _155.message]), () => ( "delete failed")),
3880
+ ..._optionalChain([e, 'optionalAccess', _156 => _156.code]) ? { code: e.code } : {}
3881
+ });
3882
+ }
3883
+ }
3884
+ return {
3885
+ success: failed.length === 0 && deleted.length > 0,
3886
+ deletedCount: deleted.length,
3887
+ failedCount: failed.length,
3888
+ deleted,
3889
+ failed
3890
+ };
3891
+ }
3892
+ /**
3893
+ * ADR-0070 D4 — duplicate a writable base into a NEW package (the Airtable
3894
+ * "duplicate base" gesture). Clones every ACTIVE item the source owns into
3895
+ * `targetPackageId`, RE-NAMESPACING object names — the blueprint prefixes a
3896
+ * base's object names with its namespace (e.g. `iojn_repair_ticket`), and
3897
+ * `sys_metadata` keys on (type,name,org), so a same-name copy would collide
3898
+ * with the source — and rewriting every intra-package reference (lookup
3899
+ * `reference`, view `object`, expressions, etc.) to the new names. Per-item
3900
+ * best-effort; one failure never aborts the whole clone.
3901
+ */
3902
+ async duplicatePackage(request) {
3903
+ const registry = this.engine.registry;
3904
+ const srcPkg = _optionalChain([registry, 'optionalAccess', _157 => _157.getPackage, 'optionalCall', _158 => _158(request.sourcePackageId)]);
3905
+ const sourceNs = _nullishCoalesce(_optionalChain([srcPkg, 'optionalAccess', _159 => _159.manifest, 'optionalAccess', _160 => _160.namespace]), () => ( (_nullishCoalesce(request.sourcePackageId.split(".").pop(), () => ( "")))));
3906
+ const targetNs = _nullishCoalesce(request.targetNamespace, () => ( (_nullishCoalesce(request.targetPackageId.split(".").pop(), () => ( request.targetPackageId)))));
3907
+ const where = { package_id: request.sourcePackageId, state: "active" };
3908
+ if (request.organizationId) where.organization_id = request.organizationId;
3909
+ const rows = await this.engine.find("sys_metadata", { where });
3910
+ const renameName = (name) => sourceNs && typeof name === "string" && name.startsWith(`${sourceNs}_`) ? `${targetNs}_${name.slice(sourceNs.length + 1)}` : name;
3911
+ const renameMap = /* @__PURE__ */ new Map();
3912
+ for (const row of rows) {
3913
+ if (_optionalChain([row, 'optionalAccess', _161 => _161.type]) === "object") {
3914
+ const nn = renameName(row.name);
3915
+ if (nn !== row.name) renameMap.set(row.name, nn);
3916
+ }
3917
+ }
3918
+ const olds = [...renameMap.keys()].sort((a, b) => b.length - a.length);
3919
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3920
+ const re = olds.length ? new RegExp(`(${olds.map(esc).join("|")})(?![A-Za-z0-9_])`, "g") : null;
3921
+ const deepRewrite = (v) => {
3922
+ if (typeof v === "string") return re ? v.replace(re, (m) => _nullishCoalesce(renameMap.get(m), () => ( m))) : v;
3923
+ if (Array.isArray(v)) return v.map(deepRewrite);
3924
+ if (v && typeof v === "object") {
3925
+ const o = {};
3926
+ for (const [k, val] of Object.entries(v)) o[k] = deepRewrite(val);
3927
+ return o;
3928
+ }
3929
+ return v;
3930
+ };
3931
+ if (_optionalChain([srcPkg, 'optionalAccess', _162 => _162.manifest]) && typeof _optionalChain([registry, 'optionalAccess', _163 => _163.installPackage]) === "function") {
3932
+ try {
3933
+ registry.installPackage({
3934
+ ...srcPkg.manifest,
3935
+ id: request.targetPackageId,
3936
+ name: _nullishCoalesce(request.targetName, () => ( `${_nullishCoalesce(srcPkg.manifest.name, () => ( request.sourcePackageId))} (copy)`)),
3937
+ namespace: targetNs
3938
+ });
3939
+ } catch (e32) {
3940
+ }
3941
+ }
3942
+ const copied = [];
3943
+ const failed = [];
3944
+ for (const row of rows) {
3945
+ const newName = renameName(row.name);
3946
+ let item;
3947
+ try {
3948
+ item = typeof row.metadata === "string" ? JSON.parse(row.metadata) : _nullishCoalesce(row.metadata, () => ( {}));
3949
+ } catch (e33) {
3950
+ failed.push({ type: row.type, name: row.name, error: "unparseable metadata" });
3951
+ continue;
3952
+ }
3953
+ const rewritten = deepRewrite(item);
3954
+ if (rewritten && typeof rewritten === "object" && !Array.isArray(rewritten)) rewritten.name = newName;
3955
+ try {
3956
+ await this.saveMetaItem({
3957
+ type: row.type,
3958
+ name: newName,
3959
+ item: rewritten,
3960
+ mode: "publish",
3961
+ packageId: request.targetPackageId,
3962
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3963
+ ...request.actor ? { actor: request.actor } : {}
3964
+ });
3965
+ copied.push({ type: row.type, name: newName });
3966
+ } catch (e) {
3967
+ failed.push({ type: row.type, name: row.name, error: _nullishCoalesce(_optionalChain([e, 'optionalAccess', _164 => _164.message]), () => ( "copy failed")) });
3968
+ }
3969
+ }
3970
+ return {
3971
+ success: failed.length === 0 && copied.length > 0,
3972
+ copiedCount: copied.length,
3973
+ failedCount: failed.length,
3974
+ targetPackageId: request.targetPackageId,
3975
+ copied,
3976
+ failed
3977
+ };
3978
+ }
3979
+ /**
3980
+ * ADR-0070 D5 — adopt orphaned (package-less) metadata into a base. The
3981
+ * pre-package-first stopgaps left runtime-authored items with
3982
+ * `package_id = null` (or the `sys_metadata` sentinel). This bulk-rebinds
3983
+ * every such orphan to `targetPackageId` so the env converges on the
3984
+ * package-first model and the "Local / Custom" migration scope can be
3985
+ * retired. Owned rows (already bound to a real package) are left untouched.
3986
+ * Updates the durable column; the in-memory registry picks the new binding
3987
+ * up on the next metadata reload.
3988
+ */
3989
+ async reassignOrphanedMetadata(request) {
3990
+ const where = {};
3991
+ if (request.organizationId) where.organization_id = request.organizationId;
3992
+ const rows = await this.engine.find("sys_metadata", { where });
3993
+ const orphans = rows.filter(
3994
+ (r) => _optionalChain([r, 'optionalAccess', _165 => _165.package_id]) == null || r.package_id === "" || r.package_id === "sys_metadata"
3995
+ );
3996
+ const reassigned = [];
3997
+ for (const row of orphans) {
3998
+ try {
3999
+ await this.engine.update(
4000
+ "sys_metadata",
4001
+ { package_id: request.targetPackageId },
4002
+ { where: { id: row.id } }
4003
+ );
4004
+ reassigned.push({ type: row.type, name: row.name });
4005
+ } catch (e34) {
4006
+ }
4007
+ }
4008
+ return {
4009
+ success: reassigned.length > 0,
4010
+ reassignedCount: reassigned.length,
4011
+ reassigned,
4012
+ targetPackageId: request.targetPackageId
4013
+ };
4014
+ }
4015
+ // ─────────────────────────────────────────────────────────────────────
4016
+ // ADR-0067 — package-scoped commit history & rollback
4017
+ // ─────────────────────────────────────────────────────────────────────
4018
+ /**
4019
+ * Record one commit row (best-effort) grouping a turn's published
4020
+ * artifacts. Returns the commit id, or null if the commit store is
4021
+ * unavailable (e.g. unit-test stubs) — recording never blocks a publish.
4022
+ */
4023
+ async recordPackageCommit(args) {
4024
+ try {
4025
+ const commitId = "cmt_" + (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${_nullishCoalesce(args.eventSeqEnd, () => ( 0))}-${args.items.length}-${args.packageId}`);
4026
+ await this.engine.insert("sys_metadata_commit", {
4027
+ id: commitId,
4028
+ package_id: args.packageId,
4029
+ operation: args.operation,
4030
+ ...args.message ? { message: args.message } : {},
4031
+ ...args.actor ? { actor: args.actor } : {},
4032
+ ...args.aiModel ? { ai_model: args.aiModel } : {},
4033
+ ...args.parentCommitId ? { parent_commit_id: args.parentCommitId } : {},
4034
+ ...args.eventSeqStart !== void 0 ? { event_seq_start: args.eventSeqStart } : {},
4035
+ ...args.eventSeqEnd !== void 0 ? { event_seq_end: args.eventSeqEnd } : {},
4036
+ items: JSON.stringify(args.items),
4037
+ item_count: args.items.length,
4038
+ organization_id: args.orgId,
4039
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
4040
+ });
4041
+ return { commitId };
4042
+ } catch (e35) {
4043
+ return null;
4044
+ }
4045
+ }
4046
+ parseCommitItems(raw) {
4047
+ if (Array.isArray(raw)) return raw;
4048
+ if (typeof raw === "string") {
4049
+ try {
4050
+ const p = JSON.parse(raw);
4051
+ return Array.isArray(p) ? p : [];
4052
+ } catch (e36) {
4053
+ return [];
4054
+ }
4055
+ }
4056
+ return [];
4057
+ }
4058
+ /**
4059
+ * List the commit timeline for a package, newest-first (ADR-0067). Returns
4060
+ * [] if the commit store is unavailable.
4061
+ */
4062
+ async listCommits(request) {
4063
+ try {
4064
+ const where = { package_id: request.packageId };
4065
+ if (request.organizationId) where.organization_id = request.organizationId;
4066
+ const rows = await this.engine.find("sys_metadata_commit", {
4067
+ where,
4068
+ ...request.limit ? { limit: request.limit } : {}
4069
+ });
4070
+ const mapped = rows.map((r) => ({
4071
+ id: r.id,
4072
+ operation: _nullishCoalesce(r.operation, () => ( "apply")),
4073
+ ...r.message ? { message: r.message } : {},
4074
+ ...r.actor ? { actor: r.actor } : {},
4075
+ ...r.ai_model ? { aiModel: r.ai_model } : {},
4076
+ ...r.parent_commit_id ? { parentCommitId: r.parent_commit_id } : {},
4077
+ itemCount: typeof r.item_count === "number" ? r.item_count : 0,
4078
+ items: this.parseCommitItems(r.items),
4079
+ ...r.created_at ? { createdAt: r.created_at } : {}
4080
+ }));
4081
+ mapped.sort((a, b) => String(_nullishCoalesce(b.createdAt, () => ( ""))).localeCompare(String(_nullishCoalesce(a.createdAt, () => ( "")))));
4082
+ return mapped;
4083
+ } catch (e37) {
4084
+ return [];
4085
+ }
4086
+ }
4087
+ /**
4088
+ * Revert a single commit (ADR-0067): undo exactly the artifacts it touched.
4089
+ * A created-by-this-commit artifact is soft-removed (metadata row deleted;
4090
+ * the data table is NOT dropped — recoverable, per ADR-0067 §5); a modified
4091
+ * artifact is restored to its pre-commit `prevVersion`. The revert is itself
4092
+ * recorded as a NEW commit (operation='revert'), so history stays
4093
+ * append-only and the revert is itself revertible.
4094
+ */
4095
+ async revertCommit(request) {
4096
+ await this.ensureOverlayIndex();
4097
+ const orgId = _nullishCoalesce(request.organizationId, () => ( null));
4098
+ const where = { id: request.commitId };
4099
+ if (request.organizationId) where.organization_id = request.organizationId;
4100
+ const row = await this.engine.findOne("sys_metadata_commit", { where });
4101
+ if (!row) {
4102
+ const err = new Error(`[commit_not_found] No commit '${request.commitId}'.`);
4103
+ err.code = "commit_not_found";
4104
+ err.status = 404;
4105
+ throw err;
4106
+ }
4107
+ const items = this.parseCommitItems(row.items);
4108
+ const repo = this.getOverlayRepo(orgId);
4109
+ const actor = _nullishCoalesce(request.actor, () => ( "system"));
4110
+ const reverted = [];
4111
+ const failed = [];
4112
+ for (const it of [...items].reverse()) {
4113
+ const ref = { type: it.type, name: it.name, org: _nullishCoalesce(orgId, () => ( "env")) };
4114
+ try {
4115
+ const current = await repo.get(ref, { state: "active" });
4116
+ if (!it.existedBefore) {
4117
+ if (current) {
4118
+ await repo.delete(ref, {
4119
+ parentVersion: current.hash,
4120
+ actor,
4121
+ source: "protocol.revertCommit",
4122
+ intent: "override-artifact",
4123
+ state: "active"
4124
+ });
4125
+ }
4126
+ reverted.push({ type: it.type, name: it.name, action: "removed" });
4127
+ } else if (it.prevVersion !== null && it.prevVersion !== void 0) {
4128
+ await repo.restoreVersion(ref, it.prevVersion, {
4129
+ actor,
4130
+ source: "protocol.revertCommit",
4131
+ message: `revert commit ${request.commitId}`
4132
+ });
4133
+ reverted.push({ type: it.type, name: it.name, action: "restored" });
4134
+ }
4135
+ } catch (e) {
4136
+ failed.push({
4137
+ type: it.type,
4138
+ name: it.name,
4139
+ error: _nullishCoalesce(_optionalChain([e, 'optionalAccess', _166 => _166.message]), () => ( "revert failed")),
4140
+ ..._optionalChain([e, 'optionalAccess', _167 => _167.code]) ? { code: e.code } : {}
4141
+ });
4142
+ }
4143
+ }
4144
+ const revertCommit = await this.recordPackageCommit({
4145
+ orgId,
4146
+ packageId: row.package_id,
4147
+ operation: "revert",
4148
+ message: `Revert: ${_nullishCoalesce(row.message, () => ( request.commitId))}`,
4149
+ ...request.actor ? { actor: request.actor } : {},
4150
+ parentCommitId: request.commitId,
4151
+ items: reverted.map((r) => ({
4152
+ type: r.type,
4153
+ name: r.name,
4154
+ existedBefore: r.action === "restored",
4155
+ prevVersion: null
4156
+ }))
4157
+ });
4158
+ return {
4159
+ success: failed.length === 0 && reverted.length > 0,
4160
+ revertedCount: reverted.length,
4161
+ failedCount: failed.length,
4162
+ reverted,
4163
+ failed,
4164
+ ...revertCommit ? { revertCommitId: revertCommit.commitId } : {}
4165
+ };
4166
+ }
4167
+ /**
4168
+ * Roll a package back THROUGH every `apply` commit newer than `commitId`
4169
+ * (newest first), leaving the package as it was at that commit. Each step is
4170
+ * an individual `revertCommit`, so the whole rollback is itself audited.
4171
+ */
4172
+ async rollbackToPackageCommit(request) {
4173
+ const where = { id: request.commitId };
4174
+ if (request.organizationId) where.organization_id = request.organizationId;
4175
+ const target = await this.engine.findOne("sys_metadata_commit", { where });
4176
+ if (!target) {
4177
+ const err = new Error(`[commit_not_found] No commit '${request.commitId}'.`);
4178
+ err.code = "commit_not_found";
4179
+ err.status = 404;
4180
+ throw err;
4181
+ }
4182
+ const all = await this.listCommits({
4183
+ packageId: target.package_id,
4184
+ ...request.organizationId ? { organizationId: request.organizationId } : {}
4185
+ });
4186
+ const targetCreatedAt = String(_nullishCoalesce(target.created_at, () => ( "")));
4187
+ const toRevert = all.filter(
4188
+ (c) => String(_nullishCoalesce(c.createdAt, () => ( ""))) > targetCreatedAt && c.operation === "apply"
4189
+ );
4190
+ const revertedCommits = [];
4191
+ const failed = [];
4192
+ for (const c of toRevert) {
4193
+ try {
4194
+ await this.revertCommit({
4195
+ commitId: c.id,
4196
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
4197
+ ...request.actor ? { actor: request.actor } : {}
4198
+ });
4199
+ revertedCommits.push(c.id);
4200
+ } catch (e) {
4201
+ failed.push({ commitId: c.id, error: _nullishCoalesce(_optionalChain([e, 'optionalAccess', _168 => _168.message]), () => ( "revert failed")) });
4202
+ }
4203
+ }
4204
+ return { success: failed.length === 0, revertedCommits, failed };
4205
+ }
4206
+ /**
4207
+ * Restore the body recorded at history `toVersion` as the new
4208
+ * live row. Writes a history event with `op='revert'`. 404
4209
+ * (`[version_not_found]`) when the target version doesn't exist;
4210
+ * 409 (`[version_not_restorable]`) when the target is a delete
4211
+ * tombstone (no body to bring back).
4212
+ */
4213
+ async rollbackMetaItem(request) {
4214
+ if (!Number.isFinite(request.toVersion) || request.toVersion < 1) {
4215
+ const err = new Error(
4216
+ `[invalid_request] rollbackMetaItem requires a positive integer 'toVersion' (got ${request.toVersion}).`
4217
+ );
4218
+ err.code = "invalid_request";
4219
+ err.status = 400;
4220
+ throw err;
4221
+ }
4222
+ const singularType = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( request.type));
4223
+ if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
4224
+ const err = new Error(
4225
+ `[not_overridable] Metadata type '${request.type}' is not revertable \u2014 no overlay/runtime-create permission.`
4226
+ );
4227
+ err.code = "not_overridable";
4228
+ err.status = 403;
4229
+ throw err;
4230
+ }
4231
+ const _rollbackLockErr = await this.assertLockAllowsWrite({
4232
+ type: request.type,
4233
+ name: request.name,
4234
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
4235
+ operation: "rollback",
4236
+ ...request.actor ? { actor: request.actor } : {},
4237
+ source: "protocol.rollbackMetaItem"
4238
+ });
4239
+ if (_rollbackLockErr) throw _rollbackLockErr;
4240
+ await this.ensureOverlayIndex();
4241
+ const orgId = _nullishCoalesce(request.organizationId, () => ( null));
4242
+ const repo = this.getOverlayRepo(orgId);
4243
+ const artifactBacked = this.isArtifactBacked(singularType, request.name);
4244
+ const intent = artifactBacked ? "override-artifact" : "runtime-only";
4245
+ const ref = {
4246
+ type: singularType,
4247
+ name: request.name,
4248
+ org: _nullishCoalesce(orgId, () => ( "env"))
4249
+ };
4250
+ try {
4251
+ const result = await repo.restoreVersion(ref, request.toVersion, {
4252
+ actor: _nullishCoalesce(request.actor, () => ( "system")),
4253
+ source: "protocol.rollbackMetaItem",
4254
+ ...request.message ? { message: request.message } : {},
4255
+ intent
4256
+ });
4257
+ this.applyObjectRegistryMutation({
4258
+ type: request.type,
4259
+ name: request.name,
4260
+ item: result.item.body
4261
+ });
4262
+ return {
4263
+ success: true,
4264
+ version: result.version,
4265
+ seq: result.seq,
4266
+ restoredFromVersion: request.toVersion,
4267
+ message: `Reverted to version ${request.toVersion} \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
4268
+ };
4269
+ } catch (err) {
4270
+ if (err instanceof _metadatacore.ConflictError) {
4271
+ const conflict = new Error(
4272
+ `[metadata_conflict] ${request.type}/${request.name} advanced during rollback. Expected parent ${_nullishCoalesce(err.expectedParent, () => ( "null"))} but current is ${_nullishCoalesce(err.actualHead, () => ( "null"))}.`
4273
+ );
4274
+ conflict.code = "metadata_conflict";
4275
+ conflict.status = 409;
4276
+ conflict.expectedParent = err.expectedParent;
4277
+ conflict.actualHead = err.actualHead;
4278
+ throw conflict;
4279
+ }
4280
+ throw err;
4281
+ }
4282
+ }
4283
+ /**
4284
+ * Compute a shallow structural diff between two historical
4285
+ * versions of a metadata item. Either side may be omitted: when
4286
+ * `toVersion` is undefined the current active body is used; when
4287
+ * `fromVersion` is undefined the immediately previous history row
4288
+ * is used. Returns `{ added, removed, changed }` keyed by JSON
4289
+ * pointer-style paths for primitive leaves; nested objects/arrays
4290
+ * are reported as a single change record.
4291
+ */
4292
+ async diffMetaItem(request) {
4293
+ const singularType = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( request.type));
4294
+ const orgId = _nullishCoalesce(request.organizationId, () => ( null));
4295
+ const events = (await this.historyMetaItem({
4296
+ type: singularType,
4297
+ name: request.name,
4298
+ ...orgId ? { organizationId: orgId } : {}
4299
+ })).events;
4300
+ const versions = events.map((ev) => ev.version).filter((v) => typeof v === "number");
4301
+ const repo = this.getOverlayRepo(orgId);
4302
+ const fullRef = {
4303
+ type: singularType,
4304
+ name: request.name,
4305
+ org: _nullishCoalesce(orgId, () => ( "env"))
4306
+ };
4307
+ const histRows = [];
4308
+ try {
4309
+ const engineAny = this.engine;
4310
+ const rows = await engineAny.find("sys_metadata_history", {
4311
+ where: {
4312
+ organization_id: orgId,
4313
+ type: singularType,
4314
+ name: request.name
4315
+ }
4316
+ });
4317
+ rows.sort((a, b) => (_nullishCoalesce(a.version, () => ( 0))) - (_nullishCoalesce(b.version, () => ( 0))));
4318
+ for (const r of rows) {
4319
+ const body = r.metadata == null ? null : typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata;
4320
+ histRows.push({ version: _nullishCoalesce(r.version, () => ( 0)), body });
4321
+ }
4322
+ } catch (e38) {
4323
+ }
4324
+ const byVersion = /* @__PURE__ */ new Map();
4325
+ for (const r of histRows) byVersion.set(r.version, r.body);
4326
+ let fromBody = null;
4327
+ let toBody = null;
4328
+ let fromVersion = null;
4329
+ let toVersion = null;
4330
+ if (request.toVersion !== void 0) {
4331
+ toVersion = request.toVersion;
4332
+ toBody = _nullishCoalesce(byVersion.get(request.toVersion), () => ( null));
4333
+ } else {
4334
+ const current = await repo.get(fullRef, { state: "active" });
4335
+ toBody = current ? current.body : null;
4336
+ toVersion = histRows.length ? histRows[histRows.length - 1].version : null;
4337
+ }
4338
+ if (request.fromVersion !== void 0) {
4339
+ fromVersion = request.fromVersion;
4340
+ fromBody = _nullishCoalesce(byVersion.get(request.fromVersion), () => ( null));
4341
+ } else if (toVersion !== null) {
4342
+ const sorted = histRows.map((r) => r.version).filter((v) => v < toVersion);
4343
+ if (sorted.length) {
4344
+ fromVersion = sorted[sorted.length - 1];
4345
+ fromBody = _nullishCoalesce(byVersion.get(fromVersion), () => ( null));
4346
+ }
4347
+ }
4348
+ const diff = diffShallow(_nullishCoalesce(fromBody, () => ( {})), _nullishCoalesce(toBody, () => ( {})));
4349
+ const _used = versions;
4350
+ void _used;
4351
+ return {
4352
+ type: request.type,
4353
+ name: request.name,
4354
+ fromVersion,
4355
+ toVersion,
4356
+ ...diff
4357
+ };
4358
+ }
4359
+ /**
4360
+ * Remove a customization overlay row for the given metadata item, so the
4361
+ * next read falls through to the artifact-loaded default. Implements the
4362
+ * "Reset to factory default" semantic from ADR-0005. Whitelist is shared
4363
+ * with {@link saveMetaItem}.
4364
+ */
4365
+ async deleteMetaItem(request) {
4366
+ if (this.environmentId !== void 0) {
4367
+ const overlayAllowed = _ObjectStackProtocolImplementation.isOverlayAllowed(request.type);
4368
+ const runtimeCreateAllowed = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(request.type);
4369
+ const artifactBacked = this.isArtifactBacked(request.type, request.name);
4370
+ if (artifactBacked && !overlayAllowed) {
4371
+ const err = new Error(
4372
+ `[not_overridable] Metadata item '${request.type}/${request.name}' is provided by a code package and the type has not opted into per-org overlay writes. See docs/adr/0005-metadata-customization-overlay.md.`
4373
+ );
4374
+ err.code = "not_overridable";
4375
+ err.status = 403;
4376
+ throw err;
4377
+ }
4378
+ if (!artifactBacked && !overlayAllowed && !runtimeCreateAllowed) {
4379
+ const err = new Error(
4380
+ `[not_creatable] Metadata type '${request.type}' does not allow runtime creation or deletion.`
4381
+ );
4382
+ err.code = "not_creatable";
4383
+ err.status = 403;
4384
+ throw err;
4385
+ }
4386
+ const lockErr = await this.assertLockAllowsDelete({
4387
+ type: request.type,
4388
+ name: request.name,
4389
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
4390
+ ...request.actor ? { actor: request.actor } : {},
4391
+ source: "protocol.deleteMetaItem"
4392
+ });
4393
+ if (lockErr) throw lockErr;
4394
+ }
4395
+ const singularTypeForRepo = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( request.type));
4396
+ const overlayAllowedForRepoDel = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
4397
+ const runtimeCreateAllowedForRepoDel = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularTypeForRepo);
4398
+ const useRepoPath = overlayAllowedForRepoDel || runtimeCreateAllowedForRepoDel;
4399
+ if (useRepoPath) {
4400
+ const orgId = _nullishCoalesce(request.organizationId, () => ( null));
4401
+ const repo = this.getOverlayRepo(orgId);
4402
+ const ref = {
4403
+ type: singularTypeForRepo,
4404
+ name: request.name,
4405
+ org: _nullishCoalesce(orgId, () => ( "env"))
4406
+ };
4407
+ try {
4408
+ const targetState = request.state === "draft" ? "draft" : "active";
4409
+ const current = await repo.get(ref, { state: targetState });
4410
+ if (!current) {
4411
+ if (targetState === "active") {
4412
+ await this.restoreArtifactRegistryView(request.type, request.name);
4413
+ }
4414
+ return {
4415
+ success: true,
4416
+ reset: false,
4417
+ message: targetState === "draft" ? `No pending draft for ${request.type}/${request.name}.` : `No customization overlay found for ${request.type}/${request.name} \u2014 already at artifact default.`
4418
+ };
4419
+ }
4420
+ const parentVersion = request.parentVersion !== void 0 ? _nullishCoalesce(request.parentVersion, () => ( current.hash)) : current.hash;
4421
+ const result = await repo.delete(ref, {
4422
+ parentVersion,
4423
+ actor: _nullishCoalesce(request.actor, () => ( "system")),
4424
+ source: "protocol.deleteMetaItem",
4425
+ intent: this.isArtifactBacked(singularTypeForRepo, request.name) ? "override-artifact" : "runtime-only",
4426
+ state: targetState
4427
+ });
4428
+ if (targetState === "active") {
4429
+ await this.restoreArtifactRegistryView(request.type, request.name);
4430
+ }
4431
+ if (this.shouldDropStorage(request.type, request.name, request.dropStorage, targetState)) {
4432
+ await this.dropObjectStorage(singularTypeForRepo, request.name);
4433
+ }
4434
+ await this.recordMetadataAudit({
4435
+ type: request.type,
4436
+ name: request.name,
4437
+ organizationId: orgId,
4438
+ operation: "delete",
4439
+ outcome: "allowed",
4440
+ code: "ok",
4441
+ ...request.actor ? { actor: request.actor } : {},
4442
+ source: "protocol.deleteMetaItem",
4443
+ note: targetState
4444
+ });
4445
+ return {
4446
+ success: true,
4447
+ reset: true,
4448
+ seq: result.seq,
4449
+ message: request.state === "draft" ? `Draft discarded \u2014 ${request.type}/${request.name}. [seq=${result.seq}]` : `Customization overlay deleted \u2014 ${request.type}/${request.name} reset to artifact default. [seq=${result.seq}]`
4450
+ };
4451
+ } catch (err) {
4452
+ if (err instanceof _metadatacore.ConflictError) {
4453
+ const conflict = new Error(
4454
+ `[metadata_conflict] ${request.type}/${request.name} has been modified since you loaded it. Expected parent ${_nullishCoalesce(err.expectedParent, () => ( "null"))} but current is ${_nullishCoalesce(err.actualHead, () => ( "null"))}.`
4455
+ );
4456
+ conflict.code = "metadata_conflict";
4457
+ conflict.status = 409;
4458
+ conflict.expectedParent = err.expectedParent;
4459
+ conflict.actualHead = err.actualHead;
4460
+ throw conflict;
4461
+ }
4462
+ const e = new Error(`Failed to delete customization overlay: ${_nullishCoalesce(err.message, () => ( err))}`);
4463
+ e.status = _nullishCoalesce(_optionalChain([err, 'optionalAccess', _169 => _169.status]), () => ( 500));
4464
+ throw e;
4465
+ }
4466
+ }
4467
+ const scopedWhere = {
4468
+ type: request.type,
4469
+ name: request.name,
4470
+ organization_id: _nullishCoalesce(request.organizationId, () => ( null))
4471
+ };
4472
+ try {
4473
+ const existing = await this.engine.findOne("sys_metadata", { where: scopedWhere });
4474
+ if (!existing) {
4475
+ return {
4476
+ success: true,
4477
+ reset: false,
4478
+ message: `No customization overlay found for ${request.type}/${request.name} \u2014 already at artifact default.`
4479
+ };
4480
+ }
4481
+ await this.engine.delete("sys_metadata", { where: { id: existing.id } });
4482
+ {
4483
+ const targetState = request.state === "draft" ? "draft" : "active";
4484
+ if (this.shouldDropStorage(request.type, request.name, request.dropStorage, targetState)) {
4485
+ await this.dropObjectStorage(_nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( request.type)), request.name);
4486
+ }
4487
+ }
4488
+ if (request.state !== "draft") {
4489
+ await this.restoreArtifactRegistryView(request.type, request.name);
4490
+ }
4491
+ return {
4492
+ success: true,
4493
+ reset: true,
4494
+ message: `Customization overlay deleted \u2014 ${request.type}/${request.name} reset to artifact default.`
4495
+ };
4496
+ } catch (err) {
4497
+ const e = new Error(`Failed to delete customization overlay: ${err.message}`);
4498
+ e.status = 500;
4499
+ throw e;
4500
+ }
4501
+ }
4502
+ /**
4503
+ * Hydrate SchemaRegistry from the database on startup.
4504
+ * Loads all active metadata records and registers them in the in-memory registry.
4505
+ * Safe to call repeatedly — idempotent (latest DB record wins).
4506
+ *
4507
+ * Per ADR-0005, project-kernel mode ALSO hydrates from sys_metadata —
4508
+ * customization overlay rows must survive restart. Scope filter
4509
+ * (`environment_id = this.environmentId ?? null`) keeps tenants isolated.
4510
+ */
4511
+ async loadMetaFromDb() {
4512
+ let loaded = 0;
4513
+ let errors = 0;
4514
+ try {
4515
+ const where = {
4516
+ state: "active",
4517
+ organization_id: null
4518
+ };
4519
+ const records = await this.engine.find("sys_metadata", { where });
4520
+ for (const record of records) {
4521
+ try {
4522
+ const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
4523
+ const normalizedType = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[record.type], () => ( record.type));
4524
+ if (normalizedType === "object") {
4525
+ this.engine.registry.registerObject(data, record.packageId || "sys_metadata");
4526
+ } else {
4527
+ const artifact = this.lookupArtifactItem(normalizedType, _optionalChain([data, 'optionalAccess', _170 => _170.name]));
4528
+ this.engine.registry.registerItem(
4529
+ normalizedType,
4530
+ mergeArtifactProtection(data, artifact),
4531
+ "name"
4532
+ );
4533
+ }
4534
+ loaded++;
4535
+ } catch (e) {
4536
+ errors++;
4537
+ console.warn(`[Protocol] Failed to hydrate ${record.type}/${record.name}: ${e instanceof Error ? e.message : String(e)}`);
4538
+ }
4539
+ }
4540
+ } catch (e) {
4541
+ if (!/no such table/i.test(_nullishCoalesce(e.message, () => ( "")))) {
4542
+ console.warn(`[Protocol] DB hydration skipped: ${e.message}`);
4543
+ }
4544
+ }
4545
+ return { loaded, errors };
4546
+ }
4547
+ // ==========================================
4548
+ // Metadata References (Phase 3a-references)
4549
+ // ==========================================
4550
+ /**
4551
+ * Scan all loaded metadata for references pointing at the given
4552
+ * `{type, name}` target. Returns one row per referring artifact with
4553
+ * the path that produced the hit, so the admin UI can render an
4554
+ * "Used by" panel before destructive actions (rename / delete /
4555
+ * type-narrowing).
4556
+ *
4557
+ * Coverage is driven by the hand-curated {@link REFERENCE_PATHS}
4558
+ * registry. Types not present in the registry simply return no hits
4559
+ * — the engine never throws.
4560
+ */
4561
+ async findReferencesToMeta(request) {
4562
+ const singularTarget = _nullishCoalesce(_shared.PLURAL_TO_SINGULAR[request.type], () => ( request.type));
4563
+ const targetName = request.name;
4564
+ const matchers = REFERENCE_PATHS[singularTarget];
4565
+ if (!matchers || matchers.length === 0) {
4566
+ return { references: [] };
4567
+ }
4568
+ const seen = /* @__PURE__ */ new Set();
4569
+ const out = [];
4570
+ await Promise.all(
4571
+ matchers.map(async (matcher) => {
4572
+ let items = [];
4573
+ try {
4574
+ const result = await this.getMetaItems({
4575
+ type: matcher.fromType,
4576
+ ...request.organizationId ? { organizationId: request.organizationId } : {}
4577
+ });
4578
+ items = _nullishCoalesce(_optionalChain([result, 'optionalAccess', _171 => _171.items]), () => ( []));
4579
+ } catch (e39) {
4580
+ return;
4581
+ }
4582
+ for (const raw of items) {
4583
+ if (!raw || typeof raw !== "object") continue;
4584
+ const sourceName = raw.name;
4585
+ if (!sourceName) continue;
4586
+ const isSelfReference = matcher.fromType === singularTarget && sourceName === targetName;
4587
+ for (const path of matcher.paths) {
4588
+ const values = extractPathValues(raw, path);
4589
+ if (!values.includes(targetName)) continue;
4590
+ if (isSelfReference && !path.includes("[]") && !path.includes("{}")) continue;
4591
+ const key = `${matcher.fromType}|${sourceName}|${path}`;
4592
+ if (seen.has(key)) continue;
4593
+ seen.add(key);
4594
+ const label = raw.label;
4595
+ out.push({
4596
+ type: matcher.fromType,
4597
+ name: sourceName,
4598
+ ...label ? { label } : {},
4599
+ path,
4600
+ kind: matcher.kind
4601
+ });
4602
+ }
4603
+ }
4604
+ })
4605
+ );
4606
+ out.sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
4607
+ return { references: out };
4608
+ }
4609
+ // ==========================================
4610
+ // Feed Operations
4611
+ // ==========================================
4612
+ async listFeed(request) {
4613
+ const svc = this.requireFeedService();
4614
+ const result = await svc.listFeed({
4615
+ object: request.object,
4616
+ recordId: request.recordId,
4617
+ filter: request.type,
4618
+ limit: request.limit,
4619
+ cursor: request.cursor
4620
+ });
4621
+ return { success: true, data: result };
4622
+ }
4623
+ async createFeedItem(request) {
4624
+ const svc = this.requireFeedService();
4625
+ const item = await svc.createFeedItem({
4626
+ object: request.object,
4627
+ recordId: request.recordId,
4628
+ type: request.type,
4629
+ actor: { type: "user", id: "current_user" },
4630
+ body: request.body,
4631
+ mentions: request.mentions,
4632
+ parentId: request.parentId,
4633
+ visibility: request.visibility
4634
+ });
4635
+ return { success: true, data: item };
4636
+ }
4637
+ async updateFeedItem(request) {
4638
+ const svc = this.requireFeedService();
4639
+ const item = await svc.updateFeedItem(request.feedId, {
4640
+ body: request.body,
4641
+ mentions: request.mentions,
4642
+ visibility: request.visibility
4643
+ });
4644
+ return { success: true, data: item };
4645
+ }
4646
+ async deleteFeedItem(request) {
4647
+ const svc = this.requireFeedService();
4648
+ await svc.deleteFeedItem(request.feedId);
4649
+ return { success: true, data: { feedId: request.feedId } };
4650
+ }
4651
+ async addReaction(request) {
4652
+ const svc = this.requireFeedService();
4653
+ const reactions = await svc.addReaction(request.feedId, request.emoji, "current_user");
4654
+ return { success: true, data: { reactions } };
4655
+ }
4656
+ async removeReaction(request) {
4657
+ const svc = this.requireFeedService();
4658
+ const reactions = await svc.removeReaction(request.feedId, request.emoji, "current_user");
4659
+ return { success: true, data: { reactions } };
4660
+ }
4661
+ async pinFeedItem(request) {
4662
+ const svc = this.requireFeedService();
4663
+ const item = await svc.getFeedItem(request.feedId);
4664
+ if (!item) throw new Error(`Feed item ${request.feedId} not found`);
4665
+ await svc.updateFeedItem(request.feedId, { visibility: item.visibility });
4666
+ return { success: true, data: { feedId: request.feedId, pinned: true, pinnedAt: (/* @__PURE__ */ new Date()).toISOString() } };
4667
+ }
4668
+ async unpinFeedItem(request) {
4669
+ const svc = this.requireFeedService();
4670
+ const item = await svc.getFeedItem(request.feedId);
4671
+ if (!item) throw new Error(`Feed item ${request.feedId} not found`);
4672
+ await svc.updateFeedItem(request.feedId, { visibility: item.visibility });
4673
+ return { success: true, data: { feedId: request.feedId, pinned: false } };
4674
+ }
4675
+ async starFeedItem(request) {
4676
+ const svc = this.requireFeedService();
4677
+ const item = await svc.getFeedItem(request.feedId);
4678
+ if (!item) throw new Error(`Feed item ${request.feedId} not found`);
4679
+ await svc.updateFeedItem(request.feedId, { visibility: item.visibility });
4680
+ return { success: true, data: { feedId: request.feedId, starred: true, starredAt: (/* @__PURE__ */ new Date()).toISOString() } };
4681
+ }
4682
+ async unstarFeedItem(request) {
4683
+ const svc = this.requireFeedService();
4684
+ const item = await svc.getFeedItem(request.feedId);
4685
+ if (!item) throw new Error(`Feed item ${request.feedId} not found`);
4686
+ await svc.updateFeedItem(request.feedId, { visibility: item.visibility });
4687
+ return { success: true, data: { feedId: request.feedId, starred: false } };
4688
+ }
4689
+ async searchFeed(request) {
4690
+ const svc = this.requireFeedService();
4691
+ const result = await svc.listFeed({
4692
+ object: request.object,
4693
+ recordId: request.recordId,
4694
+ filter: request.type,
4695
+ limit: request.limit,
4696
+ cursor: request.cursor
4697
+ });
4698
+ const queryLower = (request.query || "").toLowerCase();
4699
+ const filtered = result.items.filter(
4700
+ (item) => _optionalChain([item, 'access', _172 => _172.body, 'optionalAccess', _173 => _173.toLowerCase, 'call', _174 => _174(), 'access', _175 => _175.includes, 'call', _176 => _176(queryLower)])
4701
+ );
4702
+ return { success: true, data: { items: filtered, total: filtered.length, hasMore: false } };
4703
+ }
4704
+ async getChangelog(request) {
4705
+ const svc = this.requireFeedService();
4706
+ const result = await svc.listFeed({
4707
+ object: request.object,
4708
+ recordId: request.recordId,
4709
+ filter: "changes_only",
4710
+ limit: request.limit,
4711
+ cursor: request.cursor
4712
+ });
4713
+ const entries = result.items.map((item) => ({
4714
+ id: item.id,
4715
+ object: item.object,
4716
+ recordId: item.recordId,
4717
+ actor: item.actor,
4718
+ changes: item.changes || [],
4719
+ timestamp: item.createdAt,
4720
+ source: item.source
4721
+ }));
4722
+ return { success: true, data: { entries, total: result.total, nextCursor: result.nextCursor, hasMore: result.hasMore } };
4723
+ }
4724
+ async feedSubscribe(request) {
4725
+ const svc = this.requireFeedService();
4726
+ const subscription = await svc.subscribe({
4727
+ object: request.object,
4728
+ recordId: request.recordId,
4729
+ userId: "current_user",
4730
+ events: request.events,
4731
+ channels: request.channels
4732
+ });
4733
+ return { success: true, data: subscription };
4734
+ }
4735
+ async feedUnsubscribe(request) {
4736
+ const svc = this.requireFeedService();
4737
+ const unsubscribed = await svc.unsubscribe(request.object, request.recordId, "current_user");
4738
+ return { success: true, data: { object: request.object, recordId: request.recordId, unsubscribed } };
4739
+ }
4740
+ /**
4741
+ * Install a package from a manifest — the single canonical write primitive
4742
+ * for the package subsystem (ADR-0033 consolidation).
4743
+ *
4744
+ * It writes BOTH stores that the runtime keeps for packages, so a package
4745
+ * surfaces consistently no matter which read path is used:
4746
+ * 1. the in-memory `SchemaRegistry` (what the dispatcher's
4747
+ * `/api/v1/packages` list/detail and `getMetaItems({type:'package'})`
4748
+ * read — i.e. what Studio's package selector shows), and
4749
+ * 2. the durable `sys_packages` table via the optional `package` service
4750
+ * (so the package survives a restart; that service re-hydrates these
4751
+ * rows back into the registry on boot).
4752
+ *
4753
+ * The DB write is best-effort and non-fatal: when the `package` service is
4754
+ * absent (e.g. the `marketplace` capability is off) the package is still
4755
+ * registered in-memory and visible for the lifetime of the process.
4756
+ */
4757
+ async installPackage(request) {
4758
+ const manifest = request.manifest;
4759
+ const pkg = this.engine.registry.installPackage(manifest, request.settings);
4760
+ try {
4761
+ const services = _optionalChain([this, 'access', _177 => _177.getServicesRegistry, 'optionalCall', _178 => _178()]);
4762
+ const pkgSvc = _optionalChain([services, 'optionalAccess', _179 => _179.get, 'call', _180 => _180("package")]);
4763
+ if (_optionalChain([pkgSvc, 'optionalAccess', _181 => _181.publish]) && _optionalChain([manifest, 'optionalAccess', _182 => _182.version])) {
4764
+ await pkgSvc.publish({ manifest, metadata: {} });
4765
+ }
4766
+ } catch (e) {
4767
+ console.warn(
4768
+ `[protocol.installPackage] sys_packages persist skipped for '${_optionalChain([manifest, 'optionalAccess', _183 => _183.id])}': ${_optionalChain([e, 'optionalAccess', _184 => _184.message])}`
4769
+ );
4770
+ }
4771
+ return { package: pkg, message: `Installed package: ${_optionalChain([manifest, 'optionalAccess', _185 => _185.id])}` };
4772
+ }
4773
+ };
4774
+ /**
4775
+ * Metadata types that are customer-overridable via {@link saveMetaItem}/
4776
+ * {@link deleteMetaItem} in project-kernel mode. Derived from the canonical
4777
+ * registry in {@link DEFAULT_METADATA_TYPE_REGISTRY}: a type opts in by
4778
+ * setting `allowOrgOverride: true` on its registry entry. The set is
4779
+ * augmented with the plural form of every singular so callers using REST
4780
+ * conventions (`/api/v1/meta/views/...`) get the same gate. See ADR-0005
4781
+ * §"Whitelist enforcement" for the rationale and the per-type rollout
4782
+ * checklist.
4783
+ */
4784
+ _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
4785
+ const out = /* @__PURE__ */ new Set();
4786
+ for (const entry of _kernel.DEFAULT_METADATA_TYPE_REGISTRY) {
4787
+ if (!entry.allowOrgOverride) continue;
4788
+ out.add(entry.type);
4789
+ const plural = _shared.SINGULAR_TO_PLURAL[entry.type];
4790
+ if (plural) out.add(plural);
4791
+ }
4792
+ return out;
4793
+ })();
4794
+ /**
4795
+ * Phase 3a-env-writable: parse `OS_METADATA_WRITABLE` once.
4796
+ * Comma-separated singular type names. When the env var is set, the
4797
+ * listed types get treated as `allowOrgOverride: true` regardless of
4798
+ * their static registry entry. This is the runtime escape hatch admins
4799
+ * use to enable Studio-side editing of types whose protocol-level flag
4800
+ * is still false (object, field, permission, …).
4801
+ *
4802
+ * Memoised at first call. Tests can override by clearing the cache via
4803
+ * {@link ObjectStackProtocolImplementation.resetEnvWritableCache}.
4804
+ */
4805
+ _ObjectStackProtocolImplementation._envWritableTypes = null;
4806
+ /**
4807
+ * Types that opt into runtime creation of brand-new items (ADR-0005
4808
+ * extension — two-tier model). A type may have
4809
+ * `allowOrgOverride: false` (cannot overlay artifact-shipped items)
4810
+ * yet still set `allowRuntimeCreate: true` (users can author new
4811
+ * items in `sys_metadata`). The two flags are orthogonal; see
4812
+ * {@link isArtifactBacked} for how the protocol decides which gate
4813
+ * applies to a given save/delete.
4814
+ */
4815
+ /**
4816
+ * Set of type names that have a static entry in
4817
+ * `DEFAULT_METADATA_TYPE_REGISTRY`. Anything outside this set is
4818
+ * runtime-registered (plugin-provided types like `theme`, `api`,
4819
+ * `connector`) — the listing endpoint at `getMetaTypes()` synthesises
4820
+ * those with `allowRuntimeCreate: true`, so this gate must agree.
4821
+ */
4822
+ _ObjectStackProtocolImplementation.STATIC_REGISTRY_TYPES = (() => {
4823
+ const out = /* @__PURE__ */ new Set();
4824
+ for (const entry of _kernel.DEFAULT_METADATA_TYPE_REGISTRY) {
4825
+ out.add(entry.type);
4826
+ const plural = _shared.SINGULAR_TO_PLURAL[entry.type];
4827
+ if (plural) out.add(plural);
4828
+ }
4829
+ return out;
4830
+ })();
4831
+ _ObjectStackProtocolImplementation.RUNTIME_CREATE_ALLOWED_TYPES = (() => {
4832
+ const out = /* @__PURE__ */ new Set();
4833
+ for (const entry of _kernel.DEFAULT_METADATA_TYPE_REGISTRY) {
4834
+ if (!entry.allowRuntimeCreate) continue;
4835
+ out.add(entry.type);
4836
+ const plural = _shared.SINGULAR_TO_PLURAL[entry.type];
4837
+ if (plural) out.add(plural);
4838
+ }
4839
+ return out;
4840
+ })();
4841
+ var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
4842
+
4843
+
4844
+
4845
+
4846
+
4847
+
4848
+
4849
+
4850
+
4851
+
4852
+
4853
+
4854
+ exports.ConcurrentUpdateError = ConcurrentUpdateError; exports.ObjectStackProtocolImplementation = ObjectStackProtocolImplementation; exports.SeedLoaderService = _chunkJRNTUZG6cjs.SeedLoaderService; exports.SysMetadataRepository = SysMetadataRepository; exports.computeMetadataDiagnostics = computeMetadataDiagnostics; exports.computeViewReferenceDiagnostics = computeViewReferenceDiagnostics; exports.decorateMetadataItem = decorateMetadataItem; exports.decorateMetadataItems = decorateMetadataItems; exports.normalizeViewMetadata = normalizeViewMetadata; exports.resetEnvWritableMetadataTypes = resetEnvWritableMetadataTypes; exports.runBuildProbes = _chunk7LOFAEHAcjs.runBuildProbes;
4855
+ //# sourceMappingURL=index.cjs.map