Skip to main content

agd_core/
lib.rs

1//! agd-core reads and safely changes org-mode files. It is the library behind
2//! the `agd` command line, terminal UI and stdio MCP server, and it works on
3//! local files only.
4//!
5//! Every caller follows the same path:
6//!
7//! 1. [`load_settings`] reads `config.toml` into [`Settings`]: the roots agd
8//!    may touch, the agenda files, TODO keywords and the other Org options.
9//! 2. [`WorkspaceIndex::load`] parses every agenda file under the roots into an
10//!    [`IndexedDocument`] and gives each heading an [`Item`] with a stable
11//!    ref: its `:ID:` when unique, else `file:line#sha256:digest`.
12//! 3. [`AgendaEngine`] reads the index: agenda days, TODO lists, tags matches,
13//!    org-ql queries and saved views, and exports them as Emacs prints them.
14//! 4. [`WriteEngine`] changes files: edits, captures, refiles and undo, each
15//!    journaled so it can be reverted.
16//!
17//! # Conventions
18//!
19//! These hold in every module:
20//!
21//! - Org files are user data. A write replaces only the bytes it changes and
22//!   leaves every other byte exactly as it was, or it is refused and nothing is
23//!   written. A file's own syntax (`#+TODO`, `#+PRIORITIES`, `#+STARTUP`) wins
24//!   over configuration.
25//! - An error is an [`AgdError`] whose `code` is one of [`ErrorCode`], the one
26//!   list of agd.error/v1 codes, named by one rule (`invalid_<thing>`,
27//!   `unknown_<thing>`, `<thing>_not_found`, `<thing>_conflict`,
28//!   `<thing>_failed`; see [`ErrorCode`]); its message says what to do.
29//! - Every serialized record names its schema, `agd.<name>/v1`, with a
30//!   `*_SCHEMA` constant such as [`ITEM_SCHEMA`] or [`MUTATION_SCHEMA`].
31//! - Config keys are snake_case. Values are lowercase, kebab-case when they
32//!   name an Org option (`ids = "add-on-write"`, `sorting_strategy =
33//!   ["time-up"]`, `kind = "tags-todo"`).
34//! - Output is deterministic: lists keep a defined order, and dates are
35//!   computed from the `today` or clock the caller passes.
36//! - Nothing touches the network.
37//!
38//! # Example
39//!
40//! ```
41//! use agd_core::{
42//!     AgendaEngine, Change, EditIntent, WorkspaceIndex, WriteContext, WriteEngine, load_settings,
43//! };
44//! use chrono::NaiveDate;
45//!
46//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
47//! let dir = tempfile::tempdir()?;
48//! let tasks = dir.path().join("tasks.org");
49//! std::fs::write(&tasks, "* TODO Renew passport\n")?;
50//! let config = dir.path().join("config.toml");
51//! std::fs::write(&config, "deadline_warning_days = 7\n")?;
52//!
53//! // `load_settings(None, None)` reads the user's own config.toml instead.
54//! let settings = load_settings(Some(&config), Some(dir.path()))?;
55//! let index = WorkspaceIndex::load(settings.clone())?;
56//! let today = NaiveDate::from_ymd_opt(2026, 10, 16).unwrap();
57//! let todo = AgendaEngine::new(&index).todo(None, today)?;
58//! assert_eq!(todo[0].title, "Renew passport");
59//!
60//! let engine = WriteEngine::new(settings, dir.path().join("journal.jsonl"))?;
61//! let context = WriteContext {
62//!     actor: "example".to_string(),
63//!     project: "docs".to_string(),
64//!     now: None,
65//! };
66//! let done = EditIntent {
67//!     state: Change::Set("DONE".to_string()),
68//!     ..EditIntent::default()
69//! };
70//! let mutation = engine.edit(&todo[0].item_ref, &done, false, &context)?;
71//! assert!(mutation.diff.contains("+* DONE Renew passport"));
72//!
73//! engine.undo(&mutation.journal_id, &context)?;
74//! assert_eq!(std::fs::read_to_string(&tasks)?, "* TODO Renew passport\n");
75//! # Ok(())
76//! # }
77//! ```
78
79// Every module is private; each public item has one path, the re-export
80// from this root below.
81mod agenda;
82mod capture_line;
83mod capture_template;
84mod check;
85mod config;
86mod date;
87mod document;
88mod import;
89mod model;
90mod path;
91mod write;
92
93pub use agenda::{
94    AgendaEngine, AgendaQuery, AgendaSpan, AgendaSummary, ViewSection, ViewSectionGroup,
95    agenda_csv, agenda_row, agenda_text, agenda_title, item_row, items_csv, keyword_color,
96    keyword_colors, next_repeat, overdue_entries,
97};
98pub use capture_line::{CaptureDraft, parse_capture_line};
99pub use capture_template::{CapturePrompt, capture_prompts, render_capture_template};
100pub use check::{
101    CheckFileReport, CheckProblem, CheckReport, CheckSummary, check_settings, journal_problems,
102};
103pub use config::{
104    AgentProfile, COLOR_NAMES, CaptureTemplate, IdPolicy, LogMode, Notation, SORTING_KEYS,
105    Settings, VIEW_BLOCK_KINDS, VIEW_GROUP_BY, ViewBlock, ViewGroup, ViewSettings, WeekStart,
106    default_config_path, load_settings, parse_agenda_day, parse_config_file, render_config,
107};
108pub use date::{format_active_timestamp, parse_date_change, parse_date_input, read_date};
109pub use document::{
110    ByteSpan, ChecklistItem, HeadlineSpan, LogKind, LogSettings, OrgDocument as IndexedDocument,
111    PriorityRange, PropertySpan, TimestampSpan, TodoKeyword, WorkspaceIndex, checklist,
112    parse_timestamp,
113};
114pub use import::{
115    ConfigEditor, ConfigSources, ImportDiagnostic, ImportResult, ImportStatus, ImportedCapture,
116    ImportedCommand, ResolvedEditorConfig, apply_import, find_config_sources, import_root,
117};
118pub use model::{
119    AGENDA_ENTRY_SCHEMA, AgdError, AgendaEntry, CHECK_SCHEMA, DESCRIBE_SCHEMA, ERROR_SCHEMA,
120    ErrorCode, ErrorResponse, ITEM_SCHEMA, Item, JOURNAL_SCHEMA, MUTATION_SCHEMA, Mutation,
121    Occurrence, OccurrenceType, Planning, PlanningTimestamp, Progress, REFILE_SCHEMA, Repeat,
122    STATUS_SCHEMA, UNDO_SCHEMA, VIEW_SECTION_SCHEMA, VIEWS_SCHEMA,
123};
124pub use path::{OrgFiles, RootSet, SkipReason};
125pub use write::{
126    BatchMutation, Change, EditIntent, ItemEdit, JournalOperation, JournalRecord, JournalStatus,
127    Patch, RefileResult, StoredPatch, UndoResult, WriteContext, WriteEngine, agent_actor,
128    agent_name, apply_patches,
129};