diff --git a/corpus/page-lifecycle/page-edits/01-demoted-parent/candidate/Lifecycle.one b/corpus/page-lifecycle/page-edits/01-demoted-parent/candidate/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..02ee922ef82a1e6c0faa8e91ec8f06a67b71b2e1
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/01-demoted-parent/candidate/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/notebook/Lifecycle.one b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..7c0136ba9dd4543d45ae43636341bf44224330fa
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..a30e67158f9342a6b4d55238e26726df119e6dea
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/environment.json b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..f878668d31698718d5927d4a0291b58f4506d6f0
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-CE1D1D57",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/hierarchy.xml b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a7c033c7173eaec18f6a2091c6311591be782f02
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-000.xml b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3871f0b676295f61cbf5ee2130deb7de1a0749b9
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-000.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-001.xml b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6cf6b396ac6cf0e48ce23ba5a557eb19bd5c9905
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-001.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-002.xml b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8bd4fb48b216d6a92f92c9da72e5704af8c1b19b
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-002.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-003.xml b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e15674eda089e5fa9ad09c5b30c9d5aa7f70e2bf
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-004.xml b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a15346be5c8f31927ea5a2c96d45bf66bc57b3f1
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-004.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-005.xml b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2443a6d09daf728020b207590ef1087918aaf87f
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-005.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-006.xml b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ffb217883380c09a4c1687072d58de0a7c536e6c
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-007.xml b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4354ac0f3f46bab9ae8e4daa99991d4f40c4116d
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-008.xml b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b0aca60858539a36a570843a88b8c479592a62a4
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/page-008.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/payloads.json b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/payloads.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/01-demoted-parent/cold/read/payloads.json
@@ -0,0 +1,3 @@
+[
+
+]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/02-promoted-parent/candidate/Lifecycle.one b/corpus/page-lifecycle/page-edits/02-promoted-parent/candidate/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..4a43e79aa050a2badcfbab6b0ac93add898fee44
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/02-promoted-parent/candidate/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/notebook/Lifecycle.one b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..214289987dc06d39d2354d4192e54e4157c2f298
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..4939a05c8c788dad8828c447a8bd28ebfe9ddbcb
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/environment.json b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..f79462e576809f7bc5ccbbe0dfa224178b969229
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-C1201E9E",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/hierarchy.xml b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..31a7f72dd4c15dfb067546e1eb5421ce286c1248
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-000.xml b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0c588c6ce3fd48c495ed65fe8cd65263d7cb739e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-000.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-001.xml b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..dba4e5a944a171910152ce14238e90d5d769645c
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-001.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-002.xml b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d35e5e2010beb689a222181f54ff89ae89c308fb
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-002.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-003.xml b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2b82fba99610b9006b1607e4f3dcfa9af82331ff
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-003.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-004.xml b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4f8546c23bd97bbef087bf934a10d86b3b887253
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-004.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-005.xml b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f83c45fd825e2aaee44fefd3a14e161ecbb5da27
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-005.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-006.xml b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..34efdeeafe68eef92bdd8cec8dfccaf93d08ca66
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-007.xml b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..dde7e753ee91f8c660bdfb04fc69d8c3f3c57772
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-008.xml b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0fad9bca366af7c736dff366f9ebb2ae6040ce88
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/page-008.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/payloads.json b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3e9efde26e8b7e90a6e6e5fb61fe1be52686aa5e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/02-promoted-parent/cold/read/payloads.json
@@ -0,0 +1 @@
+../../../01-demoted-parent/cold/read/payloads.json
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/03-selected-group-move/candidate/Lifecycle.one b/corpus/page-lifecycle/page-edits/03-selected-group-move/candidate/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..b8eb4941f02694b4ed5f4d51473b1708c7bf3569
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/03-selected-group-move/candidate/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/notebook/Lifecycle.one b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..4243d2c7251823143db64ece1cd272779121db56
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..7a54e3966efa00b68d341ca0970e989d463ca203
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/environment.json b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..39d9d0eba166ae6eaf06f9e5a3c4b2537e3ffd75
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-20062E97",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/hierarchy.xml b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4d0614258475aa7745df5755508a8f9beed15055
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-000.xml b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f595a9eb95566d3ed9f2907c1e4bcce06b67a51a
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-000.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-001.xml b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7fd3e79a64cea8a6d80924ae65b266ced9704260
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-001.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-002.xml b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..decf7784bdb942df6f14cd5a0f066e8ea94017b1
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-002.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-003.xml b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c44299e64f400a3f00cb94e5609a917b9530d3e7
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-003.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-004.xml b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7df893a24846fb19bb687d5fa02d4e2599db31ec
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-004.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-005.xml b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bb90cb026518957910896b4b985c5d3878eea77a
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-005.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-006.xml b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b3ac20982d3f6eb3ea15aba2eea192b3b67617ae
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-007.xml b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..371f80042cd747451a7f6e20b5332f94b9035850
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-008.xml b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6d7e33489c75ade041ade240cbf45df396607bdf
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/page-008.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/payloads.json b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3e9efde26e8b7e90a6e6e5fb61fe1be52686aa5e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/03-selected-group-move/cold/read/payloads.json
@@ -0,0 +1 @@
+../../../01-demoted-parent/cold/read/payloads.json
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/04-subpage-move/candidate/Lifecycle.one b/corpus/page-lifecycle/page-edits/04-subpage-move/candidate/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..e7d284798163e42c0994cd802375c2e3199132ad
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/04-subpage-move/candidate/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/04-subpage-move/cold/notebook/Lifecycle.one b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..a0f1273bec9b871472b2cefa3563154b93d99d4f
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/04-subpage-move/cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..b8b9d9235a126e2f39e23a96176cb65cb501c9c3
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/environment.json b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..1d6685bc8ba36a55b0e5e1afc7b62e1350f383b9
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-6019E29D",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/hierarchy.xml b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7449bd610035caba8d16b7b5337d557d5bf60283
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-000.xml b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5920d7c3c252fd47a5c62d25858f3e576efa9a29
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-000.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-001.xml b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..23015bf65f9bebaa2f14cb0b6762444c548ea4b6
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-001.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-002.xml b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7495b5eb6b90469f0eb828daf5932a2ca66242e5
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-002.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-003.xml b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c2c135f9d0eb50b891f14149c77a1ebdc1fd0843
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-003.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-004.xml b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7a542c2b1ee029db29a9cfa19d292ac82b28fdd7
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-004.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-005.xml b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..32bb859726c73c4f2794f335571441a2c2a5348e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-005.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-006.xml b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3d0c8980f38b5ef9dbab9a40a1c840cfd3e62348
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-007.xml b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3155c374da1c00b5f2f2ac9bf8da4e9eac13ccaa
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-008.xml b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f8dfefcd48f50e646f099da7894c29c430f12136
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/page-008.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/payloads.json b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3e9efde26e8b7e90a6e6e5fb61fe1be52686aa5e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/04-subpage-move/cold/read/payloads.json
@@ -0,0 +1 @@
+../../../01-demoted-parent/cold/read/payloads.json
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/05-insert-before-subpage/candidate/Lifecycle.one b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/candidate/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..57e157c07782fcea01b7a78abb03ac6b1377cbab
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/candidate/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/notebook/Lifecycle.one b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..116953f292b3f2687826036802a13bebbf49da0b
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..25bd7a8c6b17626a2c78db73b8a5201a480f2b27
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/environment.json b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..dafaa444190f375767b64a2be16f372598148630
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-53AE91C1",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/hierarchy.xml b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8f076d8bf5848a97635fbb22d77bd5bc7aa0ed3c
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-000.xml b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..91502146c0b81e4f42d569b179b8cf1ccc44ac5c
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-000.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-001.xml b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2413985d3925cbf4661e539c41431a90f38341d7
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-001.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-002.xml b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fc01d2966973823bf350c07d5f083c398e94499e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-002.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-003.xml b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..83f09ec3b7d0e3a026743ce6550e4c67101b362c
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-003.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-004.xml b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ba4f1cfc361fe95a22d4fcb412537973da484f1e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-004.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-005.xml b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2d2f92139504aff131443af1f1d8e0f1a4d116b9
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-005.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-006.xml b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5313d9015ecff2158c23ab27ba0f87eb4ba09015
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-007.xml b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2f0c6aead2c3b89b934c05bb279d5d9a49836b4c
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-008.xml b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1a935ecf429dc85144ffcb6c3d74c73488004108
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/page-008.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/payloads.json b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3e9efde26e8b7e90a6e6e5fb61fe1be52686aa5e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/05-insert-before-subpage/cold/read/payloads.json
@@ -0,0 +1 @@
+../../../01-demoted-parent/cold/read/payloads.json
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/06-promoted-level-two/candidate/Lifecycle.one b/corpus/page-lifecycle/page-edits/06-promoted-level-two/candidate/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..73329e3bb82d0224cc49562f3c6a7da6833ec0fd
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/06-promoted-level-two/candidate/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/notebook/Lifecycle.one b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..45fa210eb7ad0822d530db43ad6aab3a7e09350c
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..df9a2a734606fc1842d8409dd463093178a27309
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/environment.json b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..f653e4cd2e011ba1916364a03da72cef4e936306
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-ECBADDC7",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/hierarchy.xml b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..384730177fe8cc03d6fdc520370434212c509a15
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-000.xml b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..962f5935af6895f46bf3dd8e73641fc50e95d6cc
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-000.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-001.xml b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2f35684cc2fc2594c1df2a11bf312ff83d3eaeda
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-001.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-002.xml b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..df20929502c2ac18aa54ae9333d5cbbf57eba284
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-002.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-003.xml b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..922751a1aea47bb7cb60df931a771c0a3bfbae87
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-004.xml b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7736846ca30bd59f8d0dfb70cf5851ceeb04471e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-004.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-005.xml b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a7d44bce221112d023e853196dcd611c7420b087
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-005.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-006.xml b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..dcb727fcecc1bb1312689fa7f79bec65112990ef
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-007.xml b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..437fe718e3041d7ac6a991ee7c50773b6ce3be9c
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-008.xml b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e19c0a024d44755093176af0853145d5a1f76d95
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/page-008.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/payloads.json b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3e9efde26e8b7e90a6e6e5fb61fe1be52686aa5e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/06-promoted-level-two/cold/read/payloads.json
@@ -0,0 +1 @@
+../../../01-demoted-parent/cold/read/payloads.json
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/07-promoted-root/candidate/Lifecycle.one b/corpus/page-lifecycle/page-edits/07-promoted-root/candidate/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..bd5c4d109055ebdaba0151a6b3de286ef9b7499b
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/07-promoted-root/candidate/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/07-promoted-root/cold/notebook/Lifecycle.one b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..df0316f71590bcf7e9886cfe2b7678f53227ff90
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/07-promoted-root/cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..e52ba77b521a93fefdeb633c59a667fb4cd17ca2
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/environment.json b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..061125b653bb6a0c4b3d13b69161d875c426e724
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-C8A7A5FF",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/hierarchy.xml b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a45a8fd30a3de577f1fbb9b8e9ebbd6a60a12028
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-000.xml b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ef3948a69f5e706b2f06c16b7534143930751067
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-000.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-001.xml b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..805f03bb5ddf145db7be6f8c66e765a588b8472c
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-001.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-002.xml b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9b395ed381889e86bed2eb7d5d949bd20af9d1c2
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-002.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-003.xml b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0c410807d7008afc5ca6c7816f75cce154eea3c7
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-004.xml b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..554088aa093a2c2947664e96b55c86a6ada17608
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-004.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-005.xml b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..91318ab5abaa167d0dd4203898872dd9e82bdb33
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-005.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-006.xml b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5aef2cc7faf10271d67cb2ede22d5dca3bce9e20
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-007.xml b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..204f8798370fbb1178974b8f43d0fd5dac0a276f
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-008.xml b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d3f2f86ac504542a5219268e17d140e9acfcb62d
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/page-008.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/payloads.json b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3e9efde26e8b7e90a6e6e5fb61fe1be52686aa5e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/07-promoted-root/cold/read/payloads.json
@@ -0,0 +1 @@
+../../../01-demoted-parent/cold/read/payloads.json
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/08-collapsed-group-move/candidate/Lifecycle.one b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/candidate/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..ad1474e022ae42d6c2e31770610688525cec739d
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/candidate/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/notebook/Lifecycle.one b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..6a6eb3b3ec7dc206e95f2f76c05b0089512ad167
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..6d9bb100f7d94a4edac18d9df008c3db11d821ad
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/environment.json b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..b613fb481e39ec2a3bc0d7c84be66e896e1aca0a
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-2C91BDCA",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/hierarchy.xml b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a05fde112fd3d506aa35fb61ba757d0bf70ddc2c
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-000.xml b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6db8c81da282509b1c93527fb779268496c95ed7
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-000.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-001.xml b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..dcc86b32ebf0658cbef422fe8409ac9e770d197d
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-001.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-002.xml b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4744cc6fff15d723c303f33ca2e7c1f3ae300429
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-002.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-003.xml b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4e3043aa4c9d322a627263562c16120c313f59ac
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-004.xml b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c9ebcc88cb8dbfe65b0a26f7e163be024935cdc9
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-004.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-005.xml b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..43dbdcb3fe03e2a7eec70b681b9bc5fb8bbd1604
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-005.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-006.xml b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0397f31f2db52e18b86251a6d238d5b393cd9604
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-007.xml b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..158155bfde6ad50b8e26d9d4e11af4ebce98c175
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-008.xml b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8f6562ba3c423850fafb99f3e813502b81dfde3a
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/page-008.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/payloads.json b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3e9efde26e8b7e90a6e6e5fb61fe1be52686aa5e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/08-collapsed-group-move/cold/read/payloads.json
@@ -0,0 +1 @@
+../../../01-demoted-parent/cold/read/payloads.json
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/README.md b/corpus/page-lifecycle/page-edits/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..841d2e3537a3427c1026fa09983052fbfb5a9353
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/README.md
@@ -0,0 +1,31 @@
+# Atomic Rust page edits
+
+Nine Rust-generated outcomes corresponding to the [native movement controls](../movement/README.md).
+Each candidate was independently opened in a fresh OneNote 2010 clone. Cold output
+preserves every byte beyond the file header and every active object graph without
+metadata repair. `provenance.json` records the generator, inputs and removed clones.
+
+Run `cargo test -p onestore --all-features --test page_movement` for native semantic
+comparisons, exact page-content/history preservation, invalid intents, and interrupted
+publication. Set `ONESTORE_PAGE_MOVEMENT_OUTPUT` to an absolute output directory to
+export the nine candidate states. Then use `tools/native_runner.py INPUT OUTPUT
+--expected-pages 9 --collect-notebook` for fresh independent captures.
+
+`PYTHONPATH=tools python3 -m unittest tools.test_page_movement -v` compares these
+committed fixtures with native XML and the complete cold-reopened graphs.
+
+`stress` retains 520 alternating nest/promote actions across counter carries and
+revision checkpoints. Its final two-page file also reopened without changes beyond
+the header. Set `ONESTORE_PAGE_MOVEMENT_STRESS_OUTPUT` to an absolute directory when
+running the Rust checkpoint test to export another stress file.
+
+`native` records OneNote renaming a moved page with a Unicode title and adding a
+body outline, followed by a cold reopen. `rust-followup` nests that page and edits
+the new native paragraph, then reopens it in OneNote again. Every original page
+identity and existing text remains accounted for across both writers.
+
+`optional-cache` removes an optional series metadata reference while retaining
+the old objects in history. Native OneNote restores those references in a different
+order from the pages. Rust preserves metadata identity through restoration and
+movement; the regression matches copies by their page identifiers. The unit test
+also retains an unknown property and rejects detached edits without reattachment.
diff --git a/corpus/page-lifecycle/page-edits/native/cold/notebook/Lifecycle.one b/corpus/page-lifecycle/page-edits/native/cold/notebook/Lifecycle.one
new file mode 120000
index 0000000000000000000000000000000000000000..0c82f1045162a6bde2dc5f5fe0881c0130aca14d
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/cold/notebook/Lifecycle.one
@@ -0,0 +1 @@
+../../notebook/Lifecycle.one
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/native/cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/page-edits/native/cold/notebook/Open Notebook.onetoc2
new file mode 120000
index 0000000000000000000000000000000000000000..6163f4bc08e3231fc79ece13ab200008054412d1
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/cold/notebook/Open Notebook.onetoc2
@@ -0,0 +1 @@
+../../notebook/Open Notebook.onetoc2
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/native/cold/read/environment.json b/corpus/page-lifecycle/page-edits/native/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..94566ad7daa5600deef10dcd7a05a95bc6a5808b
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-3E851B73",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/page-edits/native/cold/read/hierarchy.xml b/corpus/page-lifecycle/page-edits/native/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c5616f5456a162e905016c8098b31eba7ad31038
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/native/cold/read/page-000.xml b/corpus/page-lifecycle/page-edits/native/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fac8087c81b9761a4d6789f4d315bc0a11a3a0a9
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/cold/read/page-000.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/native/cold/read/page-001.xml b/corpus/page-lifecycle/page-edits/native/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c6ea3c0bc5e2061f3152857e5560cdb32cc7c3ee
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/cold/read/page-001.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/native/cold/read/page-002.xml b/corpus/page-lifecycle/page-edits/native/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4718852d25c63d5ce9606b80c37eb0edf08df934
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/cold/read/page-002.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/native/cold/read/page-003.xml b/corpus/page-lifecycle/page-edits/native/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6aaeab9ff44836ca6de937943c3ca97ab488145c
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/cold/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/native/cold/read/page-004.xml b/corpus/page-lifecycle/page-edits/native/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..929670e925dc351baa4b73c11cede1564c784381
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/cold/read/page-004.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/native/cold/read/page-005.xml b/corpus/page-lifecycle/page-edits/native/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..30def376605642e3a2e89b3168e93b726c43adb4
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/cold/read/page-005.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/native/cold/read/page-006.xml b/corpus/page-lifecycle/page-edits/native/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e3cca2ad3ee60751c4e314ddf304da9bc0c1a2dc
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/cold/read/page-006.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/native/cold/read/page-007.xml b/corpus/page-lifecycle/page-edits/native/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..22e0629494a3b83288377bfd668e189e1ebf94dd
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/cold/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/native/cold/read/page-008.xml b/corpus/page-lifecycle/page-edits/native/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d33825431f33087eb496241b16e6229b3d14ef95
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/cold/read/page-008.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/native/cold/read/payloads.json b/corpus/page-lifecycle/page-edits/native/cold/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3a36ad78c7d690cece15d7f9e17c4cf59431ad25
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/cold/read/payloads.json
@@ -0,0 +1 @@
+../../read/payloads.json
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/native/native-edit.xml b/corpus/page-lifecycle/page-edits/native/native-edit.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cd0f6865340cd6c0fbe31f486b3f15a135e355bf
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/native-edit.xml
@@ -0,0 +1 @@
+Native moved 🦋 éNative body after a Rust page move.
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/native/notebook/Lifecycle.one b/corpus/page-lifecycle/page-edits/native/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..8b109d26896f4daac076465a9fd0949a437e30e0
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/native/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/native/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/page-edits/native/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..8143de7cdacb41950a70606b44b6adb35c1b3515
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/native/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/page-edits/native/provenance.json b/corpus/page-lifecycle/page-edits/native/provenance.json
new file mode 100644
index 0000000000000000000000000000000000000000..b2c213e3b99ae5b4aa653b1bca99d5959e063a3b
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/provenance.json
@@ -0,0 +1,59 @@
+{
+ "author": {
+ "capture": "evidence/m10/page-edit-native-followup-01",
+ "machine": {
+ "name": "m6-80c251fb",
+ "hostname": "ONE-M6-80C251FB"
+ },
+ "teardown": {
+ "absent": true
+ },
+ "run": {
+ "notebook": "/Users/clo/dev/one/evidence/m10/page-edit-candidate-02/08-collapsed-group-move",
+ "expected_pages": 9,
+ "author": "tools/native/moved-pages.ps1",
+ "author_timeout_seconds": 600,
+ "inspect": false,
+ "collect_notebook": true,
+ "base": {
+ "file": "win7-office-base.qcow2",
+ "format": "qcow2",
+ "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346",
+ "virtual_size": 68719476736
+ },
+ "scripts": {
+ "author.ps1": "42c5e9aaf9bf9bb830a28e53d715bdaa3b8fb5a3ad688e5fa552079e0632d36f",
+ "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
+ "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
+ }
+ }
+ },
+ "cold": {
+ "capture": "evidence/m10/page-edit-native-followup-cold-01",
+ "machine": {
+ "name": "m6-3e851b73",
+ "hostname": "ONE-M6-3E851B73"
+ },
+ "teardown": {
+ "absent": true
+ },
+ "run": {
+ "notebook": "/Users/clo/dev/one/evidence/m10/page-edit-native-followup-01/notebook",
+ "expected_pages": 9,
+ "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/page-lifecycle/page-edits/native/read/environment.json b/corpus/page-lifecycle/page-edits/native/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..8ecf6131e623995459c02aa45c71ca0892d8e55c
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-80C251FB",
+ "cold": false,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/page-edits/native/read/hierarchy.xml b/corpus/page-lifecycle/page-edits/native/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1bee5f6a662ca568ca0170bfb00595c2092267ca
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/native/read/page-000.xml b/corpus/page-lifecycle/page-edits/native/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..650abefc63b72af1777ebf2be4bc6522308fee5d
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/read/page-000.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/native/read/page-001.xml b/corpus/page-lifecycle/page-edits/native/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3491bba0d178b9c7739fa346227f95b52326f3e6
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/read/page-001.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/native/read/page-002.xml b/corpus/page-lifecycle/page-edits/native/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..33da1e1d575c0e113b1df1a82fb73ddb8964a507
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/read/page-002.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/native/read/page-003.xml b/corpus/page-lifecycle/page-edits/native/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e7d10f039bd549a066354a8bb23e91fb11b75a1f
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/read/page-003.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/native/read/page-004.xml b/corpus/page-lifecycle/page-edits/native/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..60b44690d4868db50246299cb19857f7f74a6f1d
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/read/page-004.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/native/read/page-005.xml b/corpus/page-lifecycle/page-edits/native/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ebbe1a64337799154b690e20128702308488d671
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/read/page-005.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/native/read/page-006.xml b/corpus/page-lifecycle/page-edits/native/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5ec16f3b2079fb415752fbeaa1771c384fff4603
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/native/read/page-007.xml b/corpus/page-lifecycle/page-edits/native/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9c6f3e3ec95d46b2b60af23bbbcd23b7f7f2d15d
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/native/read/page-008.xml b/corpus/page-lifecycle/page-edits/native/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2ec839bc32d38ba809c7a87a3195c77a3ecc65fd
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/read/page-008.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/native/read/payloads.json b/corpus/page-lifecycle/page-edits/native/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..9b53e0bbb9d9e4462e144293c10e453d14f8b604
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/native/read/payloads.json
@@ -0,0 +1 @@
+../../01-demoted-parent/cold/read/payloads.json
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/candidate/Lifecycle.one b/corpus/page-lifecycle/page-edits/optional-cache/candidate/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..0ad143b42de51792c58f962202cc44fea2d64b61
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/optional-cache/candidate/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/cold/notebook/Lifecycle.one b/corpus/page-lifecycle/page-edits/optional-cache/cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..42fba54cc14de108494cc283084e328175c99b95
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/optional-cache/cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/page-edits/optional-cache/cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..12fdf5f25b6879d73aa601e39857f404cf3a947c
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/optional-cache/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/cold/read/environment.json b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..2c658fd65c4eb03bd05cdf8746ef2070d68de579
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-96DF7BE7",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/cold/read/hierarchy.xml b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6f11a1caaaa4908773cb7829d671ca1f942ae844
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-000.xml b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ed94841525728e61bd4335590639bea07ffe2a69
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-000.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-001.xml b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fccb8fff86dd1a476ac9d24d08d8a891677f4b0d
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-001.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-002.xml b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1344b947d192a01ff884a22aa6cdac2c5af915ab
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-002.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-003.xml b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..37b2d787e0e341f2dda7885567da23719bba82e6
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-004.xml b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..137d25a746b198491fb180ca00024a59fda5b917
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-004.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-005.xml b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4d98bfa2a7c341750b5e50dbb11c6d820ba4114b
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-005.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-006.xml b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b10da94272399b236c6e0ba5d1c2532722888bfc
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-006.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-007.xml b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c60951fd75aa9dea099592f50201bb44629edf17
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-008.xml b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4783a591b06c859649dbac2dc9abfe72d9c88920
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/page-008.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/cold/read/payloads.json b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3e9efde26e8b7e90a6e6e5fb61fe1be52686aa5e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/cold/read/payloads.json
@@ -0,0 +1 @@
+../../../01-demoted-parent/cold/read/payloads.json
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/followup/candidate/Lifecycle.one b/corpus/page-lifecycle/page-edits/optional-cache/followup/candidate/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..76f88662fc7e44965fa96304619125633d0b75b7
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/optional-cache/followup/candidate/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/notebook/Lifecycle.one b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..d71af9d949ee714e48ae17266faec13f0f78f1a2
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..81e17b4be6b81a9cf1f51b920b6b1f8e218f12ba
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/environment.json b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..1c0f1df920ae7193f1cf8cbd861b013056a3132c
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-76A08F71",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/hierarchy.xml b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c324c643f125d3c672b6fba71c11f76a512f8a2f
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-000.xml b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..45bdc76381f7f8fb372bfae2432f4412dce03ec1
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-000.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-001.xml b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..27c4a1fc64a088e6b2c3582d094396e5c356dfa3
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-001.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-002.xml b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a0e3db80c9c3deb91808f9c50a7914e022135b1d
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-002.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-003.xml b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e9f9435fcfd73607c7f23f171cbc4dc8c4ff38aa
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-004.xml b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bf7cb0c8627c9b1b59fd05392282d1e3f861938e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-004.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-005.xml b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0309fff2292b04831c3888e5d11ab8eaf18a0b0d
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-005.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-006.xml b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a464d36cbf98ca71eb555475ec1e1956a388020e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-007.xml b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7fc1aaf796beaea52f11e6dabd899e2e232337f2
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-008.xml b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ffc5184e44134ca59a4fd1fdfebf2aefd9b869c8
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/page-008.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/payloads.json b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..504b3b72f57a27cf5053f94b0c5fbf0cf365b104
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/followup/cold/read/payloads.json
@@ -0,0 +1 @@
+../../../../01-demoted-parent/cold/read/payloads.json
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/followup/provenance.json b/corpus/page-lifecycle/page-edits/optional-cache/followup/provenance.json
new file mode 100644
index 0000000000000000000000000000000000000000..d96419b3b221765729f42ec66403c5449bb82bdf
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/followup/provenance.json
@@ -0,0 +1,19 @@
+{
+ "generator": "crates/onestore/tests/page_movement.rs::metadata_copy_order_does_not_change_page_identity",
+ "capture": "evidence/m10/page-edit-metadata-order-cold-01",
+ "machine": {
+ "name": "m6-76a08f71",
+ "hostname": "ONE-M6-76A08F71"
+ },
+ "teardown": {
+ "absent": true
+ },
+ "source": [
+ {
+ "path": "Lifecycle.one",
+ "bytes": 59848,
+ "sha256": "c44936c8f45de1b5f136050a1c41a51a237634e154d39bafb5abbda5773b3caf",
+ "mtime_ns": 1788939065374445295
+ }
+ ]
+}
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/provenance.json b/corpus/page-lifecycle/page-edits/optional-cache/provenance.json
new file mode 100644
index 0000000000000000000000000000000000000000..67d4a456ec0be8af53585a9070169c29a52bb6b1
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/provenance.json
@@ -0,0 +1,41 @@
+{
+ "generator": "crates/onestore/src/page.rs::tests::page_movement_restores_an_optional_metadata_cache_from_history",
+ "captures": {
+ "source": {
+ "capture": "evidence/m10/page-edit-optional-source-cold-02",
+ "machine": {
+ "name": "m6-41fb00f9",
+ "hostname": "ONE-M6-41FB00F9"
+ },
+ "teardown": {
+ "absent": true
+ },
+ "source": [
+ {
+ "path": "Lifecycle.one",
+ "bytes": 54872,
+ "sha256": "d4d0955b95844a3bf578c39b68c9039334f730876617f36b655b96c5b59289cd",
+ "mtime_ns": 1788938613040603712
+ }
+ ]
+ },
+ "restored": {
+ "capture": "evidence/m10/page-edit-optional-cold-02",
+ "machine": {
+ "name": "m6-96df7be7",
+ "hostname": "ONE-M6-96DF7BE7"
+ },
+ "teardown": {
+ "absent": true
+ },
+ "source": [
+ {
+ "path": "Lifecycle.one",
+ "bytes": 57624,
+ "sha256": "c7b59e53945bd28c9ac2542890f43c7098568b50a1d14301c6ded79e9c7b5cf8",
+ "mtime_ns": 1788938613040899005
+ }
+ ]
+ }
+ }
+}
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/source-cold/notebook/Lifecycle.one b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..634ecfb9e53a014bdd35913edd2633a18cb5a9a5
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/source-cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..c3c71ff702ea517f7e7e69806fa46d2b032b38d3
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/environment.json b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..0cd5bda882a4464877afd69c20a1fcc5a20b7c65
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-41FB00F9",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/hierarchy.xml b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bd1ad2c9f0253058b6fcdf03ef0a691a5cc4dd65
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-000.xml b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fe7f4ceb4a26b199ef57f9a6f6311e3f3b8cc852
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-000.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-001.xml b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a5f768b56a7c037dea800746288ae78ca0ceb23b
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-001.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-002.xml b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5dc2240bf3ef9c6b988fc24a890e7ec9c5720ffe
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-002.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-003.xml b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..445d4ded501804c0d695b55c4b1d3d61cd21d753
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-004.xml b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8a701a0a2081a846db9f0c310d452674550a7bea
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-004.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-005.xml b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d1c5722aa176d69ca65771210e513e77ab4c67f5
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-005.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-006.xml b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fdc21be1e69e0a0a1ed50128ec6af8f932581847
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-007.xml b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..682637e0323809ca6424a66f2eb3ac016d26b2a2
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-008.xml b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..25f6ffd35d6e06ceb47bc8f813622852854aa35c
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/page-008.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/payloads.json b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3e9efde26e8b7e90a6e6e5fb61fe1be52686aa5e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/optional-cache/source-cold/read/payloads.json
@@ -0,0 +1 @@
+../../../01-demoted-parent/cold/read/payloads.json
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/optional-cache/source/Lifecycle.one b/corpus/page-lifecycle/page-edits/optional-cache/source/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..c9b7e03e668f5c7b756e2a4d72d1d17bf8d73138
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/optional-cache/source/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/provenance.json b/corpus/page-lifecycle/page-edits/provenance.json
new file mode 100644
index 0000000000000000000000000000000000000000..8e888f9509b59ed4c5ebc8d0c493575acfcc9df1
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/provenance.json
@@ -0,0 +1,169 @@
+{
+ "generator": "crates/onestore/tests/page_movement.rs::explicit_page_edits_match_native_selection_and_indentation",
+ "source": "corpus/page-lifecycle/04-nested/notebook/Lifecycle.one",
+ "candidate": "evidence/m10/page-edit-candidate-02",
+ "cold": {
+ "01-demoted-parent": {
+ "capture": "evidence/m10/page-edit-cold-01-demoted-parent",
+ "machine": {
+ "name": "m6-ce1d1d57",
+ "hostname": "ONE-M6-CE1D1D57"
+ },
+ "teardown": {
+ "absent": true
+ },
+ "source": [
+ {
+ "path": "Lifecycle.one",
+ "bytes": 56056,
+ "sha256": "2629b43d4b6c2630522862e3a854ec58279b7fe803f30242c0d834ea34b664da",
+ "mtime_ns": 1788935349739312911
+ }
+ ]
+ },
+ "02-promoted-parent": {
+ "capture": "evidence/m10/page-edit-cold-02-promoted-parent",
+ "machine": {
+ "name": "m6-c1201e9e",
+ "hostname": "ONE-M6-C1201E9E"
+ },
+ "teardown": {
+ "absent": true
+ },
+ "source": [
+ {
+ "path": "Lifecycle.one",
+ "bytes": 58960,
+ "sha256": "eb8e60206218524e11c126d40ad87531691ad74118566d24057aa98be3d494d8",
+ "mtime_ns": 1788935349797487705
+ }
+ ]
+ },
+ "03-selected-group-move": {
+ "capture": "evidence/m10/page-edit-cold-03-selected-group-move",
+ "machine": {
+ "name": "m6-20062e97",
+ "hostname": "ONE-M6-20062E97"
+ },
+ "teardown": {
+ "absent": true
+ },
+ "source": [
+ {
+ "path": "Lifecycle.one",
+ "bytes": 59688,
+ "sha256": "6048ce8cc2ca5b24da2c594c079e4c98aee8b8ee742f667e2e6beedd0917c12a",
+ "mtime_ns": 1788935349855149794
+ }
+ ]
+ },
+ "04-subpage-move": {
+ "capture": "evidence/m10/page-edit-cold-04-subpage-move",
+ "machine": {
+ "name": "m6-6019e29d",
+ "hostname": "ONE-M6-6019E29D"
+ },
+ "teardown": {
+ "absent": true
+ },
+ "source": [
+ {
+ "path": "Lifecycle.one",
+ "bytes": 62168,
+ "sha256": "e902c74b456d177fd4effbcecd73550f5abaaf1dd51d6f844db03c591a8a7707",
+ "mtime_ns": 1788935349914573000
+ }
+ ]
+ },
+ "05-insert-before-subpage": {
+ "capture": "evidence/m10/page-edit-cold-05-insert-before-subpage",
+ "machine": {
+ "name": "m6-53ae91c1",
+ "hostname": "ONE-M6-53AE91C1"
+ },
+ "teardown": {
+ "absent": true
+ },
+ "source": [
+ {
+ "path": "Lifecycle.one",
+ "bytes": 64576,
+ "sha256": "8ad33d7aef415d0c2a1f8755a43ad62a5a803c2b1b4655c9d1d1aa615f7f353e",
+ "mtime_ns": 1788935349976521109
+ }
+ ]
+ },
+ "06-promoted-level-two": {
+ "capture": "evidence/m10/page-edit-cold-06-promoted-level-two",
+ "machine": {
+ "name": "m6-ecbaddc7",
+ "hostname": "ONE-M6-ECBADDC7"
+ },
+ "teardown": {
+ "absent": true
+ },
+ "source": [
+ {
+ "path": "Lifecycle.one",
+ "bytes": 65664,
+ "sha256": "5c23f179e53441d81e2d6d84badaa3a19121276f341eb32dc214c8148107bab3",
+ "mtime_ns": 1788935350039848211
+ }
+ ]
+ },
+ "07-promoted-root": {
+ "capture": "evidence/m10/page-edit-cold-07-promoted-root",
+ "machine": {
+ "name": "m6-c8a7a5ff",
+ "hostname": "ONE-M6-C8A7A5FF"
+ },
+ "teardown": {
+ "absent": true
+ },
+ "source": [
+ {
+ "path": "Lifecycle.one",
+ "bytes": 68216,
+ "sha256": "375d815d58a5775261f54c4c23134c863b6dbe69ed48ba7636d3218008544ef8",
+ "mtime_ns": 1788935350105745801
+ }
+ ]
+ },
+ "08-collapsed-group-move": {
+ "capture": "evidence/m10/page-edit-cold-08-collapsed-group-move",
+ "machine": {
+ "name": "m6-2c91bdca",
+ "hostname": "ONE-M6-2C91BDCA"
+ },
+ "teardown": {
+ "absent": true
+ },
+ "source": [
+ {
+ "path": "Lifecycle.one",
+ "bytes": 69000,
+ "sha256": "fb2b01ee423c89cdeff62b32e1ff7edd225a0d90da27ec8d749dd3870105449c",
+ "mtime_ns": 1788935350173118884
+ }
+ ]
+ },
+ "single-page": {
+ "capture": "evidence/m10/page-edit-cold-single-page",
+ "machine": {
+ "name": "m6-8a614337",
+ "hostname": "ONE-M6-8A614337"
+ },
+ "teardown": {
+ "absent": true
+ },
+ "source": [
+ {
+ "path": "Lifecycle.one",
+ "bytes": 55040,
+ "sha256": "1a1a4adb6e5032fd7b966bce629d454e38682eb3290c16ee9c23a8eda043f8cd",
+ "mtime_ns": 1788935350218567200
+ }
+ ]
+ }
+ }
+}
diff --git a/corpus/page-lifecycle/page-edits/rust-followup/candidate/Lifecycle.one b/corpus/page-lifecycle/page-edits/rust-followup/candidate/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..158d2b3fcecbce0a004807209f05157704c1118c
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/rust-followup/candidate/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/rust-followup/cold/notebook/Lifecycle.one b/corpus/page-lifecycle/page-edits/rust-followup/cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..e74f8138bb39cb418e66d75e651a1b0aec7bd4f2
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/rust-followup/cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/rust-followup/cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/page-edits/rust-followup/cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..73787bcc8c508ceba52f23ec53cf2a36fd6baf48
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/rust-followup/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/page-edits/rust-followup/cold/read/environment.json b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..2468ead4b933b3bff242359b86545116ca18be2b
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-7EAA4E8F",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/page-edits/rust-followup/cold/read/hierarchy.xml b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ccbc95a76e9205db8d8a977d7a8070808133d7b7
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-000.xml b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..be0b5d91ab9b9df1a5caa97ffc518e0aba06ef18
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-000.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-001.xml b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6c30f6ebc07ced66dc80cab7f8b294e0074cf5fa
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-001.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-002.xml b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..918ef6f0e378538066880c69d346f95710520c97
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-002.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-003.xml b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..239c649885869b7affed0046a741f150ed69edf7
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-003.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-004.xml b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c05fe654f5b931f0b5056da25d9b561a5a09844e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-004.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-005.xml b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e774e9029cf8192e6517b5ff7b3cb10625d9d574
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-005.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-006.xml b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f0191fe342c3807c8b198a8c5104da2fff755222
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-006.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-007.xml b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..131afadfe968a5f502403a83edc5771dbcd33a06
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-008.xml b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..40ffe0067dff63b31d050628498a39161efb5e3f
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/page-008.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/rust-followup/cold/read/payloads.json b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3e9efde26e8b7e90a6e6e5fb61fe1be52686aa5e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/rust-followup/cold/read/payloads.json
@@ -0,0 +1 @@
+../../../01-demoted-parent/cold/read/payloads.json
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/rust-followup/provenance.json b/corpus/page-lifecycle/page-edits/rust-followup/provenance.json
new file mode 100644
index 0000000000000000000000000000000000000000..d1c083d7ce69bf9f4eb4ac812b2c0fa7a9504689
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/rust-followup/provenance.json
@@ -0,0 +1,19 @@
+{
+ "generator": "crates/onestore/tests/page_movement.rs::native_edits_on_moved_pages_support_more_rust_changes",
+ "capture": "evidence/m10/page-edit-rust-followup-cold-01",
+ "machine": {
+ "name": "m6-7eaa4e8f",
+ "hostname": "ONE-M6-7EAA4E8F"
+ },
+ "teardown": {
+ "absent": true
+ },
+ "source": [
+ {
+ "path": "Lifecycle.one",
+ "bytes": 76784,
+ "sha256": "d777045965f44952fc7aab6e020645d79d959b0a002c0e684f38381106713eb8",
+ "mtime_ns": 1788937158196655008
+ }
+ ]
+}
diff --git a/corpus/page-lifecycle/page-edits/single-page/candidate/Lifecycle.one b/corpus/page-lifecycle/page-edits/single-page/candidate/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..188edc7bf165d5f1699835ec639dd53041db9c17
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/single-page/candidate/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/single-page/cold/notebook/Lifecycle.one b/corpus/page-lifecycle/page-edits/single-page/cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..b6cf5fcf4982365385f9bb53455c522965336aae
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/single-page/cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/page-edits/single-page/cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/page-edits/single-page/cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..89c456d40dd0f2940a64b923ba4b4fb8c8169fa5
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/single-page/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/page-edits/single-page/cold/read/environment.json b/corpus/page-lifecycle/page-edits/single-page/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..78acba52e396b66393a6fbce5b9c657e78699ff1
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/single-page/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-8A614337",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/page-edits/single-page/cold/read/hierarchy.xml b/corpus/page-lifecycle/page-edits/single-page/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f6c5ada426ec06e19efc71b7921073d0daeda28e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/single-page/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/single-page/cold/read/page-000.xml b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f12279adc53eaa133b3e89bbcc25156675229fee
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-000.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/single-page/cold/read/page-001.xml b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d208d7eced737b33b9ea0a6b2e0c4d4f205d4a2a
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-001.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/single-page/cold/read/page-002.xml b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f35d314913c07c79968f180b2e7b7fce1e039246
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-002.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/single-page/cold/read/page-003.xml b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8656963ca76f91ad9d3b39afdce39fd9ed28512f
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/single-page/cold/read/page-004.xml b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c3964a37a1e00ba786cc8c98f7517dc0236db961
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-004.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/single-page/cold/read/page-005.xml b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c8519c4836b7ae6a863058371cbed072b6d8f2f6
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-005.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/single-page/cold/read/page-006.xml b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..305f7676ff626a3f55bcb4b773f83bb93ac47452
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/single-page/cold/read/page-007.xml b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fe75d8c6f51c8e039621a51454a75e4ebc013277
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-007.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/single-page/cold/read/page-008.xml b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f20f5e2dea7d6b7854bc9ddbe14c72bc2a109484
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/single-page/cold/read/page-008.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/page-edits/single-page/cold/read/payloads.json b/corpus/page-lifecycle/page-edits/single-page/cold/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3e9efde26e8b7e90a6e6e5fb61fe1be52686aa5e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/single-page/cold/read/payloads.json
@@ -0,0 +1 @@
+../../../01-demoted-parent/cold/read/payloads.json
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/stress/candidate/movement.one b/corpus/page-lifecycle/page-edits/stress/candidate/movement.one
new file mode 100644
index 0000000000000000000000000000000000000000..82d300255880a5020cd967ddc023fd5663f478f3
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/stress/candidate/movement.one differ
diff --git a/corpus/page-lifecycle/page-edits/stress/cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/page-edits/stress/cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..bbec2b3cf53ddd4da36fa14c81cf6cee27fe9049
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/stress/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/page-edits/stress/cold/notebook/movement.one b/corpus/page-lifecycle/page-edits/stress/cold/notebook/movement.one
new file mode 100644
index 0000000000000000000000000000000000000000..b6a432349a6a27d35eeda9597df192403f6dad04
Binary files /dev/null and b/corpus/page-lifecycle/page-edits/stress/cold/notebook/movement.one differ
diff --git a/corpus/page-lifecycle/page-edits/stress/cold/read/environment.json b/corpus/page-lifecycle/page-edits/stress/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..c3ab70aea40dd3fb0ebc12be67640dc0feb2f474
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/stress/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-40A088EC",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/page-edits/stress/cold/read/hierarchy.xml b/corpus/page-lifecycle/page-edits/stress/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ad6751baa8f875d81ab183864481c190c503b806
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/stress/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/stress/cold/read/page-000.xml b/corpus/page-lifecycle/page-edits/stress/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..53ec8f0dc30ef0c4c5b6ee1aa373b10177b51339
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/stress/cold/read/page-000.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/stress/cold/read/page-001.xml b/corpus/page-lifecycle/page-edits/stress/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a01e9753e0c69fe00274d33b324c0a6b17163904
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/stress/cold/read/page-001.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/page-edits/stress/cold/read/payloads.json b/corpus/page-lifecycle/page-edits/stress/cold/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..3e9efde26e8b7e90a6e6e5fb61fe1be52686aa5e
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/stress/cold/read/payloads.json
@@ -0,0 +1 @@
+../../../01-demoted-parent/cold/read/payloads.json
\ No newline at end of file
diff --git a/corpus/page-lifecycle/page-edits/stress/provenance.json b/corpus/page-lifecycle/page-edits/stress/provenance.json
new file mode 100644
index 0000000000000000000000000000000000000000..201ee7ff57e0ba45a9524507a119cd26d1f247c3
--- /dev/null
+++ b/corpus/page-lifecycle/page-edits/stress/provenance.json
@@ -0,0 +1,20 @@
+{
+ "generator": "crates/onestore/tests/page_movement.rs::repeated_nesting_retains_series_history_across_revision_checkpoints",
+ "edits": 520,
+ "capture": "evidence/m10/page-edit-stress-cold-01",
+ "machine": {
+ "name": "m6-40a088ec",
+ "hostname": "ONE-M6-40A088EC"
+ },
+ "teardown": {
+ "absent": true
+ },
+ "source": [
+ {
+ "path": "movement.one",
+ "bytes": 2635536,
+ "sha256": "77dbec39ca25558b596b477d57117416f23ee133519f5179d7ebf1a33b38c248",
+ "mtime_ns": 1788936082839470214
+ }
+ ]
+}
diff --git a/crates/onestore/README.md b/crates/onestore/README.md
index ef5de8c3d810f34f1293aae8bddf90ae6fef35f9..6e3f4c5bb71750b1644d7b8b7ef1a22f7a45b160 100644
--- a/crates/onestore/README.md
+++ b/crates/onestore/README.md
@@ -46,6 +46,7 @@ harness also accepts `--client-profile release`.
| `protected::UnlockedSection` (optional feature) | Own decoded buffers for explicit known-password inspection; clear those buffers on drop; derived document strings/exports remain caller-owned |
| `create_section` | Create one page containing one plain-text paragraph and an author, including Unicode |
| `PageCreation`, `PreparedEdit::create_page` | Add an empty top-level page and its section entry atomically, retaining page identities across retries |
+| `PageEdit`, `PreparedEdit::pages` | Publish explicitly selected page moves and indentation changes together, preserving page content and historical revisions |
| `create_table_of_contents` | Create ordered section entries from filenames and file identities |
| `replace_property_bytes` | Append one scalar-property revision; preserve prior revisions and unrelated property values and references |
| `replace_text`, `commit_text`, `commit_file_text` | Replace a UTF-16 range across ordinary text runs; publish text, run boundaries and modification time together |
@@ -81,6 +82,15 @@ Retain the intent to preserve its page, title and space identities; existing
identities require reconciliation before retry. Body insertion and title edits
use those identities through the existing APIs. [Native page-creation fixtures](../../corpus/page-lifecycle/creation/README.md)
cover duplicate Unicode titles, native edits and Rust follow-up edits.
+`PageEdit::set_level` changes one page's indentation in place. `PageEdit::move_to`
+moves before an existing page space, or appends for `None`, and sets its level.
+`PreparedEdit::pages` applies moves in slice order and publishes the final order,
+series membership and metadata levels in one transaction. Each page occurs once;
+levels are 1–3 and the final first page must have level 1. A following deeper-level
+page remains in place unless explicitly selected. To move a group, supply all its
+pages in order. Retain the intents across retries so newly formed series keep
+their identities. [Native page-edit fixtures](../../corpus/page-lifecycle/page-edits/README.md)
+cover individual tabs, selected and collapsed groups, nesting and promotion.
Insertions update child references, reference counts, modification times and automatic
titles atomically. Paragraphs can be nested or inserted into table cells; outline
coordinates use points. `Insertion::with_formatting` includes nonoverlapping UTF-16
diff --git a/crates/onestore/src/commit.rs b/crates/onestore/src/commit.rs
index 49aa9d68405c10697d372f37cebbd3b15110aa36..ddf2a0e427c2bd20f5103a98993f0d5ca7e7141e 100644
--- a/crates/onestore/src/commit.rs
+++ b/crates/onestore/src/commit.rs
@@ -183,6 +183,15 @@ pub struct PreparedEdit<'a> {
}
impl<'a> PreparedEdit<'a> {
+ /// Applies page moves in slice order and publishes final order and indentation atomically.
+ /// Each page occurs once; all pages to move must be explicit, including selected subpages.
+ pub fn pages(source: &'a [u8], edits: &[crate::PageEdit]) -> Result {
+ Ok(Self {
+ source,
+ written: crate::PageEdit::apply(source, edits)?,
+ })
+ }
+
/// Creates a page and its section entry in one transaction, retaining the intent's identities.
pub fn create_page(source: &'a [u8], page: &crate::PageCreation) -> Result {
Ok(Self {
diff --git a/crates/onestore/src/lib.rs b/crates/onestore/src/lib.rs
index 5334f874fc04fd30ede8640713bf77bdfe8749c3..fdcbb6a90cc801dd223dc7fdc952c72275f442cf 100644
--- a/crates/onestore/src/lib.rs
+++ b/crates/onestore/src/lib.rs
@@ -36,7 +36,7 @@ pub use formatting::TextAttribute;
pub use insertion::Insertion;
pub use objects::{Object, ObjectData, ObjectReferences, ResolvedRevision};
pub use outline::OutlineEdit;
-pub use page::PageCreation;
+pub use page::{PageCreation, PageEdit};
pub use paragraph::{ParagraphJoin, ParagraphSplit};
pub use properties::{IdStream, Property, PropertySets, Value};
pub use revisions::{ExGuid, ObjectSpace, Revision, RevisionIndex};
diff --git a/crates/onestore/src/page.rs b/crates/onestore/src/page.rs
index 4365eb796248336dce961141b15f761828d252d5..a4487113f96f32f12d9d4935059b2720d6c5fe01 100644
--- a/crates/onestore/src/page.rs
+++ b/crates/onestore/src/page.rs
@@ -1,16 +1,336 @@
use crate::{
Error, ExGuid,
create::{current_timestamps, properties, string},
- document::{Document, Kind},
+ document::{Document, FieldValue, Kind},
write::{PropertyObject, RevisionEdit, fresh_guid, write_revisions},
};
use serde::{Deserialize, Serialize};
-use std::{collections::BTreeMap, sync::Arc};
+use std::{
+ collections::{BTreeMap, BTreeSet},
+ sync::Arc,
+};
fn invalid(message: &'static str) -> Error {
Error { offset: 0, message }
}
+fn metadata_id(space: ExGuid) -> ExGuid {
+ let mut id = ExGuid {
+ guid: space.guid,
+ n: 1,
+ };
+ for (byte, salt) in id.guid.iter_mut().zip([
+ 0x31, 0xc0, 0xa8, 0x22, 0, 0x36, 0xee, 0x42, 0xb7, 0x14, 0xd7, 0xac, 0xda, 0x24, 0x35, 0xe8,
+ ]) {
+ *byte ^= salt;
+ }
+ id
+}
+
+fn metadata_guid(metadata: &crate::document::Element<'_>) -> Result<[u8; 16], Error> {
+ metadata.extra[0]
+ .iter()
+ .find_map(|field| match field.value {
+ FieldValue::Bytes(bytes) if field.id == 0x1c001c30 => bytes.try_into().ok(),
+ _ => None,
+ })
+ .ok_or_else(|| invalid("Page metadata needs its page identifier"))
+}
+
+fn set_references(object: &mut PropertyObject, property: u32, ids: &[ExGuid]) -> Result<(), Error> {
+ let mut references = Vec::new();
+ for id in ids {
+ references.extend_from_slice(&object.reference(*id)?);
+ }
+ let source = PropertyObject {
+ jcid: object.jcid,
+ bytes: properties(&[(property, references)])?,
+ global_ids: Arc::clone(&object.global_ids),
+ };
+ object.copy_property(&source, property)
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+enum Position {
+ Keep,
+ Before(Option),
+}
+
+/// A page's explicit position and indentation, with a stable identity for a new series.
+/// Moving a page does not implicitly select its following subpages.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct PageEdit {
+ guid: [u8; 16],
+ space: ExGuid,
+ level: u32,
+ position: Position,
+}
+
+impl PageEdit {
+ /// Changes one page's indentation without changing its position.
+ pub fn set_level(space: ExGuid, level: u32) -> Result {
+ Self::new(space, level, Position::Keep)
+ }
+
+ /// Moves before an existing page space, or appends when `before` is None.
+ pub fn move_to(space: ExGuid, before: Option, level: u32) -> Result {
+ Self::new(space, level, Position::Before(before))
+ }
+
+ pub fn space(&self) -> ExGuid {
+ self.space
+ }
+
+ fn new(space: ExGuid, level: u32, position: Position) -> Result {
+ let edit = Self {
+ guid: fresh_guid()?,
+ space,
+ level,
+ position,
+ };
+ edit.validate()?;
+ Ok(edit)
+ }
+
+ fn validate(&self) -> Result<(), Error> {
+ if self.guid == [0; 16] || self.space.guid == [0; 16] || !(1..=3).contains(&self.level) {
+ return Err(invalid(
+ "Choose an existing page space and indentation level 1 through 3",
+ ));
+ }
+ if let Position::Before(Some(before)) = self.position
+ && (before.guid == [0; 16] || before == self.space)
+ {
+ return Err(invalid("Choose a different page as the movement anchor"));
+ }
+ Ok(())
+ }
+
+ pub(crate) fn apply(source: &[u8], edits: &[Self]) -> Result, Error> {
+ let mut selected = BTreeMap::new();
+ let mut guids = BTreeSet::new();
+ for edit in edits {
+ edit.validate()?;
+ if selected.insert(edit.space, edit).is_some() || !guids.insert(edit.guid) {
+ return Err(invalid(
+ "Use distinct page and edit identities within a batch",
+ ));
+ }
+ }
+ write_revisions(source, |index| {
+ let document = Document::parse(index)?;
+ let pages = document.pages()?;
+ let section = &document.spaces[&document.root];
+ let view = §ion.revisions[§ion.contexts[&ExGuid::default()]];
+ let section_id = view.roots[&1];
+ let section_node = &view.nodes[§ion_id];
+ if !matches!(section_node.kind, Kind::Section { .. })
+ || section_node.extra[0]
+ .iter()
+ .any(|field| field.id == 0x88001cde)
+ {
+ return Err(invalid("Choose an editable section for page movement"));
+ }
+ let mut order = Vec::new();
+ let mut levels = BTreeMap::new();
+ let mut page_guids = BTreeMap::new();
+ for (sid, _) in &pages {
+ let page = &document.spaces[sid];
+ let page = &page.revisions[&page.contexts[&ExGuid::default()]];
+ let Some(metadata) = page.roots.get(&2).and_then(|id| page.nodes.get(id)) else {
+ return Err(invalid("Page metadata is unavailable"));
+ };
+ let Kind::Metadata { level, .. } = metadata.kind else {
+ return Err(invalid("Choose ordinary pages with page metadata"));
+ };
+ let level = level.unwrap_or(1);
+ if !(1..=3).contains(&level) || levels.insert(*sid, level).is_some() {
+ return Err(invalid(
+ "Each page space must contain one page with a valid level",
+ ));
+ }
+ if page_guids.insert(metadata_guid(metadata)?, *sid).is_some() {
+ return Err(invalid("Pages must have distinct metadata identifiers"));
+ }
+ order.push(*sid);
+ }
+ let original_order = order.clone();
+ let original_levels = levels.clone();
+ let mut series_by_head = BTreeMap::new();
+ let mut copies = BTreeMap::new();
+ let mut copy_ids = BTreeSet::new();
+ for oid in §ion_node.children {
+ let series = &view.nodes[oid];
+ let Some(head) = series.spaces.first() else {
+ return Err(invalid("A page series must contain at least one page"));
+ };
+ if levels.get(head) != Some(&1)
+ || series.spaces[1..]
+ .iter()
+ .any(|sid| levels.get(sid).is_none_or(|level| *level == 1))
+ {
+ return Err(invalid(
+ "A page series must start with its only top-level page",
+ ));
+ }
+ series_by_head.insert(*head, *oid);
+ if let Some(field) = series.extra[0].iter().find(|field| field.id == 0x24003442) {
+ let FieldValue::Objects(ids) = &field.value else {
+ return Err(invalid("Page metadata copies must be object references"));
+ };
+ if ids.len() != series.spaces.len() {
+ return Err(invalid("Page metadata copies must match their series"));
+ }
+ for id in ids {
+ if !copy_ids.insert(*id)
+ || !matches!(
+ view.nodes.get(id).map(|n| &n.kind),
+ Some(Kind::Metadata { .. })
+ )
+ {
+ return Err(invalid("Each page needs its own ordinary metadata copy"));
+ }
+ let sid = page_guids
+ .get(&metadata_guid(&view.nodes[id])?)
+ .filter(|sid| series.spaces.contains(sid))
+ .ok_or_else(|| {
+ invalid("Metadata copy must identify a page in its series")
+ })?;
+ if copies.insert(*sid, *id).is_some() {
+ return Err(invalid("Each page needs its own ordinary metadata copy"));
+ }
+ }
+ }
+ }
+ for edit in edits {
+ let Some(at) = order.iter().position(|sid| *sid == edit.space) else {
+ return Err(invalid("The selected page is no longer in the section"));
+ };
+ levels.insert(edit.space, edit.level);
+ if let Position::Before(before) = edit.position {
+ order.remove(at);
+ let position = match before {
+ None => order.len(),
+ Some(before) => {
+ order.iter().position(|sid| *sid == before).ok_or_else(|| {
+ invalid("The movement anchor is no longer in the section")
+ })?
+ }
+ };
+ order.insert(position, edit.space);
+ }
+ }
+ if order.first().is_some_and(|sid| levels[sid] != 1) {
+ return Err(invalid(
+ "The section's first page must have indentation level 1",
+ ));
+ }
+ if order == original_order && levels == original_levels {
+ return Ok(BTreeMap::new());
+ }
+ let raw = index.resolve(document.root, section.contexts[&ExGuid::default()])?;
+ let mut groups: Vec> = Vec::new();
+ for sid in order {
+ if levels[&sid] == 1 {
+ groups.push(Vec::new());
+ }
+ groups.last_mut().unwrap().push(sid);
+ }
+ let mut changes = BTreeMap::new();
+ let mut replacements = BTreeMap::new();
+ let mut children = Vec::new();
+ for spaces in groups {
+ let head = spaces[0];
+ let old_id = series_by_head.get(&head).copied();
+ let (id, mut series) = if let Some(id) = old_id {
+ (id, PropertyObject::from_object(&raw.objects[&id])?)
+ } else {
+ let edit = selected
+ .get(&head)
+ .ok_or_else(|| invalid("A new series must start at an edited page"))?;
+ let id = ExGuid {
+ guid: edit.guid,
+ n: 1,
+ };
+ if raw.objects.contains_key(&id) {
+ return Err(invalid("The new page-series identity already exists"));
+ }
+ (
+ id,
+ PropertyObject {
+ jcid: 0x60008,
+ bytes: properties(&[(0x1c001c30, edit.guid.to_vec())])?,
+ global_ids: Arc::new(BTreeMap::from([(0, edit.guid)])),
+ },
+ )
+ };
+ children.push(id);
+ let membership_changed = old_id.is_none_or(|old| view.nodes[&old].spaces != spaces);
+ if !membership_changed
+ && !spaces.iter().any(|sid| levels[sid] != original_levels[sid])
+ {
+ continue;
+ }
+ let mut metadata_ids = Vec::new();
+ for sid in &spaces {
+ let page =
+ index.resolve(*sid, index.spaces[sid].labels[&(ExGuid::default(), 1)])?;
+ let page_id = page.roots[&2];
+ let original = PropertyObject::from_object(&page.objects[&page_id])?;
+ if old_id.is_none() && *sid == head {
+ series.copy_property(&original, 0x18001c65)?;
+ }
+ if levels[sid] != original_levels[sid] {
+ let mut metadata = PropertyObject::from_object(&page.objects[&page_id])?;
+ metadata.set(&[(0x14001dff, &levels[sid].to_le_bytes())])?;
+ changes.insert(
+ *sid,
+ RevisionEdit::Update(BTreeMap::from([(page_id, metadata)])),
+ );
+ }
+ let copy_id = copies
+ .get(sid)
+ .copied()
+ .unwrap_or_else(|| metadata_id(*sid));
+ let mut copy = match raw.objects.get(©_id) {
+ Some(object) => {
+ if object.jcid != 0x20030
+ || (!copies.contains_key(sid) && copy_ids.contains(©_id))
+ {
+ return Err(invalid("Page metadata identities overlap"));
+ }
+ PropertyObject::from_object(object)?
+ }
+ None => original,
+ };
+ copy.set(&[(0x14001dff, &levels[sid].to_le_bytes())])?;
+ copy.reference(copy_id)?;
+ if replacements.insert(copy_id, copy).is_some() {
+ return Err(invalid("Page metadata identities overlap"));
+ }
+ metadata_ids.push(copy_id);
+ }
+ if membership_changed || spaces.iter().any(|sid| !copies.contains_key(sid)) {
+ set_references(&mut series, 0x2c001d63, &spaces)?;
+ set_references(&mut series, 0x24003442, &metadata_ids)?;
+ if replacements.insert(id, series).is_some() {
+ return Err(invalid("Page-series and metadata identities overlap"));
+ }
+ }
+ }
+ if children != section_node.children {
+ let mut parent = PropertyObject::from_object(&raw.objects[§ion_id])?;
+ set_references(&mut parent, 0x24001c20, &children)?;
+ replacements.insert(section_id, parent);
+ }
+ changes.insert(document.root, RevisionEdit::Update(replacements));
+ Ok(changes)
+ })
+ }
+}
+
/// An empty top-level page with stable identities and creation time.
/// Retain the intent across retries; an existing page identity rejects duplicate creation.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -127,13 +447,7 @@ impl PageCreation {
.to_vec();
let modified = || (0x14001d7a, self.created.to_le_bytes().to_vec());
let mut table = BTreeMap::from([(0, self.guid)]);
- let mut metadata_id = id(1);
- for (byte, salt) in metadata_id.guid.iter_mut().zip([
- 0x31, 0xc0, 0xa8, 0x22, 0, 0x36, 0xee, 0x42, 0xb7, 0x14, 0xd7, 0xac, 0xda, 0x24,
- 0x35, 0xe8,
- ]) {
- *byte ^= salt;
- }
+ let metadata_id = metadata_id(self.space());
table.insert(1, metadata_id.guid);
let table = Arc::new(table);
let object = |jcid, values: Vec<_>| -> Result<_, Error> {
@@ -266,3 +580,228 @@ impl PageCreation {
})
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{RevisionIndex, Store};
+
+ #[test]
+ fn page_movement_restores_an_optional_metadata_cache_from_history() {
+ let source =
+ include_bytes!("../../../corpus/page-lifecycle/04-nested/notebook/Lifecycle.one");
+ let store = Store::parse(source).unwrap();
+ let index = RevisionIndex::parse(&store).unwrap();
+ let document = Document::parse(&index).unwrap();
+ let sid = document.pages().unwrap()[3].0;
+ let section = &document.spaces[&document.root];
+ let view = §ion.revisions[§ion.contexts[&ExGuid::default()]];
+ let series_id = view.nodes[&view.roots[&1]].children[3];
+ let copy_id = metadata_id(sid);
+ for unknown in [false, true] {
+ let source = if unknown {
+ write_revisions(source, |index| {
+ let raw = index.resolve(
+ index.root,
+ index.spaces[&index.root].labels[&(ExGuid::default(), 1)],
+ )?;
+ let mut copy = PropertyObject::from_object(&raw.objects[©_id])?;
+ copy.set(&[(0x1400abcd, &77_u32.to_le_bytes())])?;
+ Ok(BTreeMap::from([(
+ index.root,
+ RevisionEdit::Update(BTreeMap::from([(copy_id, copy)])),
+ )]))
+ })
+ .unwrap()
+ } else {
+ source.to_vec()
+ };
+ let source = write_revisions(&source, |index| {
+ let raw = index.resolve(
+ index.root,
+ index.spaces[&index.root].labels[&(ExGuid::default(), 1)],
+ )?;
+ let mut series = PropertyObject::from_object(&raw.objects[&series_id])?;
+ series.remove(&[0x24003442])?;
+ Ok(BTreeMap::from([(
+ index.root,
+ RevisionEdit::Update(BTreeMap::from([(series_id, series)])),
+ )]))
+ })
+ .unwrap();
+ for changed in [false, true] {
+ for unrelated_edit in [false, true] {
+ let result = write_revisions(&source, |index| {
+ let raw = index.resolve(
+ index.root,
+ index.spaces[&index.root].labels[&(ExGuid::default(), 1)],
+ )?;
+ assert!(!raw.reachable()?.contains(©_id));
+ let mut copy = PropertyObject::from_object(&raw.objects[©_id])?;
+ if changed {
+ copy.set(&[(0x14001dff, &2_u32.to_le_bytes())])?;
+ }
+ let mut objects = BTreeMap::from([(copy_id, copy)]);
+ if unrelated_edit {
+ let mut series = PropertyObject::from_object(&raw.objects[&series_id])?;
+ series.set(&[(0x1400abcd, &88_u32.to_le_bytes())])?;
+ objects.insert(series_id, series);
+ }
+ Ok(BTreeMap::from([(
+ index.root,
+ RevisionEdit::Update(objects),
+ )]))
+ });
+ assert_eq!(
+ result.unwrap_err().message,
+ "Edited object is not reachable in the resulting revision"
+ );
+ }
+ }
+ let written =
+ PageEdit::apply(&source, &[PageEdit::set_level(sid, 2).unwrap()]).unwrap();
+ let store = Store::parse(&written).unwrap();
+ let index = RevisionIndex::parse(&store).unwrap();
+ index.validate_current().unwrap();
+ assert_eq!(Document::parse(&index).unwrap().pages().unwrap().len(), 9);
+ let current = index
+ .resolve(
+ index.root,
+ index.spaces[&index.root].labels[&(ExGuid::default(), 1)],
+ )
+ .unwrap();
+ assert!(current.reachable().unwrap().contains(©_id));
+ let copy = PropertyObject::from_object(¤t.objects[©_id]).unwrap();
+ let properties = crate::PropertySets::parse(©.bytes).unwrap();
+ assert_eq!(unknown, properties.sets[0].iter().any(|field| field.id == 0x1400abcd
+ && matches!(field.value, crate::Value::Bytes(bytes) if bytes == 77_u32.to_le_bytes())));
+ let old_store = Store::parse(&source).unwrap();
+ let old_index = RevisionIndex::parse(&old_store).unwrap();
+ for (space, revisions) in &old_index.spaces {
+ for revision in revisions.revisions.keys() {
+ assert_eq!(
+ format!("{:?}", old_index.resolve(*space, *revision).unwrap()),
+ format!("{:?}", index.resolve(*space, *revision).unwrap())
+ );
+ }
+ }
+ if !unknown && let Some(output) = std::env::var_os("ONESTORE_PAGE_MOVEMENT_OUTPUT") {
+ let output = std::path::Path::new(&output);
+ assert!(output.is_absolute());
+ for (name, bytes) in [
+ ("optional-cache-source", source),
+ ("optional-cache", written),
+ ] {
+ let path = output.join(name);
+ std::fs::create_dir_all(&path).unwrap();
+ std::fs::write(path.join("Lifecycle.one"), bytes).unwrap();
+ }
+ }
+ }
+ }
+
+ #[test]
+ fn page_movement_rejects_empty_manifests_and_series() {
+ let source =
+ include_bytes!("../../../corpus/page-lifecycle/04-nested/notebook/Lifecycle.one");
+ let store = Store::parse(source).unwrap();
+ let index = RevisionIndex::parse(&store).unwrap();
+ let document = Document::parse(&index).unwrap();
+ let sid = document.pages().unwrap()[3].0;
+ for empty_series in [false, true] {
+ let invalid_source = write_revisions(source, |index| {
+ let target = if empty_series { index.root } else { sid };
+ let raw = index.resolve(
+ target,
+ index.spaces[&target].labels[&(ExGuid::default(), 1)],
+ )?;
+ let id = if empty_series {
+ let space = &document.spaces[&document.root];
+ let view = &space.revisions[&space.contexts[&ExGuid::default()]];
+ view.nodes[&view.roots[&1]].children[3]
+ } else {
+ raw.roots[&1]
+ };
+ let mut object = PropertyObject::from_object(&raw.objects[&id])?;
+ set_references(
+ &mut object,
+ if empty_series { 0x2c001d63 } else { 0x24001c1f },
+ &[],
+ )?;
+ Ok(BTreeMap::from([(
+ target,
+ RevisionEdit::Update(BTreeMap::from([(id, object)])),
+ )]))
+ })
+ .unwrap();
+ assert!(
+ PageEdit::apply(&invalid_source, &[PageEdit::set_level(sid, 2).unwrap()]).is_err()
+ );
+ }
+ }
+
+ #[test]
+ fn page_moves_retain_unknown_series_and_metadata_properties() {
+ let source =
+ include_bytes!("../../../corpus/page-lifecycle/04-nested/notebook/Lifecycle.one");
+ let store = Store::parse(source).unwrap();
+ let index = RevisionIndex::parse(&store).unwrap();
+ let document = Document::parse(&index).unwrap();
+ let pages = document.pages().unwrap();
+ let section = &document.spaces[&document.root];
+ let view = §ion.revisions[§ion.contexts[&ExGuid::default()]];
+ let series_id = view.nodes[&view.roots[&1]].children[3];
+ let field = view.nodes[&series_id].extra[0]
+ .iter()
+ .find(|f| f.id == 0x24003442)
+ .unwrap();
+ let FieldValue::Objects(copies) = &field.value else {
+ panic!()
+ };
+ let copy_id = copies[0];
+ let source = write_revisions(source, |index| {
+ let raw = index.resolve(
+ index.root,
+ index.spaces[&index.root].labels[&(ExGuid::default(), 1)],
+ )?;
+ let mut objects = BTreeMap::new();
+ for id in [series_id, copy_id] {
+ let mut object = PropertyObject::from_object(&raw.objects[&id])?;
+ object.set(&[(0x1400abcd, &77_u32.to_le_bytes())])?;
+ objects.insert(id, object);
+ }
+ Ok(BTreeMap::from([(
+ index.root,
+ RevisionEdit::Update(objects),
+ )]))
+ })
+ .unwrap();
+ for edits in [
+ vec![PageEdit::set_level(pages[3].0, 2).unwrap()],
+ pages[3..6]
+ .iter()
+ .enumerate()
+ .map(|(i, (sid, _))| PageEdit::move_to(*sid, None, i as u32 + 1).unwrap())
+ .collect(),
+ ] {
+ let written = PageEdit::apply(&source, &edits).unwrap();
+ let old_store = Store::parse(&source).unwrap();
+ let new_store = Store::parse(&written).unwrap();
+ let old = RevisionIndex::parse(&old_store).unwrap();
+ let new = RevisionIndex::parse(&new_store).unwrap();
+ let old_id = old.spaces[&old.root].labels[&(ExGuid::default(), 1)];
+ let new_id = new.spaces[&new.root].labels[&(ExGuid::default(), 1)];
+ assert_eq!(
+ format!("{:?}", old.resolve(old.root, old_id).unwrap()),
+ format!("{:?}", new.resolve(new.root, old_id).unwrap())
+ );
+ let current = new.resolve(new.root, new_id).unwrap();
+ for id in [series_id, copy_id] {
+ let object = PropertyObject::from_object(¤t.objects[&id]).unwrap();
+ let properties = crate::PropertySets::parse(&object.bytes).unwrap();
+ assert!(properties.sets[0].iter().any(|field| field.id == 0x1400abcd
+ && matches!(field.value, crate::Value::Bytes(bytes) if bytes == 77_u32.to_le_bytes())));
+ }
+ }
+ }
+}
diff --git a/crates/onestore/src/write.rs b/crates/onestore/src/write.rs
index 6f3b7b67ce8f5681d1597d3ef3245939a9c6490d..73af1ed174e0ac4f3e31968a01747c7bcc97b61d 100644
--- a/crates/onestore/src/write.rs
+++ b/crates/onestore/src/write.rs
@@ -720,12 +720,6 @@ pub(crate) fn write_revisions(
};
for (id, replacement) in &replacements {
if let Some(object) = revision.objects.get(id) {
- if !reachable.contains(id) {
- return Err(Error {
- offset: 0,
- message: "Object is not reachable in the active revision",
- });
- }
if object.jcid & 0x100000 != 0 {
return Err(Error {
offset: 0,
@@ -813,10 +807,12 @@ pub(crate) fn write_revisions(
}
}
replacements.retain(|id, replacement| {
- !revision.objects.get(id).is_some_and(|object| {
- object.data == ObjectData::Properties(&replacement.bytes)
- && object.global_ids == replacement.global_ids
- })
+ // Detached objects must pass the final reachability check even when unchanged.
+ !reachable.contains(id)
+ || !revision.objects.get(id).is_some_and(|object| {
+ object.data == ObjectData::Properties(&replacement.bytes)
+ && object.global_ids == replacement.global_ids
+ })
});
if replacements.is_empty() {
continue;
diff --git a/crates/onestore/tests/page_movement.rs b/crates/onestore/tests/page_movement.rs
new file mode 100644
index 0000000000000000000000000000000000000000..4c4871e16ca4a965a9cc647d86b9188194213ddb
--- /dev/null
+++ b/crates/onestore/tests/page_movement.rs
@@ -0,0 +1,476 @@
+use onestore::{
+ ExGuid, PageEdit, PreparedEdit, RevisionIndex, Store,
+ document::{Document, Kind},
+};
+
+#[path = "support/current.rs"]
+mod current;
+#[path = "support/disk.rs"]
+mod disk;
+
+const SOURCE: &[u8] =
+ include_bytes!("../../../corpus/page-lifecycle/04-nested/notebook/Lifecycle.one");
+
+fn pages(source: &[u8]) -> Vec {
+ let store = Store::parse(source).unwrap();
+ let index = RevisionIndex::parse(&store).unwrap();
+ Document::parse(&index)
+ .unwrap()
+ .pages()
+ .unwrap()
+ .into_iter()
+ .map(|(sid, _)| sid)
+ .collect()
+}
+
+fn export(name: &str, bytes: &[u8]) {
+ if let Some(output) = std::env::var_os("ONESTORE_PAGE_MOVEMENT_OUTPUT") {
+ assert!(
+ std::path::Path::new(&output).is_absolute(),
+ "Use an absolute fixture output directory"
+ );
+ let path = std::path::Path::new(&output).join(name);
+ std::fs::create_dir_all(&path).unwrap();
+ std::fs::write(path.join("Lifecycle.one"), bytes).unwrap();
+ }
+}
+
+fn verify(before: &[u8], after: &[u8], native: &[u8]) {
+ let stores = [before, after, native].map(|bytes| Store::parse(bytes).unwrap());
+ let indexes = stores
+ .each_ref()
+ .map(|store| RevisionIndex::parse(store).unwrap());
+ indexes[1].validate_current().unwrap();
+ let documents = indexes
+ .each_ref()
+ .map(|index| Document::parse(index).unwrap());
+ let expected = documents[2].pages().unwrap();
+ assert_eq!(documents[1].pages().unwrap(), expected);
+ for (sid, _) in &expected {
+ let levels = [&documents[1], &documents[2]].map(|document| {
+ let space = &document.spaces[sid];
+ let view = &space.revisions[&space.contexts[&ExGuid::default()]];
+ let Kind::Metadata { level, .. } = view.nodes[&view.roots[&2]].kind else {
+ panic!()
+ };
+ level
+ });
+ assert_eq!(levels[0], levels[1]);
+ }
+ let groups = [&documents[1], &documents[2]].map(|document| {
+ let space = &document.spaces[&document.root];
+ let view = &space.revisions[&space.contexts[&ExGuid::default()]];
+ view.nodes[&view.roots[&1]]
+ .children
+ .iter()
+ .map(|id| view.nodes[id].spaces.clone())
+ .collect::>()
+ });
+ assert_eq!(groups[0], groups[1]);
+ assert_eq!(
+ stores[1].header.transaction_count,
+ stores[0].header.transaction_count + 1
+ );
+ for (sid, space) in &indexes[0].spaces {
+ for rid in space.revisions.keys() {
+ assert_eq!(
+ format!("{:?}", indexes[0].resolve(*sid, *rid).unwrap()),
+ format!("{:?}", indexes[1].resolve(*sid, *rid).unwrap())
+ );
+ }
+ if *sid != indexes[0].root {
+ let old = indexes[0]
+ .resolve(*sid, space.labels[&(ExGuid::default(), 1)])
+ .unwrap();
+ let new = indexes[1]
+ .resolve(*sid, indexes[1].spaces[sid].labels[&(ExGuid::default(), 1)])
+ .unwrap();
+ assert_eq!(old.roots, new.roots);
+ assert_eq!(
+ old.objects.keys().collect::>(),
+ new.objects.keys().collect::>()
+ );
+ for (id, object) in &old.objects {
+ if *id != old.roots[&2] {
+ assert_eq!(format!("{object:?}"), format!("{:?}", new.objects[id]));
+ }
+ }
+ }
+ }
+ let mut previous = after.to_vec();
+ previous[96..100].copy_from_slice(&before[96..100]);
+ assert_eq!(current::current(&previous), current::current(before));
+}
+
+#[test]
+fn explicit_page_edits_match_native_selection_and_indentation() {
+ let ids = pages(SOURCE);
+ let level = |i, n| PageEdit::set_level(ids[i], n).unwrap();
+ let move_to =
+ |i, before: Option, n| PageEdit::move_to(ids[i], before.map(|i| ids[i]), n).unwrap();
+ let cases: [(&str, Vec, &[u8]); 8] = [
+ (
+ "01-demoted-parent",
+ vec![level(3, 2)],
+ include_bytes!(
+ "../../../corpus/page-lifecycle/movement/01-demoted-parent/notebook/Lifecycle.one"
+ ),
+ ),
+ (
+ "02-promoted-parent",
+ vec![level(3, 1)],
+ include_bytes!(
+ "../../../corpus/page-lifecycle/movement/02-promoted-parent/notebook/Lifecycle.one"
+ ),
+ ),
+ (
+ "03-selected-group-move",
+ vec![
+ move_to(3, None, 1),
+ move_to(4, None, 2),
+ move_to(5, None, 3),
+ ],
+ include_bytes!(
+ "../../../corpus/page-lifecycle/movement/03-selected-group-move/notebook/Lifecycle.one"
+ ),
+ ),
+ (
+ "04-subpage-move",
+ vec![move_to(4, Some(6), 1)],
+ include_bytes!(
+ "../../../corpus/page-lifecycle/movement/04-subpage-move/notebook/Lifecycle.one"
+ ),
+ ),
+ (
+ "05-insert-before-subpage",
+ vec![move_to(4, Some(5), 3)],
+ include_bytes!(
+ "../../../corpus/page-lifecycle/movement/05-insert-before-subpage/notebook/Lifecycle.one"
+ ),
+ ),
+ (
+ "06-promoted-level-two",
+ vec![level(4, 2)],
+ include_bytes!(
+ "../../../corpus/page-lifecycle/movement/06-promoted-level-two/notebook/Lifecycle.one"
+ ),
+ ),
+ (
+ "07-promoted-root",
+ vec![level(4, 1)],
+ include_bytes!(
+ "../../../corpus/page-lifecycle/movement/07-promoted-root/notebook/Lifecycle.one"
+ ),
+ ),
+ (
+ "08-collapsed-group-move",
+ vec![move_to(4, Some(0), 1), move_to(5, Some(0), 3)],
+ include_bytes!(
+ "../../../corpus/page-lifecycle/movement/08-collapsed-group-move/notebook/Lifecycle.one"
+ ),
+ ),
+ ];
+ let mut source = SOURCE.to_vec();
+ for (name, edits, native) in cases {
+ let serialized = serde_json::to_vec(&edits).unwrap();
+ let retained: Vec = serde_json::from_slice(&serialized).unwrap();
+ assert_eq!(edits, retained);
+ let prepared = PreparedEdit::pages(&source, &retained).unwrap();
+ verify(&source, prepared.as_bytes(), native);
+ assert_eq!(
+ PreparedEdit::pages(prepared.as_bytes(), &retained)
+ .unwrap()
+ .as_bytes(),
+ prepared.as_bytes()
+ );
+ export(name, prepared.as_bytes());
+ source = prepared.as_bytes().to_vec();
+ }
+ let edit = PageEdit::move_to(ids[3], None, 1).unwrap();
+ let single = PreparedEdit::pages(SOURCE, &[edit]).unwrap();
+ verify(
+ SOURCE,
+ single.as_bytes(),
+ include_bytes!(
+ "../../../corpus/page-lifecycle/movement/single-page/notebook/Lifecycle.one"
+ ),
+ );
+ export("single-page", single.as_bytes());
+}
+
+#[test]
+fn invalid_and_empty_page_edits_do_not_publish() {
+ let ids = pages(SOURCE);
+ for level in [0, 4, u32::MAX] {
+ assert!(PageEdit::set_level(ids[0], level).is_err());
+ }
+ assert!(PageEdit::move_to(ids[0], Some(ids[0]), 1).is_err());
+ assert!(PageEdit::set_level(ExGuid::default(), 1).is_err());
+ assert_eq!(PreparedEdit::pages(SOURCE, &[]).unwrap().as_bytes(), SOURCE);
+ assert_eq!(
+ PreparedEdit::pages(SOURCE, &[PageEdit::set_level(ids[0], 1).unwrap()])
+ .unwrap()
+ .as_bytes(),
+ SOURCE
+ );
+ assert!(PreparedEdit::pages(SOURCE, &[PageEdit::set_level(ids[0], 2).unwrap()]).is_err());
+ let duplicate = PageEdit::set_level(ids[3], 2).unwrap();
+ assert!(PreparedEdit::pages(SOURCE, &[duplicate.clone(), duplicate]).is_err());
+ let missing = ExGuid {
+ guid: [0x77; 16],
+ n: 1,
+ };
+ assert!(PreparedEdit::pages(SOURCE, &[PageEdit::set_level(missing, 1).unwrap()]).is_err());
+ assert!(
+ PreparedEdit::pages(
+ SOURCE,
+ &[PageEdit::move_to(ids[3], Some(missing), 1).unwrap()]
+ )
+ .is_err()
+ );
+ let mut value = serde_json::to_value(PageEdit::set_level(ids[3], 2).unwrap()).unwrap();
+ value["level"] = 4.into();
+ let invalid: PageEdit = serde_json::from_value(value.clone()).unwrap();
+ assert!(PreparedEdit::pages(SOURCE, &[invalid]).is_err());
+ value["unknown"] = true.into();
+ assert!(serde_json::from_value::(value).is_err());
+}
+
+#[test]
+fn moving_pages_preserves_ink_file_data_and_native_feature_objects() {
+ for source in [
+ include_bytes!("../../../corpus/native-ink/20260905-ui/notebook/synthetic.one").as_slice(),
+ include_bytes!("../../../corpus/native-external-assets/notebook/synthetic.one").as_slice(),
+ include_bytes!("../../../corpus/m6/native-features-01/notebook/Features.one").as_slice(),
+ ] {
+ let first = pages(source)[0];
+ let created =
+ onestore::PageCreation::new(Some(first), Some("Movement anchor"), "Author").unwrap();
+ let source = PreparedEdit::create_page(source, &created)
+ .unwrap()
+ .as_bytes()
+ .to_vec();
+ let moved =
+ PreparedEdit::pages(&source, &[PageEdit::move_to(first, None, 1).unwrap()]).unwrap();
+ assert_eq!(pages(moved.as_bytes()).last(), Some(&first));
+ let stores = [&source, moved.as_bytes()].map(|bytes| Store::parse(bytes).unwrap());
+ let indexes = stores
+ .each_ref()
+ .map(|store| RevisionIndex::parse(store).unwrap());
+ for (sid, space) in &indexes[0].spaces {
+ for rid in space.revisions.keys() {
+ assert_eq!(
+ format!("{:?}", indexes[0].resolve(*sid, *rid).unwrap()),
+ format!("{:?}", indexes[1].resolve(*sid, *rid).unwrap())
+ );
+ }
+ if *sid != indexes[0].root {
+ assert_eq!(space.labels, indexes[1].spaces[sid].labels);
+ let revision = indexes[0]
+ .resolve(*sid, space.labels[&(ExGuid::default(), 1)])
+ .unwrap();
+ for object in revision.objects.values() {
+ if let Some(onestore::FileDataReference::Internal(guid)) =
+ object.file_reference().unwrap()
+ {
+ assert_eq!(
+ stores[0].file_data(guid).unwrap(),
+ stores[1].file_data(guid).unwrap()
+ );
+ }
+ }
+ }
+ }
+ }
+}
+
+#[test]
+fn metadata_copy_order_does_not_change_page_identity() {
+ let source = include_bytes!(
+ "../../../corpus/page-lifecycle/page-edits/optional-cache/source-cold/notebook/Lifecycle.one"
+ );
+ let sid = pages(source)[3];
+ let edited = PreparedEdit::pages(source, &[PageEdit::set_level(sid, 2).unwrap()]).unwrap();
+ let expected = include_bytes!(
+ "../../../corpus/page-lifecycle/page-edits/optional-cache/candidate/Lifecycle.one"
+ );
+ export("reordered-metadata", edited.as_bytes());
+ verify(source, edited.as_bytes(), expected);
+ let stores = [edited.as_bytes(), expected.as_slice()].map(|b| Store::parse(b).unwrap());
+ let indexes = stores.each_ref().map(|s| RevisionIndex::parse(s).unwrap());
+ let documents = indexes.each_ref().map(|i| Document::parse(i).unwrap());
+ for sid in documents[0].spaces.keys() {
+ let nodes = documents.each_ref().map(|d| {
+ let space = &d.spaces[sid];
+ serde_json::to_value(&space.revisions[&space.contexts[&ExGuid::default()]].nodes)
+ .unwrap()
+ });
+ assert_eq!(nodes[0], nodes[1], "{sid}");
+ }
+}
+
+#[test]
+fn native_edits_on_moved_pages_support_more_rust_changes() {
+ let source = include_bytes!(
+ "../../../corpus/page-lifecycle/page-edits/native/cold/notebook/Lifecycle.one"
+ );
+ let store = Store::parse(source).unwrap();
+ let index = RevisionIndex::parse(&store).unwrap();
+ let document = Document::parse(&index).unwrap();
+ let (sid, _) = *document.pages().unwrap().last().unwrap();
+ let space = &document.spaces[&sid];
+ let view = &space.revisions[&space.contexts[&ExGuid::default()]];
+ assert!(
+ matches!(&view.nodes[&view.roots[&2]].kind, Kind::Metadata { title: Some(title), level: Some(1) } if title == "Native moved 🦋 é")
+ );
+ let bodies: Vec<_> = view.nodes.iter().filter_map(|(id, node)|
+ matches!(&node.kind, Kind::RichText { text, .. } if text == "Native body after a Rust page move.").then_some(*id)).collect();
+ let [body] = bodies.as_slice() else { panic!() };
+ let moved = PreparedEdit::pages(source, &[PageEdit::set_level(sid, 2).unwrap()]).unwrap();
+ let edited = PreparedEdit::text(moved.as_bytes(), sid, *body, 0..0, "Rust + ").unwrap();
+ let after_store = Store::parse(edited.as_bytes()).unwrap();
+ let after_index = RevisionIndex::parse(&after_store).unwrap();
+ let after = Document::parse(&after_index).unwrap();
+ assert_eq!(after.pages().unwrap(), document.pages().unwrap());
+ let space = &after.spaces[&sid];
+ let view = &space.revisions[&space.contexts[&ExGuid::default()]];
+ assert!(
+ matches!(&view.nodes[&view.roots[&2]].kind, Kind::Metadata { title: Some(title), level: Some(2) } if title == "Native moved 🦋 é")
+ );
+ assert!(
+ matches!(&view.nodes[body].kind, Kind::RichText { text, .. } if text == "Rust + Native body after a Rust page move.")
+ );
+ for (sid, space) in &index.spaces {
+ for rid in space.revisions.keys() {
+ assert_eq!(
+ format!("{:?}", index.resolve(*sid, *rid).unwrap()),
+ format!("{:?}", after_index.resolve(*sid, *rid).unwrap())
+ );
+ }
+ }
+ export("rust-followup", edited.as_bytes());
+}
+
+#[test]
+fn grouped_page_levels_and_membership_survive_each_storage_interruption() {
+ let mut source = onestore::create_section("movement.one", "Original", "Author").unwrap();
+ for _ in 0..2 {
+ let page = onestore::PageCreation::new(None, Some("Same title"), "Author").unwrap();
+ source = PreparedEdit::create_page(&source, &page)
+ .unwrap()
+ .as_bytes()
+ .to_vec();
+ }
+ for (source, first) in [
+ (source.as_slice(), 1),
+ (
+ include_bytes!(
+ "../../../corpus/page-lifecycle/page-edits/optional-cache/source/Lifecycle.one"
+ )
+ .as_slice(),
+ 3,
+ ),
+ ] {
+ let ids = pages(source);
+ let edits = [
+ PageEdit::set_level(ids[first], 2).unwrap(),
+ PageEdit::set_level(ids[first + 1], 3).unwrap(),
+ ];
+ let prepared = PreparedEdit::pages(source, &edits).unwrap();
+ let old = current::current(source);
+ let new = current::current(prepared.as_bytes());
+ assert_eq!(new.len(), old.len());
+ for write_limit in [1, 17, 4096] {
+ let mut complete = disk::Disk {
+ visible: source.to_vec(),
+ durable: source.to_vec(),
+ operation: 0,
+ fail_at: None,
+ write_limit,
+ random: 1957,
+ };
+ prepared.commit(&mut complete).unwrap();
+ assert_eq!(complete.durable, prepared.as_bytes());
+ for fail_at in 1..=complete.operation {
+ let mut interrupted = disk::Disk {
+ visible: source.to_vec(),
+ durable: source.to_vec(),
+ operation: 0,
+ fail_at: Some(fail_at),
+ write_limit,
+ random: 1957 + fail_at as u64,
+ };
+ prepared.commit(&mut interrupted).unwrap_err();
+ let observed = current::current(&interrupted.durable);
+ assert!(
+ observed == old || observed == new,
+ "{write_limit}:{fail_at}"
+ );
+ }
+ }
+ }
+}
+
+#[test]
+fn repeated_nesting_retains_series_history_across_revision_checkpoints() {
+ let mut source = onestore::create_section("movement.one", "Original", "Author").unwrap();
+ let page = onestore::PageCreation::new(None, Some("Child"), "Author").unwrap();
+ source = PreparedEdit::create_page(&source, &page)
+ .unwrap()
+ .as_bytes()
+ .to_vec();
+ let initial = source.clone();
+ for step in 0..520 {
+ let level = if step % 2 == 0 { 2 } else { 1 };
+ let edit = PageEdit::set_level(page.space(), level).unwrap();
+ let written = PreparedEdit::pages(&source, &[edit])
+ .unwrap()
+ .as_bytes()
+ .to_vec();
+ let store = Store::parse(&written).unwrap();
+ let index = RevisionIndex::parse(&store).unwrap();
+ index.validate_current().unwrap();
+ let document = Document::parse(&index).unwrap();
+ assert_eq!(document.pages().unwrap().len(), 2);
+ let space = &document.spaces[&page.space()];
+ let view = &space.revisions[&space.contexts[&ExGuid::default()]];
+ assert!(
+ matches!(view.nodes[&view.roots[&2]].kind, Kind::Metadata { level: Some(n), .. } if n == level)
+ );
+ if step % 64 == 0 || matches!(step, 255 | 511 | 519) {
+ let mut old = written.clone();
+ old[96..100].copy_from_slice(&source[96..100]);
+ assert_eq!(current::current(&old), current::current(&source));
+ }
+ source = written;
+ }
+ let stores = [&initial, &source].map(|bytes| Store::parse(bytes).unwrap());
+ let indexes = stores
+ .each_ref()
+ .map(|store| RevisionIndex::parse(store).unwrap());
+ assert_eq!(
+ stores[1].header.transaction_count,
+ stores[0].header.transaction_count + 520
+ );
+ for (sid, space) in &indexes[0].spaces {
+ for rid in space.revisions.keys() {
+ assert_eq!(
+ format!("{:?}", indexes[0].resolve(*sid, *rid).unwrap()),
+ format!("{:?}", indexes[1].resolve(*sid, *rid).unwrap())
+ );
+ }
+ let current = &indexes[1].spaces[sid];
+ let rid = current.labels[&(ExGuid::default(), 1)];
+ assert!(
+ std::iter::successors(Some(rid), |rid| current.revisions[rid].dependency).count() < 20
+ );
+ }
+ if let Some(output) = std::env::var_os("ONESTORE_PAGE_MOVEMENT_STRESS_OUTPUT") {
+ assert!(
+ std::path::Path::new(&output).is_absolute(),
+ "Use an absolute fixture output directory"
+ );
+ std::fs::create_dir(&output).unwrap();
+ std::fs::write(std::path::Path::new(&output).join("movement.one"), &source).unwrap();
+ }
+}
diff --git a/fuzz/fuzz_targets/page.rs b/fuzz/fuzz_targets/page.rs
index 36b68f756092379a905784e3ec5a61597cf04b08..39cc235be42d27e7d8fc73904e14ab3145e2c2e9 100644
--- a/fuzz/fuzz_targets/page.rs
+++ b/fuzz/fuzz_targets/page.rs
@@ -1,10 +1,10 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use onestore::{
- CommitState, ExGuid, Insertion, PageCreation, PreparedEdit, RevisionIndex, Store,
- document::{Document, Kind},
+ CommitState, ExGuid, Insertion, PageCreation, PageEdit, PreparedEdit, RevisionIndex, Store,
+ document::{Document, FieldValue, Kind},
};
-use std::sync::LazyLock;
+use std::{collections::BTreeMap, sync::LazyLock};
#[path = "../../crates/onestore/tests/support/current.rs"]
mod current;
@@ -16,13 +16,25 @@ static SOURCE: LazyLock> = LazyLock::new(|| {
});
fuzz_target!(|input: &[u8]| {
+ let source = match input.first().copied().unwrap_or(0) % 3 {
+ 1 => include_bytes!("../../corpus/page-lifecycle/page-edits/optional-cache/source/Lifecycle.one")
+ .as_slice(),
+ 2 => include_bytes!("../../corpus/page-lifecycle/page-edits/optional-cache/source-cold/notebook/Lifecycle.one")
+ .as_slice(),
+ _ => SOURCE.as_slice(),
+ };
if let Ok(intent) = serde_json::from_slice::(input)
- && let Ok(prepared) = PreparedEdit::create_page(&SOURCE, &intent)
+ && let Ok(prepared) = PreparedEdit::create_page(source, &intent)
{
current::current(prepared.as_bytes());
}
- let mut persisted = SOURCE.clone();
- let mut caches = std::array::from_fn::<_, 12, _>(|_| SOURCE.clone());
+ if let Ok(edits) = serde_json::from_slice::>(input)
+ && let Ok(prepared) = PreparedEdit::pages(source, &edits)
+ {
+ current::current(prepared.as_bytes());
+ }
+ let mut persisted = source.to_vec();
+ let mut caches = std::array::from_fn::<_, 12, _>(|_| source.to_vec());
for step in input.chunks_exact(8).take(24) {
let actor = usize::from(step[0]) % caches.len();
if step[1] % 3 == 0 {
@@ -33,6 +45,17 @@ fuzz_target!(|input: &[u8]| {
let index = RevisionIndex::parse(&store).unwrap();
let document = Document::parse(&index).unwrap();
let mut pages = document.pages().unwrap();
+ let mut levels: BTreeMap<_, _> = pages
+ .iter()
+ .map(|(sid, _)| {
+ let space = &document.spaces[sid];
+ let view = &space.revisions[&space.contexts[&ExGuid::default()]];
+ let Kind::Metadata { level, .. } = view.nodes[&view.roots[&2]].kind else {
+ panic!()
+ };
+ (*sid, level.unwrap_or(1))
+ })
+ .collect();
let (sid, page) = pages[usize::from(step[2]) % pages.len()];
let space = &document.spaces[&sid];
let view = &space.revisions[&space.contexts[&ExGuid::default()]];
@@ -52,7 +75,41 @@ fuzz_target!(|input: &[u8]| {
.then_some(*id)
})
.collect();
- let (edit, title_update) = if step[1] & 8 != 0 && !titles.is_empty() {
+ let (edit, title_update) = if step[1] & 32 != 0 {
+ let count = 1 + usize::from(step[3] & 128 != 0 && pages.len() > 1);
+ let selected: Vec<_> = (0..count)
+ .map(|i| pages[(usize::from(step[2]) + i) % pages.len()].0)
+ .collect();
+ let before = (step[3] & 4 != 0)
+ .then_some(pages[usize::from(step[7]) % pages.len()].0)
+ .filter(|id| !selected.contains(id));
+ let mut edits = Vec::new();
+ for (ordinal, sid) in selected.iter().enumerate() {
+ let level = u32::from(if ordinal == 0 { step[3] } else { step[6] }) % 3 + 1;
+ levels.insert(*sid, level);
+ let edit = if step[3] & 8 == 0 {
+ PageEdit::set_level(*sid, level).unwrap()
+ } else {
+ let at = pages.iter().position(|p| p.0 == *sid).unwrap();
+ let page = pages.remove(at);
+ let at = before.map_or(pages.len(), |id| {
+ pages.iter().position(|p| p.0 == id).unwrap()
+ });
+ pages.insert(at, page);
+ PageEdit::move_to(*sid, before, level).unwrap()
+ };
+ edits.push(edit);
+ }
+ let restored: Vec =
+ serde_json::from_value(serde_json::to_value(&edits).unwrap()).unwrap();
+ assert_eq!(edits, restored);
+ let prepared = PreparedEdit::pages(source, &restored);
+ if levels[&pages[0].0] != 1 {
+ assert!(prepared.is_err());
+ continue;
+ }
+ (prepared.unwrap(), None)
+ } else if step[1] & 8 != 0 && !titles.is_empty() {
let object = titles[0];
let Kind::RichText { text: original, .. } = &view.nodes[&object].kind else {
unreachable!()
@@ -75,7 +132,16 @@ fuzz_target!(|input: &[u8]| {
Some((sid, intent.text_object(), text)),
)
} else {
- let before = (step[2] & 1 != 0).then_some(sid);
+ let section = &document.spaces[&document.root];
+ let section = §ion.revisions[§ion.contexts[&ExGuid::default()]];
+ let head = section.nodes[§ion.roots[&1]]
+ .children
+ .iter()
+ .map(|id| §ion.nodes[id])
+ .find(|series| series.spaces.contains(&sid))
+ .unwrap()
+ .spaces[0];
+ let before = (step[2] & 1 != 0).then_some(head);
let title = (step[3] & 4 == 0).then_some(text);
let intent = PageCreation::new(before, title, "Page fuzz").unwrap();
let restored = serde_json::from_value(serde_json::to_value(&intent).unwrap()).unwrap();
@@ -85,6 +151,7 @@ fuzz_target!(|input: &[u8]| {
pages.iter().position(|p| p.0 == sid).unwrap()
});
pages.insert(position, (intent.space(), intent.object()));
+ levels.insert(intent.space(), 1);
(
edit,
intent
@@ -96,6 +163,40 @@ fuzz_target!(|input: &[u8]| {
let after_index = RevisionIndex::parse(&after_store).unwrap();
let after_document = Document::parse(&after_index).unwrap();
assert_eq!(after_document.pages().unwrap(), pages);
+ let mut metadata_levels = BTreeMap::new();
+ for (sid, _) in &pages {
+ let space = &after_document.spaces[sid];
+ let view = &space.revisions[&space.contexts[&ExGuid::default()]];
+ let Kind::Metadata { level, .. } = view.nodes[&view.roots[&2]].kind else {
+ panic!()
+ };
+ assert_eq!(level.unwrap_or(1), levels[sid]);
+ let metadata = &view.nodes[&view.roots[&2]];
+ let FieldValue::Bytes(guid) = metadata.extra[0]
+ .iter()
+ .find(|field| field.id == 0x1c001c30)
+ .unwrap()
+ .value
+ else {
+ panic!()
+ };
+ assert!(metadata_levels.insert(guid, levels[sid]).is_none());
+ }
+ let section = &after_document.spaces[&after_document.root];
+ let view = §ion.revisions[§ion.contexts[&ExGuid::default()]];
+ for metadata in view.nodes.values() {
+ if let Kind::Metadata { level, .. } = metadata.kind {
+ let FieldValue::Bytes(guid) = metadata.extra[0]
+ .iter()
+ .find(|field| field.id == 0x1c001c30)
+ .unwrap()
+ .value
+ else {
+ panic!()
+ };
+ assert_eq!(metadata_levels.get(guid), Some(&level.unwrap_or(1)));
+ }
+ }
if let Some((sid, object, text)) = title_update {
let space = &after_document.spaces[&sid];
let view = &space.revisions[&space.contexts[&ExGuid::default()]];
diff --git a/tools/native/moved-pages.ps1 b/tools/native/moved-pages.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..b52fc3b97e52b452c99e27a7fa9ce52d2d6fcd29
--- /dev/null
+++ b/tools/native/moved-pages.ps1
@@ -0,0 +1,33 @@
+param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost)
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+& "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost
+$app = New-Object -ComObject OneNote.Application
+$notebook = ''
+try {
+ $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebook, 0)
+ $section = ''
+ $app.OpenHierarchy('Lifecycle.one', $notebook, [ref]$section, 0)
+ $deadline = [DateTime]::UtcNow.AddSeconds(30)
+ do {
+ $hierarchy = ''
+ $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
+ [xml]$tree = $hierarchy
+ $pages = @($tree.SelectNodes('//*[local-name()="Page"]'))
+ if ($pages.Count -eq 9) { break }
+ Start-Sleep -Milliseconds 200
+ } while ([DateTime]::UtcNow -lt $deadline)
+ $targets = @($pages | Where-Object { $_.GetAttribute('name') -eq 'Same title' })
+ if ($pages.Count -ne 9 -or $targets.Count -ne 1) { throw 'Expected nine pages and one moved parent.' }
+ $pageId = [Security.SecurityElement]::Escape($targets[0].GetAttribute('ID'))
+ $title = 'Native moved ' + [char]::ConvertFromUtf32(0x1F98B) + ' e' + [char]0x301
+ $title = [Security.SecurityElement]::Escape($title)
+ $xml = "$titleNative body after a Rust page move."
+ [IO.File]::WriteAllText((Join-Path $Root 'native-edit.xml'), $xml, [Text.Encoding]::UTF8)
+ $app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $false)
+ $app.SyncHierarchy($notebook)
+} finally {
+ [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+}
diff --git a/tools/test_page_movement.py b/tools/test_page_movement.py
index 1c71db44a10f26937f6f2037ca5d9007c2d019a4..99822fd6091340b086d06ad79c1563700a81e5a1 100644
--- a/tools/test_page_movement.py
+++ b/tools/test_page_movement.py
@@ -12,6 +12,54 @@ FIXTURE = Path(__file__).resolve().parent.parent / 'corpus/page-lifecycle'
class PageMovementTest(unittest.TestCase):
+ def test_optional_metadata_restoration_matches_native(self):
+ fixture = FIXTURE / 'page-edits/optional-cache'
+ for source, cold in [('source', 'source-cold'), ('candidate', 'cold'),
+ ('followup/candidate', 'followup/cold')]:
+ with self.subTest(source=source):
+ self.assertEqual(compare(fixture / source, fixture / cold), 0)
+ if source != 'source':
+ self.assertEqual((fixture / source / 'Lifecycle.one').read_bytes()[1024:],
+ (fixture / cold / 'notebook/Lifecycle.one').read_bytes()[1024:])
+
+ def test_native_and_rust_edits_preserve_moved_pages(self):
+ fixture = FIXTURE / 'page-edits'
+ with TemporaryDirectory() as temporary:
+ temporary = Path(temporary)
+ subprocess.run([EXPORTER, fixture / '08-collapsed-group-move/candidate/Lifecycle.one', temporary / 'original'], check=True)
+ original = list(ordered_pages(json.loads((temporary / 'original/document.json').read_text())))
+ for ordinal, (source, cold) in enumerate([
+ ('native/notebook', 'native/cold'), ('rust-followup/candidate', 'rust-followup/cold'),
+ ]):
+ self.assertEqual(compare(fixture / source, fixture / cold), 0)
+ output = temporary / str(ordinal)
+ subprocess.run([EXPORTER, fixture / cold / 'notebook/Lifecycle.one', output], check=True)
+ pages = list(ordered_pages(json.loads((output / 'document.json').read_text())))
+ self.assertEqual([(sid, page) for sid, _, _, page in pages],
+ [(sid, page) for sid, _, _, page in original])
+ for i, ((_, _, revision, page), (_, _, old, old_page)) in enumerate(zip(pages, original, strict=True)):
+ expected = [node['kind']['text'] for _, node in walk(old, old_page) if node['kind']['type'] == 'RichText']
+ if i == 8:
+ expected[0] = 'Native moved 🦋 é'
+ expected.append(('Rust + ' if ordinal else '') + 'Native body after a Rust page move.')
+ self.assertEqual(revision['nodes'][revision['roots']['2']]['kind'],
+ {'type': 'Metadata', 'title': expected[0], 'level': ordinal + 1})
+ self.assertEqual([node['kind']['text'] for _, node in walk(revision, page) if node['kind']['type'] == 'RichText'], expected)
+
+ def test_rust_page_edits_survive_native_reopen_without_repairs(self):
+ for phase in [phase for phase, *_ in PHASES] + ['single-page']:
+ with self.subTest(phase=phase):
+ fixture = FIXTURE / 'page-edits' / phase
+ self.assertEqual((fixture / 'candidate/Lifecycle.one').read_bytes()[1024:],
+ (fixture / 'cold/notebook/Lifecycle.one').read_bytes()[1024:])
+ self.assertEqual(compare(fixture / 'candidate', fixture / 'cold'), 0)
+
+ def test_repeated_page_nesting_survives_native_reopen_after_checkpoints(self):
+ fixture = FIXTURE / 'page-edits/stress'
+ self.assertEqual((fixture / 'candidate/movement.one').read_bytes()[1024:],
+ (fixture / 'cold/notebook/movement.one').read_bytes()[1024:])
+ self.assertEqual(compare(fixture / 'candidate', fixture / 'cold'), 0)
+
def test_native_selection_indentation_and_order_survive_cold_reopen(self):
with TemporaryDirectory() as temporary:
temporary = Path(temporary)
diff --git a/tools/verify_page_creation.py b/tools/verify_page_creation.py
index f94c595cb54a052ae7f9eafd8f41a633a5655067..9813c07af8c3a947ecc610889053c49fa960a4ca 100644
--- a/tools/verify_page_creation.py
+++ b/tools/verify_page_creation.py
@@ -32,6 +32,13 @@ def compare(source, capture, *, refresh_metadata_levels=False):
models.append(json.loads((output / 'document.json').read_text()))
before, after = models
pages = list(ordered_pages(before))
+ metadata = {}
+ for page_sid, _, page_view, _ in pages:
+ # Native uses the space GUID when it differs from the page-node GUID.
+ guid = UUID(page_sid.split('}')[0].strip('{')).bytes_le
+ salt = UUID('22a8c031-3600-42ee-b714-d7acda2435e8').bytes_le
+ oid = '{' + str(UUID(bytes_le=bytes(a ^ b for a, b in zip(guid, salt, strict=True)))).upper() + '},1'
+ metadata[oid] = (page_sid, page_view['nodes'][page_view['roots']['2']])
assert [(sid, page) for sid, _, _, page in pages] == [
(sid, page) for sid, _, _, page in ordered_pages(after)]
assert before['root'] == after['root']
@@ -47,27 +54,25 @@ def compare(source, capture, *, refresh_metadata_levels=False):
existing = [p for p in series['extra'][0] if p['id'] == 0x24003442]
added = [p for p in observed['nodes'][series_id]['extra'][0] if p['id'] == 0x24003442]
if existing and refresh_metadata_levels:
- for copy, page_sid in zip(existing[0]['value']['Objects'], series['spaces'], strict=True):
- (_, _, page_view, _), = [p for p in pages if p[0] == page_sid]
- metadata = expected[copy]['kind']
- assert metadata['type'] == 'Metadata'
- level = page_view['nodes'][page_view['roots']['2']]['kind']['level']
- refreshed += metadata['level'] != level
- metadata['level'] = level
+ for copy in existing[0]['value']['Objects']:
+ cached = expected[copy]['kind']
+ assert cached['type'] == 'Metadata'
+ level = metadata[copy][1]['kind']['level']
+ refreshed += cached['level'] != level
+ cached['level'] = level
if existing or not added:
continue
copies = []
- for page_sid in series['spaces']:
- (_, _, page_view, _), = [p for p in pages if p[0] == page_sid]
- # Native uses the space GUID when it differs from the page-node GUID.
- guid = UUID(page_sid.split('}')[0].strip('{')).bytes_le
- salt = UUID('22a8c031-3600-42ee-b714-d7acda2435e8').bytes_le
- metadata = '{' + str(UUID(bytes_le=bytes(a ^ b for a, b in zip(guid, salt, strict=True)))).upper() + '},1'
- assert metadata not in expected
- expected[metadata] = page_view['nodes'][page_view['roots']['2']]
- copies.append(metadata)
+ for oid, (page_sid, node) in metadata.items():
+ if page_sid not in series['spaces']:
+ continue
+ assert oid not in expected
+ expected[oid] = node
+ copies.append(oid)
filled += 1
- series['extra'][0].append({'id': 0x24003442, 'value': {'Objects': copies}})
+ assert len(added) == 1
+ assert sorted(copies) == sorted(added[0]['value']['Objects'])
+ series['extra'][0].append(added[0])
assert original['roots'] == observed['roots']
assert expected == observed['nodes'], sid
print(f'Passed: {len(pages)} ordered pages and preserved active graphs; {filled} missing section metadata copies filled natively; {refreshed} section metadata levels refreshed')