diff --git a/corpus/outline-edit/README.md b/corpus/outline-edit/README.md
index 8079748ea71c25f764ac72b539e8158dd01d5497..303d26d450ddfd45c810d2209f9f9950073c7086 100644
--- a/corpus/outline-edit/README.md
+++ b/corpus/outline-edit/README.md
@@ -60,3 +60,19 @@ all 304 active graph objects, ten tags and 8,457 explicit formatting comparisons
To regenerate, run `resizing_a_native_reserved_width_preserves_content` with
`ONESTORE_RESERVED_WIDTH_OUTPUT` set to a new absolute directory, then cold-open
it with the same runner arguments.
+
+`offline` reconciles local layout/default intents and later text edits against all
+fourteen native changes. Five merge automatically, five require explicit review,
+and four deleted targets retain their local branches and dependent queues. The ten
+successful cases produce eighteen publications and twenty durable receipts; the
+already-converged collapse defaults require confirmation without another append.
+The final cold capture preserves all 304 active graph objects, ten tags, and 8,937
+explicit formatting comparisons. The `cases.json` records stable targets and the
+retained/dependent outcomes used by the public test.
+
+Regenerate by running the offline sync test
+`native_moves_deletions_and_layout_changes_merge_or_retain_explicit_conflicts`
+with `ONESTORE_OFFLINE_OUTLINE_OUTPUT` set to a new absolute directory, then use
+the same cold runner arguments. The test also retains a JSON record next to that
+directory. Cache migration, uncertain revision retention, stale review, and twelve
+offline writers are exercised in `crates/onestore-offline/tests`.
diff --git a/corpus/outline-edit/offline/candidate/synthetic.one b/corpus/outline-edit/offline/candidate/synthetic.one
new file mode 100644
index 0000000000000000000000000000000000000000..72a2843936fe40a397370ea1bc5d68802ee8b425
Binary files /dev/null and b/corpus/outline-edit/offline/candidate/synthetic.one differ
diff --git a/corpus/outline-edit/offline/cases.json b/corpus/outline-edit/offline/cases.json
new file mode 100644
index 0000000000000000000000000000000000000000..40623c317bc500504538eb48cc5f9437477aa7c6
--- /dev/null
+++ b/corpus/outline-edit/offline/cases.json
@@ -0,0 +1,154 @@
+[
+ {
+ "change": {
+ "Collapsed": true
+ },
+ "dependent_text": "{033CC036-70D6-02D3-2869-6264552F7B9E},64",
+ "name": "Move leaf down",
+ "object": "{033CC036-70D6-02D3-2869-6264552F7B9E},45",
+ "retained": false,
+ "space": "{B7267338-BF83-0893-27A7-37F606DD20C8},1"
+ },
+ {
+ "change": {
+ "Collapsed": true
+ },
+ "dependent_text": "{E79A0442-BFCF-04C6-1E04-364B68877B88},75",
+ "name": "Move subtree down",
+ "object": "{E79A0442-BFCF-04C6-1E04-364B68877B88},45",
+ "retained": false,
+ "space": "{9B80D7CA-18B4-02B0-1E85-6E8AA0DAD827},1"
+ },
+ {
+ "change": {
+ "Collapsed": true
+ },
+ "dependent_text": "{3F47069A-F0C5-053E-1DEC-13B552EEE2A0},75",
+ "name": "Move subtree up",
+ "object": "{3F47069A-F0C5-053E-1DEC-13B552EEE2A0},45",
+ "retained": false,
+ "space": "{7538095C-389B-07D8-2991-B2BC46EBD52A},1"
+ },
+ {
+ "change": {
+ "Collapsed": true
+ },
+ "dependent_text": "{5A89330E-0803-08CF-2BF5-26621F998039},75",
+ "name": "Indent subtree",
+ "object": "{5A89330E-0803-08CF-2BF5-26621F998039},45",
+ "retained": false,
+ "space": "{651463E3-C768-0C93-363D-FCE642B07408},1"
+ },
+ {
+ "change": {
+ "Collapsed": true
+ },
+ "dependent_text": "{1A8D8A0B-E96E-01B0-3DA2-EF182AA19042},75",
+ "name": "Outdent subtree",
+ "object": "{1A8D8A0B-E96E-01B0-3DA2-EF182AA19042},45",
+ "retained": false,
+ "space": "{8384083C-60ED-0F7C-2F15-8B20B878B3EB},1"
+ },
+ {
+ "change": {
+ "Collapsed": true
+ },
+ "dependent_text": "{7375C8D1-28F0-0B05-00E5-093957D7F5F1},75",
+ "name": "Collapse subtree",
+ "object": "{7375C8D1-28F0-0B05-00E5-093957D7F5F1},45",
+ "retained": false,
+ "space": "{FF88B513-029C-00E2-0082-9E3DCE1D7796},1"
+ },
+ {
+ "change": {
+ "Collapsed": false
+ },
+ "dependent_text": "{B11DD73E-4234-04EF-0F4D-1F67E8F92C22},75",
+ "name": "Expand subtree",
+ "object": "{B11DD73E-4234-04EF-0F4D-1F67E8F92C22},45",
+ "retained": false,
+ "space": "{850B34E4-D62A-042B-138A-B2A905CEE28E},1"
+ },
+ {
+ "change": {
+ "Collapsed": true
+ },
+ "dependent_text": "{DC976FEC-CD30-0C41-20AD-BB462A203464},64",
+ "name": "Delete leaf",
+ "object": "{DC976FEC-CD30-0C41-20AD-BB462A203464},45",
+ "retained": true,
+ "space": "{363FE055-44E1-0B27-14A8-46278FD681FF},1"
+ },
+ {
+ "change": {
+ "Collapsed": true
+ },
+ "dependent_text": "{73987DA5-C014-0EEA-00F8-5C1E149736E4},75",
+ "name": "Delete subtree",
+ "object": "{73987DA5-C014-0EEA-00F8-5C1E149736E4},45",
+ "retained": true,
+ "space": "{8BBAD88F-D248-0959-00B3-2438A7065330},1"
+ },
+ {
+ "change": {
+ "Collapsed": true
+ },
+ "dependent_text": "{C868DD34-6E8C-07E9-116E-4C68D2C45DFC},54",
+ "name": "Delete only paragraph",
+ "object": "{C868DD34-6E8C-07E9-116E-4C68D2C45DFC},40",
+ "retained": true,
+ "space": "{A4CB3BA2-767F-0AE8-0916-AF142D995822},1"
+ },
+ {
+ "change": {
+ "Width": {
+ "points": 252.0,
+ "user_set": true
+ }
+ },
+ "dependent_text": "{78796D8B-FB39-02D8-2502-22530F00073C},75",
+ "name": "Delete outline",
+ "object": "{78796D8B-FB39-02D8-2502-22530F00073C},39",
+ "retained": true,
+ "space": "{80E80543-87A1-0C3A-0816-22B190BDFC87},1"
+ },
+ {
+ "change": {
+ "Position": {
+ "x": 252.0,
+ "y": 288.0
+ }
+ },
+ "dependent_text": "{462B969B-FCFA-0201-073A-0B4C3C23ABF5},75",
+ "name": "Move outline",
+ "object": "{462B969B-FCFA-0201-073A-0B4C3C23ABF5},39",
+ "retained": false,
+ "space": "{16207F40-69C9-0CA1-09B5-DC5712DB2BBE},1"
+ },
+ {
+ "change": {
+ "Width": {
+ "points": 252.0,
+ "user_set": true
+ }
+ },
+ "dependent_text": "{048364F9-D353-05E3-319E-2633D33434CE},75",
+ "name": "Resize outline",
+ "object": "{048364F9-D353-05E3-319E-2633D33434CE},39",
+ "retained": false,
+ "space": "{CD420421-73F7-0F11-02EC-35FCA348998C},1"
+ },
+ {
+ "change": {
+ "Width": {
+ "points": 252.0,
+ "user_set": true
+ }
+ },
+ "dependent_text": "{CE365E1B-9B76-067E-2EC7-220FB6AC76E9},75",
+ "name": "Automatic outline size",
+ "object": "{CE365E1B-9B76-067E-2EC7-220FB6AC76E9},39",
+ "retained": false,
+ "space": "{F383096F-2121-0225-1E62-AB37C9B15B33},1"
+ }
+]
\ No newline at end of file
diff --git a/corpus/outline-edit/offline/cold/commands.jsonl b/corpus/outline-edit/offline/cold/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..3ca2a0d7a0a8100956f32249e22b0b52c430db17
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/commands.jsonl
@@ -0,0 +1,3 @@
+{"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-B0D8A07E -ExpectedPages 15", "exit": 0, "stdout": "Read 1 sections and 15 pages.\r\n", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
diff --git a/corpus/outline-edit/offline/cold/machine.json b/corpus/outline-edit/offline/cold/machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..2fddeba2bd756293074dce48eecba902509d105e
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/machine.json
@@ -0,0 +1 @@
+{"name": "m6-b0d8a07e", "hostname": "ONE-M6-B0D8A07E"}
diff --git a/corpus/outline-edit/offline/cold/notebook/Open Notebook.onetoc2 b/corpus/outline-edit/offline/cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..ca2d23e914168eb4e1cf10450afb22b8af9e73cf
Binary files /dev/null and b/corpus/outline-edit/offline/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/outline-edit/offline/cold/notebook/synthetic.one b/corpus/outline-edit/offline/cold/notebook/synthetic.one
new file mode 100644
index 0000000000000000000000000000000000000000..3d2228b42f3bf92091ce641f1caf3ab4a11d11eb
Binary files /dev/null and b/corpus/outline-edit/offline/cold/notebook/synthetic.one differ
diff --git a/corpus/outline-edit/offline/cold/read/environment.json b/corpus/outline-edit/offline/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..6ae9b744b00d8ad9a4d76e32a145436c615f6563
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-B0D8A07E",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/outline-edit/offline/cold/read/hierarchy.xml b/corpus/outline-edit/offline/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0204d58383339adec8feaf54bf500679741f6b9c
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/outline-edit/offline/cold/read/page-000.xml b/corpus/outline-edit/offline/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..92615e94b447e118621d2a59144184e84e651e6e
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/page-000.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/offline/cold/read/page-001.xml b/corpus/outline-edit/offline/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6ccb6255a089fb2fe6e0c42387cc479f72ebbdea
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/page-001.xml
@@ -0,0 +1,20 @@
+
+Bold 🦀 italic é color 東京
+End]]>Fictitious: café, 東京, مرحبا]]>
diff --git a/corpus/outline-edit/offline/cold/read/page-002.xml b/corpus/outline-edit/offline/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fd2f2fbe02f1229cd212a0c1b88a10595b2870eb
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/page-002.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/offline/cold/read/page-003.xml b/corpus/outline-edit/offline/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..af9b2af57eb448d917e65d456ddb64cd33b4d3e8
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/outline-edit/offline/cold/read/page-004.xml b/corpus/outline-edit/offline/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fc84dcf2d2d8d46ecb76ab6d1a9afe5713fa331c
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/page-004.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/offline/cold/read/page-005.xml b/corpus/outline-edit/offline/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..65b18bd345968995b34b8d54d092f6d45df726f1
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/page-005.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/offline/cold/read/page-006.xml b/corpus/outline-edit/offline/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f8a14bee6efb9f1df88c8dd6d78a1e6b60a34808
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/page-006.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/offline/cold/read/page-007.xml b/corpus/outline-edit/offline/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0c3c0b97cff7e771638fc0ee59778ec92a074070
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/page-007.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/offline/cold/read/page-008.xml b/corpus/outline-edit/offline/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4a425d20db03d8ebdf3c5639ac9d1358ca4ab69f
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/page-008.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/outline-edit/offline/cold/read/page-009.xml b/corpus/outline-edit/offline/cold/read/page-009.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2e10eaeff992340702c39f5209ce5a82cd68fcb2
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/page-009.xml
@@ -0,0 +1,4 @@
+
+Target 🦀 é Link label]]>
diff --git a/corpus/outline-edit/offline/cold/read/page-010.xml b/corpus/outline-edit/offline/cold/read/page-010.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a1d53f47f159ab8288c69cc79036170a54d1d897
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/page-010.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/offline/cold/read/page-011.xml b/corpus/outline-edit/offline/cold/read/page-011.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0b524b2623a0e04b9a9dea0c6997240d04b7ad26
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/page-011.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/outline-edit/offline/cold/read/page-012.xml b/corpus/outline-edit/offline/cold/read/page-012.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3cdeb076e5761dafb8999ba352bca4bfb67d7ad4
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/page-012.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/offline/cold/read/page-013.xml b/corpus/outline-edit/offline/cold/read/page-013.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fe7126cf65360af6f16da4aa84e9916b2894258a
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/page-013.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/outline-edit/offline/cold/read/page-014.xml b/corpus/outline-edit/offline/cold/read/page-014.xml
new file mode 100644
index 0000000000000000000000000000000000000000..95cba123060980b973bd7a83c4437f5eb37b4d89
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/page-014.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/offline/cold/read/payloads.json b/corpus/outline-edit/offline/cold/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..9e1be3e2b6d87478e7c48d278ecac8ee6c97f6d0
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/read/payloads.json
@@ -0,0 +1 @@
+../../../../paragraph-edit/before/read/payloads.json
\ No newline at end of file
diff --git a/corpus/outline-edit/offline/cold/run.json b/corpus/outline-edit/offline/cold/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..afe77f78d76e0bc7a673bf8f335422d71457a911
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/run.json
@@ -0,0 +1,18 @@
+{
+ "notebook": "/Users/clo/dev/one/evidence/m10/offline-outline-candidate-01",
+ "expected_pages": 15,
+ "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/outline-edit/offline/cold/scripts/cold.ps1 b/corpus/outline-edit/offline/cold/scripts/cold.ps1
new file mode 120000
index 0000000000000000000000000000000000000000..5bc2f1837e1f0b639923a80b79c517c98c5c519a
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/scripts/cold.ps1
@@ -0,0 +1 @@
+../../../../paragraph-edit/reconciliation/keyboard/scripts/cold.ps1
\ No newline at end of file
diff --git a/corpus/outline-edit/offline/cold/scripts/read.ps1 b/corpus/outline-edit/offline/cold/scripts/read.ps1
new file mode 120000
index 0000000000000000000000000000000000000000..85257c76cede599f47da88e69419a767dd6b99ee
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/scripts/read.ps1
@@ -0,0 +1 @@
+../../../../paragraph-edit/reconciliation/keyboard/scripts/read.ps1
\ No newline at end of file
diff --git a/corpus/outline-edit/offline/cold/source.json b/corpus/outline-edit/offline/cold/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..c24898bd5a570d771a210ecb1fd572535e985442
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/source.json
@@ -0,0 +1,8 @@
+[
+ {
+ "path": "synthetic.one",
+ "bytes": 149936,
+ "sha256": "608532b4ef9be3cbeac3c0ec9b7e624a96aa4ce0ce6c0f86837933703e61fd0c",
+ "mtime_ns": 1788894002037909008
+ }
+]
diff --git a/corpus/outline-edit/offline/cold/teardown.json b/corpus/outline-edit/offline/cold/teardown.json
new file mode 100644
index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70
--- /dev/null
+++ b/corpus/outline-edit/offline/cold/teardown.json
@@ -0,0 +1 @@
+{"absent": true}
diff --git a/crates/onestore-offline/README.md b/crates/onestore-offline/README.md
index cc8593c7be7f02433d59f2c70d3d78e73d59ef0d..7d0731b71d643b3b794b88222bb4926d578e6961 100644
--- a/crates/onestore-offline/README.md
+++ b/crates/onestore-offline/README.md
@@ -30,8 +30,8 @@ intent identities. `Insertion::with_formatting` queues text and
its styles as one intent and one publication. Keep that value across retries; its `text_object()` identifies
the new text for subsequent offline edits. Pending entries expose
`Operation::Text(TextEdit)`, `Operation::Insert(Insertion)`,
-`Operation::Format(FormatEdit)`, `Operation::Split(SplitEdit)` and
-`Operation::Join(JoinEdit)` through their
+`Operation::Format(FormatEdit)`, `Operation::Split(SplitEdit)`,
+`Operation::Join(JoinEdit)` and `Operation::Outline(OutlineEdit)` through their
`operation` field. Synchronization applies these in queue order, so an inserted
outline can precede its paragraphs and their later edits. Missing anchors or
existing insertion identities preserve a conflict and the complete local image.
@@ -88,8 +88,18 @@ requested value is accepted. Enabling superscript or subscript also checks the
opposite attribute that the operation clears. If the remote image already satisfies
the whole operation, guarded confirmation still precedes a durable receipt.
-Recognized earlier caches migrate transactionally to version six, which adds
-paragraph split/join intents and structural conflicts. The migration retains
+`outline` accepts a target object and the core `onestore::OutlineEdit`: outline
+position/width or a paragraph's saved collapse default. It retains the target's
+ancestry and the properties the operation changes. Independent content, formatting
+and layout properties merge; competing selected values produce `LayoutChanged`,
+and changed ancestry produces `StructureChanged`. Width changes also check the
+reserved wrapping width that they clear. Missing targets preserve the local branch.
+`rebase_layout_conflict(id, local, remote)` explicitly reviews the original change
+against both current images, preserving dependent intents. An uncertain layout
+attempt requires its original revision; converged values cannot establish its receipt.
+
+Recognized earlier caches migrate transactionally to version seven, which adds
+layout intents and layout conflicts. The migration retains
images, local IDs, publication attempts, conflicts,
receipts and the autoincrement sequence; it does not reuse acknowledged IDs when
the pending queue is empty.
@@ -258,5 +268,4 @@ identity return `AssetChanged` and preserve the previous download.
Downloads do not change pending edits, publication attempts or receipts. Recovery
archives include cached media and expose the same bounded `cached_asset` lookup.
-Opening older live caches migrates them transactionally to schema 5; original
-schema-4 archives remain readable without migration and contain no media cache.
+Schema-4 archives remain readable without migration and contain no media cache.
diff --git a/crates/onestore-offline/examples/cache_probe.rs b/crates/onestore-offline/examples/cache_probe.rs
index 8f016e89f1cee8b67dc9f82b971ec26e1cce240b..4d5b54d41d963123843e4bbea3e58f1abeffbff8 100644
--- a/crates/onestore-offline/examples/cache_probe.rs
+++ b/crates/onestore-offline/examples/cache_probe.rs
@@ -138,8 +138,10 @@ fn main() -> Result<(), Box> {
font_size = Some(*value);
*value as u64 - 6
}
- onestore_offline::Operation::Split(_) | onestore_offline::Operation::Join(_) => {
- panic!("This fixture does not generate paragraph boundary edits")
+ onestore_offline::Operation::Split(_)
+ | onestore_offline::Operation::Join(_)
+ | onestore_offline::Operation::Outline(_) => {
+ panic!("Unexpected operation for this fixture")
}
};
assert!(operations.last().is_none_or(|last| *last < operation));
diff --git a/crates/onestore-offline/src/lib.rs b/crates/onestore-offline/src/lib.rs
index 47dc8da3c60b5fcf2cba95c2ea33775402a11ea0..37944b6343929b25dd30da6648ff374dd40137e3 100644
--- a/crates/onestore-offline/src/lib.rs
+++ b/crates/onestore-offline/src/lib.rs
@@ -10,11 +10,13 @@ use std::{fs::OpenOptions, io, ops::Range, path::Path, sync::Mutex, time::Durati
mod assets;
mod formatting;
+mod outline;
mod paragraph;
mod rebase;
mod recovery;
mod schema;
pub use formatting::FormatEdit;
+pub use outline::OutlineEdit;
pub use paragraph::{JoinEdit, SplitEdit};
pub use recovery::{Recovery, RecoverySummary};
mod sync;
@@ -47,7 +49,7 @@ pub enum Error {
type Result = std::result::Result;
const APPLICATION_ID: u32 = 0x4f4e454f;
-const SCHEMA_VERSION: u32 = 6;
+const SCHEMA_VERSION: u32 = 7;
/// Text and its observed precondition, retained across cache reopen and rebasing.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@@ -67,6 +69,7 @@ pub enum Operation {
Format(FormatEdit),
Split(SplitEdit),
Join(JoinEdit),
+ Outline(OutlineEdit),
}
/// A locally acknowledged intent; its ID remains stable across cache reopen.
@@ -279,6 +282,53 @@ impl Replica {
}
}
+fn active_paths(
+ view: &onestore::document::Revision<'_>,
+ pages: &[ExGuid],
+ objects: &[ExGuid],
+) -> Option>> {
+ use std::collections::{BTreeMap, BTreeSet};
+ let mut pending = pages.to_vec();
+ let mut seen = BTreeSet::new();
+ let mut parents = BTreeMap::<_, Vec<_>>::new();
+ while let Some(id) = pending.pop() {
+ if !seen.insert(id) {
+ continue;
+ }
+ let node = view.nodes.get(&id)?;
+ for child in node
+ .children
+ .iter()
+ .chain(&node.content)
+ .chain(&node.structure)
+ {
+ parents.entry(*child).or_default().push(id);
+ pending.push(*child);
+ }
+ }
+ objects
+ .iter()
+ .map(|object| {
+ if !seen.contains(object) {
+ return None;
+ }
+ let mut path = Vec::new();
+ let mut at = *object;
+ while !pages.contains(&at) {
+ let [parent] = parents.get(&at)?.as_slice() else {
+ return None;
+ };
+ if path.len() >= view.nodes.len() {
+ return None;
+ }
+ path.push(*parent);
+ at = *parent;
+ }
+ Some(path)
+ })
+ .collect()
+}
+
fn paragraph(source: &[u8], space: ExGuid, object: ExGuid) -> Result