pub struct Store { /* private fields */ }Expand description
The store of one checkout: the one place reasoning records are read from and written to.
It is a handle on a repository root and nothing more; constructing one touches no
file. Records live under STATE_DIR, one directory per task and one JSON file per
record.
use majordomus_cli::reasoning::store::{Store, STATE_DIR};
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path());
assert_eq!(store.dir(), dir.path().join(STATE_DIR));
assert!(store.load().records.is_empty(), "an absent store reads as empty");Implementations§
Source§impl Store
impl Store
Sourcepub fn at(root: &Path) -> Self
pub fn at(root: &Path) -> Self
The store of the repository at root.
Nothing is read or created until the store is loaded or written.
use majordomus_cli::reasoning::store::Store;
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path());
assert!(store.dir().starts_with(dir.path()));
assert!(!store.dir().exists());Sourcepub fn dir(&self) -> PathBuf
pub fn dir(&self) -> PathBuf
The directory the records live in: STATE_DIR under the repository root.
use std::path::Path;
use majordomus_cli::reasoning::store::Store;
let store = Store::at(Path::new("/repo"));
assert_eq!(store.dir(), Path::new("/repo/.ai/local/state/reasoning"));Sourcepub fn current_task(&self) -> String
pub fn current_task(&self) -> String
The task open in this checkout, from .ai/local/state/current.yaml, or
UNTASKED.
The task id is the id: line of that file, quotes trimmed. A missing file, a file
with no such line, or an empty id all mean no task is open.
use majordomus_cli::reasoning::store::{Store, UNTASKED};
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path());
assert_eq!(store.current_task(), UNTASKED);
let state = dir.path().join(".ai/local/state");
std::fs::create_dir_all(&state).unwrap();
std::fs::write(state.join("current.yaml"), "id: \"task-42\"\n").unwrap();
assert_eq!(store.current_task(), "task-42");Sourcepub fn load(&self) -> Loaded
pub fn load(&self) -> Loaded
Every record of every task, in canonical order.
Only .json files are read, so a temporary file of a write in progress is never
met. An absent store is an empty one; a file that does not parse is reported in
Loaded::unreadable.
use majordomus_cli::reasoning::record::*;
use majordomus_cli::reasoning::store::Store;
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path());
for (id, at) in [("attempt-b", "2026-10-01T00:00:02Z"), ("attempt-a", "2026-10-01T00:00:01Z")] {
store.write(&ReasoningRecord {
schema: RECORD_SCHEMA.into(),
version: RECORD_VERSION,
id: id.into(),
task: "t".into(),
recorded_at: at.into(),
head: None,
redacted: vec![],
body: ReasoningBody::Attempt(AttemptInput {
assessment: None,
attempt: "x".into(),
observed_failure: "y".into(),
assumption_invalidated: None,
evidence: vec![],
updated_hypothesis: None,
}),
})
.unwrap();
}
let ids: Vec<String> = store.load().records.into_iter().map(|r| r.id).collect();
assert_eq!(ids, ["attempt-a", "attempt-b"], "ordered by when, not by name");Sourcepub fn write(&self, record: &ReasoningRecord) -> Result<PathBuf>
pub fn write(&self, record: &ReasoningRecord) -> Result<PathBuf>
Make a record durable: temporary name, then rename. Returns its path.
The file is <task>/<id>.json under the store. A record whose file already exists
is refused with AlreadyExists: a record is written once and never overwritten.
use majordomus_cli::reasoning::record::*;
use majordomus_cli::reasoning::store::Store;
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path());
let record = ReasoningRecord {
schema: RECORD_SCHEMA.into(),
version: RECORD_VERSION,
id: "assessment-1".into(),
task: "t".into(),
recorded_at: "2026-10-01T00:00:01Z".into(),
head: None,
redacted: vec![],
body: ReasoningBody::Assessment(AssessmentInput::default()),
};
let path = store.write(&record).unwrap();
assert_eq!(path, store.dir().join("t/assessment-1.json"));
let again = store.write(&record).unwrap_err();
assert_eq!(again.kind(), std::io::ErrorKind::AlreadyExists);