@nubbin/core 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1531,6 +1531,27 @@ declare class NubbinError extends Error {
1531
1531
  */
1532
1532
  declare function parseMatchKind(route: string): RoutePointer["matchKind"];
1533
1533
 
1534
+ /** One value as it existed at a conflicting path. `present` distinguishes deletion from `undefined`. */
1535
+ interface ReconciliationValue {
1536
+ readonly present: boolean;
1537
+ readonly value?: unknown;
1538
+ }
1539
+ /** A path changed differently in both descendants of the same base. */
1540
+ interface DocumentConflict {
1541
+ readonly path: readonly string[];
1542
+ readonly base: ReconciliationValue;
1543
+ readonly local: ReconciliationValue;
1544
+ readonly remote: ReconciliationValue;
1545
+ }
1546
+ /** A local-first working copy plus every choice an author still has to make. */
1547
+ interface DocumentReconciliation {
1548
+ readonly version: DocumentVersion;
1549
+ readonly conflicts: readonly DocumentConflict[];
1550
+ }
1551
+
1552
+ /** Reconciles two descendants of one document version without discarding either side's work. */
1553
+ declare function reconcileDocumentVersion(base: DocumentVersion, local: DocumentVersion, remote: DocumentVersion): DocumentReconciliation;
1554
+
1534
1555
  /**
1535
1556
  * Throws the one-cause refusal almost every Nubbin surface raises, in one line rather than
1536
1557
  * assembling an issue array around it.
@@ -1591,6 +1612,9 @@ declare function refuse(code: NubbinIssue["code"], message: string, at?: string)
1591
1612
  */
1592
1613
  declare function removeNode(version: DocumentVersion, nodeId: string): DocumentVersion;
1593
1614
 
1615
+ /** Applies one explicit local-or-remote conflict choice to a reconciled working document. */
1616
+ declare function resolveDocumentConflict(version: DocumentVersion, conflict: DocumentConflict, choice: "local" | "remote"): DocumentVersion;
1617
+
1594
1618
  /**
1595
1619
  * A schema `core` writes itself, such as the one `richText()` returns. It satisfies both
1596
1620
  * `StandardSchemaV1` and `StandardJSONSchemaV1`, so a block, a catalog entry or an adapter takes
@@ -1738,4 +1762,4 @@ declare function setAtPath(target: Record<string, unknown>, path: string, value:
1738
1762
  */
1739
1763
  declare function setNodeProp(version: DocumentVersion, nodeId: string, path: string, value: unknown): DocumentVersion;
1740
1764
 
1741
- export { type Artifact, type ArtifactNode, type ArtifactStore, type Block, type BlockDrift, type BlockUi, type Catalog, type CatalogEntry, type CompatibilityReport, type CompileResult, type DocumentMeta, type DocumentVersion, type FieldHint, type FieldHintData, type FieldKind, type FieldNode, type Holes, type InferProps, type LiveRoute, type Manifest, type Node, NubbinError, type NubbinIssue, NubbinIssueCode, type PointerMove, RICH_TEXT_BLOCK_KINDS, RICH_TEXT_MARKS, type Registry, type RichText, type RichTextBlock, type RichTextBlockKind, type RichTextMark, type RichTextSpan, type RollbackCheck, type RouteIncompatibility, type RoutePointer, type SchemaAdapter, type SlotConstraint, type StandardDataSchema, type UnknownProps, addNode, checkCompatibility, checkRollback, compile, createRegistry, defineBlock, defineCatalog, formatCompatibilityReport, isRichTextBlockKind, isRichTextMark, moveNode, parseMatchKind, refuse, removeNode, richText, setAtPath, setNodeProp, zodAdapter };
1765
+ export { type Artifact, type ArtifactNode, type ArtifactStore, type Block, type BlockDrift, type BlockUi, type Catalog, type CatalogEntry, type CompatibilityReport, type CompileResult, type DocumentConflict, type DocumentMeta, type DocumentReconciliation, type DocumentVersion, type FieldHint, type FieldHintData, type FieldKind, type FieldNode, type Holes, type InferProps, type LiveRoute, type Manifest, type Node, NubbinError, type NubbinIssue, NubbinIssueCode, type PointerMove, RICH_TEXT_BLOCK_KINDS, RICH_TEXT_MARKS, type ReconciliationValue, type Registry, type RichText, type RichTextBlock, type RichTextBlockKind, type RichTextMark, type RichTextSpan, type RollbackCheck, type RouteIncompatibility, type RoutePointer, type SchemaAdapter, type SlotConstraint, type StandardDataSchema, type UnknownProps, addNode, checkCompatibility, checkRollback, compile, createRegistry, defineBlock, defineCatalog, formatCompatibilityReport, isRichTextBlockKind, isRichTextMark, moveNode, parseMatchKind, reconcileDocumentVersion, refuse, removeNode, resolveDocumentConflict, richText, setAtPath, setNodeProp, zodAdapter };
package/dist/index.js CHANGED
@@ -883,7 +883,7 @@ function validateStructure(version, registry) {
883
883
  }
884
884
 
885
885
  // src/version.constants.ts
886
- var NUBBIN_VERSION = "0.3.0";
886
+ var NUBBIN_VERSION = "0.5.0";
887
887
 
888
888
  // src/compile.ts
889
889
  function compile(version, catalog, registry, route) {
@@ -1112,6 +1112,97 @@ function parseMatchKind(route) {
1112
1112
  return "exact";
1113
1113
  }
1114
1114
 
1115
+ // src/reconciliationValueAt.ts
1116
+ function reconciliationValueAt(record, key) {
1117
+ return Object.hasOwn(record, key) ? { present: true, value: record[key] } : { present: false };
1118
+ }
1119
+
1120
+ // src/reconcileRecords.ts
1121
+ function reconcileRecords([base, local, remote], path, reconcile) {
1122
+ const merged = {};
1123
+ const conflicts = [];
1124
+ const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(local), ...Object.keys(remote)]);
1125
+ for (const key of keys) {
1126
+ const result = reconcile(
1127
+ reconciliationValueAt(base, key),
1128
+ reconciliationValueAt(local, key),
1129
+ reconciliationValueAt(remote, key),
1130
+ [...path, key]
1131
+ );
1132
+ if (result.value.present) merged[key] = result.value.value;
1133
+ conflicts.push(...result.conflicts);
1134
+ }
1135
+ return { value: { present: true, value: merged }, conflicts };
1136
+ }
1137
+
1138
+ // src/isPlainRecord.ts
1139
+ function isPlainRecord(value) {
1140
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1141
+ const prototype = Object.getPrototypeOf(value);
1142
+ return prototype === Object.prototype || prototype === null;
1143
+ }
1144
+
1145
+ // src/sameReconciliationValue.ts
1146
+ function sameReconciliationValue(left, right) {
1147
+ if (left.present !== right.present) return false;
1148
+ if (!left.present) return true;
1149
+ if (Object.is(left.value, right.value)) return true;
1150
+ if (Array.isArray(left.value) && Array.isArray(right.value)) {
1151
+ const leftArray = left.value;
1152
+ const rightArray = right.value;
1153
+ return leftArray.length === rightArray.length && leftArray.every(
1154
+ (value, index) => sameReconciliationValue(
1155
+ { present: true, value },
1156
+ { present: true, value: rightArray[index] }
1157
+ )
1158
+ );
1159
+ }
1160
+ if (!isPlainRecord(left.value) || !isPlainRecord(right.value)) return false;
1161
+ const leftRecord = left.value;
1162
+ const rightRecord = right.value;
1163
+ const leftKeys = Object.keys(leftRecord);
1164
+ const rightKeys = Object.keys(rightRecord);
1165
+ return leftKeys.length === rightKeys.length && leftKeys.every(
1166
+ (key) => Object.hasOwn(rightRecord, key) && sameReconciliationValue(
1167
+ { present: true, value: leftRecord[key] },
1168
+ { present: true, value: rightRecord[key] }
1169
+ )
1170
+ );
1171
+ }
1172
+
1173
+ // src/toPlainRecord.ts
1174
+ function toPlainRecord(value) {
1175
+ return value.present && isPlainRecord(value.value) ? value.value : void 0;
1176
+ }
1177
+
1178
+ // src/reconcileValue.ts
1179
+ function reconcileValue(base, local, remote, path = []) {
1180
+ if (sameReconciliationValue(local, remote)) return { value: local, conflicts: [] };
1181
+ if (sameReconciliationValue(local, base)) return { value: remote, conflicts: [] };
1182
+ if (sameReconciliationValue(remote, base)) return { value: local, conflicts: [] };
1183
+ const records = [toPlainRecord(base), toPlainRecord(local), toPlainRecord(remote)];
1184
+ if (records.some((value) => value === void 0)) {
1185
+ return { value: local, conflicts: [{ path, base, local, remote }] };
1186
+ }
1187
+ return reconcileRecords(records, path, reconcileValue);
1188
+ }
1189
+
1190
+ // src/reconcileDocumentVersion.ts
1191
+ function reconcileDocumentVersion(base, local, remote) {
1192
+ if (base.documentId !== local.documentId || base.documentId !== remote.documentId) {
1193
+ throw new TypeError("cannot reconcile versions from different documents");
1194
+ }
1195
+ const result = reconcileValue(
1196
+ { present: true, value: base },
1197
+ { present: true, value: local },
1198
+ { present: true, value: remote }
1199
+ );
1200
+ return {
1201
+ version: result.value.value,
1202
+ conflicts: result.conflicts
1203
+ };
1204
+ }
1205
+
1115
1206
  // src/removeNode.ts
1116
1207
  function removeNode(version, nodeId) {
1117
1208
  requireNode(version, nodeId);
@@ -1126,6 +1217,28 @@ function removeNode(version, nodeId) {
1126
1217
  return { ...detached, elements };
1127
1218
  }
1128
1219
 
1220
+ // src/withReconciliationValueAtPath.ts
1221
+ function withReconciliationValueAtPath(root, path, replacement) {
1222
+ const [head, ...tail] = path;
1223
+ if (head === void 0) return root;
1224
+ const record = root;
1225
+ const result = { ...record };
1226
+ if (tail.length === 0) {
1227
+ if (replacement.present) result[head] = replacement.value;
1228
+ else delete result[head];
1229
+ return result;
1230
+ }
1231
+ const child = record[head];
1232
+ if (typeof child !== "object" || child === null || Array.isArray(child)) return root;
1233
+ result[head] = withReconciliationValueAtPath(child, tail, replacement);
1234
+ return result;
1235
+ }
1236
+
1237
+ // src/resolveDocumentConflict.ts
1238
+ function resolveDocumentConflict(version, conflict, choice) {
1239
+ return withReconciliationValueAtPath(version, conflict.path, conflict[choice]);
1240
+ }
1241
+
1129
1242
  // src/defineStandardSchema.ts
1130
1243
  var STANDARD_SCHEMA_VERSION = 1;
1131
1244
  function defineStandardSchema(issuesOf, jsonSchemaOf) {
@@ -1296,8 +1409,10 @@ export {
1296
1409
  isRichTextMark,
1297
1410
  moveNode,
1298
1411
  parseMatchKind,
1412
+ reconcileDocumentVersion,
1299
1413
  refuse,
1300
1414
  removeNode,
1415
+ resolveDocumentConflict,
1301
1416
  richText,
1302
1417
  setAtPath,
1303
1418
  setNodeProp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubbin/core",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "The Nubbin contract: define blocks, split catalog from registry, and compile a page document into an immutable artifact.",
5
5
  "keywords": [
6
6
  "nubbin",