@objectstack/objectql 6.9.0 → 7.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.js CHANGED
@@ -440,6 +440,14 @@ var SchemaRegistry = class {
440
440
  if (collection.has(storageKey)) {
441
441
  this.log(`[Registry] Overwriting ${type}: ${storageKey}`);
442
442
  }
443
+ if (packageId && collection.has(baseName)) {
444
+ const dbOnly = collection.get(baseName);
445
+ if (dbOnly && !dbOnly._packageId) {
446
+ console.warn(
447
+ `[Registry] Collision: ${type}/${baseName} ships from package "${packageId}" but a runtime-authored row with the same name already exists in sys_metadata. The runtime row will shadow the package value (ADR-0005 overlay precedence). Rename one, or delete the sys_metadata row if the package value should win.`
448
+ );
449
+ }
450
+ }
443
451
  collection.set(storageKey, item);
444
452
  this.log(`[Registry] Registered ${type}: ${storageKey}`);
445
453
  }
@@ -676,6 +684,12 @@ var import_shared = require("@objectstack/spec/shared");
676
684
  var OVERLAY_ALLOWED_TYPES = new Set(
677
685
  import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowOrgOverride).map((e) => e.type)
678
686
  );
687
+ var STATIC_REGISTRY_TYPES = new Set(
688
+ import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY.map((e) => e.type)
689
+ );
690
+ var RUNTIME_CREATE_ALLOWED_TYPES = new Set(
691
+ import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowRuntimeCreate).map((e) => e.type)
692
+ );
679
693
  var _envWritableMetadataTypes = null;
680
694
  function envWritableMetadataTypes() {
681
695
  if (_envWritableMetadataTypes !== null) return _envWritableMetadataTypes;
@@ -725,11 +739,16 @@ var SysMetadataRepository = class {
725
739
  /**
726
740
  * Read the current overlay row. Returns null if no row exists —
727
741
  * callers (e.g. LayeredRepository) fall through to lower layers.
742
+ *
743
+ * `opts.state` selects which lifecycle row to read: defaults to the
744
+ * live published row (`'active'`). Pass `'draft'` to read the pending
745
+ * unpublished revision (if any).
728
746
  */
729
- async get(ref) {
747
+ async get(ref, opts) {
730
748
  this.assertOpen();
749
+ const state = opts?.state ?? "active";
731
750
  const row = await this.engine.findOne("sys_metadata", {
732
- where: this.whereFor(ref)
751
+ where: this.whereFor(ref, state)
733
752
  });
734
753
  if (!row) return null;
735
754
  return this.rowToItem(ref, row);
@@ -772,12 +791,13 @@ var SysMetadataRepository = class {
772
791
  }
773
792
  async put(ref, spec, opts) {
774
793
  this.assertOpen();
775
- this.assertAllowed(ref.type);
794
+ this.assertAllowed(ref.type, opts.intent);
795
+ const state = opts.state ?? "active";
776
796
  const body = spec ?? {};
777
797
  const hash = (0, import_metadata_core.hashSpec)(body);
778
798
  const result = await this.withTxn(async (ctx) => {
779
799
  const existing = await this.engine.findOne("sys_metadata", {
780
- where: this.whereFor(ref),
800
+ where: this.whereFor(ref, state),
781
801
  context: ctx
782
802
  });
783
803
  const existingHash = existing?.checksum ?? null;
@@ -789,7 +809,8 @@ var SysMetadataRepository = class {
789
809
  return { skipped: true, version: hash, seq: item2.seq, item: item2 };
790
810
  }
791
811
  const now = (/* @__PURE__ */ new Date()).toISOString();
792
- const op = existing ? "update" : "create";
812
+ const baseOp = existing ? "update" : "create";
813
+ const op = opts.opType ?? baseOp;
793
814
  const version = await this.nextItemVersion(ref, ctx);
794
815
  const eventSeq = await this.nextEventSeq(ctx);
795
816
  const parentRowData = {
@@ -798,7 +819,7 @@ var SysMetadataRepository = class {
798
819
  organization_id: this.organizationId,
799
820
  metadata: JSON.stringify(body),
800
821
  checksum: hash,
801
- state: "active",
822
+ state,
802
823
  version,
803
824
  updated_at: now
804
825
  };
@@ -864,25 +885,28 @@ var SysMetadataRepository = class {
864
885
  return { version: result.version, seq: result.seq, item: result.item };
865
886
  }
866
887
  this.seqCounter = result.seq;
867
- this.broadcast({
868
- seq: result.seq,
869
- op: result.op,
870
- ref: this.fullRef(ref),
871
- hash: result.version,
872
- parentHash: result.existingHash,
873
- actor: result.actor,
874
- message: result.message,
875
- ts: result.now,
876
- source: result.source
877
- });
888
+ if (state === "active") {
889
+ this.broadcast({
890
+ seq: result.seq,
891
+ op: result.op,
892
+ ref: this.fullRef(ref),
893
+ hash: result.version,
894
+ parentHash: result.existingHash,
895
+ actor: result.actor,
896
+ message: result.message,
897
+ ts: result.now,
898
+ source: result.source
899
+ });
900
+ }
878
901
  return { version: result.version, seq: result.seq, item: result.item };
879
902
  }
880
903
  async delete(ref, opts) {
881
904
  this.assertOpen();
882
- this.assertAllowed(ref.type);
905
+ this.assertAllowed(ref.type, opts.intent);
906
+ const state = opts.state ?? "active";
883
907
  const result = await this.withTxn(async (ctx) => {
884
908
  const existing = await this.engine.findOne("sys_metadata", {
885
- where: this.whereFor(ref),
909
+ where: this.whereFor(ref, state),
886
910
  context: ctx
887
911
  });
888
912
  if (!existing) {
@@ -899,32 +923,38 @@ var SysMetadataRepository = class {
899
923
  );
900
924
  }
901
925
  const now = (/* @__PURE__ */ new Date()).toISOString();
902
- const version = await this.nextItemVersion(ref, ctx);
903
- const eventSeq = await this.nextEventSeq(ctx);
926
+ let version = 0;
927
+ let eventSeq = 0;
928
+ if (state === "active") {
929
+ version = await this.nextItemVersion(ref, ctx);
930
+ eventSeq = await this.nextEventSeq(ctx);
931
+ }
904
932
  await this.engine.delete("sys_metadata", {
905
933
  where: { id: existingId },
906
934
  context: ctx
907
935
  });
908
- await this.engine.insert(
909
- this.historyTable,
910
- {
911
- id: this.uuid(),
912
- event_seq: eventSeq,
913
- type: ref.type,
914
- name: ref.name,
915
- version,
916
- operation_type: "delete",
917
- metadata: null,
918
- checksum: null,
919
- previous_checksum: existingHash,
920
- change_note: opts.message,
921
- source: opts.source ?? "sys-metadata-repo",
922
- organization_id: this.organizationId,
923
- recorded_by: opts.actor,
924
- recorded_at: now
925
- },
926
- { context: ctx }
927
- );
936
+ if (state === "active") {
937
+ await this.engine.insert(
938
+ this.historyTable,
939
+ {
940
+ id: this.uuid(),
941
+ event_seq: eventSeq,
942
+ type: ref.type,
943
+ name: ref.name,
944
+ version,
945
+ operation_type: "delete",
946
+ metadata: null,
947
+ checksum: null,
948
+ previous_checksum: existingHash,
949
+ change_note: opts.message,
950
+ source: opts.source ?? "sys-metadata-repo",
951
+ organization_id: this.organizationId,
952
+ recorded_by: opts.actor,
953
+ recorded_at: now
954
+ },
955
+ { context: ctx }
956
+ );
957
+ }
928
958
  return {
929
959
  eventSeq,
930
960
  existingHash,
@@ -934,20 +964,117 @@ var SysMetadataRepository = class {
934
964
  actor: opts.actor
935
965
  };
936
966
  });
937
- this.seqCounter = result.eventSeq;
938
- this.broadcast({
939
- seq: result.eventSeq,
940
- op: "delete",
941
- ref: this.fullRef(ref),
942
- hash: null,
943
- parentHash: result.existingHash,
944
- actor: result.actor,
945
- message: result.message,
946
- ts: result.now,
947
- source: result.source
948
- });
967
+ if (state === "active") {
968
+ this.seqCounter = result.eventSeq;
969
+ this.broadcast({
970
+ seq: result.eventSeq,
971
+ op: "delete",
972
+ ref: this.fullRef(ref),
973
+ hash: null,
974
+ parentHash: result.existingHash,
975
+ actor: result.actor,
976
+ message: result.message,
977
+ ts: result.now,
978
+ source: result.source
979
+ });
980
+ }
949
981
  return { seq: result.eventSeq };
950
982
  }
983
+ /**
984
+ * Promote the pending draft row for `ref` into the live (`active`)
985
+ * overlay. Atomic: reads the draft inside the same transaction, runs
986
+ * the canonical `put` to upsert the active row (which appends a
987
+ * history event with `operation_type='publish'`), then deletes the
988
+ * draft row.
989
+ *
990
+ * Errors if no draft exists (callers should 404). The active row's
991
+ * `parentVersion` is computed from the current active hash so this
992
+ * also surfaces optimistic-lock conflicts when something else has
993
+ * published in between (e.g. another admin reverted to an older
994
+ * version since the draft was authored).
995
+ */
996
+ async promoteDraft(ref, opts) {
997
+ this.assertOpen();
998
+ const draft = await this.get(ref, { state: "draft" });
999
+ if (!draft) {
1000
+ const err = new Error(
1001
+ `[no_draft] No pending draft exists for ${ref.type}/${ref.name} \u2014 nothing to publish.`
1002
+ );
1003
+ err.code = "no_draft";
1004
+ err.status = 404;
1005
+ throw err;
1006
+ }
1007
+ const currentActive = await this.get(ref, { state: "active" });
1008
+ const result = await this.put(ref, draft.body, {
1009
+ parentVersion: currentActive?.hash ?? null,
1010
+ actor: opts.actor,
1011
+ source: opts.source ?? "sys-metadata-repo.publish",
1012
+ message: opts.message ?? `publish draft (hash ${draft.hash})`,
1013
+ intent: opts.intent ?? "override-artifact",
1014
+ state: "active",
1015
+ opType: "publish"
1016
+ });
1017
+ try {
1018
+ await this.delete(ref, {
1019
+ parentVersion: draft.hash,
1020
+ actor: opts.actor,
1021
+ source: opts.source ?? "sys-metadata-repo.publish",
1022
+ intent: opts.intent ?? "override-artifact",
1023
+ state: "draft"
1024
+ });
1025
+ } catch {
1026
+ }
1027
+ return result;
1028
+ }
1029
+ /**
1030
+ * Restore the body recorded in history at `targetVersion` (per-org
1031
+ * lineage counter) as the new active row. Writes a history event
1032
+ * with `operation_type='revert'` so the audit trail captures the
1033
+ * intent. Does NOT touch any draft row.
1034
+ *
1035
+ * Throws `[version_not_found]` (404) if the target version row is
1036
+ * missing or is a delete tombstone (no body to restore).
1037
+ */
1038
+ async restoreVersion(ref, targetVersion, opts) {
1039
+ this.assertOpen();
1040
+ const full = this.fullRef(ref);
1041
+ const row = await this.engine.findOne(this.historyTable, {
1042
+ where: {
1043
+ organization_id: this.organizationId,
1044
+ type: full.type,
1045
+ name: full.name,
1046
+ version: targetVersion
1047
+ }
1048
+ });
1049
+ if (!row) {
1050
+ const err = new Error(
1051
+ `[version_not_found] No history row at version ${targetVersion} for ${ref.type}/${ref.name}.`
1052
+ );
1053
+ err.code = "version_not_found";
1054
+ err.status = 404;
1055
+ throw err;
1056
+ }
1057
+ const raw = row.metadata;
1058
+ if (raw === null || raw === void 0) {
1059
+ const err = new Error(
1060
+ `[version_not_restorable] Version ${targetVersion} for ${ref.type}/${ref.name} is a delete tombstone \u2014 nothing to restore.`
1061
+ );
1062
+ err.code = "version_not_restorable";
1063
+ err.status = 409;
1064
+ throw err;
1065
+ }
1066
+ const body = typeof raw === "string" ? JSON.parse(raw) : raw;
1067
+ const currentActive = await this.get(ref, { state: "active" });
1068
+ return this.put(ref, body, {
1069
+ parentVersion: currentActive?.hash ?? null,
1070
+ actor: opts.actor,
1071
+ source: opts.source ?? "sys-metadata-repo.revert",
1072
+ message: opts.message ?? `revert to version ${targetVersion}`,
1073
+ intent: opts.intent ?? "override-artifact",
1074
+ state: "active",
1075
+ opType: "revert"
1076
+ });
1077
+ }
951
1078
  async *list(filter) {
952
1079
  this.assertOpen();
953
1080
  const where = {
@@ -1002,6 +1129,7 @@ var SysMetadataRepository = class {
1002
1129
  ref: full,
1003
1130
  hash: row.checksum ?? null,
1004
1131
  parentHash: row.previous_checksum ?? null,
1132
+ version: typeof row.version === "number" ? row.version : void 0,
1005
1133
  actor: row.recorded_by ?? "unknown",
1006
1134
  message: row.change_note ?? void 0,
1007
1135
  ts: row.recorded_at ?? (/* @__PURE__ */ new Date(0)).toISOString(),
@@ -1083,29 +1211,52 @@ var SysMetadataRepository = class {
1083
1211
  assertOpen() {
1084
1212
  if (this.closed) throw new Error("SysMetadataRepository is closed");
1085
1213
  }
1086
- assertAllowed(type) {
1214
+ /**
1215
+ * Defense-in-depth authorization gate.
1216
+ *
1217
+ * `intent` defaults to `'override-artifact'` (the historical strict
1218
+ * behavior). The protocol layer passes `'runtime-only'` after it has
1219
+ * verified — via the schema registry — that no artifact item exists
1220
+ * at `(type, name)`. In that case we accept types with
1221
+ * `allowRuntimeCreate: true`, even when `allowOrgOverride` is false.
1222
+ *
1223
+ * The env-var escape hatch (`OBJECTSTACK_METADATA_WRITABLE`) still
1224
+ * applies to BOTH intents, so operators can opt into artifact
1225
+ * overrides at runtime for emergency fixes.
1226
+ */
1227
+ assertAllowed(type, intent = "override-artifact") {
1087
1228
  const singular = import_shared.PLURAL_TO_SINGULAR[type] ?? type;
1088
1229
  const allowedByRegistry = OVERLAY_ALLOWED_TYPES.has(singular) || OVERLAY_ALLOWED_TYPES.has(type);
1089
1230
  if (allowedByRegistry) return;
1231
+ if (intent === "runtime-only") {
1232
+ if (RUNTIME_CREATE_ALLOWED_TYPES.has(singular) || RUNTIME_CREATE_ALLOWED_TYPES.has(type)) {
1233
+ return;
1234
+ }
1235
+ if (!STATIC_REGISTRY_TYPES.has(singular) && !STATIC_REGISTRY_TYPES.has(type)) {
1236
+ return;
1237
+ }
1238
+ }
1090
1239
  const env = envWritableMetadataTypes();
1091
1240
  if (env.has(singular) || env.has(type)) return;
1092
1241
  const allowed = [
1093
1242
  ...OVERLAY_ALLOWED_TYPES,
1094
1243
  ...envWritableMetadataTypes()
1095
1244
  ];
1245
+ const code = intent === "runtime-only" ? "not_creatable" : "not_overridable";
1246
+ const detail = intent === "runtime-only" ? `'${type}' has neither allowOrgOverride nor allowRuntimeCreate in the registry. ` : `'${type}' is not allowOrgOverride in the registry. `;
1096
1247
  const err = new Error(
1097
- `[not_overridable] '${type}' is not allowOrgOverride in the registry. Allowed: ${Array.from(new Set(allowed)).join(", ") || "(none)"}. Set OBJECTSTACK_METADATA_WRITABLE to enable additional types at runtime.`
1248
+ `[${code}] ${detail}Overlay-allowed: ${Array.from(new Set(allowed)).join(", ") || "(none)"}. Set OBJECTSTACK_METADATA_WRITABLE to enable additional types at runtime.`
1098
1249
  );
1099
- err.code = "not_overridable";
1250
+ err.code = code;
1100
1251
  err.status = 403;
1101
1252
  throw err;
1102
1253
  }
1103
- whereFor(ref) {
1254
+ whereFor(ref, state = "active") {
1104
1255
  return {
1105
1256
  type: ref.type,
1106
1257
  name: ref.name,
1107
1258
  organization_id: this.organizationId,
1108
- state: "active"
1259
+ state
1109
1260
  };
1110
1261
  }
1111
1262
  fullRef(ref) {
@@ -1202,57 +1353,58 @@ var SysMetadataRepository = class {
1202
1353
  // src/protocol.ts
1203
1354
  var import_metadata_core2 = require("@objectstack/metadata-core");
1204
1355
  var import_data2 = require("@objectstack/spec/data");
1205
- var import_shared2 = require("@objectstack/spec/shared");
1206
- var import_ui2 = require("@objectstack/spec/ui");
1207
- var import_identity = require("@objectstack/spec/identity");
1208
- var import_security = require("@objectstack/spec/security");
1356
+ var import_shared3 = require("@objectstack/spec/shared");
1209
1357
  var import_system = require("@objectstack/spec/system");
1210
- var import_ai = require("@objectstack/spec/ai");
1211
- var import_automation = require("@objectstack/spec/automation");
1212
- var import_kernel3 = require("@objectstack/spec/kernel");
1358
+ var import_kernel4 = require("@objectstack/spec/kernel");
1213
1359
  var import_zod = require("zod");
1214
- var TYPE_TO_SCHEMA = {
1215
- object: import_data2.ObjectSchema,
1216
- field: import_data2.FieldSchema,
1217
- dashboard: import_ui2.DashboardSchema,
1218
- app: import_ui2.AppSchema,
1219
- page: import_ui2.PageSchema,
1220
- report: import_ui2.ReportSchema,
1221
- action: import_ui2.ActionSchema,
1222
- role: import_identity.RoleSchema,
1223
- permission: import_security.PermissionSetSchema,
1224
- profile: import_security.PermissionSetSchema,
1225
- email_template: import_system.EmailTemplateSchema,
1226
- tool: import_ai.ToolSchema,
1227
- skill: import_ai.SkillSchema,
1228
- agent: import_ai.AgentSchema,
1229
- flow: import_automation.FlowSchema,
1230
- workflow: import_automation.WorkflowRuleSchema,
1231
- approval: import_automation.ApprovalProcessSchema,
1232
- job: import_system.JobSchema,
1233
- hook: import_data2.HookSchema
1234
- };
1235
- var TYPE_TO_FORM = {
1236
- object: import_data2.objectForm,
1237
- field: import_data2.fieldForm,
1238
- hook: import_data2.hookForm,
1239
- report: import_ui2.reportForm,
1240
- view: import_ui2.viewForm,
1241
- app: import_ui2.appForm,
1242
- dashboard: import_ui2.dashboardForm,
1243
- role: import_identity.roleForm,
1244
- action: import_ui2.actionForm,
1245
- page: import_ui2.pageForm,
1246
- agent: import_ai.agentForm,
1247
- tool: import_ai.toolForm,
1248
- skill: import_ai.skillForm,
1249
- flow: import_automation.flowForm,
1250
- workflow: import_automation.workflowForm,
1251
- approval: import_automation.approvalForm,
1252
- permission: import_security.permissionForm,
1253
- profile: import_security.permissionForm,
1254
- email_template: import_system.emailTemplateForm
1255
- };
1360
+
1361
+ // src/metadata-diagnostics.ts
1362
+ var import_kernel3 = require("@objectstack/spec/kernel");
1363
+ var import_shared2 = require("@objectstack/spec/shared");
1364
+ function computeMetadataDiagnostics(type, item) {
1365
+ const singular = import_shared2.PLURAL_TO_SINGULAR[type] ?? type;
1366
+ const schema = (0, import_kernel3.getMetadataTypeSchema)(singular);
1367
+ if (!schema) return void 0;
1368
+ if (item === null || item === void 0 || typeof item !== "object") {
1369
+ return {
1370
+ valid: false,
1371
+ errors: [{
1372
+ path: "",
1373
+ message: "Metadata document must be a non-null object",
1374
+ code: "invalid_type"
1375
+ }]
1376
+ };
1377
+ }
1378
+ const candidate = "_diagnostics" in item ? stripDiagnostics(item) : item;
1379
+ const parsed = schema.safeParse(candidate);
1380
+ if (parsed.success) {
1381
+ return { valid: true };
1382
+ }
1383
+ const errors = parsed.error.issues.map((issue) => ({
1384
+ path: issue.path.map(String).join("."),
1385
+ message: issue.message,
1386
+ code: issue.code
1387
+ }));
1388
+ return { valid: false, errors };
1389
+ }
1390
+ function stripDiagnostics(item) {
1391
+ const { _diagnostics: _drop, ...rest } = item;
1392
+ void _drop;
1393
+ return rest;
1394
+ }
1395
+ function decorateMetadataItem(type, item) {
1396
+ if (!item || typeof item !== "object") return item;
1397
+ const diagnostics = computeMetadataDiagnostics(type, item);
1398
+ if (!diagnostics) return item;
1399
+ return { ...item, _diagnostics: diagnostics };
1400
+ }
1401
+ function decorateMetadataItems(type, items) {
1402
+ if (!Array.isArray(items)) return items;
1403
+ return items.map((item) => decorateMetadataItem(type, item));
1404
+ }
1405
+
1406
+ // src/protocol.ts
1407
+ var TYPE_TO_FORM = import_system.METADATA_FORM_REGISTRY;
1256
1408
  var _jsonSchemaCache = /* @__PURE__ */ new WeakMap();
1257
1409
  function toJsonSchemaSafe(schema) {
1258
1410
  const cached = _jsonSchemaCache.get(schema);
@@ -1281,9 +1433,17 @@ var HAND_CRAFTED_SCHEMAS = {
1281
1433
  abstract: { type: "boolean", default: false },
1282
1434
  datasource: { type: "string" },
1283
1435
  fields: {
1284
- type: "array",
1285
- default: [],
1286
- items: {
1436
+ // Canonical Object.fields is a name-keyed map
1437
+ // (Record<string, FieldDefinition>) — insertion order is
1438
+ // display order. The SchemaForm engine recognises
1439
+ // `additionalProperties` as a Record and dispatches to
1440
+ // the `record` form-field renderer (ADR-0007). The form
1441
+ // layout in `object.form.ts` declares `type: 'record'`
1442
+ // so the inner `additionalProperties` schema is used to
1443
+ // shape each value.
1444
+ type: "object",
1445
+ default: {},
1446
+ additionalProperties: {
1287
1447
  type: "object",
1288
1448
  properties: {
1289
1449
  name: { type: "string" },
@@ -1294,7 +1454,7 @@ var HAND_CRAFTED_SCHEMAS = {
1294
1454
  defaultValue: {},
1295
1455
  description: { type: "string" }
1296
1456
  },
1297
- required: ["name", "type"]
1457
+ required: ["type"]
1298
1458
  }
1299
1459
  },
1300
1460
  capabilities: { type: "object", additionalProperties: true }
@@ -1439,19 +1599,9 @@ var HAND_CRAFTED_SCHEMAS = {
1439
1599
  additionalProperties: true
1440
1600
  }
1441
1601
  };
1442
- var FORM_VIEW_TYPES = /* @__PURE__ */ new Set(["simple", "tabbed", "wizard", "split", "drawer", "modal"]);
1443
- function resolveOverlaySchema(type, item) {
1444
- const singular = import_shared2.PLURAL_TO_SINGULAR[type] ?? type;
1445
- switch (singular) {
1446
- case "view": {
1447
- const t = item && typeof item === "object" && "type" in item ? String(item.type) : void 0;
1448
- return t && FORM_VIEW_TYPES.has(t) ? import_ui2.FormViewSchema : import_ui2.ListViewSchema;
1449
- }
1450
- case "dashboard":
1451
- return import_ui2.DashboardSchema;
1452
- default:
1453
- return null;
1454
- }
1602
+ function resolveOverlaySchema(type, _item) {
1603
+ const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
1604
+ return (0, import_kernel4.getMetadataTypeSchema)(singular) ?? null;
1455
1605
  }
1456
1606
  function simpleHash(str) {
1457
1607
  let hash = 0;
@@ -1584,6 +1734,32 @@ function extractPathValues(item, path) {
1584
1734
  }
1585
1735
  return out;
1586
1736
  }
1737
+ function diffShallow(from, to) {
1738
+ const added = [];
1739
+ const removed = [];
1740
+ const changed = [];
1741
+ const fromKeys = new Set(Object.keys(from ?? {}));
1742
+ const toKeys = new Set(Object.keys(to ?? {}));
1743
+ for (const k of toKeys) {
1744
+ if (!fromKeys.has(k)) {
1745
+ added.push({ path: k, value: to[k] });
1746
+ } else {
1747
+ const a = from[k];
1748
+ const b = to[k];
1749
+ const aStr = JSON.stringify(a);
1750
+ const bStr = JSON.stringify(b);
1751
+ if (aStr !== bStr) {
1752
+ changed.push({ path: k, from: a, to: b });
1753
+ }
1754
+ }
1755
+ }
1756
+ for (const k of fromKeys) {
1757
+ if (!toKeys.has(k)) {
1758
+ removed.push({ path: k, value: from[k] });
1759
+ }
1760
+ }
1761
+ return { added, removed, changed };
1762
+ }
1587
1763
  function detectDestructiveObjectChanges(prev, next) {
1588
1764
  if (!prev || typeof prev !== "object" || !next || typeof next !== "object") return [];
1589
1765
  const prevFields = prev.fields && typeof prev.fields === "object" ? prev.fields : {};
@@ -1715,6 +1891,20 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1715
1891
  }
1716
1892
  }
1717
1893
  }
1894
+ const draftPartialSql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_sys_metadata_overlay_draft ON sys_metadata (type, name, organization_id) WHERE state = 'draft'";
1895
+ try {
1896
+ await exec(draftPartialSql);
1897
+ } catch (err) {
1898
+ const msg = err instanceof Error ? err.message : String(err);
1899
+ if (/partial|where clause|syntax/i.test(msg)) {
1900
+ try {
1901
+ await exec(
1902
+ "CREATE INDEX IF NOT EXISTS idx_sys_metadata_overlay_draft ON sys_metadata (type, name, organization_id)"
1903
+ );
1904
+ } catch {
1905
+ }
1906
+ }
1907
+ }
1718
1908
  } catch {
1719
1909
  }
1720
1910
  }
@@ -1835,11 +2025,11 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1835
2025
  const allTypes = Array.from(/* @__PURE__ */ new Set([...schemaTypes, ...runtimeTypes]));
1836
2026
  const writableOverrides = _ObjectStackProtocolImplementation.envWritableTypes();
1837
2027
  const registryByType = new Map(
1838
- import_kernel3.DEFAULT_METADATA_TYPE_REGISTRY.map((e) => [e.type, e])
2028
+ import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY.map((e) => [e.type, e])
1839
2029
  );
1840
2030
  const entries = allTypes.map((type) => {
1841
- const singular = import_shared2.PLURAL_TO_SINGULAR[type] ?? type;
1842
- const zodSchema = singular === "view" ? import_ui2.ListViewSchema : TYPE_TO_SCHEMA[singular];
2031
+ const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
2032
+ const zodSchema = (0, import_kernel4.getMetadataTypeSchema)(singular);
1843
2033
  const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : void 0) ?? HAND_CRAFTED_SCHEMAS[singular];
1844
2034
  const form = TYPE_TO_FORM[singular];
1845
2035
  const base = registryByType.get(singular);
@@ -1880,19 +2070,76 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1880
2070
  });
1881
2071
  return { types: allTypes, entries };
1882
2072
  }
2073
+ /**
2074
+ * Sweep all (or filtered) metadata types and report entries that
2075
+ * fail spec validation. Powers the Studio governance view
2076
+ * (`GET /api/v1/meta/diagnostics`) and `os doctor`-style CLI
2077
+ * checks.
2078
+ *
2079
+ * `severity` defaults to `'error'` — only entries with at least
2080
+ * one Zod error issue are returned. `'warning'` includes
2081
+ * everything we surface (warnings are reserved for a future lint
2082
+ * layer on top of spec validation).
2083
+ *
2084
+ * `type` may be either a singular (`'view'`) or plural (`'views'`)
2085
+ * identifier; the underlying `getMetaItems` already normalises.
2086
+ *
2087
+ * Implementation note: leverages the `_diagnostics` already
2088
+ * decorated onto items by `getMetaItems()` to avoid running
2089
+ * `safeParse()` twice. For types whose schema is unregistered we
2090
+ * skip silently (they cannot be validated and should not appear
2091
+ * as "valid" either — they are simply opaque to this report).
2092
+ */
2093
+ async getMetaDiagnostics(request = {}) {
2094
+ const includeWarnings = request.severity === "warning";
2095
+ const targetTypes = request.type ? [request.type] : import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => (0, import_kernel4.getMetadataTypeSchema)(e.type)).map((e) => e.type);
2096
+ const entries = [];
2097
+ let scannedItems = 0;
2098
+ for (const t of targetTypes) {
2099
+ let listed;
2100
+ try {
2101
+ listed = await this.getMetaItems({
2102
+ type: t,
2103
+ organizationId: request.organizationId,
2104
+ packageId: request.packageId
2105
+ });
2106
+ } catch {
2107
+ continue;
2108
+ }
2109
+ const items = Array.isArray(listed?.items) ? listed.items : Array.isArray(listed) ? listed : [];
2110
+ for (const item of items) {
2111
+ scannedItems += 1;
2112
+ const diag = item?._diagnostics ?? computeMetadataDiagnostics(t, item);
2113
+ if (!diag) continue;
2114
+ if (diag.valid && !includeWarnings) continue;
2115
+ if (diag.valid && includeWarnings && !diag.warnings?.length) continue;
2116
+ entries.push({
2117
+ type: t,
2118
+ name: typeof item?.name === "string" ? item.name : "<unknown>",
2119
+ diagnostics: diag
2120
+ });
2121
+ }
2122
+ }
2123
+ return {
2124
+ entries,
2125
+ total: entries.length,
2126
+ scannedTypes: targetTypes.length,
2127
+ scannedItems
2128
+ };
2129
+ }
1883
2130
  async getMetaItems(request) {
1884
2131
  const { packageId } = request;
1885
2132
  let items = [];
1886
2133
  if (this.environmentId === void 0) {
1887
2134
  items = [...this.engine.registry.listItems(request.type, packageId)];
1888
2135
  if (items.length === 0) {
1889
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2136
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
1890
2137
  if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
1891
2138
  }
1892
2139
  } else {
1893
2140
  items = [...this.engine.registry.listItems(request.type, packageId)];
1894
2141
  if (items.length === 0) {
1895
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2142
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
1896
2143
  if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
1897
2144
  }
1898
2145
  }
@@ -1907,7 +2154,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1907
2154
  if (packageId) whereClause._packageId = packageId;
1908
2155
  let rs = await this.engine.find("sys_metadata", { where: whereClause });
1909
2156
  if (!rs || rs.length === 0) {
1910
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2157
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
1911
2158
  if (alt) {
1912
2159
  const altWhere = { type: alt, state: "active", organization_id: oid };
1913
2160
  if (packageId) altWhere._packageId = packageId;
@@ -1974,28 +2221,29 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1974
2221
  }
1975
2222
  return {
1976
2223
  type: request.type,
1977
- items
2224
+ items: decorateMetadataItems(request.type, items)
1978
2225
  };
1979
2226
  }
1980
2227
  async getMetaItem(request) {
1981
2228
  let item;
1982
2229
  const orgId = request.organizationId;
2230
+ const readState = request.state === "draft" ? "draft" : "active";
1983
2231
  try {
1984
2232
  const findOverlay = async (oid) => {
1985
2233
  const where = {
1986
2234
  type: request.type,
1987
2235
  name: request.name,
1988
- state: "active",
2236
+ state: readState,
1989
2237
  organization_id: oid
1990
2238
  };
1991
2239
  const rec = await this.engine.findOne("sys_metadata", { where });
1992
2240
  if (rec) return rec;
1993
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2241
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
1994
2242
  if (alt) {
1995
2243
  const altWhere = {
1996
2244
  type: alt,
1997
2245
  name: request.name,
1998
- state: "active",
2246
+ state: readState,
1999
2247
  organization_id: oid
2000
2248
  };
2001
2249
  return await this.engine.findOne("sys_metadata", { where: altWhere });
@@ -2008,6 +2256,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2008
2256
  }
2009
2257
  } catch {
2010
2258
  }
2259
+ if (readState === "draft") {
2260
+ if (item === void 0) {
2261
+ const err = new Error(
2262
+ `[no_draft] No pending draft exists for ${request.type}/${request.name}.`
2263
+ );
2264
+ err.code = "no_draft";
2265
+ err.status = 404;
2266
+ throw err;
2267
+ }
2268
+ return { type: request.type, name: request.name, item: decorateMetadataItem(request.type, item) };
2269
+ }
2011
2270
  if (item === void 0) {
2012
2271
  try {
2013
2272
  const services = this.getServicesRegistry?.();
@@ -2017,7 +2276,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2017
2276
  if (fromService !== void 0 && fromService !== null) {
2018
2277
  item = fromService;
2019
2278
  } else {
2020
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2279
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
2021
2280
  if (alt) {
2022
2281
  const altFromService = await metadataService.get(alt, request.name);
2023
2282
  if (altFromService !== void 0 && altFromService !== null) {
@@ -2032,14 +2291,14 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2032
2291
  if (item === void 0) {
2033
2292
  item = this.engine.registry.getItem(request.type, request.name);
2034
2293
  if (item === void 0) {
2035
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2294
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
2036
2295
  if (alt) item = this.engine.registry.getItem(alt, request.name);
2037
2296
  }
2038
2297
  }
2039
2298
  return {
2040
2299
  type: request.type,
2041
2300
  name: request.name,
2042
- item
2301
+ item: decorateMetadataItem(request.type, item)
2043
2302
  };
2044
2303
  }
2045
2304
  /**
@@ -2065,7 +2324,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2065
2324
  if (metadataService && typeof metadataService.get === "function") {
2066
2325
  let fromService = await metadataService.get(request.type, request.name);
2067
2326
  if (fromService === void 0 || fromService === null) {
2068
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2327
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
2069
2328
  if (alt) fromService = await metadataService.get(alt, request.name);
2070
2329
  }
2071
2330
  if (fromService !== void 0 && fromService !== null) code = fromService;
@@ -2075,7 +2334,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2075
2334
  if (code === null) {
2076
2335
  let regItem = this.engine.registry.getItem(request.type, request.name);
2077
2336
  if (regItem === void 0) {
2078
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2337
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
2079
2338
  if (alt) regItem = this.engine.registry.getItem(alt, request.name);
2080
2339
  }
2081
2340
  if (regItem !== void 0) code = regItem;
@@ -2092,7 +2351,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2092
2351
  };
2093
2352
  let rec = await this.engine.findOne("sys_metadata", { where });
2094
2353
  if (!rec) {
2095
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2354
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
2096
2355
  if (alt) {
2097
2356
  rec = await this.engine.findOne("sys_metadata", {
2098
2357
  where: { ...where, type: alt }
@@ -2118,13 +2377,15 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2118
2377
  } catch {
2119
2378
  }
2120
2379
  const effective = overlay ?? code;
2380
+ const _diagnostics = effective !== null && effective !== void 0 ? computeMetadataDiagnostics(request.type, effective) : void 0;
2121
2381
  return {
2122
2382
  type: request.type,
2123
2383
  name: request.name,
2124
2384
  code,
2125
2385
  overlay,
2126
2386
  overlayScope,
2127
- effective
2387
+ effective,
2388
+ ..._diagnostics ? { _diagnostics } : {}
2128
2389
  };
2129
2390
  }
2130
2391
  async getUiView(request) {
@@ -3005,9 +3266,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3005
3266
  for (const tok of raw.split(",")) {
3006
3267
  const t = tok.trim();
3007
3268
  if (!t) continue;
3008
- const singular = import_shared2.PLURAL_TO_SINGULAR[t] ?? t;
3269
+ const singular = import_shared3.PLURAL_TO_SINGULAR[t] ?? t;
3009
3270
  set.add(singular);
3010
- const plural = import_shared2.SINGULAR_TO_PLURAL[singular];
3271
+ const plural = import_shared3.SINGULAR_TO_PLURAL[singular];
3011
3272
  if (plural) set.add(plural);
3012
3273
  }
3013
3274
  this._envWritableTypes = set;
@@ -3019,13 +3280,49 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3019
3280
  }
3020
3281
  /** Normalize plural→singular before consulting the allow-list. */
3021
3282
  static isOverlayAllowed(type) {
3022
- const singular = import_shared2.PLURAL_TO_SINGULAR[type] ?? type;
3283
+ const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
3023
3284
  if (this.OVERLAY_ALLOWED_TYPES.has(singular) || this.OVERLAY_ALLOWED_TYPES.has(type)) {
3024
3285
  return true;
3025
3286
  }
3026
3287
  const env = this.envWritableTypes();
3027
3288
  return env.has(singular) || env.has(type);
3028
3289
  }
3290
+ /** Does this type permit creating brand-new (artifact-free) items? */
3291
+ static isRuntimeCreateAllowed(type) {
3292
+ const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
3293
+ if (this.RUNTIME_CREATE_ALLOWED_TYPES.has(singular) || this.RUNTIME_CREATE_ALLOWED_TYPES.has(type)) {
3294
+ return true;
3295
+ }
3296
+ if (!this.STATIC_REGISTRY_TYPES.has(singular) && !this.STATIC_REGISTRY_TYPES.has(type)) {
3297
+ return true;
3298
+ }
3299
+ return false;
3300
+ }
3301
+ /**
3302
+ * Does an artifact (npm-package-loaded) item exist at `(type, name)`?
3303
+ *
3304
+ * The schema registry's `_packageId` tag is set only when
3305
+ * `registerItem(..., packageId)` is called with a truthy packageId
3306
+ * — and only artifact loaders do that. DB-rehydrated items
3307
+ * (sys_metadata rows registered back into the registry by
3308
+ * `getMetaItems` / `loadMetaFromDb`) call `registerItem` without a
3309
+ * packageId, so they carry no `_packageId` and are correctly
3310
+ * excluded here.
3311
+ *
3312
+ * Used by the two-tier authorization model to distinguish
3313
+ * "overlaying a packaged item" (requires `allowOrgOverride`) from
3314
+ * "authoring a DB-only item" (requires only `allowRuntimeCreate`).
3315
+ */
3316
+ isArtifactBacked(type, name) {
3317
+ const registry = this.engine?.registry;
3318
+ if (!registry || typeof registry.getItem !== "function") {
3319
+ return false;
3320
+ }
3321
+ const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
3322
+ const item = registry.getItem(singular, name) ?? registry.getItem(type, name);
3323
+ if (!item || !item._packageId) return false;
3324
+ return item._packageId !== "sys_metadata";
3325
+ }
3029
3326
  /**
3030
3327
  * Mirror an object-type overlay write into the in-memory engine
3031
3328
  * registry so subsequent CRUD finds the new schema. Idempotent and
@@ -3051,16 +3348,29 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3051
3348
  if (!request.item) {
3052
3349
  throw new Error("Item data is required");
3053
3350
  }
3054
- if (this.environmentId !== void 0 && !_ObjectStackProtocolImplementation.isOverlayAllowed(request.type)) {
3055
- const allowed = Array.from(_ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES).join(", ");
3056
- const err = new Error(
3057
- `[not_overridable] Metadata type '${request.type}' has not opted into per-org overlay writes. Set allowOrgOverride: true on its DEFAULT_METADATA_TYPE_REGISTRY entry to enable, or set the OBJECTSTACK_METADATA_WRITABLE env var (comma-separated singular type names) to opt in at runtime. Currently allowed: ${allowed}. See docs/adr/0005-metadata-customization-overlay.md.`
3058
- );
3059
- err.code = "not_overridable";
3060
- err.status = 403;
3061
- throw err;
3351
+ const mode = request.mode === "draft" ? "draft" : "publish";
3352
+ if (this.environmentId !== void 0) {
3353
+ const overlayAllowed = _ObjectStackProtocolImplementation.isOverlayAllowed(request.type);
3354
+ const runtimeCreateAllowed = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(request.type);
3355
+ const artifactBacked = this.isArtifactBacked(request.type, request.name);
3356
+ if (artifactBacked && !overlayAllowed) {
3357
+ const err = new Error(
3358
+ `[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 OBJECTSTACK_METADATA_WRITABLE to grant a runtime escape hatch. See docs/adr/0005-metadata-customization-overlay.md.`
3359
+ );
3360
+ err.code = "not_overridable";
3361
+ err.status = 403;
3362
+ throw err;
3363
+ }
3364
+ if (!artifactBacked && !overlayAllowed && !runtimeCreateAllowed) {
3365
+ const err = new Error(
3366
+ `[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.`
3367
+ );
3368
+ err.code = "not_creatable";
3369
+ err.status = 403;
3370
+ throw err;
3371
+ }
3062
3372
  }
3063
- const singularType = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3373
+ const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3064
3374
  if (!request.force && (singularType === "object" || singularType === "field")) {
3065
3375
  try {
3066
3376
  const existing = await this.getMetaItem({
@@ -3108,8 +3418,13 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3108
3418
  }
3109
3419
  }
3110
3420
  await this.ensureOverlayIndex();
3111
- const singularTypeForRepo = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3112
- if (_ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo)) {
3421
+ const singularTypeForRepo = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3422
+ const overlayAllowedForRepo = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
3423
+ const runtimeCreateAllowedForRepo = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularTypeForRepo);
3424
+ const useRepoPath = overlayAllowedForRepo || runtimeCreateAllowedForRepo;
3425
+ if (useRepoPath) {
3426
+ const artifactBacked = this.isArtifactBacked(singularTypeForRepo, request.name);
3427
+ const intent = artifactBacked ? "override-artifact" : "runtime-only";
3113
3428
  const orgId = request.organizationId ?? null;
3114
3429
  const repo = this.getOverlayRepo(orgId);
3115
3430
  const ref = {
@@ -3121,21 +3436,26 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3121
3436
  if (request.parentVersion !== void 0) {
3122
3437
  parentVersion = request.parentVersion;
3123
3438
  } else {
3124
- const current = await repo.get(ref);
3439
+ const current = await repo.get(ref, { state: mode === "draft" ? "draft" : "active" });
3125
3440
  parentVersion = current?.hash ?? null;
3126
3441
  }
3127
3442
  try {
3128
3443
  const result = await repo.put(ref, request.item, {
3129
3444
  parentVersion,
3130
3445
  actor: request.actor ?? "system",
3131
- source: "protocol.saveMetaItem"
3446
+ source: "protocol.saveMetaItem",
3447
+ intent,
3448
+ state: mode === "draft" ? "draft" : "active"
3132
3449
  });
3133
- this.applyObjectRegistryMutation(request);
3450
+ if (mode === "publish") {
3451
+ this.applyObjectRegistryMutation(request);
3452
+ }
3134
3453
  return {
3135
3454
  success: true,
3136
3455
  version: result.version,
3137
3456
  seq: result.seq,
3138
- message: orgId ? `Saved customization overlay (org=${orgId}) \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]` : `Saved customization overlay (env-wide) \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
3457
+ state: mode === "draft" ? "draft" : "active",
3458
+ 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}]`
3139
3459
  };
3140
3460
  } catch (err) {
3141
3461
  if (err instanceof import_metadata_core2.ConflictError) {
@@ -3219,8 +3539,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3219
3539
  * "no history" uniformly.
3220
3540
  */
3221
3541
  async historyMetaItem(request) {
3222
- const singularType = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3223
- if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType)) {
3542
+ const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3543
+ if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
3224
3544
  return { events: [] };
3225
3545
  }
3226
3546
  const orgId = request.organizationId ?? null;
@@ -3238,22 +3558,238 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3238
3558
  return { events };
3239
3559
  }
3240
3560
  /**
3241
- * Remove a customization overlay row for the given metadata item, so the
3242
- * next read falls through to the artifact-loaded default. Implements the
3243
- * "Reset to factory default" semantic from ADR-0005. Whitelist is shared
3244
- * with {@link saveMetaItem}.
3561
+ * Promote the pending draft overlay to the live (`active`) row.
3562
+ * Records a history event with `op='publish'`. 404 (`[no_draft]`)
3563
+ * when there is nothing to publish.
3245
3564
  */
3246
- async deleteMetaItem(request) {
3247
- if (this.environmentId !== void 0 && !_ObjectStackProtocolImplementation.isOverlayAllowed(request.type)) {
3565
+ async publishMetaItem(request) {
3566
+ const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3567
+ if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
3248
3568
  const err = new Error(
3249
- `[not_overridable] Metadata type '${request.type}' has not opted into per-org overlay writes. See docs/adr/0005-metadata-customization-overlay.md.`
3569
+ `[not_overridable] Metadata type '${request.type}' is not draftable \u2014 no overlay/runtime-create permission.`
3250
3570
  );
3251
3571
  err.code = "not_overridable";
3252
3572
  err.status = 403;
3253
3573
  throw err;
3254
3574
  }
3255
- const singularTypeForRepo = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3256
- const useRepoPath = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
3575
+ await this.ensureOverlayIndex();
3576
+ const orgId = request.organizationId ?? null;
3577
+ const repo = this.getOverlayRepo(orgId);
3578
+ const artifactBacked = this.isArtifactBacked(singularType, request.name);
3579
+ const intent = artifactBacked ? "override-artifact" : "runtime-only";
3580
+ const ref = {
3581
+ type: singularType,
3582
+ name: request.name,
3583
+ org: orgId ?? "env"
3584
+ };
3585
+ try {
3586
+ const result = await repo.promoteDraft(ref, {
3587
+ actor: request.actor ?? "system",
3588
+ source: "protocol.publishMetaItem",
3589
+ ...request.message ? { message: request.message } : {},
3590
+ intent
3591
+ });
3592
+ this.applyObjectRegistryMutation({
3593
+ type: request.type,
3594
+ name: request.name,
3595
+ item: result.item.body
3596
+ });
3597
+ return {
3598
+ success: true,
3599
+ version: result.version,
3600
+ seq: result.seq,
3601
+ message: `Published draft \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
3602
+ };
3603
+ } catch (err) {
3604
+ if (err instanceof import_metadata_core2.ConflictError) {
3605
+ const conflict = new Error(
3606
+ `[metadata_conflict] ${request.type}/${request.name} published row advanced while you held the draft. Expected parent ${err.expectedParent ?? "null"} but current is ${err.actualHead ?? "null"}.`
3607
+ );
3608
+ conflict.code = "metadata_conflict";
3609
+ conflict.status = 409;
3610
+ conflict.expectedParent = err.expectedParent;
3611
+ conflict.actualHead = err.actualHead;
3612
+ throw conflict;
3613
+ }
3614
+ throw err;
3615
+ }
3616
+ }
3617
+ /**
3618
+ * Restore the body recorded at history `toVersion` as the new
3619
+ * live row. Writes a history event with `op='revert'`. 404
3620
+ * (`[version_not_found]`) when the target version doesn't exist;
3621
+ * 409 (`[version_not_restorable]`) when the target is a delete
3622
+ * tombstone (no body to bring back).
3623
+ */
3624
+ async rollbackMetaItem(request) {
3625
+ if (!Number.isFinite(request.toVersion) || request.toVersion < 1) {
3626
+ const err = new Error(
3627
+ `[invalid_request] rollbackMetaItem requires a positive integer 'toVersion' (got ${request.toVersion}).`
3628
+ );
3629
+ err.code = "invalid_request";
3630
+ err.status = 400;
3631
+ throw err;
3632
+ }
3633
+ const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3634
+ if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
3635
+ const err = new Error(
3636
+ `[not_overridable] Metadata type '${request.type}' is not revertable \u2014 no overlay/runtime-create permission.`
3637
+ );
3638
+ err.code = "not_overridable";
3639
+ err.status = 403;
3640
+ throw err;
3641
+ }
3642
+ await this.ensureOverlayIndex();
3643
+ const orgId = request.organizationId ?? null;
3644
+ const repo = this.getOverlayRepo(orgId);
3645
+ const artifactBacked = this.isArtifactBacked(singularType, request.name);
3646
+ const intent = artifactBacked ? "override-artifact" : "runtime-only";
3647
+ const ref = {
3648
+ type: singularType,
3649
+ name: request.name,
3650
+ org: orgId ?? "env"
3651
+ };
3652
+ try {
3653
+ const result = await repo.restoreVersion(ref, request.toVersion, {
3654
+ actor: request.actor ?? "system",
3655
+ source: "protocol.rollbackMetaItem",
3656
+ ...request.message ? { message: request.message } : {},
3657
+ intent
3658
+ });
3659
+ this.applyObjectRegistryMutation({
3660
+ type: request.type,
3661
+ name: request.name,
3662
+ item: result.item.body
3663
+ });
3664
+ return {
3665
+ success: true,
3666
+ version: result.version,
3667
+ seq: result.seq,
3668
+ restoredFromVersion: request.toVersion,
3669
+ message: `Reverted to version ${request.toVersion} \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
3670
+ };
3671
+ } catch (err) {
3672
+ if (err instanceof import_metadata_core2.ConflictError) {
3673
+ const conflict = new Error(
3674
+ `[metadata_conflict] ${request.type}/${request.name} advanced during rollback. Expected parent ${err.expectedParent ?? "null"} but current is ${err.actualHead ?? "null"}.`
3675
+ );
3676
+ conflict.code = "metadata_conflict";
3677
+ conflict.status = 409;
3678
+ conflict.expectedParent = err.expectedParent;
3679
+ conflict.actualHead = err.actualHead;
3680
+ throw conflict;
3681
+ }
3682
+ throw err;
3683
+ }
3684
+ }
3685
+ /**
3686
+ * Compute a shallow structural diff between two historical
3687
+ * versions of a metadata item. Either side may be omitted: when
3688
+ * `toVersion` is undefined the current active body is used; when
3689
+ * `fromVersion` is undefined the immediately previous history row
3690
+ * is used. Returns `{ added, removed, changed }` keyed by JSON
3691
+ * pointer-style paths for primitive leaves; nested objects/arrays
3692
+ * are reported as a single change record.
3693
+ */
3694
+ async diffMetaItem(request) {
3695
+ const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3696
+ const orgId = request.organizationId ?? null;
3697
+ const events = (await this.historyMetaItem({
3698
+ type: singularType,
3699
+ name: request.name,
3700
+ ...orgId ? { organizationId: orgId } : {}
3701
+ })).events;
3702
+ const versions = events.map((ev) => ev.version).filter((v) => typeof v === "number");
3703
+ const repo = this.getOverlayRepo(orgId);
3704
+ const fullRef = {
3705
+ type: singularType,
3706
+ name: request.name,
3707
+ org: orgId ?? "env"
3708
+ };
3709
+ const histRows = [];
3710
+ try {
3711
+ const engineAny = this.engine;
3712
+ const rows = await engineAny.find("sys_metadata_history", {
3713
+ where: {
3714
+ organization_id: orgId,
3715
+ type: singularType,
3716
+ name: request.name
3717
+ }
3718
+ });
3719
+ rows.sort((a, b) => (a.version ?? 0) - (b.version ?? 0));
3720
+ for (const r of rows) {
3721
+ const body = r.metadata == null ? null : typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata;
3722
+ histRows.push({ version: r.version ?? 0, body });
3723
+ }
3724
+ } catch {
3725
+ }
3726
+ const byVersion = /* @__PURE__ */ new Map();
3727
+ for (const r of histRows) byVersion.set(r.version, r.body);
3728
+ let fromBody = null;
3729
+ let toBody = null;
3730
+ let fromVersion = null;
3731
+ let toVersion = null;
3732
+ if (request.toVersion !== void 0) {
3733
+ toVersion = request.toVersion;
3734
+ toBody = byVersion.get(request.toVersion) ?? null;
3735
+ } else {
3736
+ const current = await repo.get(fullRef, { state: "active" });
3737
+ toBody = current ? current.body : null;
3738
+ toVersion = histRows.length ? histRows[histRows.length - 1].version : null;
3739
+ }
3740
+ if (request.fromVersion !== void 0) {
3741
+ fromVersion = request.fromVersion;
3742
+ fromBody = byVersion.get(request.fromVersion) ?? null;
3743
+ } else if (toVersion !== null) {
3744
+ const sorted = histRows.map((r) => r.version).filter((v) => v < toVersion);
3745
+ if (sorted.length) {
3746
+ fromVersion = sorted[sorted.length - 1];
3747
+ fromBody = byVersion.get(fromVersion) ?? null;
3748
+ }
3749
+ }
3750
+ const diff = diffShallow(fromBody ?? {}, toBody ?? {});
3751
+ const _used = versions;
3752
+ void _used;
3753
+ return {
3754
+ type: request.type,
3755
+ name: request.name,
3756
+ fromVersion,
3757
+ toVersion,
3758
+ ...diff
3759
+ };
3760
+ }
3761
+ /**
3762
+ * Remove a customization overlay row for the given metadata item, so the
3763
+ * next read falls through to the artifact-loaded default. Implements the
3764
+ * "Reset to factory default" semantic from ADR-0005. Whitelist is shared
3765
+ * with {@link saveMetaItem}.
3766
+ */
3767
+ async deleteMetaItem(request) {
3768
+ if (this.environmentId !== void 0) {
3769
+ const overlayAllowed = _ObjectStackProtocolImplementation.isOverlayAllowed(request.type);
3770
+ const runtimeCreateAllowed = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(request.type);
3771
+ const artifactBacked = this.isArtifactBacked(request.type, request.name);
3772
+ if (artifactBacked && !overlayAllowed) {
3773
+ const err = new Error(
3774
+ `[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.`
3775
+ );
3776
+ err.code = "not_overridable";
3777
+ err.status = 403;
3778
+ throw err;
3779
+ }
3780
+ if (!artifactBacked && !overlayAllowed && !runtimeCreateAllowed) {
3781
+ const err = new Error(
3782
+ `[not_creatable] Metadata type '${request.type}' does not allow runtime creation or deletion.`
3783
+ );
3784
+ err.code = "not_creatable";
3785
+ err.status = 403;
3786
+ throw err;
3787
+ }
3788
+ }
3789
+ const singularTypeForRepo = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3790
+ const overlayAllowedForRepoDel = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
3791
+ const runtimeCreateAllowedForRepoDel = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularTypeForRepo);
3792
+ const useRepoPath = overlayAllowedForRepoDel || runtimeCreateAllowedForRepoDel;
3257
3793
  if (useRepoPath) {
3258
3794
  const orgId = request.organizationId ?? null;
3259
3795
  const repo = this.getOverlayRepo(orgId);
@@ -3263,19 +3799,22 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3263
3799
  org: orgId ?? "env"
3264
3800
  };
3265
3801
  try {
3266
- const current = await repo.get(ref);
3802
+ const targetState = request.state === "draft" ? "draft" : "active";
3803
+ const current = await repo.get(ref, { state: targetState });
3267
3804
  if (!current) {
3268
3805
  return {
3269
3806
  success: true,
3270
3807
  reset: false,
3271
- message: `No customization overlay found for ${request.type}/${request.name} \u2014 already at artifact default.`
3808
+ 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.`
3272
3809
  };
3273
3810
  }
3274
3811
  const parentVersion = request.parentVersion !== void 0 ? request.parentVersion ?? current.hash : current.hash;
3275
3812
  const result = await repo.delete(ref, {
3276
3813
  parentVersion,
3277
3814
  actor: request.actor ?? "system",
3278
- source: "protocol.deleteMetaItem"
3815
+ source: "protocol.deleteMetaItem",
3816
+ intent: this.isArtifactBacked(singularTypeForRepo, request.name) ? "override-artifact" : "runtime-only",
3817
+ state: targetState
3279
3818
  });
3280
3819
  if (this.environmentId === void 0) {
3281
3820
  try {
@@ -3294,7 +3833,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3294
3833
  success: true,
3295
3834
  reset: true,
3296
3835
  seq: result.seq,
3297
- message: `Customization overlay deleted \u2014 ${request.type}/${request.name} reset to artifact default. [seq=${result.seq}]`
3836
+ 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}]`
3298
3837
  };
3299
3838
  } catch (err) {
3300
3839
  if (err instanceof import_metadata_core2.ConflictError) {
@@ -3372,7 +3911,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3372
3911
  for (const record of records) {
3373
3912
  try {
3374
3913
  const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
3375
- const normalizedType = import_shared2.PLURAL_TO_SINGULAR[record.type] ?? record.type;
3914
+ const normalizedType = import_shared3.PLURAL_TO_SINGULAR[record.type] ?? record.type;
3376
3915
  if (normalizedType === "object") {
3377
3916
  this.engine.registry.registerObject(data, record.packageId || "sys_metadata");
3378
3917
  } else {
@@ -3406,7 +3945,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3406
3945
  * — the engine never throws.
3407
3946
  */
3408
3947
  async findReferencesToMeta(request) {
3409
- const singularTarget = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3948
+ const singularTarget = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3410
3949
  const targetName = request.name;
3411
3950
  const matchers = REFERENCE_PATHS[singularTarget];
3412
3951
  if (!matchers || matchers.length === 0) {
@@ -3597,10 +4136,10 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3597
4136
  */
3598
4137
  _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
3599
4138
  const out = /* @__PURE__ */ new Set();
3600
- for (const entry of import_kernel3.DEFAULT_METADATA_TYPE_REGISTRY) {
4139
+ for (const entry of import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY) {
3601
4140
  if (!entry.allowOrgOverride) continue;
3602
4141
  out.add(entry.type);
3603
- const plural = import_shared2.SINGULAR_TO_PLURAL[entry.type];
4142
+ const plural = import_shared3.SINGULAR_TO_PLURAL[entry.type];
3604
4143
  if (plural) out.add(plural);
3605
4144
  }
3606
4145
  return out;
@@ -3617,13 +4156,48 @@ _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
3617
4156
  * {@link ObjectStackProtocolImplementation.resetEnvWritableCache}.
3618
4157
  */
3619
4158
  _ObjectStackProtocolImplementation._envWritableTypes = null;
4159
+ /**
4160
+ * Types that opt into runtime creation of brand-new items (ADR-0005
4161
+ * extension — two-tier model). A type may have
4162
+ * `allowOrgOverride: false` (cannot overlay artifact-shipped items)
4163
+ * yet still set `allowRuntimeCreate: true` (users can author new
4164
+ * items in `sys_metadata`). The two flags are orthogonal; see
4165
+ * {@link isArtifactBacked} for how the protocol decides which gate
4166
+ * applies to a given save/delete.
4167
+ */
4168
+ /**
4169
+ * Set of type names that have a static entry in
4170
+ * `DEFAULT_METADATA_TYPE_REGISTRY`. Anything outside this set is
4171
+ * runtime-registered (plugin-provided types like `theme`, `api`,
4172
+ * `connector`) — the listing endpoint at `getMetaTypes()` synthesises
4173
+ * those with `allowRuntimeCreate: true`, so this gate must agree.
4174
+ */
4175
+ _ObjectStackProtocolImplementation.STATIC_REGISTRY_TYPES = (() => {
4176
+ const out = /* @__PURE__ */ new Set();
4177
+ for (const entry of import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY) {
4178
+ out.add(entry.type);
4179
+ const plural = import_shared3.SINGULAR_TO_PLURAL[entry.type];
4180
+ if (plural) out.add(plural);
4181
+ }
4182
+ return out;
4183
+ })();
4184
+ _ObjectStackProtocolImplementation.RUNTIME_CREATE_ALLOWED_TYPES = (() => {
4185
+ const out = /* @__PURE__ */ new Set();
4186
+ for (const entry of import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY) {
4187
+ if (!entry.allowRuntimeCreate) continue;
4188
+ out.add(entry.type);
4189
+ const plural = import_shared3.SINGULAR_TO_PLURAL[entry.type];
4190
+ if (plural) out.add(plural);
4191
+ }
4192
+ return out;
4193
+ })();
3620
4194
  var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
3621
4195
 
3622
4196
  // src/engine.ts
3623
- var import_kernel4 = require("@objectstack/spec/kernel");
4197
+ var import_kernel5 = require("@objectstack/spec/kernel");
3624
4198
  var import_core = require("@objectstack/core");
3625
4199
  var import_system2 = require("@objectstack/spec/system");
3626
- var import_shared3 = require("@objectstack/spec/shared");
4200
+ var import_shared4 = require("@objectstack/spec/shared");
3627
4201
  var import_formula2 = require("@objectstack/formula");
3628
4202
 
3629
4203
  // src/hook-wrappers.ts
@@ -4894,9 +5468,9 @@ var _ObjectQL = class _ObjectQL {
4894
5468
  const itemName = resolveMetadataItemName(key, item);
4895
5469
  if (itemName) {
4896
5470
  const toRegister = item.name === itemName ? item : { ...item, name: itemName };
4897
- this._registry.registerItem((0, import_shared3.pluralToSingular)(key), toRegister, "name", id);
5471
+ this._registry.registerItem((0, import_shared4.pluralToSingular)(key), toRegister, "name", id);
4898
5472
  } else {
4899
- this.logger.warn(`Skipping ${(0, import_shared3.pluralToSingular)(key)} without a derivable name`, { id });
5473
+ this.logger.warn(`Skipping ${(0, import_shared4.pluralToSingular)(key)} without a derivable name`, { id });
4900
5474
  }
4901
5475
  }
4902
5476
  }
@@ -5023,7 +5597,7 @@ var _ObjectQL = class _ObjectQL {
5023
5597
  const itemName = resolveMetadataItemName(key, item);
5024
5598
  if (itemName) {
5025
5599
  const toRegister = item.name === itemName ? item : { ...item, name: itemName };
5026
- this._registry.registerItem((0, import_shared3.pluralToSingular)(key), toRegister, "name", ownerId);
5600
+ this._registry.registerItem((0, import_shared4.pluralToSingular)(key), toRegister, "name", ownerId);
5027
5601
  }
5028
5602
  }
5029
5603
  }
@@ -5909,7 +6483,7 @@ var _ObjectQL = class _ObjectQL {
5909
6483
  */
5910
6484
  createContext(ctx) {
5911
6485
  return new ScopedContext(
5912
- import_kernel4.ExecutionContextSchema.parse(ctx),
6486
+ import_kernel5.ExecutionContextSchema.parse(ctx),
5913
6487
  this
5914
6488
  );
5915
6489
  }