Skip to main content

Real-time collaborative text file sync via CRDT

Project description

entangle — Real-Time Collaborative File Sync via CRDT

2026-04-23T02:38:53Z by Showboat 0.6.1

entangle makes a single local text file collaboratively editable across machines in real time. Run one command to share a file; anyone with the link syncs it to their filesystem and edits it with any program — vim, VS Code, sed. Changes merge automatically via CRDT (Conflict-free Replicated Data Type).

Architecture layers built in this session:

  • differ — character-level text diff → positional insert/delete ops
  • crdt — yrs (Yjs-compatible) CRDT engine wrapping a Y.Text
  • protocol — lib0 varint encoding for the y-websocket sync wire format
  • watcher — notify v6 filesystem watcher bridged to tokio async channels
  • writer — atomic file writes with a suppress flag to break echo loops
  • session — async event loop tying all layers together with debounce and reconnect
  • room — random room ID generation and share-link formatting

Build

Build the release binary from the Cargo workspace.

cargo build --release 2>&1 | tail -3
    Finished `release` profile [optimized] target(s) in 0.10s

CLI Interface

entangle exposes two subcommands: share and join.

./target/release/entangle --help
Real-time collaborative text file sync via CRDT

Usage: entangle [OPTIONS] <COMMAND>

Commands:
  share  Share a local file with others
  join   Join a shared file by its link
  help   Print this message or the help of the given subcommand(s)

Options:
  -v, --verbose  Enable debug logging
  -h, --help     Print help
  -V, --version  Print version
./target/release/entangle share --help
Share a local file with others

Usage: entangle share [OPTIONS] --server <SERVER> <FILE>

Arguments:
  <FILE>  Path to the file to share

Options:
      --server <SERVER>                y-websocket relay URL (e.g. wss://relay.example.com)
      --room <ROOM>                    Override the room name (default: auto-generated)
      --debounce <DEBOUNCE>            Debounce interval for file watcher (ms) [default: 300]
  -v, --verbose                        Enable debug logging
      --poll-interval <POLL_INTERVAL>  Fallback poll interval (ms) [default: 2000]
  -h, --help                           Print help
./target/release/entangle join --help
Join a shared file by its link

Usage: entangle join [OPTIONS] <URL>

Arguments:
  <URL>  Share link (e.g. wss://relay.example.com/r/<room-id>)

Options:
  -o, --output <OUTPUT>                Local path to write the synced file
      --debounce <DEBOUNCE>            Debounce interval for file watcher (ms) [default: 300]
      --poll-interval <POLL_INTERVAL>  Fallback poll interval (ms) [default: 2000]
  -v, --verbose                        Enable debug logging
  -h, --help                           Print help

Layer 1 — Differ

compute_diff(old, new) produces a sequence of positional insert/delete ops that transform old into new when applied left-to-right to a Y.Text. Positions are tracked in the current CRDT state frame: deletes do not advance the cursor (they remove chars at that position), while inserts advance it by the number of chars inserted.

cargo test --lib differ:: 2>&1 | grep -E 'test |FAILED|ok\.' | head -30
test differ::tests::append_lines ... ok
test differ::tests::emoji ... ok
test differ::tests::full_replace ... ok
test differ::tests::empty_to_nonempty ... ok
test differ::tests::identical_returns_no_ops ... ok
test differ::tests::nonempty_to_empty ... ok
test differ::tests::multiline ... ok
test differ::tests::prepend_lines ... ok
test differ::tests::replace_at_end ... ok
test differ::tests::replace_at_start ... ok
test differ::tests::replace_word ... ok
test differ::tests::single_char_delete ... ok
test differ::tests::single_char_insert ... ok
test differ::tests::unicode_multibyte ... ok
test differ::tests::large_document_prefix_insert ... ok
test differ::tests::large_document_append ... ok
test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 23 filtered out; finished in 0.01s

A quick inline demonstration: computing the diff between two strings and printing the ops.

# Simulate the differ logic in Python to show the op sequence
# (matches exactly what compute_diff produces in Rust)
import difflib

def compute_diff(old, new):
    ops = []
    crdt_pos = 0
    matcher = difflib.SequenceMatcher(None, list(old), list(new), autojunk=False)
    for tag, i1, i2, j1, j2 in matcher.get_opcodes():
        if tag == 'equal':
            crdt_pos += i2 - i1
        elif tag == 'delete':
            ops.append(('delete', crdt_pos, i2 - i1))
        elif tag == 'insert':
            s = new[j1:j2]
            ops.append(('insert', crdt_pos, s))
            crdt_pos += len(s)
        elif tag == 'replace':
            ops.append(('delete', crdt_pos, i2 - i1))
            s = new[j1:j2]
            ops.append(('insert', crdt_pos, s))
            crdt_pos += len(s)
    return ops

pairs = [
    ('hello world', 'hello beautiful world'),
    ('hello world', 'hi world'),
    ('abcde', 'ace'),
    ('你好', '你好世界'),
]
for old, new in pairs:
    ops = compute_diff(old, new)
    print(f'  {old!r:25} -> {new!r:25}  ops: {ops}')
  'hello world'             -> 'hello beautiful world'    ops: [('insert', 6, 'beautiful ')]
  'hello world'             -> 'hi world'                 ops: [('delete', 1, 4), ('insert', 1, 'i')]
  'abcde'                   -> 'ace'                      ops: [('delete', 1, 1), ('delete', 2, 1)]
  '你好'                      -> '你好世界'                     ops: [('insert', 2, '世界')]

Layer 2 — CRDT Engine

CrdtEngine wraps a yrs::Doc with a single Y.Text named "content". Key methods:

  • seed(content) — populate the doc with initial text (share command startup)
  • apply_local_edit(new_content) — diff snapshot → new, apply ops, return encoded delta bytes
  • apply_remote_update(bytes) — decode and apply a remote delta, return new text if changed
  • state_vector_bytes() — encode our state vector for SyncStep1
  • encode_state_as_update(peer_sv) — encode what the peer is missing for SyncStep2
cargo test --lib crdt:: 2>&1 | grep -E 'test |result:'
test crdt::tests::identical_edit_produces_no_update ... ok
test crdt::tests::remote_update_round_trip ... ok
test crdt::tests::local_edit_produces_update ... ok
test crdt::tests::concurrent_edits_converge ... ok
test crdt::tests::state_vector_sync_protocol ... ok
test crdt::tests::seed_and_read ... ok
test crdt::tests::two_peer_convergence ... ok
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 32 filtered out; finished in 0.00s

The concurrent_edits_converge test verifies the core CRDT guarantee: two peers making independent edits to the same base text will always arrive at the same final state after exchanging updates, regardless of edit order.

cargo test --lib crdt::tests::concurrent_edits_converge -- --nocapture 2>&1
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.12s
     Running unittests src/lib.rs (target/debug/deps/entangle-d26086c8a098f676)

running 1 test
test crdt::tests::concurrent_edits_converge ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 38 filtered out; finished in 0.00s

Layer 3 — y-websocket Protocol

protocol.rs implements the y-sync wire format using lib0 variable-length integer encoding. Each WebSocket binary frame is: [msg_type:varint][sync_type:varint][payload_len:varint][payload bytes].

Message types:

  • encode_sync_step1(sv) → sent on connect, announces our state to the relay
  • encode_sync_step2(update) → reply to a peer's SyncStep1, sends what they're missing
  • encode_update(update) → incremental update after each local edit
  • decode_message(bytes) → parses any incoming frame into a SyncMessage enum
cargo test --lib protocol:: 2>&1 | grep -E 'test |result:'
test protocol::tests::round_trip_sync_step1 ... ok
test protocol::tests::truncated_message_returns_none ... ok
test protocol::tests::round_trip_sync_step2 ... ok
test protocol::tests::round_trip_update ... ok
test protocol::tests::varint_multibyte ... ok
test protocol::tests::unknown_msg_type_returns_none ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 33 filtered out; finished in 0.00s

A concrete look at the wire format — encoding a SyncStep1 and inspecting the bytes:

def write_varint(n):
    buf = []
    while True:
        b = n & 0x7F
        n >>= 7
        if n == 0:
            buf.append(b)
            break
        buf.append(b | 0x80)
    return bytes(buf)

def write_var_bytes(data):
    return write_varint(len(data)) + data

def encode_sync_step1(sv: bytes) -> bytes:
    return write_varint(0) + write_varint(0) + write_var_bytes(sv)

def encode_update(update: bytes) -> bytes:
    return write_varint(0) + write_varint(2) + write_var_bytes(update)

sv = bytes([1, 0, 1, 5])          # example state vector bytes
frame = encode_sync_step1(sv)
print(f'SyncStep1 frame : {list(frame)}')
print(f'  byte 0 = {frame[0]} (msg_type=sync)')
print(f'  byte 1 = {frame[1]} (sync_type=step1)')
print(f'  byte 2 = {frame[2]} (payload length={frame[2]})')
print(f'  rest   = {list(frame[3:])} (state vector)')

update = bytes([0xde, 0xad, 0xbe, 0xef])
frame2 = encode_update(update)
print()
print(f'Update frame    : {list(frame2)}')
print(f'  byte 0 = {frame2[0]} (msg_type=sync)')
print(f'  byte 1 = {frame2[1]} (sync_type=update)')
print(f'  byte 2 = {frame2[2]} (payload length={frame2[2]})')
SyncStep1 frame : [0, 0, 4, 1, 0, 1, 5]
  byte 0 = 0 (msg_type=sync)
  byte 1 = 0 (sync_type=step1)
  byte 2 = 4 (payload length=4)
  rest   = [1, 0, 1, 5] (state vector)

Update frame    : [0, 2, 4, 222, 173, 190, 239]
  byte 0 = 0 (msg_type=sync)
  byte 1 = 2 (sync_type=update)
  byte 2 = 4 (payload length=4)

Layer 4 — File Watcher

watcher::spawn_watcher starts a notify v6 RecommendedWatcher on the parent directory of the target file (watching a single file directly is unreliable on some platforms when editors use atomic save-via-rename). Events are filtered to the target filename and bridged to a tokio channel via spawn_blocking. The suppress flag (an AtomicBool shared with the writer) prevents entangle from reacting to its own writes.

Layer 5 — Atomic File Writer

writer::write_file_atomic avoids partial reads by writing to a temp file in the same directory then renaming over the target. Before writing it sets the suppress flag so the watcher ignores the resulting fs event; a short-lived tokio task clears it 50 ms later.

cargo test --lib writer:: 2>&1 | grep -E 'test |result:'
test writer::tests::no_tmp_file_remains ... ok
test writer::tests::atomic_write_creates_file ... ok
test writer::tests::suppress_is_set_then_cleared ... ok
test writer::tests::overwrites_existing_file ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 35 filtered out; finished in 0.13s

Layer 6 — Room ID & Share Links

room::generate_room_id produces 8 random bytes hex-encoded (16 chars), e.g. a3f8c2e9b1d4f067. The share link embeds the WebSocket URL and room ID in a form the joiner can paste directly into their terminal.

cargo test --lib room:: 2>&1 | grep -E 'test |result:'
test room::tests::parse_room_id_empty_room ... ok
test room::tests::parse_room_id_missing_prefix ... ok
test room::tests::parse_room_id_valid ... ok
test room::tests::share_link_format ... ok
test room::tests::room_id_is_16_hex_chars ... ok
test room::tests::room_ids_are_unique ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 33 filtered out; finished in 0.00s

Property-Based Tests (proptest)

Four proptest properties exercise the differ + CRDT stack with randomly generated inputs:

  1. diff_roundtrip_ascii — for any two ASCII strings, applying compute_diff ops to the old string always yields the new string
  2. diff_roundtrip_unicode — same for arbitrary Unicode
  3. crdt_roundtrip — applying a local edit to a CrdtEngine always results in the engine holding the new content
  4. two_peer_convergence — two peers seeded with the same base, each making an independent edit, always converge after exchanging updates
cargo test --test differ_proptest 2>&1 | grep -E 'test |result:|FAILED'
test diff_roundtrip_unicode ... ok
test crdt_roundtrip ... ok
test two_peer_convergence ... ok
test diff_roundtrip_ascii ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.59s

Full Test Suite

All 43 tests across 39 unit tests + 4 property-based tests:

cargo test 2>&1 | grep -E '^test |^test result:'
test crdt::tests::identical_edit_produces_no_update ... ok
test crdt::tests::local_edit_produces_update ... ok
test crdt::tests::remote_update_round_trip ... ok
test crdt::tests::concurrent_edits_converge ... ok
test crdt::tests::seed_and_read ... ok
test differ::tests::append_lines ... ok
test differ::tests::emoji ... ok
test crdt::tests::state_vector_sync_protocol ... ok
test differ::tests::identical_returns_no_ops ... ok
test differ::tests::full_replace ... ok
test differ::tests::empty_to_nonempty ... ok
test crdt::tests::two_peer_convergence ... ok
test differ::tests::nonempty_to_empty ... ok
test differ::tests::multiline ... ok
test differ::tests::prepend_lines ... ok
test differ::tests::replace_at_end ... ok
test differ::tests::replace_at_start ... ok
test differ::tests::replace_word ... ok
test differ::tests::single_char_delete ... ok
test differ::tests::single_char_insert ... ok
test differ::tests::unicode_multibyte ... ok
test protocol::tests::round_trip_sync_step1 ... ok
test protocol::tests::round_trip_sync_step2 ... ok
test protocol::tests::round_trip_update ... ok
test protocol::tests::truncated_message_returns_none ... ok
test protocol::tests::unknown_msg_type_returns_none ... ok
test protocol::tests::varint_multibyte ... ok
test room::tests::parse_room_id_empty_room ... ok
test room::tests::parse_room_id_missing_prefix ... ok
test room::tests::parse_room_id_valid ... ok
test room::tests::room_id_is_16_hex_chars ... ok
test room::tests::share_link_format ... ok
test room::tests::room_ids_are_unique ... ok
test writer::tests::no_tmp_file_remains ... ok
test differ::tests::large_document_prefix_insert ... ok
test writer::tests::atomic_write_creates_file ... ok
test differ::tests::large_document_append ... ok
test writer::tests::suppress_is_set_then_cleared ... ok
test writer::tests::overwrites_existing_file ... ok
test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test diff_roundtrip_unicode ... ok
test crdt_roundtrip ... ok
test two_peer_convergence ... ok
test diff_roundtrip_ascii ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.64s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Showboat: Verify, Extract, Pop

These three showboat commands close the loop on document integrity.

extract — prints the sequence of showboat CLI commands that would rebuild this document from scratch. Useful for auditing or replaying the demo in a fresh environment.

uvx showboat extract /home/user/entangle/demo.md 2>&1
showboat init /home/user/entangle/demo.md 'entangle — Real-Time Collaborative File Sync via CRDT'
showboat note /home/user/entangle/demo.md 'entangle makes a single local text file collaboratively editable across machines in real time. Run one command to share a file; anyone with the link syncs it to their filesystem and edits it with any program — vim, VS Code, sed. Changes merge automatically via CRDT (Conflict-free Replicated Data Type).

Architecture layers built in this session:
- **differ** — character-level text diff → positional insert/delete ops
- **crdt** — yrs (Yjs-compatible) CRDT engine wrapping a Y.Text
- **protocol** — lib0 varint encoding for the y-websocket sync wire format
- **watcher** — notify v6 filesystem watcher bridged to tokio async channels
- **writer** — atomic file writes with a suppress flag to break echo loops
- **session** — async event loop tying all layers together with debounce and reconnect
- **room** — random room ID generation and share-link formatting

## Build

Build the release binary from the Cargo workspace.'
showboat exec /home/user/entangle/demo.md bash 'cargo build --release 2>&1 | tail -3'
showboat note /home/user/entangle/demo.md '## CLI Interface

entangle exposes two subcommands: `share` and `join`.'
showboat exec /home/user/entangle/demo.md bash './target/release/entangle --help'
showboat exec /home/user/entangle/demo.md bash './target/release/entangle share --help'
showboat exec /home/user/entangle/demo.md bash './target/release/entangle join --help'
showboat note /home/user/entangle/demo.md '## Layer 1 — Differ

`compute_diff(old, new)` produces a sequence of positional insert/delete ops that transform `old` into `new` when applied left-to-right to a Y.Text. Positions are tracked in the *current CRDT state* frame: deletes do not advance the cursor (they remove chars at that position), while inserts advance it by the number of chars inserted.'
showboat exec /home/user/entangle/demo.md bash 'cargo test --lib differ:: 2>&1 | grep -E '\''test |FAILED|ok\.'\'' | head -30'
showboat note /home/user/entangle/demo.md 'A quick inline demonstration: computing the diff between two strings and printing the ops.'
showboat exec /home/user/entangle/demo.md python3 '
# Simulate the differ logic in Python to show the op sequence
# (matches exactly what compute_diff produces in Rust)
import difflib

def compute_diff(old, new):
    ops = []
    crdt_pos = 0
    matcher = difflib.SequenceMatcher(None, list(old), list(new), autojunk=False)
    for tag, i1, i2, j1, j2 in matcher.get_opcodes():
        if tag == '\''equal'\'':
            crdt_pos += i2 - i1
        elif tag == '\''delete'\'':
            ops.append(('\''delete'\'', crdt_pos, i2 - i1))
        elif tag == '\''insert'\'':
            s = new[j1:j2]
            ops.append(('\''insert'\'', crdt_pos, s))
            crdt_pos += len(s)
        elif tag == '\''replace'\'':
            ops.append(('\''delete'\'', crdt_pos, i2 - i1))
            s = new[j1:j2]
            ops.append(('\''insert'\'', crdt_pos, s))
            crdt_pos += len(s)
    return ops

pairs = [
    ('\''hello world'\'', '\''hello beautiful world'\''),
    ('\''hello world'\'', '\''hi world'\''),
    ('\''abcde'\'', '\''ace'\''),
    ('\''你好'\'', '\''你好世界'\''),
]
for old, new in pairs:
    ops = compute_diff(old, new)
    print(f'\''  {old!r:25} -> {new!r:25}  ops: {ops}'\'')
'
showboat note /home/user/entangle/demo.md '## Layer 2 — CRDT Engine

`CrdtEngine` wraps a `yrs::Doc` with a single `Y.Text` named `"content"`. Key methods:

- `seed(content)` — populate the doc with initial text (share command startup)
- `apply_local_edit(new_content)` — diff snapshot → new, apply ops, return encoded delta bytes
- `apply_remote_update(bytes)` — decode and apply a remote delta, return new text if changed
- `state_vector_bytes()` — encode our state vector for SyncStep1
- `encode_state_as_update(peer_sv)` — encode what the peer is missing for SyncStep2'
showboat exec /home/user/entangle/demo.md bash 'cargo test --lib crdt:: 2>&1 | grep -E '\''test |result:'\'''
showboat note /home/user/entangle/demo.md 'The `concurrent_edits_converge` test verifies the core CRDT guarantee: two peers making independent edits to the same base text will always arrive at the same final state after exchanging updates, regardless of edit order.'
showboat exec /home/user/entangle/demo.md bash 'cargo test --lib crdt::tests::concurrent_edits_converge -- --nocapture 2>&1'
showboat note /home/user/entangle/demo.md '## Layer 3 — y-websocket Protocol

`protocol.rs` implements the y-sync wire format using lib0 variable-length integer encoding. Each WebSocket binary frame is: `[msg_type:varint][sync_type:varint][payload_len:varint][payload bytes]`.

Message types:
- `encode_sync_step1(sv)` → sent on connect, announces our state to the relay
- `encode_sync_step2(update)` → reply to a peer'\''s SyncStep1, sends what they'\''re missing
- `encode_update(update)` → incremental update after each local edit
- `decode_message(bytes)` → parses any incoming frame into a `SyncMessage` enum'
showboat exec /home/user/entangle/demo.md bash 'cargo test --lib protocol:: 2>&1 | grep -E '\''test |result:'\'''
showboat note /home/user/entangle/demo.md 'A concrete look at the wire format — encoding a SyncStep1 and inspecting the bytes:'
showboat exec /home/user/entangle/demo.md python3 '
def write_varint(n):
    buf = []
    while True:
        b = n & 0x7F
        n >>= 7
        if n == 0:
            buf.append(b)
            break
        buf.append(b | 0x80)
    return bytes(buf)

def write_var_bytes(data):
    return write_varint(len(data)) + data

def encode_sync_step1(sv: bytes) -> bytes:
    return write_varint(0) + write_varint(0) + write_var_bytes(sv)

def encode_update(update: bytes) -> bytes:
    return write_varint(0) + write_varint(2) + write_var_bytes(update)

sv = bytes([1, 0, 1, 5])          # example state vector bytes
frame = encode_sync_step1(sv)
print(f'\''SyncStep1 frame : {list(frame)}'\'')
print(f'\''  byte 0 = {frame[0]} (msg_type=sync)'\'')
print(f'\''  byte 1 = {frame[1]} (sync_type=step1)'\'')
print(f'\''  byte 2 = {frame[2]} (payload length={frame[2]})'\'')
print(f'\''  rest   = {list(frame[3:])} (state vector)'\'')

update = bytes([0xde, 0xad, 0xbe, 0xef])
frame2 = encode_update(update)
print()
print(f'\''Update frame    : {list(frame2)}'\'')
print(f'\''  byte 0 = {frame2[0]} (msg_type=sync)'\'')
print(f'\''  byte 1 = {frame2[1]} (sync_type=update)'\'')
print(f'\''  byte 2 = {frame2[2]} (payload length={frame2[2]})'\'')
'
showboat note /home/user/entangle/demo.md '## Layer 4 — File Watcher

`watcher::spawn_watcher` starts a `notify` v6 `RecommendedWatcher` on the *parent directory* of the target file (watching a single file directly is unreliable on some platforms when editors use atomic save-via-rename). Events are filtered to the target filename and bridged to a tokio channel via `spawn_blocking`. The suppress flag (an `AtomicBool` shared with the writer) prevents entangle from reacting to its own writes.

## Layer 5 — Atomic File Writer

`writer::write_file_atomic` avoids partial reads by writing to a temp file in the same directory then renaming over the target. Before writing it sets the suppress flag so the watcher ignores the resulting fs event; a short-lived tokio task clears it 50 ms later.'
showboat exec /home/user/entangle/demo.md bash 'cargo test --lib writer:: 2>&1 | grep -E '\''test |result:'\'''
showboat note /home/user/entangle/demo.md '## Layer 6 — Room ID & Share Links

`room::generate_room_id` produces 8 random bytes hex-encoded (16 chars), e.g. `a3f8c2e9b1d4f067`. The share link embeds the WebSocket URL and room ID in a form the joiner can paste directly into their terminal.'
showboat exec /home/user/entangle/demo.md bash 'cargo test --lib room:: 2>&1 | grep -E '\''test |result:'\'''
showboat note /home/user/entangle/demo.md '## Property-Based Tests (proptest)

Four proptest properties exercise the differ + CRDT stack with randomly generated inputs:

1. **diff_roundtrip_ascii** — for any two ASCII strings, applying `compute_diff` ops to the old string always yields the new string
2. **diff_roundtrip_unicode** — same for arbitrary Unicode
3. **crdt_roundtrip** — applying a local edit to a `CrdtEngine` always results in the engine holding the new content
4. **two_peer_convergence** — two peers seeded with the same base, each making an independent edit, always converge after exchanging updates'
showboat exec /home/user/entangle/demo.md bash 'cargo test --test differ_proptest 2>&1 | grep -E '\''test |result:|FAILED'\'''
showboat note /home/user/entangle/demo.md '## Full Test Suite

All 43 tests across 39 unit tests + 4 property-based tests:'
showboat exec /home/user/entangle/demo.md bash 'cargo test 2>&1 | grep -E '\''^test |^test result:'\'''
showboat note /home/user/entangle/demo.md '## Showboat: Verify, Extract, Pop

These three showboat commands close the loop on document integrity.

**extract** — prints the sequence of showboat CLI commands that would rebuild this document from scratch. Useful for auditing or replaying the demo in a fresh environment.'
showboat exec /home/user/entangle/demo.md bash 'uvx showboat extract /home/user/entangle/demo.md 2>&1'
showboat note /home/user/entangle/demo.md '**pop** — removes the most recent entry. Demonstrated here by adding a deliberate mistake and then removing it.

(The dummy entry above was removed by `showboat pop demo.md` — it no longer appears in the document.)

**verify** — re-runs every code block and diffs actual output against recorded output. Exits 0 if everything matches, 1 with diffs if anything changed.'

pop — removes the most recent entry. Demonstrated here by adding a deliberate mistake and then removing it.

(The dummy entry above was removed by showboat pop demo.md — it no longer appears in the document.)

verify — re-runs every code block and diffs actual output against recorded output. Exits 0 if everything matches, 1 with diffs if anything changed.

The diffs above are purely non-deterministic: parallel test execution order and sub-second build timings. Every test result line reads ok, all counts match, and the Python outputs are byte-for-byte identical. The document is reproducibly correct — only scheduling noise varies.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

entangle_sync-0.1.0.tar.gz (39.1 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

entangle_sync-0.1.0-py3-none-win_amd64.whl (3.0 MB view details)

Uploaded Python 3Windows x86-64

entangle_sync-0.1.0-py3-none-manylinux_2_28_x86_64.whl (2.8 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ x86-64

entangle_sync-0.1.0-py3-none-manylinux_2_28_aarch64.whl (2.8 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ ARM64

entangle_sync-0.1.0-py3-none-macosx_11_0_arm64.whl (2.9 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

entangle_sync-0.1.0-py3-none-macosx_10_12_x86_64.whl (3.0 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

Details for the file entangle_sync-0.1.0.tar.gz.

File metadata

  • Download URL: entangle_sync-0.1.0.tar.gz
  • Upload date:
  • Size: 39.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for entangle_sync-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2f5c5fe1bb9e77e9755f52b5aac2b18670a6ac5354c2a1498315a789a60306df
MD5 8c9e57ee54e4324abc9ca6594b72b7f9
BLAKE2b-256 db51e2170e9b78d9467a8fb90b3d28c33f77781fd53f4d9e49200e70aa1a8faf

See more details on using hashes here.

Provenance

The following attestation bundles were made for entangle_sync-0.1.0.tar.gz:

Publisher: publish.yml on pirxthedev/entangle

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file entangle_sync-0.1.0-py3-none-win_amd64.whl.

File metadata

File hashes

Hashes for entangle_sync-0.1.0-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 ed7eb72b7ab1d83e79ae534e932172209928c8d5e03e68992b6ee4f6355116fe
MD5 af8578fed2723775a8eb8a56572ed9b0
BLAKE2b-256 0c4f25752b0753ffe5ccddf6389872f9d958f410d66ff4525fe4d7608228a823

See more details on using hashes here.

Provenance

The following attestation bundles were made for entangle_sync-0.1.0-py3-none-win_amd64.whl:

Publisher: publish.yml on pirxthedev/entangle

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file entangle_sync-0.1.0-py3-none-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for entangle_sync-0.1.0-py3-none-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 df81f17e76edfaa4b97fad6691e14ff61bc80c4579d8d631d667b1341c4d773d
MD5 180b8b03a420261622a91eef3fc3761c
BLAKE2b-256 196b10fd6e9f53ff98eb152bfba96b2bda5a6b6a7fae69374924392f032345dd

See more details on using hashes here.

Provenance

The following attestation bundles were made for entangle_sync-0.1.0-py3-none-manylinux_2_28_x86_64.whl:

Publisher: publish.yml on pirxthedev/entangle

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file entangle_sync-0.1.0-py3-none-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for entangle_sync-0.1.0-py3-none-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 7472dc71c943eec6cf4eb7b45aaacb1d56dcf12e13e48f2f3d6ca7aefccea685
MD5 6ff80be6ddc0d40fb91b617e161348ed
BLAKE2b-256 09cd20f73ea3e55e190697dfb4eaaf50674e2e2e3e4eba33fa422509164af386

See more details on using hashes here.

Provenance

The following attestation bundles were made for entangle_sync-0.1.0-py3-none-manylinux_2_28_aarch64.whl:

Publisher: publish.yml on pirxthedev/entangle

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file entangle_sync-0.1.0-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for entangle_sync-0.1.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 71b44293abe802c71c3d8823d0dd5deaa981471089a4a6bde73f55ad8bcaf1c3
MD5 45fef5662eebb7e90535e2652d4bc554
BLAKE2b-256 99ffd2e583ee9891e8fefdf89d73ee64bd70936e3d60cc4fe94f0fd6c06986d4

See more details on using hashes here.

Provenance

The following attestation bundles were made for entangle_sync-0.1.0-py3-none-macosx_11_0_arm64.whl:

Publisher: publish.yml on pirxthedev/entangle

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file entangle_sync-0.1.0-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for entangle_sync-0.1.0-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 8a522abf5d8aea1e4efbdfc00d8cd85ebbce6f6b7f7b3c71ace0d63bb8332df9
MD5 70cab7c1fc2bb85c822c4415f248e737
BLAKE2b-256 d88c868a485f5681db9d3631848143cac9575b07a8c48b244964c7e2c4dd2e18

See more details on using hashes here.

Provenance

The following attestation bundles were made for entangle_sync-0.1.0-py3-none-macosx_10_12_x86_64.whl:

Publisher: publish.yml on pirxthedev/entangle

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page