Skip to main content

agent_doc_core/
project_config.rs

1//! # Module: project_config
2//!
3//! Project-level configuration loaded from `.agent-doc/config.toml`.
4//! Shared between binary and library for consistent project config handling.
5//!
6//! ## Spec
7//! - Defines `ProjectConfig`: per-project settings (tmux_session, components).
8//! - Defines `ComponentConfig`: per-component patch configuration (mode, timestamps, hooks).
9//! - `load_project()` reads and parses the project config file. On absence, I/O error, or parse
10//!   error, returns `ProjectConfig::default()` and emits a warning to stderr (never panics).
11//! - `project_tmux_session()` is a convenience wrapper returning the configured tmux session name.
12//! - `save_project()` serialises `ProjectConfig` to TOML and writes it to
13//!   `.agent-doc/config.toml`, creating the directory if needed.
14//!
15//! ## Agentic Contracts
16//! - Never panics on missing config: `load_project()` returns defaults when the file is absent.
17//! - Project config errors are non-fatal: errors are surfaced as stderr warnings, not propagated.
18//! - Atomic-safe directory creation: `save_project()` calls `create_dir_all` before writing.
19
20use anyhow::Result;
21use serde::{Deserialize, Serialize};
22use std::collections::BTreeMap;
23
24#[derive(Debug, Clone, Serialize, Deserialize, Default)]
25pub struct GuardConfig {
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub pending_capture: Option<crate::frontmatter::PendingCaptureGuardMode>,
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub pending_done: Option<crate::frontmatter::PendingCaptureGuardMode>,
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub review_done: Option<crate::frontmatter::PendingCaptureGuardMode>,
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub auto_done: Option<bool>,
34}
35
36/// Workspace-level lint configuration (`[lint]` section in
37/// `.agent-doc/config.toml`).
38///
39/// Currently exposes a single key, `dialect`, controlling the
40/// `tagpath lint --dialect agent-doc` finalize gate. See
41/// `crate::frontmatter::LintDialectMode` for the resolved semantics.
42#[derive(Debug, Clone, Serialize, Deserialize, Default)]
43pub struct LintConfig {
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub dialect: Option<crate::frontmatter::LintDialectMode>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, Default)]
49pub struct SshProfileConfig {
50    /// Resolved SSH targets for a named profile.
51    #[serde(default)]
52    pub targets: Vec<String>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, Default)]
56pub struct SshDocConfig {
57    /// Optional profile name to resolve for this document path.
58    #[serde(default)]
59    pub profile: Option<String>,
60    /// Optional direct targets for this document path.
61    #[serde(default)]
62    pub targets: Vec<String>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, Default)]
66pub struct SshConfig {
67    /// Named SSH profiles that expand to concrete targets.
68    #[serde(default)]
69    pub profiles: BTreeMap<String, SshProfileConfig>,
70    /// Relative document paths that require SSH metadata resolution.
71    #[serde(default)]
72    pub docs: BTreeMap<String, SshDocConfig>,
73}
74
75/// Component patch configuration (mode, timestamps, max entries, hooks).
76#[derive(Debug, Clone, Serialize, Deserialize, Default)]
77pub struct ComponentConfig {
78    /// Patch mode: "replace" (default), "append", "prepend".
79    /// `patch` is the primary key; `mode` is a backward-compatible alias.
80    #[serde(default = "default_patch_mode", alias = "mode")]
81    pub patch: String,
82    /// Merge strategy: "append-friendly" (default) or "strict".
83    /// "append-friendly" auto-resolves conflicts where both sides only appended.
84    /// "strict" preserves all conflict markers for manual resolution.
85    /// Currently parsed for config validation; merge runs at document level.
86    #[serde(default = "default_merge_strategy")]
87    #[allow(dead_code)]
88    pub merge_strategy: String,
89    /// Auto-prefix entries with ISO timestamp (for append/prepend modes)
90    #[serde(default)]
91    pub timestamp: bool,
92    /// Auto-trim old entries in append/prepend modes (0 = unlimited)
93    #[serde(default)]
94    pub max_entries: usize,
95    /// Trim component content to the last N lines after patching (0 = unlimited).
96    /// Currently used by template.rs post-patch processing.
97    #[serde(default)]
98    #[allow(dead_code)]
99    pub max_lines: usize,
100    /// Shell command to run before patching (stdin: content, stdout: transformed)
101    #[serde(default)]
102    pub pre_patch: Option<String>,
103    /// Shell command to run after patching (fire-and-forget)
104    #[serde(default)]
105    pub post_patch: Option<String>,
106}
107
108fn default_patch_mode() -> String {
109    "replace".to_string()
110}
111
112fn default_merge_strategy() -> String {
113    "append-friendly".to_string()
114}
115
116/// Project-level configuration, read from `.agent-doc/config.toml` relative to CWD.
117#[derive(Debug, Default, Serialize, Deserialize)]
118pub struct ProjectConfig {
119    /// Target tmux session name for this project.
120    #[serde(default)]
121    pub tmux_session: Option<String>,
122    /// Explicit opt-in for automatic compaction/reload policies.
123    /// Session-accretion heuristics never compact by themselves; omit to disable.
124    #[serde(default, alias = "auto_compact")]
125    pub agent_doc_auto_compact: Option<usize>,
126    /// Guard behavior overrides (for example pending-capture enforcement).
127    #[serde(default)]
128    pub guards: GuardConfig,
129    /// Lint behavior overrides for the agent-doc finalize lint gate.
130    #[serde(default)]
131    pub lint: LintConfig,
132    /// Project-local SSH requirement mappings for known ops documents.
133    #[serde(default)]
134    pub ssh: SshConfig,
135    /// Component-specific configuration (patch modes, timestamps, max_entries, hooks).
136    #[serde(default)]
137    pub components: BTreeMap<String, ComponentConfig>,
138}
139
140/// Parse a TOML string into a [`ProjectConfig`]. Pure — no fs I/O.
141///
142/// File-based loading (with legacy `components.toml` migration) lives in
143/// `crate::project_config_io` in the main `agent-doc` crate.
144pub fn parse_project_toml(content: &str) -> Result<ProjectConfig> {
145    toml::from_str(content).map_err(anyhow::Error::from)
146}
147
148/// Parse a legacy `components.toml` body (flat `[name]` tables of
149/// [`ComponentConfig`] fields) into a map. Used by the file-based migrator
150/// in `crate::project_config_io`.
151pub fn parse_legacy_components_toml(
152    content: &str,
153) -> Result<BTreeMap<String, ComponentConfig>> {
154    toml::from_str(content).map_err(anyhow::Error::from)
155}