@hraness/oh 0.3.1 → 0.4.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 (72) hide show
  1. package/README.md +194 -127
  2. package/dist/cli.d.ts +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +811 -97
  5. package/dist/errors.d.ts +39 -0
  6. package/dist/errors.d.ts.map +1 -0
  7. package/dist/graph.d.ts.map +1 -1
  8. package/dist/index.js +676 -61
  9. package/dist/libsql-semantic-v2.d.ts +160 -0
  10. package/dist/libsql-semantic-v2.d.ts.map +1 -0
  11. package/dist/libsql.d.ts.map +1 -1
  12. package/dist/libsql.js +162 -35
  13. package/dist/memory-page.js +2 -2
  14. package/dist/memory.d.ts +87 -6
  15. package/dist/memory.d.ts.map +1 -1
  16. package/dist/memory.js +1106 -148
  17. package/dist/operation.d.ts +3 -1
  18. package/dist/operation.d.ts.map +1 -1
  19. package/dist/projection-public.js +2 -2
  20. package/dist/projection-suss.js +2 -2
  21. package/dist/sdk.js +780 -88
  22. package/dist/semantic-cloud.d.ts +1 -0
  23. package/dist/semantic-cloud.d.ts.map +1 -1
  24. package/dist/semantic-cloud.js +1545 -2
  25. package/dist/semantic.js +2 -2
  26. package/dist/sqlite/index.js +1251 -306
  27. package/dist/sqlite/port.d.ts +31 -3
  28. package/dist/sqlite/port.d.ts.map +1 -1
  29. package/dist/sqlite/store.d.ts +18 -2
  30. package/dist/sqlite/store.d.ts.map +1 -1
  31. package/dist/store.d.ts +3 -12
  32. package/dist/store.d.ts.map +1 -1
  33. package/dist/store.js +154 -32
  34. package/dist/sync.d.ts +7 -1
  35. package/dist/sync.d.ts.map +1 -1
  36. package/dist/sync.js +668 -35
  37. package/package.json +5 -1
  38. package/skills/oh/SKILL.md +42 -16
  39. package/spec/README.md +10 -3
  40. package/spec/manifest.json +5 -1
  41. package/spec/v1/libsql-semantic-cache-schema-v1.sql +114 -0
  42. package/spec/v1/libsql-semantic-digest-fixture-v1.json +13 -0
  43. package/spec/v1/memory.md +134 -16
  44. package/spec/v1/storage.md +8 -5
  45. package/spec/v1/store.md +20 -0
  46. package/spec/v1/sync.md +77 -8
  47. package/spec/v2/libsql-semantic-cache-schema-v2.sql +175 -0
  48. package/spec/v2/libsql-semantic-digest-fixture-v2.json +14 -0
  49. package/spec/v2/manifest.json +11 -0
  50. package/spec/v2/semantic-cloud.md +106 -0
  51. package/src/cli.test.ts +53 -1
  52. package/src/cli.ts +34 -8
  53. package/src/errors.test.ts +87 -0
  54. package/src/errors.ts +185 -0
  55. package/src/graph.ts +2 -2
  56. package/src/libsql-semantic-v2.test.ts +1114 -0
  57. package/src/libsql-semantic-v2.ts +1891 -0
  58. package/src/libsql-semantic.test.ts +50 -1
  59. package/src/libsql.test.ts +36 -0
  60. package/src/libsql.ts +26 -5
  61. package/src/memory.test.ts +1488 -18
  62. package/src/memory.ts +1199 -122
  63. package/src/operation.ts +13 -3
  64. package/src/semantic-cloud.ts +1 -0
  65. package/src/sqlite/port.test.ts +209 -0
  66. package/src/sqlite/port.ts +118 -4
  67. package/src/sqlite/store.test.ts +106 -1
  68. package/src/sqlite/store.ts +168 -30
  69. package/src/store.test.ts +12 -0
  70. package/src/store.ts +30 -20
  71. package/src/sync.test.ts +570 -2
  72. package/src/sync.ts +586 -36
package/dist/cli.js CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
 
4
+ // src/sync.ts
5
+ import { isProxy } from "util/types";
6
+
4
7
  // src/canonical.ts
5
8
  import { createHash, randomBytes } from "crypto";
6
9
 
@@ -199,7 +202,7 @@ var OH_GRAPH_LIMITS_V1 = Object.freeze({
199
202
  recordBytes: 1024 * 1024,
200
203
  recordsPerSnapshot: 65536
201
204
  });
202
- var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
205
+ var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = Object.freeze([
203
206
  "activity",
204
207
  "assertion",
205
208
  "context",
@@ -218,7 +221,7 @@ var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
218
221
  "type-membership",
219
222
  "view",
220
223
  "vocabulary"
221
- ];
224
+ ]);
222
225
  var KNOWLEDGE_GRAPH_RECORD_KEYS_V1 = [
223
226
  "dependencies",
224
227
  "key",
@@ -1144,6 +1147,142 @@ class OhRecordCodecRegistry {
1144
1147
  }
1145
1148
  }
1146
1149
 
1150
+ // src/errors.ts
1151
+ var OH_OPERATION_SIZE_ERROR_CODE_V1 = "oh.operation-size.v1";
1152
+ var OH_CONFLICT_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhConflictError/v1");
1153
+ var OH_DEPENDENCY_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhDependencyError/v1");
1154
+ var OH_INTEGRITY_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhIntegrityError/v1");
1155
+ var OH_OPERATION_SIZE_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhOperationSizeError/v1");
1156
+ var OH_PROFILE_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhProfileError/v1");
1157
+ function immutableOwnValue(value, key) {
1158
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1159
+ return descriptor !== undefined && descriptor.get === undefined && descriptor.set === undefined && descriptor.configurable === false && descriptor.writable === false ? descriptor.value : undefined;
1160
+ }
1161
+ function brandNativeError(value, brand) {
1162
+ Object.defineProperty(value, brand, {
1163
+ configurable: false,
1164
+ enumerable: false,
1165
+ value: true,
1166
+ writable: false
1167
+ });
1168
+ }
1169
+ function hasNativeErrorBrand(value, brand) {
1170
+ try {
1171
+ return Error.isError(value) && immutableOwnValue(value, brand) === true;
1172
+ } catch {
1173
+ return false;
1174
+ }
1175
+ }
1176
+ function hasNativeSubclassInstance(constructor, value) {
1177
+ return Function.prototype[Symbol.hasInstance].call(constructor, value);
1178
+ }
1179
+ function isOhConflictError(value) {
1180
+ return hasNativeErrorBrand(value, OH_CONFLICT_ERROR_BRAND_V1);
1181
+ }
1182
+
1183
+ class OhConflictError extends Error {
1184
+ static [Symbol.hasInstance](value) {
1185
+ return this === OhConflictError ? isOhConflictError(value) : hasNativeSubclassInstance(this, value);
1186
+ }
1187
+ constructor(message) {
1188
+ super(message);
1189
+ this.name = "OhConflictError";
1190
+ brandNativeError(this, OH_CONFLICT_ERROR_BRAND_V1);
1191
+ }
1192
+ }
1193
+ function isOhIntegrityError(value) {
1194
+ return hasNativeErrorBrand(value, OH_INTEGRITY_ERROR_BRAND_V1);
1195
+ }
1196
+
1197
+ class OhIntegrityError extends Error {
1198
+ static [Symbol.hasInstance](value) {
1199
+ return this === OhIntegrityError ? isOhIntegrityError(value) : hasNativeSubclassInstance(this, value);
1200
+ }
1201
+ constructor(message) {
1202
+ super(message);
1203
+ this.name = "OhIntegrityError";
1204
+ brandNativeError(this, OH_INTEGRITY_ERROR_BRAND_V1);
1205
+ }
1206
+ }
1207
+ function isOhDependencyError(value) {
1208
+ return hasNativeErrorBrand(value, OH_DEPENDENCY_ERROR_BRAND_V1);
1209
+ }
1210
+
1211
+ class OhDependencyError extends Error {
1212
+ static [Symbol.hasInstance](value) {
1213
+ return this === OhDependencyError ? isOhDependencyError(value) : hasNativeSubclassInstance(this, value);
1214
+ }
1215
+ constructor(message) {
1216
+ super(message);
1217
+ this.name = "OhDependencyError";
1218
+ brandNativeError(this, OH_DEPENDENCY_ERROR_BRAND_V1);
1219
+ }
1220
+ }
1221
+ function isOhProfileError(value) {
1222
+ return hasNativeErrorBrand(value, OH_PROFILE_ERROR_BRAND_V1);
1223
+ }
1224
+
1225
+ class OhProfileError extends Error {
1226
+ static [Symbol.hasInstance](value) {
1227
+ return this === OhProfileError ? isOhProfileError(value) : hasNativeSubclassInstance(this, value);
1228
+ }
1229
+ constructor(message) {
1230
+ super(message);
1231
+ this.name = "OhProfileError";
1232
+ brandNativeError(this, OH_PROFILE_ERROR_BRAND_V1);
1233
+ }
1234
+ }
1235
+ function isOhOperationSizeError(value) {
1236
+ try {
1237
+ if (!Error.isError(value) || !(value instanceof RangeError))
1238
+ return false;
1239
+ const operationBytes = immutableOwnValue(value, "operationBytes");
1240
+ const maximumOperationBytes = immutableOwnValue(value, "maximumOperationBytes");
1241
+ return immutableOwnValue(value, OH_OPERATION_SIZE_ERROR_BRAND_V1) === true && immutableOwnValue(value, "code") === OH_OPERATION_SIZE_ERROR_CODE_V1 && Number.isSafeInteger(operationBytes) && operationBytes > 0 && Number.isSafeInteger(maximumOperationBytes) && maximumOperationBytes > 0 && operationBytes > maximumOperationBytes;
1242
+ } catch {
1243
+ return false;
1244
+ }
1245
+ }
1246
+
1247
+ class OhOperationSizeError extends RangeError {
1248
+ static [Symbol.hasInstance](value) {
1249
+ return this === OhOperationSizeError ? isOhOperationSizeError(value) : hasNativeSubclassInstance(this, value);
1250
+ }
1251
+ constructor(operationBytes, maximumOperationBytes) {
1252
+ if (!Number.isSafeInteger(operationBytes) || operationBytes < 1 || !Number.isSafeInteger(maximumOperationBytes) || maximumOperationBytes < 1 || operationBytes <= maximumOperationBytes) {
1253
+ throw new TypeError("Invalid Oh operation size refusal.");
1254
+ }
1255
+ super(`The ${operationBytes}-byte operation exceeds the host-declared ${maximumOperationBytes}-byte canonical bound.`);
1256
+ this.name = "OhOperationSizeError";
1257
+ Object.defineProperties(this, {
1258
+ [OH_OPERATION_SIZE_ERROR_BRAND_V1]: {
1259
+ configurable: false,
1260
+ enumerable: false,
1261
+ value: true,
1262
+ writable: false
1263
+ },
1264
+ code: {
1265
+ configurable: false,
1266
+ enumerable: true,
1267
+ value: OH_OPERATION_SIZE_ERROR_CODE_V1,
1268
+ writable: false
1269
+ },
1270
+ maximumOperationBytes: {
1271
+ configurable: false,
1272
+ enumerable: true,
1273
+ value: maximumOperationBytes,
1274
+ writable: false
1275
+ },
1276
+ operationBytes: {
1277
+ configurable: false,
1278
+ enumerable: true,
1279
+ value: operationBytes,
1280
+ writable: false
1281
+ }
1282
+ });
1283
+ }
1284
+ }
1285
+
1147
1286
  // src/operation.ts
1148
1287
  var OH_OPERATION_MAX_BYTES_V1 = 64 * 1024 * 1024;
1149
1288
  function parsePayload(value) {
@@ -1191,13 +1330,18 @@ function parsePayload(value) {
1191
1330
  v: 1
1192
1331
  } : null;
1193
1332
  }
1194
- function createOhOperationV1(input) {
1333
+ function createOhOperationV1(input, options = {}) {
1334
+ const maximumOperationBytes = options.maximumOperationBytes ?? OH_OPERATION_MAX_BYTES_V1;
1335
+ if (!Number.isSafeInteger(maximumOperationBytes) || maximumOperationBytes < 1 || maximumOperationBytes > OH_OPERATION_MAX_BYTES_V1) {
1336
+ throw new TypeError("Invalid Oh operation byte bound.");
1337
+ }
1195
1338
  const payload = parsePayload(input);
1196
1339
  if (payload === null)
1197
1340
  throw new TypeError("Invalid Oh operation payload.");
1198
1341
  const operation = { ...payload, operationSha256: canonicalSha256(payload) };
1199
- if (Buffer.byteLength(canonicalJson(operation), "utf8") > OH_OPERATION_MAX_BYTES_V1) {
1200
- throw new RangeError("Oh operation exceeds its canonical byte limit.");
1342
+ const operationBytes = Buffer.byteLength(canonicalJson(operation), "utf8");
1343
+ if (operationBytes > maximumOperationBytes) {
1344
+ throw new OhOperationSizeError(operationBytes, maximumOperationBytes);
1201
1345
  }
1202
1346
  return operation;
1203
1347
  }
@@ -1212,18 +1356,362 @@ function parseOhOperationV1(value) {
1212
1356
 
1213
1357
  // src/sync.ts
1214
1358
  var OH_SYNC_PROTOCOL_V1 = "oh.sync.v1";
1215
- function createOhSyncBundleV1(spaceId, operations) {
1216
- const parsedSpaceId = safeCode(spaceId);
1217
- if (parsedSpaceId === null || operations.length > 1000)
1218
- throw new TypeError("Invalid sync bundle.");
1359
+ var OH_SYNC_BUNDLE_MAX_OPERATIONS_V1 = 1000;
1360
+ var OH_SYNC_BUNDLE_KEYS_V1 = [
1361
+ "bundleSha256",
1362
+ "contractSha256",
1363
+ "operations",
1364
+ "protocol",
1365
+ "spaceId",
1366
+ "v"
1367
+ ];
1368
+ var OH_SYNC_HEAD_KEYS_V1 = ["operationSha256", "sequence", "v"];
1369
+ var OH_SYNC_HEAD_REF_KEYS_V1 = ["operationSha256", "sequence"];
1370
+ var OH_OPERATION_KEYS_V1 = [
1371
+ "actorId",
1372
+ "changes",
1373
+ "contractId",
1374
+ "graphRevisionSha256",
1375
+ "instant",
1376
+ "operationId",
1377
+ "operationSha256",
1378
+ "parentOperationSha256",
1379
+ "recordsSha256",
1380
+ "sequence",
1381
+ "spaceId",
1382
+ "v"
1383
+ ];
1384
+ var OH_PUT_CHANGE_KEYS_V1 = ["kind", "record", "v"];
1385
+ var OH_TOMBSTONE_CHANGE_KEYS_V1 = ["key", "kind", "priorSha256", "v"];
1386
+ var OH_RECORD_KEYS_V1 = ["dependencies", "key", "kind", "recordSha256", "v", "value"];
1387
+ var OH_SYNC_INGRESS_VALUE_DEPTH_V1 = 128;
1388
+ var OH_SYNC_INGRESS_VALUE_NODES_V1 = OH_GRAPH_LIMITS_V1.recordBytes;
1389
+ var OH_SYNC_INGRESS_OPERATION_DEPTH_V1 = OH_SYNC_INGRESS_VALUE_DEPTH_V1 + 4;
1390
+ var OH_SYNC_INGRESS_OPERATION_NODES_V1 = OH_OPERATION_MAX_BYTES_V1;
1391
+ var OH_SYNC_BUNDLE_MAX_BYTES_V1 = OH_OPERATION_MAX_BYTES_V1 + 4 * 1024;
1392
+ var OH_SYNC_INGRESS_BUNDLE_NODES_V1 = OH_SYNC_INGRESS_OPERATION_NODES_V1 + 4 * 1024;
1393
+ function exactDataRecordV1(value, expectedKeys) {
1394
+ try {
1395
+ if (typeof value !== "object" || value === null || Array.isArray(value) || isProxy(value))
1396
+ return null;
1397
+ const prototype = Object.getPrototypeOf(value);
1398
+ const keys = Reflect.ownKeys(value);
1399
+ if (prototype !== Object.prototype && prototype !== null || keys.length !== expectedKeys.length || keys.some((key) => typeof key !== "string") || expectedKeys.some((key) => !keys.includes(key)))
1400
+ return null;
1401
+ const detached = Object.create(null);
1402
+ for (const key of expectedKeys) {
1403
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1404
+ if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined)
1405
+ return null;
1406
+ Object.defineProperty(detached, key, {
1407
+ configurable: false,
1408
+ enumerable: true,
1409
+ value: descriptor.value,
1410
+ writable: false
1411
+ });
1412
+ }
1413
+ return detached;
1414
+ } catch {
1415
+ return null;
1416
+ }
1417
+ }
1418
+ function exactDataArrayV1(value, maximumLength, clone = true) {
1419
+ try {
1420
+ if (typeof value !== "object" || value === null || isProxy(value) || !Array.isArray(value))
1421
+ return null;
1422
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
1423
+ const length = lengthDescriptor?.value;
1424
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > maximumLength)
1425
+ return null;
1426
+ const keys = Reflect.ownKeys(value);
1427
+ if (keys.length !== length + 1 || !keys.includes("length") || keys.some((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length)))
1428
+ return null;
1429
+ const detached = clone ? [] : null;
1430
+ for (let index = 0;index < length; index += 1) {
1431
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
1432
+ if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined)
1433
+ return null;
1434
+ detached?.push(descriptor.value);
1435
+ }
1436
+ return detached ?? value;
1437
+ } catch {
1438
+ return null;
1439
+ }
1440
+ }
1441
+ function stagedSyncOperationV1(value, bundleBudget) {
1442
+ const operation = exactDataRecordV1(value, OH_OPERATION_KEYS_V1);
1443
+ if (operation === null || operation.v !== 1 || operation.contractId !== OH_CONTRACT_ID_V1 || safeCode(operation.actorId) === null || safeCode(operation.operationId) === null || safeCode(operation.spaceId) === null || parseCanonicalInstantV1(operation.instant) === null || parseSha256Hex(operation.operationSha256) === null || parseSha256Hex(operation.graphRevisionSha256) === null || parseSha256Hex(operation.recordsSha256) === null)
1444
+ return null;
1445
+ const parentOperationSha256 = operation.parentOperationSha256 === null ? null : parseSha256Hex(operation.parentOperationSha256);
1446
+ const sequence = Number.isSafeInteger(operation.sequence) && operation.sequence > 0 ? operation.sequence : null;
1447
+ if (sequence === null || operation.parentOperationSha256 !== null && parentOperationSha256 === null || sequence === 1 !== (parentOperationSha256 === null))
1448
+ return null;
1449
+ const changes = exactDataArrayV1(operation.changes, OH_GRAPH_LIMITS_V1.changesPerOperation, false);
1450
+ if (changes === null || changes.length === 0)
1451
+ return null;
1452
+ const aggregateDataBudget = {
1453
+ bytes: 0,
1454
+ maximumBytes: OH_OPERATION_MAX_BYTES_V1,
1455
+ maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1,
1456
+ nodes: 0
1457
+ };
1458
+ for (const change of changes) {
1459
+ const put = exactDataRecordV1(change, OH_PUT_CHANGE_KEYS_V1);
1460
+ if (put !== null && put.kind === "put" && put.v === 1) {
1461
+ const record = exactDataRecordV1(put.record, OH_RECORD_KEYS_V1);
1462
+ if (record === null)
1463
+ return null;
1464
+ const valuePreflight = preflightSyncIngressValueV1(record.value, aggregateDataBudget);
1465
+ const dependenciesPreflight = preflightSyncIngressDependenciesV1(record.dependencies, aggregateDataBudget);
1466
+ if (valuePreflight === null || dependenciesPreflight === null)
1467
+ return null;
1468
+ continue;
1469
+ }
1470
+ const tombstone = exactDataRecordV1(change, OH_TOMBSTONE_CHANGE_KEYS_V1);
1471
+ if (tombstone === null || tombstone.kind !== "tombstone" || tombstone.v !== 1)
1472
+ return null;
1473
+ }
1474
+ const detached = boundedSyncIngressV1(operation, {
1475
+ maximumBytes: OH_OPERATION_MAX_BYTES_V1,
1476
+ maximumDepth: OH_SYNC_INGRESS_OPERATION_DEPTH_V1,
1477
+ maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1
1478
+ }, bundleBudget, true);
1479
+ return detached === null ? null : detached.value;
1480
+ }
1481
+ function canonicalStringBytesV1(value, maximumBytes) {
1482
+ if (value.length + 2 > maximumBytes)
1483
+ throw new RangeError("String exceeds its canonical byte budget.");
1484
+ let bytes = 2;
1485
+ for (let index = 0;index < value.length; index += 1) {
1486
+ const code = value.charCodeAt(index);
1487
+ if (code >= 55296 && code <= 56319) {
1488
+ const next = value.charCodeAt(index + 1);
1489
+ if (!(next >= 56320 && next <= 57343))
1490
+ throw new TypeError("Invalid Unicode string.");
1491
+ bytes += 4;
1492
+ index += 1;
1493
+ } else if (code >= 56320 && code <= 57343) {
1494
+ throw new TypeError("Invalid Unicode string.");
1495
+ } else if (code === 34 || code === 92 || code === 8 || code === 9 || code === 10 || code === 12 || code === 13) {
1496
+ bytes += 2;
1497
+ } else if (code <= 31) {
1498
+ bytes += 6;
1499
+ } else if (code <= 127) {
1500
+ bytes += 1;
1501
+ } else if (code <= 2047) {
1502
+ bytes += 2;
1503
+ } else {
1504
+ bytes += 3;
1505
+ }
1506
+ if (bytes > maximumBytes)
1507
+ throw new RangeError("String exceeds its canonical byte budget.");
1508
+ }
1509
+ return bytes;
1510
+ }
1511
+ function boundedSyncIngressV1(value, limits, aggregate, clone) {
1512
+ const ancestors = new Set;
1513
+ const budget = {
1514
+ bytes: 0,
1515
+ maximumBytes: limits.maximumBytes,
1516
+ maximumNodes: limits.maximumNodes,
1517
+ nodes: 0
1518
+ };
1519
+ const canSpendBytes = (count) => budget.bytes + count <= budget.maximumBytes && (aggregate === null || aggregate.bytes + count <= aggregate.maximumBytes);
1520
+ const spendBytes = (count) => {
1521
+ if (!canSpendBytes(count))
1522
+ throw new RangeError("Sync ingress exceeds its canonical byte budget.");
1523
+ budget.bytes += count;
1524
+ if (aggregate !== null)
1525
+ aggregate.bytes += count;
1526
+ };
1527
+ const canSpendNodes = (count) => budget.nodes + count <= budget.maximumNodes && (aggregate === null || aggregate.nodes + count <= aggregate.maximumNodes);
1528
+ const spendNode = () => {
1529
+ if (!canSpendNodes(1))
1530
+ throw new RangeError("Sync ingress exceeds its node budget.");
1531
+ budget.nodes += 1;
1532
+ if (aggregate !== null)
1533
+ aggregate.nodes += 1;
1534
+ };
1535
+ const detach = (candidate, depth) => {
1536
+ if (depth > limits.maximumDepth)
1537
+ throw new RangeError("Sync ingress exceeds its depth budget.");
1538
+ spendNode();
1539
+ if (candidate === null) {
1540
+ spendBytes(4);
1541
+ return candidate;
1542
+ }
1543
+ if (typeof candidate === "boolean") {
1544
+ spendBytes(candidate ? 4 : 5);
1545
+ return candidate;
1546
+ }
1547
+ if (typeof candidate === "string") {
1548
+ spendBytes(canonicalStringBytesV1(candidate, Math.min(budget.maximumBytes - budget.bytes, aggregate === null ? Number.MAX_SAFE_INTEGER : aggregate.maximumBytes - aggregate.bytes)));
1549
+ return candidate;
1550
+ }
1551
+ if (typeof candidate === "number") {
1552
+ if (!Number.isFinite(candidate) || Object.is(candidate, -0))
1553
+ throw new TypeError("Invalid number.");
1554
+ spendBytes(utf8ByteLength(canonicalJson(candidate)));
1555
+ return candidate;
1556
+ }
1557
+ if (typeof candidate !== "object" || isProxy(candidate))
1558
+ throw new TypeError("Invalid data value.");
1559
+ if (ancestors.has(candidate))
1560
+ throw new TypeError("Cyclic data value.");
1561
+ ancestors.add(candidate);
1562
+ try {
1563
+ if (Array.isArray(candidate)) {
1564
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(candidate, "length");
1565
+ const length = lengthDescriptor?.value;
1566
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || !canSpendNodes(length) || !canSpendBytes(2 + Math.max(0, length - 1) + length)) {
1567
+ throw new TypeError("Invalid or over-budget data array.");
1568
+ }
1569
+ const keys2 = Reflect.ownKeys(candidate);
1570
+ if (keys2.length !== length + 1 || !keys2.includes("length") || keys2.some((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length))) {
1571
+ throw new TypeError("Invalid data array.");
1572
+ }
1573
+ spendBytes(2 + Math.max(0, length - 1));
1574
+ const detached2 = clone ? [] : null;
1575
+ for (let index = 0;index < length; index += 1) {
1576
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, String(index));
1577
+ if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
1578
+ throw new TypeError("Invalid data array entry.");
1579
+ }
1580
+ const item = detach(descriptor.value, depth + 1);
1581
+ detached2?.push(item);
1582
+ }
1583
+ return detached2 === null ? candidate : Object.freeze(detached2);
1584
+ }
1585
+ const prototype = Object.getPrototypeOf(candidate);
1586
+ if (prototype !== Object.prototype && prototype !== null) {
1587
+ throw new TypeError("Invalid data object.");
1588
+ }
1589
+ let containerBytes = 2;
1590
+ let properties = 0;
1591
+ for (const key in candidate) {
1592
+ if (!Object.hasOwn(candidate, key))
1593
+ continue;
1594
+ properties += 1;
1595
+ if (!canSpendNodes(properties))
1596
+ throw new RangeError("Sync ingress exceeds its node budget.");
1597
+ const remainingBytes = Math.min(budget.maximumBytes - budget.bytes - containerBytes - properties, aggregate === null ? Number.MAX_SAFE_INTEGER : aggregate.maximumBytes - aggregate.bytes - containerBytes - properties);
1598
+ containerBytes += (properties === 1 ? 0 : 1) + canonicalStringBytesV1(key, remainingBytes) + 1;
1599
+ if (!canSpendBytes(containerBytes + properties)) {
1600
+ throw new RangeError("Sync ingress exceeds its canonical byte budget.");
1601
+ }
1602
+ }
1603
+ const keys = Reflect.ownKeys(candidate);
1604
+ if (keys.length !== properties || keys.some((key) => typeof key !== "string")) {
1605
+ throw new TypeError("Invalid data object.");
1606
+ }
1607
+ spendBytes(containerBytes);
1608
+ const detached = clone ? Object.create(null) : null;
1609
+ for (const key of keys) {
1610
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
1611
+ if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
1612
+ throw new TypeError("Invalid data property.");
1613
+ }
1614
+ const item = detach(descriptor.value, depth + 1);
1615
+ if (detached !== null) {
1616
+ Object.defineProperty(detached, key, {
1617
+ configurable: false,
1618
+ enumerable: true,
1619
+ value: item,
1620
+ writable: false
1621
+ });
1622
+ }
1623
+ }
1624
+ return detached === null ? candidate : Object.freeze(detached);
1625
+ } finally {
1626
+ ancestors.delete(candidate);
1627
+ }
1628
+ };
1629
+ try {
1630
+ return Object.freeze({ value: detach(value, 0) });
1631
+ } catch {
1632
+ return null;
1633
+ }
1634
+ }
1635
+ function preflightSyncIngressValueV1(value, aggregate) {
1636
+ return boundedSyncIngressV1(value, {
1637
+ maximumBytes: OH_GRAPH_LIMITS_V1.recordBytes,
1638
+ maximumDepth: OH_SYNC_INGRESS_VALUE_DEPTH_V1,
1639
+ maximumNodes: OH_SYNC_INGRESS_VALUE_NODES_V1
1640
+ }, aggregate, false);
1641
+ }
1642
+ function preflightSyncIngressDependenciesV1(value, aggregate) {
1643
+ try {
1644
+ if (typeof value !== "object" || value === null || isProxy(value) || !Array.isArray(value)) {
1645
+ return null;
1646
+ }
1647
+ const length = Object.getOwnPropertyDescriptor(value, "length")?.value;
1648
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord)
1649
+ return null;
1650
+ const maximumBytes = length === 0 ? 2 : 1 + length * 515;
1651
+ return boundedSyncIngressV1(value, {
1652
+ maximumBytes,
1653
+ maximumDepth: 1,
1654
+ maximumNodes: length + 1
1655
+ }, aggregate, false);
1656
+ } catch {
1657
+ return null;
1658
+ }
1659
+ }
1660
+ function syncIngressBundleBudgetV1(spaceId) {
1661
+ const emptyBundle = {
1662
+ bundleSha256: "0".repeat(64),
1663
+ contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
1664
+ operations: [],
1665
+ protocol: OH_SYNC_PROTOCOL_V1,
1666
+ spaceId,
1667
+ v: 1
1668
+ };
1669
+ return {
1670
+ bytes: utf8ByteLength(canonicalJson(emptyBundle)),
1671
+ maximumBytes: OH_SYNC_BUNDLE_MAX_BYTES_V1,
1672
+ maximumNodes: OH_SYNC_INGRESS_BUNDLE_NODES_V1,
1673
+ nodes: 7
1674
+ };
1675
+ }
1676
+ function spendSyncIngressBudgetV1(budget, bytes, nodes = 0) {
1677
+ if (budget.bytes + bytes > budget.maximumBytes || budget.nodes + nodes > budget.maximumNodes)
1678
+ return false;
1679
+ budget.bytes += bytes;
1680
+ budget.nodes += nodes;
1681
+ return true;
1682
+ }
1683
+ function measureSyncOperationV1(operation) {
1684
+ const measurement = {
1685
+ bytes: 0,
1686
+ maximumBytes: OH_OPERATION_MAX_BYTES_V1,
1687
+ maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1,
1688
+ nodes: 0
1689
+ };
1690
+ return boundedSyncIngressV1(operation, {
1691
+ maximumBytes: OH_OPERATION_MAX_BYTES_V1,
1692
+ maximumDepth: OH_SYNC_INGRESS_OPERATION_DEPTH_V1,
1693
+ maximumNodes: OH_SYNC_INGRESS_OPERATION_NODES_V1
1694
+ }, measurement, false) === null ? null : measurement;
1695
+ }
1696
+ function buildOhSyncBundleV1(parsedSpaceId, operations, largestFittingPrefix) {
1219
1697
  let priorSequence = null;
1220
1698
  let priorSha256 = null;
1221
1699
  const parsed = [];
1700
+ const bundleBudget = syncIngressBundleBudgetV1(parsedSpaceId);
1222
1701
  for (const candidate of operations) {
1223
1702
  const operation = parseOhOperationV1(candidate);
1224
1703
  if (operation === null || operation.spaceId !== parsedSpaceId || priorSequence !== null && operation.sequence !== priorSequence + 1 || priorSequence !== null && operation.parentOperationSha256 !== priorSha256) {
1225
1704
  throw new TypeError("Sync operations must form one ordered chain.");
1226
1705
  }
1706
+ const measurement = measureSyncOperationV1(operation);
1707
+ if (measurement === null) {
1708
+ throw new RangeError("Sync operation exceeds its canonical byte, node, or depth limit.");
1709
+ }
1710
+ if (!spendSyncIngressBudgetV1(bundleBudget, measurement.bytes + (parsed.length === 0 ? 0 : 1), measurement.nodes)) {
1711
+ if (largestFittingPrefix && parsed.length > 0)
1712
+ break;
1713
+ throw new RangeError("Sync bundle exceeds its canonical byte or node limit.");
1714
+ }
1227
1715
  parsed.push(operation);
1228
1716
  priorSequence = operation.sequence;
1229
1717
  priorSha256 = operation.operationSha256;
@@ -1237,15 +1725,56 @@ function createOhSyncBundleV1(spaceId, operations) {
1237
1725
  };
1238
1726
  return { ...payload, bundleSha256: canonicalSha256(payload) };
1239
1727
  }
1728
+ function parseSyncHeadRefEnvelopeV1(value) {
1729
+ const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256);
1730
+ const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 && !Object.is(value.sequence, -0) ? value.sequence : null;
1731
+ return sequence !== null && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) ? { operationSha256, sequence } : null;
1732
+ }
1733
+ function parseOhSyncHeadRefV1(value) {
1734
+ const reference = exactDataRecordV1(value, OH_SYNC_HEAD_REF_KEYS_V1);
1735
+ return reference === null ? null : parseSyncHeadRefEnvelopeV1(reference);
1736
+ }
1737
+ function parseOhSyncHeadV1(value) {
1738
+ const envelope = exactDataRecordV1(value, OH_SYNC_HEAD_KEYS_V1);
1739
+ if (envelope === null || envelope.v !== 1)
1740
+ return null;
1741
+ const reference = parseSyncHeadRefEnvelopeV1(envelope);
1742
+ return reference === null ? null : { ...reference, v: 1 };
1743
+ }
1744
+ function createOhSyncBundleV1(spaceId, operations, options = {}) {
1745
+ const parsedSpaceId = safeCode(spaceId);
1746
+ const largestFittingPrefix = options.largestFittingPrefix ?? false;
1747
+ if (parsedSpaceId === null || operations.length > OH_SYNC_BUNDLE_MAX_OPERATIONS_V1 || typeof largestFittingPrefix !== "boolean") {
1748
+ throw new TypeError("Invalid sync bundle.");
1749
+ }
1750
+ return buildOhSyncBundleV1(parsedSpaceId, operations, largestFittingPrefix);
1751
+ }
1240
1752
  function parseOhSyncBundleV1(value) {
1241
- if (!isPlainRecord(value) || !hasExactKeys(value, ["bundleSha256", "contractSha256", "operations", "protocol", "spaceId", "v"]) || value.protocol !== OH_SYNC_PROTOCOL_V1 || value.v !== 1 || !Array.isArray(value.operations))
1753
+ const envelope = exactDataRecordV1(value, OH_SYNC_BUNDLE_KEYS_V1);
1754
+ if (envelope === null || envelope.protocol !== OH_SYNC_PROTOCOL_V1 || envelope.v !== 1)
1242
1755
  return null;
1243
- const bundleSha256 = parseSha256Hex(value.bundleSha256);
1244
- const contractSha256 = parseSha256Hex(value.contractSha256);
1245
- if (bundleSha256 === null || contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256)
1756
+ const bundleSha256 = parseSha256Hex(envelope.bundleSha256);
1757
+ const contractSha256 = parseSha256Hex(envelope.contractSha256);
1758
+ const spaceId = safeCode(envelope.spaceId);
1759
+ if (bundleSha256 === null || contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256 || spaceId === null)
1246
1760
  return null;
1761
+ const operations = exactDataArrayV1(envelope.operations, OH_SYNC_BUNDLE_MAX_OPERATIONS_V1);
1762
+ if (operations === null)
1763
+ return null;
1764
+ const detachedOperations = [];
1765
+ const bundleBudget = syncIngressBundleBudgetV1(spaceId);
1766
+ for (const operation of operations) {
1767
+ if (detachedOperations.length > 0 && !spendSyncIngressBudgetV1(bundleBudget, 1))
1768
+ return null;
1769
+ const detached = stagedSyncOperationV1(operation, bundleBudget);
1770
+ if (detached === null)
1771
+ return null;
1772
+ detachedOperations.push(detached);
1773
+ }
1247
1774
  try {
1248
- const created = createOhSyncBundleV1(value.spaceId, value.operations);
1775
+ const created = createOhSyncBundleV1(spaceId, detachedOperations);
1776
+ if (detachedOperations.some((operation, index) => canonicalJson(operation) !== canonicalJson(created.operations[index])))
1777
+ return null;
1249
1778
  return created.bundleSha256 === bundleSha256 ? { ...created, bundleSha256 } : null;
1250
1779
  } catch {
1251
1780
  return null;
@@ -1260,39 +1789,84 @@ async function synchronizeOhStoreV1(store, transport, options = {}) {
1260
1789
  await transport.handshake(OH_CONTRACT_MANIFEST_V1);
1261
1790
  let pulled = 0;
1262
1791
  let pushed = 0;
1792
+ const settled = (head, rounds) => {
1793
+ store.updateSyncState(remoteId, {
1794
+ pulledSequence: head.sequence,
1795
+ pushedSequence: head.sequence,
1796
+ remoteHeadSha256: head.operationSha256
1797
+ });
1798
+ return { head, pulled, pushed, rounds, v: 1 };
1799
+ };
1263
1800
  for (let round = 1;round <= maximumRounds; round += 1) {
1801
+ const remote = parseOhSyncHeadV1(await transport.head(store.spaceId));
1802
+ if (remote === null)
1803
+ throw new Error("The sync transport returned an invalid head.");
1264
1804
  const local = store.head();
1265
- const remote = await transport.head(store.spaceId);
1266
1805
  if (local.sequence === remote.sequence) {
1267
1806
  if (local.operationSha256 !== remote.operationSha256) {
1268
1807
  throw new Error("Sync conflict: equal sequence numbers have different heads.");
1269
1808
  }
1270
- store.updateSyncState(remoteId, {
1271
- pulledSequence: local.sequence,
1272
- pushedSequence: local.sequence,
1273
- remoteHeadSha256: remote.operationSha256
1274
- });
1275
- return { head: remote, pulled, pushed, rounds: round, v: 1 };
1809
+ return settled(remote, round);
1276
1810
  }
1277
1811
  if (local.sequence < remote.sequence) {
1278
1812
  const bundle = parseOhSyncBundleV1(await transport.pull(store.spaceId, local.sequence, batchSize));
1279
- if (bundle === null || bundle.operations.length === 0 || bundle.operations[0]?.sequence !== local.sequence + 1 || bundle.operations[0]?.parentOperationSha256 !== local.operationSha256) {
1813
+ const terminal = bundle?.operations.at(-1);
1814
+ if (bundle === null || bundle.operations.length === 0 || bundle.operations.length > batchSize || bundle.spaceId !== store.spaceId || bundle.operations[0]?.sequence !== local.sequence + 1 || bundle.operations[0]?.parentOperationSha256 !== local.operationSha256 || terminal === undefined || terminal.sequence > remote.sequence || terminal.sequence === remote.sequence && terminal.operationSha256 !== remote.operationSha256) {
1280
1815
  throw new Error("Sync conflict: remote history does not extend the local head.");
1281
1816
  }
1282
- for (const operation of bundle.operations) {
1283
- store.importOperation(operation);
1284
- pulled += 1;
1817
+ store.importOperations({
1818
+ expectedHead: {
1819
+ operationSha256: local.operationSha256,
1820
+ sequence: local.sequence
1821
+ },
1822
+ operations: bundle.operations
1823
+ });
1824
+ pulled += bundle.operations.length;
1825
+ const afterPull = store.head();
1826
+ if (afterPull.sequence === remote.sequence && afterPull.operationSha256 === remote.operationSha256) {
1827
+ const confirmed = parseOhSyncHeadV1(await transport.head(store.spaceId));
1828
+ if (confirmed === null)
1829
+ throw new Error("The sync transport returned an invalid head.");
1830
+ const confirmedLocal = store.head();
1831
+ if (confirmed.sequence === remote.sequence && confirmed.operationSha256 === remote.operationSha256 && confirmedLocal.sequence === confirmed.sequence && confirmedLocal.operationSha256 === confirmed.operationSha256) {
1832
+ return settled(confirmed, round);
1833
+ }
1285
1834
  }
1286
1835
  } else {
1287
- const operations = store.exportOperations(remote.sequence, batchSize);
1288
- if (operations.length === 0 || operations[0]?.sequence !== remote.sequence + 1 || operations[0]?.parentOperationSha256 !== remote.operationSha256) {
1836
+ const candidates = store.changesSince({
1837
+ operationSha256: remote.operationSha256,
1838
+ sequence: remote.sequence
1839
+ }, {
1840
+ limit: batchSize,
1841
+ through: {
1842
+ operationSha256: local.operationSha256,
1843
+ sequence: local.sequence
1844
+ }
1845
+ }).operations;
1846
+ if (candidates.length === 0 || candidates[0]?.sequence !== remote.sequence + 1 || candidates[0]?.parentOperationSha256 !== remote.operationSha256) {
1289
1847
  throw new Error("Sync conflict: local history does not extend the remote head.");
1290
1848
  }
1291
- const head = await transport.push(createOhSyncBundleV1(store.spaceId, operations));
1849
+ const bundle = createOhSyncBundleV1(store.spaceId, candidates, {
1850
+ largestFittingPrefix: true
1851
+ });
1852
+ const operations = bundle.operations;
1853
+ const head = parseOhSyncHeadV1(await transport.push(bundle));
1854
+ if (head === null)
1855
+ throw new Error("The sync transport returned an invalid push head.");
1292
1856
  if (head.sequence !== operations.at(-1)?.sequence || head.operationSha256 !== operations.at(-1)?.operationSha256) {
1293
1857
  throw new Error("The sync transport acknowledged a different head.");
1294
1858
  }
1295
1859
  pushed += operations.length;
1860
+ const afterPush = store.head();
1861
+ if (afterPush.sequence === head.sequence && afterPush.operationSha256 === head.operationSha256) {
1862
+ const confirmed = parseOhSyncHeadV1(await transport.head(store.spaceId));
1863
+ if (confirmed === null)
1864
+ throw new Error("The sync transport returned an invalid head.");
1865
+ const confirmedLocal = store.head();
1866
+ if (confirmed.sequence === head.sequence && confirmed.operationSha256 === head.operationSha256 && confirmedLocal.sequence === confirmed.sequence && confirmedLocal.operationSha256 === confirmed.operationSha256) {
1867
+ return settled(confirmed, round);
1868
+ }
1869
+ }
1296
1870
  }
1297
1871
  }
1298
1872
  throw new Error("Sync did not settle within maximumRounds.");
@@ -1356,19 +1930,48 @@ function createLibSqlOperationSyncTransportV1(client) {
1356
1930
  },
1357
1931
  head,
1358
1932
  pull: async (spaceId, afterSequence, limit) => {
1933
+ const parsedSpaceId = safeCode(spaceId);
1934
+ if (parsedSpaceId === null || !Number.isSafeInteger(afterSequence) || afterSequence < 0 || Object.is(afterSequence, -0) || !Number.isSafeInteger(limit) || limit < 1 || limit > OH_SYNC_BUNDLE_MAX_OPERATIONS_V1) {
1935
+ throw new TypeError("Invalid sync pull request.");
1936
+ }
1359
1937
  await ensure();
1360
- const result = await client.execute({ sql: `SELECT operation_json FROM oh_sync_operations
1361
- WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?`, args: [spaceId, afterSequence, limit] });
1362
- const operations = result.rows.map((row) => {
1938
+ const bundleBudget = syncIngressBundleBudgetV1(parsedSpaceId);
1939
+ const result = await client.execute({
1940
+ sql: `SELECT operation_json FROM (
1941
+ SELECT sequence, operation_json,
1942
+ row_number() OVER (ORDER BY sequence) AS ordinal,
1943
+ sum(length(CAST(operation_json AS BLOB))) OVER (
1944
+ ORDER BY sequence ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
1945
+ ) AS cumulative_bytes
1946
+ FROM (
1947
+ SELECT sequence, operation_json FROM oh_sync_operations
1948
+ WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?
1949
+ )
1950
+ ) WHERE ordinal = 1 OR cumulative_bytes + ordinal - 1 <= ? ORDER BY sequence`,
1951
+ args: [
1952
+ parsedSpaceId,
1953
+ afterSequence,
1954
+ limit,
1955
+ bundleBudget.maximumBytes - bundleBudget.bytes
1956
+ ]
1957
+ });
1958
+ const rows = result.rows;
1959
+ if (!Array.isArray(rows) || rows.length > limit) {
1960
+ throw new Error("Remote sync pull exceeded its requested row limit.");
1961
+ }
1962
+ const operations = rows.map((row) => {
1363
1963
  const json = rowValue(row, "operation_json", 0);
1364
1964
  if (typeof json !== "string")
1365
1965
  throw new Error("Invalid remote operation JSON.");
1966
+ if (utf8ByteLength(json) > OH_OPERATION_MAX_BYTES_V1) {
1967
+ throw new Error("Remote operation JSON exceeds the canonical operation byte limit.");
1968
+ }
1366
1969
  const operation = parseOhOperationV1(JSON.parse(json));
1367
1970
  if (operation === null || canonicalJson(operation) !== json)
1368
1971
  throw new Error("Invalid remote operation.");
1369
1972
  return operation;
1370
1973
  });
1371
- return createOhSyncBundleV1(spaceId, operations);
1974
+ return createOhSyncBundleV1(parsedSpaceId, operations);
1372
1975
  },
1373
1976
  push: async (value) => {
1374
1977
  await ensure();
@@ -1380,8 +1983,35 @@ function createLibSqlOperationSyncTransportV1(client) {
1380
1983
  const remote = await head(bundle.spaceId);
1381
1984
  const first = bundle.operations[0];
1382
1985
  const last = bundle.operations.at(-1);
1383
- if (remote.sequence === last.sequence && remote.operationSha256 === last.operationSha256)
1384
- return remote;
1986
+ if (remote.sequence >= last.sequence) {
1987
+ const result = await client.execute({ sql: `SELECT sequence, operation_sha256, operation_json
1988
+ FROM oh_sync_operations WHERE space_id = ? AND sequence >= ? AND sequence <= ?
1989
+ ORDER BY sequence LIMIT ?`, args: [
1990
+ bundle.spaceId,
1991
+ first.sequence,
1992
+ last.sequence,
1993
+ bundle.operations.length
1994
+ ] });
1995
+ const rows = result.rows;
1996
+ if (!Array.isArray(rows) || rows.length !== bundle.operations.length) {
1997
+ throw new Error("Sync conflict: remote history does not contain the exact pushed operations.");
1998
+ }
1999
+ for (let index = 0;index < bundle.operations.length; index += 1) {
2000
+ const operation = bundle.operations[index];
2001
+ const row = rows[index];
2002
+ if (row === undefined) {
2003
+ throw new Error("Sync conflict: remote history does not contain the exact pushed operations.");
2004
+ }
2005
+ const sequence = Number(rowValue(row, "sequence", 0));
2006
+ const operationSha256 = parseSha256Hex(rowValue(row, "operation_sha256", 1));
2007
+ const operationJson = rowValue(row, "operation_json", 2);
2008
+ const expectedJson = canonicalJson(operation);
2009
+ if (sequence !== operation.sequence || operationSha256 !== operation.operationSha256 || typeof operationJson !== "string" || utf8ByteLength(operationJson) > OH_OPERATION_MAX_BYTES_V1 || operationJson !== expectedJson) {
2010
+ throw new Error("Sync conflict: remote history differs from the pushed operations.");
2011
+ }
2012
+ }
2013
+ return { operationSha256: last.operationSha256, sequence: last.sequence, v: 1 };
2014
+ }
1385
2015
  if (first.sequence !== remote.sequence + 1 || first.parentOperationSha256 !== remote.operationSha256) {
1386
2016
  throw new Error("Sync conflict: pushed history does not extend the remote head.");
1387
2017
  }
@@ -1443,35 +2073,7 @@ async function searchOhV1(input) {
1443
2073
  // src/sqlite/store.ts
1444
2074
  import { mkdirSync } from "fs";
1445
2075
  import { dirname } from "path";
1446
-
1447
2076
  // src/store.ts
1448
- class OhConflictError extends Error {
1449
- constructor(message) {
1450
- super(message);
1451
- this.name = "OhConflictError";
1452
- }
1453
- }
1454
-
1455
- class OhIntegrityError extends Error {
1456
- constructor(message) {
1457
- super(message);
1458
- this.name = "OhIntegrityError";
1459
- }
1460
- }
1461
-
1462
- class OhDependencyError extends Error {
1463
- constructor(message) {
1464
- super(message);
1465
- this.name = "OhDependencyError";
1466
- }
1467
- }
1468
-
1469
- class OhProfileError extends Error {
1470
- constructor(message) {
1471
- super(message);
1472
- this.name = "OhProfileError";
1473
- }
1474
- }
1475
2077
  var OH_CANONICAL_STORE_PROFILE_V1 = createOhStoreProfileV1({
1476
2078
  applicationProfileSha256: null,
1477
2079
  capabilities: {
@@ -1784,6 +2386,8 @@ function transitionOhSnapshotV1(input) {
1784
2386
  sequence: head.sequence + 1,
1785
2387
  spaceId,
1786
2388
  v: 1
2389
+ }, input.maximumOperationBytes === undefined ? {} : {
2390
+ maximumOperationBytes: input.maximumOperationBytes
1787
2391
  });
1788
2392
  const nextHead = {
1789
2393
  generation: operation.sequence,
@@ -2359,6 +2963,9 @@ function normalizeLimit(value, fallback = 50, maximum = 1000) {
2359
2963
  }
2360
2964
  return value;
2361
2965
  }
2966
+ function exactHeadRef(left, right) {
2967
+ return left.sequence === right.sequence && left.operationSha256 === right.operationSha256;
2968
+ }
2362
2969
  function ftsQuery(value) {
2363
2970
  const normalized = boundedText(value.normalize("NFC"), 4096);
2364
2971
  if (normalized === null)
@@ -2475,8 +3082,7 @@ class OhSqliteStore {
2475
3082
  }
2476
3083
  return records;
2477
3084
  }
2478
- #transition(head, changes, operationId) {
2479
- const records = this.#loadRecords();
3085
+ #transition(head, changes, operationId, records = this.#loadRecords()) {
2480
3086
  for (const change of changes) {
2481
3087
  if (change.kind === "put") {
2482
3088
  records.set(change.record.key, change.record);
@@ -2533,7 +3139,7 @@ class OhSqliteStore {
2533
3139
  if (operation.sequence < 1 || operation.sequence > head.sequence) {
2534
3140
  throw new OhIntegrityError("A stored idempotent operation is not reachable from the current head.");
2535
3141
  }
2536
- const rows = this.database.query(`SELECT operation_sha256, parent_operation_sha256, sequence
3142
+ const rows = this.database.query(`SELECT ${OPERATION_COLUMNS}
2537
3143
  FROM oh_operations WHERE space_id = ? AND sequence >= ? AND sequence <= ? ORDER BY sequence`).all(this.spaceId, operation.sequence, head.sequence);
2538
3144
  if (rows.length !== head.sequence - operation.sequence + 1) {
2539
3145
  throw new OhIntegrityError("A stored idempotent operation has an incomplete path to the current head.");
@@ -2541,10 +3147,14 @@ class OhSqliteStore {
2541
3147
  let priorSha256 = operation.parentOperationSha256;
2542
3148
  for (let index = 0;index < rows.length; index += 1) {
2543
3149
  const row = rows[index];
2544
- if (row === undefined || row.sequence !== operation.sequence + index || row.parent_operation_sha256 !== priorSha256 || index === 0 && row.operation_sha256 !== operation.operationSha256) {
3150
+ if (row === undefined) {
3151
+ throw new OhIntegrityError("A stored idempotent operation is not on the current authority chain.");
3152
+ }
3153
+ const reachable = parseStoredOperationRow(row, { spaceId: this.spaceId });
3154
+ if (reachable.sequence !== operation.sequence + index || reachable.parentOperationSha256 !== priorSha256 || index === 0 && reachable.operationSha256 !== operation.operationSha256) {
2545
3155
  throw new OhIntegrityError("A stored idempotent operation is not on the current authority chain.");
2546
3156
  }
2547
- priorSha256 = row.operation_sha256;
3157
+ priorSha256 = reachable.operationSha256;
2548
3158
  }
2549
3159
  if (priorSha256 !== head.operationSha256) {
2550
3160
  throw new OhIntegrityError("A stored idempotent operation does not reach the current head digest.");
@@ -2605,8 +3215,10 @@ class OhSqliteStore {
2605
3215
  this.#assertOpen();
2606
3216
  const actorId = safeCode(input.actorId);
2607
3217
  const operationId = safeCode(input.operationId);
2608
- if (actorId === null || operationId === null)
2609
- throw new TypeError("Invalid actor or operation ID.");
3218
+ const maximumOperationBytes = input.maximumOperationBytes ?? OH_OPERATION_MAX_BYTES_V1;
3219
+ if (actorId === null || operationId === null || !Number.isSafeInteger(maximumOperationBytes) || maximumOperationBytes < 1 || maximumOperationBytes > OH_OPERATION_MAX_BYTES_V1) {
3220
+ throw new TypeError("Invalid actor, operation ID, or operation byte bound.");
3221
+ }
2610
3222
  const changes = canonicalKnowledgeGraphChangesV1(input.changes);
2611
3223
  if (changes.length === 0 || changes.length > 8192)
2612
3224
  throw new TypeError("A commit needs 1 through 8192 changes.");
@@ -2620,6 +3232,10 @@ class OhSqliteStore {
2620
3232
  if (existing.actorId !== actorId || canonicalJson(existing.changes) !== canonicalJson(changes)) {
2621
3233
  throw new OhConflictError("The operation ID is already bound to different content.");
2622
3234
  }
3235
+ const operationBytes = Buffer.byteLength(canonicalJson(existing), "utf8");
3236
+ if (operationBytes > maximumOperationBytes) {
3237
+ throw new OhOperationSizeError(operationBytes, maximumOperationBytes);
3238
+ }
2623
3239
  return existing;
2624
3240
  }
2625
3241
  if (head.generation !== input.expectedHead.generation || head.operationSha256 !== input.expectedHead.operationSha256) {
@@ -2638,6 +3254,8 @@ class OhSqliteStore {
2638
3254
  sequence: head.sequence + 1,
2639
3255
  spaceId: this.spaceId,
2640
3256
  v: 1
3257
+ }, {
3258
+ maximumOperationBytes
2641
3259
  });
2642
3260
  this.#persist(operation);
2643
3261
  return operation;
@@ -2649,30 +3267,84 @@ class OhSqliteStore {
2649
3267
  const operation = parseOhOperationV1(value);
2650
3268
  if (operation === null || operation.spaceId !== this.spaceId)
2651
3269
  throw new OhIntegrityError("Invalid imported operation.");
3270
+ const result = this.importOperations({
3271
+ expectedHead: {
3272
+ operationSha256: operation.parentOperationSha256,
3273
+ sequence: operation.sequence - 1
3274
+ },
3275
+ operations: [operation]
3276
+ });
3277
+ return { imported: result.imported === 1, operation };
3278
+ }
3279
+ importOperations(input) {
3280
+ this.#assertOpen();
3281
+ this.#assertOperationReplication();
3282
+ const expectedHead = parseOhHeadRefV1(input.expectedHead);
3283
+ if (expectedHead === null || !Array.isArray(input.operations) || input.operations.length > 1000) {
3284
+ throw new TypeError("Invalid operation import interval.");
3285
+ }
3286
+ const operations = input.operations.map((value) => {
3287
+ const operation = parseOhOperationV1(value);
3288
+ if (operation === null || operation.spaceId !== this.spaceId) {
3289
+ throw new OhIntegrityError("Invalid imported operation.");
3290
+ }
3291
+ return operation;
3292
+ });
3293
+ let prior = expectedHead;
3294
+ for (const operation of operations) {
3295
+ if (operation.sequence !== prior.sequence + 1 || operation.parentOperationSha256 !== prior.operationSha256) {
3296
+ throw new OhConflictError("Imported operations do not extend the expected head.");
3297
+ }
3298
+ prior = { operationSha256: operation.operationSha256, sequence: operation.sequence };
3299
+ }
2652
3300
  return withImmediateTransaction(this.database, () => {
2653
- const head = this.head();
2654
- this.#assertCurrentHeadAuthority(head);
2655
- const duplicate = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE operation_sha256 = ?`).get(operation.operationSha256);
2656
- if (duplicate !== null) {
2657
- const existing = parseStoredOperationRow(duplicate, {
2658
- operationSha256: operation.operationSha256,
2659
- spaceId: this.spaceId
2660
- });
2661
- this.#assertOperationReachable(existing, head);
2662
- if (canonicalJson(existing) !== canonicalJson(operation)) {
2663
- throw new OhIntegrityError("An operation digest is bound to different bytes.");
3301
+ const current = this.head();
3302
+ this.#assertCurrentHeadAuthority(current);
3303
+ this.#headAt(expectedHead);
3304
+ if (!exactHeadRef(current, expectedHead)) {
3305
+ if (operations.length === 0 || current.sequence < prior.sequence) {
3306
+ throw new OhConflictError("The imported operation interval does not extend the local head.");
2664
3307
  }
2665
- return { imported: false, operation };
3308
+ for (const operation of operations) {
3309
+ const row = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND sequence = ?`).get(this.spaceId, operation.sequence);
3310
+ if (row === null)
3311
+ throw new OhIntegrityError("An imported replay is missing from the authority chain.");
3312
+ const existing = parseStoredOperationRow(row, { spaceId: this.spaceId });
3313
+ if (existing.operationSha256 !== operation.operationSha256) {
3314
+ throw new OhConflictError("The imported operation interval diverges from the local authority chain.");
3315
+ }
3316
+ if (canonicalJson(existing) !== canonicalJson(operation)) {
3317
+ throw new OhIntegrityError("An operation digest is bound to different bytes.");
3318
+ }
3319
+ }
3320
+ this.#assertOperationReachable(operations[operations.length - 1], current);
3321
+ return { head: current, imported: 0, status: "already-present", v: 1 };
2666
3322
  }
2667
- if (operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256) {
2668
- throw new OhConflictError("The imported operation does not extend the local head.");
3323
+ if (operations.length === 0) {
3324
+ return { head: current, imported: 0, status: "already-present", v: 1 };
2669
3325
  }
2670
- const transition = this.#transition(head, operation.changes, operation.operationId);
2671
- if (transition.recordsSha256 !== operation.recordsSha256 || transition.graphRevisionSha256 !== operation.graphRevisionSha256) {
2672
- throw new OhIntegrityError("The imported operation does not reproduce its declared graph head.");
3326
+ const records = this.#loadRecords();
3327
+ let head = current;
3328
+ for (const operation of operations) {
3329
+ const duplicate = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND operation_id = ?`).get(this.spaceId, operation.operationId);
3330
+ if (duplicate !== null) {
3331
+ throw new OhConflictError("An imported operation ID is already bound on the local authority chain.");
3332
+ }
3333
+ const transition = this.#transition(head, operation.changes, operation.operationId, records);
3334
+ if (transition.recordsSha256 !== operation.recordsSha256 || transition.graphRevisionSha256 !== operation.graphRevisionSha256) {
3335
+ throw new OhIntegrityError("An imported operation does not reproduce its declared graph head.");
3336
+ }
3337
+ this.#persist(operation);
3338
+ head = {
3339
+ generation: operation.sequence,
3340
+ graphRevisionSha256: operation.graphRevisionSha256,
3341
+ operationSha256: operation.operationSha256,
3342
+ recordsSha256: operation.recordsSha256,
3343
+ sequence: operation.sequence,
3344
+ v: 1
3345
+ };
2673
3346
  }
2674
- this.#persist(operation);
2675
- return { imported: true, operation };
3347
+ return { head, imported: operations.length, status: "imported", v: 1 };
2676
3348
  });
2677
3349
  }
2678
3350
  exportOperations(afterSequence = 0, limit = 1000) {
@@ -2890,6 +3562,10 @@ class OhSqliteStore {
2890
3562
  const integrity = this.database.query("PRAGMA integrity_check").get();
2891
3563
  if (integrity?.integrity_check !== "ok")
2892
3564
  throw new OhIntegrityError("SQLite integrity_check failed.");
3565
+ const foreignKeyViolations = this.database.query("PRAGMA foreign_key_check").all();
3566
+ if (foreignKeyViolations.length !== 0) {
3567
+ throw new OhIntegrityError("SQLite foreign_key_check failed.");
3568
+ }
2893
3569
  const storedCount = this.database.query("SELECT count(*) AS count FROM oh_operations WHERE space_id = ?").get(this.spaceId)?.count ?? 0;
2894
3570
  const operations = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? ORDER BY sequence`).all(this.spaceId).map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId }));
2895
3571
  if (operations.length !== storedCount)
@@ -2966,6 +3642,22 @@ class OhSqliteStore {
2966
3642
  if (canonicalJson(storedDependencies) !== canonicalJson(expectedDependencies)) {
2967
3643
  throw new OhIntegrityError("Materialized dependencies do not match operation replay.");
2968
3644
  }
3645
+ const expectedSearchDocuments = [...records.values()].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0).map((record) => ({
3646
+ record_key: record.key,
3647
+ record_sha256: record.recordSha256,
3648
+ text: `${record.key} ${record.kind} ${extractSearchText(record.value)}`
3649
+ }));
3650
+ const storedSearchDocuments = this.database.query(`SELECT record_key, record_sha256, text FROM oh_search_documents
3651
+ WHERE space_id = ? ORDER BY record_key`).all(this.spaceId);
3652
+ if (canonicalJson(storedSearchDocuments) !== canonicalJson(expectedSearchDocuments)) {
3653
+ throw new OhIntegrityError("Materialized search documents do not match operation replay.");
3654
+ }
3655
+ const expectedSearchFts = expectedSearchDocuments.map(({ record_key, text }) => ({ record_key, text }));
3656
+ const storedSearchFts = this.database.query(`SELECT record_key, text FROM oh_search_fts
3657
+ WHERE space_id = ? ORDER BY record_key, text, rowid`).all(this.spaceId);
3658
+ if (canonicalJson(storedSearchFts) !== canonicalJson(expectedSearchFts)) {
3659
+ throw new OhIntegrityError("Materialized full-text search rows do not match operation replay.");
3660
+ }
2969
3661
  const storedOperationRecords = this.database.query(`SELECT materialized.operation_sha256, materialized.ordinal, materialized.record_key,
2970
3662
  materialized.change_kind, materialized.record_sha256
2971
3663
  FROM oh_operation_records AS materialized
@@ -3143,8 +3835,8 @@ class Oh {
3143
3835
  }
3144
3836
 
3145
3837
  // src/cli.ts
3146
- import { readFile } from "fs/promises";
3147
- var OH_PACKAGE_VERSION = "0.3.1";
3838
+ import { lstat, readFile } from "fs/promises";
3839
+ var OH_PACKAGE_VERSION = "0.4.0";
3148
3840
  var KNOWN_OPTIONS = new Set([
3149
3841
  "actor",
3150
3842
  "after",
@@ -3314,7 +4006,7 @@ async function validateInvocation(command, parsed) {
3314
4006
  const file = one(parsed, "file");
3315
4007
  if (file === undefined)
3316
4008
  throw new TypeError("sync import needs --file.");
3317
- syncBundle = parseOhSyncBundleV1(JSON.parse(await readFile(file, "utf8")));
4009
+ syncBundle = parseOhSyncBundleV1(await readSyncBundleFile(file));
3318
4010
  if (syncBundle === null)
3319
4011
  throw new TypeError("Invalid sync bundle.");
3320
4012
  } else {
@@ -3333,6 +4025,18 @@ function print(value) {
3333
4025
  process.stdout.write(`${canonicalJson(value)}
3334
4026
  `);
3335
4027
  }
4028
+ async function readSyncBundleFile(path) {
4029
+ const maximumFileBytes = OH_SYNC_BUNDLE_MAX_BYTES_V1 + 1;
4030
+ const metadata = await lstat(path);
4031
+ if (!metadata.isFile() || !Number.isSafeInteger(metadata.size) || metadata.size > maximumFileBytes) {
4032
+ throw new RangeError(`Sync bundle file must be a regular file of at most ${maximumFileBytes} bytes.`);
4033
+ }
4034
+ const contents = await readFile(path);
4035
+ if (contents.byteLength > maximumFileBytes) {
4036
+ throw new RangeError(`Sync bundle file must be at most ${maximumFileBytes} bytes.`);
4037
+ }
4038
+ return JSON.parse(contents.toString("utf8"));
4039
+ }
3336
4040
  var HELP = `oh ${OH_PACKAGE_VERSION}
3337
4041
 
3338
4042
  Usage:
@@ -3455,18 +4159,28 @@ async function runOhCli(arguments_) {
3455
4159
  if (action === "export") {
3456
4160
  const after = integer(one(parsed, "after"), "after") ?? 0;
3457
4161
  const limit = integer(one(parsed, "limit"), "limit") ?? 1000;
3458
- print(createOhSyncBundleV1(oh.store.spaceId, oh.store.exportOperations(after, limit)));
4162
+ print(createOhSyncBundleV1(oh.store.spaceId, oh.store.exportOperations(after, limit), {
4163
+ largestFittingPrefix: true
4164
+ }));
3459
4165
  return 0;
3460
4166
  }
3461
4167
  if (action === "import") {
3462
4168
  const bundle = validated.syncBundle;
3463
4169
  if (bundle === null)
3464
4170
  throw new TypeError("Invalid prepared sync import command.");
3465
- let imported = 0;
3466
- for (const operation of bundle.operations)
3467
- if (oh.store.importOperation(operation).imported)
3468
- imported += 1;
3469
- print({ head: oh.head(), imported, v: 1 });
4171
+ const first = bundle.operations[0];
4172
+ if (first === undefined) {
4173
+ print({ head: oh.head(), imported: 0, v: 1 });
4174
+ return 0;
4175
+ }
4176
+ const imported = oh.store.importOperations({
4177
+ expectedHead: {
4178
+ operationSha256: first.parentOperationSha256,
4179
+ sequence: first.sequence - 1
4180
+ },
4181
+ operations: bundle.operations
4182
+ });
4183
+ print({ head: imported.head, imported: imported.imported, v: 1 });
3470
4184
  return 0;
3471
4185
  }
3472
4186
  throw new TypeError("sync needs export or import.");