Guidance for AI coding agents working on the automerge package. automerge provides R bindings to the Automerge Conflict-free Replicated Data Type (CRDT) library via its C FFI, enabling automatic merging of concurrent changes across distributed systems without conflicts. Zero R package dependencies (only base R).
Claude Code users: add .claude/CLAUDE.md containing @../AGENTS.md to import this file (.claude/ is gitignored).
- R package with C bindings to the Rust-based automerge-c library
- R layer provides idiomatic S3 methods (
$,[[,length,names,as.list) - C layer wraps the automerge-c API with memory-safe external pointer management
devtools::load_all() # load package for development
devtools::test() # run all tests
testthat::test_file("tests/testthat/test-document.R") # run a single test file
devtools::document() # roxygen2 -> man/, NAMESPACER CMD INSTALL . # install package (triggers configure + build)
R CMD build .
R CMD check automerge_*.tar.gz
./cleanup # clean build artifactsThe package uses a two-phase build approach:
-
System Library Detection: Tries to find system-installed
automerge-c- Searches standard prefixes and pkg-config
- Verifies UTF-32 character indexing compatibility
- Set
AUTOMERGE_LIBS=1to force bundled build
-
Bundled Build: If system library not found, builds from
src/automerge/rust/automerge-c/- Uses CMake with
-DUTF32_INDEXING=ON - Requires Rust >= 1.85.0 and CMake >= 3.25
- Uses CMake with
document.R: Document lifecycle (create, save, load, fork, merge, commit, rollback, clone)objects.R: Object operations (put, get, delete, insert) and type constructorsmethods.R: S3 methods foram_doc,am_object, and subtypessync.R: Synchronization (am_sync, am_sync_encode/decode, change tracking)cursors.R: Cursor and mark operations for textconvenience.R: High-level convenience functionsconstants.R: Exported constants (AM_ROOT,AM_OBJ_TYPE_*,AM_MARK_EXPAND_*)
All R functions are thin .Call() wrappers (e.g., am_put <- function(doc, obj, key, value) invisible(.Call(C_am_put, doc, obj, key, value))). All logic lives in C.
automerge.h: Data structures, function declarations, CHECK_RESULT macrodocument.c: Document lifecycle operationsobjects.c: Map/list/text operations and R↔Automerge type conversionchanges.c: Change wrapping (owned vs borrowed) and introspectionsync.c: Sync protocol implementationcursors.c: Cursor and mark operationsmemory.c: External pointer wrappers and finalizerserrors.c: Error handling with file/line contextinit.c: R package registration (83 C functions)
Air, configured in air.toml (default settings).
- Comments should be sparse and address the "why", not the "what"
1-based indexing (element indices):
- List operations:
am_get(),am_put(),am_delete(),am_insert() - Counter operations in lists
0-based indexing (inter-character positions):
- Text operations:
am_text_splice() - Cursor operations:
am_cursor(),am_cursor_position() - Mark operations:
am_mark(),am_marks() - Positions specify locations between characters (0 = before first character)
Text positions use Unicode code points, not bytes. The emoji "😀" counts as 1 character at position 5 in "Hello😀", matching R's nchar() behavior. JavaScript uses UTF-16, so positions may differ for some Unicode characters.
am_docandam_syncstatestructs wrap AMresult* (owns memory) and borrowed pointers (AMdoc*, AMsyncState*)- External pointers use finalizers to prevent memory leaks
- Protection chain: Nested objects form a chain
obj_id_ptr → result_ptr → doc_ptrviaR_ExternalPtrProtected(). This keeps the parent document alive while nested objects exist, and enablesget_doc_from_objid()to traverse back to the doc — which is howuser$nested$key <- valueworks without passing the doc explicitly - Owned vs borrowed changes:
changes.cuses a dual finalizer pattern — owned changes (am_change_datastruct withAMresult*) are freed by the finalizer, while borrowed changes (rawAMchange*with parent protection) are no-ops. Distinguished by checkingR_ExternalPtrProtected() == R_NilValue
CHECK_RESULT(result, expected_type)macro validates AMresult* status and value type- Automatically frees AMresult* on error before calling
Rf_error()— caller must not use the result after CHECK_RESULT - Includes file/line context for debugging (
__FILE__,__LINE__)
- roxygen2 with markdown;
NAMESPACEis generated — never hand-edit. - Version is
major.minor.patch.dev(current dev tag.9000). AGENTS.md,.claude/, and.posit/are in.Rbuildignoreand don't ship to CRAN.