fix makefile for cross compile
23 files changed,  +400, -1186
M .gitignore
+0, -1
1@@ -1,2 +1 @@
2 examples/load_document
3-lib/
M Makefile
+68, -31
  1@@ -1,53 +1,90 @@
  2-.PHONY: all build test clean libyrs update-yffi
  3+.PHONY: all build test clean libyrs libyrs-local libyrs-cross \
  4+        libyrs-linux-amd64 libyrs-linux-arm64 \
  5+        libyrs-freebsd-amd64 libyrs-freebsd-arm64 \
  6+        install-rust-targets update-yffi install-rust-default
  7 
  8-# Rust toolchain and target
  9-RUST_TARGET :=
 10-ifdef TARGET
 11-    RUST_TARGET := --target $(TARGET)
 12+# Detect platform and architecture
 13+UNAME_S := $(shell uname -s)
 14+UNAME_M := $(shell uname -m)
 15+
 16+# Map architecture names
 17+ifeq ($(UNAME_M),x86_64)
 18+    ARCH := amd64
 19+endif
 20+ifeq ($(UNAME_M),amd64)
 21+    ARCH := amd64
 22+endif
 23+ifeq ($(UNAME_M),aarch64)
 24+    ARCH := arm64
 25+endif
 26+ifeq ($(UNAME_M),arm64)
 27+    ARCH := arm64
 28 endif
 29 
 30-# Library extensions by platform
 31-UNAME_S := $(shell uname -s)
 32+# Set platform name
 33 ifeq ($(UNAME_S),Linux)
 34-    STATIC_LIB := libyrs.a
 35-    DYNAMIC_LIB := libyrs.so
 36-endif
 37-ifeq ($(UNAME_S),Darwin)
 38-    STATIC_LIB := libyrs.a
 39-    DYNAMIC_LIB := libyrs.dylib
 40+    PLATFORM := linux
 41 endif
 42 ifeq ($(UNAME_S),FreeBSD)
 43-    STATIC_LIB := libyrs.a
 44-    DYNAMIC_LIB := libyrs.so
 45-endif
 46-ifeq ($(OS),Windows_NT)
 47-    STATIC_LIB := yrs.lib
 48-    DYNAMIC_LIB := yrs.dll
 49+    PLATFORM := freebsd
 50 endif
 51 
 52-all: libyrs build
 53+# Export CGO_LDFLAGS to use the correct pre-built library
 54+export CGO_LDFLAGS := -L$(PWD)/lib/$(PLATFORM)-$(ARCH) -lyrs -ldl -lm
 55+
 56+all: build
 57 
 58-# Build the Rust static library from y-crdt submodule
 59-libyrs:
 60-	cd y-crdt && cargo build --release -p yffi $(RUST_TARGET)
 61-	mkdir -p lib
 62-	cp y-crdt/target/release/$(STATIC_LIB) lib/ 2>/dev/null || \
 63-		cp y-crdt/target/$(TARGET)/release/$(STATIC_LIB) lib/
 64+# Install Rust cross-compilation targets for FreeBSD
 65+install-rust-targets:
 66+	rustup default nightly
 67+
 68+# Build all cross-platform libraries (requires install-rust-targets for FreeBSD)
 69+libyrs-cross: install-rust-default libyrs-linux-amd64 libyrs-freebsd-amd64
 70+	@echo "All cross-platform libraries built successfully"
 71+
 72+# Individual platform builds
 73+libyrs-linux-amd64: install-rust-targets
 74+	cd y-crdt && rustup run nightly cargo rustc --release -p yffi --target x86_64-unknown-linux-gnu --crate-type staticlib
 75+	mkdir -p lib/linux-amd64
 76+	cp y-crdt/target/x86_64-unknown-linux-gnu/release/libyrs.a lib/linux-amd64/
 77+	cp y-crdt/tests-ffi/include/libyrs.h lib/include/
 78+	@echo "Built libyrs.a for linux-amd64"
 79+
 80+libyrs-freebsd-amd64: install-rust-targets
 81+	rustup target add x86_64-unknown-freebsd --toolchain nightly
 82+	cd y-crdt && rustup run nightly cargo rustc --release -p yffi --target x86_64-unknown-freebsd --crate-type staticlib
 83+	mkdir -p lib/freebsd-amd64
 84+	cp y-crdt/target/x86_64-unknown-freebsd/release/libyrs.a lib/freebsd-amd64/
 85 	cp y-crdt/tests-ffi/include/libyrs.h lib/include/
 86+	@echo "Built libyrs.a for freebsd-amd64"
 87 
 88-build: libyrs
 89+# Local build for current platform (overwrites prebuilt if exists)
 90+libyrs-local:
 91+	rustup default nightly
 92+	cd y-crdt && rustup run nightly cargo build --release -p yffi
 93+	mkdir -p lib/$(PLATFORM)-$(ARCH)
 94+	cp y-crdt/target/release/libyrs.a lib/$(PLATFORM)-$(ARCH)/
 95+	cp y-crdt/tests-ffi/include/libyrs.h lib/include/
 96+	@echo "Built libyrs.a for local platform: $(PLATFORM)-$(ARCH)"
 97+
 98+# Alias for local build
 99+libyrs: libyrs-local
100+
101+build:
102 	go build .
103 
104-test: libyrs
105+test:
106 	go test -v .
107 
108+examples:
109+	cd examples && CGO_LDFLAGS="$(CGO_LDFLAGS)" go build .
110+
111 clean:
112 	cd y-crdt && cargo clean
113-	rm -rf lib/
114+	rm -rf lib/*/libyrs.a
115 	go clean -cache
116 
117 # Update y-crdt submodule to the latest tagged release
118-# This is a manual target - not a precondition for building
119 update-yffi:
120 	@echo "Updating y-crdt submodule to latest tagged release..."
121 	git submodule update --init --recursive
122@@ -60,4 +97,4 @@ update-yffi:
123 		cd .. && \
124 		sed -i "s/const YFFIVersion = \"[^\"]*/const YFFIVersion = \"$$LATEST_TAG/" yjs.go && \
125 		echo "Updated YFFIVersion constant in yjs.go to $$LATEST_TAG"
126-	@echo "Run 'make libyrs' to rebuild with the updated version"
127+	@echo "Run 'make libyrs-cross' to rebuild all libraries"
A README.md
+264, -0
  1@@ -0,0 +1,264 @@
  2+# ygo
  3+
  4+Go bindings for the Rust [y-crdt](https://github.com/y-crdt/y-crdt) library, providing CRDT (Conflict-free Replicated Data Types) functionality for building collaborative applications.
  5+
  6+## Overview
  7+
  8+This library provides Go bindings to y-crdt's Yjs-compatible CRDT implementation via CGO. It enables real-time collaborative editing with support for:
  9+
 10+- **Documents** - The core unit of collaborative state
 11+- **Text** - Rich text with formatting and attributes
 12+- **Arrays** - Ordered collections with move operations
 13+- **Maps** - Key-value stores with nested types
 14+- **XML** - XML fragment and element types
 15+- **Transactions** - Atomic read/write operations
 16+- **Updates** - State synchronization between peers
 17+- **Undo/Redo** - Operation history management
 18+
 19+## Installation
 20+
 21+```bash
 22+go get github.com/BTBurke/ygo
 23+```
 24+
 25+## Building
 26+
 27+### Prerequisites
 28+
 29+- Go 1.22 or later
 30+- C compiler (gcc or clang)
 31+
 32+The library includes pre-built static libraries for Linux and FreeBSD on amd64 and arm64. No Rust toolchain is required for normal use.
 33+
 34+### Linux / FreeBSD
 35+
 36+**Simple usage (using pre-built libraries):**
 37+
 38+```bash
 39+go get github.com/BTBurke/ygo
 40+go build .
 41+```
 42+
 43+**For static linking (recommended for distribution):**
 44+
 45+```bash
 46+CGO_ENABLED=1 go build -ldflags '-linkmode external -extldflags "-static"' .
 47+```
 48+
 49+For a completely static binary (no dynamic library dependencies):
 50+```bash
 51+CGO_ENABLED=1 go build -ldflags '-linkmode external -extldflags "-static -ldl -lm"' .
 52+```
 53+
 54+### Building from Source (optional)
 55+
 56+If you need to build the Rust library from source (e.g., for a different platform or to apply patches):
 57+
 58+1. **Install Rust toolchain:**
 59+   ```bash
 60+   curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
 61+   ```
 62+
 63+2. **Clone with submodules and build:**
 64+   ```bash
 65+   git clone --recursive https://github.com/BTBurke/ygo
 66+   cd ygo
 67+   make libyrs-local  # Builds for current platform, overwrites prebuilt
 68+   go build .
 69+   ```
 70+
 71+3. **Cross-compile for all supported platforms:**
 72+   ```bash
 73+   make libyrs-cross  # Builds for linux-amd64, linux-arm64, freebsd-amd64, freebsd-arm64
 74+   ```
 75+
 76+### macOS
 77+
 78+macOS does not support fully static binaries due to system requirements. Use dynamic linking:
 79+
 80+```bash
 81+make libyrs
 82+go build .
 83+```
 84+
 85+### Windows
 86+
 87+Windows support requires MinGW-w64:
 88+
 89+```bash
 90+make libyrs
 91+set CGO_ENABLED=1
 92+go build .
 93+```
 94+
 95+## Quick Start
 96+
 97+```go
 98+package main
 99+
100+import (
101+    "fmt"
102+    "log"
103+    
104+    "github.com/BTBurke/ygo"
105+)
106+
107+func main() {
108+    // Create a new document
109+    doc, err := ygo.NewDoc()
110+    if err != nil {
111+        log.Fatal(err)
112+    }
113+    defer doc.Destroy()
114+    
115+    // Get or create a text field
116+    txt, err := doc.GetText("content")
117+    if err != nil {
118+        log.Fatal(err)
119+    }
120+    defer txt.Destroy()
121+    
122+    // Insert text within a transaction
123+    err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
124+        txt.Insert(txn, 0, "Hello, collaborative world!")
125+        return nil
126+    })
127+    if err != nil {
128+        log.Fatal(err)
129+    }
130+    
131+    // Read the text back
132+    var content string
133+    err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
134+        var err error
135+        content, err = txt.String(txn)
136+        return err
137+    })
138+    if err != nil {
139+        log.Fatal(err)
140+    }
141+    
142+    fmt.Println(content) // "Hello, collaborative world!"
143+}
144+```
145+
146+## Serialization
147+
148+Documents support standard Go binary marshaling:
149+
150+```go
151+// Serialize document to bytes
152+data, err := doc.MarshalBinary()
153+if err != nil {
154+    log.Fatal(err)
155+}
156+
157+// Save to file or send over network
158+os.WriteFile("document.yjs", data, 0644)
159+
160+// Later, load it back
161+data, _ := os.ReadFile("document.yjs")
162+doc2, _ := ygo.NewDoc()
163+txt2, _ := doc2.GetText("content")
164+if err := doc2.UnmarshalBinary(data); err != nil {
165+    log.Fatal(err)
166+}
167+```
168+
169+## Collaborative Updates
170+
171+Synchronize documents between clients:
172+
173+```go
174+// Get current state vector
175+var sv1 *ygo.StateVector
176+err = doc1.WithReadTransaction(func(txn *ygo.Transaction) error {
177+    sv1 = txn.GetStateVector()
178+    return nil
179+})
180+
181+// Get state diff (updates needed by remote)
182+var diff *ygo.Update
183+err = doc1.WithWriteTransaction(func(txn *ygo.Transaction) error {
184+    diff = txn.GetStateDiff(sv2) // sv2 is remote's state vector
185+    return nil
186+})
187+
188+// Apply diff on remote
189+doc2.WithWriteTransaction(func(txn *ygo.Transaction) error {
190+    return txn.ApplyUpdate(diff)
191+})
192+```
193+
194+## API Documentation
195+
196+The library implements standard Go interfaces for seamless integration:
197+
198+- `encoding.BinaryMarshaler` / `encoding.BinaryUnmarshaler` - Document serialization
199+- Callback-based transactions with automatic commit/rollback
200+- Idiomatic Go error handling throughout
201+
202+See the [examples](examples/) directory for more complete usage examples.
203+
204+## Architecture
205+
206+```
207+Go Application
208+    ↓ (CGO)
209+ygo (Go bindings)
210+    ↓ (FFI)
211+libyrs (Rust static library)
212+    ↓
213+y-crdt (Rust CRDT implementation)
214+```
215+
216+The native Rust library is built as a static library and linked into your Go binary.
217+
218+## Platform Support
219+
220+Pre-built static libraries are included for:
221+
222+| Platform | Architecture | Status |
223+|----------|--------------|--------|
224+| Linux    | amd64, arm64 | ✅ Pre-built libraries included |
225+| FreeBSD  | amd64, arm64 | ✅ Pre-built libraries included |
226+
227+The library automatically selects the correct pre-built library based on your platform. Cross-compilation is not supported - build on the target platform or use the provided pre-built libraries.
228+
229+## Updating the Native Library (for maintainers)
230+
231+To update the pre-built libraries to the latest y-crdt release:
232+
233+```bash
234+# Update submodule and version constant
235+make update-yffi
236+
237+# Build for all supported platforms (requires Rust toolchain)
238+make libyrs-cross
239+
240+# The lib/ directory now contains updated libraries for all platforms
241+# Commit the changes to lib/ directory
242+```
243+
244+For local development (overwrites pre-built library for current platform):
245+
246+```bash
247+make libyrs-local
248+go build .
249+```
250+
251+## License
252+
253+MIT License - See [LICENSE](LICENSE) for details.
254+
255+## Contributing
256+
257+Contributions are welcome! Please ensure:
258+
259+1. Code follows existing patterns and conventions
260+2. Tests pass: `go test ./...`
261+3. Examples build: `cd examples && go build .`
262+
263+## Acknowledgments
264+
265+This project is a Go wrapper around the excellent [y-crdt](https://github.com/y-crdt/y-crdt) Rust library by the Yjs team. All CRDT logic and algorithms are implemented in y-crdt; ygo provides the Go bindings and API layer.
M array_test.go
+2, -1
 1@@ -1,8 +1,9 @@
 2 package ygo_test
 3 
 4 import (
 5-	"github.com/y-crdt/ygo"
 6 	"testing"
 7+
 8+	"github.com/BTBurke/ygo"
 9 )
10 
11 func TestArrayBasic(t *testing.T) {
M devbox.json
+3, -2
 1@@ -3,12 +3,13 @@
 2   "packages": [
 3     "go@latest",
 4     "gnumake@latest",
 5-    "rustc@latest",
 6     "cargo@latest",
 7     "coreutils@latest",
 8     "bash@latest",
 9     "ripgrep@latest",
10-    "yarn@latest"
11+    "yarn@latest",
12+    "rustup@latest",
13+    "rustc@latest"
14   ],
15   "shell": {
16     "init_hook": [
M devbox.lock
+49, -0
 1@@ -538,6 +538,55 @@
 2         }
 3       }
 4     },
 5+    "rustup@latest": {
 6+      "last_modified": "2026-03-21T07:29:51Z",
 7+      "plugin_version": "0.0.1",
 8+      "resolved": "github:NixOS/nixpkgs/09061f748ee21f68a089cd5d91ec1859cd93d0be#rustup",
 9+      "source": "devbox-search",
10+      "version": "1.28.2",
11+      "systems": {
12+        "aarch64-darwin": {
13+          "outputs": [
14+            {
15+              "name": "out",
16+              "path": "/nix/store/z151hcfq4djwzmxxfdiy5cih1nly05aq-rustup-1.28.2",
17+              "default": true
18+            }
19+          ],
20+          "store_path": "/nix/store/z151hcfq4djwzmxxfdiy5cih1nly05aq-rustup-1.28.2"
21+        },
22+        "aarch64-linux": {
23+          "outputs": [
24+            {
25+              "name": "out",
26+              "path": "/nix/store/l1bgj6d6p3n9868n91vhb8fvzj3l0n69-rustup-1.28.2",
27+              "default": true
28+            }
29+          ],
30+          "store_path": "/nix/store/l1bgj6d6p3n9868n91vhb8fvzj3l0n69-rustup-1.28.2"
31+        },
32+        "x86_64-darwin": {
33+          "outputs": [
34+            {
35+              "name": "out",
36+              "path": "/nix/store/3rczx3p8ayxam5xp1h1jpq3cwwrzpak4-rustup-1.28.2",
37+              "default": true
38+            }
39+          ],
40+          "store_path": "/nix/store/3rczx3p8ayxam5xp1h1jpq3cwwrzpak4-rustup-1.28.2"
41+        },
42+        "x86_64-linux": {
43+          "outputs": [
44+            {
45+              "name": "out",
46+              "path": "/nix/store/8bwaqs52z4830qljdx989b86lqjq5ah3-rustup-1.28.2",
47+              "default": true
48+            }
49+          ],
50+          "store_path": "/nix/store/8bwaqs52z4830qljdx989b86lqjq5ah3-rustup-1.28.2"
51+        }
52+      }
53+    },
54     "yarn@latest": {
55       "last_modified": "2026-03-21T07:29:51Z",
56       "resolved": "github:NixOS/nixpkgs/09061f748ee21f68a089cd5d91ec1859cd93d0be#yarn",
D docs/plans/2026-03-30-binary-marshaler.md
+0, -595
  1@@ -1,595 +0,0 @@
  2-# BinaryMarshaler and BinaryUnmarshaler Implementation Plan
  3-
  4-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
  5-
  6-**Goal:** Implement the standard Go `encoding.BinaryMarshaler` and `encoding.BinaryUnmarshaler` interfaces for the `Doc` type to enable idiomatic binary serialization with V1 encoding.
  7-
  8-**Architecture:** Add `MarshalBinary() ([]byte, error)` and `UnmarshalBinary([]byte) error` methods to the `Doc` type. MarshalBinary will use a read transaction to capture the full document state. UnmarshalBinary will use a write transaction to apply the binary data as an update. This provides a standard Go interface that works seamlessly with encoding packages.
  9-
 10-**Tech Stack:** Go, CGO with yffi library
 11-
 12----
 13-
 14-## Task Overview
 15-
 16-1. Add `MarshalBinary()` method to Doc type in `document.go`
 17-2. Add `UnmarshalBinary()` method to Doc type in `document.go`
 18-3. Write comprehensive tests in `document_test.go`
 19-4. Update `example_test.go` to demonstrate idiomatic usage
 20-5. Update `examples/load_document.go` to use the new API
 21-6. Verify all tests pass
 22-
 23----
 24-
 25-## Task 1: Implement MarshalBinary Method
 26-
 27-**Files:**
 28-- Modify: `/home/btburke/projects/ygo/document.go`
 29-
 30-**Implementation Details:**
 31-
 32-Add the `MarshalBinary` method to the Doc type. This method should:
 33-1. Check if the document pointer is valid (return error if nil)
 34-2. Use `WithReadTransaction` to create a read-only transaction
 35-3. Inside the transaction, call `txn.GetStateDiff(nil)` to get the full document state
 36-4. Return the raw bytes from the Update's `Data()` method
 37-
 38-**Code to Add:**
 39-
 40-```go
 41-// MarshalBinary implements encoding.BinaryMarshaler.
 42-// Returns the document state as V1-encoded binary data.
 43-// This enables idiomatic usage with Go's encoding packages.
 44-func (d *Doc) MarshalBinary() ([]byte, error) {
 45-	if d.ptr == nil {
 46-		return nil, ErrNilDocument
 47-	}
 48-
 49-	var data []byte
 50-	err := d.WithReadTransaction(func(txn *Transaction) error {
 51-		update := txn.GetStateDiff(nil)
 52-		if update == nil {
 53-			return fmt.Errorf("failed to get document state")
 54-		}
 55-		data = update.Data()
 56-		return nil
 57-	})
 58-	
 59-	if err != nil {
 60-		return nil, fmt.Errorf("failed to marshal document: %w", err)
 61-	}
 62-	
 63-	return data, nil
 64-}
 65-```
 66-
 67-**Step 1: Add the import for "encoding" if not present**
 68-
 69-Check if the encoding package is imported. If not, add it to the imports.
 70-
 71-**Step 2: Add the MarshalBinary method**
 72-
 73-Add the method after the existing methods in document.go (around line 100, after Clone).
 74-
 75-**Step 3: Verify build**
 76-
 77-Run: `go build ./...`
 78-Expected: SUCCESS (no errors)
 79-
 80----
 81-
 82-## Task 2: Implement UnmarshalBinary Method
 83-
 84-**Files:**
 85-- Modify: `/home/btburke/projects/ygo/document.go`
 86-
 87-**Implementation Details:**
 88-
 89-Add the `UnmarshalBinary` method to the Doc type. This method should:
 90-1. Check if the document pointer is valid (return error if nil)
 91-2. Check if data is empty (return nil/error as appropriate)
 92-3. Create an Update from the bytes using `UpdateFromBytes(data)`
 93-4. Use `WithWriteTransaction` to apply the update inside a transaction
 94-5. Return any error from the application
 95-
 96-**Code to Add:**
 97-
 98-```go
 99-// UnmarshalBinary implements encoding.BinaryUnmarshaler.
100-// Applies V1-encoded binary data to the document.
101-// This enables idiomatic usage with Go's encoding packages.
102-func (d *Doc) UnmarshalBinary(data []byte) error {
103-	if d.ptr == nil {
104-		return ErrNilDocument
105-	}
106-	
107-	if len(data) == 0 {
108-		return nil // Nothing to apply
109-	}
110-	
111-	update := UpdateFromBytes(data)
112-	if update == nil {
113-		return fmt.Errorf("failed to create update from data")
114-	}
115-	
116-	return d.WithWriteTransaction(func(txn *Transaction) error {
117-		return txn.ApplyUpdate(update)
118-	})
119-}
120-```
121-
122-**Step 1: Add the UnmarshalBinary method**
123-
124-Add the method immediately after MarshalBinary in document.go.
125-
126-**Step 2: Verify build**
127-
128-Run: `go build ./...`
129-Expected: SUCCESS (no errors)
130-
131----
132-
133-## Task 3: Write Tests for MarshalBinary and UnmarshalBinary
134-
135-**Files:**
136-- Modify: `/home/btburke/projects/ygo/document_test.go`
137-
138-**Tests to Add:**
139-
140-**Test 1: TestMarshalUnmarshalRoundTrip**
141-
142-Tests that marshaling and unmarshaling preserves document content.
143-
144-```go
145-func TestMarshalUnmarshalRoundTrip(t *testing.T) {
146-	// Create document with content
147-	doc1, err := yjs.NewDoc()
148-	if err != nil {
149-		t.Fatalf("failed to create doc1: %v", err)
150-	}
151-	defer doc1.Destroy()
152-	
153-	txt1, err := doc1.GetText("content")
154-	if err != nil {
155-		t.Fatalf("failed to get text: %v", err)
156-	}
157-	defer txt1.Destroy()
158-	
159-	// Add some content
160-	err = doc1.WithWriteTransaction(func(txn *yjs.Transaction) error {
161-		txt1.Insert(txn, 0, "Hello World!")
162-		return nil
163-	})
164-	if err != nil {
165-		t.Fatalf("failed to insert text: %v", err)
166-	}
167-	
168-	// Marshal the document
169-	data, err := doc1.MarshalBinary()
170-	if err != nil {
171-		t.Fatalf("failed to marshal: %v", err)
172-	}
173-	if len(data) == 0 {
174-		t.Fatal("marshal returned empty data")
175-	}
176-	
177-	// Create new document and unmarshal
178-	doc2, err := yjs.NewDoc()
179-	if err != nil {
180-		t.Fatalf("failed to create doc2: %v", err)
181-	}
182-	defer doc2.Destroy()
183-	
184-	// IMPORTANT: Initialize shared types before unmarshaling
185-	txt2, err := doc2.GetText("content")
186-	if err != nil {
187-		t.Fatalf("failed to get text2: %v", err)
188-	}
189-	defer txt2.Destroy()
190-	
191-	// Unmarshal the data
192-	if err := doc2.UnmarshalBinary(data); err != nil {
193-		t.Fatalf("failed to unmarshal: %v", err)
194-	}
195-	
196-	// Verify content matches
197-	var content string
198-	err = doc2.WithReadTransaction(func(txn *yjs.Transaction) error {
199-		var err error
200-		content, err = txt2.String(txn)
201-		return err
202-	})
203-	if err != nil {
204-		t.Fatalf("failed to read content: %v", err)
205-	}
206-	
207-	if content != "Hello World!" {
208-		t.Errorf("expected 'Hello World!', got '%s'", content)
209-	}
210-}
211-```
212-
213-**Test 2: TestMarshalNilDocument**
214-
215-Tests that marshaling a nil document returns an error.
216-
217-```go
218-func TestMarshalNilDocument(t *testing.T) {
219-	var doc *yjs.Doc
220-	data, err := doc.MarshalBinary()
221-	if err == nil {
222-		t.Error("expected error when marshaling nil document")
223-	}
224-	if data != nil {
225-		t.Error("expected nil data when marshaling nil document")
226-	}
227-}
228-```
229-
230-**Test 3: TestUnmarshalNilDocument**
231-
232-Tests that unmarshaling to a nil document returns an error.
233-
234-```go
235-func TestUnmarshalNilDocument(t *testing.T) {
236-	var doc *yjs.Doc
237-	err := doc.UnmarshalBinary([]byte{1, 2, 3})
238-	if err == nil {
239-		t.Error("expected error when unmarshaling to nil document")
240-	}
241-}
242-```
243-
244-**Test 4: TestUnmarshalEmptyData**
245-
246-Tests that unmarshaling empty data is a no-op.
247-
248-```go
249-func TestUnmarshalEmptyData(t *testing.T) {
250-	doc, err := yjs.NewDoc()
251-	if err != nil {
252-		t.Fatalf("failed to create doc: %v", err)
253-	}
254-	defer doc.Destroy()
255-	
256-	// Unmarshal empty data should succeed and be a no-op
257-	if err := doc.UnmarshalBinary([]byte{}); err != nil {
258-		t.Fatalf("failed to unmarshal empty data: %v", err)
259-	}
260-	if err := doc.UnmarshalBinary(nil); err != nil {
261-		t.Fatalf("failed to unmarshal nil data: %v", err)
262-	}
263-}
264-```
265-
266-**Test 5: TestMarshalEmptyDocument**
267-
268-Tests that marshaling an empty document returns valid data.
269-
270-```go
271-func TestMarshalEmptyDocument(t *testing.T) {
272-	doc, err := yjs.NewDoc()
273-	if err != nil {
274-		t.Fatalf("failed to create doc: %v", err)
275-	}
276-	defer doc.Destroy()
277-	
278-	data, err := doc.MarshalBinary()
279-	if err != nil {
280-		t.Fatalf("failed to marshal empty doc: %v", err)
281-	}
282-	// Empty document still has some metadata, so data should not be empty
283-	if len(data) == 0 {
284-		t.Error("marshal of empty document should return some data (metadata)")
285-	}
286-}
287-```
288-
289-**Step 1: Add imports if needed**
290-
291-Ensure the test file imports the testing package and yjs.
292-
293-**Step 2: Add all 5 test functions**
294-
295-Add these tests after the existing tests in document_test.go.
296-
297-**Step 3: Run tests**
298-
299-Run: `go test -v -run TestMarshal ./...`
300-Expected: All 5 tests pass
301-
302----
303-
304-## Task 4: Update example_test.go to Demonstrate Idiomatic Usage
305-
306-**Files:**
307-- Modify: `/home/btburke/projects/ygo/example_test.go`
308-
309-**Update TestExampleDocumentLoading**
310-
311-Replace the current implementation that manually uses transactions with the idiomatic MarshalBinary/UnmarshalBinary approach:
312-
313-**Current Pattern:**
314-```go
315-// Create sample document
316-var fullState *yjs.Update
317-err = doc.WithWriteTransaction(func(txn *yjs.Transaction) error {
318-    txt.Insert(txn, 0, "Hello from Yjs!")
319-    fullState = txn.GetStateDiff(nil)
320-    return nil
321-})
322-
323-// Write to file
324-os.WriteFile(filePath, fullState.Data(), 0644)
325-
326-// Load it back
327-data, _ := os.ReadFile(filePath)
328-doc2, _ := yjs.NewDoc()
329-txt2, _ := doc2.GetText("content")
330-err = doc2.WithWriteTransaction(func(txn *yjs.Transaction) error {
331-    update := yjs.UpdateFromBytes(data)
332-    return txn.ApplyUpdate(update)
333-})
334-```
335-
336-**New Idiomatic Pattern:**
337-```go
338-// Create sample document
339-err = doc.WithWriteTransaction(func(txn *yjs.Transaction) error {
340-    txt.Insert(txn, 0, "Hello from Yjs!")
341-    return nil
342-})
343-
344-// Marshal document to bytes
345-data, err := doc.MarshalBinary()
346-if err != nil {
347-    t.Fatalf("failed to marshal: %v", err)
348-}
349-
350-// Write to file
351-os.WriteFile(filePath, data, 0644)
352-
353-// Load it back
354-data, _ = os.ReadFile(filePath)
355-doc2, _ := yjs.NewDoc()
356-txt2, _ := doc2.GetText("content")
357-
358-// Unmarshal directly
359-doc2.UnmarshalBinary(data)
360-
361-// Verify content
362-var content string
363-err = doc2.WithReadTransaction(func(txn *yjs.Transaction) error {
364-    content, err = txt2.String(txn)
365-    return err
366-})
367-```
368-
369-**Step 1: Read the current example_test.go**
370-
371-**Step 2: Update the test to use MarshalBinary and UnmarshalBinary**
372-
373-Keep the t.Log() calls for progress tracking, but replace the transaction-heavy code with the simpler marshal/unmarshal approach.
374-
375-**Step 3: Run the test**
376-
377-Run: `go test -v -run TestExample ./...`
378-Expected: PASS
379-
380----
381-
382-## Task 5: Update examples/load_document.go
383-
384-**Files:**
385-- Modify: `/home/btburke/projects/ygo/examples/load_document.go`
386-
387-**Update Both Functions:**
388-
389-**loadAndDisplayDocument:**
390-- Replace the manual transaction-based loading with `doc.UnmarshalBinary(data)`
391-- Keep the transaction-based reading for display purposes (needed to access content)
392-
393-**createSampleDocument:**
394-- Replace the manual transaction-based state capture with `doc.MarshalBinary()`
395-
396-**Step 1: Read current examples/load_document.go**
397-
398-**Step 2: Update createSampleDocument**
399-
400-Old:
401-```go
402-func createSampleDocument(filePath string) error {
403-    // ... create doc, get types ...
404-    
405-    txn, err := doc.WriteTransaction()
406-    if err != nil {
407-        return fmt.Errorf("failed to create transaction: %w", err)
408-    }
409-    
410-    // Add content
411-    txt.Insert(txn, 0, "Hello from Yjs!")
412-    arr.InsertRange(txn, 0, items)
413-    
414-    fullState := txn.GetStateDiff(nil)
415-    txn.Commit()
416-    
417-    os.WriteFile(filePath, fullState.Data(), 0644)
418-    return nil
419-}
420-```
421-
422-New:
423-```go
424-func createSampleDocument(filePath string) error {
425-    // ... create doc, get types ...
426-    
427-    // Add content
428-    err = doc.WithWriteTransaction(func(txn *yjs.Transaction) error {
429-        txt.Insert(txn, 0, "Hello from Yjs!")
430-        arr.InsertRange(txn, 0, items)
431-        return nil
432-    })
433-    if err != nil {
434-        return fmt.Errorf("failed to add content: %w", err)
435-    }
436-    
437-    // Marshal document
438-    data, err := doc.MarshalBinary()
439-    if err != nil {
440-        return fmt.Errorf("failed to marshal document: %w", err)
441-    }
442-    
443-    os.WriteFile(filePath, data, 0644)
444-    return nil
445-}
446-```
447-
448-**Step 3: Update loadAndDisplayDocument**
449-
450-Old:
451-```go
452-func loadAndDisplayDocument(filePath string) error {
453-    data, _ := os.ReadFile(filePath)
454-    
455-    doc := yjs.NewDoc()
456-    // ... initialize types ...
457-    
458-    txn, err := doc.WriteTransaction()
459-    if err != nil {
460-        return fmt.Errorf("failed to create transaction: %w", err)
461-    }
462-    
463-    update := yjs.UpdateFromBytes(data)
464-    if err := txn.ApplyUpdate(update); err != nil {
465-        txn.Commit()
466-        return fmt.Errorf("failed to apply update: %w", err)
467-    }
468-    txn.Commit()
469-    
470-    // ... read and display ...
471-}
472-```
473-
474-New:
475-```go
476-func loadAndDisplayDocument(filePath string) error {
477-    data, err := os.ReadFile(filePath)
478-    if err != nil {
479-        return fmt.Errorf("failed to read file: %w", err)
480-    }
481-    
482-    doc, err := yjs.NewDoc()
483-    if err != nil {
484-        return fmt.Errorf("failed to create doc: %w", err)
485-    }
486-    defer doc.Destroy()
487-    
488-    // IMPORTANT: Initialize shared types BEFORE unmarshaling
489-    txt, err := doc.GetText("content")
490-    if err != nil {
491-        return fmt.Errorf("failed to get text: %w", err)
492-    }
493-    defer txt.Destroy()
494-    
495-    arr, err := doc.GetArray("items")
496-    if err != nil {
497-        return fmt.Errorf("failed to get array: %w", err)
498-    }
499-    defer arr.Destroy()
500-    
501-    // Unmarshal directly
502-    if err := doc.UnmarshalBinary(data); err != nil {
503-        return fmt.Errorf("failed to unmarshal document: %w", err)
504-    }
505-    
506-    // ... read and display using transactions ...
507-}
508-```
509-
510-**Step 4: Build and test example**
511-
512-Run: `cd examples && go build -o load_document .`
513-Expected: SUCCESS
514-
515-Run: `./examples/load_document`
516-Expected: Displays document content successfully
517-
518----
519-
520-## Task 6: Final Verification
521-
522-**Step 1: Run full test suite**
523-
524-Run: `go test -v ./...`
525-Expected: All tests pass
526-
527-**Step 2: Verify example builds**
528-
529-Run: `go build ./... && cd examples && go build .`
530-Expected: Both build successfully
531-
532-**Step 3: Check interface compliance**
533-
534-Add a compile-time check to ensure Doc implements the interfaces:
535-
536-```go
537-// Compile-time interface compliance check
538-var (
539-    _ encoding.BinaryMarshaler   = (*Doc)(nil)
540-    _ encoding.BinaryUnmarshaler = (*Doc)(nil)
541-)
542-```
543-
544-Add this to document.go after the imports.
545-
546-Run: `go build ./...`
547-Expected: SUCCESS (confirms interface compliance)
548-
549----
550-
551-## Summary
552-
553-**Files Modified:**
554-1. `/home/btburke/projects/ygo/document.go` - Add MarshalBinary and UnmarshalBinary methods
555-2. `/home/btburke/projects/ygo/document_test.go` - Add 5 comprehensive tests
556-3. `/home/btburke/projects/ygo/example_test.go` - Update to use idiomatic API
557-4. `/home/btburke/projects/ygo/examples/load_document.go` - Simplify using new API
558-
559-**Benefits of New API:**
560-
561-1. **Idiomatic Go** - Implements standard library interfaces
562-2. **Simpler usage** - No manual transaction management for serialization
563-3. **Works with encoding packages** - Can be used with `gob`, `json` (when embedded), etc.
564-4. **Cleaner code** - Reduces boilerplate significantly
565-5. **Composability** - Easy to serialize documents in larger data structures
566-
567-**Before/After Comparison:**
568-
569-```go
570-// BEFORE (old API)
571-data, err := doc.WithWriteTransaction(func(txn *yjs.Transaction) error {
572-    update := txn.GetStateDiff(nil)
573-    data = update.Data()
574-    return nil
575-})
576-os.WriteFile("doc.yjs", data, 0644)
577-
578-// Load back
579-data, _ := os.ReadFile("doc.yjs")
580-doc2.WithWriteTransaction(func(txn *yjs.Transaction) error {
581-    update := yjs.UpdateFromBytes(data)
582-    return txn.ApplyUpdate(update)
583-})
584-
585-// AFTER (new idiomatic API)
586-data, _ := doc.MarshalBinary()
587-os.WriteFile("doc.yjs", data, 0644)
588-
589-// Load back
590-data, _ = os.ReadFile("doc.yjs")
591-doc2.UnmarshalBinary(data)
592-```
593-
594-**Note on Backward Compatibility:**
595-
596-The existing `txn.GetStateDiff()`, `txn.ApplyUpdate()`, and `yjs.UpdateFromBytes()` functions remain available for advanced use cases (e.g., V2 encoding, partial updates, delta sync). The new MarshalBinary/UnmarshalBinary methods provide the idiomatic 90% use case.
D docs/plans/2026-03-30-callback-transaction-api.md
+0, -542
  1@@ -1,542 +0,0 @@
  2-# Callback-Based Transaction API Implementation Plan
  3-
  4-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
  5-
  6-**Goal:** Replace manual transaction management with a callback-based API that automatically commits or rolls back based on callback return values.
  7-
  8-**Architecture:** Replace `WriteTransaction()` and `ReadTransaction()` methods with `WithWriteTransaction(fn)` and `WithReadTransaction(fn)` that take a callback function. The function creates the transaction, executes the callback, and handles commit/rollback automatically based on whether the callback returns an error.
  9-
 10-**Tech Stack:** Go, CGO with yffi library
 11-
 12----
 13-
 14-## Task Overview
 15-
 16-1. Update `transaction.go` - Replace existing methods with callback-based API
 17-2. Update test files - All 55 transaction calls across 11 test files  
 18-3. Update example - 3 transaction calls in `examples/load_document.go`
 19-4. Run full test suite to verify all changes work
 20-
 21----
 22-
 23-## Task 1: Update transaction.go with Callback API
 24-
 25-**Files:**
 26-- Modify: `/home/btburke/projects/ygo/transaction.go`
 27-
 28-**Step 1: Add Rollback method for write transactions**
 29-
 30-Add a new `Rollback()` method to handle transaction rollback (needed for write transactions that fail):
 31-
 32-```go
 33-// Rollback aborts the transaction without applying changes.
 34-// This is used internally when a callback returns an error.
 35-func (t *Transaction) Rollback() {
 36-	if t.ptr != nil {
 37-		C.ytransaction_commit(t.ptr) // yffi uses commit to end, even for rollback
 38-		t.ptr = nil
 39-		runtime.SetFinalizer(t, nil)
 40-	}
 41-}
 42-```
 43-
 44-**Step 2: Replace ReadTransaction with WithReadTransaction**
 45-
 46-Replace the existing `ReadTransaction()` method with:
 47-
 48-```go
 49-// WithReadTransaction executes a callback within a read-only transaction.
 50-// The transaction is automatically committed after the callback completes.
 51-// Returns any error from the callback.
 52-func (d *Doc) WithReadTransaction(fn func(*Transaction) error) error {
 53-	if d.ptr == nil {
 54-		return ErrNilDocument
 55-	}
 56-	
 57-	txn := C.ydoc_read_transaction(d.ptr)
 58-	if txn == nil {
 59-		return fmt.Errorf("failed to create read transaction: another transaction may be active")
 60-	}
 61-	t := &Transaction{ptr: txn, doc: d}
 62-	
 63-	// Execute callback
 64-	err := fn(t)
 65-	
 66-	// Always commit read transactions (they don't modify state)
 67-	t.Commit()
 68-	
 69-	return err
 70-}
 71-```
 72-
 73-**Step 3: Replace WriteTransaction and WriteTransactionWithOrigin**
 74-
 75-Replace both methods with:
 76-
 77-```go
 78-// WithWriteTransaction executes a callback within a read-write transaction.
 79-// If the callback returns nil, the transaction is committed.
 80-// If the callback returns an error, the transaction is rolled back.
 81-// Returns any error from the callback.
 82-func (d *Doc) WithWriteTransaction(fn func(*Transaction) error) error {
 83-	return d.WithWriteTransactionWithOrigin(nil, fn)
 84-}
 85-
 86-// WithWriteTransactionWithOrigin executes a callback within a read-write transaction with an origin marker.
 87-// The origin can be used by event handlers and undo managers to identify change sources.
 88-// If the callback returns nil, the transaction is committed.
 89-// If the callback returns an error, the transaction is rolled back.
 90-func (d *Doc) WithWriteTransactionWithOrigin(origin []byte, fn func(*Transaction) error) error {
 91-	if d.ptr == nil {
 92-		return ErrNilDocument
 93-	}
 94-
 95-	var originPtr *C.char
 96-	var originLen C.uint32_t
 97-	if len(origin) > 0 {
 98-		originPtr = (*C.char)(unsafe.Pointer(&origin[0]))
 99-		originLen = C.uint32_t(len(origin))
100-	}
101-
102-	txn := C.ydoc_write_transaction(d.ptr, originLen, originPtr)
103-	if txn == nil {
104-		return fmt.Errorf("failed to create write transaction: another transaction may be active")
105-	}
106-	t := &Transaction{ptr: txn, doc: d}
107-	
108-	// Execute callback
109-	err := fn(t)
110-	
111-	if err != nil {
112-		// Rollback on error
113-		t.Rollback()
114-		return err
115-	}
116-	
117-	// Commit on success
118-	t.Commit()
119-	return nil
120-}
121-```
122-
123-**Step 4: Remove the finalizer from transaction creation**
124-
125-Since we now handle commit/rollback explicitly in the With* methods, remove:
126-```go
127-runtime.SetFinalizer(t, (*Transaction).Commit)  // REMOVE THIS LINE
128-```
129-
130-**Step 5: Verify build**
131-
132-Run: `go build ./...`
133-Expected: SUCCESS (no errors)
134-
135----
136-
137-## Task 2: Update transaction_test.go
138-
139-**Files:**
140-- Modify: `/home/btburke/projects/ygo/transaction_test.go`
141-
142-**Step 1: Update TestReadTransaction**
143-
144-Replace:
145-```go
146-func TestReadTransaction(t *testing.T) {
147-	doc, err := yjs.NewDoc()
148-	if err != nil {
149-		t.Fatalf("failed to create doc: %v", err)
150-	}
151-	defer doc.Destroy()
152-
153-	txn, err := doc.ReadTransaction()
154-	if err != nil {
155-		t.Fatalf("failed to create read transaction: %v", err)
156-	}
157-	defer txn.Commit()
158-
159-	if txn.IsWriteable() {
160-		t.Error("read transaction should not be writeable")
161-	}
162-}
163-```
164-
165-With:
166-```go
167-func TestReadTransaction(t *testing.T) {
168-	doc, err := yjs.NewDoc()
169-	if err != nil {
170-		t.Fatalf("failed to create doc: %v", err)
171-	}
172-	defer doc.Destroy()
173-
174-	err = doc.WithReadTransaction(func(txn *yjs.Transaction) error {
175-		if txn.IsWriteable() {
176-			t.Error("read transaction should not be writeable")
177-		}
178-		return nil
179-	})
180-	if err != nil {
181-		t.Fatalf("transaction failed: %v", err)
182-	}
183-}
184-```
185-
186-**Step 2: Update TestWriteTransaction**
187-
188-Replace with callback pattern.
189-
190-**Step 3: Update TestTransactionConflict**
191-
192-This test needs special handling since it tests concurrent transactions. The first transaction should use the callback pattern, but the second transaction attempt (which should fail) remains as a simple call since it tests the failure case:
193-
194-```go
195-func TestTransactionConflict(t *testing.T) {
196-	doc, err := yjs.NewDoc()
197-	if err != nil {
198-		t.Fatalf("failed to create doc: %v", err)
199-	}
200-	defer doc.Destroy()
201-
202-	// First transaction uses callback pattern
203-	err = doc.WithWriteTransaction(func(txn1 *yjs.Transaction) error {
204-		if !txn1.IsWriteable() {
205-			t.Error("write transaction should be writeable")
206-		}
207-		
208-		// Try to create second concurrent transaction (should fail)
209-		err := doc.WithWriteTransaction(func(txn2 *yjs.Transaction) error {
210-			// This should not execute
211-			t.Error("expected second write transaction to fail while first is open")
212-			return nil
213-		})
214-		if err == nil {
215-			t.Error("expected second write transaction to fail while first is open")
216-		}
217-		
218-		return nil
219-	})
220-	if err != nil {
221-		t.Fatalf("first transaction failed: %v", err)
222-	}
223-}
224-```
225-
226-**Step 4: Update TestTransactionWithOrigin**
227-
228-Replace with callback pattern.
229-
230-**Step 5: Run tests**
231-
232-Run: `go test -v -run TestReadTransaction|TestWriteTransaction|TestTransaction ./...`
233-Expected: All transaction tests pass
234-
235----
236-
237-## Task 3: Update document_test.go
238-
239-**Files:**
240-- Modify: `/home/btburke/projects/ygo/document_test.go` (no transaction calls, just verify it still compiles)
241-
242-Run: `go test -v -run TestNewDoc ./...`
243-Expected: PASS
244-
245----
246-
247-## Task 4: Update text_test.go
248-
249-**Files:**
250-- Modify: `/home/btburke/projects/ygo/text_test.go`
251-
252-**Step 1: Update TestTextBasic**
253-
254-Replace:
255-```go
256-	txn, err := doc.WriteTransaction()
257-	if err != nil {
258-		t.Fatalf("failed to create transaction: %v", err)
259-	}
260-	defer txn.Commit()
261-
262-	txt.Insert(txn, 0, "hello")
263-	txt.Insert(txn, 5, " world")
264-	txt.RemoveRange(txn, 0, 6)
265-
266-	length, err := txt.Len(txn)
267-	...
268-```
269-
270-With:
271-```go
272-	var length uint32
273-	var str string
274-	
275-	err = doc.WithWriteTransaction(func(txn *yjs.Transaction) error {
276-		txt.Insert(txn, 0, "hello")
277-		txt.Insert(txn, 5, " world")
278-		txt.RemoveRange(txn, 0, 6)
279-
280-		var err error
281-		length, err = txt.Len(txn)
282-		if err != nil {
283-			return err
284-		}
285-		
286-		str, err = txt.String(txn)
287-		return err
288-	})
289-	if err != nil {
290-		t.Fatalf("transaction failed: %v", err)
291-	}
292-	
293-	if length != 5 {
294-		t.Errorf("expected length 5, got %d", length)
295-	}
296-	if str != "world" {
297-		t.Errorf("expected 'world', got '%s'", str)
298-	}
299-```
300-
301-**Step 2: Update TestTextInsertWithAttributes**
302-
303-Similar pattern - move all operations inside callback, use closure variables for results.
304-
305-**Step 3: Update TestTextFormat**
306-
307-Same pattern.
308-
309-**Step 4: Update TestTextUnicode**
310-
311-Same pattern.
312-
313-**Step 5: Run tests**
314-
315-Run: `go test -v -run TestText ./...`
316-Expected: All text tests pass
317-
318----
319-
320-## Task 5: Update array_test.go
321-
322-**Files:**
323-- Modify: `/home/btburke/projects/ygo/array_test.go`
324-
325-**Step 1-3: Update TestArrayBasic, TestArrayPush, TestArrayMove**
326-
327-Replace each transaction pattern with callback style, using closure variables to capture results that need to be checked after the transaction.
328-
329-**Step 4: Run tests**
330-
331-Run: `go test -v -run TestArray ./...`
332-Expected: All array tests pass
333-
334----
335-
336-## Task 6: Update map_test.go
337-
338-**Files:**
339-- Modify: `/home/btburke/projects/ygo/map_test.go`
340-
341-**Step 1-2: Update TestMapBasic, TestMapNested**
342-
343-Replace transaction patterns with callbacks.
344-
345-**Step 3: Run tests**
346-
347-Run: `go test -v -run TestMap ./...`
348-Expected: All map tests pass
349-
350----
351-
352-## Task 7: Update xml_test.go
353-
354-**Files:**
355-- Modify: `/home/btburke/projects/ygo/xml_test.go`
356-
357-**Step 1-3: Update TestXmlElementBasic, TestXmlText, TestXmlNestedElements**
358-
359-Replace transaction patterns with callbacks. These tests have more complex assertions, so use closure variables effectively.
360-
361-**Step 4: Run tests**
362-
363-Run: `go test -v -run TestXml ./...`
364-Expected: All XML tests pass
365-
366----
367-
368-## Task 8: Update update_test.go
369-
370-**Files:**
371-- Modify: `/home/btburke/projects/ygo/update_test.go`
372-
373-**Step 1-2: Update TestUpdateExchange, TestFullStateSnapshot**
374-
375-These tests have two documents with concurrent transactions. Structure them carefully:
376-
377-For TestUpdateExchange:
378-```go
379-// Make concurrent edits using callbacks
380-var sv1, sv2 *yjs.StateVector
381-var diff1, diff2 *yjs.Update
382-
383-err = d1.WithWriteTransaction(func(txn1 *yjs.Transaction) error {
384-	txt1.Insert(txn1, 0, "world")
385-	
386-	// Get state vector
387-	sv1 = txn1.GetStateVector()
388-	
389-	// Get diff needs sv2, but it's not available yet
390-	// We need to restructure this test...
391-	
392-	return nil
393-})
394-```
395-
396-This test is tricky because it exchanges state vectors between two concurrent transactions. We may need to restructure it to create one transaction at a time rather than trying to hold both open simultaneously.
397-
398-**Step 3: Run tests**
399-
400-Run: `go test -v -run TestUpdate ./...`
401-Expected: All update tests pass
402-
403----
404-
405-## Task 9: Update undo_test.go
406-
407-**Files:**
408-- Modify: `/home/btburke/projects/ygo/undo_test.go`
409-
410-**Step 1-3: Update TestUndoManagerBasic, TestUndoManagerStop, TestUndoManagerWithRemoteChanges**
411-
412-These tests involve undo manager with explicit `mgr.Stop()` calls between transactions. The callback pattern works well here since each transaction is independent.
413-
414-**Step 4: Run tests**
415-
416-Run: `go test -v -run TestUndo ./...`
417-Expected: All undo tests pass
418-
419----
420-
421-## Task 10: Update sticky_test.go
422-
423-**Files:**
424-- Modify: `/home/btburke/projects/ygo/sticky_test.go`
425-
426-**Step 1-3: Update TestStickyIndexBasic, TestStickyIndexSurvivesChanges, TestStickyIndexJSON**
427-
428-These tests mix write and read transactions. Replace each appropriately.
429-
430-**Step 4: Run tests**
431-
432-Run: `go test -v -run TestSticky ./...`
433-Expected: All sticky tests pass
434-
435----
436-
437-## Task 11: Update weak_test.go
438-
439-**Files:**
440-- Modify: `/home/btburke/projects/ygo/weak_test.go`
441-
442-**Step 1-4: Update TestWeakLinkText, TestWeakLinkArrayCreateOnly, TestWeakLinkArrayInputOnly, TestWeakLinkArrayFullWorkflow**
443-
444-Replace transaction patterns. Note that `TestWeakLinkArrayFullWorkflow` is already skipped due to known memory corruption.
445-
446-**Step 5: Run tests**
447-
448-Run: `go test -v -run TestWeak ./...`
449-Expected: All weak tests pass (except skipped one)
450-
451----
452-
453-## Task 12: Update example_test.go
454-
455-**Files:**
456-- Modify: `/home/btburke/projects/ygo/example_test.go`
457-
458-**Step 1: Update TestExampleDocumentLoading**
459-
460-This is the most complex test with multiple sequential transactions. Replace:
461-- First WriteTransaction for creating content
462-- Second WriteTransaction for applying update  
463-- ReadTransaction for verifying content
464-
465-Use closure variables to capture `fullState` and `content` between transactions.
466-
467-**Step 2: Run tests**
468-
469-Run: `go test -v -run TestExample ./...`
470-Expected: Example test passes
471-
472----
473-
474-## Task 13: Update examples/load_document.go
475-
476-**Files:**
477-- Modify: `/home/btburke/projects/ygo/examples/load_document.go`
478-
479-**Step 1: Update loadAndDisplayDocument function**
480-
481-Replace the read transaction at lines 72-61 with callback pattern.
482-
483-**Step 2: Update createSampleDocument function**
484-
485-Replace the write transaction at lines 163-153 with callback pattern.
486-
487-**Step 3: Build example**
488-
489-Run: `cd examples && go build -o load_document .`
490-Expected: SUCCESS
491-
492-**Step 4: Run example**
493-
494-Run: `./examples/load_document`
495-Expected: Displays document content successfully (may crash at end due to known yffi issue)
496-
497----
498-
499-## Task 14: Final Verification
500-
501-**Step 1: Run full test suite**
502-
503-Run: `go test -v ./...`
504-Expected: All tests pass
505-
506-**Step 2: Verify build of main package and example**
507-
508-Run: `go build ./... && cd examples && go build .`
509-Expected: Both build successfully
510-
511-**Step 3: Document the breaking change**
512-
513-Add to CHANGELOG or README that this is a breaking API change:
514-- `doc.WriteTransaction()` → `doc.WithWriteTransaction(func(*Transaction) error) error`
515-- `doc.ReadTransaction()` → `doc.WithReadTransaction(func(*Transaction) error) error`
516-- `defer txn.Commit()` pattern no longer needed
517-
518----
519-
520-## Summary of Changes
521-
522-**Files Modified:**
523-1. `/home/btburke/projects/ygo/transaction.go` - Core API change
524-2. `/home/btburke/projects/ygo/transaction_test.go` - 4 tests
525-3. `/home/btburke/projects/ygo/text_test.go` - 4 tests
526-4. `/home/btburke/projects/ygo/array_test.go` - 3 tests
527-5. `/home/btburke/projects/ygo/map_test.go` - 2 tests
528-6. `/home/btburke/projects/ygo/xml_test.go` - 3 tests
529-7. `/home/btburke/projects/ygo/update_test.go` - 2 tests
530-8. `/home/btburke/projects/ygo/undo_test.go` - 3 tests
531-9. `/home/btburke/projects/ygo/sticky_test.go` - 3 tests
532-10. `/home/btburke/projects/ygo/weak_test.go` - 4 tests
533-11. `/home/btburke/projects/ygo/example_test.go` - 1 test
534-12. `/home/btburke/projects/ygo/examples/load_document.go` - 2 functions
535-
536-**Total: 58 transaction call sites updated**
537-
538-**Benefits of New API:**
539-- No more `defer txn.Commit()` footgun
540-- Automatic rollback on write errors
541-- Cleaner, more Go-idiomatic code
542-- Easier to reason about transaction lifecycle
543-- Reduced boilerplate (no explicit Commit calls)
M document_test.go
+1, -1
1@@ -1,7 +1,7 @@
2 package ygo_test
3 
4 import (
5-	"github.com/y-crdt/ygo"
6+	"github.com/BTBurke/ygo"
7 	"testing"
8 )
9 
M example_test.go
+1, -1
1@@ -2,7 +2,7 @@ package ygo_test
2 
3 import (
4 	"fmt"
5-	"github.com/y-crdt/ygo"
6+	"github.com/BTBurke/ygo"
7 	"os"
8 	"testing"
9 )
M examples/go.mod
+2, -2
1@@ -2,6 +2,6 @@ module load_example
2 
3 go 1.22
4 
5-replace github.com/y-crdt/ygo => ../
6+replace github.com/BTBurke/ygo => ../
7 
8-require github.com/y-crdt/ygo v0.0.0
9+require github.com/BTBurke/ygo v0.0.0
M examples/load_document.go
+1, -1
1@@ -5,7 +5,7 @@ import (
2 	"log"
3 	"os"
4 
5-	ygo "github.com/y-crdt/ygo"
6+	ygo "github.com/BTBurke/ygo"
7 )
8 
9 // loadAndDisplayDocument reads a Yjs document from a binary file and displays its contents
M go.mod
+1, -1
1@@ -1,3 +1,3 @@
2-module github.com/y-crdt/ygo
3+module github.com/BTBurke/ygo
4 
5 go 1.22
A lib/freebsd-amd64/libyrs.a
+0, -0
A lib/linux-amd64/libyrs.a
+0, -0
M map_test.go
+1, -1
1@@ -1,7 +1,7 @@
2 package ygo_test
3 
4 import (
5-	"github.com/y-crdt/ygo"
6+	"github.com/BTBurke/ygo"
7 	"testing"
8 )
9 
M sticky_test.go
+1, -1
1@@ -3,7 +3,7 @@ package ygo_test
2 import (
3 	"unsafe"
4 
5-	"github.com/y-crdt/ygo"
6+	"github.com/BTBurke/ygo"
7 	"testing"
8 )
9 
M text_test.go
+1, -1
1@@ -1,7 +1,7 @@
2 package ygo_test
3 
4 import (
5-	"github.com/y-crdt/ygo"
6+	"github.com/BTBurke/ygo"
7 	"testing"
8 )
9 
M transaction_test.go
+1, -1
1@@ -1,7 +1,7 @@
2 package ygo_test
3 
4 import (
5-	"github.com/y-crdt/ygo"
6+	"github.com/BTBurke/ygo"
7 	"testing"
8 )
9 
M undo_test.go
+1, -1
1@@ -1,7 +1,7 @@
2 package ygo_test
3 
4 import (
5-	"github.com/y-crdt/ygo"
6+	"github.com/BTBurke/ygo"
7 	"testing"
8 )
9 
M update_test.go
+1, -1
1@@ -1,7 +1,7 @@
2 package ygo_test
3 
4 import (
5-	"github.com/y-crdt/ygo"
6+	"github.com/BTBurke/ygo"
7 	"testing"
8 )
9 
M weak_test.go
+1, -1
1@@ -1,7 +1,7 @@
2 package ygo_test
3 
4 import (
5-	"github.com/y-crdt/ygo"
6+	"github.com/BTBurke/ygo"
7 	"testing"
8 )
9 
M xml_test.go
+1, -1
1@@ -1,7 +1,7 @@
2 package ygo_test
3 
4 import (
5-	"github.com/y-crdt/ygo"
6+	"github.com/BTBurke/ygo"
7 	"strings"
8 	"testing"
9 )