@xnetjs/data 0.12.0 → 2.0.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.
Files changed (160) hide show
  1. package/dist/account-ZD3HMRY6.js +12 -0
  2. package/dist/{account-ledger-R2GAFJL6.js → account-ledger-ZHBNVREV.js} +3 -3
  3. package/dist/auth/index.d.ts +15 -5
  4. package/dist/auth/index.js +1 -1
  5. package/dist/budget-FTGZEXX6.js +12 -0
  6. package/dist/canvas-NQBTRNLS.js +10 -0
  7. package/dist/channel-474QAZGP.js +12 -0
  8. package/dist/chat-message-LGLCITLE.js +10 -0
  9. package/dist/checkpoint-MZHJ7PCZ.js +12 -0
  10. package/dist/{chunk-VQ7JHB67.js → chunk-22FKOLMX.js} +2 -2
  11. package/dist/{chunk-LYSWLCOI.js → chunk-26HTTVJN.js} +2 -2
  12. package/dist/{chunk-PH3PVAR6.js → chunk-3PV6QTKF.js} +2 -2
  13. package/dist/{chunk-KQUALW4O.js → chunk-4KJ4NAMC.js} +1 -1
  14. package/dist/{chunk-JFMLCFZJ.js → chunk-4S73XDWH.js} +585 -230
  15. package/dist/{chunk-U64CW73O.js → chunk-52DR23IW.js} +2 -2
  16. package/dist/{chunk-SVNGSZZA.js → chunk-57UZYV3L.js} +2 -2
  17. package/dist/{chunk-QVQVKZ3U.js → chunk-5UJMY3AH.js} +6 -5
  18. package/dist/chunk-6ZUJID52.js +41 -0
  19. package/dist/{chunk-3J3HILXO.js → chunk-7PCMTV6J.js} +2 -2
  20. package/dist/{chunk-YSUIXD2Y.js → chunk-C4CQH7UD.js} +2 -2
  21. package/dist/{chunk-PNESGUH5.js → chunk-D4EVJN2P.js} +2 -2
  22. package/dist/{chunk-RL64OJJ5.js → chunk-D4LUUZYD.js} +36 -9
  23. package/dist/{chunk-Q67VT5XL.js → chunk-DESSPQBS.js} +59 -10
  24. package/dist/{chunk-W7EBL7ZU.js → chunk-DP4Q2HMO.js} +2 -2
  25. package/dist/{chunk-5EXHNXPL.js → chunk-DPJ4T2LQ.js} +2 -2
  26. package/dist/{chunk-BQLQSWKZ.js → chunk-EN6XTVLZ.js} +2 -2
  27. package/dist/{chunk-TORDOOJ5.js → chunk-EYRZ6ZUB.js} +1 -1
  28. package/dist/{chunk-NI4FHG2K.js → chunk-EZTBWWMS.js} +2 -2
  29. package/dist/{chunk-UQM3G5A2.js → chunk-FWCM2TKJ.js} +2 -2
  30. package/dist/{chunk-OSAWNZVM.js → chunk-GCTONBF6.js} +1 -1
  31. package/dist/{chunk-2ZNJWIQG.js → chunk-HPUXHSKR.js} +2 -2
  32. package/dist/{chunk-ZPZ7XDA6.js → chunk-HTZTBXKL.js} +2 -2
  33. package/dist/{chunk-MCKP4F72.js → chunk-I7JMGCV6.js} +26 -5
  34. package/dist/{chunk-OGJCRNGE.js → chunk-IA23CLGV.js} +2 -2
  35. package/dist/{chunk-YEFKQYKA.js → chunk-IKGUBSZP.js} +2 -2
  36. package/dist/{chunk-XMSKJ5PV.js → chunk-IKI5FBNO.js} +2 -2
  37. package/dist/{chunk-OCMSAKWV.js → chunk-JA56EQJO.js} +15 -3
  38. package/dist/{chunk-TCTZW4A6.js → chunk-JLUFPUIL.js} +2 -2
  39. package/dist/{chunk-WEPK7SZF.js → chunk-KTPSQJ5L.js} +2 -2
  40. package/dist/{chunk-T5AZAOG5.js → chunk-LK5ZIAIE.js} +1 -1
  41. package/dist/{chunk-XROI44ZP.js → chunk-M4KGY2PC.js} +2 -2
  42. package/dist/chunk-MEAIGK3W.js +58 -0
  43. package/dist/{chunk-QJW5LDP4.js → chunk-NSMSOIEW.js} +2 -2
  44. package/dist/{chunk-FB63TZVI.js → chunk-OF6455OV.js} +2 -2
  45. package/dist/{chunk-6OMG4I7M.js → chunk-OIWW5TG3.js} +2 -2
  46. package/dist/{chunk-6KLGA6IR.js → chunk-Q6VTTPG4.js} +2 -2
  47. package/dist/{chunk-J4ANIXIV.js → chunk-QDLWTOII.js} +2 -2
  48. package/dist/{chunk-EBNMV2VO.js → chunk-RRXJNZX5.js} +1 -1
  49. package/dist/{chunk-FVEACSKE.js → chunk-RXBLWQQI.js} +2 -2
  50. package/dist/{chunk-L7NRPLE5.js → chunk-S6JWYPVW.js} +2 -2
  51. package/dist/{chunk-JDFNMFOZ.js → chunk-SOXXBQ4P.js} +2 -2
  52. package/dist/{chunk-4EYKGTSQ.js → chunk-T45PWXNA.js} +2 -2
  53. package/dist/{chunk-MD553LDL.js → chunk-TRPXLCND.js} +2 -2
  54. package/dist/{chunk-ZMZSBJBC.js → chunk-U223J425.js} +174 -170
  55. package/dist/{chunk-ZCOFZY5M.js → chunk-UUQVNDBG.js} +2 -2
  56. package/dist/{chunk-53F4PRNC.js → chunk-W3QOEPUN.js} +2 -2
  57. package/dist/{chunk-S6U6TCMN.js → chunk-WC7EDJHC.js} +2 -2
  58. package/dist/{chunk-KQAT4XBL.js → chunk-WWQO7QKV.js} +2 -2
  59. package/dist/{chunk-BY5O6LNC.js → chunk-XJBDESCX.js} +1 -1
  60. package/dist/{chunk-ZZ6TWKGS.js → chunk-XQDXQ7WG.js} +2 -2
  61. package/dist/{chunk-MBTUO3ZL.js → chunk-YD2ZPFT7.js} +2 -2
  62. package/dist/{chunk-ZLU4Y3O2.js → chunk-YHO7GNZK.js} +2 -2
  63. package/dist/{chunk-Y3S5SRVM.js → chunk-YSD4NSCE.js} +2 -2
  64. package/dist/chunk-Z5I2T75Z.js +70 -0
  65. package/dist/{chunk-IJ44LR7N.js → chunk-ZKJXGQCR.js} +2 -2
  66. package/dist/{chunk-GU6THOAB.js → chunk-ZKMWJNDQ.js} +2 -2
  67. package/dist/{chunk-OPPHF3TF.js → chunk-ZKVRGKLM.js} +3 -3
  68. package/dist/{clone-CbmoWwu4.d.ts → clone-DNcpkYAA.d.ts} +2 -2
  69. package/dist/comment-IAZJHG6G.js +10 -0
  70. package/dist/{crm-L5RYAAF3.js → crm-AFJSSPHK.js} +5 -5
  71. package/dist/dashboard-2DLFZM2J.js +10 -0
  72. package/dist/database/index.d.ts +3 -3
  73. package/dist/database/index.js +11 -11
  74. package/dist/database-5M7ZA33U.js +10 -0
  75. package/dist/database-field-IFUWOK7X.js +10 -0
  76. package/dist/database-row-C52KCOPB.js +10 -0
  77. package/dist/database-select-option-AEAPKXEE.js +10 -0
  78. package/dist/database-view-GUGJHS7J.js +10 -0
  79. package/dist/debug-report-BTNZAKX3.js +10 -0
  80. package/dist/draft-KGHITKX2.js +10 -0
  81. package/dist/experiment-JXHENWXV.js +10 -0
  82. package/dist/{external-item-P2E5X7BZ.js → external-item-ZVD33IXV.js} +5 -5
  83. package/dist/external-reference-LGFNVEKC.js +8 -0
  84. package/dist/feed-CLEHVA6Q.js +12 -0
  85. package/dist/feed-item-QXRIE6FU.js +12 -0
  86. package/dist/{folder-6KM3KYD5.js → folder-NU5TJFOT.js} +3 -3
  87. package/dist/{game-5AK5XWBR.js → game-7WW2JX6O.js} +5 -5
  88. package/dist/grant-W772T6FE.js +8 -0
  89. package/dist/{grant-expiration-cleaner-Bm1fSG42.d.ts → grant-expiration-cleaner-1by9wRlb.d.ts} +7 -2
  90. package/dist/import-batch-ZRU4FY6Y.js +12 -0
  91. package/dist/{inbox-state-E5NSVOGP.js → inbox-state-IO5C7HVE.js} +3 -3
  92. package/dist/index.d.ts +8 -7
  93. package/dist/index.js +175 -157
  94. package/dist/map-BZAVGCR6.js +10 -0
  95. package/dist/media-asset-VHFL65HI.js +8 -0
  96. package/dist/{meeting-LMUYTZBF.js → meeting-PVNKBGTI.js} +5 -5
  97. package/dist/{memory-PNRTMBZO.js → memory-5PD3XCKB.js} +3 -3
  98. package/dist/metric-6IGGFJMB.js +10 -0
  99. package/dist/milestone-MZAZCWGR.js +12 -0
  100. package/dist/{moderation-FFHETMRB.js → moderation-CXQB77CP.js} +3 -3
  101. package/dist/observation-O3DLKVIH.js +10 -0
  102. package/dist/page-UE5NPYEF.js +10 -0
  103. package/dist/posting-5VZZMOKS.js +12 -0
  104. package/dist/{profile-XBPZQA3C.js → profile-USKJ6PN7.js} +3 -3
  105. package/dist/project-UHGGEFLP.js +10 -0
  106. package/dist/{query-ast-Bvg0Wolx.d.ts → query-ast-NV5StnHo.d.ts} +1 -1
  107. package/dist/reaction-KD3G75UH.js +10 -0
  108. package/dist/{registry-_49rC6IG.d.ts → registry-s1fYgCSj.d.ts} +1 -1
  109. package/dist/saved-view-AZTUINZA.js +10 -0
  110. package/dist/schema/index.d.ts +173 -12
  111. package/dist/schema/index.js +96 -80
  112. package/dist/{schema-extension-LNRF2YRD.js → schema-extension-DV5D7JCQ.js} +5 -5
  113. package/dist/{space-IJU24PBH.js → space-THD2MWHR.js} +6 -6
  114. package/dist/{space-membership-DEUMM7NV.js → space-membership-M3QFMONE.js} +3 -3
  115. package/dist/store/index.d.ts +18 -5
  116. package/dist/store/index.js +4 -4
  117. package/dist/{store-DEWnip2G.d.ts → store-BIDKtpjW.d.ts} +56 -1
  118. package/dist/{system-ILHCZOVO.js → system-LFIMY3QQ.js} +3 -3
  119. package/dist/{tag-SKCHM3U5.js → tag-W7ZYPXHK.js} +3 -3
  120. package/dist/{task-7AOBP7WC.js → task-DHTPD66H.js} +5 -5
  121. package/dist/task-view-J2WRE62C.js +10 -0
  122. package/dist/transaction-ANNDXCJM.js +12 -0
  123. package/dist/transcription-DFTQ4FQ4.js +12 -0
  124. package/dist/{types-BB8-SLEa.d.ts → types-C64g-IXg.d.ts} +87 -2
  125. package/dist/user-widget-5IRVORF3.js +8 -0
  126. package/dist/workspace-RC4V4IF3.js +8 -0
  127. package/package.json +7 -7
  128. package/dist/account-MWNCH4GJ.js +0 -12
  129. package/dist/budget-NN7P35XP.js +0 -12
  130. package/dist/canvas-6WYBNH53.js +0 -10
  131. package/dist/channel-AT3LQF4A.js +0 -12
  132. package/dist/chat-message-47LETTYF.js +0 -10
  133. package/dist/comment-NIMFL4XB.js +0 -10
  134. package/dist/dashboard-JMKTTRA7.js +0 -10
  135. package/dist/database-GIXSPZJW.js +0 -10
  136. package/dist/database-field-3EPAMZUR.js +0 -10
  137. package/dist/database-row-5MEIOJ52.js +0 -10
  138. package/dist/database-select-option-BBBJFLLH.js +0 -10
  139. package/dist/database-view-DM7YCTZT.js +0 -10
  140. package/dist/experiment-DOVOZ34Y.js +0 -10
  141. package/dist/external-reference-7BSFF6SA.js +0 -8
  142. package/dist/feed-2IXHHP6O.js +0 -12
  143. package/dist/feed-item-DPE6LN7I.js +0 -12
  144. package/dist/grant-WTQSNT5A.js +0 -8
  145. package/dist/import-batch-IXBJLQPJ.js +0 -12
  146. package/dist/map-RG5Y3CPC.js +0 -10
  147. package/dist/media-asset-QTCGLIZA.js +0 -8
  148. package/dist/metric-65SVOX5C.js +0 -10
  149. package/dist/milestone-XEO6ZGDG.js +0 -12
  150. package/dist/observation-2Y3QKUXM.js +0 -10
  151. package/dist/page-2V27JGKJ.js +0 -10
  152. package/dist/posting-GBOSZCMZ.js +0 -12
  153. package/dist/project-CQL5LN26.js +0 -10
  154. package/dist/reaction-T2DTJBBV.js +0 -10
  155. package/dist/saved-view-4R2DBCL3.js +0 -10
  156. package/dist/task-view-NUGKELJP.js +0 -10
  157. package/dist/transaction-ZDSEGDIR.js +0 -12
  158. package/dist/transcription-PSCLDAGX.js +0 -12
  159. package/dist/user-widget-RLY2N46A.js +0 -8
  160. package/dist/workspace-WDKSQMOK.js +0 -8
@@ -8,11 +8,11 @@ import {
8
8
  SYSTEM_SCHEMA_BASE_IRIS,
9
9
  isSystemNamespaceResource,
10
10
  isSystemSchemaIri
11
- } from "./chunk-BY5O6LNC.js";
11
+ } from "./chunk-XJBDESCX.js";
12
12
  import {
13
13
  createNodeId,
14
14
  getBaseSchemaIRI
15
- } from "./chunk-EBNMV2VO.js";
15
+ } from "./chunk-RRXJNZX5.js";
16
16
 
17
17
  // src/store/query-ast.ts
18
18
  var QUERY_AST_VERSION = 1;
@@ -887,187 +887,23 @@ function evaluateQueryASTPlannerGate(value) {
887
887
  }
888
888
 
889
889
  // src/store/store.ts
890
- import { compareChangeApplicationOrder, lwwWins } from "@xnetjs/core";
891
-
892
- // src/store/transaction-executor.ts
893
- async function executeTransactionOperations(host, input) {
894
- const results = [];
895
- const changes = [];
896
- const events = [];
897
- for (let i = 0; i < input.operations.length; i++) {
898
- const op = input.operations[i];
899
- let change;
900
- let result = null;
901
- let previousNode = null;
902
- switch (op.type) {
903
- case "create": {
904
- const id = op.options.id ?? createNodeId();
905
- const payload = {
906
- nodeId: id,
907
- schemaId: op.options.schemaId,
908
- properties: op.options.properties
909
- };
910
- change = await host.createBatchedChange(
911
- "node-change",
912
- payload,
913
- input.lamport,
914
- input.now,
915
- input.batchId,
916
- i,
917
- input.batchSize,
918
- input.storage
919
- );
920
- await host.applyChange(change, input.storage);
921
- result = await input.storage.getNode(id);
922
- await host.persistEncryptedNodeSnapshot(result, input.storage);
923
- break;
924
- }
925
- case "update": {
926
- const existing = host.cloneNodeState(await input.storage.getNode(op.nodeId));
927
- if (!existing) {
928
- throw new Error(`Node not found: ${op.nodeId}`);
929
- }
930
- previousNode = existing;
931
- const payload = {
932
- nodeId: op.nodeId,
933
- properties: op.options.properties
934
- };
935
- change = await host.createBatchedChange(
936
- "node-change",
937
- payload,
938
- input.lamport,
939
- input.now,
940
- input.batchId,
941
- i,
942
- input.batchSize,
943
- input.storage
944
- );
945
- await host.applyChange(change, input.storage);
946
- result = await input.storage.getNode(op.nodeId);
947
- await host.persistEncryptedNodeSnapshot(result, input.storage);
948
- break;
949
- }
950
- case "delete": {
951
- const existing = host.cloneNodeState(await input.storage.getNode(op.nodeId));
952
- if (!existing) {
953
- throw new Error(`Node not found: ${op.nodeId}`);
954
- }
955
- previousNode = existing;
956
- const payload = {
957
- nodeId: op.nodeId,
958
- properties: {},
959
- deleted: true
960
- };
961
- change = await host.createBatchedChange(
962
- "node-change",
963
- payload,
964
- input.lamport,
965
- input.now,
966
- input.batchId,
967
- i,
968
- input.batchSize,
969
- input.storage
970
- );
971
- await host.applyChange(change, input.storage);
972
- result = null;
973
- break;
974
- }
975
- case "restore": {
976
- const existing = host.cloneNodeState(await input.storage.getNode(op.nodeId));
977
- if (!existing) {
978
- throw new Error(`Node not found: ${op.nodeId}`);
979
- }
980
- previousNode = existing;
981
- const payload = {
982
- nodeId: op.nodeId,
983
- properties: {},
984
- deleted: false
985
- };
986
- change = await host.createBatchedChange(
987
- "node-change",
988
- payload,
989
- input.lamport,
990
- input.now,
991
- input.batchId,
992
- i,
993
- input.batchSize,
994
- input.storage
995
- );
996
- await host.applyChange(change, input.storage);
997
- result = await input.storage.getNode(op.nodeId);
998
- await host.persistEncryptedNodeSnapshot(result, input.storage);
999
- break;
1000
- }
1001
- }
1002
- changes.push(change);
1003
- results.push(result);
1004
- events.push({ change, result, previousNode });
1005
- }
1006
- return { results, changes, events };
1007
- }
1008
- async function executeTransactionOperationsFast(host, input) {
1009
- const planned = input.operations.map((op) => ({
1010
- op,
1011
- id: op.type === "create" ? op.options.id ?? createNodeId() : op.nodeId
1012
- }));
1013
- const preflight = await host.getBatchPreflight(
1014
- planned.map((entry) => entry.id),
1015
- host.storage
1016
- );
1017
- const nodesById = host.cloneNodeMap(preflight.nodesById);
1018
- const lastChanges = new Map(preflight.lastChangesByNodeId);
1019
- const results = [];
1020
- const changes = [];
1021
- const events = [];
1022
- const affectedSchemaIds = /* @__PURE__ */ new Set();
1023
- for (let i = 0; i < planned.length; i++) {
1024
- const { op, id } = planned[i];
1025
- const existing = nodesById.get(id) ?? null;
1026
- if (op.type !== "create" && !existing) {
1027
- throw new Error(`Node not found: ${id}`);
1028
- }
1029
- const payload = op.type === "create" ? { nodeId: id, schemaId: op.options.schemaId, properties: op.options.properties } : op.type === "update" ? { nodeId: id, properties: op.options.properties } : { nodeId: id, properties: {}, deleted: op.type === "delete" };
1030
- const change = await host.createBatchedChangeWithParentHash(
1031
- "node-change",
1032
- payload,
1033
- lastChanges.get(id)?.hash ?? null,
1034
- input.lamport,
1035
- input.now,
1036
- input.batchId,
1037
- i,
1038
- input.batchSize
1039
- );
1040
- const schemaId = existing?.schemaId ?? (op.type === "create" ? op.options.schemaId : null);
1041
- if (!schemaId) {
1042
- throw new Error(`First change for node ${id} must include schemaId`);
1043
- }
1044
- const node = host.materializeNodeChange(
1045
- change,
1046
- existing ?? host.createInitialNodeFromChange(change, schemaId)
1047
- );
1048
- nodesById.set(id, node);
1049
- lastChanges.set(id, change);
1050
- affectedSchemaIds.add(schemaId);
1051
- changes.push(change);
1052
- const result = op.type === "delete" ? null : node;
1053
- results.push(result);
1054
- events.push({ change, result, previousNode: existing });
1055
- }
1056
- const touchedIds = Array.from(new Set(planned.map((entry) => entry.id)));
1057
- await host.storage.applyNodeBatch({
1058
- batchId: input.batchId,
1059
- nodes: touchedIds.flatMap((id) => {
1060
- const node = nodesById.get(id);
1061
- return node ? [node] : [];
1062
- }),
1063
- changes,
1064
- lastLamportTime: host.clockTime(),
1065
- affectedSchemaIds: Array.from(affectedSchemaIds),
1066
- indexMode: "touched",
1067
- indexProperties: true
1068
- });
1069
- return { results, changes, events };
1070
- }
890
+ import {
891
+ LWW_TIEBREAK_KEY_VERSION,
892
+ compareChangeApplicationOrder,
893
+ computeLwwTiebreakKey,
894
+ lwwWins
895
+ } from "@xnetjs/core";
896
+ import { base64ToBytes, bytesToBase64 } from "@xnetjs/crypto";
897
+ import { parseDID } from "@xnetjs/identity";
898
+ import {
899
+ createLamportClock,
900
+ tick,
901
+ receive,
902
+ signChange,
903
+ createUnsignedChange,
904
+ createBatchId
905
+ } from "@xnetjs/sync";
906
+ import { verifyChange, verifyChangeHash } from "@xnetjs/sync";
1071
907
 
1072
908
  // src/store/batch-write-orchestrator.ts
1073
909
  var elapsedMs = (startedAt) => Math.max(0, Date.now() - startedAt);
@@ -1159,19 +995,6 @@ async function executeDeterministicNodeImport(host, input) {
1159
995
  };
1160
996
  }
1161
997
 
1162
- // src/store/store.ts
1163
- import { base64ToBytes, bytesToBase64 } from "@xnetjs/crypto";
1164
- import { parseDID } from "@xnetjs/identity";
1165
- import {
1166
- createLamportClock,
1167
- tick,
1168
- receive,
1169
- signChange,
1170
- createUnsignedChange,
1171
- createBatchId
1172
- } from "@xnetjs/sync";
1173
- import { verifyChange, verifyChangeHash } from "@xnetjs/sync";
1174
-
1175
998
  // src/store/permission-error.ts
1176
999
  import { TaggedError } from "@xnetjs/core";
1177
1000
  var PermissionError = class extends TaggedError {
@@ -1364,6 +1187,186 @@ function findSchemaIdForNode(operations, nodeId, tempIds) {
1364
1187
  return void 0;
1365
1188
  }
1366
1189
 
1190
+ // src/store/transaction-executor.ts
1191
+ async function executeTransactionOperations(host, input) {
1192
+ const results = [];
1193
+ const changes = [];
1194
+ const events = [];
1195
+ for (let i = 0; i < input.operations.length; i++) {
1196
+ const op = input.operations[i];
1197
+ let change;
1198
+ let result = null;
1199
+ let previousNode = null;
1200
+ switch (op.type) {
1201
+ case "create": {
1202
+ const id = op.options.id ?? createNodeId();
1203
+ const payload = {
1204
+ nodeId: id,
1205
+ schemaId: op.options.schemaId,
1206
+ properties: op.options.properties
1207
+ };
1208
+ change = await host.createBatchedChange(
1209
+ "node-change",
1210
+ payload,
1211
+ input.lamport,
1212
+ input.now,
1213
+ input.batchId,
1214
+ i,
1215
+ input.batchSize,
1216
+ input.storage
1217
+ );
1218
+ await host.applyChange(change, input.storage);
1219
+ result = await input.storage.getNode(id);
1220
+ await host.persistEncryptedNodeSnapshot(result, input.storage);
1221
+ break;
1222
+ }
1223
+ case "update": {
1224
+ const existing = host.cloneNodeState(await input.storage.getNode(op.nodeId));
1225
+ if (!existing) {
1226
+ throw new Error(`Node not found: ${op.nodeId}`);
1227
+ }
1228
+ previousNode = existing;
1229
+ const payload = {
1230
+ nodeId: op.nodeId,
1231
+ properties: op.options.properties
1232
+ };
1233
+ change = await host.createBatchedChange(
1234
+ "node-change",
1235
+ payload,
1236
+ input.lamport,
1237
+ input.now,
1238
+ input.batchId,
1239
+ i,
1240
+ input.batchSize,
1241
+ input.storage
1242
+ );
1243
+ await host.applyChange(change, input.storage);
1244
+ result = await input.storage.getNode(op.nodeId);
1245
+ await host.persistEncryptedNodeSnapshot(result, input.storage);
1246
+ break;
1247
+ }
1248
+ case "delete": {
1249
+ const existing = host.cloneNodeState(await input.storage.getNode(op.nodeId));
1250
+ if (!existing) {
1251
+ throw new Error(`Node not found: ${op.nodeId}`);
1252
+ }
1253
+ previousNode = existing;
1254
+ const payload = {
1255
+ nodeId: op.nodeId,
1256
+ properties: {},
1257
+ deleted: true
1258
+ };
1259
+ change = await host.createBatchedChange(
1260
+ "node-change",
1261
+ payload,
1262
+ input.lamport,
1263
+ input.now,
1264
+ input.batchId,
1265
+ i,
1266
+ input.batchSize,
1267
+ input.storage
1268
+ );
1269
+ await host.applyChange(change, input.storage);
1270
+ result = null;
1271
+ break;
1272
+ }
1273
+ case "restore": {
1274
+ const existing = host.cloneNodeState(await input.storage.getNode(op.nodeId));
1275
+ if (!existing) {
1276
+ throw new Error(`Node not found: ${op.nodeId}`);
1277
+ }
1278
+ previousNode = existing;
1279
+ const payload = {
1280
+ nodeId: op.nodeId,
1281
+ properties: {},
1282
+ deleted: false
1283
+ };
1284
+ change = await host.createBatchedChange(
1285
+ "node-change",
1286
+ payload,
1287
+ input.lamport,
1288
+ input.now,
1289
+ input.batchId,
1290
+ i,
1291
+ input.batchSize,
1292
+ input.storage
1293
+ );
1294
+ await host.applyChange(change, input.storage);
1295
+ result = await input.storage.getNode(op.nodeId);
1296
+ await host.persistEncryptedNodeSnapshot(result, input.storage);
1297
+ break;
1298
+ }
1299
+ }
1300
+ changes.push(change);
1301
+ results.push(result);
1302
+ events.push({ change, result, previousNode });
1303
+ }
1304
+ return { results, changes, events };
1305
+ }
1306
+ async function executeTransactionOperationsFast(host, input) {
1307
+ const planned = input.operations.map((op) => ({
1308
+ op,
1309
+ id: op.type === "create" ? op.options.id ?? createNodeId() : op.nodeId
1310
+ }));
1311
+ const preflight = await host.getBatchPreflight(
1312
+ planned.map((entry) => entry.id),
1313
+ host.storage
1314
+ );
1315
+ const nodesById = host.cloneNodeMap(preflight.nodesById);
1316
+ const lastChanges = new Map(preflight.lastChangesByNodeId);
1317
+ const results = [];
1318
+ const changes = [];
1319
+ const events = [];
1320
+ const affectedSchemaIds = /* @__PURE__ */ new Set();
1321
+ for (let i = 0; i < planned.length; i++) {
1322
+ const { op, id } = planned[i];
1323
+ const existing = nodesById.get(id) ?? null;
1324
+ if (op.type !== "create" && !existing) {
1325
+ throw new Error(`Node not found: ${id}`);
1326
+ }
1327
+ const payload = op.type === "create" ? { nodeId: id, schemaId: op.options.schemaId, properties: op.options.properties } : op.type === "update" ? { nodeId: id, properties: op.options.properties } : { nodeId: id, properties: {}, deleted: op.type === "delete" };
1328
+ const change = await host.createBatchedChangeWithParentHash(
1329
+ "node-change",
1330
+ payload,
1331
+ lastChanges.get(id)?.hash ?? null,
1332
+ input.lamport,
1333
+ input.now,
1334
+ input.batchId,
1335
+ i,
1336
+ input.batchSize
1337
+ );
1338
+ const schemaId = existing?.schemaId ?? (op.type === "create" ? op.options.schemaId : null);
1339
+ if (!schemaId) {
1340
+ throw new Error(`First change for node ${id} must include schemaId`);
1341
+ }
1342
+ const node = host.materializeNodeChange(
1343
+ change,
1344
+ existing ?? host.createInitialNodeFromChange(change, schemaId)
1345
+ );
1346
+ nodesById.set(id, node);
1347
+ lastChanges.set(id, change);
1348
+ affectedSchemaIds.add(schemaId);
1349
+ changes.push(change);
1350
+ const result = op.type === "delete" ? null : node;
1351
+ results.push(result);
1352
+ events.push({ change, result, previousNode: existing });
1353
+ }
1354
+ const touchedIds = Array.from(new Set(planned.map((entry) => entry.id)));
1355
+ await host.storage.applyNodeBatch({
1356
+ batchId: input.batchId,
1357
+ nodes: touchedIds.flatMap((id) => {
1358
+ const node = nodesById.get(id);
1359
+ return node ? [node] : [];
1360
+ }),
1361
+ changes,
1362
+ lastLamportTime: host.clockTime(),
1363
+ affectedSchemaIds: Array.from(affectedSchemaIds),
1364
+ indexMode: "touched",
1365
+ indexProperties: true
1366
+ });
1367
+ return { results, changes, events };
1368
+ }
1369
+
1367
1370
  // src/store/store.ts
1368
1371
  var MAX_CONFLICTS = 200;
1369
1372
  var ENCRYPTED_NODE_MARKER = "xnet:node-encrypted:v1";
@@ -1390,6 +1393,14 @@ var NodeStore = class {
1390
1393
  listeners = /* @__PURE__ */ new Set();
1391
1394
  nodeListeners = /* @__PURE__ */ new Map();
1392
1395
  batchListeners = /* @__PURE__ */ new Set();
1396
+ // Draft overlay (exploration 0329)
1397
+ checkedOutDraft = null;
1398
+ cloneToOriginal = /* @__PURE__ */ new Map();
1399
+ draftMemberSet = /* @__PURE__ */ new Set();
1400
+ draftDynamicMembers = false;
1401
+ /** null = conservative "all schemas" (memberSchemaIds omitted). */
1402
+ draftMemberSchemaSet = null;
1403
+ draftOverlayListeners = /* @__PURE__ */ new Set();
1393
1404
  schemaLookup;
1394
1405
  propertyLookup;
1395
1406
  lensRegistry;
@@ -1463,12 +1474,13 @@ var NodeStore = class {
1463
1474
  schemaId: options.schemaId,
1464
1475
  properties: options.properties
1465
1476
  },
1466
- authAction: "write",
1477
+ authAction: "create",
1467
1478
  requireExisting: false,
1468
1479
  createSchemaId: options.schemaId
1469
1480
  });
1470
1481
  this.emit(change2, node2, null, false);
1471
1482
  this.authEvaluator?.invalidate(node2.id);
1483
+ this.notifyDraftNodeCreated(node2.id, node2.schemaId);
1472
1484
  if (this.telemetry) {
1473
1485
  this.telemetry.reportPerformance("data.create", Date.now() - start);
1474
1486
  this.telemetry.reportUsage("data.create", 1);
@@ -1477,7 +1489,7 @@ var NodeStore = class {
1477
1489
  }
1478
1490
  await this.assertAuthorized({
1479
1491
  subject: this.authorDID,
1480
- action: "write",
1492
+ action: "create",
1481
1493
  nodeId: id,
1482
1494
  patch: options.properties,
1483
1495
  node: {
@@ -1504,6 +1516,7 @@ var NodeStore = class {
1504
1516
  await this.persistEncryptedNodeSnapshot(node);
1505
1517
  this.emit(change, node, null, false);
1506
1518
  this.authEvaluator?.invalidate(node.id);
1519
+ this.notifyDraftNodeCreated(node.id, node.schemaId);
1507
1520
  if (this.telemetry) {
1508
1521
  this.telemetry.reportPerformance("data.create", Date.now() - start);
1509
1522
  this.telemetry.reportUsage("data.create", 1);
@@ -1520,10 +1533,186 @@ var NodeStore = class {
1520
1533
  * Get a Node by ID.
1521
1534
  */
1522
1535
  async get(id) {
1536
+ const cloneId = this.checkedOutDraft?.clones[id];
1537
+ if (cloneId) {
1538
+ const clone = await this.getRaw(cloneId);
1539
+ if (clone) return { ...clone, id };
1540
+ }
1541
+ return this.getRaw(id);
1542
+ }
1543
+ /**
1544
+ * Read a node WITHOUT the draft overlay (exploration 0329) — merge review
1545
+ * and diff surfaces use this to see main's true state while checked out.
1546
+ */
1547
+ async getRaw(id) {
1523
1548
  const node = await this.storage.getNode(id);
1524
1549
  const decrypted = await this.decryptNodeSnapshotIfPresent(node);
1525
1550
  return await this.canReadNode(decrypted) ? decrypted : null;
1526
1551
  }
1552
+ // ==========================================================================
1553
+ // Draft overlay (exploration 0329)
1554
+ // ==========================================================================
1555
+ /**
1556
+ * Check a draft out (or pass null to return to main). While checked out,
1557
+ * member reads swap to clone content under original ids, member writes
1558
+ * redirect to clones (lazily forking via `onMissingMember`), and clone
1559
+ * change events mirror to original-id subscribers.
1560
+ */
1561
+ setCheckedOutDraft(overlay) {
1562
+ this.checkedOutDraft = overlay;
1563
+ this.cloneToOriginal = new Map(
1564
+ overlay ? Object.entries(overlay.clones).map(([orig, clone]) => [clone, orig]) : []
1565
+ );
1566
+ this.draftMemberSet = overlay && overlay.members !== "dynamic" ? new Set(overlay.members) : /* @__PURE__ */ new Set();
1567
+ this.draftDynamicMembers = overlay?.members === "dynamic";
1568
+ this.draftMemberSchemaSet = overlay?.memberSchemaIds ? new Set(overlay.memberSchemaIds) : null;
1569
+ this.notifyDraftOverlayListeners();
1570
+ }
1571
+ /** Report a locally-created node to the checkout's draft-born hook (0329 P4). */
1572
+ notifyDraftNodeCreated(nodeId, schemaId) {
1573
+ const overlay = this.checkedOutDraft;
1574
+ if (!overlay?.onNodeCreated) return;
1575
+ if (nodeId === overlay.draftId || this.cloneToOriginal.has(nodeId)) return;
1576
+ try {
1577
+ overlay.onNodeCreated(nodeId, schemaId);
1578
+ } catch (err) {
1579
+ console.error("Error in draft onNodeCreated hook:", err);
1580
+ }
1581
+ }
1582
+ notifyDraftOverlayListeners() {
1583
+ for (const listener of this.draftOverlayListeners) {
1584
+ try {
1585
+ listener();
1586
+ } catch (err) {
1587
+ console.error("Error in draft overlay listener:", err);
1588
+ }
1589
+ }
1590
+ }
1591
+ getCheckedOutDraft() {
1592
+ return this.checkedOutDraft;
1593
+ }
1594
+ /**
1595
+ * Device-local draft privacy (exploration 0329): ids marked here (draft
1596
+ * bookkeeping nodes, clones, draft-born nodes) are excluded from outbound
1597
+ * sync publication by the personal node-sync room's `shouldPublish`
1598
+ * predicate. In-memory only — the draft module rehydrates it from open
1599
+ * Draft nodes at boot, BEFORE sync starts.
1600
+ */
1601
+ draftPrivateNodeIds = /* @__PURE__ */ new Set();
1602
+ markDraftPrivate(ids) {
1603
+ for (const id of ids) this.draftPrivateNodeIds.add(id);
1604
+ }
1605
+ unmarkDraftPrivate(ids) {
1606
+ for (const id of ids) this.draftPrivateNodeIds.delete(id);
1607
+ }
1608
+ isDraftPrivate(id) {
1609
+ return this.draftPrivateNodeIds.has(id);
1610
+ }
1611
+ /** Notified on every checkout change (including clone-map refreshes). */
1612
+ subscribeToDraftOverlay(listener) {
1613
+ this.draftOverlayListeners.add(listener);
1614
+ return () => this.draftOverlayListeners.delete(listener);
1615
+ }
1616
+ /**
1617
+ * Swap checked-out members' content to their clones (original ids kept, so
1618
+ * MV id lists and grid ordering are untouched). No-op without a checkout.
1619
+ */
1620
+ async overlayStates(states) {
1621
+ const overlay = this.checkedOutDraft;
1622
+ if (!overlay) return states;
1623
+ const visible = this.cloneToOriginal.size > 0 ? states.filter((state) => !this.cloneToOriginal.has(state.id)) : states;
1624
+ const cloneIds = visible.map((s) => overlay.clones[s.id]).filter((cloneId) => cloneId !== void 0);
1625
+ if (cloneIds.length === 0) return visible;
1626
+ const cloneMap = await this.getNodesById(cloneIds, this.storage);
1627
+ return Promise.all(
1628
+ visible.map(async (state) => {
1629
+ const cloneId = overlay.clones[state.id];
1630
+ const clone = cloneId ? cloneMap.get(cloneId) : void 0;
1631
+ if (!clone) return state;
1632
+ const decrypted = await this.decryptNodeSnapshotIfPresent(clone);
1633
+ return decrypted ? { ...decrypted, id: state.id } : state;
1634
+ })
1635
+ );
1636
+ }
1637
+ /**
1638
+ * Draft-aware query path (0329 P5): candidates from storage WITHOUT
1639
+ * pagination, unioned with checked-out members the predicate may have
1640
+ * missed (their clone values can change membership), content-swapped,
1641
+ * clone rows hidden, then the full descriptor re-applied in JS so
1642
+ * filter/sort/pagination reflect draft values.
1643
+ */
1644
+ async draftAwareQuery(descriptor, start) {
1645
+ const overlay = this.checkedOutDraft;
1646
+ const unpaginated = withoutNodeQueryMaterializedView(withoutNodeQueryPagination(descriptor));
1647
+ let candidates;
1648
+ if (this.storage.queryNodes && !this.nodeContentCipher) {
1649
+ candidates = (await this.storage.queryNodes(unpaginated)).nodes;
1650
+ } else {
1651
+ const fallback = await this.loadQueryFallbackCandidates(unpaginated);
1652
+ const decrypted = await Promise.all(
1653
+ fallback.nodes.map((node) => this.decryptNodeSnapshotIfPresent(node))
1654
+ );
1655
+ candidates = decrypted.filter((node) => node !== null);
1656
+ }
1657
+ if (overlay) {
1658
+ const present = new Set(candidates.map((node) => node.id));
1659
+ const missing = Object.keys(overlay.clones).filter(
1660
+ (id) => !present.has(id)
1661
+ );
1662
+ if (missing.length > 0) {
1663
+ const extra = await this.getNodesById(missing, this.storage);
1664
+ for (const node of extra.values()) {
1665
+ const decrypted = await this.decryptNodeSnapshotIfPresent(node);
1666
+ if (!decrypted) continue;
1667
+ if (decrypted.schemaId !== descriptor.schemaId) continue;
1668
+ if (decrypted.deleted && !descriptor.includeDeleted) continue;
1669
+ candidates.push(decrypted);
1670
+ }
1671
+ }
1672
+ }
1673
+ const readable = await this.filterReadableNodes(candidates);
1674
+ const swapped = await this.overlayStates(readable);
1675
+ const filtered = applyNodeQueryDescriptor(swapped, withoutNodeQueryPagination(descriptor));
1676
+ const result = applyNodeQueryDescriptor(swapped, descriptor);
1677
+ return {
1678
+ nodes: result,
1679
+ totalCount: filtered.length,
1680
+ plan: {
1681
+ strategy: "draft-overlay",
1682
+ candidateNodeCount: readable.length,
1683
+ hydratedNodeCount: swapped.length,
1684
+ returnedNodeCount: result.length,
1685
+ durationMs: Date.now() - start
1686
+ }
1687
+ };
1688
+ }
1689
+ /**
1690
+ * Resolve where a write to `id` lands under the checkout: an existing
1691
+ * clone, a lazily-forked one (`onMissingMember`), or — for non-members,
1692
+ * clone ids, and declined forks — the id itself.
1693
+ */
1694
+ async resolveDraftWriteTarget(id) {
1695
+ const overlay = this.checkedOutDraft;
1696
+ if (!overlay) return id;
1697
+ const existing = overlay.clones[id];
1698
+ if (existing) return existing;
1699
+ if (this.draftDynamicMembers) {
1700
+ if (id === overlay.draftId || this.cloneToOriginal.has(id)) return id;
1701
+ } else if (!this.draftMemberSet.has(id)) {
1702
+ return id;
1703
+ }
1704
+ if (overlay.onMissingMember) {
1705
+ const cloneId = await overlay.onMissingMember(id);
1706
+ if (cloneId && this.checkedOutDraft === overlay) {
1707
+ this.checkedOutDraft = { ...overlay, clones: { ...overlay.clones, [id]: cloneId } };
1708
+ this.cloneToOriginal.set(cloneId, id);
1709
+ this.notifyDraftOverlayListeners();
1710
+ return cloneId;
1711
+ }
1712
+ if (cloneId) return cloneId;
1713
+ }
1714
+ return id;
1715
+ }
1527
1716
  /**
1528
1717
  * Return the subset of IDs that already exist in materialized storage.
1529
1718
  *
@@ -1603,6 +1792,7 @@ var NodeStore = class {
1603
1792
  */
1604
1793
  async update(id, options) {
1605
1794
  const start = this.telemetry ? Date.now() : 0;
1795
+ id = await this.resolveDraftWriteTarget(id);
1606
1796
  try {
1607
1797
  if (this.canUseSingleWriteFastPath()) {
1608
1798
  const { node: node2, previousNode, change: change2 } = await this.applySingleNodeWrite({
@@ -1611,7 +1801,7 @@ var NodeStore = class {
1611
1801
  nodeId: id,
1612
1802
  properties: options.properties
1613
1803
  },
1614
- authAction: "write",
1804
+ authAction: "update",
1615
1805
  requireExisting: true
1616
1806
  });
1617
1807
  this.emit(change2, node2, previousNode, false);
@@ -1635,7 +1825,7 @@ var NodeStore = class {
1635
1825
  }
1636
1826
  await this.assertAuthorized({
1637
1827
  subject: this.authorDID,
1638
- action: "write",
1828
+ action: "update",
1639
1829
  nodeId: id,
1640
1830
  patch: options.properties,
1641
1831
  node: {
@@ -1685,6 +1875,7 @@ var NodeStore = class {
1685
1875
  */
1686
1876
  async delete(id) {
1687
1877
  const start = this.telemetry ? Date.now() : 0;
1878
+ id = await this.resolveDraftWriteTarget(id);
1688
1879
  try {
1689
1880
  if (this.canUseSingleWriteFastPath()) {
1690
1881
  const { node, previousNode, change: change2 } = await this.applySingleNodeWrite({
@@ -1750,6 +1941,7 @@ var NodeStore = class {
1750
1941
  * Restore a deleted Node.
1751
1942
  */
1752
1943
  async restore(id) {
1944
+ id = await this.resolveDraftWriteTarget(id);
1753
1945
  if (this.canUseSingleWriteFastPath()) {
1754
1946
  const { node: node2, previousNode, change: change2 } = await this.applySingleNodeWrite({
1755
1947
  nodeId: id,
@@ -1758,7 +1950,7 @@ var NodeStore = class {
1758
1950
  properties: {},
1759
1951
  deleted: false
1760
1952
  },
1761
- authAction: "write",
1953
+ authAction: "update",
1762
1954
  requireExisting: true
1763
1955
  });
1764
1956
  this.emit(change2, node2, previousNode, false);
@@ -1771,7 +1963,7 @@ var NodeStore = class {
1771
1963
  }
1772
1964
  await this.assertAuthorized({
1773
1965
  subject: this.authorDID,
1774
- action: "write",
1966
+ action: "update",
1775
1967
  nodeId: id,
1776
1968
  node: {
1777
1969
  schemaId: existing.schemaId,
@@ -1818,7 +2010,8 @@ var NodeStore = class {
1818
2010
  const readable = await this.filterReadableNodes(
1819
2011
  decrypted.filter((node) => node !== null)
1820
2012
  );
1821
- const result = this.authEvaluator ? this.applyListPagination(readable, options) : readable;
2013
+ const paged = this.authEvaluator ? this.applyListPagination(readable, options) : readable;
2014
+ const result = await this.overlayStates(paged);
1822
2015
  if (this.telemetry) {
1823
2016
  this.telemetry.reportPerformance("data.list", Date.now() - start);
1824
2017
  this.telemetry.reportUsage("data.list", 1);
@@ -1837,10 +2030,13 @@ var NodeStore = class {
1837
2030
  async query(descriptor) {
1838
2031
  const start = Date.now();
1839
2032
  try {
2033
+ if (this.checkedOutDraft && Object.keys(this.checkedOutDraft.clones).length > 0 && (this.draftMemberSchemaSet === null || this.draftMemberSchemaSet.has(descriptor.schemaId))) {
2034
+ return this.draftAwareQuery(descriptor, start);
2035
+ }
1840
2036
  if (this.storage.queryNodes && !this.nodeContentCipher && !this.authEvaluator) {
1841
2037
  const result2 = await this.storage.queryNodes(descriptor);
1842
2038
  return {
1843
- nodes: result2.nodes,
2039
+ nodes: await this.overlayStates(result2.nodes),
1844
2040
  totalCount: result2.totalCount,
1845
2041
  plan: {
1846
2042
  ...result2.plan,
@@ -1852,7 +2048,7 @@ var NodeStore = class {
1852
2048
  const authFingerprint = await this.authFingerprint();
1853
2049
  const result2 = await this.storage.queryNodes({ ...descriptor, authFingerprint });
1854
2050
  return {
1855
- nodes: result2.nodes,
2051
+ nodes: await this.overlayStates(result2.nodes),
1856
2052
  totalCount: result2.totalCount,
1857
2053
  plan: {
1858
2054
  ...result2.plan,
@@ -1863,7 +2059,7 @@ var NodeStore = class {
1863
2059
  if (this.storage.queryNodes && !this.nodeContentCipher && this.authEvaluator && !descriptor.nodeId && descriptor.materializedView === void 0) {
1864
2060
  const candidates = await this.storage.queryNodes(withoutNodeQueryPagination(descriptor));
1865
2061
  const readable2 = await this.filterReadableNodes(candidates.nodes);
1866
- const result2 = applyNodeQueryDescriptor(readable2, descriptor);
2062
+ const result2 = await this.overlayStates(applyNodeQueryDescriptor(readable2, descriptor));
1867
2063
  return {
1868
2064
  nodes: result2,
1869
2065
  totalCount: readable2.length,
@@ -1887,7 +2083,9 @@ var NodeStore = class {
1887
2083
  const readable = await this.filterReadableNodes(
1888
2084
  decrypted.filter((node) => node !== null)
1889
2085
  );
1890
- const result = applyNodeQueryDescriptor(readable, fallback.postFilterDescriptor);
2086
+ const result = await this.overlayStates(
2087
+ applyNodeQueryDescriptor(readable, fallback.postFilterDescriptor)
2088
+ );
1891
2089
  const totalCount = fallback.totalCount ?? (fallback.paginatedInStorage ? void 0 : applyNodeQueryDescriptor(
1892
2090
  readable,
1893
2091
  withoutNodeQueryPagination(fallback.postFilterDescriptor)
@@ -2472,12 +2670,21 @@ var NodeStore = class {
2472
2670
  );
2473
2671
  }
2474
2672
  if (this.authEvaluator) {
2475
- const action = this.inferActionFromChange(change);
2673
+ const stored = await this.storage.getNode(change.payload.nodeId);
2674
+ const action = this.inferActionFromChange(change, stored);
2476
2675
  const decision = await this.authEvaluator.can({
2477
2676
  subject: change.authorDID,
2478
2677
  action,
2479
2678
  nodeId: change.payload.nodeId,
2480
- patch: action === "write" ? change.payload.properties : void 0
2679
+ patch: action === "create" || action === "update" ? change.payload.properties : void 0,
2680
+ // A remote create has no stored node to evaluate against — build the
2681
+ // draft from the payload so container-relation roles resolve
2682
+ // (exploration 0304). Update/delete load the stored node as before.
2683
+ node: action === "create" && change.payload.schemaId ? {
2684
+ schemaId: change.payload.schemaId,
2685
+ createdBy: change.authorDID,
2686
+ properties: change.payload.properties
2687
+ } : void 0
2481
2688
  });
2482
2689
  if (!decision.allowed) {
2483
2690
  this.telemetry?.reportSecurityEvent("data.unauthorized_remote_change", "medium");
@@ -2673,6 +2880,24 @@ var NodeStore = class {
2673
2880
  }
2674
2881
  }
2675
2882
  }
2883
+ const originalId = this.cloneToOriginal.get(change.payload.nodeId);
2884
+ if (originalId) {
2885
+ const originalListeners = this.nodeListeners.get(originalId);
2886
+ if (originalListeners) {
2887
+ const mirrored = {
2888
+ ...event,
2889
+ node: event.node ? { ...event.node, id: originalId } : event.node,
2890
+ previousNode: event.previousNode ? { ...event.previousNode, id: originalId } : event.previousNode
2891
+ };
2892
+ for (const listener of originalListeners) {
2893
+ try {
2894
+ listener(mirrored);
2895
+ } catch (err) {
2896
+ console.error("Error in NodeStore draft-mirror listener:", err);
2897
+ }
2898
+ }
2899
+ }
2900
+ }
2676
2901
  }
2677
2902
  // ==========================================================================
2678
2903
  // Internal Methods
@@ -2922,7 +3147,11 @@ var NodeStore = class {
2922
3147
  const newTs = {
2923
3148
  lamport: change.lamport,
2924
3149
  author: change.authorDID,
2925
- wallTime: change.wallTime
3150
+ wallTime: change.wallTime,
3151
+ // Grinding-resistant final tiebreak (exploration 0305): only v4+ changes
3152
+ // carry a key, so a v4-vs-legacy comparison degrades to the author DID
3153
+ // and mixed fleets still agree.
3154
+ ...(change.protocolVersion ?? 0) >= LWW_TIEBREAK_KEY_VERSION ? { tiebreakKey: computeLwwTiebreakKey(change.authorDID, key, value) } : {}
2926
3155
  };
2927
3156
  const incomingWins = !existingTs || this.shouldReplace(existingTs, newTs);
2928
3157
  const target = isUnknownProperty ? node._unknown ??= {} : node.properties;
@@ -3057,7 +3286,7 @@ var NodeStore = class {
3057
3286
  if (operation.type === "create") {
3058
3287
  await this.assertAuthorized({
3059
3288
  subject: this.authorDID,
3060
- action: "write",
3289
+ action: "create",
3061
3290
  nodeId: operation.options.id ?? "",
3062
3291
  node: {
3063
3292
  schemaId: operation.options.schemaId,
@@ -3077,17 +3306,20 @@ var NodeStore = class {
3077
3306
  }
3078
3307
  await this.assertAuthorized({
3079
3308
  subject: this.authorDID,
3080
- action: "write",
3309
+ action: "update",
3081
3310
  nodeId: operation.nodeId,
3082
3311
  patch: operation.type === "update" ? operation.options.properties : void 0
3083
3312
  });
3084
3313
  }
3085
3314
  }
3086
- inferActionFromChange(change) {
3315
+ inferActionFromChange(change, stored) {
3087
3316
  if (change.payload.deleted === true) {
3088
3317
  return "delete";
3089
3318
  }
3090
- return "write";
3319
+ if (!stored && change.payload.schemaId) {
3320
+ return "create";
3321
+ }
3322
+ return "update";
3091
3323
  }
3092
3324
  shouldRecomputeRecipients(schemaId, patch) {
3093
3325
  const authRelevant = this.authRelevantPropertyLookup?.(schemaId);
@@ -3396,6 +3628,41 @@ var MemoryNodeStorageAdapter = class {
3396
3628
  this.yjsSnapshotStore = this.yjsSnapshotStore.filter((s) => s.nodeId !== nodeId);
3397
3629
  }
3398
3630
  // ==========================================================================
3631
+ // Pin Registry (exploration 0329)
3632
+ // ==========================================================================
3633
+ /** key -> ownerId -> entry */
3634
+ pinStore = /* @__PURE__ */ new Map();
3635
+ pins = {
3636
+ addPins: async (pins) => {
3637
+ for (const pin of pins) {
3638
+ let owners = this.pinStore.get(pin.key);
3639
+ if (!owners) {
3640
+ owners = /* @__PURE__ */ new Map();
3641
+ this.pinStore.set(pin.key, owners);
3642
+ }
3643
+ owners.set(pin.ownerId, { ...pin });
3644
+ }
3645
+ },
3646
+ removePinsByOwner: async (ownerId) => {
3647
+ for (const [key, owners] of this.pinStore) {
3648
+ owners.delete(ownerId);
3649
+ if (owners.size === 0) this.pinStore.delete(key);
3650
+ }
3651
+ },
3652
+ getPinnedKeysAmong: async (keys) => {
3653
+ const pinned = /* @__PURE__ */ new Set();
3654
+ for (const key of keys) {
3655
+ if (this.pinStore.has(key)) pinned.add(key);
3656
+ }
3657
+ return pinned;
3658
+ },
3659
+ countPins: async () => {
3660
+ let count2 = 0;
3661
+ for (const owners of this.pinStore.values()) count2 += owners.size;
3662
+ return count2;
3663
+ }
3664
+ };
3665
+ // ==========================================================================
3399
3666
  // Utility Methods (for testing)
3400
3667
  // ==========================================================================
3401
3668
  /**
@@ -3407,6 +3674,7 @@ var MemoryNodeStorageAdapter = class {
3407
3674
  this.nodes.clear();
3408
3675
  this.documentContentStore.clear();
3409
3676
  this.yjsSnapshotStore = [];
3677
+ this.pinStore.clear();
3410
3678
  this.lastLamportTime = 0;
3411
3679
  this.syncCursors.clear();
3412
3680
  }
@@ -3446,6 +3714,9 @@ var MemoryNodeStorageAdapter = class {
3446
3714
  ])
3447
3715
  ),
3448
3716
  yjsSnapshotStore: structuredClone(this.yjsSnapshotStore),
3717
+ pinStore: new Map(
3718
+ Array.from(this.pinStore.entries(), ([key, owners]) => [key, new Map(owners)])
3719
+ ),
3449
3720
  lastLamportTime: this.lastLamportTime
3450
3721
  };
3451
3722
  }
@@ -3455,6 +3726,7 @@ var MemoryNodeStorageAdapter = class {
3455
3726
  this.nodes = snapshot.nodes;
3456
3727
  this.documentContentStore = snapshot.documentContentStore;
3457
3728
  this.yjsSnapshotStore = snapshot.yjsSnapshotStore;
3729
+ this.pinStore = snapshot.pinStore;
3458
3730
  this.lastLamportTime = snapshot.lastLamportTime;
3459
3731
  }
3460
3732
  };
@@ -3520,7 +3792,10 @@ function hydrateJoinedRows(rows) {
3520
3792
  node.timestamps[row.property_key] = {
3521
3793
  lamport: row.lamport_time ?? 0,
3522
3794
  author: row.updated_by ?? "",
3523
- wallTime: row.prop_updated_at ?? 0
3795
+ wallTime: row.prop_updated_at ?? 0,
3796
+ // Round-trip the stored key so the in-memory LWW comparison matches the
3797
+ // SQL guard exactly (0305); NULL legacy rows keep the author tiebreak.
3798
+ ...row.tiebreak_key != null ? { tiebreakKey: row.tiebreak_key } : {}
3524
3799
  };
3525
3800
  if ((row.prop_updated_at ?? 0) >= node.updatedAt) {
3526
3801
  node.updatedBy = row.updated_by ?? node.createdBy;
@@ -3540,7 +3815,10 @@ function hydrateAggregatedRows(rows) {
3540
3815
  timestamps[key] = {
3541
3816
  lamport: entry.l ?? 0,
3542
3817
  author: entry.b ?? "",
3543
- wallTime: entry.w ?? 0
3818
+ wallTime: entry.w ?? 0,
3819
+ // Round-trip the stored tiebreak key (0305) so aggregated hydrate
3820
+ // matches the SQL guard; NULL/absent → author tiebreak.
3821
+ ...entry.k != null ? { tiebreakKey: entry.k } : {}
3544
3822
  };
3545
3823
  if ((entry.w ?? 0) >= row.updated_at) {
3546
3824
  node.updatedBy = entry.b ?? row.created_by;
@@ -3571,6 +3849,7 @@ function buildHydrateChunkQuery(ids) {
3571
3849
  p.lamport_time,
3572
3850
  p.updated_by,
3573
3851
  p.updated_at AS prop_updated_at,
3852
+ p.tiebreak_key,
3574
3853
  wanted.ordinal
3575
3854
  FROM wanted
3576
3855
  JOIN nodes n ON n.id = wanted.id
@@ -3599,7 +3878,7 @@ function buildAggregatedHydrateChunkQuery(ids) {
3599
3878
  FILTER (WHERE p.property_key IS NOT NULL) AS props_json,
3600
3879
  json_group_object(
3601
3880
  p.property_key,
3602
- json_object('l', p.lamport_time, 'b', p.updated_by, 'w', p.updated_at)
3881
+ json_object('l', p.lamport_time, 'b', p.updated_by, 'w', p.updated_at, 'k', p.tiebreak_key)
3603
3882
  ) FILTER (WHERE p.property_key IS NOT NULL) AS meta_json
3604
3883
  FROM wanted
3605
3884
  JOIN nodes n ON n.id = wanted.id
@@ -4522,7 +4801,12 @@ var NODE_PROPERTIES_LWW_GUARD = lwwUpdateGuardSql({
4522
4801
  table: "node_properties",
4523
4802
  lamportColumn: "lamport_time",
4524
4803
  wallTimeColumn: "updated_at",
4525
- authorColumn: "updated_by"
4804
+ authorColumn: "updated_by",
4805
+ // Grinding-resistant final tiebreak (exploration 0305): larger key wins when
4806
+ // both rows carry one (v4+), else the author DID. The key is precomputed in
4807
+ // application code and stored, so SQL only compares the opaque hex — byte
4808
+ // identical to `shouldReplace` in ./store.ts, no user-defined function.
4809
+ tiebreakKeyColumn: "tiebreak_key"
4526
4810
  });
4527
4811
  var DEFAULT_ADAPTIVE_INDEXING = {
4528
4812
  enabled: false,
@@ -4622,6 +4906,7 @@ var SQLiteNodeStorageAdapter = class {
4622
4906
  nodeReadAuthorizer;
4623
4907
  /** One-time guard: ensure the `auth_fingerprint` column exists on upgraded DBs. */
4624
4908
  materializationColumnsReady = false;
4909
+ nodePropertyColumnsReady = null;
4625
4910
  /**
4626
4911
  * Descriptor→SQL compilation lives in `query-compiler.ts` (exploration
4627
4912
  * 0276). Flags are read per-compile so the compiler never captures stale
@@ -4916,12 +5201,14 @@ var SQLiteNodeStorageAdapter = class {
4916
5201
  }
4917
5202
  // ─── Materialized State Operations ────────────────────────────────────────
4918
5203
  async getNode(id) {
5204
+ await this.ensureNodePropertyColumns();
4919
5205
  const nodes = await hydrateNodesByIds(this.db, [id], this.aggregatedHydration);
4920
5206
  return nodes[0] ?? null;
4921
5207
  }
4922
5208
  async getNodes(ids) {
4923
5209
  const uniqueIds = Array.from(new Set(ids));
4924
5210
  if (uniqueIds.length === 0) return [];
5211
+ await this.ensureNodePropertyColumns();
4925
5212
  return hydrateNodesByIds(this.db, uniqueIds, this.aggregatedHydration);
4926
5213
  }
4927
5214
  async getExistingNodeIds(ids) {
@@ -4966,6 +5253,7 @@ var SQLiteNodeStorageAdapter = class {
4966
5253
  * Used by importNodes to avoid nested transactions.
4967
5254
  */
4968
5255
  async _setNodeInternal(node, options) {
5256
+ await this.ensureNodePropertyColumns();
4969
5257
  await this.db.run(
4970
5258
  `INSERT INTO nodes (id, schema_id, created_at, updated_at, created_by, deleted_at)
4971
5259
  VALUES (?, ?, ?, ?, ?, ?)
@@ -4987,13 +5275,14 @@ var SQLiteNodeStorageAdapter = class {
4987
5275
  const timestamp = node.timestamps[key];
4988
5276
  if (!timestamp) continue;
4989
5277
  await this.db.run(
4990
- `INSERT INTO node_properties (node_id, property_key, value, lamport_time, updated_by, updated_at)
4991
- VALUES (?, ?, ?, ?, ?, ?)
5278
+ `INSERT INTO node_properties (node_id, property_key, value, lamport_time, updated_by, updated_at, tiebreak_key)
5279
+ VALUES (?, ?, ?, ?, ?, ?, ?)
4992
5280
  ON CONFLICT(node_id, property_key) DO UPDATE SET
4993
5281
  value = excluded.value,
4994
5282
  lamport_time = excluded.lamport_time,
4995
5283
  updated_by = excluded.updated_by,
4996
- updated_at = excluded.updated_at
5284
+ updated_at = excluded.updated_at,
5285
+ tiebreak_key = excluded.tiebreak_key
4997
5286
  WHERE ${NODE_PROPERTIES_LWW_GUARD}`,
4998
5287
  [
4999
5288
  node.id,
@@ -5001,7 +5290,8 @@ var SQLiteNodeStorageAdapter = class {
5001
5290
  this.serializeValue(value),
5002
5291
  timestamp.lamport,
5003
5292
  timestamp.author,
5004
- timestamp.wallTime
5293
+ timestamp.wallTime,
5294
+ timestamp.tiebreakKey ?? null
5005
5295
  ]
5006
5296
  );
5007
5297
  }
@@ -5040,6 +5330,7 @@ var SQLiteNodeStorageAdapter = class {
5040
5330
  * Fetches nodes and properties in one query for better performance with large datasets.
5041
5331
  */
5042
5332
  async listNodesOptimized(options) {
5333
+ await this.ensureNodePropertyColumns();
5043
5334
  let whereClause = "1=1";
5044
5335
  const params = [];
5045
5336
  if (options?.schemaId) {
@@ -5063,7 +5354,7 @@ var SQLiteNodeStorageAdapter = class {
5063
5354
  )
5064
5355
  SELECT
5065
5356
  ln.id, ln.schema_id, ln.created_at, ln.updated_at, ln.created_by, ln.deleted_at,
5066
- p.property_key, p.value, p.lamport_time, p.updated_by, p.updated_at as prop_updated_at,
5357
+ p.property_key, p.value, p.lamport_time, p.updated_by, p.updated_at as prop_updated_at, p.tiebreak_key,
5067
5358
  NULL as ordinal
5068
5359
  FROM limited_nodes ln
5069
5360
  LEFT JOIN node_properties p ON ln.id = p.node_id
@@ -5075,7 +5366,7 @@ var SQLiteNodeStorageAdapter = class {
5075
5366
  sql = `
5076
5367
  SELECT
5077
5368
  n.id, n.schema_id, n.created_at, n.updated_at, n.created_by, n.deleted_at,
5078
- p.property_key, p.value, p.lamport_time, p.updated_by, p.updated_at as prop_updated_at,
5369
+ p.property_key, p.value, p.lamport_time, p.updated_by, p.updated_at as prop_updated_at, p.tiebreak_key,
5079
5370
  NULL as ordinal
5080
5371
  FROM nodes n
5081
5372
  LEFT JOIN node_properties p ON n.id = p.node_id
@@ -5101,6 +5392,7 @@ var SQLiteNodeStorageAdapter = class {
5101
5392
  }
5102
5393
  async queryNodes(descriptor) {
5103
5394
  const start = Date.now();
5395
+ await this.ensureNodePropertyColumns();
5104
5396
  if (descriptor.materializedView) {
5105
5397
  return this.queryMaterializedView(descriptor, start);
5106
5398
  }
@@ -5291,6 +5583,47 @@ var SQLiteNodeStorageAdapter = class {
5291
5583
  [nodeId, content, Date.now()]
5292
5584
  );
5293
5585
  }
5586
+ // ─── Pin Registry (exploration 0329) ──────────────────────────────────────
5587
+ pins = {
5588
+ addPins: async (pins) => {
5589
+ if (pins.length === 0) return;
5590
+ await this.enqueueWrite(async () => {
5591
+ const now = Date.now();
5592
+ for (const chunk of chunkItems(pins, 200)) {
5593
+ const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
5594
+ const params = [];
5595
+ for (const pin of chunk) params.push(pin.key, pin.ownerId, pin.reason, now);
5596
+ await this.db.run(
5597
+ `INSERT INTO pinned_changes (pin_key, owner_id, reason, created_at)
5598
+ VALUES ${placeholders}
5599
+ ON CONFLICT(pin_key, owner_id) DO NOTHING`,
5600
+ params
5601
+ );
5602
+ }
5603
+ });
5604
+ },
5605
+ removePinsByOwner: async (ownerId) => {
5606
+ await this.enqueueWrite(async () => {
5607
+ await this.db.run(`DELETE FROM pinned_changes WHERE owner_id = ?`, [ownerId]);
5608
+ });
5609
+ },
5610
+ getPinnedKeysAmong: async (keys) => {
5611
+ const pinned = /* @__PURE__ */ new Set();
5612
+ for (const chunk of chunkItems(keys, 500)) {
5613
+ const placeholders = chunk.map(() => "?").join(", ");
5614
+ const rows = await this.db.query(
5615
+ `SELECT DISTINCT pin_key FROM pinned_changes WHERE pin_key IN (${placeholders})`,
5616
+ [...chunk]
5617
+ );
5618
+ for (const row of rows) pinned.add(row.pin_key);
5619
+ }
5620
+ return pinned;
5621
+ },
5622
+ countPins: async () => {
5623
+ const row = await this.db.queryOne(`SELECT COUNT(*) AS n FROM pinned_changes`);
5624
+ return row?.n ?? 0;
5625
+ }
5626
+ };
5294
5627
  // ─── Yjs Snapshots (Extended) ─────────────────────────────────────────────
5295
5628
  /**
5296
5629
  * Save a Yjs snapshot for time travel.
@@ -5588,6 +5921,7 @@ var SQLiteNodeStorageAdapter = class {
5588
5921
  if (!this.db.transactionBatch) {
5589
5922
  throw new Error("SQLite transactionBatch is not available.");
5590
5923
  }
5924
+ await this.ensureNodePropertyColumns();
5591
5925
  const operations = [];
5592
5926
  const indexProperties = options?.indexProperties ?? true;
5593
5927
  const hasFullTextSearch = await this.fullTextIndexing.hasTable();
@@ -5638,16 +5972,17 @@ var SQLiteNodeStorageAdapter = class {
5638
5972
  const timestamp = node.timestamps[key];
5639
5973
  if (!timestamp) continue;
5640
5974
  operations.push({
5641
- // Full LWW ordering triple — keep in lockstep with the setNode
5642
- // upsert above and `shouldReplace` in ./store.ts (0272).
5975
+ // Full LWW ordering chain — keep in lockstep with the setNode
5976
+ // upsert above and `shouldReplace` in ./store.ts (0272/0305).
5643
5977
  sql: `INSERT INTO node_properties
5644
- (node_id, property_key, value, lamport_time, updated_by, updated_at)
5645
- VALUES (?, ?, ?, ?, ?, ?)
5978
+ (node_id, property_key, value, lamport_time, updated_by, updated_at, tiebreak_key)
5979
+ VALUES (?, ?, ?, ?, ?, ?, ?)
5646
5980
  ON CONFLICT(node_id, property_key) DO UPDATE SET
5647
5981
  value = excluded.value,
5648
5982
  lamport_time = excluded.lamport_time,
5649
5983
  updated_by = excluded.updated_by,
5650
- updated_at = excluded.updated_at
5984
+ updated_at = excluded.updated_at,
5985
+ tiebreak_key = excluded.tiebreak_key
5651
5986
  WHERE ${NODE_PROPERTIES_LWW_GUARD}`,
5652
5987
  params: [
5653
5988
  node.id,
@@ -5655,7 +5990,8 @@ var SQLiteNodeStorageAdapter = class {
5655
5990
  this.serializeValue(value),
5656
5991
  timestamp.lamport,
5657
5992
  timestamp.author,
5658
- timestamp.wallTime
5993
+ timestamp.wallTime,
5994
+ timestamp.tiebreakKey ?? null
5659
5995
  ]
5660
5996
  });
5661
5997
  }
@@ -5907,6 +6243,25 @@ var SQLiteNodeStorageAdapter = class {
5907
6243
  }
5908
6244
  this.materializationColumnsReady = true;
5909
6245
  }
6246
+ /**
6247
+ * Ensure the `node_properties.tiebreak_key` column exists (exploration 0305).
6248
+ * Fresh databases get it from the DDL; databases created before schema v8 need
6249
+ * this in-place add so the grinding-resistant LWW guard has a column to
6250
+ * compare. Idempotent, memoized, and non-fatal on races — a legacy NULL key
6251
+ * just falls back to the author-DID tiebreak.
6252
+ */
6253
+ async ensureNodePropertyColumns() {
6254
+ this.nodePropertyColumnsReady ??= (async () => {
6255
+ try {
6256
+ const columns = await this.db.query(`PRAGMA table_info(node_properties)`);
6257
+ if (!columns.some((column) => column.name === "tiebreak_key")) {
6258
+ await this.db.run("ALTER TABLE node_properties ADD COLUMN tiebreak_key TEXT");
6259
+ }
6260
+ } catch {
6261
+ }
6262
+ })();
6263
+ return this.nodePropertyColumnsReady;
6264
+ }
5910
6265
  setNodeReadAuthorizer(authorizer) {
5911
6266
  this.nodeReadAuthorizer = authorizer;
5912
6267
  }