generated from eblume/project-template
Add `Store::set_task_project` (heph-core + RemoteStore) and the `task.set_project` RPC: tombstone the task's existing `in-project` link(s) and add a new one (or none, to unfile). A given project id must name a live project-kind node, else InvalidArg/NodeNotFound. Route `heph edit --project` through it, fixing a duplicate-link bug (the old path added an in-project link without removing the prior one); `--project none` now unfiles. Factor a `links::tombstone` helper out of `sync_wiki_links`. Tests: core move/unfile/reject + a duplicate-link regression; a socket dispatch test. The TUI `m` gesture follows in the next commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
196 lines
9.3 KiB
Rust
196 lines
9.3 KiB
Rust
//! The storage abstraction (tech-spec §3.1).
|
|
//!
|
|
//! A runtime points at *something that stores nodes*; whether that is a local
|
|
//! SQLite file ([`crate::sqlite::LocalStore`]) or a remote server (a future
|
|
//! `RemoteStore`) is configuration. This trait is the seam.
|
|
|
|
use crate::error::Result;
|
|
use crate::filter::ListFilter;
|
|
use crate::model::{
|
|
Attention, Conflict, Health, Link, LinkType, NewNode, NewTask, Node, NodeKind, SchedulePatch,
|
|
SyncCursors, Task, TaskState,
|
|
};
|
|
use crate::oplog::Op;
|
|
use crate::ranking::RankedTask;
|
|
|
|
/// A backend that can store and retrieve nodes, tasks, and links.
|
|
///
|
|
/// Methods that mutate take `&mut self`: a `LocalStore` holds an exclusive lock
|
|
/// on its file, so single-writer semantics are honest at the type level.
|
|
pub trait Store {
|
|
// --- nodes ---
|
|
|
|
/// Create a node, assigning it an id and timestamps. Returns the stored row.
|
|
fn create_node(&mut self, input: NewNode) -> Result<Node>;
|
|
|
|
/// Fetch a node by id. Returns `None` if it does not exist.
|
|
///
|
|
/// Tombstoned nodes are still returned here (callers that must exclude them
|
|
/// — `next`, `list`, `search`, `export` — filter explicitly).
|
|
fn get_node(&self, id: &str) -> Result<Option<Node>>;
|
|
|
|
/// Update a node's title and/or body. A body update re-runs markdown
|
|
/// extraction and reconciles this node's `wiki` links (tech-spec §5, §6).
|
|
fn update_node(
|
|
&mut self,
|
|
id: &str,
|
|
title: Option<String>,
|
|
body: Option<String>,
|
|
) -> Result<Node>;
|
|
|
|
/// Tombstone (soft-delete) a node. No hard deletes (tech-spec §4.3).
|
|
fn tombstone_node(&mut self, id: &str) -> Result<()>;
|
|
|
|
/// List non-tombstoned nodes (owner-scoped), optionally filtered by `kind`,
|
|
/// ordered by title. The enumeration surfaces (projects, tags) build on this
|
|
/// (tech-spec §6 `node.list`).
|
|
fn list_nodes(&self, kind: Option<NodeKind>) -> Result<Vec<Node>>;
|
|
|
|
/// Resolve a wiki-link target (`[[title]]`) to a node, **exactly** — an
|
|
/// alias match first, then an exact, owner-scoped, non-tombstoned title
|
|
/// match; `None` if nothing matches (an unresolved link is allowed, §5).
|
|
///
|
|
/// This is the same mapping the store uses to materialize `wiki` links, so
|
|
/// a surface's "follow link under cursor" jumps to the *same* node the
|
|
/// stored link points at — unlike fuzzy `search` (tech-spec §6, §8).
|
|
fn resolve_node(&self, title: &str) -> Result<Option<Node>>;
|
|
|
|
// --- tasks ---
|
|
|
|
/// Create a committed task, auto-creating its canonical context `doc` and
|
|
/// the `canonical-context` link (tech-spec §6).
|
|
fn create_task(&mut self, input: NewTask) -> Result<Task>;
|
|
|
|
/// Fetch a task by its node id.
|
|
fn get_task(&self, node_id: &str) -> Result<Option<Task>>;
|
|
|
|
/// Set a task's lifecycle state. Completing a **recurring** task rolls it
|
|
/// forward in place — fresh checklist, logged occurrence, advanced do-date
|
|
/// (tech-spec §4.4) — rather than marking it done.
|
|
fn set_task_state(&mut self, node_id: &str, state: TaskState) -> Result<Task>;
|
|
|
|
/// Skip the current occurrence of a recurring task: advance its do-date
|
|
/// without logging a completion (tech-spec §4.4). Errors on a non-recurring
|
|
/// task.
|
|
fn skip_recurrence(&mut self, node_id: &str) -> Result<Task>;
|
|
|
|
/// Set a task's attention-state.
|
|
fn set_task_attention(&mut self, node_id: &str, attention: Attention) -> Result<Task>;
|
|
|
|
/// Apply a partial update to a task's schedule scalars — do-date, late-on,
|
|
/// recurrence (tech-spec §6 `task.set_schedule`). Each [`SchedulePatch`]
|
|
/// field is a double option: absent = unchanged, `null` = clear, value =
|
|
/// set. This is the "reschedule" path (the scalars with no dedicated setter).
|
|
fn set_task_schedule(&mut self, node_id: &str, patch: SchedulePatch) -> Result<Task>;
|
|
|
|
/// Re-file a task under a project (or unfile it when `project_id` is
|
|
/// `None`) — the move-to-project path (tech-spec §8.1). OR-set link
|
|
/// semantics: the old `in-project` link is tombstoned and a new one added.
|
|
/// A given `project_id` must name a live `project`-kind node.
|
|
fn set_task_project(&mut self, node_id: &str, project_id: Option<&str>) -> Result<Task>;
|
|
|
|
/// Promote a `- [ ]` context-item line in `container_id`'s body into a
|
|
/// committed task, rewriting that source line into a `[[link]]` to the new
|
|
/// task (Fork A, tech-spec §4.3, §6). `item_ref` is the 1-based index of the
|
|
/// item among the container's context items in document order.
|
|
fn promote(
|
|
&mut self,
|
|
container_id: &str,
|
|
item_ref: usize,
|
|
attention: Option<Attention>,
|
|
project_id: Option<String>,
|
|
) -> Result<Task>;
|
|
|
|
/// The Tactical "what is next?" ranking (tech-spec §7), using the store's
|
|
/// injected clock as `now`. `scope`, when `Some`, restricts to a project
|
|
/// node id; `red` items always appear regardless of `limit`.
|
|
fn next(&self, scope: Option<&str>, limit: usize) -> Result<Vec<RankedTask>>;
|
|
|
|
/// Enumerate outstanding committed tasks matching a [`ListFilter`] — the
|
|
/// data-expressed predicate behind filter views (tech-spec §8.2). Returns
|
|
/// **titled** [`RankedTask`] rows (the same shape `next` returns, so the
|
|
/// survey view needs no N+1 `get_node`). An empty filter is the whole
|
|
/// outstanding set (the Organizational survey, tech-spec §6).
|
|
fn list(&self, filter: &ListFilter) -> Result<Vec<RankedTask>>;
|
|
|
|
/// Run a built-in named filter view (`tom|ondeck|chores|work|tasks`,
|
|
/// tech-spec §8.2): resolve its project names to ids (subtree-expanded),
|
|
/// build the [`ListFilter`], and return the matching rows. Errors on an
|
|
/// unknown view name.
|
|
fn view(&self, name: &str) -> Result<Vec<RankedTask>>;
|
|
|
|
/// Working-set health — orange/active/on-deck/conflict counts (tech-spec §7).
|
|
fn health(&self) -> Result<Health>;
|
|
|
|
/// Full-text search over title + body (FTS5), owner-scoped, best-match
|
|
/// first, tombstones excluded (tech-spec §6). `query` is FTS5 MATCH syntax.
|
|
fn search(&self, query: &str) -> Result<Vec<Node>>;
|
|
|
|
/// Open (creating if absent) the journal node for an ISO `date`. The id is
|
|
/// deterministic in `(owner, date)` so offline replicas converge (§3.1).
|
|
fn journal_open_or_create(&mut self, date: &str) -> Result<Node>;
|
|
|
|
// --- links ---
|
|
|
|
/// Add a typed link between two nodes.
|
|
fn add_link(&mut self, src_id: &str, dst_id: &str, link_type: LinkType) -> Result<Link>;
|
|
|
|
/// All non-tombstoned links originating at `id`.
|
|
fn outgoing_links(&self, id: &str) -> Result<Vec<Link>>;
|
|
|
|
/// All non-tombstoned links pointing at `id` (backlinks).
|
|
fn backlinks(&self, id: &str) -> Result<Vec<Link>>;
|
|
|
|
// --- per-task log ([[design]] §6.4) ---
|
|
|
|
/// Append a line to a task's append-only log (creating the log on first
|
|
/// use). The log is the resumption breadcrumb store.
|
|
fn log_append(&mut self, task_id: &str, text: &str) -> Result<()>;
|
|
|
|
/// The task's latest `n` log entries (oldest→newest); empty if it has none.
|
|
fn log_tail(&self, task_id: &str, n: usize) -> Result<Vec<String>>;
|
|
|
|
/// Export every non-tombstoned node to a `.md` directory tree under `dir`,
|
|
/// returning the count written (tech-spec §5). One-way; no import.
|
|
fn export(&self, dir: &std::path::Path) -> Result<usize>;
|
|
|
|
// --- sync (op-log) ---
|
|
|
|
/// Ops for this owner with HLC strictly greater than `after` (None ⇒ all),
|
|
/// in causal order — the push cursor for sync (tech-spec §12).
|
|
fn ops_since(&self, after: Option<&str>) -> Result<Vec<Op>>;
|
|
|
|
/// Apply a foreign op with the merge rules (LWW / OR-set / tombstone),
|
|
/// idempotently. Returns `true` if newly applied. Ops should be applied in
|
|
/// HLC order (tech-spec §12).
|
|
fn apply_op(&mut self, op: &Op) -> Result<bool>;
|
|
|
|
/// Rewrite this replica's `owner_id` to a canonical user id — the one-time,
|
|
/// pre-first-sync adoption of tech-spec §13. After this all data is owned by
|
|
/// `canonical`; replicas that adopt the same id can sync. (Full adoption,
|
|
/// incl. owner-embedded deterministic ids, is refined with auth.)
|
|
fn adopt_owner(&mut self, canonical: &str) -> Result<()>;
|
|
|
|
/// The push/pull HLC cursors for a sync `peer` (the hub url). Defaults to
|
|
/// empty cursors when this replica has never synced with `peer` (§12).
|
|
fn sync_state(&self, peer: &str) -> Result<SyncCursors>;
|
|
|
|
/// Record progress with a sync `peer`: advance the `pushed`/`pulled` HLC
|
|
/// cursors (each `None` leaves that direction unchanged). Upserts the
|
|
/// `sync_state` row (§12).
|
|
fn record_sync(&mut self, peer: &str, pushed: Option<&str>, pulled: Option<&str>)
|
|
-> Result<()>;
|
|
|
|
/// Single-tenant authentication gate (tech-spec §13). Map an OIDC `sub` to
|
|
/// this store's owner: on first sight, **claim** the owner by binding its
|
|
/// `oidc_sub`; thereafter authorize only that same `sub`. Returns `true` if
|
|
/// the `sub` owns this store, `false` if a different identity presented a
|
|
/// token. A hub calls this before serving any op exchange.
|
|
fn authorize_owner_sub(&mut self, sub: &str) -> Result<bool>;
|
|
|
|
/// Open merge conflicts surfaced for the user (`heph conflicts`).
|
|
fn conflicts_list(&self) -> Result<Vec<Conflict>>;
|
|
|
|
/// Settle a conflict by the user's choice (`"local"`/`"remote"`).
|
|
fn conflicts_resolve(&mut self, id: &str, choice: &str) -> Result<()>;
|
|
}
|