Skip to content

Refactor CLI into modular command structure - #246

Merged
samkeen merged 34 commits into
mainfrom
claude/tech-debt-cleanup-y3s69c
Sep 26, 2026
Merged

samkeen merged 34 commits into
mainfrom
claude/tech-debt-cleanup-y3s69c

Conversation

@samkeen

@samkeen samkeen commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Restructure the b2-cli crate from a monolithic main.rs into a modular command-based architecture. This refactoring extracts command implementations into separate modules (args, chat, read, write, reindex, cancel, error, logging, wiring) while maintaining the same CLI interface and behavior.

Key Changes

Implementation Details

  • The CLI dispatch now routes each subcommand through its domain module via dispatch function
  • Cancel handling unified: SIGINT handler and cross-process b2 reindex --cancel both flip the same CANCEL flag, read by reindex embed loop and chat token callback
  • Error translation centralizes user-facing messages (no internal leaks, B2_DEBUG for detail)
  • Vault opening abstracted in wiring.rs: embedder choice (real/fake) and chat provider injection
  • Eval harness now measures quality out of CI with per-query tables, discovery lines, and A/B diffs
  • All changes preserve existing CLI behavior and --json output contract

https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w

Tooltips, menu chords and dialog hints spelled the shipped chords by hand
(⌘E, ⌘[, ⌘], ⇧⌘F, ⌘,, ⌘N, ⇧⌘N, F2, ⇧F10, ⌘⌫, ⌘⏎, ⌃Tab), so they went stale
the moment a user rebound one. They now come out of displayKeys, and the
shell's static hints live in hints.ts and are repainted by setOverrides
(the shell and the live editor's bar are the two surfaces render() doesn't
rebuild, so #open-chat's title used to stay on the boot-time chord).

hints.test.ts pins the hints under a rebind and walks every UI string
literal through TypeScript's parser, failing on a chord spelled by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
- api.ts: the ping and neighbors wrappers (nothing calls them; the host
  commands are going too).
- bindings.ts: chordsOverlap, unused since menukeys.ts compares keystrokes.
- keymap.ts: currentKeys, never called.
- recorder.ts: reservedChords and its test. Its doc said the recorder names
  the menu's chords up front, but recorderHtml never rendered them.
- style.css: .modal-wide and its stale anomaly-panel comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
- beginIndexRun/endIndexRun for the three index runs' meter lifecycle, and
  refreshOpenNoteAfterIndex for the after-run re-read + discovery refresh
  (autoIndexOnOpen still skips it entirely while an editor is open).
- enterDocument: the core loadNote and loadResource share.
- clearDiscovery/clearConnections/adoptExplain, adoptResource, adoptCoverage
  (a VaultInfo's embedding coverage).
- Chat: applyChatConfig is now really the shared tail of Save, the model
  picker and Remove key; adoptChatSetup; openChat; chatReady/chatStateOf in
  chat.ts replace three inline chatEmptyState(...) === "ready" reads; and
  LOCAL_CHAT_ENDPOINT moves to chat.ts, where the Endpoint placeholder uses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
- The contextmenu handler, the drag start and the drop target rebuilt a
  tree row's node ref by hand; they use treeRowRef (and a TREE_ROW selector)
  now, with the folder a row stands for from move.ts's folderContext.
- openDocPath (state.ts) for the pane's document path.
- revealDir replaces expandAncestors (a re-implementation of dirChain) and
  the five hand-written dirChain loops.
- isWithin (move.ts) for "this folder or under it", written four ways before;
  moveRefusal says why a destination is refused, canMoveInto is defined
  from it, and the Move… modal reads the reason instead of re-deriving it.
- render.ts uses baseName instead of split("/").pop().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
The search caveat, the Similar empty state, the graph's ghost hint,
Settings → Index and chat's retrieval note each re-derived the embedding
tiers from the three raw numbers. coverage.ts reads them once — the model
as its own fact, the embedded fraction as a tier — and each surface keeps
its own copy and its own order of asking. Every surface's output was
snapshotted across a state matrix before and after (224 cases, identical);
coverage.test.ts pins the classifier and each surface's copy per tier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
…etup sentences

b2-llm and b2-embed now answer the questions both adapters were each
answering for themselves:

- `fake_requested()` + `ENV_LLM`/`ENV_EMBEDDER`, and b2-llm's
  `FAKE_NOTICE`, so B2_LLM=fake / B2_EMBEDDER=fake mean one thing.
- `b2_llm::provider` / `probed_provider` and `b2_embed::embedder_for`:
  the one wiring rule (fake when forced, else the real thing) in one place.
- `unreachable_message` / `model_missing_message` are public (the unused
  detail parameter dropped), and the Ollama sentence uses `pull_command`.
  The non-Ollama model-missing sentence now says "Load it there."

Smaller clean-ups on the way through:

- sse: EOF is dispatched as a final blank line instead of a copy of that
  branch; a tool call under assembly is a named `PartialCall`.
- One `is_valid_tool_call_cap` predicate behind both cap checks.
- setup: host_of/origin_of share `scheme_and_authority`;
  `installed_models` returns Option; `setup` is a private module and
  `is_local`, `tier_for_ram`, `system_ram_gb` are private; the unused
  `OpenAiCompatProvider::config()` is gone.
- b2-embed: HF_ENDPOINT is passed into `from_raw` (testable, pinned);
  the Metal fallback reports via tracing (b2::embed) instead of eprintln;
  single and batch embedding share one forward/CLS/normalize step (the
  single path still encodes one unpadded sequence); one cache-path join;
  `find_model`, `model_dir`, `sanitize`, `default_cache_dir` internal.
  `LocalEmbedder::configured_model()` exposes the untagged repo id.
- quickstart documents source/cache_dir/query_prefix and HF_ENDPOINT.
- wire.rs: drop an unused `mut` clippy now flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
vault.rs had grown to 2,300 lines holding three modules' worth of code. The
view types move to vault/views.rs (re-exported, so b2_core::vault::X paths
are unchanged), ask + the Why? tool turn to vault/chat.rs, and the one-line
excerpt helpers to snippet.rs. An orphaned doc comment that rustdoc was
attaching to ToolDesk goes back onto snippet().

While moving, the façade stops repeating itself:
- write and write_frontmatter share one save_guarded (resolve, revision
  guard, splice, write, re-project, read the revision back).
- One require_resource check and one refuse_resource_anchor guard replace
  five hand-written copies.
- A search hit resolves in one chunks JOIN notes read (db::chunk_hit)
  instead of three or four, so the note and chunk come from one snapshot.
- A neighbor's title and created date are one read (db::note_header).
- neighbor_paths / shared_neighbors / note_summary replace the neighbor
  sets and NoteSummary builds written out by hand in why_similar and
  explain_similar; ToolDesk no longer copies the pair WhyFacts carries.
- query_snippet windows on search::query_terms, the tokenizer the lexical
  half ranked with, instead of a second one.
- AddReport and ReindexProgress are re-exported from vault like every
  other report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
…+ editor commands out of main.ts

- wireEvents was one 1,350-line function; it is now a list of named wiring
  functions (focus memory, clicks, context menu, tree keys, side keys, find
  bar, forms, chords, mouse history, window blur, drags) called in the old
  registration order. Settings' click branch is settingsClick.
- The context-menu click branch's thirteen near-identical blocks are three
  tables of MenuItem (attr, closeFirst, act) and one contextMenuClick.
- findfield.ts: the editor find StateField, its effect and decorations.
- editorcmds.ts: format, list shift, table, rich paste and [[ completion,
  each split into a pure plan over an EditorState and a run… that
  dispatches it. Both modules have EditorState tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
A pure move: no line of behaviour changes. main.rs keeps the module doc,
`main`, `dispatch` and `print_json`; the rest goes where its domain is:

- args.rs     Cli, Command, LlmArgs, AGENT_HELP, the vault-root rules
- logging.rs  init_logging
- wiring.rs   open_vault / open_llm and the fake switches
- error.rs    CliError, user_message and its unit tests
- reindex.rs  init, reindex, status, the lock and --cancel
- chat.rs     ask, why, chat and the streamed rendering
- read.rs     neighbors, explain, search, similar (+ --explain), the
              presentation helpers and the search_report tests
- write.rs    add, write, mv, rm, link and their tallies
- cancel.rs   the CANCEL/ANSWERING flags and cancel_flow, which both
              reindex.rs and chat.rs read

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
The four harness examples each carried their own copy of the same plumbing.
They now share one module, `crates/b2-embed/examples/common/mod.rs` (Cargo
discovers only `examples/*.rs` and `examples/*/main.rs`, so it is a module,
not an example). `groundedness` reuses it from b2-llm by `#[path]`; the module
imports nothing b2-llm's dev-dependencies don't also carry.

Shared: `truncate` (the saturating form, so `max == 0` no longer underflows
in eval/calibrate/stability), `git_short_sha`, `append_result`, `cosine_of`,
`pile_stats`, the strength-band landmarks with one `Band` classification, the
harness's z restatement (`passage_z`; calibrate applies its replayed
MIN_POPULATION floor first, as before), the flat-corpus throwaway vault
(`ScratchVault`), the title-as-query probe, and stability's
`reject_unknown_flags`/`has_flag`. The flag check also refuses a value on a
switch (`--verbose=1` passed the name check and then matched no `has_flag`,
silently running a different measurement — the case the check exists for).

`calibrate --search` now reads term coverage off the engine's
`QueryTermView::idf` instead of re-deriving the IDF by hand (same formula,
same inputs). The deliberate restatements stay restatements: eval's
labelled bake-off still recomputes coverage itself as the drift check, and
the z recheck is still the harness's own arithmetic, never an engine call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
`eval` read its flags with `args().any(|a| a == "--sweep")`, so a typo
(`--swep`, `--stemmer=1`) ran the default measurement alone and printed a
report the reader took for the A/B they asked for. It now goes through the
shared `reject_unknown_flags` (the check `stability` already had) and exits 1
on anything but `--sweep`/`--stemmer`. A bare `--` is still dropped, as
`stability` does, so a pasted `cargo run … -- --sweep` keeps working; every
invocation in the Makefile and docs/evals.md is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
render.ts (2,687 lines) keeps the tree, note pane, discovery column, menus
and dialogs, and re-exports everything the app and the suite imported from
it. The rest moves to modules named for their surface:

- markdown.ts: renderMarkdown and the marked config — the one sanitizing
  Markdown path, now importable without the panes (livepreview.ts does).
- chatview.ts, graphview.ts, explainview.ts, keysview.ts, settingsview.ts.
- widgets.ts: markup more than one surface paints — sideTab, strengthHtml,
  segmentedHtml (theme and chat-mode controls), reindexMeterHtml (the top
  bar's meter and Settings → Index's).

Also keysGroupsHtml (the reference and the ⌘ sheet), modelDetail in chat.ts
(the setup card and the model picker), and tabDomId exported from
settingstabs.ts for main.ts's two hand-built settings-tab ids. The module
graph is acyclic. Every exported surface was snapshotted across a state
matrix before and after the move (755 cases, identical).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
…ly, render once

Wiring: open_vault/open_llm shrink to b2_embed::embedder_for and
b2_llm::probed_provider over the CLI's flags; the fake switches and the
fake-chat notice are the libraries' (the notice now prints as b2-llm's
sentence, without the "note:" prefix).

Errors:
- Unreachable / model-missing use b2-llm's sentences and append only the
  CLI's flag hint (--llm-url, --llm-model with the served list).
- The InvalidRelation verb list is read from b2_core::relation::CORE.
- The `_ =>` catch-all is gone: EmbedError::Load/Config/Io/UnknownModel
  get their own messages (Config names the config.toml path), so
  `b2 init` no longer says "check the vault path"; the internal
  remainder is spelled out so a new CliError variant fails to compile.
- is_ollama is no longer re-exported by b2-llm (no caller left).

Rendering:
- `emit(json, &value, human)` replaces the per-command
  `if cli.json { print_json } else { … }` blocks; explain and
  similar --explain get named human printers.
- `install_cancel_on_sigint(exit_when_idle)` is the one Ctrl-C handler
  (the REPL passes true), and `while_answering` owns the per-turn
  CANCEL/ANSWERING bookkeeping, so the flags are private to cancel.rs.
- `status --json` and `reindex --cancel --json` serialize typed
  StatusView/CancelView structs with the same keys.

Comments: the orphan is_ollama block, cmd_mv's doubled comment, the
VaultRequired doc (every write command, not just reindex) and the
write-command list in Cli.vault are fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
…kernel and search's fusion tail

Ingest (flow ①):
- ingest_file / project_file / ingest_vault return () — every caller
  discarded Ingested / Projected. plan_reindex returns two counts, not a
  per-note Vec. ingest_vault_with_progress and IngestOutcome go:
  Vault::reindex composes project_vault + embed_vault itself.
- project_note_and_chunks takes a Rechunk enum instead of a (force,
  consult_vectors) bool pair whose fourth combination never occurred.
- One walk_vault (walk + sort), one body_hash shared by the projection
  and the dry-run forecast, one skipped() constructor.

Discovery: stage 2 and passage_pairs ran two hand-copied max-sim loops
that had to agree for the card and its explanation to agree. They now
share nearest_pairs, so they agree by construction. Population's unread
n field goes and the type is private.

Search: keyword-only and hybrid share one fuse() tail and
Retrieval::empty(). Float sorts use total_cmp (same order for every
reachable value, a defined place for NaN).

db: embedding_space_exists is table_exists; embedding_space_matches reads
recorded_embedder instead of re-parsing meta. Error::Serde was never
constructed. Missing Debug derives added; docs citing retired plans
("slice 1", "research §9b", migrate()) corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
link.rs exposes link_spans, a per-line span scanner that parse_links builds on,
and a move now rewrites targets from those spans instead of its own whole-file
scan. A stray [[ on one line no longer pairs with a ]] on a later one, so a link
ingest projected is a link a move repairs. ParsedLink loses alias and typed (read
only by link.rs's own tests); render_relation is the inverse of parse_relation.

move_note, move_resource and move_dir now only validate and build a MoveSet; one
executor plans the rewrites, commits them with the rename, re-keys every member
and re-projects every inbound source. Behaviour made consistent: a self-link is
reported under the note's new path, a resource move re-projects every inbound
source (not only the rewritten ones), and a wikilink keeps its #heading fragment.
A resource can no longer be moved to a .md path (MoveDestination), which a rebuild
would have indexed as a note. A resource row missing from the index is inventoried
at its new path instead of failing after the rename.

delete_note, delete_resource and delete_dir run one delete pipeline over
db::inbound_sources. db gains delete_note_row, delete_resource_row and
repoint_resource (moved from mv), and the note/resource twin helpers share
private implementations. pathspec gains the vault-relative path algebra
(file_name, parent_dir, extension, is_md, is_under, rebase, relativize), and
normalize_rel_md no longer turns Foo.MD into Foo.MD.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
NoteFields.title (read only by tests: a note's title is its filename and the
frontmatter title: key is inert, round-tripped with the raw bytes) and
CoreVerb::symmetric / relation::is_symmetric (unused; contradicts being its own
inverse stays pinned by a test on the inverse labels). display_title reads the
file name and extension through pathspec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
add checked abs.exists() and then wrote, so a file appearing in between was
clobbered. It now goes through import's create-new placement (import::place_new),
where 'does it exist' and 'claim it' are one syscall and AlreadyExists is the op's
own target-exists error. The refusal now also covers a folder at the path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
…rrent docs

ContextPassage::block is the one passage layout; ChatRequest::system_message and
chat::passage_block (kept for the façade) both render through it, byte-identical.

FakeEmbedder::new clamps a zero dimension to 1 instead of asserting on a
production path; unpack_f32 goes through unpack_f32_into.

chunk.rs cites docs/index-engine.md §1 instead of a deleted chunking spec's
D1-D5 (which collided with invariants.md's D ids) and names its levers in plain
words; scan and protected_regions share one fence-line rule. Stale docs fixed
in embed.rs and graph.rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
… storage move out

- leaveEdits (the drawer guard + close-the-editor pair before a navigation),
  teardownEditor, bufferUnsaved, refusedWhileIndexing, confirmDelete (the
  delete confirm's button and ⏎), searchInput, and findByData behind
  treeRowEl/sideRowEl/gnodeEl.
- executeMove calls one mover chosen by kind (all three report alike);
  executeDelete shares the note and resource branch.
- theme.ts owns the appearance preference's values and storage, and
  embedreminder.ts the banner's opt-out, the way zoom.ts, panes.ts and
  keymap.ts own theirs. Both are tested with and without storage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
state.ts: contextMenu covers the file tree as well as a discovery card,
current is the centre pane's note (not a "left pane"), and Settings is a
full-window surface rather than a modal box. api.ts: menuChords is read once
to check menukeys.ts's mirror; the keyboard reference never listed from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
…red wiring

- `Slot` (slot.rs) replaces the two hand-rolled state machines
  (reindex_running/reindex_cancel and ask_running/ask_cancel, ReindexGuard
  and AskGuard, claim-then-arm at each call site): `try_claim` returns a
  guard that releases on drop and clears a stale cancel; `cancel`,
  `cancelled`, `flow`, `cancel_and_wait`. Tests re-pointed; the two
  pure state-machine tests move to slot.rs.
- `read_op` / `semantic_op` make every command body one line; the
  `*_impl` fns the tests call stay, as one-liners over them.
- Wiring goes through b2_embed::embedder_for, b2_embed::fake_requested,
  b2_llm::provider and b2_llm::fake_requested. open_vault returns the id
  of the model it actually loaded, so `embed` no longer re-reads the
  config (which could name a different model) to attribute its time.
- Dead IPC removed: the `ping` and `neighbors` commands (only ui/src/api.ts
  wrappers referenced them) and the ping test.
- `open_resource` reads the root once, so the inventory check and the path
  handed to the OS belong to the same vault.
- stats returns the IPC row itself (`EmbedStat`, ModelStat flattened —
  same model/total_ms/chunks/runs keys, pinned by a test) instead of a
  mirrored struct plus a hand-written From.
- state_file.rs: one path + best-effort-write helper behind last-vault,
  chat.json and embed-stats.json.
- The app failing to start logs and exits 1 instead of `.expect()`.
- error.rs: the relation verbs come from b2_core::relation::CORE, and a
  broken embedder config names config.toml instead of "check the vault".
- logging.rs: the implied-filter comment no longer claims the CLI differs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
eval.rs had grown to ~4,500 lines. It is now `examples/eval/main.rs` (still
the `eval` example, so `cargo run -p b2-embed --example eval` and every
Makefile target are unchanged) plus one module per block of the report:
labels (sets + lint), metrics, retrieval, discovery (per-mate, strangers, z
dump), fold (GH #200 bake-off), evidence (D2 calibration + bake-off), tail
(GH #206 bake-off + cross-bench join), dense (the single-domain fixture),
ablation (--stemmer / --sweep), report (default table + JSONL row),
instrument (batch ≡ single, timed embed, pool-blind warning) and gate (the
exit-gate constants and assertions). main.rs is the run's order of
operations and nothing else.

Moved verbatim apart from visibility, imports and doc links, with three
shape changes:
- `result_row`'s 18 positional parameters become `RunId` (who scored the
  run; also taken by `dense_row`) plus a `RowInputs` view whose four
  default-row-only readings travel together as `Calibration`. Every row key
  and value is unchanged; the ablation rows' calibration keys still read null.
- The stemmer and sweep blocks become `ablation::stemmer`/`ablation::sweep`
  over a `Baseline` view of the default passes; the stemmer's `.expect` on
  the unstemmed BM25 pass is gone (the pass is handed over by `if let`).
- The exit gate becomes `gate::passes`, returning `false` where `run`
  returned `Ok(false)`.

Also reattaches `QueryEvidence`'s doc comment, which had drifted onto
`ServedRow`, and points docs/evals.md's exit-gate link at eval/gate.rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
19 inline copy-open(-reindex) sequences use common::opened_vault /
reindexed_vault, and 11 inline opens of .b2/b2.sqlite use common::index_conn.
The identical inbound() of mv.rs and props.rs, the near-identical write_note of
discover.rs and why.rs, and the token callbacks (ask.rs stream_all, why.rs
keep_streaming and the inline closures) move into common as inbound, write_note,
keep_streaming and stream_into. No assertion changes. The Capture writers in
logging.rs and discover_query_count.rs stay deliberate copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
…e SQL order

MoveSet looks moved notes up by old path; sorting in Rust keeps that invariant
local instead of resting on SQLite's BINARY collation matching str ordering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
…cleanup-y3s69c

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
…leanup-y3s69c

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
… drop the unbuilt graph join

- Vault::link renders its b2_relations entry with link::render_relation,
  the inverse of the parser that reads it back.
- Every resource op checks the inventory in the façade (require_resource);
  rm::delete_resource no longer checks a second time. New
  Vault::resource_path gives the desktop's Open in system default one
  inventory-checked call instead of borrowing explain_resource's backlink
  query to validate.
- ingest resolves a note-relative Markdown target with
  pathspec::parent_dir / pathspec::join_relative, next to relativize, its
  inverse (round-trip tested).
- search::graph_filtered_search had no caller outside its tests: no command
  needs "nearest chunks within k hops", and discovery is its complement.
  It goes, with db::vector_search_all and db::chunk_note_map, which existed
  only for it. index-engine.md no longer calls it the substrate b2 similar
  runs on; it says what similar actually reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
…a 7)

notes.type, notes.description, notes.updated (+ notes_type_idx) and the
whole note_aliases table were written on every projection and read by
nothing: NoteView takes type/updated straight from the parsed file, the
resolver never consulted aliases, and no embed prompt used description.
They go, with the NoteRow/NoteFields fields that fed them and the test
that pinned the alias table. SCHEMA_VERSION 6 -> 7, so an existing index
rebuilds on open and re-embeds (S5: a version bump plus a rebuild).

The spec said more than the code did, so it now says what is true:
data-model.md §1 no longer claims description feeds the embedding prompt
or that aliases resolve links (both round-trip, neither is read yet), and
type no longer "defaults to note". index-engine.md's schema sketch matches
the DDL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
…s name the split modules

executeMove adopted the moved resource only after its picture's bytes
arrived, so the watcher pulse the move itself causes could find state still
naming the old path and warn "no longer on disk" about B2's own move. The
path is adopted first; the picture follows if the resource is still open.

Comments that pointed at render.ts for code that now lives in
markdown.ts / settingsview.ts / keysview.ts / chatview.ts / graphview.ts /
explainview.ts / widgets.ts name the module it moved to, including the
sanitizer's trust-boundary header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
…is true

From review of the cleanup:
- b2-embed's Metal-to-CPU fallback had become a tracing event, which neither
  adapter shows unless logging is asked for. A fallback changes the
  embedding space's identity (no @metal tag), so a Metal-built index then
  reads as a model change; the stderr notice explaining why is back, and
  the tracing dependency it pulled in goes.
- mv.rs claimed a move rewrites "exactly the links that point at it". It
  rewrites by written text within an inbound file, so a second link with
  the same text that resolves elsewhere is rewritten too (true before this
  cleanup as well). The docs now name that gap instead of denying it; the
  move_note doc no longer promises a Markdown-form link always dangles.
- The three a_*_move_leaves_the_index_a_rebuild_would_project tests compared
  only edges and resources; projection() now compares notes and chunks too,
  so the names hold.
- Stale mentions of the dropped note_aliases table (mv, rm, index-engine.md)
  and a SCHEMA_VERSION doc entry for 7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
A warm eval loaded LocalEmbedder up to three times: inside provision()'s
"already installed" check, again right after, and a third time for the
dense fixture. examples/common gains load_or_provision (load first,
provision only if that fails); eval shares one Rc-backed SharedEmbedder
across its fixtures, and score_dense takes the embedder instead of
loading its own. stability and groundedness use load_or_provision too.
Verified on full --stemmer --sweep runs: identical stdout and results
rows against the pre-change binary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCwoP7o76vxQ8rEQq7zM8w
@coderabbitai

coderabbitai Bot commented Sep 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 132 files, which is 32 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 748562a9-d4f6-4125-acae-edd2c8f64c05

📥 Commits

Reviewing files that changed from the base of the PR and between 2e7c93e and 9f4e25f.

📒 Files selected for processing (132)
  • crates/b2-cli/src/args.rs
  • crates/b2-cli/src/cancel.rs
  • crates/b2-cli/src/chat.rs
  • crates/b2-cli/src/error.rs
  • crates/b2-cli/src/logging.rs
  • crates/b2-cli/src/main.rs
  • crates/b2-cli/src/read.rs
  • crates/b2-cli/src/reindex.rs
  • crates/b2-cli/src/wiring.rs
  • crates/b2-cli/src/write.rs
  • crates/b2-core/src/add.rs
  • crates/b2-core/src/chat.rs
  • crates/b2-core/src/chunk.rs
  • crates/b2-core/src/db.rs
  • crates/b2-core/src/discover.rs
  • crates/b2-core/src/embed.rs
  • crates/b2-core/src/error.rs
  • crates/b2-core/src/graph.rs
  • crates/b2-core/src/import.rs
  • crates/b2-core/src/ingest.rs
  • crates/b2-core/src/lib.rs
  • crates/b2-core/src/link.rs
  • crates/b2-core/src/llm.rs
  • crates/b2-core/src/mv.rs
  • crates/b2-core/src/note.rs
  • crates/b2-core/src/pathspec.rs
  • crates/b2-core/src/relation.rs
  • crates/b2-core/src/resource.rs
  • crates/b2-core/src/rm.rs
  • crates/b2-core/src/search.rs
  • crates/b2-core/src/snippet.rs
  • crates/b2-core/src/vault.rs
  • crates/b2-core/src/vault/chat.rs
  • crates/b2-core/src/vault/views.rs
  • crates/b2-core/tests/add.rs
  • crates/b2-core/tests/ask.rs
  • crates/b2-core/tests/cancel.rs
  • crates/b2-core/tests/chunks.rs
  • crates/b2-core/tests/common/mod.rs
  • crates/b2-core/tests/discover.rs
  • crates/b2-core/tests/embed.rs
  • crates/b2-core/tests/ingest_resolver.rs
  • crates/b2-core/tests/mv.rs
  • crates/b2-core/tests/project_embed.rs
  • crates/b2-core/tests/props.rs
  • crates/b2-core/tests/resources.rs
  • crates/b2-core/tests/roundtrip.rs
  • crates/b2-core/tests/search.rs
  • crates/b2-core/tests/substrate.rs
  • crates/b2-core/tests/vault.rs
  • crates/b2-core/tests/vector_pool_scale.rs
  • crates/b2-core/tests/why.rs
  • crates/b2-core/tests/write.rs
  • crates/b2-core/tests/write_frontmatter.rs
  • crates/b2-desktop/CLAUDE.md
  • crates/b2-desktop/src/chat.rs
  • crates/b2-desktop/src/commands.rs
  • crates/b2-desktop/src/error.rs
  • crates/b2-desktop/src/logging.rs
  • crates/b2-desktop/src/main.rs
  • crates/b2-desktop/src/slot.rs
  • crates/b2-desktop/src/state_file.rs
  • crates/b2-desktop/src/stats.rs
  • crates/b2-embed/examples/calibrate.rs
  • crates/b2-embed/examples/common/mod.rs
  • crates/b2-embed/examples/eval.rs
  • crates/b2-embed/examples/eval/ablation.rs
  • crates/b2-embed/examples/eval/dense.rs
  • crates/b2-embed/examples/eval/discovery.rs
  • crates/b2-embed/examples/eval/evidence.rs
  • crates/b2-embed/examples/eval/fold.rs
  • crates/b2-embed/examples/eval/gate.rs
  • crates/b2-embed/examples/eval/instrument.rs
  • crates/b2-embed/examples/eval/labels.rs
  • crates/b2-embed/examples/eval/main.rs
  • crates/b2-embed/examples/eval/metrics.rs
  • crates/b2-embed/examples/eval/report.rs
  • crates/b2-embed/examples/eval/retrieval.rs
  • crates/b2-embed/examples/eval/tail.rs
  • crates/b2-embed/examples/stability.rs
  • crates/b2-embed/src/config.rs
  • crates/b2-embed/src/lib.rs
  • crates/b2-embed/src/model.rs
  • crates/b2-llm/examples/groundedness.rs
  • crates/b2-llm/src/lib.rs
  • crates/b2-llm/src/provider.rs
  • crates/b2-llm/src/setup.rs
  • crates/b2-llm/src/sse.rs
  • crates/b2-llm/tests/wire.rs
  • docs/data-model.md
  • docs/evals.md
  • docs/index-engine.md
  • docs/quickstart.md
  • ui/src/api.ts
  • ui/src/bindings.ts
  • ui/src/chat.test.ts
  • ui/src/chat.ts
  • ui/src/chatview.ts
  • ui/src/coverage.test.ts
  • ui/src/coverage.ts
  • ui/src/droplink.test.ts
  • ui/src/droplink.ts
  • ui/src/editorcmds.test.ts
  • ui/src/editorcmds.ts
  • ui/src/embedreminder.test.ts
  • ui/src/embedreminder.ts
  • ui/src/explainview.ts
  • ui/src/findfield.test.ts
  • ui/src/findfield.ts
  • ui/src/graphview.ts
  • ui/src/hints.test.ts
  • ui/src/hints.ts
  • ui/src/keymap.ts
  • ui/src/keysview.ts
  • ui/src/livepreview.ts
  • ui/src/main.ts
  • ui/src/markdown.ts
  • ui/src/move.test.ts
  • ui/src/move.ts
  • ui/src/recorder.test.ts
  • ui/src/recorder.ts
  • ui/src/render.ts
  • ui/src/sanitize.ts
  • ui/src/settingstabs.test.ts
  • ui/src/settingstabs.ts
  • ui/src/settingsview.ts
  • ui/src/shortcuts.ts
  • ui/src/state.ts
  • ui/src/theme.test.ts
  • ui/src/theme.ts
  • ui/src/widgets.ts
  • ui/style.css

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@samkeen
samkeen merged commit 3637de4 into main Sep 26, 2026
2 checks passed
@samkeen
samkeen deleted the claude/tech-debt-cleanup-y3s69c branch September 26, 2026 23:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants