@objectstack/objectql 7.0.0 → 7.2.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
  }
@@ -496,7 +504,9 @@ var SchemaRegistry = class {
496
504
  const direct = collection.get(name);
497
505
  if (direct) return direct;
498
506
  for (const [key, item] of collection) {
499
- if (key.endsWith(`:${name}`)) return item;
507
+ if (key.endsWith(`:${name}`)) {
508
+ return item;
509
+ }
500
510
  }
501
511
  return void 0;
502
512
  }
@@ -676,6 +686,12 @@ var import_shared = require("@objectstack/spec/shared");
676
686
  var OVERLAY_ALLOWED_TYPES = new Set(
677
687
  import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowOrgOverride).map((e) => e.type)
678
688
  );
689
+ var STATIC_REGISTRY_TYPES = new Set(
690
+ import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY.map((e) => e.type)
691
+ );
692
+ var RUNTIME_CREATE_ALLOWED_TYPES = new Set(
693
+ import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowRuntimeCreate).map((e) => e.type)
694
+ );
679
695
  var _envWritableMetadataTypes = null;
680
696
  function envWritableMetadataTypes() {
681
697
  if (_envWritableMetadataTypes !== null) return _envWritableMetadataTypes;
@@ -725,11 +741,16 @@ var SysMetadataRepository = class {
725
741
  /**
726
742
  * Read the current overlay row. Returns null if no row exists —
727
743
  * callers (e.g. LayeredRepository) fall through to lower layers.
744
+ *
745
+ * `opts.state` selects which lifecycle row to read: defaults to the
746
+ * live published row (`'active'`). Pass `'draft'` to read the pending
747
+ * unpublished revision (if any).
728
748
  */
729
- async get(ref) {
749
+ async get(ref, opts) {
730
750
  this.assertOpen();
751
+ const state = opts?.state ?? "active";
731
752
  const row = await this.engine.findOne("sys_metadata", {
732
- where: this.whereFor(ref)
753
+ where: this.whereFor(ref, state)
733
754
  });
734
755
  if (!row) return null;
735
756
  return this.rowToItem(ref, row);
@@ -772,12 +793,13 @@ var SysMetadataRepository = class {
772
793
  }
773
794
  async put(ref, spec, opts) {
774
795
  this.assertOpen();
775
- this.assertAllowed(ref.type);
796
+ this.assertAllowed(ref.type, opts.intent);
797
+ const state = opts.state ?? "active";
776
798
  const body = spec ?? {};
777
799
  const hash = (0, import_metadata_core.hashSpec)(body);
778
800
  const result = await this.withTxn(async (ctx) => {
779
801
  const existing = await this.engine.findOne("sys_metadata", {
780
- where: this.whereFor(ref),
802
+ where: this.whereFor(ref, state),
781
803
  context: ctx
782
804
  });
783
805
  const existingHash = existing?.checksum ?? null;
@@ -789,7 +811,8 @@ var SysMetadataRepository = class {
789
811
  return { skipped: true, version: hash, seq: item2.seq, item: item2 };
790
812
  }
791
813
  const now = (/* @__PURE__ */ new Date()).toISOString();
792
- const op = existing ? "update" : "create";
814
+ const baseOp = existing ? "update" : "create";
815
+ const op = opts.opType ?? baseOp;
793
816
  const version = await this.nextItemVersion(ref, ctx);
794
817
  const eventSeq = await this.nextEventSeq(ctx);
795
818
  const parentRowData = {
@@ -798,7 +821,7 @@ var SysMetadataRepository = class {
798
821
  organization_id: this.organizationId,
799
822
  metadata: JSON.stringify(body),
800
823
  checksum: hash,
801
- state: "active",
824
+ state,
802
825
  version,
803
826
  updated_at: now
804
827
  };
@@ -864,25 +887,28 @@ var SysMetadataRepository = class {
864
887
  return { version: result.version, seq: result.seq, item: result.item };
865
888
  }
866
889
  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
- });
890
+ if (state === "active") {
891
+ this.broadcast({
892
+ seq: result.seq,
893
+ op: result.op,
894
+ ref: this.fullRef(ref),
895
+ hash: result.version,
896
+ parentHash: result.existingHash,
897
+ actor: result.actor,
898
+ message: result.message,
899
+ ts: result.now,
900
+ source: result.source
901
+ });
902
+ }
878
903
  return { version: result.version, seq: result.seq, item: result.item };
879
904
  }
880
905
  async delete(ref, opts) {
881
906
  this.assertOpen();
882
- this.assertAllowed(ref.type);
907
+ this.assertAllowed(ref.type, opts.intent);
908
+ const state = opts.state ?? "active";
883
909
  const result = await this.withTxn(async (ctx) => {
884
910
  const existing = await this.engine.findOne("sys_metadata", {
885
- where: this.whereFor(ref),
911
+ where: this.whereFor(ref, state),
886
912
  context: ctx
887
913
  });
888
914
  if (!existing) {
@@ -899,32 +925,38 @@ var SysMetadataRepository = class {
899
925
  );
900
926
  }
901
927
  const now = (/* @__PURE__ */ new Date()).toISOString();
902
- const version = await this.nextItemVersion(ref, ctx);
903
- const eventSeq = await this.nextEventSeq(ctx);
928
+ let version = 0;
929
+ let eventSeq = 0;
930
+ if (state === "active") {
931
+ version = await this.nextItemVersion(ref, ctx);
932
+ eventSeq = await this.nextEventSeq(ctx);
933
+ }
904
934
  await this.engine.delete("sys_metadata", {
905
935
  where: { id: existingId },
906
936
  context: ctx
907
937
  });
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
- );
938
+ if (state === "active") {
939
+ await this.engine.insert(
940
+ this.historyTable,
941
+ {
942
+ id: this.uuid(),
943
+ event_seq: eventSeq,
944
+ type: ref.type,
945
+ name: ref.name,
946
+ version,
947
+ operation_type: "delete",
948
+ metadata: null,
949
+ checksum: null,
950
+ previous_checksum: existingHash,
951
+ change_note: opts.message,
952
+ source: opts.source ?? "sys-metadata-repo",
953
+ organization_id: this.organizationId,
954
+ recorded_by: opts.actor,
955
+ recorded_at: now
956
+ },
957
+ { context: ctx }
958
+ );
959
+ }
928
960
  return {
929
961
  eventSeq,
930
962
  existingHash,
@@ -934,20 +966,117 @@ var SysMetadataRepository = class {
934
966
  actor: opts.actor
935
967
  };
936
968
  });
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
- });
969
+ if (state === "active") {
970
+ this.seqCounter = result.eventSeq;
971
+ this.broadcast({
972
+ seq: result.eventSeq,
973
+ op: "delete",
974
+ ref: this.fullRef(ref),
975
+ hash: null,
976
+ parentHash: result.existingHash,
977
+ actor: result.actor,
978
+ message: result.message,
979
+ ts: result.now,
980
+ source: result.source
981
+ });
982
+ }
949
983
  return { seq: result.eventSeq };
950
984
  }
985
+ /**
986
+ * Promote the pending draft row for `ref` into the live (`active`)
987
+ * overlay. Atomic: reads the draft inside the same transaction, runs
988
+ * the canonical `put` to upsert the active row (which appends a
989
+ * history event with `operation_type='publish'`), then deletes the
990
+ * draft row.
991
+ *
992
+ * Errors if no draft exists (callers should 404). The active row's
993
+ * `parentVersion` is computed from the current active hash so this
994
+ * also surfaces optimistic-lock conflicts when something else has
995
+ * published in between (e.g. another admin reverted to an older
996
+ * version since the draft was authored).
997
+ */
998
+ async promoteDraft(ref, opts) {
999
+ this.assertOpen();
1000
+ const draft = await this.get(ref, { state: "draft" });
1001
+ if (!draft) {
1002
+ const err = new Error(
1003
+ `[no_draft] No pending draft exists for ${ref.type}/${ref.name} \u2014 nothing to publish.`
1004
+ );
1005
+ err.code = "no_draft";
1006
+ err.status = 404;
1007
+ throw err;
1008
+ }
1009
+ const currentActive = await this.get(ref, { state: "active" });
1010
+ const result = await this.put(ref, draft.body, {
1011
+ parentVersion: currentActive?.hash ?? null,
1012
+ actor: opts.actor,
1013
+ source: opts.source ?? "sys-metadata-repo.publish",
1014
+ message: opts.message ?? `publish draft (hash ${draft.hash})`,
1015
+ intent: opts.intent ?? "override-artifact",
1016
+ state: "active",
1017
+ opType: "publish"
1018
+ });
1019
+ try {
1020
+ await this.delete(ref, {
1021
+ parentVersion: draft.hash,
1022
+ actor: opts.actor,
1023
+ source: opts.source ?? "sys-metadata-repo.publish",
1024
+ intent: opts.intent ?? "override-artifact",
1025
+ state: "draft"
1026
+ });
1027
+ } catch {
1028
+ }
1029
+ return result;
1030
+ }
1031
+ /**
1032
+ * Restore the body recorded in history at `targetVersion` (per-org
1033
+ * lineage counter) as the new active row. Writes a history event
1034
+ * with `operation_type='revert'` so the audit trail captures the
1035
+ * intent. Does NOT touch any draft row.
1036
+ *
1037
+ * Throws `[version_not_found]` (404) if the target version row is
1038
+ * missing or is a delete tombstone (no body to restore).
1039
+ */
1040
+ async restoreVersion(ref, targetVersion, opts) {
1041
+ this.assertOpen();
1042
+ const full = this.fullRef(ref);
1043
+ const row = await this.engine.findOne(this.historyTable, {
1044
+ where: {
1045
+ organization_id: this.organizationId,
1046
+ type: full.type,
1047
+ name: full.name,
1048
+ version: targetVersion
1049
+ }
1050
+ });
1051
+ if (!row) {
1052
+ const err = new Error(
1053
+ `[version_not_found] No history row at version ${targetVersion} for ${ref.type}/${ref.name}.`
1054
+ );
1055
+ err.code = "version_not_found";
1056
+ err.status = 404;
1057
+ throw err;
1058
+ }
1059
+ const raw = row.metadata;
1060
+ if (raw === null || raw === void 0) {
1061
+ const err = new Error(
1062
+ `[version_not_restorable] Version ${targetVersion} for ${ref.type}/${ref.name} is a delete tombstone \u2014 nothing to restore.`
1063
+ );
1064
+ err.code = "version_not_restorable";
1065
+ err.status = 409;
1066
+ throw err;
1067
+ }
1068
+ const body = typeof raw === "string" ? JSON.parse(raw) : raw;
1069
+ const currentActive = await this.get(ref, { state: "active" });
1070
+ return this.put(ref, body, {
1071
+ parentVersion: currentActive?.hash ?? null,
1072
+ actor: opts.actor,
1073
+ source: opts.source ?? "sys-metadata-repo.revert",
1074
+ message: opts.message ?? `revert to version ${targetVersion}`,
1075
+ intent: opts.intent ?? "override-artifact",
1076
+ state: "active",
1077
+ opType: "revert"
1078
+ });
1079
+ }
951
1080
  async *list(filter) {
952
1081
  this.assertOpen();
953
1082
  const where = {
@@ -1002,6 +1131,7 @@ var SysMetadataRepository = class {
1002
1131
  ref: full,
1003
1132
  hash: row.checksum ?? null,
1004
1133
  parentHash: row.previous_checksum ?? null,
1134
+ version: typeof row.version === "number" ? row.version : void 0,
1005
1135
  actor: row.recorded_by ?? "unknown",
1006
1136
  message: row.change_note ?? void 0,
1007
1137
  ts: row.recorded_at ?? (/* @__PURE__ */ new Date(0)).toISOString(),
@@ -1083,29 +1213,52 @@ var SysMetadataRepository = class {
1083
1213
  assertOpen() {
1084
1214
  if (this.closed) throw new Error("SysMetadataRepository is closed");
1085
1215
  }
1086
- assertAllowed(type) {
1216
+ /**
1217
+ * Defense-in-depth authorization gate.
1218
+ *
1219
+ * `intent` defaults to `'override-artifact'` (the historical strict
1220
+ * behavior). The protocol layer passes `'runtime-only'` after it has
1221
+ * verified — via the schema registry — that no artifact item exists
1222
+ * at `(type, name)`. In that case we accept types with
1223
+ * `allowRuntimeCreate: true`, even when `allowOrgOverride` is false.
1224
+ *
1225
+ * The env-var escape hatch (`OBJECTSTACK_METADATA_WRITABLE`) still
1226
+ * applies to BOTH intents, so operators can opt into artifact
1227
+ * overrides at runtime for emergency fixes.
1228
+ */
1229
+ assertAllowed(type, intent = "override-artifact") {
1087
1230
  const singular = import_shared.PLURAL_TO_SINGULAR[type] ?? type;
1088
1231
  const allowedByRegistry = OVERLAY_ALLOWED_TYPES.has(singular) || OVERLAY_ALLOWED_TYPES.has(type);
1089
1232
  if (allowedByRegistry) return;
1233
+ if (intent === "runtime-only") {
1234
+ if (RUNTIME_CREATE_ALLOWED_TYPES.has(singular) || RUNTIME_CREATE_ALLOWED_TYPES.has(type)) {
1235
+ return;
1236
+ }
1237
+ if (!STATIC_REGISTRY_TYPES.has(singular) && !STATIC_REGISTRY_TYPES.has(type)) {
1238
+ return;
1239
+ }
1240
+ }
1090
1241
  const env = envWritableMetadataTypes();
1091
1242
  if (env.has(singular) || env.has(type)) return;
1092
1243
  const allowed = [
1093
1244
  ...OVERLAY_ALLOWED_TYPES,
1094
1245
  ...envWritableMetadataTypes()
1095
1246
  ];
1247
+ const code = intent === "runtime-only" ? "not_creatable" : "not_overridable";
1248
+ const detail = intent === "runtime-only" ? `'${type}' has neither allowOrgOverride nor allowRuntimeCreate in the registry. ` : `'${type}' is not allowOrgOverride in the registry. `;
1096
1249
  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.`
1250
+ `[${code}] ${detail}Overlay-allowed: ${Array.from(new Set(allowed)).join(", ") || "(none)"}. Set OBJECTSTACK_METADATA_WRITABLE to enable additional types at runtime.`
1098
1251
  );
1099
- err.code = "not_overridable";
1252
+ err.code = code;
1100
1253
  err.status = 403;
1101
1254
  throw err;
1102
1255
  }
1103
- whereFor(ref) {
1256
+ whereFor(ref, state = "active") {
1104
1257
  return {
1105
1258
  type: ref.type,
1106
1259
  name: ref.name,
1107
1260
  organization_id: this.organizationId,
1108
- state: "active"
1261
+ state
1109
1262
  };
1110
1263
  }
1111
1264
  fullRef(ref) {
@@ -1202,36 +1355,58 @@ var SysMetadataRepository = class {
1202
1355
  // src/protocol.ts
1203
1356
  var import_metadata_core2 = require("@objectstack/metadata-core");
1204
1357
  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");
1358
+ var import_shared3 = require("@objectstack/spec/shared");
1209
1359
  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");
1360
+ var import_kernel4 = require("@objectstack/spec/kernel");
1361
+ var import_kernel5 = require("@objectstack/spec/kernel");
1213
1362
  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
- };
1363
+
1364
+ // src/metadata-diagnostics.ts
1365
+ var import_kernel3 = require("@objectstack/spec/kernel");
1366
+ var import_shared2 = require("@objectstack/spec/shared");
1367
+ function computeMetadataDiagnostics(type, item) {
1368
+ const singular = import_shared2.PLURAL_TO_SINGULAR[type] ?? type;
1369
+ const schema = (0, import_kernel3.getMetadataTypeSchema)(singular);
1370
+ if (!schema) return void 0;
1371
+ if (item === null || item === void 0 || typeof item !== "object") {
1372
+ return {
1373
+ valid: false,
1374
+ errors: [{
1375
+ path: "",
1376
+ message: "Metadata document must be a non-null object",
1377
+ code: "invalid_type"
1378
+ }]
1379
+ };
1380
+ }
1381
+ const candidate = "_diagnostics" in item ? stripDiagnostics(item) : item;
1382
+ const parsed = schema.safeParse(candidate);
1383
+ if (parsed.success) {
1384
+ return { valid: true };
1385
+ }
1386
+ const errors = parsed.error.issues.map((issue) => ({
1387
+ path: issue.path.map(String).join("."),
1388
+ message: issue.message,
1389
+ code: issue.code
1390
+ }));
1391
+ return { valid: false, errors };
1392
+ }
1393
+ function stripDiagnostics(item) {
1394
+ const { _diagnostics: _drop, ...rest } = item;
1395
+ void _drop;
1396
+ return rest;
1397
+ }
1398
+ function decorateMetadataItem(type, item) {
1399
+ if (!item || typeof item !== "object") return item;
1400
+ const diagnostics = computeMetadataDiagnostics(type, item);
1401
+ if (!diagnostics) return item;
1402
+ return { ...item, _diagnostics: diagnostics };
1403
+ }
1404
+ function decorateMetadataItems(type, items) {
1405
+ if (!Array.isArray(items)) return items;
1406
+ return items.map((item) => decorateMetadataItem(type, item));
1407
+ }
1408
+
1409
+ // src/protocol.ts
1235
1410
  var TYPE_TO_FORM = import_system.METADATA_FORM_REGISTRY;
1236
1411
  var _jsonSchemaCache = /* @__PURE__ */ new WeakMap();
1237
1412
  function toJsonSchemaSafe(schema) {
@@ -1261,9 +1436,17 @@ var HAND_CRAFTED_SCHEMAS = {
1261
1436
  abstract: { type: "boolean", default: false },
1262
1437
  datasource: { type: "string" },
1263
1438
  fields: {
1264
- type: "array",
1265
- default: [],
1266
- items: {
1439
+ // Canonical Object.fields is a name-keyed map
1440
+ // (Record<string, FieldDefinition>) — insertion order is
1441
+ // display order. The SchemaForm engine recognises
1442
+ // `additionalProperties` as a Record and dispatches to
1443
+ // the `record` form-field renderer (ADR-0007). The form
1444
+ // layout in `object.form.ts` declares `type: 'record'`
1445
+ // so the inner `additionalProperties` schema is used to
1446
+ // shape each value.
1447
+ type: "object",
1448
+ default: {},
1449
+ additionalProperties: {
1267
1450
  type: "object",
1268
1451
  properties: {
1269
1452
  name: { type: "string" },
@@ -1274,7 +1457,7 @@ var HAND_CRAFTED_SCHEMAS = {
1274
1457
  defaultValue: {},
1275
1458
  description: { type: "string" }
1276
1459
  },
1277
- required: ["name", "type"]
1460
+ required: ["type"]
1278
1461
  }
1279
1462
  },
1280
1463
  capabilities: { type: "object", additionalProperties: true }
@@ -1419,19 +1602,22 @@ var HAND_CRAFTED_SCHEMAS = {
1419
1602
  additionalProperties: true
1420
1603
  }
1421
1604
  };
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
- }
1605
+ function resolveOverlaySchema(type, _item) {
1606
+ const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
1607
+ return (0, import_kernel4.getMetadataTypeSchema)(singular) ?? null;
1608
+ }
1609
+ function mergeArtifactProtection(item, artifactItem) {
1610
+ if (item === void 0 || item === null) return item;
1611
+ if (artifactItem === void 0 || artifactItem === null) return item;
1612
+ const a = artifactItem;
1613
+ if (typeof a !== "object") return item;
1614
+ const out = { ...item };
1615
+ if (a._lock !== void 0) out._lock = a._lock;
1616
+ if (a._lockReason !== void 0) out._lockReason = a._lockReason;
1617
+ if (a._packageId !== void 0) out._packageId = a._packageId;
1618
+ if (a._packageVersion !== void 0) out._packageVersion = a._packageVersion;
1619
+ if (a._provenance !== void 0) out._provenance = a._provenance;
1620
+ return out;
1435
1621
  }
1436
1622
  function simpleHash(str) {
1437
1623
  let hash = 0;
@@ -1564,6 +1750,32 @@ function extractPathValues(item, path) {
1564
1750
  }
1565
1751
  return out;
1566
1752
  }
1753
+ function diffShallow(from, to) {
1754
+ const added = [];
1755
+ const removed = [];
1756
+ const changed = [];
1757
+ const fromKeys = new Set(Object.keys(from ?? {}));
1758
+ const toKeys = new Set(Object.keys(to ?? {}));
1759
+ for (const k of toKeys) {
1760
+ if (!fromKeys.has(k)) {
1761
+ added.push({ path: k, value: to[k] });
1762
+ } else {
1763
+ const a = from[k];
1764
+ const b = to[k];
1765
+ const aStr = JSON.stringify(a);
1766
+ const bStr = JSON.stringify(b);
1767
+ if (aStr !== bStr) {
1768
+ changed.push({ path: k, from: a, to: b });
1769
+ }
1770
+ }
1771
+ }
1772
+ for (const k of fromKeys) {
1773
+ if (!toKeys.has(k)) {
1774
+ removed.push({ path: k, value: from[k] });
1775
+ }
1776
+ }
1777
+ return { added, removed, changed };
1778
+ }
1567
1779
  function detectDestructiveObjectChanges(prev, next) {
1568
1780
  if (!prev || typeof prev !== "object" || !next || typeof next !== "object") return [];
1569
1781
  const prevFields = prev.fields && typeof prev.fields === "object" ? prev.fields : {};
@@ -1695,6 +1907,20 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1695
1907
  }
1696
1908
  }
1697
1909
  }
1910
+ const draftPartialSql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_sys_metadata_overlay_draft ON sys_metadata (type, name, organization_id) WHERE state = 'draft'";
1911
+ try {
1912
+ await exec(draftPartialSql);
1913
+ } catch (err) {
1914
+ const msg = err instanceof Error ? err.message : String(err);
1915
+ if (/partial|where clause|syntax/i.test(msg)) {
1916
+ try {
1917
+ await exec(
1918
+ "CREATE INDEX IF NOT EXISTS idx_sys_metadata_overlay_draft ON sys_metadata (type, name, organization_id)"
1919
+ );
1920
+ } catch {
1921
+ }
1922
+ }
1923
+ }
1698
1924
  } catch {
1699
1925
  }
1700
1926
  }
@@ -1815,11 +2041,11 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1815
2041
  const allTypes = Array.from(/* @__PURE__ */ new Set([...schemaTypes, ...runtimeTypes]));
1816
2042
  const writableOverrides = _ObjectStackProtocolImplementation.envWritableTypes();
1817
2043
  const registryByType = new Map(
1818
- import_kernel3.DEFAULT_METADATA_TYPE_REGISTRY.map((e) => [e.type, e])
2044
+ import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY.map((e) => [e.type, e])
1819
2045
  );
1820
2046
  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];
2047
+ const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
2048
+ const zodSchema = (0, import_kernel4.getMetadataTypeSchema)(singular);
1823
2049
  const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : void 0) ?? HAND_CRAFTED_SCHEMAS[singular];
1824
2050
  const form = TYPE_TO_FORM[singular];
1825
2051
  const base = registryByType.get(singular);
@@ -1860,19 +2086,82 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1860
2086
  });
1861
2087
  return { types: allTypes, entries };
1862
2088
  }
2089
+ /**
2090
+ * Sweep all (or filtered) metadata types and report entries that
2091
+ * fail spec validation. Powers the Studio governance view
2092
+ * (`GET /api/v1/meta/diagnostics`) and `os doctor`-style CLI
2093
+ * checks.
2094
+ *
2095
+ * `severity` defaults to `'error'` — only entries with at least
2096
+ * one Zod error issue are returned. `'warning'` includes
2097
+ * everything we surface (warnings are reserved for a future lint
2098
+ * layer on top of spec validation).
2099
+ *
2100
+ * `type` may be either a singular (`'view'`) or plural (`'views'`)
2101
+ * identifier; the underlying `getMetaItems` already normalises.
2102
+ *
2103
+ * Implementation note: leverages the `_diagnostics` already
2104
+ * decorated onto items by `getMetaItems()` to avoid running
2105
+ * `safeParse()` twice. For types whose schema is unregistered we
2106
+ * skip silently (they cannot be validated and should not appear
2107
+ * as "valid" either — they are simply opaque to this report).
2108
+ */
2109
+ async getMetaDiagnostics(request = {}) {
2110
+ const includeWarnings = request.severity === "warning";
2111
+ const targetTypes = request.type ? [request.type] : import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => (0, import_kernel4.getMetadataTypeSchema)(e.type)).map((e) => e.type);
2112
+ const entries = [];
2113
+ const stats = {};
2114
+ let scannedItems = 0;
2115
+ for (const t of targetTypes) {
2116
+ let listed;
2117
+ try {
2118
+ listed = await this.getMetaItems({
2119
+ type: t,
2120
+ organizationId: request.organizationId,
2121
+ packageId: request.packageId
2122
+ });
2123
+ } catch {
2124
+ continue;
2125
+ }
2126
+ const items = Array.isArray(listed?.items) ? listed.items : Array.isArray(listed) ? listed : [];
2127
+ const pkgSet = /* @__PURE__ */ new Set();
2128
+ for (const item of items) {
2129
+ scannedItems += 1;
2130
+ const pkg = item?._packageId ?? null;
2131
+ if (pkg) pkgSet.add(pkg);
2132
+ const diag = item?._diagnostics ?? computeMetadataDiagnostics(t, item);
2133
+ if (!diag) continue;
2134
+ if (diag.valid && !includeWarnings) continue;
2135
+ if (diag.valid && includeWarnings && !diag.warnings?.length) continue;
2136
+ entries.push({
2137
+ type: t,
2138
+ name: typeof item?.name === "string" ? item.name : "<unknown>",
2139
+ diagnostics: diag
2140
+ });
2141
+ }
2142
+ stats[t] = { count: items.length, packages: [...pkgSet].sort() };
2143
+ }
2144
+ return {
2145
+ entries,
2146
+ total: entries.length,
2147
+ scannedTypes: targetTypes.length,
2148
+ scannedItems,
2149
+ stats
2150
+ };
2151
+ }
1863
2152
  async getMetaItems(request) {
1864
2153
  const { packageId } = request;
1865
2154
  let items = [];
1866
2155
  if (this.environmentId === void 0) {
1867
2156
  items = [...this.engine.registry.listItems(request.type, packageId)];
1868
2157
  if (items.length === 0) {
1869
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2158
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
1870
2159
  if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
1871
2160
  }
1872
2161
  } else {
1873
2162
  items = [...this.engine.registry.listItems(request.type, packageId)];
1874
2163
  if (items.length === 0) {
1875
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2164
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
1876
2165
  if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
1877
2166
  }
1878
2167
  }
@@ -1887,7 +2176,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1887
2176
  if (packageId) whereClause._packageId = packageId;
1888
2177
  let rs = await this.engine.find("sys_metadata", { where: whereClause });
1889
2178
  if (!rs || rs.length === 0) {
1890
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2179
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
1891
2180
  if (alt) {
1892
2181
  const altWhere = { type: alt, state: "active", organization_id: oid };
1893
2182
  if (packageId) altWhere._packageId = packageId;
@@ -1954,28 +2243,38 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1954
2243
  }
1955
2244
  return {
1956
2245
  type: request.type,
1957
- items
2246
+ items: decorateMetadataItems(
2247
+ request.type,
2248
+ items.map((it) => {
2249
+ const a = this.lookupArtifactItem(
2250
+ request.type,
2251
+ it?.name
2252
+ );
2253
+ return mergeArtifactProtection(it, a);
2254
+ })
2255
+ )
1958
2256
  };
1959
2257
  }
1960
2258
  async getMetaItem(request) {
1961
2259
  let item;
1962
2260
  const orgId = request.organizationId;
2261
+ const readState = request.state === "draft" ? "draft" : "active";
1963
2262
  try {
1964
2263
  const findOverlay = async (oid) => {
1965
2264
  const where = {
1966
2265
  type: request.type,
1967
2266
  name: request.name,
1968
- state: "active",
2267
+ state: readState,
1969
2268
  organization_id: oid
1970
2269
  };
1971
2270
  const rec = await this.engine.findOne("sys_metadata", { where });
1972
2271
  if (rec) return rec;
1973
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2272
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
1974
2273
  if (alt) {
1975
2274
  const altWhere = {
1976
2275
  type: alt,
1977
2276
  name: request.name,
1978
- state: "active",
2277
+ state: readState,
1979
2278
  organization_id: oid
1980
2279
  };
1981
2280
  return await this.engine.findOne("sys_metadata", { where: altWhere });
@@ -1988,6 +2287,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1988
2287
  }
1989
2288
  } catch {
1990
2289
  }
2290
+ if (readState === "draft") {
2291
+ if (item === void 0) {
2292
+ const err = new Error(
2293
+ `[no_draft] No pending draft exists for ${request.type}/${request.name}.`
2294
+ );
2295
+ err.code = "no_draft";
2296
+ err.status = 404;
2297
+ throw err;
2298
+ }
2299
+ return { type: request.type, name: request.name, item: decorateMetadataItem(request.type, item) };
2300
+ }
1991
2301
  if (item === void 0) {
1992
2302
  try {
1993
2303
  const services = this.getServicesRegistry?.();
@@ -1997,7 +2307,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1997
2307
  if (fromService !== void 0 && fromService !== null) {
1998
2308
  item = fromService;
1999
2309
  } else {
2000
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2310
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
2001
2311
  if (alt) {
2002
2312
  const altFromService = await metadataService.get(alt, request.name);
2003
2313
  if (altFromService !== void 0 && altFromService !== null) {
@@ -2012,14 +2322,30 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2012
2322
  if (item === void 0) {
2013
2323
  item = this.engine.registry.getItem(request.type, request.name);
2014
2324
  if (item === void 0) {
2015
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2325
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
2016
2326
  if (alt) item = this.engine.registry.getItem(alt, request.name);
2017
2327
  }
2018
2328
  }
2329
+ const artifactItem = this.lookupArtifactItem(request.type, request.name);
2330
+ const decorated = decorateMetadataItem(
2331
+ request.type,
2332
+ mergeArtifactProtection(item, artifactItem)
2333
+ );
2334
+ const artifactBacked = this.isArtifactBacked(request.type, request.name);
2335
+ const lockState = (0, import_kernel5.resolveLockState)(decorated, artifactBacked);
2019
2336
  return {
2020
2337
  type: request.type,
2021
2338
  name: request.name,
2022
- item
2339
+ item: decorated,
2340
+ lock: lockState.lock,
2341
+ ...lockState.lockReason !== void 0 ? { lockReason: lockState.lockReason } : {},
2342
+ ...lockState.lockSource !== void 0 ? { lockSource: lockState.lockSource } : {},
2343
+ ...lockState.provenance !== void 0 ? { provenance: lockState.provenance } : {},
2344
+ ...lockState.packageId !== void 0 ? { packageId: lockState.packageId } : {},
2345
+ ...lockState.packageVersion !== void 0 ? { packageVersion: lockState.packageVersion } : {},
2346
+ editable: lockState.editable,
2347
+ deletable: lockState.deletable,
2348
+ resettable: lockState.resettable
2023
2349
  };
2024
2350
  }
2025
2351
  /**
@@ -2045,7 +2371,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2045
2371
  if (metadataService && typeof metadataService.get === "function") {
2046
2372
  let fromService = await metadataService.get(request.type, request.name);
2047
2373
  if (fromService === void 0 || fromService === null) {
2048
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2374
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
2049
2375
  if (alt) fromService = await metadataService.get(alt, request.name);
2050
2376
  }
2051
2377
  if (fromService !== void 0 && fromService !== null) code = fromService;
@@ -2055,7 +2381,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2055
2381
  if (code === null) {
2056
2382
  let regItem = this.engine.registry.getItem(request.type, request.name);
2057
2383
  if (regItem === void 0) {
2058
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2384
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
2059
2385
  if (alt) regItem = this.engine.registry.getItem(alt, request.name);
2060
2386
  }
2061
2387
  if (regItem !== void 0) code = regItem;
@@ -2072,7 +2398,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2072
2398
  };
2073
2399
  let rec = await this.engine.findOne("sys_metadata", { where });
2074
2400
  if (!rec) {
2075
- const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2401
+ const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
2076
2402
  if (alt) {
2077
2403
  rec = await this.engine.findOne("sys_metadata", {
2078
2404
  where: { ...where, type: alt }
@@ -2098,15 +2424,80 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2098
2424
  } catch {
2099
2425
  }
2100
2426
  const effective = overlay ?? code;
2427
+ const _diagnostics = effective !== null && effective !== void 0 ? computeMetadataDiagnostics(request.type, effective) : void 0;
2428
+ const artifactBacked = this.isArtifactBacked(request.type, request.name);
2429
+ const lockSource = code ?? overlay ?? {};
2430
+ const lockState = (0, import_kernel5.resolveLockState)(lockSource, artifactBacked);
2101
2431
  return {
2102
2432
  type: request.type,
2103
2433
  name: request.name,
2104
2434
  code,
2105
2435
  overlay,
2106
2436
  overlayScope,
2107
- effective
2437
+ effective,
2438
+ ..._diagnostics ? { _diagnostics } : {},
2439
+ lock: lockState.lock,
2440
+ ...lockState.lockReason !== void 0 ? { lockReason: lockState.lockReason } : {},
2441
+ ...lockState.lockSource !== void 0 ? { lockSource: lockState.lockSource } : {},
2442
+ ...lockState.provenance !== void 0 ? { provenance: lockState.provenance } : {},
2443
+ ...lockState.packageId !== void 0 ? { packageId: lockState.packageId } : {},
2444
+ ...lockState.packageVersion !== void 0 ? { packageVersion: lockState.packageVersion } : {},
2445
+ editable: lockState.editable,
2446
+ deletable: lockState.deletable,
2447
+ resettable: lockState.resettable
2108
2448
  };
2109
2449
  }
2450
+ /**
2451
+ * ADR-0010 §3.6 / Phase 4.1 — read the metadata-protection audit log
2452
+ * for a single item. Returns the most-recent rows of
2453
+ * `sys_metadata_audit` for this (type, name) tuple, sorted newest
2454
+ * first. Refused (`denied`) and forced (`forced`) writes both appear
2455
+ * here — they never reach the `history` endpoint, which only tracks
2456
+ * successful body snapshots.
2457
+ *
2458
+ * The table is provisioned by `platform-objects` and is the
2459
+ * compliance surface for the lock-enforcement story. When the
2460
+ * environment has not yet provisioned the table (legacy install
2461
+ * prior to ADR-0010) the call returns `{ events: [] }` instead of
2462
+ * raising, keeping the Studio tab harmless.
2463
+ */
2464
+ async auditMetaItem(request) {
2465
+ const singular = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
2466
+ const limit = Math.min(
2467
+ Math.max(1, request.limit ?? 100),
2468
+ 500
2469
+ );
2470
+ try {
2471
+ const where = {
2472
+ type: singular,
2473
+ name: request.name
2474
+ };
2475
+ const rows = await this.engine.find("sys_metadata_audit", {
2476
+ where,
2477
+ orderBy: [{ field: "occurred_at", direction: "desc" }],
2478
+ limit
2479
+ });
2480
+ const events = (Array.isArray(rows) ? rows : []).map((r) => ({
2481
+ id: r.id,
2482
+ occurredAt: typeof r.occurred_at === "string" ? r.occurred_at : r.occurred_at instanceof Date ? r.occurred_at.toISOString() : String(r.occurred_at ?? ""),
2483
+ actor: String(r.actor ?? "system"),
2484
+ source: r.source ?? null,
2485
+ operation: r.operation,
2486
+ outcome: r.outcome,
2487
+ code: String(r.code ?? ""),
2488
+ lockState: r.lock_state ?? null,
2489
+ lockOverridden: Boolean(r.lock_overridden),
2490
+ requestId: r.request_id ?? null,
2491
+ note: r.note ?? null
2492
+ }));
2493
+ return { events };
2494
+ } catch (err) {
2495
+ console.warn(
2496
+ `[Protocol] auditMetaItem read failed for ${request.type}/${request.name}: ${err?.message ?? err}`
2497
+ );
2498
+ return { events: [] };
2499
+ }
2500
+ }
2110
2501
  async getUiView(request) {
2111
2502
  const schema = this.engine.registry.getObject(request.object);
2112
2503
  if (!schema) throw new Error(`Object ${request.object} not found`);
@@ -2985,9 +3376,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2985
3376
  for (const tok of raw.split(",")) {
2986
3377
  const t = tok.trim();
2987
3378
  if (!t) continue;
2988
- const singular = import_shared2.PLURAL_TO_SINGULAR[t] ?? t;
3379
+ const singular = import_shared3.PLURAL_TO_SINGULAR[t] ?? t;
2989
3380
  set.add(singular);
2990
- const plural = import_shared2.SINGULAR_TO_PLURAL[singular];
3381
+ const plural = import_shared3.SINGULAR_TO_PLURAL[singular];
2991
3382
  if (plural) set.add(plural);
2992
3383
  }
2993
3384
  this._envWritableTypes = set;
@@ -2999,13 +3390,201 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2999
3390
  }
3000
3391
  /** Normalize plural→singular before consulting the allow-list. */
3001
3392
  static isOverlayAllowed(type) {
3002
- const singular = import_shared2.PLURAL_TO_SINGULAR[type] ?? type;
3393
+ const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
3003
3394
  if (this.OVERLAY_ALLOWED_TYPES.has(singular) || this.OVERLAY_ALLOWED_TYPES.has(type)) {
3004
3395
  return true;
3005
3396
  }
3006
3397
  const env = this.envWritableTypes();
3007
3398
  return env.has(singular) || env.has(type);
3008
3399
  }
3400
+ /** Does this type permit creating brand-new (artifact-free) items? */
3401
+ static isRuntimeCreateAllowed(type) {
3402
+ const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
3403
+ if (this.RUNTIME_CREATE_ALLOWED_TYPES.has(singular) || this.RUNTIME_CREATE_ALLOWED_TYPES.has(type)) {
3404
+ return true;
3405
+ }
3406
+ if (!this.STATIC_REGISTRY_TYPES.has(singular) && !this.STATIC_REGISTRY_TYPES.has(type)) {
3407
+ return true;
3408
+ }
3409
+ return false;
3410
+ }
3411
+ /**
3412
+ * Does an artifact (npm-package-loaded) item exist at `(type, name)`?
3413
+ *
3414
+ * The schema registry's `_packageId` tag is set only when
3415
+ * `registerItem(..., packageId)` is called with a truthy packageId
3416
+ * — and only artifact loaders do that. DB-rehydrated items
3417
+ * (sys_metadata rows registered back into the registry by
3418
+ * `getMetaItems` / `loadMetaFromDb`) call `registerItem` without a
3419
+ * packageId, so they carry no `_packageId` and are correctly
3420
+ * excluded here.
3421
+ *
3422
+ * Used by the two-tier authorization model to distinguish
3423
+ * "overlaying a packaged item" (requires `allowOrgOverride`) from
3424
+ * "authoring a DB-only item" (requires only `allowRuntimeCreate`).
3425
+ */
3426
+ isArtifactBacked(type, name) {
3427
+ const registry = this.engine?.registry;
3428
+ if (!registry || typeof registry.getItem !== "function") {
3429
+ return false;
3430
+ }
3431
+ const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
3432
+ const item = registry.getItem(singular, name) ?? registry.getItem(type, name);
3433
+ if (!item || !item._packageId) return false;
3434
+ return item._packageId !== "sys_metadata";
3435
+ }
3436
+ // ───────────────────────────────────────────────────────────────────
3437
+ // ADR-0010 — metadata protection (Phase 1: L3 item-level lock)
3438
+ // ───────────────────────────────────────────────────────────────────
3439
+ /**
3440
+ * Look up an item from the artifact registry across both the requested
3441
+ * type and its singular/plural twin. Returns `undefined` when the
3442
+ * registry is unavailable or the item is not artifact-backed.
3443
+ */
3444
+ lookupArtifactItem(type, name) {
3445
+ const registry = this.engine?.registry;
3446
+ if (!registry || typeof registry.getItem !== "function") return void 0;
3447
+ const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
3448
+ return registry.getItem(singular, name) ?? registry.getItem(type, name);
3449
+ }
3450
+ /**
3451
+ * Resolve the effective `_lock` for an item by consulting the
3452
+ * artifact registry first, then the persisted overlay row. Artifact
3453
+ * always wins — by design, an overlay cannot loosen a packaged
3454
+ * lock (ADR-0010 §3.3).
3455
+ *
3456
+ * Returns `'none'` when nothing is locked, which is the common
3457
+ * case. Safe to call when `environmentId` is undefined (control-
3458
+ * plane bootstrap) — the lock check is only meaningful in tenant
3459
+ * scope and the caller is expected to also gate on `environmentId`.
3460
+ */
3461
+ async getEffectiveLock(type, name, organizationId) {
3462
+ const registry = this.engine?.registry;
3463
+ const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
3464
+ let artifactItem;
3465
+ if (registry && typeof registry.getItem === "function") {
3466
+ artifactItem = registry.getItem(singular, name) ?? registry.getItem(type, name);
3467
+ }
3468
+ if (artifactItem && artifactItem._packageId && artifactItem._packageId !== "sys_metadata") {
3469
+ const p = (0, import_kernel5.extractProtection)(artifactItem);
3470
+ if (p.lock !== "none") {
3471
+ return { lock: p.lock, lockReason: p.lockReason, lockSource: "artifact" };
3472
+ }
3473
+ }
3474
+ try {
3475
+ const where = {
3476
+ type,
3477
+ name,
3478
+ state: "active",
3479
+ organization_id: organizationId ?? null
3480
+ };
3481
+ const row = await this.engine.findOne("sys_metadata", { where });
3482
+ if (row) {
3483
+ const body = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
3484
+ const p = (0, import_kernel5.extractProtection)(body);
3485
+ if (p.lock !== "none") {
3486
+ return { lock: p.lock, lockReason: p.lockReason, lockSource: "overlay" };
3487
+ }
3488
+ }
3489
+ } catch {
3490
+ }
3491
+ return { lock: "none", lockReason: void 0, lockSource: void 0 };
3492
+ }
3493
+ /**
3494
+ * Best-effort audit-row writer (ADR-0010 §3.6). Failures here are
3495
+ * logged but never block the underlying decision: an environment
3496
+ * without the audit table provisioned (legacy installs before this
3497
+ * ADR landed) still answers normal API calls, just without the
3498
+ * compliance trail. Phase 2 will make the audit table a hard
3499
+ * dependency.
3500
+ */
3501
+ async recordMetadataAudit(entry) {
3502
+ try {
3503
+ await this.engine.insert("sys_metadata_audit", {
3504
+ occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
3505
+ actor: entry.actor ?? "system",
3506
+ source: entry.source ?? "protocol",
3507
+ type: import_shared3.PLURAL_TO_SINGULAR[entry.type] ?? entry.type,
3508
+ name: entry.name,
3509
+ organization_id: entry.organizationId ?? null,
3510
+ operation: entry.operation,
3511
+ outcome: entry.outcome,
3512
+ code: entry.code,
3513
+ lock_state: entry.lockState ?? "none",
3514
+ lock_overridden: entry.lockOverridden ?? false,
3515
+ request_id: entry.requestId ?? null,
3516
+ note: entry.note ?? null
3517
+ });
3518
+ } catch (err) {
3519
+ console.warn(
3520
+ `[Protocol] sys_metadata_audit write failed for ${entry.type}/${entry.name}: ${err?.message ?? err}`
3521
+ );
3522
+ }
3523
+ }
3524
+ /**
3525
+ * Phase 1 L3 enforcement for write operations (save / publish /
3526
+ * rollback). Returns null on allow. Returns the structured `Error`
3527
+ * the caller should `throw` on deny — also records the denial in
3528
+ * the audit log so refused attempts are visible in compliance
3529
+ * reports (refused writes never reach sys_metadata_history).
3530
+ */
3531
+ async assertLockAllowsWrite(args) {
3532
+ if (this.environmentId === void 0) return null;
3533
+ const state = await this.getEffectiveLock(args.type, args.name, args.organizationId ?? null);
3534
+ const refusal = (0, import_kernel5.evaluateLockForWrite)(state.lock);
3535
+ if (!refusal) return null;
3536
+ const reason = state.lockReason ?? refusal.reason;
3537
+ const err = new Error(
3538
+ `[item_locked] ${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ""}). ${reason} \u2014 See ADR-0010 \xA73.3.`
3539
+ );
3540
+ err.code = "item_locked";
3541
+ err.status = 403;
3542
+ err.lock = state.lock;
3543
+ err.lockReason = reason;
3544
+ await this.recordMetadataAudit({
3545
+ type: args.type,
3546
+ name: args.name,
3547
+ organizationId: args.organizationId ?? null,
3548
+ operation: args.operation,
3549
+ outcome: "denied",
3550
+ code: "item_locked",
3551
+ lockState: state.lock,
3552
+ actor: args.actor,
3553
+ source: args.source ?? `protocol.${args.operation}MetaItem`,
3554
+ requestId: args.requestId,
3555
+ note: reason
3556
+ });
3557
+ return err;
3558
+ }
3559
+ /** Counterpart of {@link assertLockAllowsWrite} for delete. */
3560
+ async assertLockAllowsDelete(args) {
3561
+ if (this.environmentId === void 0) return null;
3562
+ const state = await this.getEffectiveLock(args.type, args.name, args.organizationId ?? null);
3563
+ const refusal = (0, import_kernel5.evaluateLockForDelete)(state.lock);
3564
+ if (!refusal) return null;
3565
+ const reason = state.lockReason ?? refusal.reason;
3566
+ const err = new Error(
3567
+ `[item_locked] ${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ""}). ${reason} \u2014 See ADR-0010 \xA73.3.`
3568
+ );
3569
+ err.code = "item_locked";
3570
+ err.status = 403;
3571
+ err.lock = state.lock;
3572
+ err.lockReason = reason;
3573
+ await this.recordMetadataAudit({
3574
+ type: args.type,
3575
+ name: args.name,
3576
+ organizationId: args.organizationId ?? null,
3577
+ operation: "delete",
3578
+ outcome: "denied",
3579
+ code: "item_locked",
3580
+ lockState: state.lock,
3581
+ actor: args.actor,
3582
+ source: args.source ?? "protocol.deleteMetaItem",
3583
+ requestId: args.requestId,
3584
+ note: reason
3585
+ });
3586
+ return err;
3587
+ }
3009
3588
  /**
3010
3589
  * Mirror an object-type overlay write into the in-memory engine
3011
3590
  * registry so subsequent CRUD finds the new schema. Idempotent and
@@ -3031,16 +3610,38 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3031
3610
  if (!request.item) {
3032
3611
  throw new Error("Item data is required");
3033
3612
  }
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;
3613
+ const mode = request.mode === "draft" ? "draft" : "publish";
3614
+ if (this.environmentId !== void 0) {
3615
+ const overlayAllowed = _ObjectStackProtocolImplementation.isOverlayAllowed(request.type);
3616
+ const runtimeCreateAllowed = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(request.type);
3617
+ const artifactBacked = this.isArtifactBacked(request.type, request.name);
3618
+ if (artifactBacked && !overlayAllowed) {
3619
+ const err = new Error(
3620
+ `[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.`
3621
+ );
3622
+ err.code = "not_overridable";
3623
+ err.status = 403;
3624
+ throw err;
3625
+ }
3626
+ if (!artifactBacked && !overlayAllowed && !runtimeCreateAllowed) {
3627
+ const err = new Error(
3628
+ `[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.`
3629
+ );
3630
+ err.code = "not_creatable";
3631
+ err.status = 403;
3632
+ throw err;
3633
+ }
3634
+ const lockErr = await this.assertLockAllowsWrite({
3635
+ type: request.type,
3636
+ name: request.name,
3637
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3638
+ operation: "save",
3639
+ ...request.actor ? { actor: request.actor } : {},
3640
+ source: "protocol.saveMetaItem"
3641
+ });
3642
+ if (lockErr) throw lockErr;
3042
3643
  }
3043
- const singularType = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3644
+ const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3044
3645
  if (!request.force && (singularType === "object" || singularType === "field")) {
3045
3646
  try {
3046
3647
  const existing = await this.getMetaItem({
@@ -3088,8 +3689,13 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3088
3689
  }
3089
3690
  }
3090
3691
  await this.ensureOverlayIndex();
3091
- const singularTypeForRepo = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3092
- if (_ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo)) {
3692
+ const singularTypeForRepo = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3693
+ const overlayAllowedForRepo = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
3694
+ const runtimeCreateAllowedForRepo = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularTypeForRepo);
3695
+ const useRepoPath = overlayAllowedForRepo || runtimeCreateAllowedForRepo;
3696
+ if (useRepoPath) {
3697
+ const artifactBacked = this.isArtifactBacked(singularTypeForRepo, request.name);
3698
+ const intent = artifactBacked ? "override-artifact" : "runtime-only";
3093
3699
  const orgId = request.organizationId ?? null;
3094
3700
  const repo = this.getOverlayRepo(orgId);
3095
3701
  const ref = {
@@ -3101,21 +3707,37 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3101
3707
  if (request.parentVersion !== void 0) {
3102
3708
  parentVersion = request.parentVersion;
3103
3709
  } else {
3104
- const current = await repo.get(ref);
3710
+ const current = await repo.get(ref, { state: mode === "draft" ? "draft" : "active" });
3105
3711
  parentVersion = current?.hash ?? null;
3106
3712
  }
3107
3713
  try {
3108
3714
  const result = await repo.put(ref, request.item, {
3109
3715
  parentVersion,
3110
3716
  actor: request.actor ?? "system",
3111
- source: "protocol.saveMetaItem"
3717
+ source: "protocol.saveMetaItem",
3718
+ intent,
3719
+ state: mode === "draft" ? "draft" : "active"
3720
+ });
3721
+ if (mode === "publish") {
3722
+ this.applyObjectRegistryMutation(request);
3723
+ }
3724
+ await this.recordMetadataAudit({
3725
+ type: request.type,
3726
+ name: request.name,
3727
+ organizationId: orgId,
3728
+ operation: "save",
3729
+ outcome: "allowed",
3730
+ code: "ok",
3731
+ ...request.actor ? { actor: request.actor } : {},
3732
+ source: "protocol.saveMetaItem",
3733
+ note: mode === "draft" ? "draft" : "active"
3112
3734
  });
3113
- this.applyObjectRegistryMutation(request);
3114
3735
  return {
3115
3736
  success: true,
3116
3737
  version: result.version,
3117
3738
  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}]`
3739
+ state: mode === "draft" ? "draft" : "active",
3740
+ 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
3741
  };
3120
3742
  } catch (err) {
3121
3743
  if (err instanceof import_metadata_core2.ConflictError) {
@@ -3199,8 +3821,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3199
3821
  * "no history" uniformly.
3200
3822
  */
3201
3823
  async historyMetaItem(request) {
3202
- const singularType = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3203
- if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType)) {
3824
+ const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3825
+ if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
3204
3826
  return { events: [] };
3205
3827
  }
3206
3828
  const orgId = request.organizationId ?? null;
@@ -3218,22 +3840,264 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3218
3840
  return { events };
3219
3841
  }
3220
3842
  /**
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}.
3843
+ * Promote the pending draft overlay to the live (`active`) row.
3844
+ * Records a history event with `op='publish'`. 404 (`[no_draft]`)
3845
+ * when there is nothing to publish.
3225
3846
  */
3226
- async deleteMetaItem(request) {
3227
- if (this.environmentId !== void 0 && !_ObjectStackProtocolImplementation.isOverlayAllowed(request.type)) {
3847
+ async publishMetaItem(request) {
3848
+ const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3849
+ if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
3850
+ const err = new Error(
3851
+ `[not_overridable] Metadata type '${request.type}' is not draftable \u2014 no overlay/runtime-create permission.`
3852
+ );
3853
+ err.code = "not_overridable";
3854
+ err.status = 403;
3855
+ throw err;
3856
+ }
3857
+ const _publishLockErr = await this.assertLockAllowsWrite({
3858
+ type: request.type,
3859
+ name: request.name,
3860
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3861
+ operation: "publish",
3862
+ ...request.actor ? { actor: request.actor } : {},
3863
+ source: "protocol.publishMetaItem"
3864
+ });
3865
+ if (_publishLockErr) throw _publishLockErr;
3866
+ await this.ensureOverlayIndex();
3867
+ const orgId = request.organizationId ?? null;
3868
+ const repo = this.getOverlayRepo(orgId);
3869
+ const artifactBacked = this.isArtifactBacked(singularType, request.name);
3870
+ const intent = artifactBacked ? "override-artifact" : "runtime-only";
3871
+ const ref = {
3872
+ type: singularType,
3873
+ name: request.name,
3874
+ org: orgId ?? "env"
3875
+ };
3876
+ try {
3877
+ const result = await repo.promoteDraft(ref, {
3878
+ actor: request.actor ?? "system",
3879
+ source: "protocol.publishMetaItem",
3880
+ ...request.message ? { message: request.message } : {},
3881
+ intent
3882
+ });
3883
+ this.applyObjectRegistryMutation({
3884
+ type: request.type,
3885
+ name: request.name,
3886
+ item: result.item.body
3887
+ });
3888
+ return {
3889
+ success: true,
3890
+ version: result.version,
3891
+ seq: result.seq,
3892
+ message: `Published draft \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
3893
+ };
3894
+ } catch (err) {
3895
+ if (err instanceof import_metadata_core2.ConflictError) {
3896
+ const conflict = new Error(
3897
+ `[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"}.`
3898
+ );
3899
+ conflict.code = "metadata_conflict";
3900
+ conflict.status = 409;
3901
+ conflict.expectedParent = err.expectedParent;
3902
+ conflict.actualHead = err.actualHead;
3903
+ throw conflict;
3904
+ }
3905
+ throw err;
3906
+ }
3907
+ }
3908
+ /**
3909
+ * Restore the body recorded at history `toVersion` as the new
3910
+ * live row. Writes a history event with `op='revert'`. 404
3911
+ * (`[version_not_found]`) when the target version doesn't exist;
3912
+ * 409 (`[version_not_restorable]`) when the target is a delete
3913
+ * tombstone (no body to bring back).
3914
+ */
3915
+ async rollbackMetaItem(request) {
3916
+ if (!Number.isFinite(request.toVersion) || request.toVersion < 1) {
3917
+ const err = new Error(
3918
+ `[invalid_request] rollbackMetaItem requires a positive integer 'toVersion' (got ${request.toVersion}).`
3919
+ );
3920
+ err.code = "invalid_request";
3921
+ err.status = 400;
3922
+ throw err;
3923
+ }
3924
+ const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3925
+ if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
3228
3926
  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.`
3927
+ `[not_overridable] Metadata type '${request.type}' is not revertable \u2014 no overlay/runtime-create permission.`
3230
3928
  );
3231
3929
  err.code = "not_overridable";
3232
3930
  err.status = 403;
3233
3931
  throw err;
3234
3932
  }
3235
- const singularTypeForRepo = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3236
- const useRepoPath = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
3933
+ const _rollbackLockErr = await this.assertLockAllowsWrite({
3934
+ type: request.type,
3935
+ name: request.name,
3936
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3937
+ operation: "rollback",
3938
+ ...request.actor ? { actor: request.actor } : {},
3939
+ source: "protocol.rollbackMetaItem"
3940
+ });
3941
+ if (_rollbackLockErr) throw _rollbackLockErr;
3942
+ await this.ensureOverlayIndex();
3943
+ const orgId = request.organizationId ?? null;
3944
+ const repo = this.getOverlayRepo(orgId);
3945
+ const artifactBacked = this.isArtifactBacked(singularType, request.name);
3946
+ const intent = artifactBacked ? "override-artifact" : "runtime-only";
3947
+ const ref = {
3948
+ type: singularType,
3949
+ name: request.name,
3950
+ org: orgId ?? "env"
3951
+ };
3952
+ try {
3953
+ const result = await repo.restoreVersion(ref, request.toVersion, {
3954
+ actor: request.actor ?? "system",
3955
+ source: "protocol.rollbackMetaItem",
3956
+ ...request.message ? { message: request.message } : {},
3957
+ intent
3958
+ });
3959
+ this.applyObjectRegistryMutation({
3960
+ type: request.type,
3961
+ name: request.name,
3962
+ item: result.item.body
3963
+ });
3964
+ return {
3965
+ success: true,
3966
+ version: result.version,
3967
+ seq: result.seq,
3968
+ restoredFromVersion: request.toVersion,
3969
+ message: `Reverted to version ${request.toVersion} \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
3970
+ };
3971
+ } catch (err) {
3972
+ if (err instanceof import_metadata_core2.ConflictError) {
3973
+ const conflict = new Error(
3974
+ `[metadata_conflict] ${request.type}/${request.name} advanced during rollback. Expected parent ${err.expectedParent ?? "null"} but current is ${err.actualHead ?? "null"}.`
3975
+ );
3976
+ conflict.code = "metadata_conflict";
3977
+ conflict.status = 409;
3978
+ conflict.expectedParent = err.expectedParent;
3979
+ conflict.actualHead = err.actualHead;
3980
+ throw conflict;
3981
+ }
3982
+ throw err;
3983
+ }
3984
+ }
3985
+ /**
3986
+ * Compute a shallow structural diff between two historical
3987
+ * versions of a metadata item. Either side may be omitted: when
3988
+ * `toVersion` is undefined the current active body is used; when
3989
+ * `fromVersion` is undefined the immediately previous history row
3990
+ * is used. Returns `{ added, removed, changed }` keyed by JSON
3991
+ * pointer-style paths for primitive leaves; nested objects/arrays
3992
+ * are reported as a single change record.
3993
+ */
3994
+ async diffMetaItem(request) {
3995
+ const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3996
+ const orgId = request.organizationId ?? null;
3997
+ const events = (await this.historyMetaItem({
3998
+ type: singularType,
3999
+ name: request.name,
4000
+ ...orgId ? { organizationId: orgId } : {}
4001
+ })).events;
4002
+ const versions = events.map((ev) => ev.version).filter((v) => typeof v === "number");
4003
+ const repo = this.getOverlayRepo(orgId);
4004
+ const fullRef = {
4005
+ type: singularType,
4006
+ name: request.name,
4007
+ org: orgId ?? "env"
4008
+ };
4009
+ const histRows = [];
4010
+ try {
4011
+ const engineAny = this.engine;
4012
+ const rows = await engineAny.find("sys_metadata_history", {
4013
+ where: {
4014
+ organization_id: orgId,
4015
+ type: singularType,
4016
+ name: request.name
4017
+ }
4018
+ });
4019
+ rows.sort((a, b) => (a.version ?? 0) - (b.version ?? 0));
4020
+ for (const r of rows) {
4021
+ const body = r.metadata == null ? null : typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata;
4022
+ histRows.push({ version: r.version ?? 0, body });
4023
+ }
4024
+ } catch {
4025
+ }
4026
+ const byVersion = /* @__PURE__ */ new Map();
4027
+ for (const r of histRows) byVersion.set(r.version, r.body);
4028
+ let fromBody = null;
4029
+ let toBody = null;
4030
+ let fromVersion = null;
4031
+ let toVersion = null;
4032
+ if (request.toVersion !== void 0) {
4033
+ toVersion = request.toVersion;
4034
+ toBody = byVersion.get(request.toVersion) ?? null;
4035
+ } else {
4036
+ const current = await repo.get(fullRef, { state: "active" });
4037
+ toBody = current ? current.body : null;
4038
+ toVersion = histRows.length ? histRows[histRows.length - 1].version : null;
4039
+ }
4040
+ if (request.fromVersion !== void 0) {
4041
+ fromVersion = request.fromVersion;
4042
+ fromBody = byVersion.get(request.fromVersion) ?? null;
4043
+ } else if (toVersion !== null) {
4044
+ const sorted = histRows.map((r) => r.version).filter((v) => v < toVersion);
4045
+ if (sorted.length) {
4046
+ fromVersion = sorted[sorted.length - 1];
4047
+ fromBody = byVersion.get(fromVersion) ?? null;
4048
+ }
4049
+ }
4050
+ const diff = diffShallow(fromBody ?? {}, toBody ?? {});
4051
+ const _used = versions;
4052
+ void _used;
4053
+ return {
4054
+ type: request.type,
4055
+ name: request.name,
4056
+ fromVersion,
4057
+ toVersion,
4058
+ ...diff
4059
+ };
4060
+ }
4061
+ /**
4062
+ * Remove a customization overlay row for the given metadata item, so the
4063
+ * next read falls through to the artifact-loaded default. Implements the
4064
+ * "Reset to factory default" semantic from ADR-0005. Whitelist is shared
4065
+ * with {@link saveMetaItem}.
4066
+ */
4067
+ async deleteMetaItem(request) {
4068
+ if (this.environmentId !== void 0) {
4069
+ const overlayAllowed = _ObjectStackProtocolImplementation.isOverlayAllowed(request.type);
4070
+ const runtimeCreateAllowed = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(request.type);
4071
+ const artifactBacked = this.isArtifactBacked(request.type, request.name);
4072
+ if (artifactBacked && !overlayAllowed) {
4073
+ const err = new Error(
4074
+ `[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.`
4075
+ );
4076
+ err.code = "not_overridable";
4077
+ err.status = 403;
4078
+ throw err;
4079
+ }
4080
+ if (!artifactBacked && !overlayAllowed && !runtimeCreateAllowed) {
4081
+ const err = new Error(
4082
+ `[not_creatable] Metadata type '${request.type}' does not allow runtime creation or deletion.`
4083
+ );
4084
+ err.code = "not_creatable";
4085
+ err.status = 403;
4086
+ throw err;
4087
+ }
4088
+ const lockErr = await this.assertLockAllowsDelete({
4089
+ type: request.type,
4090
+ name: request.name,
4091
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
4092
+ ...request.actor ? { actor: request.actor } : {},
4093
+ source: "protocol.deleteMetaItem"
4094
+ });
4095
+ if (lockErr) throw lockErr;
4096
+ }
4097
+ const singularTypeForRepo = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
4098
+ const overlayAllowedForRepoDel = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
4099
+ const runtimeCreateAllowedForRepoDel = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularTypeForRepo);
4100
+ const useRepoPath = overlayAllowedForRepoDel || runtimeCreateAllowedForRepoDel;
3237
4101
  if (useRepoPath) {
3238
4102
  const orgId = request.organizationId ?? null;
3239
4103
  const repo = this.getOverlayRepo(orgId);
@@ -3243,19 +4107,22 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3243
4107
  org: orgId ?? "env"
3244
4108
  };
3245
4109
  try {
3246
- const current = await repo.get(ref);
4110
+ const targetState = request.state === "draft" ? "draft" : "active";
4111
+ const current = await repo.get(ref, { state: targetState });
3247
4112
  if (!current) {
3248
4113
  return {
3249
4114
  success: true,
3250
4115
  reset: false,
3251
- message: `No customization overlay found for ${request.type}/${request.name} \u2014 already at artifact default.`
4116
+ 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
4117
  };
3253
4118
  }
3254
4119
  const parentVersion = request.parentVersion !== void 0 ? request.parentVersion ?? current.hash : current.hash;
3255
4120
  const result = await repo.delete(ref, {
3256
4121
  parentVersion,
3257
4122
  actor: request.actor ?? "system",
3258
- source: "protocol.deleteMetaItem"
4123
+ source: "protocol.deleteMetaItem",
4124
+ intent: this.isArtifactBacked(singularTypeForRepo, request.name) ? "override-artifact" : "runtime-only",
4125
+ state: targetState
3259
4126
  });
3260
4127
  if (this.environmentId === void 0) {
3261
4128
  try {
@@ -3270,11 +4137,22 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3270
4137
  } catch {
3271
4138
  }
3272
4139
  }
4140
+ await this.recordMetadataAudit({
4141
+ type: request.type,
4142
+ name: request.name,
4143
+ organizationId: orgId,
4144
+ operation: "delete",
4145
+ outcome: "allowed",
4146
+ code: "ok",
4147
+ ...request.actor ? { actor: request.actor } : {},
4148
+ source: "protocol.deleteMetaItem",
4149
+ note: targetState
4150
+ });
3273
4151
  return {
3274
4152
  success: true,
3275
4153
  reset: true,
3276
4154
  seq: result.seq,
3277
- message: `Customization overlay deleted \u2014 ${request.type}/${request.name} reset to artifact default. [seq=${result.seq}]`
4155
+ 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
4156
  };
3279
4157
  } catch (err) {
3280
4158
  if (err instanceof import_metadata_core2.ConflictError) {
@@ -3352,7 +4230,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3352
4230
  for (const record of records) {
3353
4231
  try {
3354
4232
  const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
3355
- const normalizedType = import_shared2.PLURAL_TO_SINGULAR[record.type] ?? record.type;
4233
+ const normalizedType = import_shared3.PLURAL_TO_SINGULAR[record.type] ?? record.type;
3356
4234
  if (normalizedType === "object") {
3357
4235
  this.engine.registry.registerObject(data, record.packageId || "sys_metadata");
3358
4236
  } else {
@@ -3386,7 +4264,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3386
4264
  * — the engine never throws.
3387
4265
  */
3388
4266
  async findReferencesToMeta(request) {
3389
- const singularTarget = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
4267
+ const singularTarget = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3390
4268
  const targetName = request.name;
3391
4269
  const matchers = REFERENCE_PATHS[singularTarget];
3392
4270
  if (!matchers || matchers.length === 0) {
@@ -3577,10 +4455,10 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3577
4455
  */
3578
4456
  _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
3579
4457
  const out = /* @__PURE__ */ new Set();
3580
- for (const entry of import_kernel3.DEFAULT_METADATA_TYPE_REGISTRY) {
4458
+ for (const entry of import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY) {
3581
4459
  if (!entry.allowOrgOverride) continue;
3582
4460
  out.add(entry.type);
3583
- const plural = import_shared2.SINGULAR_TO_PLURAL[entry.type];
4461
+ const plural = import_shared3.SINGULAR_TO_PLURAL[entry.type];
3584
4462
  if (plural) out.add(plural);
3585
4463
  }
3586
4464
  return out;
@@ -3597,13 +4475,48 @@ _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
3597
4475
  * {@link ObjectStackProtocolImplementation.resetEnvWritableCache}.
3598
4476
  */
3599
4477
  _ObjectStackProtocolImplementation._envWritableTypes = null;
4478
+ /**
4479
+ * Types that opt into runtime creation of brand-new items (ADR-0005
4480
+ * extension — two-tier model). A type may have
4481
+ * `allowOrgOverride: false` (cannot overlay artifact-shipped items)
4482
+ * yet still set `allowRuntimeCreate: true` (users can author new
4483
+ * items in `sys_metadata`). The two flags are orthogonal; see
4484
+ * {@link isArtifactBacked} for how the protocol decides which gate
4485
+ * applies to a given save/delete.
4486
+ */
4487
+ /**
4488
+ * Set of type names that have a static entry in
4489
+ * `DEFAULT_METADATA_TYPE_REGISTRY`. Anything outside this set is
4490
+ * runtime-registered (plugin-provided types like `theme`, `api`,
4491
+ * `connector`) — the listing endpoint at `getMetaTypes()` synthesises
4492
+ * those with `allowRuntimeCreate: true`, so this gate must agree.
4493
+ */
4494
+ _ObjectStackProtocolImplementation.STATIC_REGISTRY_TYPES = (() => {
4495
+ const out = /* @__PURE__ */ new Set();
4496
+ for (const entry of import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY) {
4497
+ out.add(entry.type);
4498
+ const plural = import_shared3.SINGULAR_TO_PLURAL[entry.type];
4499
+ if (plural) out.add(plural);
4500
+ }
4501
+ return out;
4502
+ })();
4503
+ _ObjectStackProtocolImplementation.RUNTIME_CREATE_ALLOWED_TYPES = (() => {
4504
+ const out = /* @__PURE__ */ new Set();
4505
+ for (const entry of import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY) {
4506
+ if (!entry.allowRuntimeCreate) continue;
4507
+ out.add(entry.type);
4508
+ const plural = import_shared3.SINGULAR_TO_PLURAL[entry.type];
4509
+ if (plural) out.add(plural);
4510
+ }
4511
+ return out;
4512
+ })();
3600
4513
  var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
3601
4514
 
3602
4515
  // src/engine.ts
3603
- var import_kernel4 = require("@objectstack/spec/kernel");
4516
+ var import_kernel6 = require("@objectstack/spec/kernel");
3604
4517
  var import_core = require("@objectstack/core");
3605
4518
  var import_system2 = require("@objectstack/spec/system");
3606
- var import_shared3 = require("@objectstack/spec/shared");
4519
+ var import_shared4 = require("@objectstack/spec/shared");
3607
4520
  var import_formula2 = require("@objectstack/formula");
3608
4521
 
3609
4522
  // src/hook-wrappers.ts
@@ -4874,9 +5787,9 @@ var _ObjectQL = class _ObjectQL {
4874
5787
  const itemName = resolveMetadataItemName(key, item);
4875
5788
  if (itemName) {
4876
5789
  const toRegister = item.name === itemName ? item : { ...item, name: itemName };
4877
- this._registry.registerItem((0, import_shared3.pluralToSingular)(key), toRegister, "name", id);
5790
+ this._registry.registerItem((0, import_shared4.pluralToSingular)(key), toRegister, "name", id);
4878
5791
  } else {
4879
- this.logger.warn(`Skipping ${(0, import_shared3.pluralToSingular)(key)} without a derivable name`, { id });
5792
+ this.logger.warn(`Skipping ${(0, import_shared4.pluralToSingular)(key)} without a derivable name`, { id });
4880
5793
  }
4881
5794
  }
4882
5795
  }
@@ -5003,7 +5916,7 @@ var _ObjectQL = class _ObjectQL {
5003
5916
  const itemName = resolveMetadataItemName(key, item);
5004
5917
  if (itemName) {
5005
5918
  const toRegister = item.name === itemName ? item : { ...item, name: itemName };
5006
- this._registry.registerItem((0, import_shared3.pluralToSingular)(key), toRegister, "name", ownerId);
5919
+ this._registry.registerItem((0, import_shared4.pluralToSingular)(key), toRegister, "name", ownerId);
5007
5920
  }
5008
5921
  }
5009
5922
  }
@@ -5889,7 +6802,7 @@ var _ObjectQL = class _ObjectQL {
5889
6802
  */
5890
6803
  createContext(ctx) {
5891
6804
  return new ScopedContext(
5892
- import_kernel4.ExecutionContextSchema.parse(ctx),
6805
+ import_kernel6.ExecutionContextSchema.parse(ctx),
5893
6806
  this
5894
6807
  );
5895
6808
  }