hephaestus/crates/heph-core/src/model.rs
Erich Blume bbac338f76
Some checks failed
Build / validate (pull_request) Failing after 3s
Scaffold cargo workspace + heph-core foundation
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>
2026-05-31 18:52:15 -07:00

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()),
}
}
}