oficina / docs Public

core: JS reads what Rust writes, and gate both directions

One direction is not compatibility -- a Rust implementation that reads
JS updates but writes something JS cannot read would have passed the
previous task completely.

verify-rust.mjs checks toDelta() as well as toString(), not just plain
text: emit_update.rs applies a bold Text::format() over part of the
inserted run, so the emitted update carries real formatting bytes, and
a Rust side that dropped formatting fails the delta assertion (plain
text alone can't tell the difference, the same gap Task 2 had to
close). Verified by temporarily removing the format() call: the text
assertion still passed but the delta assertion failed as expected,
then reverted.

wire-gate.sh also fails if regenerating fixtures changes them, which
makes 'never edit a fixture to make a test pass' mechanical.

build-wasm.sh now builds --lib only: emit_update is native-only
tooling for the host side of wire-gate.sh, and building it for
wasm32-unknown-unknown fails to link (it never defines
__getrandom_v03_custom itself) since it was never meant to run there;
the shipped wasm artifact is the library crate regardless.
Paul Campbell pushed by paul@keymail.dev c52c1391b12466ae72fd6378e2f68e4fc2f80c82 parent de0bc75
4 files changed, +100 −1
  • core/src/bin/emit_update.rs +29 −0
  • fixtures/verify-rust.mjs +42 −0
  • scripts/build-wasm.sh +6 −1
  • scripts/wire-gate.sh +23 −0
diff --git a/core/src/bin/emit_update.rs b/core/src/bin/emit_update.rs
new file mode 100644
index 0000000..f1c1f51
--- /dev/null
+++ b/core/src/bin/emit_update.rs
@@ -0,0 +1,29 @@
+//! Builds a document in Rust and writes a v1 update to stdout, so the JS
+//! side can verify it reads what Rust writes. This is the direction Task 3
+//! cannot test: a Rust implementation that reads JS updates but writes
+//! something JS cannot read would have passed Task 3 completely.
+use std::io::Write;
+use yrs::types::Attrs;
+use yrs::{Doc, Options, ReadTxn, Text, Transact};
+
+fn main() {
+ // Both values fixed: never drawn from yrs (Global Constraints -- see
+ // core/src/lib.rs and clippy.toml, which bans the alternatives that draw
+ // entropy).
+ let guid: yrs::Uuid = "emit-update-guid".into();
+ let doc = Doc::with_options(Options::with_guid_and_client_id(guid, 7001));
+ {
+ let text = doc.get_or_insert_text("content");
+ let mut txn = doc.transact_mut();
+ text.insert(&mut txn, 0, "from rust 🦀 café");
+ // Format part of the inserted text so the emitted update carries
+ // real formatting bytes, not just plain-text inserts. A plain-text
+ // check alone cannot distinguish a correct implementation from one
+ // that dropped formatting (see fixtures/verify-rust.mjs).
+ let attrs: Attrs = Attrs::from([("bold".into(), true.into())]);
+ text.format(&mut txn, 5, 4, attrs);
+ }
+ let txn = doc.transact();
+ let update = txn.encode_state_as_update_v1(&yrs::StateVector::default());
+ std::io::stdout().write_all(&update).unwrap();
+}
diff --git a/fixtures/verify-rust.mjs b/fixtures/verify-rust.mjs
new file mode 100644
index 0000000..4a032c0
--- /dev/null
+++ b/fixtures/verify-rust.mjs
@@ -0,0 +1,42 @@
+// Reads a Rust-produced v1 update on stdin and asserts JS yjs decodes it to
+// the expected text AND the expected formatting. This is the direction
+// Task 3 cannot test. Plain text alone cannot distinguish a correct
+// implementation from one that dropped formatting, so this also checks
+// toDelta(), the way fixtures/generate.mjs's expected.json does.
+import * as Y from 'yjs'
+import assert from 'node:assert/strict'
+
+const EXPECTED_TEXT = 'from rust 🦀 café'
+const EXPECTED_DELTA = [
+ { insert: 'from ' },
+ { insert: 'rust', attributes: { bold: true } },
+ { insert: ' 🦀 café' }
+]
+
+const chunks = []
+for await (const chunk of process.stdin) chunks.push(chunk)
+const update = new Uint8Array(Buffer.concat(chunks))
+
+if (update.length === 0) {
+ console.error('verify-rust: empty update on stdin')
+ process.exit(1)
+}
+
+const doc = new Y.Doc()
+Y.applyUpdate(doc, update)
+const text = doc.getText('content')
+const actualText = text.toString()
+const actualDelta = text.toDelta()
+
+try {
+ assert.strictEqual(actualText, EXPECTED_TEXT)
+ assert.deepStrictEqual(actualDelta, EXPECTED_DELTA)
+} catch (err) {
+ console.error('verify-rust: mismatch')
+ console.error(` text: expected ${JSON.stringify(EXPECTED_TEXT)}, got ${JSON.stringify(actualText)}`)
+ console.error(` delta: expected ${JSON.stringify(EXPECTED_DELTA)}, got ${JSON.stringify(actualDelta)}`)
+ console.error(err.message)
+ process.exit(1)
+}
+
+console.log(`verify-rust: ok (${update.length} bytes)`)
diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh
index d2cf2e2..12dd863 100755
--- a/scripts/build-wasm.sh
+++ b/scripts/build-wasm.sh
@@ -9,7 +9,12 @@ cd "$(dirname "$0")/.."
# for. A doc comment alone has been wrong, or gotten around, more than
# once; this is the actual enforcement.
cargo clippy --manifest-path core/Cargo.toml --all-targets -- -D warnings -D clippy::disallowed_methods
-cargo build --release --manifest-path core/Cargo.toml --target wasm32-unknown-unknown --locked
+# --lib only: the wasm module shipped to wazero is the library crate. Native
+# tooling under src/bin (e.g. emit_update, used from the host side of
+# wire-gate.sh) is never part of that artifact and does not define the
+# __getrandom_v03_custom symbol itself, so building it for wasm32 fails to
+# link -- it was never meant to run there.
+cargo build --release --manifest-path core/Cargo.toml --target wasm32-unknown-unknown --locked --lib
out="core/target/wasm32-unknown-unknown/release/docs_core.wasm"
[ -f "$out" ] || { echo "build-wasm: no artifact at $out" >&2; exit 1; }
echo "$out ($(wc -c < "$out") bytes)"
diff --git a/scripts/wire-gate.sh b/scripts/wire-gate.sh
new file mode 100755
index 0000000..9ce81af
--- /dev/null
+++ b/scripts/wire-gate.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+# The wire-compatibility gate, both directions. This is the CRDT half of the
+# spec's §0: two implementations, fixtures as the contract.
+set -euo pipefail
+cd "$(dirname "$0")/.."
+
+echo "--- regenerating fixtures from JS yjs"
+(cd fixtures && node generate.mjs)
+
+echo "--- fixtures must be unchanged (they are the contract, not an output)"
+if ! git diff --quiet -- fixtures/data; then
+ echo "wire-gate: fixtures changed. If deliberate, commit them WITH the rule change." >&2
+ git diff --stat -- fixtures/data >&2
+ exit 1
+fi
+
+echo "--- Rust reads what JS wrote"
+(cd core && cargo test --test wire_compat)
+
+echo "--- JS reads what Rust wrote"
+(cd core && cargo run --quiet --bin emit_update) | node fixtures/verify-rust.mjs
+
+echo "wire-gate: ok"