hephaestus/crates/heph-core/src/store.rs
Erich Blume d0debfceb9
Some checks failed
Build / validate (pull_request) Failing after 4s
heph-core: recurrence (roll-forward in place) + per-task logs
Slice 5 (tech-spec §4.4). Completing a recurring task rolls it forward in
place instead of marking it done — the Todoist-corner-avoiding model.

Pure recurrence module:
- next_occurrence(rrule, anchor, after): lazy RRULE expansion (rrule +
  chrono/UTC) returning the next instance strictly after `after`,
  skipping missed occurrences; None when a finite series is exhausted.
- reset_checkboxes(body): the fresh-checklist transform — unchecks every
  `- [x]`, idempotent, preserves indentation/bullet/line-endings.

Storage roll-forward (one transaction, on set_state(done) of a recurring
task): reset the canonical context doc's checklist, append the completed
occurrence to the task's log, advance do_date to the next instance after
now (skipping misses); finite series finally goes done. `skip` advances
the same way without logging. Non-recurring done is unchanged.

Per-task append-only log (`log-of` doc): log_append / log_tail — the
resumption breadcrumb + recurring-completion narrative ([[design]] §6.4).

Tests: 7 recurrence unit + 2 proptests (no checked marker survives reset;
reset idempotent for any body) + 6 end-to-end incl. five-occurrence
no-carry-forward and missed-collapse-to-one. 53 tests green. This
completes the heph-core library layer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:14:22 -07:00

82 lines
3.4 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::model::{Attention, Link, LinkType, NewNode, NewTask, Node, Task, TaskState};
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>;
// --- 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>;
/// 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>>;
// --- 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>>;
}