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}