| 1 | # Conflict pages |
| 2 | |
| 3 | What OneNote 2010 does when a sync finds that two clients changed the same object, and |
| 4 | that it reads and deletes the conflict pages Rust writes. `tools/native_conflict.py` |
| 5 | made every capture; `tools/test_conflict_page.py` checks them without a VM. |
| 6 | |
| 7 | `native` is two OneNote clients on one Samba notebook (`native_conflict.py sync`, fixture |
| 8 | `native-ink/cold-ui-ink`): client A, offline, rewrote the first outline's paragraph and the |
| 9 | second outline's; client B rewrote the same first paragraph and the left table cell and |
| 10 | published (`b-published`); A reconnected and synced (`conflict`). A kept B's version of the |
| 11 | paragraph both changed and merged its own disjoint edit into the page; its own version of the |
| 12 | page became a conflict page: |
| 13 | |
| 14 | - its own object space, listed only in the page manifest's `ChildGraphSpaceElementNodes` |
| 15 | (`0x2c001d63`), never in the page series, with a copy of its metadata in the manifest's |
| 16 | `0x24003442` whose identity is the space's XOR the metadata salt (MS-ONE 2.2.81); |
| 17 | - metadata root `jcidConflictPageMetaData` (`0x20038`): `CachedTitleString`, a fresh |
| 18 | `NotebookManagementEntityGuid`, `PageLevel`, both schema revisions (`0x28`), |
| 19 | `TopologyCreationTimeStamp` of the merge, `ConflictingUserName` and |
| 20 | `ConflictingUserInitials` of the client whose version it is; |
| 21 | - a page node with `IsReadOnly`, `Deletable` and `IsConflictPage`, holding A's whole page; |
| 22 | only the rich text of the paragraph both changed has `IsConflictObjectForSelection` and |
| 23 | `IsConflictObjectForRender`; |
| 24 | - the page's metadata and the section's copy of it gain `HasConflictPages`. |
| 25 | |
| 26 | Both clients show the same UI (`a/`, `b/`): the page's tab carries a conflict mark and a |
| 27 | yellow bar reads "This page has changes that could not be merged during synchronization. |
| 28 | Click here to show versions of the page with unmerged changes." Clicking it lists the |
| 29 | conflict pages under the page, each as its date and user ("9/26/2026 snow"), and opens the |
| 30 | first; that page's bar reads "Conflicting changes are highlighted in red. This page cannot be |
| 31 | edited, but you can copy changes to the primary page. Click here for more options." over a |
| 32 | light red highlight (RGB 255, 214, 214), and its menu offers Delete Conflict Page, Copy Page |
| 33 | To..., Select Previous/Next Conflicting Change and Collapse Conflict Pages. |
| 34 | |
| 35 | `native-three` is `evidence/m8/conflict-abrupt-cold-01`: three clients' conflict pages under |
| 36 | one page. OneNote appends each to `ChildGraphSpaceElementNodes` and lists them last first |
| 37 | (`conflicts.png`); the metadata copies are in their own order. |
| 38 | |
| 39 | `native-delete` is OneNote's Delete Conflict Page on `native/conflict` in a fresh clone |
| 40 | (`native_conflict.py cold --delete`): the manifest loses `ChildGraphSpaceElementNodes` (its |
| 41 | last entry) but keeps the metadata copy, the page's metadata and the section's copy lose |
| 42 | `HasConflictPages`, and the conflict page's space is left as it was. |
| 43 | |
| 44 | `native-pages` and `native-restore` are page-list merges (`native_conflict.py pages`, |
| 45 | `restore`): client A, offline, moved pages through COM and edited `Target`'s body while |
| 46 | client B moved pages and deleted `Target`; A reconnected (`merged`, both clients' `listed.json` |
| 47 | and screenshots). OneNote merges page series, not page positions. A page a client moves |
| 48 | gets a series of its own, and a COM move also re-series every page it jumps over (a drag |
| 49 | re-series only the dragged pages, `page-lifecycle/movement`). The merge keeps each page in |
| 50 | the series of the client that re-seriesed it, the server's where both did, leaving the |
| 51 | other's series empty (`native-pages` keeps two, which MS-ONE's one-page minimum does not |
| 52 | allow); series only A made go after the server's. `Target` comes back as a new page space |
| 53 | holding A's edit, in a new series where A had it; B's version stays in the recycle bin. |
| 54 | |
| 55 | - `native-pages`: from One, Two, Three, Four, Target, A moved Four before One and Two last |
| 56 | (re-seriesing One, Two, Three), B moved Four last and Three before One (re-seriesing One, |
| 57 | Two, Four): One, Two, Four, Three, Target. |
| 58 | - `native-restore`: from One, Two, Target, Three, Four, A moved Four before One |
| 59 | (re-seriesing One, Two, Target, Three) and B deleted Target: Four, One, Two, Target, Three. |
| 60 | |
| 61 | `series-move` is `a_move_after_a_native_merge_drops_its_empty_series` |
| 62 | (`crates/onestore/tests/page_movement.rs`, `ONESTORE_EMPTY_SERIES_EXPORT`): Rust moves a |
| 63 | page of `native-pages/merged`, giving it a series of its own and dropping the empty |
| 64 | series; OneNote's cold read (`cold`) keeps every byte after the header. |
| 65 | |
| 66 | `page_moves_and_a_removed_page_merge_as_onenote_merges_them` in |
| 67 | `crates/notebook/tests/sync_pages.rs` replays both with Snowbound's queue, A's moves as |
| 68 | moves of the pages A re-seriesed, and reaches OneNote's order. |
| 69 | |
| 70 | `candidate` is `a_conflict_page_is_stored_as_onenote_stores_one` in |
| 71 | `crates/onestore/tests/conflict.rs` (`ONESTORE_CONFLICT_EXPORT`): the remote renamed the |
| 72 | first word of the fixture's first paragraph, this machine did too, and `SectionOp::Conflict` |
| 73 | kept this machine's page for "Clover Snow", whose author objects carry her initials beside |
| 74 | her name as OneNote's do. `cold` is OneNote's fresh read and the same UI |
| 75 | walk, ending in Delete Conflict Page on the Rust-written page: its bar and list entry show, |
| 76 | the conflict page opens (window title) with the paragraph highlighted, and the deletion |
| 77 | stores what `native-delete` did. |
| 78 | |
| 79 | `candidate-delete` is `deleting_a_conflict_page_stores_what_onenote_stores` |
| 80 | (`ONESTORE_CONFLICT_DELETE_EXPORT`): `SectionOp::Delete` of `native/conflict`'s conflict page, |
| 81 | which stores the objects OneNote's deletion did; `cold-delete` is its fresh read, without a |
| 82 | bar. |