cmux-bookmarks-core: the bookmark op rules as a crate the daemon uses, plus the in-memory reducer for the app (V-11)

Merged
LA
3d ago
+2109-274

The app keeps a hand copy of the daemon's bookmark rules (CmuxNextBookmarks/Model/BookmarkTree+Apply.swift) to show an edit before the daemon's echo, and a second quick edit flickers away until its echo (layer-ownership.md V-11, bead cx-e59e). This PR moves the rules into a new pure crate, cmux-bookmarks-core, which the daemon uses now, and adds the in-memory reducer the app will call. It changes no app FFI input. After it lands, step A of a two-step FFI move adds the op bookmarks_apply to the app FFI (branch rust-migration/bookmarks-ffi-move); step B, after the re-pin, makes the app call it and deletes BookmarkTree+Apply.swift.

What changes

  • New crate cmux-tui/crates/cmux-bookmarks-core (no I/O; zero hits of the panic ratchet's deny lints). It holds the limits, the validation, the record and op types (Bookmark, BookmarkOp, ...) and Tree: one profile's tree in memory with the daemon's op reducer, step for step (same checks in the same order, same messages, same index clamping, same result JSON; every check runs before the first change, like the daemon's transaction). preview::respond is the request the app will send: a confirmed listing plus pending ops in, the visible tree plus one result per op out. Each refusal also names its rule (Reason), so the app can keep its localized refusal texts.
  • The daemon uses it, with no behavior change. workspace_registry/personal_bookmarks.rs re-exports the moved items and keeps its SQLite rows, replay ledger and journal. The op types serialize as before, so the replay fingerprint of a keyed op does not change. URLs are still parsed with url::Url (the crate's url feature, which cmux-tui-core turns on). Without the feature, the check is the scheme shape (has_url_scheme), which accepts every URL the parser accepts in the test corpus; that is what the app FFI will use, so it links no URL parser.
  • Wiring: a cmux-tui workspace member and its Cargo.lock entry; one comment in cmux-tui/scripts/check-sdk-schema.py.

Known gaps between Tree and the daemon (also in tree.rs): Tree holds one profile, so it cannot check that a profile exists in browser_profiles (it refuses another profile as unknown) or that an id is free in every profile; and it does not check a folder icon's syntax or that its stored asset exists. When the daemon refuses one of these, the app gets the command's error.

Follow-up: two Rust implementations of the reducer now exist (the daemon's SQL ops and Tree), held equal by the parity test. A later change can make the daemon apply each op through Tree on a loaded working set (the profile's tree, then write the changed rows), if a bench shows that is cheap at 100,000 nodes; then the SQL ops go.

Tests

  • cmux-bookmarks-core tests/bookmarks/tree.rs: the Swift BookmarkTreeTests of the hand copy (deleted with the CmuxNext unit suites in 36f2381bf1a6) ported with the daemon's ids and refusals, plus the daemon rules the Swift copy lacked (import and replace, id and profile checks, icon only on a folder, no favicon key on an import), each refusal's Reason, the op JSON shape, the preview (two pending edits both show; the echo does not move the view), id minting, and the scheme check against url::Url::parse.
  • cmux-tui-core server/bookmark_parity_tests.rs: 1000 seeded random ops (4 seeds x 250 steps: creates, moves, deletes, updates, icons, imports and replaces, with bad parents, bad URLs, NUL titles, unknown ids). Each step reads the daemon's listing, applies the op to the daemon and to Tree, and requires the same tree, the same result and the same refusal code and message.

This is a move with no bug to reproduce: the crate does not exist on the base, so there is no red step. The parity test is the proof that the Rust reducer equals the daemon.

Observed / Not run

Observed (fleet):

  • Test cmux-tui-rust-check.sh test bookmark at 43dd4a9c6cade307ad96d004612bccabf7a758ec: step 17a8481bae447211ebc8f64b, 44 tests run, 44 passed (the 19 bookmark_tree:: tests of the crate, server::bookmarks::tests::parity::the_daemon_and_the_shared_reducer_give_the_same_tree_result_and_refusal, and the existing daemon bookmark tests). The head eedc9e098d8c only merges feat-cmux-next on top; the patch is the same.
  • Clippy at eedc9e098d8ca1d1db01409a92fa6df7053e8822: step 92dda7db7281ab6a12a6f6c9, passed. (Step 77510e2b148e at 43dd4a9c6cad failed on cmux-tui/src/plugin_manager.rs:233 suspicious_open_options, a base red fixed on feat by ca404c45ae1; hence the merge of the base.)
  • Panic ratchet at 43dd4a9c6cad: step 9ff1b6682ea5a465dfa60b74 RED on acpmux clippy::indexing_slicing 697 > baseline 686 (base finding; this PR touches no acpmux file). No cmux-bookmarks-core hit is listed.
  • Head aaa5fcdb649 did not compile (BookmarkOutcome, a daemon-only type, was cut with the moved records; fixed in 2473f9377ff). Its fmt step 2d19e4be180c failed only on acpmux/src/hub/list_guard.rs and hub/mod.rs (base red).
  • Local: scripts/cmux-next/prepush-gate.py no new violations; cmux-tui/scripts/check-sdk-schema.py ok; .github/scripts/rust-workspaces.py check ok.

Not run: no Swift compile (no Swift change); no perf A/B (nothing calls the preview yet).

Changelog

none