diff --git a/corpus/paragraph-edit/reconciliation/README.md b/corpus/paragraph-edit/reconciliation/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..66f2d1bb8e4dd97316f8c026464ff90f2e368457
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/README.md
@@ -0,0 +1,37 @@
+# Native paragraph reconciliation controls
+
+`before` contains sixteen OneNote-authored pages and one unchanged source page.
+`tools/native/paragraph-reconciliation.ps1` creates these cases and applies the
+separate COM replacement controls retained in `remote`. Whole-page COM updates
+replace rich-text identities, including unchanged text. Every local split/join
+and dependent edit therefore remains a conflict without remote publication.
+
+`keyboard` begins from the same notebook and records actual native keystrokes,
+screenshots, XML, and saved bytes. Prefix/boundary edits, children, bullets, tags,
+formatting, sibling replacement, and empty-paragraph typing exercise independent
+changes. Bullets alter ancestry: the first paragraph gains an outline group,
+and a later bullet becomes a child of its preceding paragraph. Replacing a whole
+selected sibling creates a new paragraph; the split/join targets retain identity.
+
+`reconciled` retains Rust output and a fresh OneNote cold-open. Six cases publish
+automatically; eight publish after explicit review. Each includes a dependent
+text edit, with SQLite reopen between local acknowledgements and publications.
+The changed empty-left survivor and nonadjacent bulleted join retain conflicts.
+Public tests check all 28 receipts, exact active graph/child levels, native
+characters/styles, and the saved graph's identities. COM replacement controls
+also retain all sixteen local branches across reopen without publishing.
+
+Regenerate keyboard controls with `tools/native_runner.py` on `before/notebook`,
+using `--expected-pages 17 --inspect --collect-notebook`. Once inspection is
+ready, run `tools/native_paragraph_reconciliation.py OUTPUT`; it records actions
+and requests final capture. The runner removes its owned clone. The ignored
+Rust test `paragraph::export_native_reconciliation` generates reconciled output
+at a new absolute `ONESTORE_NATIVE_RECONCILIATION_OUTPUT` directory. Cold-open its
+`candidate` with the same runner, without inspection. Verification lives in
+`tools/test_paragraph_edit.py` and the offline crate's `tests/sync.rs`.
+
+The original COM pass stopped at its fifth case because its tag definition
+followed page settings. The recorded continuation fixes the schema order and
+applies only the remaining twelve cases. Scripts, commands, and both phase
+hashes retain this provenance; the before-image was preserved throughout.
+Identical captured files link to their canonical corpus copy.
diff --git a/corpus/paragraph-edit/reconciliation/before/notebook/Open Notebook.onetoc2 b/corpus/paragraph-edit/reconciliation/before/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..caf6cd8dfb5dd42a5594ea49ff31560c9a6fea0b
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/before/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/paragraph-edit/reconciliation/before/notebook/synthetic.one b/corpus/paragraph-edit/reconciliation/before/notebook/synthetic.one
new file mode 100644
index 0000000000000000000000000000000000000000..0bd7ce42587425376a287f055c68110a32fd1147
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/before/notebook/synthetic.one differ
diff --git a/corpus/paragraph-edit/reconciliation/before/read/environment.json b/corpus/paragraph-edit/reconciliation/before/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..a2ba5b139e91fa58403585eb573049f0aee59904
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-9095E701",
+ "cold": false,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/paragraph-edit/reconciliation/before/read/hierarchy.xml b/corpus/paragraph-edit/reconciliation/before/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..dea1607b9b6054f1f070215831ee1f9f617cb59f
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-000.xml b/corpus/paragraph-edit/reconciliation/before/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5e819ef3003b03e35e97d6aa3d8788cb958b4566
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-000.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-001.xml b/corpus/paragraph-edit/reconciliation/before/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9b0995d1c94483dd420e5dc694cf2005e11390cf
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-001.xml
@@ -0,0 +1,3 @@
+
+Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-002.xml b/corpus/paragraph-edit/reconciliation/before/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d0454b76f58c57eba16b2ac2f055e116b3ef2b3f
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-002.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-003.xml b/corpus/paragraph-edit/reconciliation/before/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3e209bfd20e9d469d59212f213a8d784b616d4d4
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-003.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-004.xml b/corpus/paragraph-edit/reconciliation/before/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c98a95b72f2afa5010cdc9b1bb63f960fc175d65
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-004.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-005.xml b/corpus/paragraph-edit/reconciliation/before/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..091cf3e0238d4d124fcf66d8fd02cc0f1ced06c6
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-005.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-006.xml b/corpus/paragraph-edit/reconciliation/before/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f2894e1fc41a58e593c4840caeb932be0abbd933
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-006.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-007.xml b/corpus/paragraph-edit/reconciliation/before/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d2088474d3286cada212fab98732f9fa04bac96f
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-007.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-008.xml b/corpus/paragraph-edit/reconciliation/before/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..82c81d2dd07dffa35f77fe1d4764e93983b3f8c7
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-008.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-009.xml b/corpus/paragraph-edit/reconciliation/before/read/page-009.xml
new file mode 100644
index 0000000000000000000000000000000000000000..396bc4d356195778ed0ebb02fd4769f93677cd38
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-009.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-010.xml b/corpus/paragraph-edit/reconciliation/before/read/page-010.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f9d31f5eb92946f7339ab4515b37fe424d6b8bd8
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-010.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-011.xml b/corpus/paragraph-edit/reconciliation/before/read/page-011.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4211655454b98ec2e7e78a9119fb62f29feb13e5
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-011.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-012.xml b/corpus/paragraph-edit/reconciliation/before/read/page-012.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c91727f672f10c2be25a91726480da4da466145b
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-012.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-013.xml b/corpus/paragraph-edit/reconciliation/before/read/page-013.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cf2e5fa90eebc053af209c4c46fbdc183fd5f9fa
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-013.xml
@@ -0,0 +1,20 @@
+
+Bold 🦀 italic é color 東京
+End]]>Fictitious: café, 東京, مرحبا]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-014.xml b/corpus/paragraph-edit/reconciliation/before/read/page-014.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1bcca85eac36511908652a3e5587e00f0960ec4a
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-014.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-015.xml b/corpus/paragraph-edit/reconciliation/before/read/page-015.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e3a8febd3975cbe1f21237e948a401aaa9133c2b
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-015.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/page-016.xml b/corpus/paragraph-edit/reconciliation/before/read/page-016.xml
new file mode 100644
index 0000000000000000000000000000000000000000..420e7421ad930b250a497bea2c5751a487abf334
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/page-016.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/before/read/payloads.json b/corpus/paragraph-edit/reconciliation/before/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3cf45dc1acb611d7f764e0340d73e87192f81407
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/before/read/payloads.json
@@ -0,0 +1 @@
+../../../before/read/payloads.json
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/cases.json b/corpus/paragraph-edit/reconciliation/cases.json
new file mode 100644
index 0000000000000000000000000000000000000000..c1fc8edd9bcc4026137439fb9ca07a8264b05639
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/cases.json
@@ -0,0 +1,82 @@
+[
+ {
+ "operation": "split",
+ "change": "prefix",
+ "name": "Split remote prefix"
+ },
+ {
+ "operation": "split",
+ "change": "boundary",
+ "name": "Split remote boundary"
+ },
+ {
+ "operation": "split",
+ "change": "child",
+ "name": "Split remote child"
+ },
+ {
+ "operation": "split",
+ "change": "list",
+ "name": "Split remote list"
+ },
+ {
+ "operation": "split",
+ "change": "tag",
+ "name": "Split remote tag"
+ },
+ {
+ "operation": "split",
+ "change": "format",
+ "name": "Split remote format"
+ },
+ {
+ "operation": "split",
+ "change": "sibling",
+ "name": "Split remote sibling"
+ },
+ {
+ "operation": "join",
+ "change": "prefix",
+ "name": "Join remote prefix"
+ },
+ {
+ "operation": "join",
+ "change": "boundary",
+ "name": "Join left boundary"
+ },
+ {
+ "operation": "join",
+ "change": "right-boundary",
+ "name": "Join right boundary"
+ },
+ {
+ "operation": "join",
+ "change": "child",
+ "name": "Join remote child"
+ },
+ {
+ "operation": "join",
+ "change": "list",
+ "name": "Join remote list"
+ },
+ {
+ "operation": "join",
+ "change": "tag",
+ "name": "Join remote tag"
+ },
+ {
+ "operation": "join",
+ "change": "format",
+ "name": "Join remote format"
+ },
+ {
+ "operation": "join",
+ "change": "sibling",
+ "name": "Join remote sibling"
+ },
+ {
+ "operation": "join",
+ "change": "adoption",
+ "name": "Join empty adoption"
+ }
+]
diff --git a/corpus/paragraph-edit/reconciliation/commands.jsonl b/corpus/paragraph-edit/reconciliation/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..c765e35203b05ff7b47d532fd820c6d3ed4b25db
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/commands.jsonl
@@ -0,0 +1,9 @@
+{"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\author.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-9095E701", "exit": 0, "stdout": "", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-9095E701 -ExpectedPages 17 -UseCurrentCache -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 17 pages.\r\n", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\remote-paragraphs.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-9095E701 -Remote", "exit": 1, "stdout": "", "stderr": "C:\\one-tests\\remote-paragraphs.ps1 : Exception calling \"UpdatePageContent\" \r\nwith \"4\" argument(s): \"Exception from HRESULT: 0x80042001\"\r\n + CategoryInfo : NotSpecified: (:) [remote-paragraphs.ps1], Metho \r\n dInvocationException\r\n + FullyQualifiedErrorId : COMException,remote-paragraphs.ps1\r\n \r\n", "error": null}
+{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\remote-remaining.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-9095E701 -Remote", "exit": 0, "stdout": "Updating: Split remote tag\r\nApplied: Split remote tag\r\nUpdating: Split remote format\r\nApplied: Split remote format\r\nUpdating: Split remote sibling\r\nApplied: Split remote sibling\r\nUpdating: Join remote prefix\r\nApplied: Join remote prefix\r\nUpdating: Join left boundary\r\nApplied: Join left boundary\r\nUpdating: Join right boundary\r\nApplied: Join right boundary\r\nUpdating: Join remote child\r\nApplied: Join remote child\r\nUpdating: Join remote list\r\nApplied: Join remote list\r\nUpdating: Join remote tag\r\nApplied: Join remote tag\r\nUpdating: Join remote format\r\nApplied: Join remote format\r\nUpdating: Join remote sibling\r\nApplied: Join remote sibling\r\nUpdating: Join empty adoption\r\nApplied: Join empty adoption\r\n", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -Command \"Rename-Item C:\\one-tests\\runs\\capture\\read before-read\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-9095E701 -UseCurrentCache -ExpectedPages -1", "exit": 0, "stdout": "Read 1 sections and 17 pages.\r\n", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/environment.json b/corpus/paragraph-edit/reconciliation/keyboard/before-read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..ec50e97d0556a6542bdc449aaac4beaf6480c8d1
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-9E5BF58B",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/hierarchy.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..808381e23ba8cbc53f30b37bfa0341dcdad7c608
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-000.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bf7b8b30b28d4d40e88a494e67993a5c26b9aa54
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-000.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-001.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1714a322c2eefd02fb76e1bc979eccf9854e5b5e
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-001.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-002.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..63f7dfee059db8e1d9d17df972b64599b568db05
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-002.xml
@@ -0,0 +1,3 @@
+
+Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-003.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c19dea5cd069ba2bc925090db7a71d15c07c898a
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-003.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-004.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..44da7709e34e34a6d90b2a9d7cfe068cf66fb5b5
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-004.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-005.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f11b5f6ca8c941f3da8031a7fbfce4e6faad4feb
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-005.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-006.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..785bb4f4b19981fd48bc02ca4bb604b8ec5b5971
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-006.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-007.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5ff6c0118c8a147bb8e023aaa9b0fa5544a7bcc9
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-007.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-008.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0634f3ae3b027ea11ad895aed7f033f1ace67869
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-008.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-009.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-009.xml
new file mode 100644
index 0000000000000000000000000000000000000000..86401caeefb1ff6f2156df7607321a873df09979
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-009.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-010.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-010.xml
new file mode 100644
index 0000000000000000000000000000000000000000..281f0fc050bd88bd705980c371e7bde592343744
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-010.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-011.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-011.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a0f5faf2b5e67e46083931ec3b0d207dd189156b
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-011.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-012.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-012.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bd03ad783c5068905602383f8a5a296f427d2fb7
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-012.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-013.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-013.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9d27858c41fd796eff0e13e327ecbc5a76a5de2a
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-013.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-014.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-014.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9e75784766110628c3848f7c213bde4f5ea75bf7
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-014.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-015.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-015.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fd8fe79dc73e66d6f49d5d6e1ea249f0251aeb34
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-015.xml
@@ -0,0 +1,20 @@
+
+Bold 🦀 italic é color 東京
+End]]>Fictitious: café, 東京, مرحبا]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-016.xml b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-016.xml
new file mode 100644
index 0000000000000000000000000000000000000000..95c2f2c4a8168f4676e94c924ec043e2c7ec11ac
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/page-016.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/before-read/payloads.json b/corpus/paragraph-edit/reconciliation/keyboard/before-read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3cf45dc1acb611d7f764e0340d73e87192f81407
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/before-read/payloads.json
@@ -0,0 +1 @@
+../../../before/read/payloads.json
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/commands.jsonl b/corpus/paragraph-edit/reconciliation/keyboard/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..fd04864244b9fed72b9a82b2e420b1bfce0b4615
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/commands.jsonl
@@ -0,0 +1,6 @@
+{"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-9E5BF58B -ExpectedPages 17 -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 17 pages.\r\n", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -Command \"Rename-Item C:\\one-tests\\runs\\capture\\read before-read\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-9E5BF58B -UseCurrentCache -ExpectedPages -1", "exit": 0, "stdout": "Read 1 sections and 17 pages.\r\n", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-empty-adoption.ahk b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-empty-adoption.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..952b13d3fd35261e422ac85e69511f0463bbc7a4
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-empty-adoption.ahk
@@ -0,0 +1,14 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{339CBA14-5F09-4E74-98D2-DAC2C4AF85DA}{1}{B0}", "{753F2A26-21FA-49A5-A7E3-3D97DFE56B0F}{40}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "{Left}X"
+Sleep 300
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-empty-adoption.json b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-empty-adoption.json
new file mode 100644
index 0000000000000000000000000000000000000000..ecf6ab42df682e3e6d448858290b3747d8beb17c
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-empty-adoption.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Join empty adoption - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-empty-adoption.png b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-empty-adoption.png
new file mode 100644
index 0000000000000000000000000000000000000000..d70628354ce632013f0abd59c2be48ea2c6e08c2
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-empty-adoption.png differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-left-boundary.ahk b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-left-boundary.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..7aa38e62012770e7cad2a06bca4ba7f453048222
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-left-boundary.ahk
@@ -0,0 +1,14 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{C7C8C394-529E-47CA-A13B-7931CC047FB6}{1}{B0}", "{A5D6D966-6004-458B-92AA-51C8DD659D3A}{40}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "{Right}X"
+Sleep 300
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-left-boundary.json b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-left-boundary.json
new file mode 100644
index 0000000000000000000000000000000000000000..70b32f42a826feb7397a2b29a419de789b8445cf
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-left-boundary.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Join left boundary - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-left-boundary.png b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-left-boundary.png
new file mode 100644
index 0000000000000000000000000000000000000000..0061ba5ef8cb7e064d1a88b40df29dd75e3c1a5f
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-left-boundary.png differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-child.ahk b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-child.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..99cbc6d0c5d9382d512d71698c0aee0068463658
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-child.ahk
@@ -0,0 +1,14 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{AD28524E-CC50-40D0-84FA-157F9867EC05}{1}{B0}", "{E971E60D-4048-4A2C-9151-0CE852F4405D}{46}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "{Right}{Enter}{Tab}Native child"
+Sleep 300
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-child.json b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-child.json
new file mode 100644
index 0000000000000000000000000000000000000000..0e7acec2fc8e2e4832e2c58689fb8327cf66b07e
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-child.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Join remote child - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-child.png b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-child.png
new file mode 100644
index 0000000000000000000000000000000000000000..41f358fadfe146526c8c5a3f4e5d5d5fe62124cc
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-child.png differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-format.ahk b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-format.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..40a9f7997ee7fae5360904700409bb6f818512e5
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-format.ahk
@@ -0,0 +1,14 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{CF3EFEEF-026C-4156-B3B5-5D9FD0637BB0}{1}{B0}", "{C9681C2A-35ED-4F34-85EB-C6297F12C13F}{46}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "^i"
+Sleep 300
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-format.json b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-format.json
new file mode 100644
index 0000000000000000000000000000000000000000..bc5d5b3890d6473f956ab04494bf3afe72fcad2d
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-format.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Join remote format - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-format.png b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-format.png
new file mode 100644
index 0000000000000000000000000000000000000000..24e088b87ca10e388076a229845c5fff2bbfd5c6
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-format.png differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-list.ahk b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-list.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..7686c7bace1e07b5467e518c43c355d48cfde258
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-list.ahk
@@ -0,0 +1,14 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{70252131-654A-4CB2-9015-3264EF828208}{1}{B0}", "{24C6ABE1-25B8-4183-90D0-07A1BFD645EF}{46}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "{Left}^."
+Sleep 300
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-list.json b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-list.json
new file mode 100644
index 0000000000000000000000000000000000000000..f321adc53b804c55d4b61bf21d247b4a3787e38e
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-list.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Join remote list - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-list.png b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-list.png
new file mode 100644
index 0000000000000000000000000000000000000000..d2bb110f48463299fce4cbee4039848035c220f2
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-list.png differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-prefix.ahk b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-prefix.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..f4c750c81ac7e368dd709d3c7d57cd78a15b85fc
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-prefix.ahk
@@ -0,0 +1,25 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{DEA76FCD-5853-477C-AD4A-F165915338BB}{1}{B0}", "{62483E36-C372-4F75-90BF-DE6D1F7A5FC1}{40}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "{Left}X"
+Sleep 300
+app.NavigateTo("{DEA76FCD-5853-477C-AD4A-F165915338BB}{1}{B0}", "{62483E36-C372-4F75-90BF-DE6D1F7A5FC1}{46}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "{Right}Y"
+Sleep 300
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-prefix.json b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-prefix.json
new file mode 100644
index 0000000000000000000000000000000000000000..2427316a487d1601e67641fb36d88590c9927223
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-prefix.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Join remote prefix - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-prefix.png b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-prefix.png
new file mode 100644
index 0000000000000000000000000000000000000000..2778d739d07991a1b9c42d492564ca5fdb7f6b82
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-prefix.png differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-sibling.ahk b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-sibling.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..2198d2df7852d8f6ecde054d3476e336bb9d6bab
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-sibling.ahk
@@ -0,0 +1,14 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{3E68D1A6-BF4B-413B-ACE3-56435E4546F5}{1}{B0}", "{3750DABC-69EB-4A22-82ED-2972300956DE}{51}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "Native sibling"
+Sleep 300
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-sibling.json b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-sibling.json
new file mode 100644
index 0000000000000000000000000000000000000000..03dac1c8180bcfbcb753bfeeb2825938b8b58257
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-sibling.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Join remote sibling - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-sibling.png b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-sibling.png
new file mode 100644
index 0000000000000000000000000000000000000000..f7f8cf46343ea0a3d5ed15c5d9d22e1695ca34a6
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-sibling.png differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-tag.ahk b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-tag.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..9578ae8ae9af6a57fc5f21c66468d2296249a1e0
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-tag.ahk
@@ -0,0 +1,14 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{49638AD3-7021-4A1C-AAA3-B08AB5F5506D}{1}{B0}", "{D86013FB-B193-4A3B-BF7A-753A51E01437}{46}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "{Left}^1"
+Sleep 300
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-tag.json b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-tag.json
new file mode 100644
index 0000000000000000000000000000000000000000..7f3e8b40186521e9992e2cca24f7d2acdbbffece
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-tag.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Join remote tag - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-tag.png b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-tag.png
new file mode 100644
index 0000000000000000000000000000000000000000..f01b0131a1c497972638468e7a0c999dac3ff464
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-remote-tag.png differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-right-boundary.ahk b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-right-boundary.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..492c369ac1f450835d8c1dce3eb33b1a8cd6a783
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-right-boundary.ahk
@@ -0,0 +1,14 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{6098C8BD-5256-42BE-A5A7-561BF30DE077}{1}{B0}", "{0355EDE0-60F1-4C03-BF26-8EB483A0F5AA}{46}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "{Left}X"
+Sleep 300
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-right-boundary.json b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-right-boundary.json
new file mode 100644
index 0000000000000000000000000000000000000000..fd2eb7acc479a71c84b79c2662be49534f28bc74
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-right-boundary.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Join right boundary - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-right-boundary.png b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-right-boundary.png
new file mode 100644
index 0000000000000000000000000000000000000000..6ac724939b56453008f029b61a65fa677bb5edae
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/join-right-boundary.png differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-boundary.ahk b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-boundary.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..9b8fec16d0f58c0851233907872c998f799aa61e
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-boundary.ahk
@@ -0,0 +1,14 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{4E20F67A-9223-4BB5-9191-7EC10CE43F1F}{1}{B0}", "{3224CEC3-F663-4F74-8CBE-540DDC982B1E}{40}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "{Left}{Right 2}X"
+Sleep 300
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-boundary.json b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-boundary.json
new file mode 100644
index 0000000000000000000000000000000000000000..93f62ef7ba9db2af2f44660c7ac577007e4414ec
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-boundary.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Split remote boundary - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-boundary.png b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-boundary.png
new file mode 100644
index 0000000000000000000000000000000000000000..92e84b8dda2e79d94478e727ea16ec0c56d7b22b
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-boundary.png differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-child.ahk b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-child.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..27b163b98270070b5ad3a0c7e12a30e25355f29d
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-child.ahk
@@ -0,0 +1,14 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{AA94EAE9-FD63-4922-86A1-458933A73EA4}{1}{B0}", "{92829D34-66A4-48FE-AC75-907125308AE5}{40}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "{Right}{Enter}{Tab}Native child"
+Sleep 300
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-child.json b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-child.json
new file mode 100644
index 0000000000000000000000000000000000000000..bcc084b8978e3e13c7cce3e92d048b95a3ac8f95
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-child.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Split remote child - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-child.png b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-child.png
new file mode 100644
index 0000000000000000000000000000000000000000..aa79c64fd6a61504778de730ddecf92a29aca2f8
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-child.png differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-format.ahk b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-format.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..cdd54492e2e0dfed7470b87b8387d8e0a445d13b
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-format.ahk
@@ -0,0 +1,14 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{57F7D738-2BD4-42F6-B4AF-DB0B2FC7574E}{1}{B0}", "{12E5B354-A883-4DEB-984F-1461EE79A1D5}{40}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "^i"
+Sleep 300
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-format.json b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-format.json
new file mode 100644
index 0000000000000000000000000000000000000000..483bfdc4151988a5a6752e30df772669f79c31cf
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-format.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Split remote format - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-format.png b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-format.png
new file mode 100644
index 0000000000000000000000000000000000000000..84342d3615920400383f6e7093e0aae8b7edf23e
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-format.png differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-list.ahk b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-list.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..3bb8e6d61e570afc4c9ba8e47def810c5c7a93e9
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-list.ahk
@@ -0,0 +1,14 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{F848F52D-F95C-4D45-B54A-E6E7875961C3}{1}{B0}", "{748CAF50-7793-4033-9CE8-1E8CF040DEBF}{40}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "{Left}^."
+Sleep 300
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-list.json b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-list.json
new file mode 100644
index 0000000000000000000000000000000000000000..c4020169963efb3d26aca606e105882e201ba531
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-list.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Split remote list - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-list.png b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-list.png
new file mode 100644
index 0000000000000000000000000000000000000000..9b784cf0d557d655ded2e7acaf013e379c7659e2
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-list.png differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-prefix.ahk b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-prefix.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..4a47646099cd512508c7ad5e83a89f039c60867e
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-prefix.ahk
@@ -0,0 +1,14 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{2491D9D8-BFA7-46D8-A855-D07263E6470E}{1}{B0}", "{460B2629-58B0-40B0-8590-702A35A91D80}{40}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "{Left}X"
+Sleep 300
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-prefix.json b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-prefix.json
new file mode 100644
index 0000000000000000000000000000000000000000..ca9485dc851e38c515344c4473bd8bd7fd2a8073
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-prefix.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Split remote prefix - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-prefix.png b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-prefix.png
new file mode 100644
index 0000000000000000000000000000000000000000..0204978cd12d0bb4b8166d1ff765dff64bfe30fb
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-prefix.png differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-sibling.ahk b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-sibling.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..b8f92ab23be07941534744220b14e64d15602a57
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-sibling.ahk
@@ -0,0 +1,14 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{A35EA4E1-8740-466B-BBC1-D410A7075FB0}{1}{B0}", "{7FF116ED-B5D3-4F42-B7EC-C3CE00FDC9B0}{46}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "Native sibling"
+Sleep 300
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-sibling.json b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-sibling.json
new file mode 100644
index 0000000000000000000000000000000000000000..798b9aec4480d5c47d8cde873ff40e65ff78bb14
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-sibling.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Split remote sibling - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-sibling.png b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-sibling.png
new file mode 100644
index 0000000000000000000000000000000000000000..67fb9bcd296fea94ee9a9ba63472b711e5c5d017
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-sibling.png differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-tag.ahk b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-tag.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..5b17e5d8801dff64f63535a4199565ca4196dc7d
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-tag.ahk
@@ -0,0 +1,14 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{2245C86F-7CED-4CCD-A2B5-64835D267233}{1}{B0}", "{FF6F426F-6458-4D21-8CA2-C8232DA59D33}{40}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+Sleep 300
+Send "{Left}^1"
+Sleep 300
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-tag.json b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-tag.json
new file mode 100644
index 0000000000000000000000000000000000000000..9c9c0ff33bb97d1537c5d0657ace90a607f47a37
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-tag.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Split remote tag - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-tag.png b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-tag.png
new file mode 100644
index 0000000000000000000000000000000000000000..7e81427bba061f0279451a4f14b96588f01c78f6
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/keyboard/split-remote-tag.png differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/notebook/Open Notebook.onetoc2 b/corpus/paragraph-edit/reconciliation/keyboard/notebook/Open Notebook.onetoc2
new file mode 120000
index 0000000000000000000000000000000000000000..a6c7d197861f52dda5ecaf4da2ab7e471f409518
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/notebook/Open Notebook.onetoc2
@@ -0,0 +1 @@
+../../before/notebook/Open Notebook.onetoc2
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/notebook/synthetic.one b/corpus/paragraph-edit/reconciliation/keyboard/notebook/synthetic.one
new file mode 100644
index 0000000000000000000000000000000000000000..d6e676ae156c22e19ef2bddf12564db6de4a7787
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/keyboard/notebook/synthetic.one differ
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/environment.json b/corpus/paragraph-edit/reconciliation/keyboard/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..c82c7b8b6cdc98c6ae3a69c54d2a990ac668973f
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-9E5BF58B",
+ "cold": false,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/hierarchy.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..419e92d8a14692128ebc7f48cf3b20e52d11931d
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-000.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d9a1f82daa2b5e7b2ed3fa4335172e9dbad09a7b
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-000.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-001.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8f8d40512bec0dd9c467aa5a5c0f74fffe3bb133
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-001.xml
@@ -0,0 +1,3 @@
+
+Xab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-002.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..43ad77528a96ab071122a78a5418187269ac9a4d
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-002.xml
@@ -0,0 +1,3 @@
+
+Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-003.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..34cd615a152bbe14c7cfaaa933fa34f533ce1b3f
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-003.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-004.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..893b75022527482fbcf3499187d4e042b09e6b5e
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-004.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-005.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4f01b688aa176e5c3a188c63f66241684b641d41
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-005.xml
@@ -0,0 +1,3 @@
+
+abX🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-006.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..324ca5e9fb53c82ce7f3219854d430e7d5abd587
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-006.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-007.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..51f5fe1209e28cad1e8abd8c6e58db8c58de2534
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-007.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>XRight]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-008.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9098763fe61c6b672676d8a0598eaa6555602740
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-008.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-009.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-009.xml
new file mode 100644
index 0000000000000000000000000000000000000000..dd981c0dc44d3a625bd8598441a3ad7d7d10d1d7
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-009.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-010.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-010.xml
new file mode 100644
index 0000000000000000000000000000000000000000..83cbece05782059718ed4ae5db93fdb90478589c
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-010.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Native child]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-011.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-011.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6410c5700135cd7d69369642581fe57c46866c26
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-011.xml
@@ -0,0 +1,5 @@
+
+ab🦀cd]]>Right]]>Native child]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-012.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-012.xml
new file mode 100644
index 0000000000000000000000000000000000000000..01c8bad008914d47f41c7b02c8c3b88e6f898afe
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-012.xml
@@ -0,0 +1,4 @@
+
+ab🦀cdX]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-013.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-013.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6bd6fa9d17a5baa409ef05318f89de90729848de
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-013.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-014.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-014.xml
new file mode 100644
index 0000000000000000000000000000000000000000..772d113b0ab2df27a5d8a5f5caf94ccefc7bf8c4
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-014.xml
@@ -0,0 +1,4 @@
+
+Xab🦀cd]]>RightY]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-015.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-015.xml
new file mode 100644
index 0000000000000000000000000000000000000000..23c9b8a06544516a5beeef3db356a4678e1a7f5e
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-015.xml
@@ -0,0 +1,20 @@
+
+Bold 🦀 italic é color 東京
+End]]>Fictitious: café, 東京, مرحبا]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/page-016.xml b/corpus/paragraph-edit/reconciliation/keyboard/read/page-016.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e7a190f3bb55733af77e41212a839ca17e573112
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/page-016.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/read/payloads.json b/corpus/paragraph-edit/reconciliation/keyboard/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3cf45dc1acb611d7f764e0340d73e87192f81407
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/read/payloads.json
@@ -0,0 +1 @@
+../../../before/read/payloads.json
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/run.json b/corpus/paragraph-edit/reconciliation/keyboard/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..0c95f16db00ca937d70cd26052e8ac06f656c1a5
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/run.json
@@ -0,0 +1,18 @@
+{
+ "notebook": "/Users/clo/dev/one/corpus/paragraph-edit/reconciliation/before/notebook",
+ "expected_pages": 17,
+ "author": null,
+ "author_timeout_seconds": 600,
+ "inspect": true,
+ "collect_notebook": true,
+ "base": {
+ "file": "win7-office-base.qcow2",
+ "format": "qcow2",
+ "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346",
+ "virtual_size": 68719476736
+ },
+ "scripts": {
+ "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
+ "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
+ }
+}
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/scripts/cold.ps1 b/corpus/paragraph-edit/reconciliation/keyboard/scripts/cold.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/scripts/cold.ps1
@@ -0,0 +1,27 @@
+param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '')
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
+if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
+ throw 'Choose a run directly below C:\one-tests\runs.'
+}
+if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' }
+$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote'
+if ($CloneHost) {
+ if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) {
+ throw 'The disposable clone hostname does not match this machine.'
+ }
+ New-Item "$key\Options\Paths" -Force | Out-Null
+ New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null
+} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or
+ -not (Test-Path 'C:\one-tests\profile-original-cache')) {
+ throw 'Park the personal OneNote profile before resetting the test cache.'
+}
+$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0'
+$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root))
+if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' }
+New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null
+if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked }
+if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse }
+New-Item "$key\OpenNotebooks" | Out-Null
+New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/scripts/read.ps1 b/corpus/paragraph-edit/reconciliation/keyboard/scripts/read.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/scripts/read.ps1
@@ -0,0 +1,142 @@
+param(
+ [Parameter(Mandatory=$true)][string]$Root,
+ [int]$ExpectedPages = -1,
+ [switch]$UseCurrentCache,
+ [switch]$Pdf,
+ [switch]$KeepOpen,
+ [string]$CloneHost = ''
+)
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
+if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
+ throw 'Choose a run directly below C:\one-tests\runs.'
+}
+$notebook = Join-Path $root 'notebook'
+$output = Join-Path $root 'read'
+if (Test-Path $output) { throw 'Choose a new read destination.' }
+if ($UseCurrentCache) {
+ if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') {
+ throw 'Park the personal OneNote profile before reading test notebooks.'
+ }
+} else {
+ & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost
+}
+New-Item -ItemType Directory -Path $output | Out-Null
+$app = New-Object -ComObject OneNote.Application
+$notebookId = ''
+$failure = $null
+try {
+ $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0)
+ $process = Get-Process ONENOTE
+ @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion;
+ powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } |
+ ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8
+ $sections = @()
+ foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) {
+ $id = ''
+ $app.OpenHierarchy($file.FullName, '', [ref]$id, 0)
+ $sections += $id
+ }
+ $deadline = [DateTime]::UtcNow.AddSeconds(300)
+ $previous = ''
+ $lastChange = ''
+ $stableSince = [DateTime]::UtcNow
+ $settled = $false
+ do {
+ $pages = @{}
+ foreach ($section in $sections) {
+ $hierarchy = ''
+ $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
+ [xml]$xml = $hierarchy
+ foreach ($node in $xml.SelectNodes('//*[@path]')) {
+ if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) {
+ throw 'OneNote opened a section outside the copied notebook.'
+ }
+ }
+ foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) {
+ $id = $node.GetAttribute('ID')
+ $content = ''
+ $app.GetPageContent($id, [ref]$content, 1, 1)
+ $pages[$id] = $content
+ }
+ }
+ $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] }))
+ if ($signature -ne $previous) {
+ $lastChange = $previous
+ $previous = $signature
+ $stableSince = [DateTime]::UtcNow
+ }
+ if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or
+ ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and
+ ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break }
+ Start-Sleep -Milliseconds 250
+ } while ([DateTime]::UtcNow -lt $deadline)
+ if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) {
+ [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8)
+ $index = 0
+ foreach ($id in @($pages.Keys | Sort-Object)) {
+ [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
+ $index++
+ }
+ $hierarchy = ''
+ $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1)
+ [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8)
+ throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled."
+ }
+ $index = 0
+ $payloads = @()
+ foreach ($id in @($pages.Keys | Sort-Object)) {
+ [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
+ if ($Pdf) {
+ $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index)
+ $app.NavigateTo($id, '', $false)
+ $app.Publish($id, $pdfPath, 3, '')
+ if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' }
+ }
+ [xml]$page = $pages[$id]
+ foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) {
+ $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache'))
+ $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant()
+ [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes)
+ $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID');
+ kind = $file.LocalName; name = $file.GetAttribute('preferredName');
+ sha256 = $hash; bytes = $bytes.Length }
+ }
+ $index++
+ }
+ [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8)
+ $all = ''
+ $app.GetHierarchy($notebookId, 4, [ref]$all, 1)
+ [xml]$finalTree = $all
+ $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique)
+ if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) {
+ throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.'
+ }
+ [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8)
+ Write-Output "Read $($sections.Count) sections and $($pages.Count) pages."
+} catch {
+ $failure = $_
+ [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
+ throw
+} finally {
+ try {
+ try {
+ if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) }
+ if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) {
+ $app.CloseNotebook($notebookId, $false)
+ }
+ } catch {
+ if ($null -eq $failure) { throw }
+ [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
+ }
+ } finally {
+ [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
+ }
+ $app = $null
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+ if (-not $UseCurrentCache -and -not $CloneHost) {
+ Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10
+ }
+}
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/source.json b/corpus/paragraph-edit/reconciliation/keyboard/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..e58756a6eb74645ea0cbc4787009065b7d62a892
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 4800,
+ "sha256": "1122dbd6fe3ee18a8cdf6c1e65521faa69119bafb220828fc86e7c858d7e3e76",
+ "mtime_ns": 1788887226300088396
+ },
+ {
+ "path": "synthetic.one",
+ "bytes": 100944,
+ "sha256": "c40709e26e9e5f177956747cb4b862f8a9f2c1c0e155b77dd54abb148ae9bba5",
+ "mtime_ns": 1788887226300365479
+ }
+]
diff --git a/corpus/paragraph-edit/reconciliation/keyboard/teardown.json b/corpus/paragraph-edit/reconciliation/keyboard/teardown.json
new file mode 120000
index 0000000000000000000000000000000000000000..b0d2ee26322df655cbbf9e42f6c30b72a4d110a5
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/keyboard/teardown.json
@@ -0,0 +1 @@
+../../joined/teardown.json
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/candidate/synthetic.one b/corpus/paragraph-edit/reconciliation/reconciled/candidate/synthetic.one
new file mode 100644
index 0000000000000000000000000000000000000000..1d8e772b17c02f0800c4007232b3e463f018afd2
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/reconciled/candidate/synthetic.one differ
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/manifest.json b/corpus/paragraph-edit/reconciliation/reconciled/manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..8b9e4249a69e21b98eb28554ee01660413e1874c
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/manifest.json
@@ -0,0 +1,323 @@
+[
+ {
+ "case": "Split remote prefix",
+ "dependent_receipt": "{CE7388B9-8702-4628-B5FD-1CB0A86C7579},1",
+ "intent": {
+ "author": "Offline author",
+ "created": 1473355695,
+ "guid": [
+ 72,
+ 3,
+ 21,
+ 225,
+ 173,
+ 175,
+ 163,
+ 66,
+ 181,
+ 182,
+ 27,
+ 215,
+ 235,
+ 175,
+ 30,
+ 101
+ ],
+ "offset": 2,
+ "text": "{C4C5964A-00F6-06E5-1947-EF9DD9BFF93E},44"
+ },
+ "outcome": "automatic",
+ "receipt": "{32C8046A-B530-44D9-932E-964D583CE2DC},1",
+ "space": "{A65F69BB-E7E1-008D-3482-4FC58FF0A3B0},1"
+ },
+ {
+ "case": "Split remote boundary",
+ "dependent_receipt": "{BA2BDF8F-DCC0-4065-90F1-6983A1F37E15},1",
+ "intent": {
+ "author": "Offline author",
+ "created": 1473355696,
+ "guid": [
+ 121,
+ 163,
+ 49,
+ 221,
+ 144,
+ 2,
+ 69,
+ 72,
+ 145,
+ 115,
+ 17,
+ 231,
+ 77,
+ 231,
+ 13,
+ 167
+ ],
+ "offset": 2,
+ "text": "{B0EA7EA0-AE25-0921-1069-CBBA308ECFA0},44"
+ },
+ "outcome": "reviewed",
+ "receipt": "{EE6FDF11-A1B9-402D-B1BE-F7EE9004909F},1",
+ "space": "{CCEE4619-CA65-0DE0-0D46-E176E0F2DBA1},1"
+ },
+ {
+ "case": "Split remote child",
+ "dependent_receipt": "{FFEEB215-51E2-44B6-8D3B-475400347A22},1",
+ "intent": {
+ "author": "Offline author",
+ "created": 1473355696,
+ "guid": [
+ 229,
+ 160,
+ 224,
+ 139,
+ 80,
+ 49,
+ 248,
+ 79,
+ 147,
+ 199,
+ 22,
+ 222,
+ 227,
+ 116,
+ 32,
+ 195
+ ],
+ "offset": 2,
+ "text": "{104C2D57-3EE2-0EAB-30A2-0FC6C9266E5B},44"
+ },
+ "outcome": "reviewed",
+ "receipt": "{B3E8433E-8363-4424-8B20-C0F3A9712D81},1",
+ "space": "{285A5A8A-A525-0F77-1A76-DA3EDFB1DA1A},1"
+ },
+ {
+ "case": "Split remote list",
+ "dependent_receipt": "{A50CB64F-C580-43E9-B1DF-522FB18B6B51},1",
+ "intent": {
+ "author": "Offline author",
+ "created": 1473355697,
+ "guid": [
+ 6,
+ 118,
+ 122,
+ 220,
+ 71,
+ 210,
+ 164,
+ 79,
+ 154,
+ 8,
+ 22,
+ 195,
+ 142,
+ 16,
+ 93,
+ 166
+ ],
+ "offset": 2,
+ "text": "{F6421F33-2FD5-0666-003F-813B1C563A01},44"
+ },
+ "outcome": "reviewed",
+ "receipt": "{967507AF-A4FC-4593-85DA-DB4BDB215CB8},1",
+ "space": "{7A86454E-A11A-0B10-299D-79506B4F857D},1"
+ },
+ {
+ "case": "Split remote tag",
+ "dependent_receipt": "{8C68C1BF-FA7C-414C-B4C8-6D6330816B67},1",
+ "intent": {
+ "author": "Offline author",
+ "created": 1473355698,
+ "guid": [
+ 190,
+ 238,
+ 93,
+ 84,
+ 186,
+ 28,
+ 195,
+ 65,
+ 140,
+ 108,
+ 184,
+ 101,
+ 128,
+ 188,
+ 55,
+ 19
+ ],
+ "offset": 2,
+ "text": "{7DA1F20C-3C1E-0B74-1075-5794C1B3798D},44"
+ },
+ "outcome": "reviewed",
+ "receipt": "{ABF29E0E-5267-416D-BCDC-C6A5E7378D30},1",
+ "space": "{A08B780C-24AB-0A98-3E62-FB34B130968D},1"
+ },
+ {
+ "case": "Split remote format",
+ "dependent_receipt": "{D9948EA1-D7AA-450F-949C-1489C5B37D92},1",
+ "intent": {
+ "author": "Offline author",
+ "created": 1473355698,
+ "guid": [
+ 56,
+ 178,
+ 113,
+ 83,
+ 197,
+ 69,
+ 46,
+ 64,
+ 181,
+ 97,
+ 8,
+ 109,
+ 138,
+ 17,
+ 10,
+ 110
+ ],
+ "offset": 2,
+ "text": "{902B0337-F0C5-0BBE-0498-8BD6026F456B},44"
+ },
+ "outcome": "automatic",
+ "receipt": "{C93BEE05-161E-45D1-90A7-9EBCB1889577},1",
+ "space": "{D539675B-7392-04A3-2878-44BCC3D1B3F0},1"
+ },
+ {
+ "case": "Split remote sibling",
+ "dependent_receipt": "{971EC3CC-2FD5-4A40-A9DF-A7E250057BF6},1",
+ "intent": {
+ "author": "Offline author",
+ "created": 1473355699,
+ "guid": [
+ 29,
+ 215,
+ 155,
+ 66,
+ 220,
+ 243,
+ 26,
+ 76,
+ 178,
+ 203,
+ 57,
+ 141,
+ 56,
+ 243,
+ 174,
+ 46
+ ],
+ "offset": 2,
+ "text": "{FD3FA68E-ED95-0917-2B3B-5C79ECEB2D0E},44"
+ },
+ "outcome": "automatic",
+ "receipt": "{CF2A16BE-1BC9-4A42-BA60-EA637480552C},1",
+ "space": "{21901482-DF06-003E-2716-4BA74B11BB0E},1"
+ },
+ {
+ "case": "Join remote prefix",
+ "dependent_receipt": "{2D17C528-B546-40FA-8485-16C9B0DABF6E},1",
+ "intent": {
+ "author": "Offline author",
+ "left": "{E0868E55-9B34-0920-0C68-41DAF36CBB7F},44",
+ "right": "{E0868E55-9B34-0920-0C68-41DAF36CBB7F},50"
+ },
+ "outcome": "automatic",
+ "receipt": "{F63692D3-3D09-43A6-890D-10C65A159B16},1",
+ "space": "{5C69DFAE-0015-0129-319D-6ED27D45DC05},1"
+ },
+ {
+ "case": "Join left boundary",
+ "dependent_receipt": "{0CA7FB85-F492-47BD-919C-6A919CFB409E},1",
+ "intent": {
+ "author": "Offline author",
+ "left": "{27186905-3842-03DE-0E7D-CE7F31737984},44",
+ "right": "{27186905-3842-03DE-0E7D-CE7F31737984},50"
+ },
+ "outcome": "reviewed",
+ "receipt": "{166E0BE9-230F-43E9-8555-C6776B5D6183},1",
+ "space": "{450673F7-0AD8-019F-3DEC-E68620129B08},1"
+ },
+ {
+ "case": "Join right boundary",
+ "dependent_receipt": "{F89D5093-F20C-4391-9F38-6C716922731D},1",
+ "intent": {
+ "author": "Offline author",
+ "left": "{819B5D83-38B7-0A56-23F1-11036FB61114},44",
+ "right": "{819B5D83-38B7-0A56-23F1-11036FB61114},50"
+ },
+ "outcome": "reviewed",
+ "receipt": "{084D104B-CB74-492A-8EAF-0D9E8E6F6099},1",
+ "space": "{E25678DE-0A10-04EB-3970-C9AC1F1B04C9},1"
+ },
+ {
+ "case": "Join remote child",
+ "dependent_receipt": "{2196E1EA-49A6-4A1C-B0FE-3CCBCD1EA993},1",
+ "intent": {
+ "author": "Offline author",
+ "left": "{6BBF566E-180E-0C79-0D86-935FBEE2A4E3},44",
+ "right": "{6BBF566E-180E-0C79-0D86-935FBEE2A4E3},50"
+ },
+ "outcome": "reviewed",
+ "receipt": "{69B85C08-6FD0-4756-B20A-39A342BA516C},1",
+ "space": "{2FE6E22D-9416-0685-182D-8AC8747108BB},1"
+ },
+ {
+ "case": "Join remote list",
+ "intent": {
+ "author": "Offline author",
+ "left": "{A6081B82-7DFE-07D6-0C07-981653C0A151},44",
+ "right": "{A6081B82-7DFE-07D6-0C07-981653C0A151},50"
+ },
+ "outcome": "retained_conflict",
+ "space": "{F2EB9152-3D0C-0AE7-0CC2-ADD3039466B6},1"
+ },
+ {
+ "case": "Join remote tag",
+ "dependent_receipt": "{F4EB3C75-AB8C-4E7C-9A64-69593946321E},1",
+ "intent": {
+ "author": "Offline author",
+ "left": "{5AAEA398-E9D5-0C6E-23AD-EA8DBDF6F089},44",
+ "right": "{5AAEA398-E9D5-0C6E-23AD-EA8DBDF6F089},50"
+ },
+ "outcome": "reviewed",
+ "receipt": "{5AC635E3-AC61-41EF-9424-737AD0EAAF32},1",
+ "space": "{CBAD3AB0-2867-0C49-3674-2F3D59E3B4D3},1"
+ },
+ {
+ "case": "Join remote format",
+ "dependent_receipt": "{649CC682-7CCB-4A9E-9C26-06DDF3A7F49E},1",
+ "intent": {
+ "author": "Offline author",
+ "left": "{4BA6AC49-6DAB-0961-193C-599E93042581},44",
+ "right": "{4BA6AC49-6DAB-0961-193C-599E93042581},50"
+ },
+ "outcome": "automatic",
+ "receipt": "{F63BD836-C10E-4928-890D-9BCB13C1C3F5},1",
+ "space": "{4DF04E8C-5A2A-0703-2F62-C2283C759F0E},1"
+ },
+ {
+ "case": "Join remote sibling",
+ "dependent_receipt": "{2A4C6024-4FF9-46A9-83A1-34F1A7EF62F2},1",
+ "intent": {
+ "author": "Offline author",
+ "left": "{B59E6ADF-31AD-0C77-1E3A-B6C5DC1FB260},44",
+ "right": "{B59E6ADF-31AD-0C77-1E3A-B6C5DC1FB260},50"
+ },
+ "outcome": "automatic",
+ "receipt": "{B29D40A5-BAE2-48BA-9028-25A1B14CF27F},1",
+ "space": "{BCA661C5-E70D-076E-3034-C9F4B253A24B},1"
+ },
+ {
+ "case": "Join empty adoption",
+ "intent": {
+ "author": "Offline author",
+ "left": "{F7F19A45-79BC-0FF0-3B34-A22033F38FB1},45",
+ "right": "{F7F19A45-79BC-0FF0-3B34-A22033F38FB1},50"
+ },
+ "outcome": "retained_conflict",
+ "space": "{B1520A77-074F-0821-0405-457528B96164},1"
+ }
+]
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/commands.jsonl b/corpus/paragraph-edit/reconciliation/reconciled/native/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..cb3c2a111b21a7326b68fd579f1793e987266689
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/commands.jsonl
@@ -0,0 +1,3 @@
+{"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-C4BC54FD -ExpectedPages 17", "exit": 0, "stdout": "Read 1 sections and 17 pages.\r\n", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/notebook/Open Notebook.onetoc2 b/corpus/paragraph-edit/reconciliation/reconciled/native/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..b9bab8c52537429d44220695235ee851fb06b25f
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/reconciled/native/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/notebook/synthetic.one b/corpus/paragraph-edit/reconciliation/reconciled/native/notebook/synthetic.one
new file mode 100644
index 0000000000000000000000000000000000000000..936b9ab9cb382a5f778585b059c36bb42ec3cfd7
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/reconciled/native/notebook/synthetic.one differ
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/environment.json b/corpus/paragraph-edit/reconciliation/reconciled/native/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..65aa2c11c1ebdbd6f42f5804c7aab2e333b85de8
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-C4BC54FD",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/hierarchy.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0441bcc4c362b34c6f9558ee8700e0455d3b0dbf
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-000.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..81c1ddb53e5bf54933e0d631bcd218afca494a83
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-000.xml
@@ -0,0 +1,4 @@
+
+Xab]]>🦀Cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-001.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1e3c0a44d06f38f8164cd0599202c8bc0e71ae26
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-001.xml
@@ -0,0 +1,4 @@
+
+ab]]>🦀Cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-002.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d3fc936e7fe651db0077215d8b151acaec9f3a81
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-002.xml
@@ -0,0 +1,3 @@
+
+Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-003.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..74ac54b851bf4c5a248faac016ae80a3491f38d8
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-003.xml
@@ -0,0 +1,3 @@
+
+ab🦀CdRight]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-004.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7f704143d0cd08864436d46fc2d9c435b3b7224c
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-004.xml
@@ -0,0 +1,3 @@
+
+ab🦀CdXRight]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-005.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..73e4802abc70716f755ab5abcda28d93156d84b6
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-005.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-006.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8c186c2049b406f4d2988b54dc6c4ab768fa1a63
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-006.xml
@@ -0,0 +1,4 @@
+
+abX]]>🦀Cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-007.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6caf646c16101926c31bf4e183e22b1a71fe0bc8
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-007.xml
@@ -0,0 +1,3 @@
+
+ab🦀CdRight]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-008.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a64bd030159d49e5a39d1dea87b50737558f004d
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-008.xml
@@ -0,0 +1,4 @@
+
+ab]]>🦀Cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-009.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-009.xml
new file mode 100644
index 0000000000000000000000000000000000000000..574e842905eee4665974e53774334610e01e604d
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-009.xml
@@ -0,0 +1,4 @@
+
+ab]]>🦀Cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-010.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-010.xml
new file mode 100644
index 0000000000000000000000000000000000000000..53ce13775c9a4597cacd837f842cdc5c766c9f57
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-010.xml
@@ -0,0 +1,4 @@
+
+ab🦀CdRight]]>Native child]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-011.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-011.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5f9da940793a81e5edd9793998588904f3761844
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-011.xml
@@ -0,0 +1,5 @@
+
+ab]]>🦀Cd]]>Native child]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-012.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-012.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e0f5b22e79144bbc918089e91c4c232a4304ca4a
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-012.xml
@@ -0,0 +1,20 @@
+
+Bold 🦀 italic é color 東京
+End]]>Fictitious: café, 東京, مرحبا]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-013.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-013.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2674fc5835ded06fdb7c449ffa12c29ea2bad9d2
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-013.xml
@@ -0,0 +1,4 @@
+
+ab]]>🦀Cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-014.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-014.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ea2d860a04647d8391d6e08fcb720119ceeeeb76
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-014.xml
@@ -0,0 +1,3 @@
+
+ab🦀CdXRight]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-015.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-015.xml
new file mode 100644
index 0000000000000000000000000000000000000000..95b0b9ab9926bf25580ce122c42b43d21e5dbe21
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-015.xml
@@ -0,0 +1,4 @@
+
+ab🦀CdRight]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-016.xml b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-016.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fba65e6542efe81eaa6b811b94f4d245b44bdf4c
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/page-016.xml
@@ -0,0 +1,3 @@
+
+Xab🦀CdRightY]]>
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/read/payloads.json b/corpus/paragraph-edit/reconciliation/reconciled/native/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..787b4bae85038cda922f8befac6c8ed361fa8035
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/read/payloads.json
@@ -0,0 +1 @@
+../../../../before/read/payloads.json
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/run.json b/corpus/paragraph-edit/reconciliation/reconciled/native/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..546fce9d6b573ef588180ddb0b1e6da6b0b249c6
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/run.json
@@ -0,0 +1,18 @@
+{
+ "notebook": "/Users/clo/dev/one/evidence/m10/paragraph-native-reconciled-01/candidate",
+ "expected_pages": 17,
+ "author": null,
+ "author_timeout_seconds": 600,
+ "inspect": false,
+ "collect_notebook": true,
+ "base": {
+ "file": "win7-office-base.qcow2",
+ "format": "qcow2",
+ "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346",
+ "virtual_size": 68719476736
+ },
+ "scripts": {
+ "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
+ "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
+ }
+}
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/scripts/cold.ps1 b/corpus/paragraph-edit/reconciliation/reconciled/native/scripts/cold.ps1
new file mode 120000
index 0000000000000000000000000000000000000000..26b7b0452f69923a3f8cb748d6b5206dbee28c45
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/scripts/cold.ps1
@@ -0,0 +1 @@
+../../../keyboard/scripts/cold.ps1
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/scripts/read.ps1 b/corpus/paragraph-edit/reconciliation/reconciled/native/scripts/read.ps1
new file mode 120000
index 0000000000000000000000000000000000000000..112acdbaf069e052b224eff502f91d8a6b8b626e
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/scripts/read.ps1
@@ -0,0 +1 @@
+../../../keyboard/scripts/read.ps1
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/source.json b/corpus/paragraph-edit/reconciliation/reconciled/native/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..7840b52fea33cd3cbab2c8bfb2072069366e8215
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/source.json
@@ -0,0 +1,8 @@
+[
+ {
+ "path": "synthetic.one",
+ "bytes": 171880,
+ "sha256": "1d35503a9b47fcd2f1e776e77f200a4d7390c6e1d3a63974f4e2b2cea7959692",
+ "mtime_ns": 1788888504006534380
+ }
+]
diff --git a/corpus/paragraph-edit/reconciliation/reconciled/native/teardown.json b/corpus/paragraph-edit/reconciliation/reconciled/native/teardown.json
new file mode 120000
index 0000000000000000000000000000000000000000..47a1192a30ea7da90676d631bb9a2687d829b43e
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/reconciled/native/teardown.json
@@ -0,0 +1 @@
+../../../joined/teardown.json
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote-remaining-script.json b/corpus/paragraph-edit/reconciliation/remote-remaining-script.json
new file mode 100644
index 0000000000000000000000000000000000000000..4df2d62d423d2a267df09aa87e10ef250a32727b
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote-remaining-script.json
@@ -0,0 +1,5 @@
+{
+ "sha256": "a7e23f927eaa9b7205aca31a37ac3eea3f27766c3661ac631cd07eb2d65f4ef4",
+ "source": "tools/native/paragraph-reconciliation.ps1",
+ "change": "Resume only cases 4 through 15 after observed first four applied updates; TagDef moved before PageSettings per published XSD"
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote-script.json b/corpus/paragraph-edit/reconciliation/remote-script.json
new file mode 100644
index 0000000000000000000000000000000000000000..b74a04ca57829688a890f7454a1ca83b4262b623
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote-script.json
@@ -0,0 +1,5 @@
+{
+ "sha256": "c9da4ad1ca8af8203f1ceed49d34f6bd37cfb0b383082e4c808f22c34b5424b9",
+ "source": "tools/native/paragraph-reconciliation.ps1",
+ "switch": "Remote"
+}
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote-updates/Join empty adoption.xml b/corpus/paragraph-edit/reconciliation/remote-updates/Join empty adoption.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6e9b989ba2bea5d78848e235446f5a1d816ac570
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote-updates/Join empty adoption.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ X
+
+
+ Right]]>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote-updates/Join left boundary.xml b/corpus/paragraph-edit/reconciliation/remote-updates/Join left boundary.xml
new file mode 100644
index 0000000000000000000000000000000000000000..649b08b1dbea08c72caf0ff48748c7f9d70c39ea
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote-updates/Join left boundary.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <span
+style='font-weight:bold'>ab🦀cd</span>X
+
+
+ Right]]>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote-updates/Join remote child.xml b/corpus/paragraph-edit/reconciliation/remote-updates/Join remote child.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7d103b9d1f228f5ed2acf5f5da485734a81a3895
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote-updates/Join remote child.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ab🦀cd]]>
+
+
+ Right]]>
+
+
+ Native child
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote-updates/Join remote format.xml b/corpus/paragraph-edit/reconciliation/remote-updates/Join remote format.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a7577dc5529eca0e09c26e9ee0fc8cfc785e5e3c
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote-updates/Join remote format.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ab🦀cd]]>
+
+
+ <i>Right</i>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote-updates/Join remote list.xml b/corpus/paragraph-edit/reconciliation/remote-updates/Join remote list.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e393bde54011f484dc827a1849cf47b95bd9031f
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote-updates/Join remote list.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ab🦀cd]]>
+
+
+
+
+
+ Right]]>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote-updates/Join remote prefix.xml b/corpus/paragraph-edit/reconciliation/remote-updates/Join remote prefix.xml
new file mode 100644
index 0000000000000000000000000000000000000000..52875cc8a7e087674e0ddf2e3e7a18dfd4884e07
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote-updates/Join remote prefix.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ X<span
+style='font-weight:bold'>ab🦀cd</span>
+
+
+ <span
+style='font-weight:bold'>Right</span>Y
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote-updates/Join remote sibling.xml b/corpus/paragraph-edit/reconciliation/remote-updates/Join remote sibling.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0d928152d16a316a4fe0f7c74d931a63a78d1219
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote-updates/Join remote sibling.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ab🦀cd]]>
+
+
+ Right]]>
+
+
+ Native sibling
+
+
+
+
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote-updates/Join remote tag.xml b/corpus/paragraph-edit/reconciliation/remote-updates/Join remote tag.xml
new file mode 100644
index 0000000000000000000000000000000000000000..227960ad01d80afc5b30b948a747a28213e07005
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote-updates/Join remote tag.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ab🦀cd]]>
+
+
+
+ Right]]>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote-updates/Join right boundary.xml b/corpus/paragraph-edit/reconciliation/remote-updates/Join right boundary.xml
new file mode 100644
index 0000000000000000000000000000000000000000..047877a4097d7ee7fac81b1ab952b9fb12b157fc
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote-updates/Join right boundary.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ab🦀cd]]>
+
+
+ X<span
+style='font-weight:bold'>Right</span>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote-updates/Split remote format.xml b/corpus/paragraph-edit/reconciliation/remote-updates/Split remote format.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0e968891ebfd260b0b443a9db10f083699efe535
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote-updates/Split remote format.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <i>ab🦀cd</i>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote-updates/Split remote sibling.xml b/corpus/paragraph-edit/reconciliation/remote-updates/Split remote sibling.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ea38f26d72f1371d5d46f52d35f41ba2424bccbf
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote-updates/Split remote sibling.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ab🦀cd]]>
+
+
+ Native sibling
+
+
+
+
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote-updates/Split remote tag.xml b/corpus/paragraph-edit/reconciliation/remote-updates/Split remote tag.xml
new file mode 100644
index 0000000000000000000000000000000000000000..97c71db5ab80678dcda683481491c56bd590f28d
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote-updates/Split remote tag.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ab🦀cd]]>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote/notebook/Open Notebook.onetoc2 b/corpus/paragraph-edit/reconciliation/remote/notebook/Open Notebook.onetoc2
new file mode 120000
index 0000000000000000000000000000000000000000..a6c7d197861f52dda5ecaf4da2ab7e471f409518
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/notebook/Open Notebook.onetoc2
@@ -0,0 +1 @@
+../../before/notebook/Open Notebook.onetoc2
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote/notebook/synthetic.one b/corpus/paragraph-edit/reconciliation/remote/notebook/synthetic.one
new file mode 100644
index 0000000000000000000000000000000000000000..317425aa5b0367d84b785d972cc4a4bcf286ecc4
Binary files /dev/null and b/corpus/paragraph-edit/reconciliation/remote/notebook/synthetic.one differ
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/environment.json b/corpus/paragraph-edit/reconciliation/remote/read/environment.json
new file mode 120000
index 0000000000000000000000000000000000000000..032ba3b41dd4114f7b416f0781d9c5fb61796d7f
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/environment.json
@@ -0,0 +1 @@
+../../before/read/environment.json
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/hierarchy.xml b/corpus/paragraph-edit/reconciliation/remote/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0efc0fd8af4ecb859ebc80e4c3ad2d10231ff47d
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-000.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..48b6406f1a0124f1c3a0e537b1ba087b433f683f
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-000.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-001.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c1876c96a6dc66136dd0908386d1a9d25b48a4a9
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-001.xml
@@ -0,0 +1,4 @@
+
+ab🦀cdX]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-002.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..78025286ae6e0798de72860799f72e78b2301a6a
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-002.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>RightY]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-003.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..190d65e02977855084eb3375dd41ba7dd2250d77
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-003.xml
@@ -0,0 +1,20 @@
+
+Bold 🦀 italic é color 東京
+End]]>Fictitious: café, 東京, مرحبا]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-004.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6313d0ac357e8394824dd785fabcc9e759246b2d
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-004.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-005.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f5f83c486fec6dae9898b422d7211de61dc777fa
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-005.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-006.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7d7d75d926c6854fbaf261b76cdcbec4a67a0fbb
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-006.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-007.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9f3c8510f80e5cf239032fbd00f60a2e6f6ec025
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-007.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-008.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a8162ce14ca9ae4e5a2538fa9480af7701304cd7
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-008.xml
@@ -0,0 +1,3 @@
+
+abX🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-009.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-009.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ada9dbe3ec9126928bd5a151719c61b4ab678e41
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-009.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-010.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-010.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8eea3550dfa7046e7d4460d30c243a0d1919364d
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-010.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-011.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-011.xml
new file mode 100644
index 0000000000000000000000000000000000000000..dd80b12e9c3458b94ae87426c15b98872c84a310
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-011.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-012.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-012.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5a58d4f7d6a459b9e14c20b30ebea7eeb78056b2
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-012.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-013.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-013.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e06525cfacb955dc729d15c6bcec45ac3cd87291
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-013.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-014.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-014.xml
new file mode 100644
index 0000000000000000000000000000000000000000..aa6494b99e07b4576b677ed40d0b9aff44cf4129
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-014.xml
@@ -0,0 +1,3 @@
+
+ab🦀cd]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-015.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-015.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3b084c66185892c8120066dd399341a089d29022
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-015.xml
@@ -0,0 +1,4 @@
+
+ab🦀cd]]>Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/page-016.xml b/corpus/paragraph-edit/reconciliation/remote/read/page-016.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8bab569f32010cc2aef793831fade3caed64fc8c
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/page-016.xml
@@ -0,0 +1,3 @@
+
+Right]]>
diff --git a/corpus/paragraph-edit/reconciliation/remote/read/payloads.json b/corpus/paragraph-edit/reconciliation/remote/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3cf45dc1acb611d7f764e0340d73e87192f81407
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/remote/read/payloads.json
@@ -0,0 +1 @@
+../../../before/read/payloads.json
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/run.json b/corpus/paragraph-edit/reconciliation/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..712a6a5028a94f2693f4b3215433a520f2d4c439
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/run.json
@@ -0,0 +1,19 @@
+{
+ "notebook": "/Users/clo/dev/one/corpus/formatted-insertion/native-paragraph/candidate",
+ "expected_pages": 17,
+ "author": "tools/native/paragraph-reconciliation.ps1",
+ "author_timeout_seconds": 600,
+ "inspect": true,
+ "collect_notebook": true,
+ "base": {
+ "file": "win7-office-base.qcow2",
+ "format": "qcow2",
+ "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346",
+ "virtual_size": 68719476736
+ },
+ "scripts": {
+ "author.ps1": "72afcfc5d33a43a2c03744b3378be461e869ba4e81fad6af8e73de17d99c2b5e",
+ "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
+ "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
+ }
+}
diff --git a/corpus/paragraph-edit/reconciliation/scripts/author.ps1 b/corpus/paragraph-edit/reconciliation/scripts/author.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..3404b6e657a91af38ae38baaed70ac43ed5d30a6
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/scripts/author.ps1
@@ -0,0 +1,122 @@
+param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost, [switch]$Remote)
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+if (-not $Remote) { & "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost }
+$app = New-Object -ComObject OneNote.Application
+$notebook = ''
+$ns = 'http://schemas.microsoft.com/office/onenote/2010/onenote'
+try {
+ $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebook, 0)
+ $section = ''
+ $app.OpenHierarchy('synthetic.one', $notebook, [ref]$section, 0)
+ if (-not $Remote) {
+ $cases = @(
+ @{ name = 'Split remote prefix'; operation = 'split'; change = 'prefix' },
+ @{ name = 'Split remote boundary'; operation = 'split'; change = 'boundary' },
+ @{ name = 'Split remote child'; operation = 'split'; change = 'child' },
+ @{ name = 'Split remote list'; operation = 'split'; change = 'list' },
+ @{ name = 'Split remote tag'; operation = 'split'; change = 'tag' },
+ @{ name = 'Split remote format'; operation = 'split'; change = 'format' },
+ @{ name = 'Split remote sibling'; operation = 'split'; change = 'sibling' },
+ @{ name = 'Join remote prefix'; operation = 'join'; change = 'prefix' },
+ @{ name = 'Join left boundary'; operation = 'join'; change = 'boundary' },
+ @{ name = 'Join right boundary'; operation = 'join'; change = 'right-boundary' },
+ @{ name = 'Join remote child'; operation = 'join'; change = 'child' },
+ @{ name = 'Join remote list'; operation = 'join'; change = 'list' },
+ @{ name = 'Join remote tag'; operation = 'join'; change = 'tag' },
+ @{ name = 'Join remote format'; operation = 'join'; change = 'format' },
+ @{ name = 'Join remote sibling'; operation = 'join'; change = 'sibling' },
+ @{ name = 'Join empty adoption'; operation = 'join'; change = 'adoption' }
+ )
+ foreach ($case in $cases) {
+ $page = ''
+ $app.CreateNewPage($section, [ref]$page, 0)
+ $left = if ($case.change -eq 'adoption') { '' } else { 'ab' + [char]::ConvertFromUtf32(0x1F980) + 'cd' }
+ $body = '' + $left + ']]>'
+ if ($case.operation -eq 'join') { $body += 'Right]]>' }
+ $body += 'Preserved sibling'
+ $xml = '' + $case.name + '' + $body + ''
+ $app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $true)
+ }
+ $cases | ConvertTo-Json -Depth 6 | Set-Content (Join-Path $Root 'cases.json') -Encoding UTF8
+ } else {
+ $cases = Get-Content (Join-Path $Root 'cases.json') -Raw | ConvertFrom-Json
+ $hierarchy = ''
+ $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
+ [xml]$tree = $hierarchy
+ $manager = New-Object Xml.XmlNamespaceManager($tree.NameTable)
+ $manager.AddNamespace('one', $ns)
+ foreach ($case in $cases) {
+ $page = @($tree.SelectNodes('//one:Page', $manager) | Where-Object { $_.GetAttribute('name') -eq $case.name })
+ if ($page.Count -ne 1) { throw ('Expected one native control page: ' + $case.name) }
+ $content = ''
+ $app.GetPageContent($page[0].GetAttribute('ID'), [ref]$content, 0, 1)
+ [xml]$xml = $content
+ $manager = New-Object Xml.XmlNamespaceManager($xml.NameTable)
+ $manager.AddNamespace('one', $ns)
+ $outline = $xml.SelectSingleNode('/one:Page/one:Outline/one:OEChildren', $manager)
+ $paragraphs = @($outline.SelectNodes('one:OE', $manager))
+ $left = $paragraphs[0]
+ $target = if ($case.operation -eq 'join') { $paragraphs[1] } else { $left }
+ switch ($case.change) {
+ 'prefix' {
+ $text = $left.SelectSingleNode('one:T', $manager)
+ $text.InnerText = 'X' + $text.InnerText
+ if ($case.operation -eq 'join') {
+ $text = $target.SelectSingleNode('one:T', $manager)
+ $text.InnerText += 'Y'
+ }
+ }
+ 'boundary' {
+ $text = $left.SelectSingleNode('one:T', $manager)
+ if ($case.operation -eq 'split') {
+ if (-not $text.InnerText.Contains('ab')) { throw 'The split boundary control lost its text' }
+ $text.InnerText = $text.InnerText.Replace('ab', 'abX')
+ } else { $text.InnerText += 'X' }
+ }
+ 'right-boundary' {
+ $text = $target.SelectSingleNode('one:T', $manager)
+ $text.InnerText = 'X' + $text.InnerText
+ }
+ 'child' {
+ $children = $xml.CreateElement('one', 'OEChildren', $ns)
+ $child = $xml.CreateElement('one', 'OE', $ns)
+ $text = $xml.CreateElement('one', 'T', $ns)
+ $text.InnerText = 'Native child'
+ [void]$child.AppendChild($text)
+ [void]$children.AppendChild($child)
+ [void]$target.AppendChild($children)
+ }
+ 'list' {
+ $list = $xml.CreateElement('one', 'List', $ns)
+ $bullet = $xml.CreateElement('one', 'Bullet', $ns)
+ $bullet.SetAttribute('bullet', '3')
+ $bullet.SetAttribute('fontSize', '11')
+ [void]$list.AppendChild($bullet)
+ [void]$target.PrependChild($list)
+ }
+ 'tag' {
+ $tag = $xml.CreateElement('one', 'Tag', $ns)
+ foreach ($pair in @{ index='0'; completed='false'; disabled='false'; creationDate='2020-01-02T03:04:05.000Z' }.GetEnumerator()) { $tag.SetAttribute($pair.Key, $pair.Value) }
+ [void]$target.PrependChild($tag)
+ }
+ 'format' {
+ $text = $target.SelectSingleNode('one:T', $manager)
+ $plain = if ($case.operation -eq 'join') { 'Right' } else { 'ab' + [char]::ConvertFromUtf32(0x1F980) + 'cd' }
+ $text.InnerText = '' + $plain + ''
+ }
+ 'sibling' { $paragraphs[-1].SelectSingleNode('one:T', $manager).InnerText = 'Native sibling' }
+ 'adoption' { $left.SelectSingleNode('one:T', $manager).InnerText = 'X' }
+ default { throw ('Unknown native control: ' + $case.change) }
+ }
+ $app.UpdatePageContent($xml.OuterXml, [DateTime]::MinValue, 1, $true)
+ }
+ }
+ $app.SyncHierarchy($notebook)
+} finally {
+ if ($notebook) { $app.CloseNotebook($notebook, $false) }
+ [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
+ $app = $null
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+}
diff --git a/corpus/paragraph-edit/reconciliation/scripts/cold.ps1 b/corpus/paragraph-edit/reconciliation/scripts/cold.ps1
new file mode 120000
index 0000000000000000000000000000000000000000..7692cdad6c6ff5292511de8850bf236e8cb3dab7
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/scripts/cold.ps1
@@ -0,0 +1 @@
+../keyboard/scripts/cold.ps1
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/scripts/probe.ps1 b/corpus/paragraph-edit/reconciliation/scripts/probe.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..441f5ff2c9cbd1e1d35abc94f2d4a13ad0982ca0
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/scripts/probe.ps1
@@ -0,0 +1,24 @@
+$app = New-Object -ComObject OneNote.Application
+$notebook = ''
+$app.OpenHierarchy('C:\one-tests\runs\capture\notebook', '', [ref]$notebook, 0)
+$section = ''
+$app.OpenHierarchy('synthetic.one', $notebook, [ref]$section, 0)
+$hierarchy = ''
+$deadline = [DateTime]::UtcNow.AddSeconds(30)
+do {
+ $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
+ [xml]$tree = $hierarchy
+ $pages = @($tree.SelectNodes('//*[local-name()="Page"]'))
+ if ($pages.Count -eq 17) { break }
+ Start-Sleep -Milliseconds 200
+} while ([DateTime]::UtcNow -lt $deadline)
+Write-Output ('pages=' + $pages.Count)
+Write-Output $hierarchy
+foreach ($page in $tree.SelectNodes('//*[local-name()="Page"]')) {
+ $content = ''
+ $app.GetPageContent($page.GetAttribute('ID'), [ref]$content, 0, 1)
+ [xml]$xml = $content
+ $texts = @($xml.SelectNodes('//*[local-name()="Outline"]//*[local-name()="T"]') | ForEach-Object { $_.InnerText })
+ @{ name=$page.GetAttribute('name'); texts=$texts; tags=@($xml.SelectNodes('//*[local-name()="Tag"]')).Count; lists=@($xml.SelectNodes('//*[local-name()="List"]')).Count } | ConvertTo-Json -Compress
+}
+$app.CloseNotebook($notebook, $false)
diff --git a/corpus/paragraph-edit/reconciliation/scripts/read.ps1 b/corpus/paragraph-edit/reconciliation/scripts/read.ps1
new file mode 120000
index 0000000000000000000000000000000000000000..65e900c42b74f25f049c62b00e7c1042301f07cc
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/scripts/read.ps1
@@ -0,0 +1 @@
+../keyboard/scripts/read.ps1
\ No newline at end of file
diff --git a/corpus/paragraph-edit/reconciliation/scripts/remote-remaining.ps1 b/corpus/paragraph-edit/reconciliation/scripts/remote-remaining.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..1c1629d7e7ff570e48670852e2b62410836387bb
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/scripts/remote-remaining.ps1
@@ -0,0 +1,138 @@
+param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost, [switch]$Remote)
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+if (-not $Remote) { & "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost }
+$app = New-Object -ComObject OneNote.Application
+$notebook = ''
+$ns = 'http://schemas.microsoft.com/office/onenote/2010/onenote'
+$tagDefinition = ''
+try {
+ $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebook, 0)
+ $section = ''
+ $app.OpenHierarchy('synthetic.one', $notebook, [ref]$section, 0)
+ if (-not $Remote) {
+ $cases = @(
+ @{ name = 'Split remote prefix'; operation = 'split'; change = 'prefix' },
+ @{ name = 'Split remote boundary'; operation = 'split'; change = 'boundary' },
+ @{ name = 'Split remote child'; operation = 'split'; change = 'child' },
+ @{ name = 'Split remote list'; operation = 'split'; change = 'list' },
+ @{ name = 'Split remote tag'; operation = 'split'; change = 'tag' },
+ @{ name = 'Split remote format'; operation = 'split'; change = 'format' },
+ @{ name = 'Split remote sibling'; operation = 'split'; change = 'sibling' },
+ @{ name = 'Join remote prefix'; operation = 'join'; change = 'prefix' },
+ @{ name = 'Join left boundary'; operation = 'join'; change = 'boundary' },
+ @{ name = 'Join right boundary'; operation = 'join'; change = 'right-boundary' },
+ @{ name = 'Join remote child'; operation = 'join'; change = 'child' },
+ @{ name = 'Join remote list'; operation = 'join'; change = 'list' },
+ @{ name = 'Join remote tag'; operation = 'join'; change = 'tag' },
+ @{ name = 'Join remote format'; operation = 'join'; change = 'format' },
+ @{ name = 'Join remote sibling'; operation = 'join'; change = 'sibling' },
+ @{ name = 'Join empty adoption'; operation = 'join'; change = 'adoption' }
+ )
+ foreach ($case in $cases) {
+ $page = ''
+ $app.CreateNewPage($section, [ref]$page, 0)
+ $left = if ($case.change -eq 'adoption') { '' } else { 'ab' + [char]::ConvertFromUtf32(0x1F980) + 'cd' }
+ $body = '' + $left + ']]>'
+ if ($case.operation -eq 'join') { $body += 'Right]]>' }
+ $body += 'Preserved sibling'
+ $xml = '' + $tagDefinition + '' + $case.name + '' + $body + ''
+ $app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $true)
+ }
+ $cases | ConvertTo-Json -Depth 6 | Set-Content (Join-Path $Root 'cases.json') -Encoding UTF8
+ } else {
+ $cases = Get-Content (Join-Path $Root 'cases.json') -Raw | ConvertFrom-Json
+ $hierarchy = ''
+ $deadline = [DateTime]::UtcNow.AddSeconds(30)
+ do {
+ $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
+ [xml]$tree = $hierarchy
+ $manager = New-Object Xml.XmlNamespaceManager($tree.NameTable)
+ $manager.AddNamespace('one', $ns)
+ if (@($tree.SelectNodes('//one:Page', $manager)).Count -eq $cases.Count + 1) { break }
+ Start-Sleep -Milliseconds 200
+ } while ([DateTime]::UtcNow -lt $deadline)
+ $updates = Join-Path $Root 'remote-updates'
+ [void](New-Item -ItemType Directory -Path $updates -Force)
+ foreach ($case in @($cases)[4..15]) {
+ $page = @($tree.SelectNodes('//one:Page', $manager) | Where-Object { $_.GetAttribute('name') -eq $case.name })
+ if ($page.Count -ne 1) { throw ('Expected one native control page: ' + $case.name) }
+ $content = ''
+ $app.GetPageContent($page[0].GetAttribute('ID'), [ref]$content, 0, 1)
+ [xml]$xml = $content
+ $manager = New-Object Xml.XmlNamespaceManager($xml.NameTable)
+ $manager.AddNamespace('one', $ns)
+ $outline = $xml.SelectSingleNode('/one:Page/one:Outline/one:OEChildren', $manager)
+ $paragraphs = @($outline.SelectNodes('one:OE', $manager))
+ $left = $paragraphs[0]
+ $target = if ($case.operation -eq 'join') { $paragraphs[1] } else { $left }
+ switch ($case.change) {
+ 'prefix' {
+ $text = $left.SelectSingleNode('one:T', $manager)
+ $text.InnerText = 'X' + $text.InnerText
+ if ($case.operation -eq 'join') {
+ $text = $target.SelectSingleNode('one:T', $manager)
+ $text.InnerText += 'Y'
+ }
+ }
+ 'boundary' {
+ $text = $left.SelectSingleNode('one:T', $manager)
+ if ($case.operation -eq 'split') {
+ if (-not $text.InnerText.Contains('ab')) { throw 'The split boundary control lost its text' }
+ $text.InnerText = $text.InnerText.Replace('ab', 'abX')
+ } else { $text.InnerText += 'X' }
+ }
+ 'right-boundary' {
+ $text = $target.SelectSingleNode('one:T', $manager)
+ $text.InnerText = 'X' + $text.InnerText
+ }
+ 'child' {
+ $children = $xml.CreateElement('one', 'OEChildren', $ns)
+ $child = $xml.CreateElement('one', 'OE', $ns)
+ $text = $xml.CreateElement('one', 'T', $ns)
+ $text.InnerText = 'Native child'
+ [void]$child.AppendChild($text)
+ [void]$children.AppendChild($child)
+ [void]$target.AppendChild($children)
+ }
+ 'list' {
+ $list = $xml.CreateElement('one', 'List', $ns)
+ $bullet = $xml.CreateElement('one', 'Bullet', $ns)
+ $bullet.SetAttribute('bullet', '3')
+ $bullet.SetAttribute('fontSize', '11')
+ [void]$list.AppendChild($bullet)
+ [void]$target.PrependChild($list)
+ }
+ 'tag' {
+ if (-not $xml.SelectSingleNode('/one:Page/one:TagDef[@index="0"]', $manager)) {
+ $definition = $xml.CreateDocumentFragment()
+ $definition.InnerXml = $tagDefinition
+ [void]$xml.DocumentElement.PrependChild($definition)
+ }
+ $tag = $xml.CreateElement('one', 'Tag', $ns)
+ foreach ($pair in @{ index='0'; completed='false'; disabled='false'; creationDate='2020-01-02T03:04:05.000Z' }.GetEnumerator()) { $tag.SetAttribute($pair.Key, $pair.Value) }
+ [void]$target.PrependChild($tag)
+ }
+ 'format' {
+ $text = $target.SelectSingleNode('one:T', $manager)
+ $plain = if ($case.operation -eq 'join') { 'Right' } else { 'ab' + [char]::ConvertFromUtf32(0x1F980) + 'cd' }
+ $text.InnerText = '' + $plain + ''
+ }
+ 'sibling' { $paragraphs[-1].SelectSingleNode('one:T', $manager).InnerText = 'Native sibling' }
+ 'adoption' { $left.SelectSingleNode('one:T', $manager).InnerText = 'X' }
+ default { throw ('Unknown native control: ' + $case.change) }
+ }
+ $xml.Save((Join-Path $updates ($case.name + '.xml')))
+ Write-Output ('Updating: ' + $case.name)
+ $app.UpdatePageContent($xml.OuterXml, [DateTime]::MinValue, 1, $true)
+ Write-Output ('Applied: ' + $case.name)
+ }
+ }
+ $app.SyncHierarchy($notebook)
+} finally {
+ if ($notebook) { $app.CloseNotebook($notebook, $false) }
+ [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
+ $app = $null
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+}
diff --git a/corpus/paragraph-edit/reconciliation/scripts/remote.ps1 b/corpus/paragraph-edit/reconciliation/scripts/remote.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..ba1f06b8a9b6aa0e1b2230125fbff9c2625c69e9
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/scripts/remote.ps1
@@ -0,0 +1,128 @@
+param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost, [switch]$Remote)
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+if (-not $Remote) { & "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost }
+$app = New-Object -ComObject OneNote.Application
+$notebook = ''
+$ns = 'http://schemas.microsoft.com/office/onenote/2010/onenote'
+$tagDefinition = ''
+try {
+ $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebook, 0)
+ $section = ''
+ $app.OpenHierarchy('synthetic.one', $notebook, [ref]$section, 0)
+ if (-not $Remote) {
+ $cases = @(
+ @{ name = 'Split remote prefix'; operation = 'split'; change = 'prefix' },
+ @{ name = 'Split remote boundary'; operation = 'split'; change = 'boundary' },
+ @{ name = 'Split remote child'; operation = 'split'; change = 'child' },
+ @{ name = 'Split remote list'; operation = 'split'; change = 'list' },
+ @{ name = 'Split remote tag'; operation = 'split'; change = 'tag' },
+ @{ name = 'Split remote format'; operation = 'split'; change = 'format' },
+ @{ name = 'Split remote sibling'; operation = 'split'; change = 'sibling' },
+ @{ name = 'Join remote prefix'; operation = 'join'; change = 'prefix' },
+ @{ name = 'Join left boundary'; operation = 'join'; change = 'boundary' },
+ @{ name = 'Join right boundary'; operation = 'join'; change = 'right-boundary' },
+ @{ name = 'Join remote child'; operation = 'join'; change = 'child' },
+ @{ name = 'Join remote list'; operation = 'join'; change = 'list' },
+ @{ name = 'Join remote tag'; operation = 'join'; change = 'tag' },
+ @{ name = 'Join remote format'; operation = 'join'; change = 'format' },
+ @{ name = 'Join remote sibling'; operation = 'join'; change = 'sibling' },
+ @{ name = 'Join empty adoption'; operation = 'join'; change = 'adoption' }
+ )
+ foreach ($case in $cases) {
+ $page = ''
+ $app.CreateNewPage($section, [ref]$page, 0)
+ $left = if ($case.change -eq 'adoption') { '' } else { 'ab' + [char]::ConvertFromUtf32(0x1F980) + 'cd' }
+ $body = '' + $left + ']]>'
+ if ($case.operation -eq 'join') { $body += 'Right]]>' }
+ $body += 'Preserved sibling'
+ $xml = '' + $tagDefinition + '' + $case.name + '' + $body + ''
+ $app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $true)
+ }
+ $cases | ConvertTo-Json -Depth 6 | Set-Content (Join-Path $Root 'cases.json') -Encoding UTF8
+ } else {
+ $cases = Get-Content (Join-Path $Root 'cases.json') -Raw | ConvertFrom-Json
+ $hierarchy = ''
+ $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
+ [xml]$tree = $hierarchy
+ $manager = New-Object Xml.XmlNamespaceManager($tree.NameTable)
+ $manager.AddNamespace('one', $ns)
+ foreach ($case in $cases) {
+ $page = @($tree.SelectNodes('//one:Page', $manager) | Where-Object { $_.GetAttribute('name') -eq $case.name })
+ if ($page.Count -ne 1) { throw ('Expected one native control page: ' + $case.name) }
+ $content = ''
+ $app.GetPageContent($page[0].GetAttribute('ID'), [ref]$content, 0, 1)
+ [xml]$xml = $content
+ $manager = New-Object Xml.XmlNamespaceManager($xml.NameTable)
+ $manager.AddNamespace('one', $ns)
+ $outline = $xml.SelectSingleNode('/one:Page/one:Outline/one:OEChildren', $manager)
+ $paragraphs = @($outline.SelectNodes('one:OE', $manager))
+ $left = $paragraphs[0]
+ $target = if ($case.operation -eq 'join') { $paragraphs[1] } else { $left }
+ switch ($case.change) {
+ 'prefix' {
+ $text = $left.SelectSingleNode('one:T', $manager)
+ $text.InnerText = 'X' + $text.InnerText
+ if ($case.operation -eq 'join') {
+ $text = $target.SelectSingleNode('one:T', $manager)
+ $text.InnerText += 'Y'
+ }
+ }
+ 'boundary' {
+ $text = $left.SelectSingleNode('one:T', $manager)
+ if ($case.operation -eq 'split') {
+ if (-not $text.InnerText.Contains('ab')) { throw 'The split boundary control lost its text' }
+ $text.InnerText = $text.InnerText.Replace('ab', 'abX')
+ } else { $text.InnerText += 'X' }
+ }
+ 'right-boundary' {
+ $text = $target.SelectSingleNode('one:T', $manager)
+ $text.InnerText = 'X' + $text.InnerText
+ }
+ 'child' {
+ $children = $xml.CreateElement('one', 'OEChildren', $ns)
+ $child = $xml.CreateElement('one', 'OE', $ns)
+ $text = $xml.CreateElement('one', 'T', $ns)
+ $text.InnerText = 'Native child'
+ [void]$child.AppendChild($text)
+ [void]$children.AppendChild($child)
+ [void]$target.AppendChild($children)
+ }
+ 'list' {
+ $list = $xml.CreateElement('one', 'List', $ns)
+ $bullet = $xml.CreateElement('one', 'Bullet', $ns)
+ $bullet.SetAttribute('bullet', '3')
+ $bullet.SetAttribute('fontSize', '11')
+ [void]$list.AppendChild($bullet)
+ [void]$target.PrependChild($list)
+ }
+ 'tag' {
+ if (-not $xml.SelectSingleNode('/one:Page/one:TagDef[@index="0"]', $manager)) {
+ $definition = $xml.CreateDocumentFragment()
+ $definition.InnerXml = $tagDefinition
+ [void]$xml.DocumentElement.InsertBefore($definition, $xml.SelectSingleNode('/one:Page/one:Title', $manager))
+ }
+ $tag = $xml.CreateElement('one', 'Tag', $ns)
+ foreach ($pair in @{ index='0'; completed='false'; disabled='false'; creationDate='2020-01-02T03:04:05.000Z' }.GetEnumerator()) { $tag.SetAttribute($pair.Key, $pair.Value) }
+ [void]$target.PrependChild($tag)
+ }
+ 'format' {
+ $text = $target.SelectSingleNode('one:T', $manager)
+ $plain = if ($case.operation -eq 'join') { 'Right' } else { 'ab' + [char]::ConvertFromUtf32(0x1F980) + 'cd' }
+ $text.InnerText = '' + $plain + ''
+ }
+ 'sibling' { $paragraphs[-1].SelectSingleNode('one:T', $manager).InnerText = 'Native sibling' }
+ 'adoption' { $left.SelectSingleNode('one:T', $manager).InnerText = 'X' }
+ default { throw ('Unknown native control: ' + $case.change) }
+ }
+ $app.UpdatePageContent($xml.OuterXml, [DateTime]::MinValue, 1, $true)
+ }
+ }
+ $app.SyncHierarchy($notebook)
+} finally {
+ if ($notebook) { $app.CloseNotebook($notebook, $false) }
+ [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
+ $app = $null
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+}
diff --git a/corpus/paragraph-edit/reconciliation/teardown.json b/corpus/paragraph-edit/reconciliation/teardown.json
new file mode 120000
index 0000000000000000000000000000000000000000..8b4fa29c76b0c84be8e3815d693a242cc55fbf90
--- /dev/null
+++ b/corpus/paragraph-edit/reconciliation/teardown.json
@@ -0,0 +1 @@
+../joined/teardown.json
\ No newline at end of file
diff --git a/crates/onestore-offline/tests/sync.rs b/crates/onestore-offline/tests/sync.rs
index 9062a51cdfdae824e39ac4c086ce85f85cf9a24f..72f5f91088e951a2b0fe71ac7b683cbe3619d936 100644
--- a/crates/onestore-offline/tests/sync.rs
+++ b/crates/onestore-offline/tests/sync.rs
@@ -26,6 +26,234 @@ mod paragraph {
(source, sid, left, parent)
}
+ fn native_reconciliation(keyboard: bool) -> (Vec, Vec) {
+ let source = include_bytes!(
+ "../../../corpus/paragraph-edit/reconciliation/before/notebook/synthetic.one"
+ );
+ let native: &[u8] = if keyboard {
+ include_bytes!(
+ "../../../corpus/paragraph-edit/reconciliation/keyboard/notebook/synthetic.one"
+ )
+ } else {
+ include_bytes!(
+ "../../../corpus/paragraph-edit/reconciliation/remote/notebook/synthetic.one"
+ )
+ };
+ let store = Store::parse(source).unwrap();
+ let index = RevisionIndex::parse(&store).unwrap();
+ let document = Document::parse(&index).unwrap();
+ let mut server = Server::new(native);
+ let mut recorded = Vec::new();
+ let mut counts = [0; 3];
+ for (sid, page) in document.pages().unwrap() {
+ let space = &document.spaces[&sid];
+ let view = &space.revisions[&space.contexts[&ExGuid::default()]];
+ let Kind::Metadata {
+ title: Some(name), ..
+ } = &view.nodes[&view.roots[&2]].kind
+ else {
+ continue;
+ };
+ if !name.starts_with("Split ") && !name.starts_with("Join ") {
+ continue;
+ }
+ let outline = view.nodes[&page]
+ .children
+ .iter()
+ .find(|id| matches!(view.nodes[id].kind, Kind::Outline { .. }))
+ .unwrap();
+ let children = &view.nodes[outline].children;
+ let left = view.nodes[&children[0]].content[0];
+ let split = name.starts_with("Split ");
+ let adoption = name == "Join empty adoption";
+ let directory = tempfile::tempdir().unwrap();
+ let path = directory.path().join("native-reconciliation.sqlite");
+ let mut cache = Replica::create(&path, source).unwrap();
+ let (id, target, intent) = if split {
+ let intent = ParagraphSplit::new(left, 2, "Offline author").unwrap();
+ (
+ cache.split(source, sid, &intent).unwrap().unwrap(),
+ intent.text_object(),
+ serde_json::to_value(intent).unwrap(),
+ )
+ } else {
+ let right = view.nodes[&children[1]].content[0];
+ let intent = ParagraphJoin::new(left, right, "Offline author").unwrap();
+ (
+ cache.join(source, sid, &intent).unwrap().unwrap(),
+ if adoption { right } else { left },
+ serde_json::to_value(intent).unwrap(),
+ )
+ };
+ drop(cache);
+ cache = Replica::open(&path).unwrap();
+ let at = if split {
+ 2
+ } else if adoption {
+ 1
+ } else {
+ 4
+ };
+ let dependent = cache
+ .edit_text(
+ &cache.snapshot().unwrap(),
+ sid,
+ target,
+ at..at + 1,
+ if adoption { "I" } else { "C" },
+ )
+ .unwrap()
+ .unwrap();
+ let local = cache.snapshot().unwrap();
+ let pending = cache.pending().unwrap();
+ drop(cache);
+ cache = Replica::open(&path).unwrap();
+ assert_eq!(cache.snapshot().unwrap(), local);
+ assert_eq!(cache.pending().unwrap(), pending);
+ let previous = server.publications;
+ let remote = server.visible.clone();
+ let (actual, status) = cache.sync_once(&mut server).unwrap().unwrap();
+ assert_eq!(actual, id);
+ assert_eq!(cache.snapshot().unwrap(), local);
+ if !keyboard {
+ counts[2] += 1;
+ assert_eq!(
+ status,
+ EditStatus::Conflict(ConflictKind::TargetUnavailable),
+ "{name}"
+ );
+ assert_eq!(server.publications, previous);
+ assert_eq!(server.visible, remote);
+ drop(cache);
+ cache = Replica::open(&path).unwrap();
+ assert_eq!(cache.status(id).unwrap(), Some(status));
+ assert_eq!(cache.snapshot().unwrap(), local);
+ assert_eq!(cache.pending().unwrap(), pending);
+ continue;
+ }
+ let automatic = ["prefix", "format", "sibling"]
+ .iter()
+ .any(|suffix| name.ends_with(suffix));
+ if automatic {
+ counts[0] += 1;
+ assert!(
+ matches!(status, EditStatus::Published { .. }),
+ "{name}: {status:?}"
+ );
+ } else {
+ let expected = if name.ends_with("boundary") || adoption {
+ ConflictKind::TextChanged
+ } else {
+ ConflictKind::StructureChanged
+ };
+ assert_eq!(status, EditStatus::Conflict(expected), "{name}");
+ assert_eq!(server.publications, previous);
+ assert_eq!(server.visible, remote);
+ assert_eq!(cache.pending().unwrap(), pending);
+ drop(cache);
+ cache = Replica::open(&path).unwrap();
+ assert_eq!(cache.status(id).unwrap(), Some(status));
+ assert_eq!(cache.snapshot().unwrap(), local);
+ assert_eq!(cache.pending().unwrap(), pending);
+ if adoption || name == "Join remote list" {
+ counts[2] += 1;
+ assert!(cache.rebase_join_conflict(id, &local, &remote).is_err());
+ assert_eq!(cache.pending().unwrap(), pending);
+ assert_eq!(cache.snapshot().unwrap(), local);
+ assert_eq!(cache.status(id).unwrap(), Some(status));
+ recorded.push(serde_json::json!({"case": name, "space": sid, "intent": intent, "outcome": "retained_conflict"}));
+ continue;
+ }
+ counts[1] += 1;
+ if split {
+ let offset = if name.ends_with("boundary") { 3 } else { 2 };
+ cache
+ .rebase_conflict(id, &local, &remote, offset..offset)
+ .unwrap();
+ let onestore_offline::Operation::Split(edit) =
+ &cache.pending().unwrap()[0].operation
+ else {
+ panic!()
+ };
+ assert_eq!(
+ edit.intent,
+ serde_json::from_value::(intent.clone())
+ .unwrap()
+ .reposition(offset)
+ );
+ } else {
+ cache.rebase_join_conflict(id, &local, &remote).unwrap();
+ }
+ drop(cache);
+ cache = Replica::open(&path).unwrap();
+ let (actual, status) = cache.sync_once(&mut server).unwrap().unwrap();
+ assert_eq!(actual, id);
+ assert!(
+ matches!(status, EditStatus::Published { .. }),
+ "{name}: {status:?}"
+ );
+ }
+ drop(cache);
+ cache = Replica::open(&path).unwrap();
+ let first_receipt = cache.status(id).unwrap().unwrap();
+ let (actual, status) = cache.sync_once(&mut server).unwrap().unwrap();
+ assert_eq!(actual, dependent);
+ assert!(
+ matches!(status, EditStatus::Published { .. }),
+ "{name} dependent: {status:?}"
+ );
+ assert_eq!(server.publications, previous + 2);
+ drop(cache);
+ cache = Replica::open(&path).unwrap();
+ assert_eq!(cache.status(id).unwrap(), Some(first_receipt));
+ assert_eq!(cache.status(dependent).unwrap(), Some(status));
+ assert!(cache.pending().unwrap().is_empty());
+ assert_eq!(cache.snapshot().unwrap(), server.durable);
+ let EditStatus::Published { revision } = first_receipt else {
+ panic!()
+ };
+ let EditStatus::Published {
+ revision: dependent_revision,
+ } = status
+ else {
+ panic!()
+ };
+ recorded.push(serde_json::json!({"case": name, "space": sid, "intent": intent,
+ "outcome": if automatic { "automatic" } else { "reviewed" }, "receipt": revision, "dependent_receipt": dependent_revision}));
+ }
+ assert_eq!(counts, if keyboard { [6, 8, 2] } else { [0, 0, 16] });
+ assert_eq!(server.publications, if keyboard { 28 } else { 0 });
+ (server.durable, recorded)
+ }
+
+ #[test]
+ fn native_com_replacement_preserves_every_local_paragraph_branch() {
+ native_reconciliation(false);
+ }
+
+ #[test]
+ fn native_changes_commute_or_retain_reviewable_paragraph_dependencies() {
+ native_reconciliation(true);
+ }
+
+ #[test]
+ #[ignore = "exports native-controlled reconciliation for cold OneNote validation"]
+ fn export_native_reconciliation() {
+ let output = std::path::PathBuf::from(
+ std::env::var_os("ONESTORE_NATIVE_RECONCILIATION_OUTPUT").unwrap(),
+ );
+ assert!(output.is_absolute());
+ std::fs::create_dir(&output).unwrap();
+ let (bytes, cases) = native_reconciliation(true);
+ std::fs::create_dir(output.join("candidate")).unwrap();
+ std::fs::write(output.join("candidate/synthetic.one"), bytes).unwrap();
+ std::fs::write(
+ output.join("manifest.json"),
+ serde_json::to_vec_pretty(&cases).unwrap(),
+ )
+ .unwrap();
+ }
+
#[test]
fn native_splits_retain_independent_local_identities_and_changed_boundaries() {
let source = include_bytes!("../../../corpus/paragraph-edit/before/notebook/synthetic.one");
diff --git a/tools/native/paragraph-reconciliation.ps1 b/tools/native/paragraph-reconciliation.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..f6053c4d4652024a212e43037601a5acfbf745a6
--- /dev/null
+++ b/tools/native/paragraph-reconciliation.ps1
@@ -0,0 +1,137 @@
+param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost, [switch]$Remote)
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+if (-not $Remote) { & "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost }
+$app = New-Object -ComObject OneNote.Application
+$notebook = ''
+$ns = 'http://schemas.microsoft.com/office/onenote/2010/onenote'
+try {
+ $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebook, 0)
+ $section = ''
+ $app.OpenHierarchy('synthetic.one', $notebook, [ref]$section, 0)
+ if (-not $Remote) {
+ $cases = @(
+ @{ name = 'Split remote prefix'; operation = 'split'; change = 'prefix' },
+ @{ name = 'Split remote boundary'; operation = 'split'; change = 'boundary' },
+ @{ name = 'Split remote child'; operation = 'split'; change = 'child' },
+ @{ name = 'Split remote list'; operation = 'split'; change = 'list' },
+ @{ name = 'Split remote tag'; operation = 'split'; change = 'tag' },
+ @{ name = 'Split remote format'; operation = 'split'; change = 'format' },
+ @{ name = 'Split remote sibling'; operation = 'split'; change = 'sibling' },
+ @{ name = 'Join remote prefix'; operation = 'join'; change = 'prefix' },
+ @{ name = 'Join left boundary'; operation = 'join'; change = 'boundary' },
+ @{ name = 'Join right boundary'; operation = 'join'; change = 'right-boundary' },
+ @{ name = 'Join remote child'; operation = 'join'; change = 'child' },
+ @{ name = 'Join remote list'; operation = 'join'; change = 'list' },
+ @{ name = 'Join remote tag'; operation = 'join'; change = 'tag' },
+ @{ name = 'Join remote format'; operation = 'join'; change = 'format' },
+ @{ name = 'Join remote sibling'; operation = 'join'; change = 'sibling' },
+ @{ name = 'Join empty adoption'; operation = 'join'; change = 'adoption' }
+ )
+ foreach ($case in $cases) {
+ $page = ''
+ $app.CreateNewPage($section, [ref]$page, 0)
+ $left = if ($case.change -eq 'adoption') { '' } else { 'ab' + [char]::ConvertFromUtf32(0x1F980) + 'cd' }
+ $body = '' + $left + ']]>'
+ if ($case.operation -eq 'join') { $body += 'Right]]>' }
+ $body += 'Preserved sibling'
+ $xml = '' + $case.name + '' + $body + ''
+ $app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $true)
+ }
+ $cases | ConvertTo-Json -Depth 6 | Set-Content (Join-Path $Root 'cases.json') -Encoding UTF8
+ } else {
+ $cases = Get-Content (Join-Path $Root 'cases.json') -Raw | ConvertFrom-Json
+ $hierarchy = ''
+ $deadline = [DateTime]::UtcNow.AddSeconds(30)
+ do {
+ $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
+ [xml]$tree = $hierarchy
+ $manager = New-Object Xml.XmlNamespaceManager($tree.NameTable)
+ $manager.AddNamespace('one', $ns)
+ if (@($tree.SelectNodes('//one:Page', $manager)).Count -eq $cases.Count + 1) { break }
+ Start-Sleep -Milliseconds 200
+ } while ([DateTime]::UtcNow -lt $deadline)
+ $updates = Join-Path $Root 'remote-updates'
+ [void](New-Item -ItemType Directory -Path $updates -Force)
+ foreach ($case in $cases) {
+ $page = @($tree.SelectNodes('//one:Page', $manager) | Where-Object { $_.GetAttribute('name') -eq $case.name })
+ if ($page.Count -ne 1) { throw ('Expected one native control page: ' + $case.name) }
+ $content = ''
+ $app.GetPageContent($page[0].GetAttribute('ID'), [ref]$content, 0, 1)
+ [xml]$xml = $content
+ $manager = New-Object Xml.XmlNamespaceManager($xml.NameTable)
+ $manager.AddNamespace('one', $ns)
+ $outline = $xml.SelectSingleNode('/one:Page/one:Outline/one:OEChildren', $manager)
+ $paragraphs = @($outline.SelectNodes('one:OE', $manager))
+ $left = $paragraphs[0]
+ $target = if ($case.operation -eq 'join') { $paragraphs[1] } else { $left }
+ switch ($case.change) {
+ 'prefix' {
+ $text = $left.SelectSingleNode('one:T', $manager)
+ $text.InnerText = 'X' + $text.InnerText
+ if ($case.operation -eq 'join') {
+ $text = $target.SelectSingleNode('one:T', $manager)
+ $text.InnerText += 'Y'
+ }
+ }
+ 'boundary' {
+ $text = $left.SelectSingleNode('one:T', $manager)
+ if ($case.operation -eq 'split') {
+ if (-not $text.InnerText.Contains('ab')) { throw 'The split boundary control lost its text' }
+ $text.InnerText = $text.InnerText.Replace('ab', 'abX')
+ } else { $text.InnerText += 'X' }
+ }
+ 'right-boundary' {
+ $text = $target.SelectSingleNode('one:T', $manager)
+ $text.InnerText = 'X' + $text.InnerText
+ }
+ 'child' {
+ $children = $xml.CreateElement('one', 'OEChildren', $ns)
+ $child = $xml.CreateElement('one', 'OE', $ns)
+ $text = $xml.CreateElement('one', 'T', $ns)
+ $text.InnerText = 'Native child'
+ [void]$child.AppendChild($text)
+ [void]$children.AppendChild($child)
+ [void]$target.AppendChild($children)
+ }
+ 'list' {
+ $list = $xml.CreateElement('one', 'List', $ns)
+ $bullet = $xml.CreateElement('one', 'Bullet', $ns)
+ $bullet.SetAttribute('bullet', '3')
+ $bullet.SetAttribute('fontSize', '11')
+ [void]$list.AppendChild($bullet)
+ [void]$target.PrependChild($list)
+ }
+ 'tag' {
+ if (-not $xml.SelectSingleNode('/one:Page/one:TagDef[@index="0"]', $manager)) {
+ $definition = $xml.CreateDocumentFragment()
+ $definition.InnerXml = ''
+ [void]$xml.DocumentElement.PrependChild($definition)
+ }
+ $tag = $xml.CreateElement('one', 'Tag', $ns)
+ foreach ($pair in @{ index='0'; completed='false'; disabled='false'; creationDate='2020-01-02T03:04:05.000Z' }.GetEnumerator()) { $tag.SetAttribute($pair.Key, $pair.Value) }
+ [void]$target.PrependChild($tag)
+ }
+ 'format' {
+ $text = $target.SelectSingleNode('one:T', $manager)
+ $plain = if ($case.operation -eq 'join') { 'Right' } else { 'ab' + [char]::ConvertFromUtf32(0x1F980) + 'cd' }
+ $text.InnerText = '' + $plain + ''
+ }
+ 'sibling' { $paragraphs[-1].SelectSingleNode('one:T', $manager).InnerText = 'Native sibling' }
+ 'adoption' { $left.SelectSingleNode('one:T', $manager).InnerText = 'X' }
+ default { throw ('Unknown native control: ' + $case.change) }
+ }
+ $xml.Save((Join-Path $updates ($case.name + '.xml')))
+ Write-Output ('Updating: ' + $case.name)
+ $app.UpdatePageContent($xml.OuterXml, [DateTime]::MinValue, 1, $true)
+ Write-Output ('Applied: ' + $case.name)
+ }
+ }
+ $app.SyncHierarchy($notebook)
+} finally {
+ if ($notebook) { $app.CloseNotebook($notebook, $false) }
+ [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
+ $app = $null
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+}
diff --git a/tools/native_paragraph_reconciliation.py b/tools/native_paragraph_reconciliation.py
new file mode 100644
index 0000000000000000000000000000000000000000..6b2569661f8e4b2316e4bdbe0a1ddd8714b4ba90
--- /dev/null
+++ b/tools/native_paragraph_reconciliation.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+"""Apply keyboard controls during a native paragraph-fixture inspection."""
+import argparse
+import base64
+import json
+from pathlib import Path
+import re
+import xml.etree.ElementTree as ET
+
+from native_runner import windows
+from native_xml import ns
+
+
+def apply(output):
+ target = json.loads((output / 'machine.json').read_text())['name']
+ pages = {page.get('name'): page for path in (output / 'read').glob('page-*.xml')
+ for page in [ET.parse(path).getroot()]}
+ cases = json.loads((Path(__file__).resolve().parent.parent /
+ 'corpus/paragraph-edit/reconciliation/cases.json').read_text(encoding='utf-8-sig'))
+ scripts = output / 'keyboard'
+ scripts.mkdir()
+ for case in cases:
+ page = pages[case['name']]
+ paragraphs = page.findall('one:Outline/one:OEChildren/one:OE', ns)
+ join = case['operation'] == 'join'
+ change = case['change']
+ at = 1 if join and change in ('child', 'list', 'tag', 'format', 'right-boundary') else 0
+ keys = {
+ 'prefix': '{Left}X',
+ 'boundary': '{Right}X' if join else '{Left}{Right 2}X',
+ 'right-boundary': '{Left}X',
+ 'child': '{Right}{Enter}{Tab}Native child',
+ 'list': '{Left}^.',
+ 'tag': '{Left}^1',
+ 'format': '^i',
+ 'sibling': 'Native sibling',
+ 'adoption': '{Left}X',
+ }[change]
+ if change == 'sibling':
+ at = len(paragraphs) - 1
+ steps = [(paragraphs[at].get('objectID'), keys)]
+ if join and change == 'prefix':
+ steps.append((paragraphs[1].get('objectID'), '{Right}Y'))
+ script = ('#Requires AutoHotkey v2.0\n'
+ 'OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))\n'
+ 'app := ComObject("OneNote.Application")\n')
+ for oid, keys in steps:
+ for identifier in (page.get('ID'), oid):
+ if not re.fullmatch(r'\{[0-9A-Fa-f-]+\}\{[0-9]+\}\{[0-9A-Fa-f]+\}', identifier):
+ raise ValueError('Invalid native fixture identity')
+ script += (f'app.NavigateTo("{page.get("ID")}", "{oid}", false)\n'
+ 'hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)\n'
+ 'if !hwnd\n throw Error("OneNote window did not appear")\n'
+ 'WinMaximize(hwnd)\nWinActivate(hwnd)\n'
+ 'if !WinWaitActive(hwnd,, 10)\n throw Error("OneNote did not become active")\n'
+ f'Sleep 300\nSend "{keys}"\nSleep 300\n')
+ stem = scripts / case['name'].lower().replace(' ', '-')
+ stem.with_suffix('.ahk').write_text(script)
+ result = windows.do_exec(script, target=target, shot_delay_ms=500)
+ stem.with_suffix('.json').write_text(json.dumps({k: v for k, v in result.items() if k != 'png_b64'}, indent=2))
+ if result.get('png_b64'):
+ stem.with_suffix('.png').write_bytes(base64.b64decode(result['png_b64']))
+ if result.get('error') or result.get('exit') != 0 or not result.get('png_b64'):
+ raise RuntimeError(windows.text_result(result))
+ print(case['name'], flush=True)
+ (output / 'finish').touch()
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('output', type=Path)
+ apply(parser.parse_args().output)
diff --git a/tools/test_paragraph_edit.py b/tools/test_paragraph_edit.py
index 62bfe9cd08b5214d27602dd37f5b6e0f5a2cf497..a2530729f3cfd50aed366d457775afc2511e60d0 100644
--- a/tools/test_paragraph_edit.py
+++ b/tools/test_paragraph_edit.py
@@ -10,6 +10,7 @@ import xml.etree.ElementTree as ET
from document_model import EXPORTER, ordered_pages, walk
from native_format import compare_formats, native_characters
from native_xml import ns
+from offline_document_history import identity
ROOT = Path(__file__).resolve().parent.parent
FIXTURE = ROOT / 'corpus/paragraph-edit'
@@ -17,6 +18,156 @@ compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare']
class ParagraphEditTest(unittest.TestCase):
+ def test_reconciled_native_edits_keep_remote_content_and_exact_graphs(self):
+ fixture = FIXTURE / 'reconciliation'
+ result = fixture / 'reconciled'
+ self.assert_cold_graph(result)
+ manifest = json.loads((result / 'manifest.json').read_text())
+ self.assertEqual(len(manifest), 16)
+ self.assertEqual({case['case'] for case in manifest if case['outcome'] == 'retained_conflict'},
+ {'Join remote list', 'Join empty adoption'})
+ receipts = [case[key] for case in manifest for key in ('receipt', 'dependent_receipt') if key in case]
+ self.assertEqual(len(set(receipts)), 28)
+ models, captures = {}, {}
+ with TemporaryDirectory() as temporary:
+ for phase, notebook, read in [('remote', fixture / 'keyboard/notebook', fixture / 'keyboard/read'),
+ ('reconciled', result / 'candidate', result / 'native/read')]:
+ folder = Path(temporary) / phase
+ subprocess.run([EXPORTER, notebook / 'synthetic.one', folder], check=True)
+ model = json.loads((folder / 'document.json').read_text())
+ models[phase] = {r['nodes'][r['roots']['2']]['kind']['title']: (r, page)
+ for _, _, r, page in ordered_pages(model)}
+ captures[phase] = {page.get('name'): native_characters(page, page.findall('one:Outline', ns))
+ for path in read.glob('page-*.xml') for page in [ET.parse(path).getroot()]}
+ for case in manifest:
+ name, intent = case['case'], case['intent']
+ old, page = models['remote'][name]
+ new, new_page = models['reconciled'][name]
+ self.assertEqual(new_page, page)
+ outline, = [oid for oid in old['nodes'][page]['children'] if old['nodes'][oid]['kind']['type'] == 'Outline']
+ graph = {oid: [list(node['children']), list(node['content']), node['child_level']]
+ for oid, node in walk(old, outline)}
+ characters = [list(paragraph) for paragraph in captures['remote'][name]]
+ if case['outcome'] != 'retained_conflict':
+ if name.startswith('Split '):
+ text = intent['text']
+ paragraph, = [oid for oid, (_, content, _) in graph.items() if content == [text]]
+ parent, = [oid for oid, (children, _, _) in graph.items() if paragraph in children]
+ offset = 3 if name.endswith(('prefix', 'boundary')) else 2
+ characters[:1] = [characters[0][:offset], characters[0][offset:]]
+ characters[1] = [('C' if char == 'c' else char, style) for char, style in characters[1]]
+ right, right_text = identity(intent, 1), identity(intent, 2)
+ self.assertNotIn(right, graph)
+ self.assertNotIn(right_text, graph)
+ graph[parent][0].insert(graph[parent][0].index(paragraph) + 1, right)
+ graph[right] = [graph[paragraph][0], [right_text], graph[paragraph][2]]
+ graph[right_text] = [[], [], None]
+ graph[paragraph][0] = []
+ self.assertEqual(new['nodes'][right_text]['tags'], [])
+ else:
+ left, right = intent['left'], intent['right']
+ a, = [oid for oid, (_, content, _) in graph.items() if content == [left]]
+ b, = [oid for oid, (_, content, _) in graph.items() if content == [right]]
+ parent, = [oid for oid, (children, _, _) in graph.items() if a in children]
+ graph[parent][0].remove(b)
+ graph[a][0] += graph[b][0]
+ del graph[b], graph[right]
+ characters[:2] = [characters[0] + characters[1]]
+ characters[0] = [('C' if char == 'c' else char, style) for char, style in characters[0]]
+ self.assertEqual(new['nodes'][left]['tags'], old['nodes'][left]['tags'])
+ with self.subTest(case=name):
+ self.assertEqual({oid: [node['children'], node['content'], node['child_level']]
+ for oid, node in walk(new, outline)}, graph)
+ for expected, actual in zip(characters, captures['reconciled'][name], strict=True):
+ for (a, before), (b, after) in zip(expected, actual, strict=True):
+ self.assertEqual(a, b)
+ for key in before.keys() | after.keys():
+ default = 'automatic' if key in ('color', 'highlight') else False
+ self.assertEqual(before.get(key, default), after.get(key, default))
+
+ def test_native_reconciliation_controls_preserve_the_intended_edit_scope(self):
+ fixture = FIXTURE / 'reconciliation'
+ cases = json.loads((fixture / 'cases.json').read_text(encoding='utf-8-sig'))
+ self.assertEqual(len(cases), 16)
+ models, captures = {}, {}
+ with TemporaryDirectory() as temporary:
+ for phase in ('before', 'remote', 'keyboard'):
+ folder = Path(temporary) / phase
+ shutil.copytree(fixture / phase / 'read', folder / 'read')
+ compare(fixture / phase / 'notebook', folder / 'read')
+ subprocess.run([EXPORTER, fixture / phase / 'notebook/synthetic.one', folder / 'model'], check=True)
+ model = json.loads((folder / 'model/document.json').read_text())
+ models[phase] = {r['nodes'][r['roots']['2']]['kind']['title']: (r, page)
+ for _, _, r, page in ordered_pages(model)}
+ captures[phase] = {page.get('name'): native_characters(page, page.findall('one:Outline', ns))
+ for path in (folder / 'read').glob('page-*.xml') for page in [ET.parse(path).getroot()]}
+ self.assertEqual(len(models[phase]), 17)
+ for case in cases:
+ name, change = case['name'], case['change']
+ join = case['operation'] == 'join'
+ old, page = models['before'][name]
+ outline, = [oid for oid in old['nodes'][page]['children'] if old['nodes'][oid]['kind']['type'] == 'Outline']
+ paragraphs = old['nodes'][outline]['children']
+ target = paragraphs[1 if join and change in ('child', 'list', 'tag', 'format', 'right-boundary') else 0]
+ expected = ['' if change == 'adoption' else 'ab🦀cd'] + (['Right'] if join else []) + ['Preserved sibling']
+ self.assertEqual([old['nodes'][old['nodes'][oid]['content'][0]]['kind']['text'] for oid in paragraphs], expected)
+ if change == 'prefix':
+ expected[0] = 'X' + expected[0]
+ if join: expected[1] += 'Y'
+ elif change == 'boundary':
+ expected[0] = expected[0] + 'X' if join else 'abX🦀cd'
+ elif change == 'right-boundary': expected[1] = 'XRight'
+ elif change == 'sibling': expected[-1] = 'Native sibling'
+ elif change == 'adoption': expected[0] = 'X'
+ for phase in ('remote', 'keyboard'):
+ with self.subTest(case=name, phase=phase):
+ current, current_page = models[phase][name]
+ if change == 'format':
+ for char, style in captures['before'][name][int(join)]:
+ self.assertTrue(style['bold'])
+ self.assertFalse(style.get('italic', False))
+ for char, style in captures[phase][name][int(join)]:
+ self.assertTrue(style['italic'])
+ self.assertEqual(style.get('bold', False), phase == 'keyboard')
+ self.assertEqual(current_page, page)
+ active = [oid for oid, node in walk(current, outline) if node['kind']['type'] == 'Paragraph']
+ expected_ids = list(paragraphs)
+ if phase == 'keyboard' and change == 'sibling':
+ self.assertNotEqual(active[-1], paragraphs[-1])
+ expected_ids[-1] = active[-1]
+ if change == 'child':
+ child, = current['nodes'][target]['children']
+ expected_ids.insert(expected_ids.index(target) + 1, child)
+ self.assertEqual(active, expected_ids)
+ actual_texts = [current['nodes'][current['nodes'][oid]['content'][0]]['kind']['text'] for oid in active]
+ expected_texts = list(expected)
+ if change == 'child': expected_texts.insert(2 if join else 1, 'Native child')
+ self.assertEqual(actual_texts, expected_texts)
+ top = list(paragraphs)
+ if phase == 'keyboard' and change == 'sibling': top[-1] = active[-1]
+ if phase == 'keyboard' and change == 'list':
+ if join:
+ top.remove(target)
+ self.assertEqual(current['nodes'][paragraphs[0]]['children'], [target])
+ else:
+ group = current['nodes'][outline]['children'][0]
+ self.assertEqual(current['nodes'][group]['kind']['type'], 'OutlineGroup')
+ self.assertEqual(current['nodes'][group]['children'], [target])
+ self.assertEqual(current['nodes'][group]['content'], [])
+ top[0] = group
+ self.assertEqual(current['nodes'][outline]['children'], top)
+ for oid in active:
+ node = current['nodes'][oid]
+ text, = node['content']
+ if oid in paragraphs:
+ if phase == 'keyboard': self.assertEqual(node['content'], old['nodes'][oid]['content'])
+ else: self.assertNotEqual(node['content'], old['nodes'][oid]['content'])
+ child_count = int(oid == target and change == 'child')
+ if phase == 'keyboard' and join and change == 'list' and oid == paragraphs[0]: child_count = 1
+ self.assertEqual(len(node['children']), child_count)
+ self.assertEqual(len(node['kind']['lists']), int(oid == target and change == 'list'))
+ self.assertEqual(len(current['nodes'][text]['tags']), int(oid == target and change == 'tag'))
+
def assert_cold_graph(self, fixture):
with TemporaryDirectory() as temporary:
models = []