Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
4.1 KiB
Crate: crates/engine (notescraft-engine, layer L4). Slice 2: the Session and the first real commands. Slice 1 (the generic command registry Registry<C>, CommandSpec<C>, ParamSpec, Error) already exists: read src/lib.rs and src/registry.rs, src/spec.rs, src/params.rs first and follow their style; do not change registry behaviour. SPEC sections 3.1, 4.1-4.3, 6 items 1-3, 10. Allowed new dependencies (all { workspace = true }): notescraft-vault, notescraft-markdown, notescraft-index, serde, serde_json, thiserror. Read crates/vault/src/lib.rs, crates/index/src/lib.rs and crates/markdown/src/lib.rs for the exact APIs. Build in SMALL steps: one new file per tool call (<=250 lines), build and run clippy after each file, declare every module in lib.rs.
Implement:
session.rs:Session::open(root: &Path) -> Result<Session>opens the vault (Vault::open), scanslist()and feeds every.mdfile intonotescraft_index::Index(update_note) and registers other files withadd_file;Session::open_read_only. Fields are private; accessorsvault(),index(). A file that fails to read or index is skipped and recorded insession.diagnostics()(never aborts the open).buffers.rs: open note buffers:Buffer { path, text, saved_text, undo: Vec<Edit>, redo: Vec<Edit> }withedit(range, text)usingnotescraft_markdown::Document::replace_rangesemantics (invalid ranges are errors),undo(),redo(),is_dirty(), cap 1000 undo steps and 50 MiB per buffer;Session::open_buffer(path),close_buffer(path)(error if dirty unlessforce),save_buffer(path)(writes throughVault::write, then re-indexes),buffers().notes.rsoperations on the session (pure Rust methods the commands call):read_note,write_note(path, content)(vault write +update_note),create_note(path, content)(fails if exists; path must end in.md),delete_note(to trash +remove_note),rename_note(old, new, update_links: bool): moves the file, re-keys the index, and whenupdate_linksis true rewrites every inbound link in other notes (useIndex::backlinksfor source and byte ranges andnotescraft_markdown::parseto find the link item; keep the alias and the#heading/#^blocksuffix, keep the basename form when the link used a bare name, use the new path form when it used a path); all link rewrites are collected first and applied only if every note can be rewritten (otherwise nothing is written and an error lists the failures). Returns aRenameReport { rewritten: Vec<String> }.external.rs:Session::apply_event(VaultEvent)re-reads a changed file and updates the index; ignores events for files with a dirty buffer (records a conflict insession.conflicts()instead).commands.rs:pub fn register_core(reg: &mut Registry<Session>)registering with exact ids:vault.info(note count, tag count, unresolved count),note.list(optionalfolder),note.read,note.write,note.create,note.delete,note.rename(from,to, optionalupdate_linksdefault true),links.outgoing,links.backlinks,links.unresolved,tags.list(optionalprefix),buffer.open,buffer.edit,buffer.save,buffer.undo,buffer.redo. Each has fullParamSpecs, adoc,enabledrules (write commands disabled on a read-only session) and returns JSON. Commands never panic on any params (the registry validates types; you still validate values: paths, ranges).
Tests (integration in tests/, build temp vaults under std::env::temp_dir with unique names): open a vault with 20 notes and check index counts; read-only session rejects writes; create/write/delete; rename with link rewriting (bare, aliased, heading, path-form, embed, markdown link) and the all-or-nothing behaviour; buffer edit/undo/redo/dirty/close rules; external change applied and conflict recorded when dirty; every core command callable through Registry::run with valid params and rejected with bad params; the registry panic_hunt adversarial params against all core commands (no panic escapes). Reserved paths .obsidian/ and .notescraft/ are rejected by the vault already: assert the commands surface that error.