Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
557b83a
Preserve text following horizontal rules
cursoragent Aug 8, 2026
7830893
Extend native and reference Automerge backends
cursoragent Aug 9, 2026
fe8e024
Expand public Automerge API for parity
cursoragent Aug 9, 2026
5cd4caa
Add upstream Automerge parity test suite
cursoragent Aug 9, 2026
e6c1e2c
Add Automerge parity tooling and benchmarks
cursoragent Aug 9, 2026
8863a5a
Document Automerge parity plan
cursoragent Aug 9, 2026
c4e50d5
Record author's prior change in change dependencies
cursoragent Aug 9, 2026
c5ae7be
Expose list element object replacement
cursoragent Aug 9, 2026
e1aa61b
Cover Rust core-model conflict and history parity
cursoragent Aug 9, 2026
421e141
Cover Rust list-indexing, rollback, and delete-actor parity
cursoragent Aug 9, 2026
6c19c09
Cover empty-commit and op-tree boundary save/load parity
cursoragent Aug 9, 2026
6599bb0
Resolve list-index conflicts against all visible values
cursoragent Aug 9, 2026
1c1db76
Cover Rust list counter deletion parity
cursoragent Aug 9, 2026
7b9ca5f
Cover Rust read-only synchronization parity
cursoragent Aug 9, 2026
90135b3
Cover Rust batch-insertion parity
cursoragent Aug 9, 2026
a6072d6
Cover Rust incremental orphan-load rejection
cursoragent Aug 9, 2026
f3fab7f
Fix native sync message generation edge cases
cursoragent Aug 9, 2026
51f9cd1
Cover previously deferred read-only sync parity
cursoragent Aug 9, 2026
dc6cd17
Cover Rust rich-text span parity
cursoragent Aug 9, 2026
66c6ed3
Include block markers in the rich-text index space
cursoragent Aug 9, 2026
812288a
Cover block-adjacent mark and splice parity
cursoragent Aug 9, 2026
c887260
Normalize splice positions inside UTF-16 characters
cursoragent Aug 9, 2026
c425d0c
Cover UTF-16 mid-character splice parity
cursoragent Aug 9, 2026
e78f5ec
Preserve expanding marks across boundary replacements
cursoragent Aug 9, 2026
bdc0b62
Cover mark-boundary splice parity
cursoragent Aug 9, 2026
f158d71
Expose active text marks and historical marks
cursoragent Aug 9, 2026
bdc86ad
Cover active-mark reads and record expansion defect
cursoragent Aug 9, 2026
f6cd639
Cover branchy synchronization convergence
cursoragent Aug 9, 2026
3965069
Cover JavaScript Date interoperability in maps and lists
cursoragent Aug 9, 2026
3fe0b7e
Cover reusing JavaScript dates across documents
cursoragent Aug 9, 2026
5956470
Expose document statistics
cursoragent Aug 9, 2026
5571974
Cover document statistics parity
cursoragent Aug 9, 2026
6a22995
Add current-state materialization patches
cursoragent Aug 9, 2026
d6428af
Cover current-state materialization parity
cursoragent Aug 9, 2026
350d336
Cover remaining current-state materialization cases
cursoragent Aug 9, 2026
6e09542
Add diff patches between two frontiers
cursoragent Aug 9, 2026
5ffdf25
Cover diff reversal parity
cursoragent Aug 9, 2026
2c2a61e
Cover large list diff index parity
cursoragent Aug 9, 2026
4de11d9
Add grapheme-aware update_text with Rust parity
cursoragent Aug 9, 2026
cdf5781
Index text operations by UTF-16 code units
cursoragent Aug 9, 2026
da8e0ac
Diff text sequences by UTF-16 with deletions
cursoragent Aug 9, 2026
25620fa
Emit mark patches from text diffs
cursoragent Aug 9, 2026
ea832ec
Add incremental diff cursor and patch log
cursoragent Aug 9, 2026
56bcbf7
Carry marks on text splice patches
cursoragent Aug 9, 2026
51a4078
Derive mark patches from mark operations
cursoragent Aug 9, 2026
cc2dfe9
Expand marks across same-anchor insertions
cursoragent Aug 9, 2026
f423b30
Cover cross-page mark patch isolation
cursoragent Aug 9, 2026
41a2ed9
Add update_spans for text and marks
cursoragent Aug 9, 2026
c3f4706
Support blocks in update_spans
cursoragent Aug 9, 2026
7804f80
Cover batch-insert patch generation
cursoragent Aug 9, 2026
a5aab3d
Add historical spans and block diff materialization
cursoragent Aug 9, 2026
84d7486
Cover JavaScript text behaviors
cursoragent Aug 9, 2026
efec1da
Cover JavaScript block behaviors
cursoragent Aug 9, 2026
82c789e
Cover transaction rollback parity
cursoragent Aug 9, 2026
056f07c
Cover incremental mark patch loading
cursoragent Aug 9, 2026
23367d9
Cover list-range values and conflicts
cursoragent Aug 9, 2026
b9117d9
Add string-to-text load migration
cursoragent Aug 9, 2026
2e1e75c
Add historical text cursors
cursoragent Aug 9, 2026
745251d
Cover clock ancestry and scope internal doctests
cursoragent Aug 9, 2026
7f529c0
Cover whole-document first sync response
cursoragent Aug 9, 2026
7ef435c
Scope legacy sync out and cover change encoding
cursoragent Aug 9, 2026
bfd08e1
Derive mark patches from operations, not state
cursoragent Aug 9, 2026
a58d144
Scope internal-only upstream cases
cursoragent Aug 9, 2026
d09c6cc
Cover loading current state from a fixture
cursoragent Aug 9, 2026
2de0183
Cover unmarked insertion after a block span
cursoragent Aug 9, 2026
d8dc5ad
Expose bloom, isolation, and save reference FFIs
cursoragent Aug 9, 2026
c319204
Reach complete V2 interoperability parity
cursoragent Aug 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ GOLINTCMD ?= golangci-lint
SWIFTLINTCMD ?= swiftlint
SWIFTCMD ?= swift
RUST_TOOLCHAIN ?= 1.89.0
AUTOMERGE_FUZZ_TIME ?= 10s
SWIFT_ENROLL_UI ?= cmd/probo-agent/installer/macos/enroll-ui
SWIFT_FORMAT_CONFIG ?= .swift-format
SWIFTLINT_CONFIG ?= .swiftlint.yml
Expand Down Expand Up @@ -219,6 +220,34 @@ test-automerge-conformance: ## Test Go binary compatibility with official Autome
AUTOMERGE_JS_ORACLE=$(CURDIR)/packages/automerge-conformance/oracle.mjs \
$(GO_BASE) test -count=1 ./pkg/automerge

.PHONY: audit-automerge-parity
audit-automerge-parity: test-automerge-conformance
audit-automerge-parity: ## Require every pinned upstream Automerge test to be mapped
AUTOMERGE_REQUIRE_FULL_PARITY=1 \
$(GO_BASE) test -count=1 -run '^TestUpstreamParityManifest$$' ./pkg/automerge

.PHONY: audit-automerge-interop
audit-automerge-interop: test-automerge-conformance
audit-automerge-interop: ## Require complete Rust/JS wire and state interoperability
AUTOMERGE_REQUIRE_FULL_INTEROP=1 \
$(GO_BASE) test -count=1 -run '^TestUpstreamParityManifest$$' ./pkg/automerge

.PHONY: benchmark-automerge
benchmark-automerge: ## Benchmark native and Rust/WASM Automerge engines
$(GO_BASE) test -run '^$$' -bench . -benchmem ./pkg/automerge

.PHONY: benchmark-automerge-native
benchmark-automerge-native: ## Compare optimized native Go and native Rust
$(NPM) -w @probo/automerge-benchmark run compare

.PHONY: fuzz-automerge
fuzz-automerge: ## Fuzz Automerge public, wire, sync, and projection surfaces
$(GO_BASE) test -run '^$$' -fuzz '^FuzzLoad$$' -fuzztime=$(AUTOMERGE_FUZZ_TIME) ./pkg/automerge
$(GO_BASE) test -run '^$$' -fuzz '^FuzzCoreOperations$$' -fuzztime=$(AUTOMERGE_FUZZ_TIME) ./pkg/automerge
$(GO_BASE) test -run '^$$' -fuzz '^FuzzDecode$$' -fuzztime=$(AUTOMERGE_FUZZ_TIME) ./pkg/automerge/internal/native
$(GO_BASE) test -run '^$$' -fuzz '^FuzzParseSyncMessage$$' -fuzztime=$(AUTOMERGE_FUZZ_TIME) ./pkg/automerge/internal/native
$(GO_BASE) test -run '^$$' -fuzz '^FuzzRender$$' -fuzztime=$(AUTOMERGE_FUZZ_TIME) ./pkg/automerge/prosemirror

.PHONY: coverage-report
coverage-report: test ## Generate HTML coverage report
$(GO) tool cover -html=coverage.out -o coverage.html
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ require (
github.com/pdfcpu/pdfcpu v0.13.0
github.com/pires/go-proxyproto v0.15.0
github.com/prometheus/client_golang v1.24.1
github.com/rivo/uniseg v0.4.7
github.com/scim2/filter-parser/v2 v2.3.1
github.com/sigstore/sigstore-go v1.3.0
github.com/stretchr/testify v1.11.1
Expand Down Expand Up @@ -137,7 +138,6 @@ require (
github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/q-uint/parser v0.3.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/automerge-benchmark/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/rust/target/
/go/probo-automerge-go-benchmark
167 changes: 167 additions & 0 deletions packages/automerge-benchmark/compare.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";

const directory = path.dirname(fileURLToPath(import.meta.url));
const repository = path.resolve(directory, "../..");
const goBinary = path.join(os.tmpdir(), "probo-automerge-go-benchmark");
const fixture = path.join(os.tmpdir(), "probo-automerge-benchmark-fixture");
const rustManifest = path.join(directory, "rust", "Cargo.toml");
const rustBinary = path.join(
directory,
"rust",
"target",
"release",
"probo-automerge-native-benchmark",
);

const scenarios = [
{ workload: "create", size: 0, iterations: 100_000 },
{ workload: "map", size: 100, iterations: 500 },
{ workload: "map", size: 1_000, iterations: 30 },
{ workload: "text", size: 100, iterations: 500 },
{ workload: "text", size: 1_000, iterations: 30 },
{ workload: "load", size: 10_000, iterations: 50 },
{ workload: "save", size: 10_000, iterations: 1_000 },
];
const samples = 3;
const goVersion = execFileSync("go", ["version"], { encoding: "utf8" }).trim();
const rustVersion = execFileSync("rustc", ["+1.89.0", "--version"], {
encoding: "utf8",
}).trim();

execFileSync(
"go",
["build", "-trimpath", "-ldflags=-s -w", "-o", goBinary, "./packages/automerge-benchmark/go"],
{ cwd: repository, stdio: "inherit" },
);
execFileSync(
"cargo",
[
"+1.89.0",
"build",
"--release",
"--locked",
"--manifest-path",
rustManifest,
],
{ cwd: repository, stdio: "inherit" },
);
execFileSync(
goBinary,
[
"--workload",
"fixture",
"--size",
"10000",
"--fixture",
fixture,
],
{ stdio: "inherit" },
);

const rows = [];
try {
for (const scenario of scenarios) {
const go = measure(goBinary, scenario);
const rust = measure(rustBinary, scenario);
if (go.checksum !== rust.checksum) {
throw new Error(
`checksum mismatch for ${scenario.workload}/${scenario.size}: Go ${go.checksum}, Rust ${rust.checksum}`,
);
}

rows.push({
workload: scenario.workload,
size: scenario.size,
goNS: go.ns,
rustNS: rust.ns,
ratio: rust.ns / go.ns,
});
}
} finally {
fs.rmSync(goBinary, { force: true });
fs.rmSync(fixture, { force: true });
}

process.stdout.write(`Host: ${os.platform()}/${os.arch()} — ${os.cpus()[0]?.model ?? "unknown CPU"}\n`);
process.stdout.write(`Go: ${goVersion}\n`);
process.stdout.write(`Rust: ${rustVersion}\n`);
process.stdout.write(`Samples: ${samples} (median reported)\n\n`);
process.stdout.write("| Workload | Size | Native Go | Native Rust | Rust/Go |\n");
process.stdout.write("|---|---:|---:|---:|---:|\n");
for (const row of rows) {
process.stdout.write(
`| ${row.workload} | ${row.size || "—"} | ${duration(row.goNS)} | ${duration(row.rustNS)} | ${row.ratio.toFixed(2)}x |\n`,
);
}

function measure(binary, scenario) {
const values = [];
let checksum;
for (let sample = 0; sample < samples; sample++) {
const commandArguments = [
"--workload",
scenario.workload,
"--size",
String(scenario.size),
"--iterations",
String(scenario.iterations),
"--warmups",
"3",
];
if (scenario.workload === "load" || scenario.workload === "save") {
commandArguments.push("--fixture", fixture);
}
const output = execFileSync(
binary,
commandArguments,
{ encoding: "utf8" },
);
const result = JSON.parse(output);
values.push(Number(result.nsPerOp));
checksum ??= result.checksum;
if (checksum !== result.checksum) {
throw new Error(
`${binary} produced unstable checksums for ${scenario.workload}/${scenario.size}`,
);
}
}
values.sort((left, right) => left - right);
return {
checksum,
ns: values[Math.floor(values.length / 2)],
};
}

function duration(nanoseconds) {
if (nanoseconds < 1_000) return `${nanoseconds.toFixed(0)} ns`;
if (nanoseconds < 1_000_000) return `${(nanoseconds / 1_000).toFixed(2)} µs`;
if (nanoseconds < 1_000_000_000) {
return `${(nanoseconds / 1_000_000).toFixed(2)} ms`;
}
return `${(nanoseconds / 1_000_000_000).toFixed(2)} s`;
}
Loading
Loading