1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//! agd-core reads and safely changes org-mode files. It is the library behind
//! the `agd` command line, terminal UI and stdio MCP server, and it works on
//! local files only.
//!
//! Every caller follows the same path:
//!
//! 1. [`load_settings`] reads `config.toml` into [`Settings`]: the roots agd
//! may touch, the agenda files, TODO keywords and the other Org options.
//! 2. [`WorkspaceIndex::load`] parses every agenda file under the roots into an
//! [`IndexedDocument`] and gives each heading an [`Item`] with a stable
//! ref: its `:ID:` when unique, else `file:line#sha256:digest`.
//! 3. [`AgendaEngine`] reads the index: agenda days, TODO lists, tags matches,
//! org-ql queries and saved views, and exports them as Emacs prints them.
//! 4. [`WriteEngine`] changes files: edits, captures, refiles and undo, each
//! journaled so it can be reverted.
//!
//! # Conventions
//!
//! These hold in every module:
//!
//! - Org files are user data. A write replaces only the bytes it changes and
//! leaves every other byte exactly as it was, or it is refused and nothing is
//! written. A file's own syntax (`#+TODO`, `#+PRIORITIES`, `#+STARTUP`) wins
//! over configuration.
//! - An error is an [`AgdError`] whose `code` is one of [`ErrorCode`], the one
//! list of agd.error/v1 codes, named by one rule (`invalid_<thing>`,
//! `unknown_<thing>`, `<thing>_not_found`, `<thing>_conflict`,
//! `<thing>_failed`; see [`ErrorCode`]); its message says what to do.
//! - Every serialized record names its schema, `agd.<name>/v1`, with a
//! `*_SCHEMA` constant such as [`ITEM_SCHEMA`] or [`MUTATION_SCHEMA`].
//! - Config keys are snake_case. Values are lowercase, kebab-case when they
//! name an Org option (`ids = "add-on-write"`, `sorting_strategy =
//! ["time-up"]`, `kind = "tags-todo"`).
//! - Output is deterministic: lists keep a defined order, and dates are
//! computed from the `today` or clock the caller passes.
//! - Nothing touches the network.
//!
//! # Example
//!
//! ```
//! use agd_core::{
//! AgendaEngine, Change, EditIntent, WorkspaceIndex, WriteContext, WriteEngine, load_settings,
//! };
//! use chrono::NaiveDate;
//!
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let dir = tempfile::tempdir()?;
//! let tasks = dir.path().join("tasks.org");
//! std::fs::write(&tasks, "* TODO Renew passport\n")?;
//! let config = dir.path().join("config.toml");
//! std::fs::write(&config, "deadline_warning_days = 7\n")?;
//!
//! // `load_settings(None, None)` reads the user's own config.toml instead.
//! let settings = load_settings(Some(&config), Some(dir.path()))?;
//! let index = WorkspaceIndex::load(settings.clone())?;
//! let today = NaiveDate::from_ymd_opt(2026, 10, 16).unwrap();
//! let todo = AgendaEngine::new(&index).todo(None, today)?;
//! assert_eq!(todo[0].title, "Renew passport");
//!
//! let engine = WriteEngine::new(settings, dir.path().join("journal.jsonl"))?;
//! let context = WriteContext {
//! actor: "example".to_string(),
//! project: "docs".to_string(),
//! now: None,
//! };
//! let done = EditIntent {
//! state: Change::Set("DONE".to_string()),
//! ..EditIntent::default()
//! };
//! let mutation = engine.edit(&todo[0].item_ref, &done, false, &context)?;
//! assert!(mutation.diff.contains("+* DONE Renew passport"));
//!
//! engine.undo(&mutation.journal_id, &context)?;
//! assert_eq!(std::fs::read_to_string(&tasks)?, "* TODO Renew passport\n");
//! # Ok(())
//! # }
//! ```
// Every module is private; each public item has one path, the re-export
// from this root below.
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;