@a3s-lab/office 0.50.0 → 0.51.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/README.md CHANGED
@@ -309,6 +309,23 @@ and [CLI reference](./docs/latest/en/cli-reference.md).
309
309
 
310
310
  ## Current release
311
311
 
312
+ Version `0.51.0` extends Writer Compare with bounded, WPS-referenced
313
+ cross-paragraph text moves:
314
+
315
+ - A unique lexical range can move between aligned simple text paragraphs or
316
+ headings in the same section and become one paired `move` review item.
317
+ Separators travel with the range; accepting, rejecting, undoing, or reopening
318
+ the DOCX preserves exact source and revised text.
319
+ - The Review ribbon and Changes pane keep one localized **移动** card with
320
+ source/destination marks, destination navigation, author guidance, and
321
+ move-aware toast counts. Duplicate or mark-mismatched candidates, section
322
+ boundaries, rich or relationship-bound content, tables, and over-limit input
323
+ remain fail-closed ordinary revisions or diagnostics.
324
+ - The WPS 12.0 COM/UIA probe observed `CompareDocuments` returning ordinary
325
+ delete/insert records for the tested reorder. A3S therefore documents this
326
+ paired inference as a bounded local enhancement, with no claim that WPS
327
+ exposed a native move type through that API.
328
+
312
329
  Version `0.50.0` extends Writer Compare with a bounded, WPS-referenced text
313
330
  move workflow:
314
331
 
@@ -17496,29 +17496,40 @@ function work_document_compare_moves_appendRevisionUnits(target, units, kind, id
17496
17496
  ]));
17497
17497
  }
17498
17498
  function inferInlineMovePairs(changes, marksEqual) {
17499
+ return inferInlineMovePairsAcrossScopes([
17500
+ {
17501
+ scope: '',
17502
+ changes
17503
+ }
17504
+ ], marksEqual);
17505
+ }
17506
+ function inferInlineMovePairsAcrossScopes(comparisons, marksEqual) {
17499
17507
  const fullCandidates = [];
17500
- for (const [stepIndex, change] of changes.entries()){
17508
+ for (const comparison of comparisons)for (const [stepIndex, change] of comparison.changes.entries()){
17501
17509
  if ('equal' === change.kind) continue;
17502
17510
  const units = 'delete' === change.kind ? change.left : change.right;
17503
- const candidate = inlineMoveCandidate(stepIndex, change.kind, units);
17511
+ const candidate = inlineMoveCandidate(stepIndex, change.kind, units, comparison.scope);
17504
17512
  if (candidate) fullCandidates.push(candidate);
17505
17513
  }
17506
17514
  const fullPairs = pairInlineMoveCandidates(fullCandidates, marksEqual);
17507
17515
  const consumedSteps = new Set();
17508
17516
  for (const pair of fullPairs){
17509
- consumedSteps.add(pair.deletion.stepIndex);
17510
- consumedSteps.add(pair.insertion.stepIndex);
17517
+ consumedSteps.add(moveStepKey(pair.deletion));
17518
+ consumedSteps.add(moveStepKey(pair.insertion));
17511
17519
  }
17512
17520
  const tokenCandidates = [];
17513
- for (const [stepIndex, change] of changes.entries()){
17514
- if ('equal' === change.kind || consumedSteps.has(stepIndex)) continue;
17521
+ for (const comparison of comparisons)for (const [stepIndex, change] of comparison.changes.entries()){
17522
+ if ('equal' === change.kind || consumedSteps.has(moveStepKey({
17523
+ scope: comparison.scope,
17524
+ stepIndex
17525
+ }))) continue;
17515
17526
  const units = 'delete' === change.kind ? change.left : change.right;
17516
- tokenCandidates.push(...inlineMoveTokenCandidates(stepIndex, change.kind, units));
17527
+ tokenCandidates.push(...inlineMoveTokenCandidates(stepIndex, change.kind, units, comparison.scope));
17517
17528
  }
17518
17529
  return [
17519
17530
  ...fullPairs,
17520
17531
  ...pairInlineMoveCandidates(tokenCandidates, marksEqual)
17521
- ].sort((left, right)=>left.deletion.stepIndex - right.deletion.stepIndex || left.insertion.stepIndex - right.insertion.stepIndex || left.deletion.start - right.deletion.start).slice(0, MAX_INFERRED_MOVES);
17532
+ ].sort(compareMovePairs).slice(0, MAX_INFERRED_MOVES);
17522
17533
  }
17523
17534
  function appendComparisonRevisionUnits(target, units, kind, schema, factory, stripReviewMarks, appendRevisionUnits, moves) {
17524
17535
  if (!moves?.length) return void appendRevisionUnits(target, units, kind, factory.create(kind), schema, stripReviewMarks);
@@ -17579,9 +17590,9 @@ function pairInlineMoveCandidates(candidates, marksEqual) {
17579
17590
  return Array.from(grouped.values()).filter(({ deletion, insertion })=>1 === deletion.length && 1 === insertion.length && inlineMoveCandidatesEqual(deletion[0], insertion[0], marksEqual)).map(({ deletion, insertion })=>({
17580
17591
  deletion: deletion[0],
17581
17592
  insertion: insertion[0]
17582
- })).sort((left, right)=>left.deletion.stepIndex - right.deletion.stepIndex || left.insertion.stepIndex - right.insertion.stepIndex).slice(0, MAX_INFERRED_MOVES);
17593
+ })).sort(compareMovePairs).slice(0, MAX_INFERRED_MOVES);
17583
17594
  }
17584
- function inlineMoveCandidate(stepIndex, kind, units) {
17595
+ function inlineMoveCandidate(stepIndex, kind, units, scope = '') {
17585
17596
  let start = 0;
17586
17597
  while(start < units.length && isWhitespaceUnit(units[start]))start += 1;
17587
17598
  let end = units.length;
@@ -17594,6 +17605,9 @@ function inlineMoveCandidate(stepIndex, kind, units) {
17594
17605
  const text = units.slice(start, end).map((unit, index, selected)=>unit.text.slice(0 === index ? startOffset : 0, index === selected.length - 1 ? endOffset : unit.text.length)).join('');
17595
17606
  if (!text || text.length > MAX_INFERRED_MOVE_TEXT || !/[\p{L}\p{N}]/u.test(text)) return null;
17596
17607
  return {
17608
+ ...scope ? {
17609
+ scope
17610
+ } : {},
17597
17611
  stepIndex,
17598
17612
  kind,
17599
17613
  units,
@@ -17604,12 +17618,15 @@ function inlineMoveCandidate(stepIndex, kind, units) {
17604
17618
  text
17605
17619
  };
17606
17620
  }
17607
- function inlineMoveTokenCandidates(stepIndex, kind, units) {
17621
+ function inlineMoveTokenCandidates(stepIndex, kind, units, scope = '') {
17608
17622
  const candidates = [];
17609
17623
  for (const [index, unit] of units.entries()){
17610
17624
  const leadingWhitespace = unit.text.length - unit.text.trimStart().length;
17611
17625
  const text = unit.text.slice(leadingWhitespace);
17612
17626
  if (text && !(text.length > MAX_INFERRED_MOVE_TEXT) && /[\p{L}\p{N}]/u.test(text)) candidates.push({
17627
+ ...scope ? {
17628
+ scope
17629
+ } : {},
17613
17630
  stepIndex,
17614
17631
  kind,
17615
17632
  units,
@@ -17622,6 +17639,12 @@ function inlineMoveTokenCandidates(stepIndex, kind, units) {
17622
17639
  }
17623
17640
  return candidates;
17624
17641
  }
17642
+ function moveStepKey(candidate) {
17643
+ return `${candidate.scope ?? ''}\u0000${candidate.stepIndex}`;
17644
+ }
17645
+ function compareMovePairs(left, right) {
17646
+ return (left.deletion.scope ?? '').localeCompare(right.deletion.scope ?? '') || left.deletion.stepIndex - right.deletion.stepIndex || (left.insertion.scope ?? '').localeCompare(right.insertion.scope ?? '') || left.insertion.stepIndex - right.insertion.stepIndex || left.deletion.start - right.deletion.start;
17647
+ }
17625
17648
  function inlineMoveCandidatesEqual(left, right, marksEqual) {
17626
17649
  const leftUnits = selectedMoveUnits(left);
17627
17650
  const rightUnits = selectedMoveUnits(right);
@@ -17798,40 +17821,129 @@ function compareSectionBlocks(currentSection, revisedSection, sectionIndex, fact
17798
17821
  });
17799
17822
  return null;
17800
17823
  }
17801
- const result = [];
17824
+ const slots = [];
17802
17825
  for (const [blockIndex, step] of alignment.entries()){
17803
17826
  if ('equal' === step.kind) {
17804
- result.push(step.left);
17827
+ slots.push({
17828
+ kind: 'node',
17829
+ node: step.left
17830
+ });
17805
17831
  continue;
17806
17832
  }
17807
17833
  if ('delete' === step.kind) {
17808
17834
  const deleted = structuralChangeBlock(step.left, 'deletion', factory, `section-${sectionIndex}-delete-${blockIndex}`);
17809
- if (deleted) result.push(deleted);
17835
+ if (deleted) slots.push({
17836
+ kind: 'node',
17837
+ node: deleted
17838
+ });
17810
17839
  else diagnostics.push(structuralBlockDiagnostic(step.left, sectionIndex, blockIndex));
17811
17840
  continue;
17812
17841
  }
17813
17842
  if ('insert' === step.kind) {
17814
17843
  const inserted = structuralChangeBlock(step.right, 'insertion', factory, `section-${sectionIndex}-insert-${blockIndex}`);
17815
- if (inserted) result.push(inserted);
17844
+ if (inserted) slots.push({
17845
+ kind: 'node',
17846
+ node: inserted
17847
+ });
17816
17848
  else diagnostics.push(structuralBlockDiagnostic(step.right, sectionIndex, blockIndex));
17817
17849
  continue;
17818
17850
  }
17819
- const compared = comparePairedBlocks(step.left, step.right, factory);
17820
- if (compared) {
17821
- result.push(compared);
17851
+ const prepared = preparePairedBlock(step.left, step.right, `section-${sectionIndex}-block-${blockIndex}`);
17852
+ if (prepared) {
17853
+ slots.push({
17854
+ kind: 'paired',
17855
+ comparison: prepared
17856
+ });
17822
17857
  continue;
17823
17858
  }
17824
17859
  const deleted = structuralChangeBlock(step.left, 'deletion', factory, `section-${sectionIndex}-replace-delete-${blockIndex}`);
17825
17860
  const inserted = structuralChangeBlock(step.right, 'insertion', factory, `section-${sectionIndex}-replace-insert-${blockIndex}`);
17826
- if (deleted && inserted) result.push(deleted, inserted);
17827
- else diagnostics.push(structuralBlockDiagnostic(step.left, sectionIndex, blockIndex));
17861
+ if (deleted) slots.push({
17862
+ kind: 'node',
17863
+ node: deleted
17864
+ });
17865
+ if (inserted) slots.push({
17866
+ kind: 'node',
17867
+ node: inserted
17868
+ });
17869
+ if (!deleted || !inserted) diagnostics.push(structuralBlockDiagnostic(step.left, sectionIndex, blockIndex));
17828
17870
  }
17829
- return result;
17871
+ const pairedSlots = slots.filter((slot)=>'paired' === slot.kind);
17872
+ const initialComparisons = pairedSlots.map(({ comparison })=>({
17873
+ scope: comparison.scope,
17874
+ changes: comparison.inline.variant.changes
17875
+ }));
17876
+ const initialMovePairs = inferInlineMovePairsAcrossScopes(initialComparisons, work_document_compare_marksEqual);
17877
+ const attachedComparisons = pairedSlots.map(({ comparison })=>({
17878
+ scope: comparison.scope,
17879
+ changes: comparison.inline.attached?.changes ?? comparison.inline.variant.changes
17880
+ }));
17881
+ const attachedMovePairs = inferInlineMovePairsAcrossScopes(attachedComparisons, work_document_compare_marksEqual);
17882
+ const pairedByScope = new Map(pairedSlots.map((slot)=>[
17883
+ slot.comparison.scope,
17884
+ slot.comparison
17885
+ ]));
17886
+ const attachedScopes = new Set();
17887
+ for (const pair of [
17888
+ ...initialMovePairs,
17889
+ ...attachedMovePairs
17890
+ ]){
17891
+ const deletionScope = pair.deletion.scope ?? '';
17892
+ const insertionScope = pair.insertion.scope ?? '';
17893
+ if (deletionScope === insertionScope) continue;
17894
+ const deletionBlock = pairedByScope.get(deletionScope);
17895
+ const insertionBlock = pairedByScope.get(insertionScope);
17896
+ if (deletionBlock?.inline.attached && insertionBlock?.inline.attached) {
17897
+ attachedScopes.add(deletionScope);
17898
+ attachedScopes.add(insertionScope);
17899
+ }
17900
+ }
17901
+ const selectedComparisons = pairedSlots.map(({ comparison })=>({
17902
+ scope: comparison.scope,
17903
+ changes: (attachedScopes.has(comparison.scope) ? comparison.inline.attached : comparison.inline.variant)?.changes ?? comparison.inline.variant.changes
17904
+ }));
17905
+ const movePairs = inferInlineMovePairsAcrossScopes(selectedComparisons, work_document_compare_marksEqual);
17906
+ const useAttachedVariants = attachedScopes.size > 0 && movePairs.length >= initialMovePairs.length;
17907
+ const movesByScope = new Map();
17908
+ for (const pair of useAttachedVariants ? movePairs : initialMovePairs){
17909
+ const identity = factory.create('move');
17910
+ appendMoveAssignment(movesByScope, pair.deletion, 'from', identity);
17911
+ appendMoveAssignment(movesByScope, pair.insertion, 'to', identity);
17912
+ }
17913
+ return slots.flatMap((slot)=>{
17914
+ if ('node' === slot.kind) return [
17915
+ slot.node
17916
+ ];
17917
+ const compared = renderPairedBlock(slot.comparison, factory, movesByScope.get(slot.comparison.scope), useAttachedVariants && attachedScopes.has(slot.comparison.scope) ? slot.comparison.inline.attached ?? slot.comparison.inline.variant : slot.comparison.inline.variant);
17918
+ return compared ? [
17919
+ compared
17920
+ ] : [];
17921
+ });
17922
+ }
17923
+ function appendMoveAssignment(target, candidate, role, identity) {
17924
+ const scope = candidate.scope ?? '';
17925
+ const assignments = target.get(scope) ?? [];
17926
+ assignments.push({
17927
+ role,
17928
+ candidate,
17929
+ identity
17930
+ });
17931
+ target.set(scope, assignments);
17830
17932
  }
17831
- function comparePairedBlocks(current, revised, factory) {
17933
+ function preparePairedBlock(current, revised, scope) {
17832
17934
  if (!isSimpleTextBlock(current) || !isSimpleTextBlock(revised)) return null;
17833
17935
  if (current.type !== revised.type || blockStructuralSignature(current) !== blockStructuralSignature(revised)) return null;
17834
- const inline = compareInlineContent(current, revised, factory);
17936
+ const comparison = prepareInlineComparison(current, revised);
17937
+ return comparison ? {
17938
+ current,
17939
+ revised,
17940
+ scope,
17941
+ inline: comparison
17942
+ } : null;
17943
+ }
17944
+ function renderPairedBlock(comparison, factory, moves, variant) {
17945
+ const { current, revised } = comparison;
17946
+ const inline = renderInlineComparison(current, variant, factory, moves);
17835
17947
  if (!inline) return null;
17836
17948
  const before = serializeDocumentParagraphFormatting(current.attrs);
17837
17949
  const after = serializeDocumentParagraphFormatting(revised.attrs);
@@ -17853,65 +17965,65 @@ function comparePairedBlocks(current, revised, factory) {
17853
17965
  }
17854
17966
  return current.type.create(attributes, inline);
17855
17967
  }
17856
- function compareInlineContent(current, revised, factory) {
17857
- let currentUnits = inlineUnits(current);
17858
- let revisedUnits = inlineUnits(revised);
17859
- if (!currentUnits || !revisedUnits) return null;
17860
- const diff = boundedDocumentSequenceDiff(currentUnits, revisedUnits, (left, right)=>left.text === right.text, MAX_INLINE_DIFF_CELLS);
17861
- let changes = diff ?? [
17862
- {
17863
- kind: 'delete',
17864
- left: currentUnits,
17865
- right: []
17866
- },
17867
- {
17868
- kind: 'insert',
17869
- left: [],
17870
- right: revisedUnits
17871
- }
17872
- ];
17873
- const baseMovePairs = inferInlineMovePairs(changes, work_document_compare_marksEqual);
17968
+ function prepareInlineComparison(current, revised) {
17969
+ const baseCurrentUnits = inlineUnits(current);
17970
+ const baseRevisedUnits = inlineUnits(revised);
17971
+ if (!baseCurrentUnits || !baseRevisedUnits) return null;
17972
+ const diff = boundedDocumentSequenceDiff(baseCurrentUnits, baseRevisedUnits, (left, right)=>left.text === right.text, MAX_INLINE_DIFF_CELLS);
17973
+ const baseVariant = {
17974
+ currentUnits: baseCurrentUnits,
17975
+ revisedUnits: baseRevisedUnits,
17976
+ changes: diff ?? [
17977
+ {
17978
+ kind: 'delete',
17979
+ left: baseCurrentUnits,
17980
+ right: []
17981
+ },
17982
+ {
17983
+ kind: 'insert',
17984
+ left: [],
17985
+ right: baseRevisedUnits
17986
+ }
17987
+ ]
17988
+ };
17989
+ const variant = baseVariant;
17990
+ const baseMovePairs = inferInlineMovePairs(variant.changes, work_document_compare_marksEqual);
17874
17991
  const moveCurrentUnits = inlineUnits(current, true);
17875
17992
  const moveRevisedUnits = inlineUnits(revised, true);
17876
17993
  if (moveCurrentUnits && moveRevisedUnits && current.textContent.length + revised.textContent.length <= 131072) {
17877
17994
  const moveDiff = boundedDocumentSequenceDiff(moveCurrentUnits, moveRevisedUnits, (left, right)=>left.text === right.text, MAX_INLINE_DIFF_CELLS);
17878
17995
  if (moveDiff) {
17879
17996
  const moveChanges = moveDiff;
17997
+ const moveVariant = {
17998
+ currentUnits: moveCurrentUnits,
17999
+ revisedUnits: moveRevisedUnits,
18000
+ changes: moveChanges
18001
+ };
17880
18002
  const movePairs = inferInlineMovePairs(moveChanges, work_document_compare_marksEqual);
17881
- if (movePairs.length >= baseMovePairs.length && movePairs.length > 0) {
17882
- currentUnits = moveCurrentUnits;
17883
- revisedUnits = moveRevisedUnits;
17884
- changes = moveChanges;
17885
- }
18003
+ if (movePairs.length >= baseMovePairs.length && movePairs.length > 0) return {
18004
+ variant: moveVariant,
18005
+ attached: moveVariant
18006
+ };
18007
+ return {
18008
+ variant,
18009
+ attached: moveVariant
18010
+ };
17886
18011
  }
17887
18012
  }
17888
- const movePairs = inferInlineMovePairs(changes, work_document_compare_marksEqual);
17889
- const movesByStep = new Map();
17890
- for (const pair of movePairs){
17891
- const identity = factory.create('move');
17892
- const deletion = movesByStep.get(pair.deletion.stepIndex) ?? [];
17893
- deletion.push({
17894
- role: 'from',
17895
- candidate: pair.deletion,
17896
- identity
17897
- });
17898
- movesByStep.set(pair.deletion.stepIndex, deletion);
17899
- const insertion = movesByStep.get(pair.insertion.stepIndex) ?? [];
17900
- insertion.push({
17901
- role: 'to',
17902
- candidate: pair.insertion,
17903
- identity
17904
- });
17905
- movesByStep.set(pair.insertion.stepIndex, insertion);
17906
- }
18013
+ return {
18014
+ variant
18015
+ };
18016
+ }
18017
+ function renderInlineComparison(current, variant, factory, moves) {
18018
+ const { changes } = variant;
17907
18019
  const nodes = [];
17908
18020
  for (const [stepIndex, change] of changes.entries()){
17909
18021
  if ('delete' === change.kind) {
17910
- appendComparisonRevisionUnits(nodes, change.left, 'deletion', current.type.schema, factory, withoutReviewMarks, work_document_compare_moves_appendRevisionUnits, movesByStep.get(stepIndex));
18022
+ appendComparisonRevisionUnits(nodes, change.left, 'deletion', current.type.schema, factory, withoutReviewMarks, work_document_compare_moves_appendRevisionUnits, moves?.filter((move)=>move.candidate.stepIndex === stepIndex));
17911
18023
  continue;
17912
18024
  }
17913
18025
  if ('insert' === change.kind) {
17914
- appendComparisonRevisionUnits(nodes, change.right, 'insertion', current.type.schema, factory, withoutReviewMarks, work_document_compare_moves_appendRevisionUnits, movesByStep.get(stepIndex));
18026
+ appendComparisonRevisionUnits(nodes, change.right, 'insertion', current.type.schema, factory, withoutReviewMarks, work_document_compare_moves_appendRevisionUnits, moves?.filter((move)=>move.candidate.stepIndex === stepIndex));
17915
18027
  continue;
17916
18028
  }
17917
18029
  let formattingIdentity = null;
@@ -20,6 +20,8 @@ export type InlineDiffStep = {
20
20
  right: InlineUnit[];
21
21
  };
22
22
  export interface InlineMoveCandidate {
23
+ /** Stable comparison scope (for example, the containing paragraph). */
24
+ scope?: string;
23
25
  stepIndex: number;
24
26
  kind: 'delete' | 'insert';
25
27
  units: InlineUnit[];
@@ -33,6 +35,10 @@ export interface InlineMovePair {
33
35
  deletion: InlineMoveCandidate;
34
36
  insertion: InlineMoveCandidate;
35
37
  }
38
+ export interface InlineMoveComparison {
39
+ scope: string;
40
+ changes: readonly InlineDiffStep[];
41
+ }
36
42
  export interface InlineMoveAssignment {
37
43
  role: WorkDocumentMoveRole;
38
44
  candidate: InlineMoveCandidate;
@@ -47,11 +53,17 @@ type AppendRevisionUnits = (target: ProseMirrorNode[], units: readonly InlineUni
47
53
  export declare const MAX_INFERRED_MOVE_TEXT = 65536;
48
54
  export declare function appendRevisionUnits(target: ProseMirrorNode[], units: readonly InlineUnit[], kind: 'insertion' | 'deletion', identity: WorkDocumentChangeIdentity, schema: Schema, stripReviewMarks: StripReviewMarks): void;
49
55
  /**
50
- * Pairs only deterministic same-text ranges inside one block. Duplicate
51
- * candidates and ranges whose marks differ are deliberately left as ordinary
52
- * insertions/deletions so Compare never guesses at an ambiguous move.
56
+ * Pairs deterministic same-text ranges. A comparison may contain several
57
+ * simple blocks, which lets Compare represent a bounded move between
58
+ * paragraphs while still rejecting duplicate or mark-mismatched candidates.
53
59
  */
54
60
  export declare function inferInlineMovePairs(changes: readonly InlineDiffStep[], marksEqual: MarksEqual): InlineMovePair[];
61
+ /**
62
+ * Infers moves across a bounded set of simple-block diffs. Candidate scopes
63
+ * are retained on each side so callers can put the resulting marks back into
64
+ * the right paragraph without flattening the document tree.
65
+ */
66
+ export declare function inferInlineMovePairsAcrossScopes(comparisons: readonly InlineMoveComparison[], marksEqual: MarksEqual): InlineMovePair[];
55
67
  /**
56
68
  * Splits an ordinary diff step around paired move ranges while retaining the
57
69
  * exact whitespace and mark boundaries needed to reconstruct either version.
@@ -296,16 +296,19 @@ The current and imported TipTap/ProseMirror trees remain immutable while a
296
296
  deterministic weighted block alignment pairs related paragraphs and headings;
297
297
  an LCS token diff then produces insertion and deletion marks, while exact mark
298
298
  and paragraph-property snapshots produce character- and paragraph-formatting
299
- revisions. A bounded same-paragraph pass may pair a lexical range that occurs
300
- exactly once in a delete chunk and once in an insert chunk when its marks and
301
- carried separators match; the result is one `move` identity with source and
299
+ revisions. A bounded move pass may pair a lexical range that occurs exactly
300
+ once in a delete chunk and once in an insert chunk when its marks and carried
301
+ separators match. The pass retains a stable paragraph scope, so it can pair
302
+ aligned simple text blocks within one document section without flattening the
303
+ document tree; section boundaries, rich blocks, and structural objects remain
304
+ outside this inference. The result is one `move` identity with source and
302
305
  destination roles. Stable semantic signatures seed generated identities, so the
303
306
  same inputs and options produce the same ordering and IDs. Only a fully
304
307
  admitted plan replaces the mounted document, and that replacement is one
305
308
  transaction, one controlled publication, and one Undo record. Duplicate,
306
- rich, cross-paragraph, and over-limit candidates remain ordinary revisions or
307
- diagnostics rather than being guessed into moves; native paired moves remain
308
- reviewable when they already exist.
309
+ mark-mismatched, section-boundary, rich, and over-limit candidates remain
310
+ ordinary revisions or diagnostics rather than being guessed into moves; native
311
+ paired moves remain reviewable when they already exist.
309
312
 
310
313
  Combine validates rather than guesses. It rejects every imported revision on
311
314
  an immutable reviewed snapshot and compares the resulting semantic signature
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a3s-lab/office",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "Open-source collaborative browser editors for documents, Markdown, spreadsheets, presentations, and PDFs.",
5
5
  "keywords": [
6
6
  "office",