@objectstack/objectql 7.0.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,36 +1353,57 @@ 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
- };
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
1235
1407
  var TYPE_TO_FORM = import_system.METADATA_FORM_REGISTRY;
1236
1408
  var _jsonSchemaCache = /* @__PURE__ */ new WeakMap();
1237
1409
  function toJsonSchemaSafe(schema) {
@@ -1261,9 +1433,17 @@ var HAND_CRAFTED_SCHEMAS = {
1261
1433
  abstract: { type: "boolean", default: false },
1262
1434
  datasource: { type: "string" },
1263
1435
  fields: {
1264
- type: "array",
1265
- default: [],
1266
- 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: {
1267
1447
  type: "object",
1268
1448
  properties: {
1269
1449
  name: { type: "string" },
@@ -1274,7 +1454,7 @@ var HAND_CRAFTED_SCHEMAS = {
1274
1454
  defaultValue: {},
1275
1455
  description: { type: "string" }
1276
1456
  },
1277
- required: ["name", "type"]
1457
+ required: ["type"]
1278
1458
  }
1279
1459
  },
1280
1460
  capabilities: { type: "object", additionalProperties: true }
@@ -1419,19 +1599,9 @@ var HAND_CRAFTED_SCHEMAS = {
1419
1599
  additionalProperties: true
1420
1600
  }
1421
1601
  };
1422
- var FORM_VIEW_TYPES = /* @__PURE__ */ new Set(["simple", "tabbed", "wizard", "split", "drawer", "modal"]);
1423
- function resolveOverlaySchema(type, item) {
1424
- const singular = import_shared2.PLURAL_TO_SINGULAR[type] ?? type;
1425
- switch (singular) {
1426
- case "view": {
1427
- const t = item && typeof item === "object" && "type" in item ? String(item.type) : void 0;
1428
- return t && FORM_VIEW_TYPES.has(t) ? import_ui2.FormViewSchema : import_ui2.ListViewSchema;
1429
- }
1430
- case "dashboard":
1431
- return import_ui2.DashboardSchema;
1432
- default:
1433
- return null;
1434
- }
1602
+ function resolveOverlaySchema(type, _item) {
1603
+ const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
1604
+ return (0, import_kernel4.getMetadataTypeSchema)(singular) ?? null;
1435
1605
  }
1436
1606
  function simpleHash(str) {
1437
1607
  let hash = 0;
@@ -1564,6 +1734,32 @@ function extractPathValues(item, path) {
1564
1734
  }
1565
1735
  return out;
1566
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
+ }
1567
1763
  function detectDestructiveObjectChanges(prev, next) {
1568
1764
  if (!prev || typeof prev !== "object" || !next || typeof next !== "object") return [];
1569
1765
  const prevFields = prev.fields && typeof prev.fields === "object" ? prev.fields : {};
@@ -1695,6 +1891,20 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1695
1891
  }
1696
1892
  }
1697
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
+ }
1698
1908
  } catch {
1699
1909
  }
1700
1910
  }
@@ -1815,11 +2025,11 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1815
2025
  const allTypes = Array.from(/* @__PURE__ */ new Set([...schemaTypes, ...runtimeTypes]));
1816
2026
  const writableOverrides = _ObjectStackProtocolImplementation.envWritableTypes();
1817
2027
  const registryByType = new Map(
1818
- import_kernel3.DEFAULT_METADATA_TYPE_REGISTRY.map((e) => [e.type, e])
2028
+ import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY.map((e) => [e.type, e])
1819
2029
  );
1820
2030
  const entries = allTypes.map((type) => {
1821
- const singular = import_shared2.PLURAL_TO_SINGULAR[type] ?? type;
1822
- 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);
1823
2033
  const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : void 0) ?? HAND_CRAFTED_SCHEMAS[singular];
1824
2034
  const form = TYPE_TO_FORM[singular];
1825
2035
  const base = registryByType.get(singular);
@@ -1860,19 +2070,76 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1860
2070
  });
1861
2071
  return { types: allTypes, entries };
1862
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
+ }
1863
2130
  async getMetaItems(request) {
1864
2131
  const { packageId } = request;
1865
2132
  let items = [];
1866
2133
  if (this.environmentId === void 0) {
1867
2134
  items = [...this.engine.registry.listItems(request.type, packageId)];
1868
2135
  if (items.length === 0) {
1869
- 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];
1870
2137
  if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
1871
2138
  }
1872
2139
  } else {
1873
2140
  items = [...this.engine.registry.listItems(request.type, packageId)];
1874
2141
  if (items.length === 0) {
1875
- 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];
1876
2143
  if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
1877
2144
  }
1878
2145
  }
@@ -1887,7 +2154,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1887
2154
  if (packageId) whereClause._packageId = packageId;
1888
2155
  let rs = await this.engine.find("sys_metadata", { where: whereClause });
1889
2156
  if (!rs || rs.length === 0) {
1890
- 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];
1891
2158
  if (alt) {
1892
2159
  const altWhere = { type: alt, state: "active", organization_id: oid };
1893
2160
  if (packageId) altWhere._packageId = packageId;
@@ -1954,28 +2221,29 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1954
2221
  }
1955
2222
  return {
1956
2223
  type: request.type,
1957
- items
2224
+ items: decorateMetadataItems(request.type, items)
1958
2225
  };
1959
2226
  }
1960
2227
  async getMetaItem(request) {
1961
2228
  let item;
1962
2229
  const orgId = request.organizationId;
2230
+ const readState = request.state === "draft" ? "draft" : "active";
1963
2231
  try {
1964
2232
  const findOverlay = async (oid) => {
1965
2233
  const where = {
1966
2234
  type: request.type,
1967
2235
  name: request.name,
1968
- state: "active",
2236
+ state: readState,
1969
2237
  organization_id: oid
1970
2238
  };
1971
2239
  const rec = await this.engine.findOne("sys_metadata", { where });
1972
2240
  if (rec) return rec;
1973
- 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];
1974
2242
  if (alt) {
1975
2243
  const altWhere = {
1976
2244
  type: alt,
1977
2245
  name: request.name,
1978
- state: "active",
2246
+ state: readState,
1979
2247
  organization_id: oid
1980
2248
  };
1981
2249
  return await this.engine.findOne("sys_metadata", { where: altWhere });
@@ -1988,6 +2256,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1988
2256
  }
1989
2257
  } catch {
1990
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
+ }
1991
2270
  if (item === void 0) {
1992
2271
  try {
1993
2272
  const services = this.getServicesRegistry?.();
@@ -1997,7 +2276,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1997
2276
  if (fromService !== void 0 && fromService !== null) {
1998
2277
  item = fromService;
1999
2278
  } else {
2000
- 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];
2001
2280
  if (alt) {
2002
2281
  const altFromService = await metadataService.get(alt, request.name);
2003
2282
  if (altFromService !== void 0 && altFromService !== null) {
@@ -2012,14 +2291,14 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2012
2291
  if (item === void 0) {
2013
2292
  item = this.engine.registry.getItem(request.type, request.name);
2014
2293
  if (item === void 0) {
2015
- 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];
2016
2295
  if (alt) item = this.engine.registry.getItem(alt, request.name);
2017
2296
  }
2018
2297
  }
2019
2298
  return {
2020
2299
  type: request.type,
2021
2300
  name: request.name,
2022
- item
2301
+ item: decorateMetadataItem(request.type, item)
2023
2302
  };
2024
2303
  }
2025
2304
  /**
@@ -2045,7 +2324,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2045
2324
  if (metadataService && typeof metadataService.get === "function") {
2046
2325
  let fromService = await metadataService.get(request.type, request.name);
2047
2326
  if (fromService === void 0 || fromService === null) {
2048
- 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];
2049
2328
  if (alt) fromService = await metadataService.get(alt, request.name);
2050
2329
  }
2051
2330
  if (fromService !== void 0 && fromService !== null) code = fromService;
@@ -2055,7 +2334,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2055
2334
  if (code === null) {
2056
2335
  let regItem = this.engine.registry.getItem(request.type, request.name);
2057
2336
  if (regItem === void 0) {
2058
- 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];
2059
2338
  if (alt) regItem = this.engine.registry.getItem(alt, request.name);
2060
2339
  }
2061
2340
  if (regItem !== void 0) code = regItem;
@@ -2072,7 +2351,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2072
2351
  };
2073
2352
  let rec = await this.engine.findOne("sys_metadata", { where });
2074
2353
  if (!rec) {
2075
- 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];
2076
2355
  if (alt) {
2077
2356
  rec = await this.engine.findOne("sys_metadata", {
2078
2357
  where: { ...where, type: alt }
@@ -2098,13 +2377,15 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2098
2377
  } catch {
2099
2378
  }
2100
2379
  const effective = overlay ?? code;
2380
+ const _diagnostics = effective !== null && effective !== void 0 ? computeMetadataDiagnostics(request.type, effective) : void 0;
2101
2381
  return {
2102
2382
  type: request.type,
2103
2383
  name: request.name,
2104
2384
  code,
2105
2385
  overlay,
2106
2386
  overlayScope,
2107
- effective
2387
+ effective,
2388
+ ..._diagnostics ? { _diagnostics } : {}
2108
2389
  };
2109
2390
  }
2110
2391
  async getUiView(request) {
@@ -2985,9 +3266,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2985
3266
  for (const tok of raw.split(",")) {
2986
3267
  const t = tok.trim();
2987
3268
  if (!t) continue;
2988
- const singular = import_shared2.PLURAL_TO_SINGULAR[t] ?? t;
3269
+ const singular = import_shared3.PLURAL_TO_SINGULAR[t] ?? t;
2989
3270
  set.add(singular);
2990
- const plural = import_shared2.SINGULAR_TO_PLURAL[singular];
3271
+ const plural = import_shared3.SINGULAR_TO_PLURAL[singular];
2991
3272
  if (plural) set.add(plural);
2992
3273
  }
2993
3274
  this._envWritableTypes = set;
@@ -2999,13 +3280,49 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2999
3280
  }
3000
3281
  /** Normalize plural→singular before consulting the allow-list. */
3001
3282
  static isOverlayAllowed(type) {
3002
- const singular = import_shared2.PLURAL_TO_SINGULAR[type] ?? type;
3283
+ const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
3003
3284
  if (this.OVERLAY_ALLOWED_TYPES.has(singular) || this.OVERLAY_ALLOWED_TYPES.has(type)) {
3004
3285
  return true;
3005
3286
  }
3006
3287
  const env = this.envWritableTypes();
3007
3288
  return env.has(singular) || env.has(type);
3008
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
+ }
3009
3326
  /**
3010
3327
  * Mirror an object-type overlay write into the in-memory engine
3011
3328
  * registry so subsequent CRUD finds the new schema. Idempotent and
@@ -3031,16 +3348,29 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3031
3348
  if (!request.item) {
3032
3349
  throw new Error("Item data is required");
3033
3350
  }
3034
- if (this.environmentId !== void 0 && !_ObjectStackProtocolImplementation.isOverlayAllowed(request.type)) {
3035
- const allowed = Array.from(_ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES).join(", ");
3036
- const err = new Error(
3037
- `[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.`
3038
- );
3039
- err.code = "not_overridable";
3040
- err.status = 403;
3041
- 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
+ }
3042
3372
  }
3043
- const singularType = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3373
+ const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3044
3374
  if (!request.force && (singularType === "object" || singularType === "field")) {
3045
3375
  try {
3046
3376
  const existing = await this.getMetaItem({
@@ -3088,8 +3418,13 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3088
3418
  }
3089
3419
  }
3090
3420
  await this.ensureOverlayIndex();
3091
- const singularTypeForRepo = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3092
- 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";
3093
3428
  const orgId = request.organizationId ?? null;
3094
3429
  const repo = this.getOverlayRepo(orgId);
3095
3430
  const ref = {
@@ -3101,21 +3436,26 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3101
3436
  if (request.parentVersion !== void 0) {
3102
3437
  parentVersion = request.parentVersion;
3103
3438
  } else {
3104
- const current = await repo.get(ref);
3439
+ const current = await repo.get(ref, { state: mode === "draft" ? "draft" : "active" });
3105
3440
  parentVersion = current?.hash ?? null;
3106
3441
  }
3107
3442
  try {
3108
3443
  const result = await repo.put(ref, request.item, {
3109
3444
  parentVersion,
3110
3445
  actor: request.actor ?? "system",
3111
- source: "protocol.saveMetaItem"
3446
+ source: "protocol.saveMetaItem",
3447
+ intent,
3448
+ state: mode === "draft" ? "draft" : "active"
3112
3449
  });
3113
- this.applyObjectRegistryMutation(request);
3450
+ if (mode === "publish") {
3451
+ this.applyObjectRegistryMutation(request);
3452
+ }
3114
3453
  return {
3115
3454
  success: true,
3116
3455
  version: result.version,
3117
3456
  seq: result.seq,
3118
- 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}]`
3119
3459
  };
3120
3460
  } catch (err) {
3121
3461
  if (err instanceof import_metadata_core2.ConflictError) {
@@ -3199,8 +3539,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3199
3539
  * "no history" uniformly.
3200
3540
  */
3201
3541
  async historyMetaItem(request) {
3202
- const singularType = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3203
- if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType)) {
3542
+ const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3543
+ if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
3204
3544
  return { events: [] };
3205
3545
  }
3206
3546
  const orgId = request.organizationId ?? null;
@@ -3218,22 +3558,238 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3218
3558
  return { events };
3219
3559
  }
3220
3560
  /**
3221
- * Remove a customization overlay row for the given metadata item, so the
3222
- * next read falls through to the artifact-loaded default. Implements the
3223
- * "Reset to factory default" semantic from ADR-0005. Whitelist is shared
3224
- * 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.
3225
3564
  */
3226
- async deleteMetaItem(request) {
3227
- 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)) {
3228
3568
  const err = new Error(
3229
- `[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.`
3230
3570
  );
3231
3571
  err.code = "not_overridable";
3232
3572
  err.status = 403;
3233
3573
  throw err;
3234
3574
  }
3235
- const singularTypeForRepo = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3236
- 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;
3237
3793
  if (useRepoPath) {
3238
3794
  const orgId = request.organizationId ?? null;
3239
3795
  const repo = this.getOverlayRepo(orgId);
@@ -3243,19 +3799,22 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3243
3799
  org: orgId ?? "env"
3244
3800
  };
3245
3801
  try {
3246
- const current = await repo.get(ref);
3802
+ const targetState = request.state === "draft" ? "draft" : "active";
3803
+ const current = await repo.get(ref, { state: targetState });
3247
3804
  if (!current) {
3248
3805
  return {
3249
3806
  success: true,
3250
3807
  reset: false,
3251
- 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.`
3252
3809
  };
3253
3810
  }
3254
3811
  const parentVersion = request.parentVersion !== void 0 ? request.parentVersion ?? current.hash : current.hash;
3255
3812
  const result = await repo.delete(ref, {
3256
3813
  parentVersion,
3257
3814
  actor: request.actor ?? "system",
3258
- source: "protocol.deleteMetaItem"
3815
+ source: "protocol.deleteMetaItem",
3816
+ intent: this.isArtifactBacked(singularTypeForRepo, request.name) ? "override-artifact" : "runtime-only",
3817
+ state: targetState
3259
3818
  });
3260
3819
  if (this.environmentId === void 0) {
3261
3820
  try {
@@ -3274,7 +3833,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3274
3833
  success: true,
3275
3834
  reset: true,
3276
3835
  seq: result.seq,
3277
- 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}]`
3278
3837
  };
3279
3838
  } catch (err) {
3280
3839
  if (err instanceof import_metadata_core2.ConflictError) {
@@ -3352,7 +3911,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3352
3911
  for (const record of records) {
3353
3912
  try {
3354
3913
  const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
3355
- const normalizedType = import_shared2.PLURAL_TO_SINGULAR[record.type] ?? record.type;
3914
+ const normalizedType = import_shared3.PLURAL_TO_SINGULAR[record.type] ?? record.type;
3356
3915
  if (normalizedType === "object") {
3357
3916
  this.engine.registry.registerObject(data, record.packageId || "sys_metadata");
3358
3917
  } else {
@@ -3386,7 +3945,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3386
3945
  * — the engine never throws.
3387
3946
  */
3388
3947
  async findReferencesToMeta(request) {
3389
- const singularTarget = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3948
+ const singularTarget = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3390
3949
  const targetName = request.name;
3391
3950
  const matchers = REFERENCE_PATHS[singularTarget];
3392
3951
  if (!matchers || matchers.length === 0) {
@@ -3577,10 +4136,10 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3577
4136
  */
3578
4137
  _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
3579
4138
  const out = /* @__PURE__ */ new Set();
3580
- for (const entry of import_kernel3.DEFAULT_METADATA_TYPE_REGISTRY) {
4139
+ for (const entry of import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY) {
3581
4140
  if (!entry.allowOrgOverride) continue;
3582
4141
  out.add(entry.type);
3583
- const plural = import_shared2.SINGULAR_TO_PLURAL[entry.type];
4142
+ const plural = import_shared3.SINGULAR_TO_PLURAL[entry.type];
3584
4143
  if (plural) out.add(plural);
3585
4144
  }
3586
4145
  return out;
@@ -3597,13 +4156,48 @@ _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
3597
4156
  * {@link ObjectStackProtocolImplementation.resetEnvWritableCache}.
3598
4157
  */
3599
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
+ })();
3600
4194
  var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
3601
4195
 
3602
4196
  // src/engine.ts
3603
- var import_kernel4 = require("@objectstack/spec/kernel");
4197
+ var import_kernel5 = require("@objectstack/spec/kernel");
3604
4198
  var import_core = require("@objectstack/core");
3605
4199
  var import_system2 = require("@objectstack/spec/system");
3606
- var import_shared3 = require("@objectstack/spec/shared");
4200
+ var import_shared4 = require("@objectstack/spec/shared");
3607
4201
  var import_formula2 = require("@objectstack/formula");
3608
4202
 
3609
4203
  // src/hook-wrappers.ts
@@ -4874,9 +5468,9 @@ var _ObjectQL = class _ObjectQL {
4874
5468
  const itemName = resolveMetadataItemName(key, item);
4875
5469
  if (itemName) {
4876
5470
  const toRegister = item.name === itemName ? item : { ...item, name: itemName };
4877
- this._registry.registerItem((0, import_shared3.pluralToSingular)(key), toRegister, "name", id);
5471
+ this._registry.registerItem((0, import_shared4.pluralToSingular)(key), toRegister, "name", id);
4878
5472
  } else {
4879
- 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 });
4880
5474
  }
4881
5475
  }
4882
5476
  }
@@ -5003,7 +5597,7 @@ var _ObjectQL = class _ObjectQL {
5003
5597
  const itemName = resolveMetadataItemName(key, item);
5004
5598
  if (itemName) {
5005
5599
  const toRegister = item.name === itemName ? item : { ...item, name: itemName };
5006
- this._registry.registerItem((0, import_shared3.pluralToSingular)(key), toRegister, "name", ownerId);
5600
+ this._registry.registerItem((0, import_shared4.pluralToSingular)(key), toRegister, "name", ownerId);
5007
5601
  }
5008
5602
  }
5009
5603
  }
@@ -5889,7 +6483,7 @@ var _ObjectQL = class _ObjectQL {
5889
6483
  */
5890
6484
  createContext(ctx) {
5891
6485
  return new ScopedContext(
5892
- import_kernel4.ExecutionContextSchema.parse(ctx),
6486
+ import_kernel5.ExecutionContextSchema.parse(ctx),
5893
6487
  this
5894
6488
  );
5895
6489
  }