generated from eblume/project-template
Some checks failed
Build / validate (pull_request) Failing after 3s
Kick off Phase 1 (v1 prototype) per tech-spec §11.1. Sets up the Cargo workspace and the first TDD slice of heph-core: - Migration runner + §4.5 SQLite schema (nodes, tasks, links, aliases, users, oplog, sync_state, conflicts), versioned via PRAGMA user_version. - Clock-injected `Clock` trait (no ambient wall-clock reads; §2). - `Store` trait + `LocalStore` SQLite backend with node create/get, bootstrapping the single local user (oidc_sub NULL, §13). - Node model (kinds: doc/task/project/tag/journal). Repo housekeeping: fill AGENTS.md Project Structure (last template TODO), ignore /target, add self-bootstrapping .forgejo/scripts/build that runs cargo fmt/clippy/test in CI (§9), changelog fragment. Tests green: 4 unit tests (migration version, local-user idempotency, create/get round-trip, missing-node None). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
2.7 KiB
Rust
91 lines
2.7 KiB
Rust
//! Core data model: nodes and their kinds (tech-spec §4).
|
|
//!
|
|
//! Every first-class entity is a [`Node`]. Tasks, links, recurrence, and the
|
|
//! derived context-item index build on top of this base in later slices.
|
|
|
|
use crate::error::{Error, Result};
|
|
|
|
/// Discriminator for the kind of thing a node is (tech-spec §4.1).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum NodeKind {
|
|
/// Rich context document (knowledge base, work-logs). Body = markdown.
|
|
Doc,
|
|
/// Thin task. Carries no body; context arrives via links.
|
|
Task,
|
|
/// Grouping/context for tasks.
|
|
Project,
|
|
/// A label.
|
|
Tag,
|
|
/// A daily note, titled by ISO date.
|
|
Journal,
|
|
}
|
|
|
|
impl NodeKind {
|
|
/// The wire/storage string for this kind.
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
NodeKind::Doc => "doc",
|
|
NodeKind::Task => "task",
|
|
NodeKind::Project => "project",
|
|
NodeKind::Tag => "tag",
|
|
NodeKind::Journal => "journal",
|
|
}
|
|
}
|
|
|
|
/// Parse a storage string back into a [`NodeKind`].
|
|
pub fn parse(s: &str) -> Result<NodeKind> {
|
|
Ok(match s {
|
|
"doc" => NodeKind::Doc,
|
|
"task" => NodeKind::Task,
|
|
"project" => NodeKind::Project,
|
|
"tag" => NodeKind::Tag,
|
|
"journal" => NodeKind::Journal,
|
|
other => return Err(Error::Integrity(format!("unknown node kind: {other}"))),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// A persisted node (a row of the `nodes` table).
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Node {
|
|
/// Stable, sync-safe id (ULID for content nodes; deterministic for journal/tag).
|
|
pub id: String,
|
|
/// Owning user id (per-user isolation).
|
|
pub owner_id: String,
|
|
/// What kind of node this is.
|
|
pub kind: NodeKind,
|
|
/// Human-facing title.
|
|
pub title: String,
|
|
/// Markdown body (nullable — tasks have none).
|
|
pub body: Option<String>,
|
|
/// Creation time, epoch ms.
|
|
pub created_at: i64,
|
|
/// Last-modified time, epoch ms.
|
|
pub modified_at: i64,
|
|
/// Hybrid logical clock of the last write (sync ordering; placeholder until §12).
|
|
pub hlc: String,
|
|
/// Whether the node is tombstoned (soft-deleted).
|
|
pub tombstoned: bool,
|
|
}
|
|
|
|
/// Input for creating a node.
|
|
#[derive(Debug, Clone)]
|
|
pub struct NewNode {
|
|
/// What kind of node to create.
|
|
pub kind: NodeKind,
|
|
/// Human-facing title.
|
|
pub title: String,
|
|
/// Optional markdown body.
|
|
pub body: Option<String>,
|
|
}
|
|
|
|
impl NewNode {
|
|
/// A document node with a title and body.
|
|
pub fn doc(title: impl Into<String>, body: impl Into<String>) -> NewNode {
|
|
NewNode {
|
|
kind: NodeKind::Doc,
|
|
title: title.into(),
|
|
body: Some(body.into()),
|
|
}
|
|
}
|
|
}
|