generated from eblume/project-template
Phase 1: v1 prototype #1
14 changed files with 1010 additions and 17 deletions
Scaffold cargo workspace + heph-core foundation
Some checks failed
Build / validate (pull_request) Failing after 3s
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>
commit
bbac338f76
30
.forgejo/scripts/build
Executable file
30
.forgejo/scripts/build
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Project build hook (tech-spec §9): run the Rust workspace checks in CI.
|
||||
#
|
||||
# Invoked by .forgejo/workflows/build.yaml after the generic `prek` pass.
|
||||
# Bootstraps a minimal Rust toolchain via rustup when the runner image lacks
|
||||
# one, so the suite runs without a bespoke CI image. The headless nvim e2e
|
||||
# suite is added here once heph.nvim exists.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v cargo >/dev/null 2>&1; then
|
||||
echo "cargo not found; bootstrapping Rust toolchain via rustup..."
|
||||
export RUSTUP_HOME="${RUSTUP_HOME:-$HOME/.rustup}"
|
||||
export CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}"
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||
| sh -s -- -y --profile minimal
|
||||
# shellcheck disable=SC1091
|
||||
. "$CARGO_HOME/env"
|
||||
rustup component add clippy rustfmt
|
||||
fi
|
||||
|
||||
echo "== cargo fmt --check =="
|
||||
cargo fmt --all --check
|
||||
|
||||
echo "== cargo clippy =="
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
echo "== cargo test =="
|
||||
cargo test --all
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -6,6 +6,9 @@ __pycache__/
|
|||
*.pyo
|
||||
.venv/
|
||||
|
||||
# Rust
|
||||
/target/
|
||||
|
||||
# Linter caches
|
||||
.ruff_cache/
|
||||
|
||||
|
|
|
|||
33
AGENTS.md
33
AGENTS.md
|
|
@ -6,18 +6,7 @@ Guidance for Claude Code working in this repository. See also [[ai-assistance-gu
|
|||
|
||||
**hephaestus** — Personal context management system: wiki-style knowledge base and task management.
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
This repository was instantiated from the `project-template` Forgejo template. Setup status:
|
||||
|
||||
- [x] `baseUrl` in `docs/quartz.config.ts` set to `localhost` (update once docs are hosted)
|
||||
- [x] Dagger module renamed to `.dagger/src/hephaestus_ci/` (`HephaestusCi` class)
|
||||
- [ ] Fill in the Project Structure section below once the Rust workspace is scaffolded
|
||||
- [x] Fill in license info in `README.md` (All Rights Reserved / private)
|
||||
|
||||
Delete this section once the remaining items are resolved.
|
||||
|
||||
> **This is now a generated repo, not the template source.** C1/C2 changes use feature branches + PRs (`tea pr create`); noteworthy changes get changelog fragments in `docs/changelog.d/`.
|
||||
> **This is a generated repo, not the template source.** C1/C2 changes use feature branches + PRs (`tea pr create`); noteworthy changes get changelog fragments in `docs/changelog.d/`.
|
||||
|
||||
## Rules
|
||||
|
||||
|
|
@ -52,13 +41,23 @@ See [[agent-change-process]] for the full methodology.
|
|||
|
||||
## Project Structure
|
||||
|
||||
A Cargo workspace (`Cargo.toml` at the root) plus the Neovim plugin and repo tooling. The build follows the tech-spec §11.1 slice order; crates are added to the workspace as their slice begins, so not every crate below exists yet.
|
||||
|
||||
```
|
||||
./docs/ # Diataxis docs, Quartz config, and release content
|
||||
./docs/changelog.d/ # keep only .gitkeep in the template; generated repos add towncrier fragments here
|
||||
./Cargo.toml # workspace manifest (shared deps + members)
|
||||
./crates/heph-core/ # core lib: data model, Store trait + SQLite store, extraction,
|
||||
# recurrence, "what is next?" ranking, op-log/HLC/CRDT sync
|
||||
./crates/hephd/ # daemon (planned): local/server/client modes; JSON-RPC over unix socket
|
||||
./crates/heph/ # CLI (planned): export, scripting, `heph conflicts`
|
||||
./heph.nvim/ # Neovim plugin (planned): primary surface; replaces obsidian.nvim
|
||||
./docs/ # Diataxis docs (incl. [[design]] + [[tech-spec]]), Quartz config, release content
|
||||
./docs/changelog.d/ # towncrier fragments for noteworthy changes
|
||||
./.dagger/ # Dagger module (src/hephaestus_ci/) backing docs builds and releases
|
||||
./.forgejo/workflows/ # generic build and release workflows for generated repos
|
||||
./.forgejo/scripts/ # optional per-project build/release hooks consumed by the workflows
|
||||
./.forgejo/workflows/ # build + release workflows
|
||||
./.forgejo/scripts/ # per-project build/release hooks (build runs cargo test once present)
|
||||
./mise-tasks/ # repo automation via `mise run`
|
||||
```
|
||||
|
||||
Other code paths will be listed via ai-docs. When you encounter wiki-links (`[[like-this]]`) it is referring to docs/ cards.
|
||||
**Development is TDD** (tech-spec §2, §9): failing test first, implement to green, commit on green. `heph-core` is clock-injected — no ambient wall-clock reads; time is always passed in. Canonical spec is [[tech-spec]]; rationale is [[design]].
|
||||
|
||||
Other doc paths are listed via `mise run ai-docs`. Wiki-links (`[[like-this]]`) refer to `docs/` cards.
|
||||
|
|
|
|||
420
Cargo.lock
generated
Normal file
420
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heph-core"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"rusqlite",
|
||||
"thiserror",
|
||||
"ulid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.99"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.32.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ulid"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe"
|
||||
dependencies = [
|
||||
"rand",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.122"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.122"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.122"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.122"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
20
Cargo.toml
Normal file
20
Cargo.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/heph-core"]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
version = "0.0.0"
|
||||
license = "LicenseRef-Proprietary"
|
||||
publish = false
|
||||
authors = ["Erich Blume <blume.erich@gmail.com>"]
|
||||
rust-version = "1.85"
|
||||
|
||||
[workspace.dependencies]
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
ulid = "1"
|
||||
thiserror = "2"
|
||||
anyhow = "1"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
14
crates/heph-core/Cargo.toml
Normal file
14
crates/heph-core/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "heph-core"
|
||||
description = "Hephaestus core: data model, storage backend, query engine, extraction, recurrence, and sync."
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
authors.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
rusqlite.workspace = true
|
||||
ulid.workspace = true
|
||||
thiserror.workspace = true
|
||||
22
crates/heph-core/src/clock.rs
Normal file
22
crates/heph-core/src/clock.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
//! Clock injection.
|
||||
//!
|
||||
//! `heph-core` never reads the ambient wall clock (tech-spec §2): the current
|
||||
//! time is always supplied through a [`Clock`]. This keeps ranking and
|
||||
//! recurrence deterministic and testable. The daemon wires in a real
|
||||
//! system clock; tests wire in a [`FixedClock`].
|
||||
|
||||
/// Source of the current time, in epoch milliseconds.
|
||||
pub trait Clock: Send + Sync {
|
||||
/// Current time as epoch milliseconds.
|
||||
fn now_ms(&self) -> i64;
|
||||
}
|
||||
|
||||
/// A clock pinned to a fixed instant — for deterministic tests.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct FixedClock(pub i64);
|
||||
|
||||
impl Clock for FixedClock {
|
||||
fn now_ms(&self) -> i64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
24
crates/heph-core/src/error.rs
Normal file
24
crates/heph-core/src/error.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
//! Error type for `heph-core`.
|
||||
|
||||
/// Errors surfaced by the core library.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// A SQLite-level failure.
|
||||
#[error("sqlite: {0}")]
|
||||
Sqlite(#[from] rusqlite::Error),
|
||||
|
||||
/// The DB file is already locked by another `local`/`server` process.
|
||||
#[error("store is already locked by another process: {0}")]
|
||||
Locked(String),
|
||||
|
||||
/// A referenced node does not exist.
|
||||
#[error("node not found: {0}")]
|
||||
NodeNotFound(String),
|
||||
|
||||
/// A value in the database did not match the expected shape.
|
||||
#[error("data integrity: {0}")]
|
||||
Integrity(String),
|
||||
}
|
||||
|
||||
/// Convenience result alias.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
21
crates/heph-core/src/lib.rs
Normal file
21
crates/heph-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
//! `heph-core` — the Hephaestus core library.
|
||||
//!
|
||||
//! Data model, the [`Store`](store::Store) abstraction + local SQLite store,
|
||||
//! query engine, markdown extraction, recurrence, and (later) the sync engine.
|
||||
//! See `docs/reference/tech-spec.md` for the canonical specification.
|
||||
//!
|
||||
//! The library is synchronous and side-effect-light. All time enters through an
|
||||
//! injected [`Clock`](clock::Clock) (tech-spec §2) so ranking and recurrence are
|
||||
//! deterministic.
|
||||
|
||||
pub mod clock;
|
||||
pub mod error;
|
||||
pub mod model;
|
||||
pub mod sqlite;
|
||||
pub mod store;
|
||||
|
||||
pub use clock::{Clock, FixedClock};
|
||||
pub use error::{Error, Result};
|
||||
pub use model::{NewNode, Node, NodeKind};
|
||||
pub use sqlite::LocalStore;
|
||||
pub use store::Store;
|
||||
91
crates/heph-core/src/model.rs
Normal file
91
crates/heph-core/src/model.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
//! 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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
112
crates/heph-core/src/sqlite/migrations.rs
Normal file
112
crates/heph-core/src/sqlite/migrations.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
//! Schema migrations, applied in order against SQLite's `user_version`.
|
||||
//!
|
||||
//! Each migration is `(version, sql)`. On open we read `PRAGMA user_version`
|
||||
//! and apply every migration whose version is greater, bumping the pragma as we
|
||||
//! go. Migrations are additive across build slices (tech-spec §4.5 is a
|
||||
//! starting point; later slices add tables such as `nodes_fts`).
|
||||
|
||||
use crate::error::Result;
|
||||
use rusqlite::Connection;
|
||||
|
||||
/// The ordered list of migrations. Never reorder or mutate a shipped entry —
|
||||
/// only append.
|
||||
const MIGRATIONS: &[(i64, &str)] = &[(1, MIGRATION_0001)];
|
||||
|
||||
/// v1 — the base node graph, identity, and sync scaffolding (tech-spec §4.5).
|
||||
const MIGRATION_0001: &str = r#"
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
oidc_sub TEXT UNIQUE,
|
||||
name TEXT,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE nodes (
|
||||
id TEXT PRIMARY KEY,
|
||||
owner_id TEXT NOT NULL REFERENCES users(id),
|
||||
kind TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
body TEXT,
|
||||
body_crdt BLOB,
|
||||
created_at INTEGER NOT NULL,
|
||||
modified_at INTEGER NOT NULL,
|
||||
hlc TEXT NOT NULL,
|
||||
tombstoned INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX idx_nodes_owner_kind ON nodes(owner_id, kind);
|
||||
|
||||
CREATE TABLE tasks (
|
||||
node_id TEXT PRIMARY KEY REFERENCES nodes(id),
|
||||
attention TEXT,
|
||||
do_date INTEGER,
|
||||
late_on INTEGER,
|
||||
state TEXT NOT NULL,
|
||||
recurrence TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE links (
|
||||
id TEXT PRIMARY KEY,
|
||||
src_id TEXT NOT NULL REFERENCES nodes(id),
|
||||
dst_id TEXT NOT NULL REFERENCES nodes(id),
|
||||
type TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
tombstoned INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX idx_links_src ON links(src_id, type);
|
||||
CREATE INDEX idx_links_dst ON links(dst_id, type);
|
||||
|
||||
CREATE TABLE aliases (
|
||||
node_id TEXT REFERENCES nodes(id),
|
||||
alias TEXT
|
||||
);
|
||||
CREATE INDEX idx_aliases_alias ON aliases(alias);
|
||||
|
||||
CREATE TABLE oplog (
|
||||
id TEXT PRIMARY KEY,
|
||||
owner_id TEXT NOT NULL REFERENCES users(id),
|
||||
hlc TEXT NOT NULL,
|
||||
origin TEXT NOT NULL,
|
||||
op_type TEXT NOT NULL,
|
||||
target_id TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
applied INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE sync_state (
|
||||
peer TEXT PRIMARY KEY,
|
||||
last_pushed_hlc TEXT,
|
||||
last_pulled_hlc TEXT,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE conflicts (
|
||||
id TEXT PRIMARY KEY,
|
||||
owner_id TEXT NOT NULL REFERENCES users(id),
|
||||
node_id TEXT NOT NULL REFERENCES nodes(id),
|
||||
field TEXT NOT NULL,
|
||||
local_val TEXT,
|
||||
remote_val TEXT,
|
||||
local_hlc TEXT,
|
||||
remote_hlc TEXT,
|
||||
status TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
"#;
|
||||
|
||||
/// Apply all pending migrations to `conn`.
|
||||
pub fn migrate(conn: &Connection) -> Result<()> {
|
||||
let current: i64 = conn.query_row("PRAGMA user_version", [], |r| r.get(0))?;
|
||||
for &(version, sql) in MIGRATIONS {
|
||||
if version > current {
|
||||
conn.execute_batch(sql)?;
|
||||
// user_version doesn't accept bound params; the value is a trusted const.
|
||||
conn.execute_batch(&format!("PRAGMA user_version = {version}"))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The schema version this build migrates up to (the latest migration number).
|
||||
pub fn latest_version() -> i64 {
|
||||
MIGRATIONS.last().map(|&(v, _)| v).unwrap_or(0)
|
||||
}
|
||||
213
crates/heph-core/src/sqlite/mod.rs
Normal file
213
crates/heph-core/src/sqlite/mod.rs
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
//! SQLite-backed [`Store`] implementation.
|
||||
//!
|
||||
//! `LocalStore` opens a SQLite file directly. The exclusive-lock handoff of
|
||||
//! tech-spec §3.1 is layered on by `hephd` when it owns the file; the store
|
||||
//! itself stays a thin, synchronous SQLite wrapper so it is trivially testable
|
||||
//! against an in-memory database.
|
||||
|
||||
mod migrations;
|
||||
|
||||
pub use migrations::latest_version;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use rusqlite::{Connection, OptionalExtension, Row};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::clock::Clock;
|
||||
use crate::error::Result;
|
||||
use crate::model::{NewNode, Node, NodeKind};
|
||||
use crate::store::Store;
|
||||
|
||||
/// A SQLite file (or in-memory database) opened directly as a backend.
|
||||
pub struct LocalStore {
|
||||
conn: Connection,
|
||||
owner_id: String,
|
||||
clock: Box<dyn Clock>,
|
||||
}
|
||||
|
||||
impl LocalStore {
|
||||
/// Open (creating if needed) a SQLite database at `path`.
|
||||
pub fn open(path: impl AsRef<Path>, clock: Box<dyn Clock>) -> Result<Self> {
|
||||
let conn = Connection::open(path)?;
|
||||
Self::init(conn, clock)
|
||||
}
|
||||
|
||||
/// Open a throwaway in-memory database — for tests.
|
||||
pub fn open_in_memory(clock: Box<dyn Clock>) -> Result<Self> {
|
||||
let conn = Connection::open_in_memory()?;
|
||||
Self::init(conn, clock)
|
||||
}
|
||||
|
||||
fn init(conn: Connection, clock: Box<dyn Clock>) -> Result<Self> {
|
||||
conn.execute_batch("PRAGMA foreign_keys = ON;")?;
|
||||
migrations::migrate(&conn)?;
|
||||
let owner_id = ensure_local_user(&conn, clock.as_ref())?;
|
||||
Ok(LocalStore {
|
||||
conn,
|
||||
owner_id,
|
||||
clock,
|
||||
})
|
||||
}
|
||||
|
||||
/// The id of the user whose data this store reads/writes.
|
||||
///
|
||||
/// For a local-only instance this is the single generated local user
|
||||
/// (`oidc_sub = NULL`, tech-spec §13).
|
||||
pub fn owner_id(&self) -> &str {
|
||||
&self.owner_id
|
||||
}
|
||||
|
||||
/// Placeholder HLC string until the real hybrid logical clock lands (§12).
|
||||
///
|
||||
/// Zero-padded epoch ms keeps it lexically sortable in the meantime.
|
||||
fn next_hlc(&self, now_ms: i64) -> String {
|
||||
format!("{now_ms:016}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure a single local user exists, returning its id.
|
||||
fn ensure_local_user(conn: &Connection, clock: &dyn Clock) -> Result<String> {
|
||||
if let Some(id) = conn
|
||||
.query_row(
|
||||
"SELECT id FROM users ORDER BY created_at LIMIT 1",
|
||||
[],
|
||||
|r| r.get::<_, String>(0),
|
||||
)
|
||||
.optional()?
|
||||
{
|
||||
return Ok(id);
|
||||
}
|
||||
let id = Ulid::new().to_string();
|
||||
conn.execute(
|
||||
"INSERT INTO users (id, oidc_sub, name, created_at) VALUES (?1, NULL, 'local', ?2)",
|
||||
(&id, clock.now_ms()),
|
||||
)?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Map a `nodes` row to a [`Node`]. Column order must match the SELECT.
|
||||
fn row_to_node(row: &Row) -> rusqlite::Result<Node> {
|
||||
Ok(Node {
|
||||
id: row.get("id")?,
|
||||
owner_id: row.get("owner_id")?,
|
||||
kind: NodeKind::parse(&row.get::<_, String>("kind")?)
|
||||
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?,
|
||||
title: row.get("title")?,
|
||||
body: row.get("body")?,
|
||||
created_at: row.get("created_at")?,
|
||||
modified_at: row.get("modified_at")?,
|
||||
hlc: row.get("hlc")?,
|
||||
tombstoned: row.get::<_, i64>("tombstoned")? != 0,
|
||||
})
|
||||
}
|
||||
|
||||
const NODE_COLUMNS: &str =
|
||||
"id, owner_id, kind, title, body, created_at, modified_at, hlc, tombstoned";
|
||||
|
||||
impl Store for LocalStore {
|
||||
fn create_node(&mut self, input: NewNode) -> Result<Node> {
|
||||
let now = self.clock.now_ms();
|
||||
let node = Node {
|
||||
id: Ulid::new().to_string(),
|
||||
owner_id: self.owner_id.clone(),
|
||||
kind: input.kind,
|
||||
title: input.title,
|
||||
body: input.body,
|
||||
created_at: now,
|
||||
modified_at: now,
|
||||
hlc: self.next_hlc(now),
|
||||
tombstoned: false,
|
||||
};
|
||||
self.conn.execute(
|
||||
"INSERT INTO nodes (id, owner_id, kind, title, body, created_at, modified_at, hlc, tombstoned)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 0)",
|
||||
(
|
||||
&node.id,
|
||||
&node.owner_id,
|
||||
node.kind.as_str(),
|
||||
&node.title,
|
||||
&node.body,
|
||||
node.created_at,
|
||||
node.modified_at,
|
||||
&node.hlc,
|
||||
),
|
||||
)?;
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
fn get_node(&self, id: &str) -> Result<Option<Node>> {
|
||||
let node = self
|
||||
.conn
|
||||
.query_row(
|
||||
&format!("SELECT {NODE_COLUMNS} FROM nodes WHERE id = ?1"),
|
||||
[id],
|
||||
row_to_node,
|
||||
)
|
||||
.optional()?;
|
||||
Ok(node)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::clock::FixedClock;
|
||||
|
||||
fn store_at(now_ms: i64) -> LocalStore {
|
||||
LocalStore::open_in_memory(Box::new(FixedClock(now_ms))).expect("open in-memory store")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrations_bring_schema_to_latest() {
|
||||
let store = store_at(0);
|
||||
let v: i64 = store
|
||||
.conn
|
||||
.query_row("PRAGMA user_version", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(v, latest_version());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_twice_is_idempotent_for_the_local_user() {
|
||||
// Re-running init against the same connection-equivalent must not create
|
||||
// a second local user; ensure_local_user is the guard.
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
conn.execute_batch("PRAGMA foreign_keys = ON;").unwrap();
|
||||
migrations::migrate(&conn).unwrap();
|
||||
let a = ensure_local_user(&conn, &FixedClock(1)).unwrap();
|
||||
let b = ensure_local_user(&conn, &FixedClock(2)).unwrap();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_then_get_round_trips() {
|
||||
let mut store = store_at(1_700_000_000_000);
|
||||
let created = store
|
||||
.create_node(NewNode::doc(
|
||||
"Roof leak log",
|
||||
"# Roof\n\nCalled contractor.",
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(created.kind, NodeKind::Doc);
|
||||
assert_eq!(created.title, "Roof leak log");
|
||||
assert_eq!(
|
||||
created.body.as_deref(),
|
||||
Some("# Roof\n\nCalled contractor.")
|
||||
);
|
||||
assert_eq!(created.created_at, 1_700_000_000_000);
|
||||
assert_eq!(created.modified_at, 1_700_000_000_000);
|
||||
assert!(!created.tombstoned);
|
||||
assert_eq!(created.owner_id, store.owner_id());
|
||||
|
||||
let fetched = store.get_node(&created.id).unwrap();
|
||||
assert_eq!(fetched.as_ref(), Some(&created));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_missing_node_is_none() {
|
||||
let store = store_at(0);
|
||||
assert_eq!(store.get_node("01ARYZ6S41NONEXISTENT00000").unwrap(), None);
|
||||
}
|
||||
}
|
||||
23
crates/heph-core/src/store.rs
Normal file
23
crates/heph-core/src/store.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
//! 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::{NewNode, Node};
|
||||
|
||||
/// A backend that can store and retrieve nodes.
|
||||
///
|
||||
/// 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 {
|
||||
/// 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>>;
|
||||
}
|
||||
1
docs/changelog.d/v1-prototype.feature.md
Normal file
1
docs/changelog.d/v1-prototype.feature.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Begin the v1 prototype (Phase 1): scaffold the Cargo workspace and the `heph-core` crate with a migration-run SQLite schema (tech-spec §4.5), a clock-injected `Store` trait, and a `LocalStore` backend with node create/get. CI now runs the Rust suite (fmt/clippy/test) via the project build hook.
|
||||
Loading…
Add table
Add a link
Reference in a new issue