@ai-matrx/office 0.8.21 → 0.8.23

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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.23
4
+ ### C9: comment anchors that follow concurrent edits
5
+
6
+ - **Docs:** `addCommentAnchorActions(body, start, end, id)` anchors a passage with a `COMMENT` custom range (Univer's own builder, so the op has the link family's shape C0 proved); `commentAnchorsOf(body)` reads every anchor back (split pieces joined, Univer's inclusive end as `[start, end)`); `removeCommentAnchorActions` takes a cancelled draft's anchor out; `commentPartAnchor` / `anchorIdOfPartAnchor` are the thread's `part_anchor` (`anchor:<id>`, quote as `label`). Fuzz on the C0 corpora: anchor × every captured family and anchor × anchor, 0 divergences in 180,000 checks at 2,000 seeds; red check without a transform.
7
+ - **Sheets:** `installSheetCommentAnchors(univer)` registers `matrx.mutation.set-comment-anchor`, the `SHEET_MATRX_COMMENT_ANCHORS_PLUGIN` resource and a listener that moves anchors through every applied row/column insert, remove and move (and drops them with their row, column or sheet). `transformCommentAnchor` (own file) is called first by `transformMutation`; the registry declares the id (family `anchor`), and the census exempts our own ids from the Univer scan. Fuzz: 40,000 checks, 0 divergences (it caught the same anchor id on two sheets being treated as one); two headless seats: B5 becomes B6 when a peer inserts a row above, survives save and reload, and is gone on both seats when its row is deleted. The fold installs the anchors too.
8
+ - New export `./sheets/comment-anchors`. Consumer action: call `installSheetCommentAnchors(univer)` before creating a workbook unit that co-edits comments.
9
+
10
+ ## 0.8.22
11
+ ### C5c: Sheets co-editing — merge divergence fixed, number formats, counts and sheets travel
12
+
13
+ - **Merge divergence across chained row/column deletes, fixed at its cause.** Univer 1.0.4's structural commands only fix the merges that intersect the COMMAND's range on the other axis (any `-by-range` call with a partial range, an API call) and only while its ref-range registration is current (never refreshed after a content load or a remove-merge). A cut merge it skipped stayed put on the sender while every transform moved it, so seats ended a column apart (the session fuzz's merge at column -1 vs 0). Capture now completes the command (`mergeFixupsOwed`): the cut merges still on the sheet are removed right before the structural mutation and re-added at their model position right after it, in the traveling group and on the sender alike. Multi-seat session fuzz: 0 hash mismatches (was 2–18 per run).
14
+ - **Number formats travel** (`sheet.mutation.set.numfmt` / `remove.numfmt`, family `numfmt`): typing a date or a currency no longer rolls back. Ranges follow row/column inserts, removes and moves cell by cell; against another write of the style key `n` (a numfmt, a style with `n`, a style clear, a cell delete) the later-committed op wins.
15
+ - **Row/column counts travel** (`set-worksheet-row-count` / `-column-count`, family `count`): a concurrent insert/remove on that axis shifts the count; two counts at once, the larger wins. "Refresh from table" growing a sheet is no longer rolled back.
16
+ - **Sheets: add, remove, order travel** (family `sheet`): the sheet order is transformed as one more axis. Capture records a removed sheet's index and how many sheets are left (`annotateSheetOps`, side-car `sheetIndex`, `sheetsLeft`); an insert-sheet's styles are inlined (`inlineSheetStyles`). An edit to a sheet removed at the same time is dropped and its author told; a formula naming a removed sheet, and two removes that would leave no sheet, are barriers.
17
+ - **Sort travels** (`reorder-range`, family `sort`): it is a row permutation inside its range, so concurrent edits follow their rows (a cell op inside the sorted columns lands where the sort moved its row; number formats likewise), a row insert keeps the new rows in place, column inserts/removes widen or clip the range. Barriers: two overlapping sorts, a row/column move through the sorted range, a row remove of rows the sort moved. The inverse is the inverted permutation.
18
+ - **Selections: a seat's own row/column/count/sheet change keeps its selection inside the sheet** (API calls set no selection; the editor crashed with "Range is out of bounds"); a remote count/sheet change clamps it too.
19
+ - **Transform speed:** the group transform walks the OT grid iteratively; untouched cell ops pass through by identity (no overlap, no formula, change elsewhere); barrier checks are cached per op; undo-stack rebase reuses each list's traveling shape and compares by identity.
20
+ - **Tests:** new chained TP1 fuzz (`sheet-transform-chain-fuzz.test.ts`, 3–5 real commands per side, plus capture fidelity); the multi-seat session fuzz is seed-repeatable (`createSheetsOtAdapter` hook `stateHash`, a synchronous hash in tests) and asserts zero incidents. `SHEET_TRANSFORMER_VERSION` = `c5c-1`.
21
+ - `SHEETS_COLLAB_PROTO` = 2: a room opened by this build is raised to proto 2 (when its checkpoint is at head) and older tabs open it read-only, so seats with and without merge completion never share a room.
22
+ - Consumer action: none.
23
+
3
24
  ## 0.8.21
4
25
  ### C5c: Sheets co-editing — merge divergence fixed, number formats, counts and sheets travel
5
26
 
@@ -86,6 +86,9 @@ var SHEET_MUTATIONS = {
86
86
  "sheet.mutation.insert-sheet": covered("sheet", "index", "sheet"),
87
87
  "sheet.mutation.remove-sheet": covered("sheet", "sheetIndex", "sheetsLeft"),
88
88
  "sheet.mutation.set-worksheet-order": covered("sheet", "fromOrder", "toOrder"),
89
+ // ── covered (C9): our own cell comment anchors (comment-anchor-transform.ts; not emitted by Univer) ──────
90
+ "matrx.mutation.set-comment-anchor": covered("anchor", "anchorId", "cell"),
91
+ "sheet.mutation.reorder-range": covered("sort", "range", "order"),
89
92
  // ── never travels ────────────────────────────────────────────────────────────────────────────────
90
93
  "formula.mutation.set-formula-calculation-start": never(FORMULA_RESULT),
91
94
  "formula.mutation.set-trigger-formula-calculation-start": never(FORMULA_RESULT),
@@ -119,7 +122,6 @@ var SHEET_MUTATIONS = {
119
122
  "thread-comment.mutation.resolve-comment": never(UNIVER_COMMENTS),
120
123
  "thread-comment.mutation.delete-comment": never(UNIVER_COMMENTS),
121
124
  // ── barriers: X (no transform yet, design §10) ───────────────────────────────────────────────────
122
- "sheet.mutation.reorder-range": barrier("X", "sort: a permutation, not transformed yet"),
123
125
  "sheet.mutation.move-range": barrier("X", "cut-paste carries the sender's cell matrices"),
124
126
  "sheet.mutation.set-worksheet-hidden": barrier("X", "sheet-level setting: not in C5a"),
125
127
  "sheet.mutation.set-tab-color": barrier("X", "sheet-level setting: not in C5a"),
@@ -528,6 +530,74 @@ function refSheetsFor(texts, sheets) {
528
530
  return Object.keys(out).length ? out : void 0;
529
531
  }
530
532
 
533
+ // src/collab/sheets/comment-anchor-transform.ts
534
+ var SET_SHEET_COMMENT_ANCHOR = "matrx.mutation.set-comment-anchor";
535
+ var REMOVE_SHEET = "sheet.mutation.remove-sheet";
536
+ function structuralAxisOp(id, params) {
537
+ const p = params;
538
+ switch (id) {
539
+ case "sheet.mutation.insert-row":
540
+ return p.range ? { axis: "row", op: { kind: "insert", at: p.range.startRow, count: p.range.endRow - p.range.startRow + 1 } } : null;
541
+ case "sheet.mutation.remove-rows":
542
+ return p.range ? { axis: "row", op: { kind: "remove", start: p.range.startRow, count: p.range.endRow - p.range.startRow + 1 } } : null;
543
+ case "sheet.mutation.insert-col":
544
+ return p.range ? { axis: "col", op: { kind: "insert", at: p.range.startColumn, count: p.range.endColumn - p.range.startColumn + 1 } } : null;
545
+ case "sheet.mutation.remove-col":
546
+ return p.range ? { axis: "col", op: { kind: "remove", start: p.range.startColumn, count: p.range.endColumn - p.range.startColumn + 1 } } : null;
547
+ case "sheet.mutation.move-rows":
548
+ return p.sourceRange && p.targetRange ? { axis: "row", op: { kind: "move", from: p.sourceRange.startRow, count: p.sourceRange.endRow - p.sourceRange.startRow + 1, to: p.targetRange.startRow } } : null;
549
+ case "sheet.mutation.move-columns":
550
+ return p.sourceRange && p.targetRange ? {
551
+ axis: "col",
552
+ op: { kind: "move", from: p.sourceRange.startColumn, count: p.sourceRange.endColumn - p.sourceRange.startColumn + 1, to: p.targetRange.startColumn }
553
+ } : null;
554
+ default:
555
+ return null;
556
+ }
557
+ }
558
+ function moveSheetAnchorCell(cell, sheet, id, params) {
559
+ if (String(params.subUnitId ?? "") !== sheet) return cell;
560
+ if (id === REMOVE_SHEET) return null;
561
+ const s = structuralAxisOp(id, params);
562
+ if (!s) return cell;
563
+ if (s.axis === "row") {
564
+ const row = mapIndex(s.op, cell.row);
565
+ return row === null ? null : row === cell.row ? cell : { row, col: cell.col };
566
+ }
567
+ const col = mapIndex(s.op, cell.col);
568
+ return col === null ? null : col === cell.col ? cell : { row: cell.row, col };
569
+ }
570
+ function colName(c) {
571
+ let s = "";
572
+ for (let n = c + 1; n > 0; n = Math.floor((n - 1) / 26)) s = String.fromCharCode(65 + (n - 1) % 26) + s;
573
+ return s;
574
+ }
575
+ var sheetCellName = (cell) => `${colName(cell.col)}${cell.row + 1}`;
576
+ function cellOf(m) {
577
+ const c = m.params.cell;
578
+ return c && typeof c.row === "number" && typeof c.col === "number" ? c : null;
579
+ }
580
+ var ok = (op, lost = [], dropped = []) => ({ op, lost, dropped });
581
+ var label = (m) => {
582
+ const c = cellOf(m);
583
+ return `${String(m.params.subUnitId ?? "")}!${c ? sheetCellName(c) : "?"} comment`;
584
+ };
585
+ function transformCommentAnchor(x, y, side) {
586
+ const xa = x.id === SET_SHEET_COMMENT_ANCHOR;
587
+ const ya = y.id === SET_SHEET_COMMENT_ANCHOR;
588
+ if (!xa && !ya) return null;
589
+ if (!xa) return ok([x]);
590
+ if (ya) {
591
+ if (x.params.anchorId !== y.params.anchorId || x.params.subUnitId !== y.params.subUnitId) return ok([x]);
592
+ return side === "left" ? ok([x], [label(y)]) : ok([], [], [label(x)]);
593
+ }
594
+ const cell = cellOf(x);
595
+ if (!cell) return ok([x]);
596
+ const moved = moveSheetAnchorCell(cell, String(x.params.subUnitId ?? ""), y.id, y.params);
597
+ if (!moved) return ok([], [], [label(x)]);
598
+ return ok([moved === cell ? x : { id: x.id, params: { ...x.params, cell: moved } }]);
599
+ }
600
+
531
601
  // src/collab/sheets/transform/transform.ts
532
602
  var SHEET_TRANSFORMER_VERSION = "c5c-1";
533
603
  var ID = {
@@ -553,21 +623,22 @@ var ID = {
553
623
  colCount: "sheet.mutation.set-worksheet-column-count",
554
624
  insertSheet: "sheet.mutation.insert-sheet",
555
625
  removeSheet: "sheet.mutation.remove-sheet",
556
- sheetOrder: "sheet.mutation.set-worksheet-order"
626
+ sheetOrder: "sheet.mutation.set-worksheet-order",
627
+ sort: "sheet.mutation.reorder-range"
557
628
  };
558
629
  var PAIR_BARRIERS = {};
559
630
  var pairKey = (a, b) => a < b ? `${a}|${b}` : `${b}|${a}`;
560
631
  var barrier2 = (reason) => ({ barrier: true, reason });
561
- var ok = (op, lost = [], dropped = []) => ({ op, lost, dropped });
632
+ var ok2 = (op, lost = [], dropped = []) => ({ op, lost, dropped });
562
633
  var flip = (side) => side === "left" ? "right" : "left";
563
634
  var sheetOf = (m) => String(m.params.subUnitId ?? "");
564
- function colName(c) {
635
+ function colName2(c) {
565
636
  let s = "";
566
637
  for (let n = c + 1; n > 0; n = Math.floor((n - 1) / 26)) s = String.fromCharCode(65 + (n - 1) % 26) + s;
567
638
  return s;
568
639
  }
569
- var cellRef = (sheet, row, col) => `${sheet}!${colName(col)}${row + 1}`;
570
- var rangeRef = (sheet, r) => `${sheet}!${colName(r.startColumn)}${r.startRow + 1}:${colName(r.endColumn)}${r.endRow + 1}`;
640
+ var cellRef = (sheet, row, col) => `${sheet}!${colName2(col)}${row + 1}`;
641
+ var rangeRef = (sheet, r) => `${sheet}!${colName2(r.startColumn)}${r.startRow + 1}:${colName2(r.endColumn)}${r.endRow + 1}`;
571
642
  function structuralOf(m) {
572
643
  const p = m.params;
573
644
  switch (m.id) {
@@ -822,9 +893,9 @@ function transformCell(X, Y, side) {
822
893
  return { entries: Object.keys(out).length ? [out, ...tail] : tail, xLost, yLost };
823
894
  }
824
895
  function transformCellsOverCells(x, y, side) {
825
- if (sheetOf(x) !== sheetOf(y)) return ok([x]);
896
+ if (sheetOf(x) !== sheetOf(y)) return ok2([x]);
826
897
  const ycells = cellsOf(y);
827
- if (!cellsOverlap(cellsOf(x), ycells)) return ok([x]);
898
+ if (!cellsOverlap(cellsOf(x), ycells)) return ok2([x]);
828
899
  const xd = isDerived(x);
829
900
  const yd = isDerived(y);
830
901
  const eff = derivedSide(x, y, side);
@@ -850,7 +921,7 @@ function transformCellsOverCells(x, y, side) {
850
921
  });
851
922
  if (why) return barrier2(why);
852
923
  const op = passes.filter((p) => !isEmptyMatrix(p)).map((p) => cellsMutation(x, p));
853
- return ok(op, lost, dropped);
924
+ return ok2(op, lost, dropped);
854
925
  }
855
926
  var SIDE_CAR_PARAMS = ["refSheets", "previousName", "derived", "sheetIndex", "sheetsLeft"];
856
927
  var isDerived = (m) => m.params.derived === true;
@@ -921,7 +992,7 @@ function cellThrough(sheet, change, r, c) {
921
992
  return nr === null || nc === null ? null : [nr, nc];
922
993
  }
923
994
  function cellsOverChange(x, change) {
924
- if (!hasFormula(x) && (change.kind !== "axis" || change.sheet !== sheetOf(x))) return ok([x]);
995
+ if (!hasFormula(x) && (change.kind !== "axis" || change.sheet !== sheetOf(x))) return ok2([x]);
925
996
  const ctx = formulaCtx(x);
926
997
  const out = {};
927
998
  const dropped = [];
@@ -950,9 +1021,9 @@ function cellsOverChange(x, change) {
950
1021
  });
951
1022
  if (why) return barrier2(why);
952
1023
  const rsAfter = refSheetsAfter(ctx.refSheets, change);
953
- if (!changed && JSON.stringify(rsAfter ?? null) === JSON.stringify(ctx.refSheets ?? null)) return ok([x]);
1024
+ if (!changed && JSON.stringify(rsAfter ?? null) === JSON.stringify(ctx.refSheets ?? null)) return ok2([x]);
954
1025
  const params = { ...withRefSheets(x.params, rsAfter), cellValue: out };
955
- return ok(isEmptyMatrix(out) ? [] : [{ id: x.id, params }], [], dropped);
1026
+ return ok2(isEmptyMatrix(out) ? [] : [{ id: x.id, params }], [], dropped);
956
1027
  }
957
1028
  function rewritesOf(y, change) {
958
1029
  if (!hasFormula(y)) return null;
@@ -999,36 +1070,36 @@ function formulaPair(x, y, side) {
999
1070
  const fy = familyOf(y.id);
1000
1071
  const SET_NAME_BARRIER = "set-defined-name without previousName (capture must record the name it replaces) against formula text";
1001
1072
  if (fx === "defname" && fy === "defname") {
1002
- if (String(x.params.id) !== String(y.params.id)) return ok([x]);
1073
+ if (String(x.params.id) !== String(y.params.id)) return ok2([x]);
1003
1074
  const eff = derivedSide(x, y, side);
1004
- if (eff === "left") return ok([x], isDerived(y) ? [] : [`defined name "${String(y.params.name)}"`]);
1005
- return ok([], [], isDerived(x) ? [] : [`defined name "${String(x.params.name)}"`]);
1075
+ if (eff === "left") return ok2([x], isDerived(y) ? [] : [`defined name "${String(y.params.name)}"`]);
1076
+ return ok2([], [], isDerived(x) ? [] : [`defined name "${String(x.params.name)}"`]);
1006
1077
  }
1007
1078
  if (fx === "defname") {
1008
1079
  if (fy === "cell") {
1009
1080
  if (setNameWithoutPrevious(x) && hasFormula(y)) return barrier2(SET_NAME_BARRIER);
1010
1081
  const ch2 = refChangeOf(x);
1011
- if (!ch2) return ok([x]);
1082
+ if (!ch2) return ok2([x]);
1012
1083
  const add = rewritesOf(y, ch2);
1013
1084
  if (typeof add === "string") return barrier2(add);
1014
- return ok(add ? [x, add] : [x]);
1085
+ return ok2(add ? [x, add] : [x]);
1015
1086
  }
1016
1087
  const ch = refChangeOf(y);
1017
- if (!ch) return ok([x]);
1088
+ if (!ch) return ok2([x]);
1018
1089
  const r = definedNameThrough(x, ch);
1019
- return typeof r === "string" ? barrier2(r) : ok([r.op]);
1090
+ return typeof r === "string" ? barrier2(r) : ok2([r.op]);
1020
1091
  }
1021
1092
  if (fy === "defname") {
1022
1093
  if (fx === "cell") {
1023
1094
  if (setNameWithoutPrevious(y) && hasFormula(x)) return barrier2(SET_NAME_BARRIER);
1024
1095
  const ch2 = refChangeOf(y);
1025
- return ch2 ? cellsOverChange(x, ch2) : ok([x]);
1096
+ return ch2 ? cellsOverChange(x, ch2) : ok2([x]);
1026
1097
  }
1027
1098
  const ch = refChangeOf(x);
1028
- if (!ch) return ok([x]);
1099
+ if (!ch) return ok2([x]);
1029
1100
  const r = definedNameThrough(y, ch);
1030
1101
  if (typeof r === "string") return barrier2(r);
1031
- return ok(r.changed ? [x, asDerived(r.op)] : [x]);
1102
+ return ok2(r.changed ? [x, asDerived(r.op)] : [x]);
1032
1103
  }
1033
1104
  if (fx === "cell") {
1034
1105
  const ch = refChangeOf(y);
@@ -1043,7 +1114,7 @@ function formulaPair(x, y, side) {
1043
1114
  });
1044
1115
  const add = rewritesOf(y, ch);
1045
1116
  if (typeof add === "string") return barrier2(add);
1046
- return ok(add ? [x, add] : [x], lost);
1117
+ return ok2(add ? [x, add] : [x], lost);
1047
1118
  }
1048
1119
  return null;
1049
1120
  }
@@ -1191,20 +1262,20 @@ function numfmtPair(x, y, side, sx, sy) {
1191
1262
  const nx = isNumfmt(x);
1192
1263
  const ny = isNumfmt(y);
1193
1264
  if (!nx && !ny) return null;
1194
- if (nx && sy) return ok(nfBuild(x, nfParts(x).map((p) => ({ ...p, ranges: rectsThrough(sy, p.ranges) }))));
1195
- if (ny && sx) return ok([x]);
1265
+ if (nx && sy) return ok2(nfBuild(x, nfParts(x).map((p) => ({ ...p, ranges: rectsThrough(sy, p.ranges) }))));
1266
+ if (ny && sx) return ok2([x]);
1196
1267
  if (nx && ny) {
1197
- if (side === "left") return ok([x]);
1268
+ if (side === "left") return ok2([x]);
1198
1269
  const ys = nfParts(y).flatMap((p) => p.ranges);
1199
- return ok(nfBuild(x, nfParts(x).map((p) => ({ ...p, ranges: rectsMinus(p.ranges, ys) }))));
1270
+ return ok2(nfBuild(x, nfParts(x).map((p) => ({ ...p, ranges: rectsMinus(p.ranges, ys) }))));
1200
1271
  }
1201
1272
  if (nx && y.id === ID.values) {
1202
- if (side === "left") return ok([x]);
1273
+ if (side === "left") return ok2([x]);
1203
1274
  const ys = nWrites(y);
1204
- return ys.length ? ok(nfBuild(x, nfParts(x).map((p) => ({ ...p, ranges: rectsMinus(p.ranges, ys) })))) : ok([x]);
1275
+ return ys.length ? ok2(nfBuild(x, nfParts(x).map((p) => ({ ...p, ranges: rectsMinus(p.ranges, ys) })))) : ok2([x]);
1205
1276
  }
1206
1277
  if (ny && x.id === ID.values) {
1207
- if (side === "left") return ok([x]);
1278
+ if (side === "left") return ok2([x]);
1208
1279
  const out = {};
1209
1280
  const tail = {};
1210
1281
  let touched = false;
@@ -1230,31 +1301,31 @@ function numfmtPair(x, y, side, sx, sy) {
1230
1301
  }
1231
1302
  putCell(out, r, c, v);
1232
1303
  });
1233
- if (!touched) return ok([x]);
1304
+ if (!touched) return ok2([x]);
1234
1305
  const ops = [];
1235
1306
  if (!isEmptyMatrix(out)) ops.push(cellsMutation(x, out));
1236
1307
  if (!isEmptyMatrix(tail)) ops.push({ id: ID.values, params: { subUnitId: sheetOf(x), cellValue: tail } });
1237
- return ok(ops);
1308
+ return ok2(ops);
1238
1309
  }
1239
- return ok([x]);
1310
+ return ok2([x]);
1240
1311
  }
1241
1312
  var isCount = (m) => m.id === ID.rowCount || m.id === ID.colCount;
1242
1313
  var countAxis = (m) => m.id === ID.rowCount ? "row" : "col";
1243
1314
  var countKey = (m) => m.id === ID.rowCount ? "rowCount" : "columnCount";
1244
1315
  function countPair(x, y, sy) {
1245
- if (!isCount(x)) return isCount(y) ? ok([x]) : null;
1316
+ if (!isCount(x)) return isCount(y) ? ok2([x]) : null;
1246
1317
  const k = countKey(x);
1247
1318
  const v = Number(x.params[k]);
1248
1319
  if (sy) {
1249
- if (sy.axis !== countAxis(x) || sy.op.kind === "move") return ok([x]);
1320
+ if (sy.axis !== countAxis(x) || sy.op.kind === "move") return ok2([x]);
1250
1321
  const d = sy.op.kind === "insert" ? sy.op.count : -sy.op.count;
1251
- return ok([{ id: x.id, params: { ...x.params, [k]: Math.max(1, v + d) } }]);
1322
+ return ok2([{ id: x.id, params: { ...x.params, [k]: Math.max(1, v + d) } }]);
1252
1323
  }
1253
1324
  if (isCount(y) && y.id === x.id) {
1254
1325
  const w = Number(y.params[k]);
1255
- return ok([w > v ? { id: x.id, params: { ...x.params, [k]: w } } : x]);
1326
+ return ok2([w > v ? { id: x.id, params: { ...x.params, [k]: w } } : x]);
1256
1327
  }
1257
- return ok([x]);
1328
+ return ok2([x]);
1258
1329
  }
1259
1330
  var isSheetOp = (m) => m.id === ID.insertSheet || m.id === ID.removeSheet || m.id === ID.sheetOrder;
1260
1331
  var sheetIdOf = (m) => m.id === ID.insertSheet ? String(m.params.sheet?.id ?? "") : sheetOf(m);
@@ -1287,31 +1358,31 @@ function sheetPair(x, y, side) {
1287
1358
  const b = sheetAxisOf(y);
1288
1359
  if (!a || !b) return barrier2("a sheet add or remove without its index (capture must call annotateSheetOps)");
1289
1360
  if (x.id === ID.insertSheet && y.id === ID.insertSheet && sheetIdOf(x) === sheetIdOf(y)) {
1290
- return JSON.stringify(x.params) === JSON.stringify(y.params) ? ok([]) : barrier2("the same sheet id added twice with different content");
1361
+ return JSON.stringify(x.params) === JSON.stringify(y.params) ? ok2([]) : barrier2("the same sheet id added twice with different content");
1291
1362
  }
1292
1363
  if (x.id === ID.removeSheet && typeof x.params.sheetsLeft === "number" && sheetIdOf(x) !== sheetIdOf(y) && (y.id === ID.removeSheet || y.id === ID.insertSheet)) {
1293
1364
  const left = x.params.sheetsLeft + (y.id === ID.insertSheet ? 1 : -1);
1294
1365
  if (left < 1) return barrier2("concurrent sheet removes would leave the workbook without a sheet");
1295
1366
  const out2 = transformAxis(a, b, side);
1296
1367
  if (!out2) return barrier2("sheet order: no transform");
1297
- return ok(out2.map((o) => buildSheetOp({ id: x.id, params: { ...x.params, sheetsLeft: left } }, o)));
1368
+ return ok2(out2.map((o) => buildSheetOp({ id: x.id, params: { ...x.params, sheetsLeft: left } }, o)));
1298
1369
  }
1299
1370
  const out = transformAxis(a, b, side);
1300
1371
  if (!out) return barrier2("sheet order: no transform");
1301
- return ok(out.map((o) => buildSheetOp(x, o)), [], out.length === 0 && x.id === ID.sheetOrder ? [`${sheetIdOf(x)}: sheet order`] : []);
1372
+ return ok2(out.map((o) => buildSheetOp(x, o)), [], out.length === 0 && x.id === ID.sheetOrder ? [`${sheetIdOf(x)}: sheet order`] : []);
1302
1373
  }
1303
1374
  if (ys) {
1304
- if (y.id !== ID.removeSheet) return ok([x]);
1375
+ if (y.id !== ID.removeSheet) return ok2([x]);
1305
1376
  const gone2 = sheetOf(y);
1306
1377
  if (namesSheet(x, gone2)) return barrier2("a formula names a sheet removed at the same time");
1307
1378
  if (sheetOf(x) === gone2 && x.id !== ID.setName && x.id !== ID.removeName)
1308
- return ok([], [], isDerived(x) ? [] : [`${gone2}: the sheet was removed`]);
1309
- return ok([x]);
1379
+ return ok2([], [], isDerived(x) ? [] : [`${gone2}: the sheet was removed`]);
1380
+ return ok2([x]);
1310
1381
  }
1311
- if (x.id !== ID.removeSheet) return ok([x]);
1382
+ if (x.id !== ID.removeSheet) return ok2([x]);
1312
1383
  const gone = sheetOf(x);
1313
1384
  if (namesSheet(y, gone)) return barrier2("a formula names a sheet removed at the same time");
1314
- return ok([x], sheetOf(y) === gone && !isDerived(y) && y.id !== ID.setName && y.id !== ID.removeName ? [`${gone}: the sheet was removed`] : []);
1385
+ return ok2([x], sheetOf(y) === gone && !isDerived(y) && y.id !== ID.setName && y.id !== ID.removeName ? [`${gone}: the sheet was removed`] : []);
1315
1386
  }
1316
1387
  function annotateSheetOps(group, sheetOrder) {
1317
1388
  const order = [...sheetOrder];
@@ -1350,6 +1421,119 @@ function inlineSheetStyles(sheet, styleOf) {
1350
1421
  }
1351
1422
  return out;
1352
1423
  }
1424
+ var isSort = (m) => m.id === ID.sort;
1425
+ function sortOf(m) {
1426
+ const P = /* @__PURE__ */ new Map();
1427
+ for (const [t, src] of Object.entries(m.params.order ?? {})) P.set(Number(t), Number(src));
1428
+ return { range: { ...m.params.range }, P };
1429
+ }
1430
+ var sortQ = (o) => new Map([...o.P].map(([t, s0]) => [s0, t]));
1431
+ function buildSort(template, o) {
1432
+ const order = {};
1433
+ for (const [t, s0] of [...o.P].sort((a, b) => a[0] - b[0])) if (t !== s0) order[t] = s0;
1434
+ if (!Object.keys(order).length || o.range.startRow > o.range.endRow || o.range.startColumn > o.range.endColumn) return [];
1435
+ return [{ id: template.id, params: { ...template.params, range: o.range, order } }];
1436
+ }
1437
+ var inSortCols = (o, c) => c >= o.range.startColumn && c <= o.range.endColumn;
1438
+ function sortThrough(o, st) {
1439
+ const r = o.range;
1440
+ if (st.axis === "col") {
1441
+ if (st.op.kind === "move") {
1442
+ const { from, count, to } = st.op;
1443
+ const touches = !(from + count - 1 < r.startColumn || from > r.endColumn) || to > r.startColumn && to <= r.endColumn;
1444
+ if (touches) return null;
1445
+ const spans3 = spanThrough(st.op, r.startColumn, r.endColumn);
1446
+ return spans3.length === 1 ? { range: { ...r, startColumn: spans3[0][0], endColumn: spans3[0][1] }, P: o.P } : null;
1447
+ }
1448
+ const spans2 = spanThrough(st.op, r.startColumn, r.endColumn);
1449
+ if (!spans2.length) return { range: { ...r, startColumn: 1, endColumn: 0 }, P: /* @__PURE__ */ new Map() };
1450
+ return { range: { ...r, startColumn: spans2[0][0], endColumn: spans2[spans2.length - 1][1] }, P: o.P };
1451
+ }
1452
+ const op = st.op;
1453
+ if (op.kind === "move") {
1454
+ const touches = !(op.from + op.count - 1 < r.startRow || op.from > r.endRow) || op.to > r.startRow && op.to <= r.endRow;
1455
+ if (touches) return null;
1456
+ }
1457
+ if (op.kind === "insert" || op.kind === "move") {
1458
+ const P2 = /* @__PURE__ */ new Map();
1459
+ for (const [t, s0] of o.P) P2.set(mapIndex(op, t), mapIndex(op, s0));
1460
+ const spans2 = spanThrough(op, r.startRow, r.endRow);
1461
+ return { range: { ...r, startRow: spans2[0][0], endRow: spans2[spans2.length - 1][1] }, P: P2 };
1462
+ }
1463
+ const removed = (i) => i >= op.start && i < op.start + op.count;
1464
+ const Q = sortQ(o);
1465
+ const goneTargets = [];
1466
+ for (let i = op.start; i < op.start + op.count; i++) goneTargets.push(Q.get(i) ?? i);
1467
+ goneTargets.sort((a, b) => a - b);
1468
+ const compact = (t) => t - goneTargets.filter((g) => g < t).length;
1469
+ const P = /* @__PURE__ */ new Map();
1470
+ for (const [t, s0] of o.P) if (!removed(s0)) P.set(compact(t), mapIndex(op, s0));
1471
+ const spans = spanThrough(op, r.startRow, r.endRow);
1472
+ if (!spans.length) return { range: { ...r, startRow: 1, endRow: 0 }, P: /* @__PURE__ */ new Map() };
1473
+ return { range: { ...r, startRow: spans[0][0], endRow: spans[spans.length - 1][1] }, P };
1474
+ }
1475
+ function sortPair(x, y, sx, sy) {
1476
+ const xs = isSort(x);
1477
+ const ys = isSort(y);
1478
+ if (!xs && !ys) return null;
1479
+ if (xs && ys) {
1480
+ const a = sortOf(x).range;
1481
+ const b = sortOf(y).range;
1482
+ const overlap = a.startRow <= b.endRow && b.startRow <= a.endRow && a.startColumn <= b.endColumn && b.startColumn <= a.endColumn;
1483
+ return overlap ? barrier2("two sorts of overlapping ranges at once") : ok2([x]);
1484
+ }
1485
+ if (xs) {
1486
+ if (sy) {
1487
+ const t = sortThrough(sortOf(x), sy);
1488
+ return t ? ok2(buildSort(x, t)) : barrier2("a row or column move through a sorted range");
1489
+ }
1490
+ return ok2([x]);
1491
+ }
1492
+ const o = sortOf(y);
1493
+ const Q = sortQ(o);
1494
+ const moved = (r) => r >= o.range.startRow && r <= o.range.endRow && Q.has(r);
1495
+ if (sx) {
1496
+ if (sx.axis === "col") return sx.op.kind === "move" && !sortThrough(o, sx) ? barrier2("a column move through a sorted range") : ok2([x]);
1497
+ if (sx.op.kind === "insert") return ok2([x]);
1498
+ if (sx.op.kind === "move") return sortThrough(o, sx) ? ok2([x]) : barrier2("a row move through a sorted range");
1499
+ const rows = [];
1500
+ let partial = false;
1501
+ for (let i = sx.op.start; i < sx.op.start + sx.op.count; i++) {
1502
+ if (moved(i)) partial = true;
1503
+ rows.push(Q.get(i) ?? i);
1504
+ }
1505
+ if (partial) return barrier2("a row remove against a concurrent sort of those rows");
1506
+ return ok2([x]);
1507
+ }
1508
+ if (x.id === ID.values) {
1509
+ const out = {};
1510
+ let changed = false;
1511
+ forEachCell(cellsOf(x), (r, c, v) => {
1512
+ if (inSortCols(o, c) && moved(r)) {
1513
+ putCell(out, Q.get(r), c, v);
1514
+ changed = true;
1515
+ } else putCell(out, r, c, v);
1516
+ });
1517
+ return changed ? ok2([cellsMutation(x, out)]) : ok2([x]);
1518
+ }
1519
+ if (isNumfmt(x)) {
1520
+ const parts = nfParts(x).map((p) => {
1521
+ const ranges = [];
1522
+ for (const rc of p.ranges) {
1523
+ const inner = { ...rc, startRow: Math.max(rc.startRow, o.range.startRow), endRow: Math.min(rc.endRow, o.range.endRow), startColumn: Math.max(rc.startColumn, o.range.startColumn), endColumn: Math.min(rc.endColumn, o.range.endColumn) };
1524
+ if (inner.startRow > inner.endRow || inner.startColumn > inner.endColumn) {
1525
+ ranges.push(rc);
1526
+ continue;
1527
+ }
1528
+ ranges.push(...rectMinus(rc, inner));
1529
+ for (let r = inner.startRow; r <= inner.endRow; r++) ranges.push({ ...inner, startRow: Q.get(r) ?? r, endRow: Q.get(r) ?? r });
1530
+ }
1531
+ return { ...p, ranges };
1532
+ });
1533
+ return ok2(nfBuild(x, parts));
1534
+ }
1535
+ return ok2([x]);
1536
+ }
1353
1537
  function familyOf(id) {
1354
1538
  const e = SHEET_MUTATIONS[id];
1355
1539
  return e && e.kind === "covered" ? e.family : "";
@@ -1364,27 +1548,31 @@ function transformMutation(x, y, side) {
1364
1548
  if (why) return barrier2(why);
1365
1549
  }
1366
1550
  }
1551
+ const ca = transformCommentAnchor(x, y, side);
1552
+ if (ca) return ca;
1367
1553
  const pb = PAIR_BARRIERS[pairKey(x.id, y.id)];
1368
1554
  if (pb) return barrier2(`${x.id} \xD7 ${y.id}: ${pb.reason}${pb.seed === null ? "" : ` (fuzz seed ${pb.seed})`}`);
1369
1555
  const sp = sheetPair(x, y, side);
1370
1556
  if (sp) return sp;
1371
1557
  const fp = formulaPair(x, y, side);
1372
1558
  if (fp) return fp;
1373
- if (sheetOf(x) !== sheetOf(y)) return ok([x]);
1559
+ if (sheetOf(x) !== sheetOf(y)) return ok2([x]);
1374
1560
  const fx = familyOf(x.id);
1375
1561
  const fy = familyOf(y.id);
1376
1562
  const sy = structuralOf(y);
1377
1563
  const sx = structuralOf(x);
1564
+ const sortp = sortPair(x, y, sx, sy);
1565
+ if (sortp) return sortp;
1378
1566
  const cp = countPair(x, y, sy);
1379
1567
  if (cp) return cp;
1380
1568
  const np = numfmtPair(x, y, side, sx, sy);
1381
1569
  if (np) return np;
1382
1570
  if (sx && sy) {
1383
- if (sx.axis !== sy.axis) return ok([x]);
1571
+ if (sx.axis !== sy.axis) return ok2([x]);
1384
1572
  const out = transformAxis(sx.op, sy.op, side);
1385
1573
  if (!out) return barrier2(`${x.id} over ${y.id}: moved blocks overlap in part, or the block would split`);
1386
1574
  const dropped = out.length === 0 && sx.op.kind === "move" ? [`${sheetOf(x)}: moved ${sx.axis}s`] : [];
1387
- return ok(out.map((o) => buildStructural(x, sx.axis, o)), [], dropped);
1575
+ return ok2(out.map((o) => buildStructural(x, sx.axis, o)), [], dropped);
1388
1576
  }
1389
1577
  if (sy) {
1390
1578
  if (fx === "merge") {
@@ -1395,23 +1583,23 @@ function transformMutation(x, y, side) {
1395
1583
  if (t) kept.push(t);
1396
1584
  else dropped.push(rangeRef(sheetOf(x), r));
1397
1585
  }
1398
- return ok(kept.length ? [withRanges(x, kept)] : [], [], x.id === ID.addMerge ? dropped : []);
1586
+ return ok2(kept.length ? [withRanges(x, kept)] : [], [], x.id === ID.addMerge ? dropped : []);
1399
1587
  }
1400
1588
  if (fx === "size") {
1401
1589
  const size = sizeOf(x);
1402
- if (size.axis !== sy.axis) return ok([x]);
1590
+ if (size.axis !== sy.axis) return ok2([x]);
1403
1591
  const next = /* @__PURE__ */ new Map();
1404
1592
  for (const [i, v] of size.values) {
1405
1593
  const m = mapIndex(sy.op, i);
1406
1594
  if (m !== null) next.set(m, v);
1407
1595
  }
1408
1596
  const built = buildSize(x, next);
1409
- return ok(built ? [built] : []);
1597
+ return ok2(built ? [built] : []);
1410
1598
  }
1411
- return ok([x]);
1599
+ return ok2([x]);
1412
1600
  }
1413
1601
  if (sx) {
1414
- if (fy !== "merge" || y.id !== ID.addMerge) return ok([x]);
1602
+ if (fy !== "merge" || y.id !== ID.addMerge) return ok2([x]);
1415
1603
  const remove = [];
1416
1604
  const add = [];
1417
1605
  for (const r of mergeRanges(y)) {
@@ -1421,11 +1609,11 @@ function transformMutation(x, y, side) {
1421
1609
  if (t.range) add.push(t.range);
1422
1610
  }
1423
1611
  const lostMerges = mergeRanges(y).filter((r) => mergeThrough(sx, r).range === null).map((r) => rangeRef(sheetOf(y), r));
1424
- if (!remove.length) return ok([x], lostMerges);
1612
+ if (!remove.length) return ok2([x], lostMerges);
1425
1613
  const sheet = sheetOf(x);
1426
1614
  const pre = sx.op.kind === "insert" ? [] : [mergeMutation(ID.removeMerge, sheet, remove)];
1427
1615
  const mid = sx.op.kind === "insert" ? [mergeMutation(ID.removeMerge, sheet, remove)] : [];
1428
- return ok([...pre, x, ...mid, ...add.length ? [mergeMutation(ID.addMerge, sheet, add)] : []], lostMerges);
1616
+ return ok2([...pre, x, ...mid, ...add.length ? [mergeMutation(ID.addMerge, sheet, add)] : []], lostMerges);
1429
1617
  }
1430
1618
  if (fx === "cell" && fy === "cell") return transformCellsOverCells(x, y, side);
1431
1619
  if (fx === "merge" && fy === "merge") {
@@ -1434,45 +1622,45 @@ function transformMutation(x, y, side) {
1434
1622
  const hit = (r) => yr.some((q) => intersects(r, q));
1435
1623
  const sheet = sheetOf(x);
1436
1624
  if (x.id === ID.addMerge && y.id === ID.addMerge) {
1437
- if (!xr.some(hit)) return ok([x]);
1625
+ if (!xr.some(hit)) return ok2([x]);
1438
1626
  if (side === "left") {
1439
1627
  const lost = yr.filter((q) => xr.some((r) => intersects(r, q))).map((q) => rangeRef(sheet, q));
1440
- return ok([mergeMutation(ID.removeMerge, sheet, xr.filter(hit)), x], lost);
1628
+ return ok2([mergeMutation(ID.removeMerge, sheet, xr.filter(hit)), x], lost);
1441
1629
  }
1442
1630
  const kept = xr.filter((r) => !hit(r));
1443
- return ok(kept.length ? [withRanges(x, kept)] : [], [], xr.filter(hit).map((r) => rangeRef(sheet, r)));
1631
+ return ok2(kept.length ? [withRanges(x, kept)] : [], [], xr.filter(hit).map((r) => rangeRef(sheet, r)));
1444
1632
  }
1445
1633
  if (x.id === ID.addMerge && y.id === ID.removeMerge) {
1446
- if (side === "left" || !xr.some(hit)) return ok([x], side === "left" && xr.some(hit) ? yr.filter((q) => xr.some((r) => intersects(r, q))).map((q) => rangeRef(sheet, q)) : []);
1634
+ if (side === "left" || !xr.some(hit)) return ok2([x], side === "left" && xr.some(hit) ? yr.filter((q) => xr.some((r) => intersects(r, q))).map((q) => rangeRef(sheet, q)) : []);
1447
1635
  const kept = xr.filter((r) => !hit(r));
1448
- return ok(kept.length ? [withRanges(x, kept)] : [], [], xr.filter(hit).map((r) => rangeRef(sheet, r)));
1636
+ return ok2(kept.length ? [withRanges(x, kept)] : [], [], xr.filter(hit).map((r) => rangeRef(sheet, r)));
1449
1637
  }
1450
1638
  if (x.id === ID.removeMerge && y.id === ID.addMerge) {
1451
1639
  const touched = yr.filter((q) => xr.some((r) => intersects(r, q)));
1452
- if (!touched.length) return ok([x]);
1453
- if (side === "left") return ok([x], touched.map((q) => rangeRef(sheet, q)));
1454
- return ok([x, mergeMutation(ID.addMerge, sheet, touched)], [], xr.filter(hit).map((r) => rangeRef(sheet, r)));
1640
+ if (!touched.length) return ok2([x]);
1641
+ if (side === "left") return ok2([x], touched.map((q) => rangeRef(sheet, q)));
1642
+ return ok2([x, mergeMutation(ID.addMerge, sheet, touched)], [], xr.filter(hit).map((r) => rangeRef(sheet, r)));
1455
1643
  }
1456
- return ok([x]);
1644
+ return ok2([x]);
1457
1645
  }
1458
1646
  if (fx === "size" && fy === "size") {
1459
1647
  const a = sizeOf(x);
1460
1648
  const b = sizeOf(y);
1461
- if (a.axis !== b.axis || a.attr !== b.attr) return ok([x]);
1649
+ if (a.axis !== b.axis || a.attr !== b.attr) return ok2([x]);
1462
1650
  if (side === "left") {
1463
1651
  const lost = [...a.values.keys()].filter((i) => b.values.has(i)).map((i) => `${sheetOf(x)}: ${a.axis} ${i + 1} ${a.attr}`);
1464
- return ok([x], lost);
1652
+ return ok2([x], lost);
1465
1653
  }
1466
1654
  const next = new Map([...a.values].filter(([i]) => !b.values.has(i)));
1467
1655
  const built = buildSize(x, next);
1468
1656
  const dropped = [...a.values.keys()].filter((i) => b.values.has(i)).map((i) => `${sheetOf(x)}: ${a.axis} ${i + 1} ${a.attr}`);
1469
- return ok(built ? [built] : [], [], dropped);
1657
+ return ok2(built ? [built] : [], [], dropped);
1470
1658
  }
1471
1659
  if (fx === "name" && fy === "name") {
1472
- if (side === "left") return ok([x], [`${sheetOf(x)}: name "${String(y.params.name)}"`]);
1473
- return ok([], [], [`${sheetOf(x)}: name "${String(x.params.name)}"`]);
1660
+ if (side === "left") return ok2([x], [`${sheetOf(x)}: name "${String(y.params.name)}"`]);
1661
+ return ok2([], [], [`${sheetOf(x)}: name "${String(x.params.name)}"`]);
1474
1662
  }
1475
- return ok([x]);
1663
+ return ok2([x]);
1476
1664
  }
1477
1665
  var blocked = (p) => !Array.isArray(p);
1478
1666
  function tSeq(xs, ys, side, acc) {