acp/config/
mod.rs

1//! @acp:module "Configuration"
2//! @acp:summary "Project configuration loading and defaults (schema-compliant)"
3//! @acp:domain cli
4//! @acp:layer config
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10use crate::bridge::config as bridge_config;
11
12fn default_config_schema() -> String {
13    "https://acp-protocol.dev/schemas/v1/config.schema.json".to_string()
14}
15
16fn default_version() -> String {
17    "1.0.0".to_string()
18}
19
20/// @acp:summary "Main ACP configuration structure (schema-compliant)"
21/// @acp:lock normal
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Config {
24    /// JSON Schema URL for validation
25    #[serde(rename = "$schema", default = "default_config_schema")]
26    pub schema: String,
27
28    /// ACP specification version
29    #[serde(default = "default_version")]
30    pub version: String,
31
32    /// File patterns to include (glob syntax)
33    #[serde(default = "default_include")]
34    pub include: Vec<String>,
35
36    /// File patterns to exclude (glob syntax)
37    #[serde(default = "default_exclude")]
38    pub exclude: Vec<String>,
39
40    /// Error handling configuration
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub error_handling: Option<ErrorHandling>,
43
44    /// Constraint configuration
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub constraints: Option<ConstraintConfig>,
47
48    /// Domain patterns for automatic classification
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub domains: Option<HashMap<String, DomainPatternConfig>>,
51
52    /// Call graph generation configuration
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub call_graph: Option<CallGraphConfig>,
55
56    /// Implementation limits
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub limits: Option<LimitsConfig>,
59
60    // Internal CLI settings (not in schema but allowed as additional properties)
61    /// Project root directory (internal)
62    #[serde(default = "default_root", skip_serializing_if = "is_default_root")]
63    pub root: PathBuf,
64
65    /// Output paths configuration (internal)
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub output: Option<OutputConfig>,
68
69    /// RFC-0006: Documentation bridging configuration
70    #[serde(default)]
71    pub bridge: bridge_config::BridgeConfig,
72
73    /// RFC-0003: Annotation generation configuration
74    #[serde(default)]
75    pub annotate: AnnotateConfig,
76
77    /// RFC-0002: Documentation references and style guides
78    #[serde(default)]
79    pub documentation: DocumentationConfig,
80}
81
82fn is_default_root(p: &std::path::Path) -> bool {
83    p == std::path::Path::new(".")
84}
85
86impl Default for Config {
87    fn default() -> Self {
88        Self {
89            schema: default_config_schema(),
90            version: default_version(),
91            include: default_include(),
92            exclude: default_exclude(),
93            error_handling: None,
94            constraints: None,
95            domains: None,
96            call_graph: None,
97            limits: None,
98            root: default_root(),
99            output: None,
100            bridge: bridge_config::BridgeConfig::default(),
101            annotate: AnnotateConfig::default(),
102            documentation: DocumentationConfig::default(),
103        }
104    }
105}
106
107impl Config {
108    /// @acp:summary "Load config from .acp.config.json file"
109    pub fn load<P: AsRef<std::path::Path>>(path: P) -> crate::Result<Self> {
110        let content = std::fs::read_to_string(path)?;
111        Ok(serde_json::from_str(&content)?)
112    }
113
114    /// @acp:summary "Save config to a file"
115    pub fn save<P: AsRef<std::path::Path>>(&self, path: P) -> crate::Result<()> {
116        let content = serde_json::to_string_pretty(self)?;
117        std::fs::write(path, content)?;
118        Ok(())
119    }
120
121    /// @acp:summary "Load from default location or create default config"
122    pub fn load_or_default() -> Self {
123        Self::load(".acp.config.json").unwrap_or_default()
124    }
125
126    /// Get cache output path
127    pub fn cache_path(&self) -> PathBuf {
128        self.output
129            .as_ref()
130            .map(|o| o.cache.clone())
131            .unwrap_or_else(default_cache_path)
132    }
133
134    /// Get vars output path
135    pub fn vars_path(&self) -> PathBuf {
136        self.output
137            .as_ref()
138            .map(|o| o.vars.clone())
139            .unwrap_or_else(default_vars_path)
140    }
141}
142
143fn default_root() -> PathBuf {
144    PathBuf::from(".")
145}
146
147fn default_include() -> Vec<String> {
148    vec![
149        "**/*.ts".to_string(),
150        "**/*.tsx".to_string(),
151        "**/*.js".to_string(),
152        "**/*.jsx".to_string(),
153        "**/*.rs".to_string(),
154        "**/*.py".to_string(),
155        "**/*.go".to_string(),
156        "**/*.java".to_string(),
157    ]
158}
159
160fn default_exclude() -> Vec<String> {
161    vec![
162        // Package managers
163        "**/node_modules/**".to_string(),
164        "**/vendor/**".to_string(),
165        // Build outputs
166        "**/dist/**".to_string(),
167        "**/build/**".to_string(),
168        "**/target/**".to_string(),
169        "**/out/**".to_string(),
170        // Framework-specific
171        "**/.next/**".to_string(),       // Next.js
172        "**/.nuxt/**".to_string(),       // Nuxt.js
173        "**/.output/**".to_string(),     // Nitro/Nuxt 3
174        "**/.svelte-kit/**".to_string(), // SvelteKit
175        "**/.vite/**".to_string(),       // Vite
176        "**/.turbo/**".to_string(),      // Turborepo
177        // Cache/temp
178        "**/.cache/**".to_string(),
179        "**/coverage/**".to_string(),
180        "**/__pycache__/**".to_string(),
181        "**/.pytest_cache/**".to_string(),
182        // VCS
183        "**/.git/**".to_string(),
184        // IDE
185        "**/.idea/**".to_string(),
186        "**/.vscode/**".to_string(),
187    ]
188}
189
190/// @acp:summary "Error handling configuration (schema-compliant)"
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct ErrorHandling {
193    /// Error handling strictness mode
194    #[serde(default = "default_strictness")]
195    pub strictness: Strictness,
196
197    /// Maximum number of errors before aborting (permissive mode only)
198    #[serde(default = "default_max_errors")]
199    pub max_errors: usize,
200
201    /// Whether to automatically fix common errors
202    #[serde(default)]
203    pub auto_correct: bool,
204}
205
206fn default_strictness() -> Strictness {
207    Strictness::Permissive
208}
209
210fn default_max_errors() -> usize {
211    100
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
215#[serde(rename_all = "lowercase")]
216pub enum Strictness {
217    Permissive,
218    Strict,
219}
220
221/// @acp:summary "Constraint configuration (schema-compliant)"
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct ConstraintConfig {
224    /// Default constraint values
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub defaults: Option<ConstraintDefaults>,
227
228    /// Enable tracking of constraint violations
229    #[serde(default)]
230    pub track_violations: bool,
231
232    /// Violation log file path
233    #[serde(default = "default_audit_file")]
234    pub audit_file: String,
235}
236
237fn default_audit_file() -> String {
238    ".acp.violations.log".to_string()
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct ConstraintDefaults {
243    /// Default lock level
244    #[serde(default = "default_lock_level")]
245    pub lock: LockLevel,
246
247    /// Default style guide
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub style: Option<String>,
250
251    /// Default AI behavior
252    #[serde(default)]
253    pub behavior: Behavior,
254}
255
256fn default_lock_level() -> LockLevel {
257    LockLevel::Normal
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
261#[serde(rename_all = "kebab-case")]
262#[derive(Default)]
263pub enum LockLevel {
264    Frozen,
265    Restricted,
266    ApprovalRequired,
267    TestsRequired,
268    DocsRequired,
269    ReviewRequired,
270    #[default]
271    Normal,
272    Experimental,
273}
274
275#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
276#[serde(rename_all = "lowercase")]
277pub enum Behavior {
278    Conservative,
279    #[default]
280    Balanced,
281    Aggressive,
282}
283
284/// @acp:summary "Domain pattern configuration (schema-compliant)"
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct DomainPatternConfig {
287    /// Glob patterns for this domain
288    pub patterns: Vec<String>,
289}
290
291/// @acp:summary "Call graph generation configuration (schema-compliant)"
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct CallGraphConfig {
294    /// Include standard library calls
295    #[serde(default)]
296    pub include_stdlib: bool,
297
298    /// Maximum call depth (null = unlimited)
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub max_depth: Option<usize>,
301
302    /// Patterns to exclude from graph
303    #[serde(default)]
304    pub exclude_patterns: Vec<String>,
305}
306
307/// @acp:summary "Implementation limits (schema-compliant)"
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct LimitsConfig {
310    /// Maximum source file size in MB
311    #[serde(default = "default_max_file_size")]
312    pub max_file_size_mb: usize,
313
314    /// Maximum files in project
315    #[serde(default = "default_max_files")]
316    pub max_files: usize,
317
318    /// Maximum annotations per file
319    #[serde(default = "default_max_annotations")]
320    pub max_annotations_per_file: usize,
321
322    /// Maximum cache file size in MB
323    #[serde(default = "default_max_cache_size")]
324    pub max_cache_size_mb: usize,
325}
326
327fn default_max_file_size() -> usize {
328    10
329}
330
331fn default_max_files() -> usize {
332    100_000
333}
334
335fn default_max_annotations() -> usize {
336    1000
337}
338
339fn default_max_cache_size() -> usize {
340    100
341}
342
343/// @acp:summary "Output file path configuration (internal)"
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct OutputConfig {
346    /// Cache file output path
347    #[serde(default = "default_cache_path")]
348    pub cache: PathBuf,
349
350    /// Vars file output path
351    #[serde(default = "default_vars_path")]
352    pub vars: PathBuf,
353
354    /// Whether to also output SQLite database
355    #[serde(default)]
356    pub sqlite: bool,
357}
358
359impl Default for OutputConfig {
360    fn default() -> Self {
361        Self {
362            cache: default_cache_path(),
363            vars: default_vars_path(),
364            sqlite: false,
365        }
366    }
367}
368
369fn default_cache_path() -> PathBuf {
370    PathBuf::from(".acp/acp.cache.json")
371}
372
373fn default_vars_path() -> PathBuf {
374    PathBuf::from(".acp/acp.vars.json")
375}
376
377// =============================================================================
378// RFC-0003: Annotation generation configuration
379// =============================================================================
380
381fn default_true() -> bool {
382    true
383}
384
385fn default_review_threshold() -> f64 {
386    0.8
387}
388
389fn default_min_confidence() -> f64 {
390    0.5
391}
392
393/// @acp:summary "Annotation generation configuration (RFC-0003)"
394#[derive(Debug, Clone, Default, Serialize, Deserialize)]
395pub struct AnnotateConfig {
396    /// Provenance tracking settings
397    #[serde(default)]
398    pub provenance: AnnotateProvenanceConfig,
399
400    /// Default settings for annotation generation
401    #[serde(default)]
402    pub defaults: AnnotateDefaults,
403}
404
405/// @acp:summary "Provenance tracking configuration"
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct AnnotateProvenanceConfig {
408    /// Enable provenance tracking for generated annotations
409    #[serde(default = "default_true")]
410    pub enabled: bool,
411
412    /// Include confidence scores in generated annotations
413    #[serde(default = "default_true", rename = "includeConfidence")]
414    pub include_confidence: bool,
415
416    /// Confidence threshold below which annotations are flagged for review
417    #[serde(default = "default_review_threshold", rename = "reviewThreshold")]
418    pub review_threshold: f64,
419
420    /// Minimum confidence required to emit an annotation
421    #[serde(default = "default_min_confidence", rename = "minConfidence")]
422    pub min_confidence: f64,
423}
424
425impl Default for AnnotateProvenanceConfig {
426    fn default() -> Self {
427        Self {
428            enabled: true,
429            include_confidence: true,
430            review_threshold: 0.8,
431            min_confidence: 0.5,
432        }
433    }
434}
435
436/// @acp:summary "Default annotation generation settings"
437#[derive(Debug, Clone, Default, Serialize, Deserialize)]
438pub struct AnnotateDefaults {
439    /// Mark all generated annotations as needing review
440    #[serde(default, rename = "markNeedsReview")]
441    pub mark_needs_review: bool,
442
443    /// Overwrite existing annotations when generating
444    #[serde(default, rename = "overwriteExisting")]
445    pub overwrite_existing: bool,
446}
447
448// =============================================================================
449// RFC-0002: Documentation references and style guides
450// =============================================================================
451
452/// @acp:summary "Documentation configuration (RFC-0002)"
453#[derive(Debug, Clone, Default, Serialize, Deserialize)]
454pub struct DocumentationConfig {
455    /// Trusted documentation sources for this project
456    #[serde(default, rename = "approvedSources")]
457    pub approved_sources: Vec<ApprovedSource>,
458
459    /// Custom style guide definitions
460    #[serde(default, rename = "styleGuides")]
461    pub style_guides: HashMap<String, StyleGuideDefinition>,
462
463    /// Default documentation settings
464    #[serde(default)]
465    pub defaults: DocumentationDefaults,
466
467    /// Reference validation settings
468    #[serde(default)]
469    pub validation: DocumentationValidation,
470}
471
472/// @acp:summary "Approved documentation source (RFC-0002)"
473#[derive(Debug, Clone, Serialize, Deserialize)]
474pub struct ApprovedSource {
475    /// Unique identifier for this source (used in @acp:ref)
476    pub id: String,
477
478    /// Base URL for documentation
479    pub url: String,
480
481    /// Version of documentation (semver or custom)
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub version: Option<String>,
484
485    /// Human-readable description
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub description: Option<String>,
488
489    /// Named section shortcuts
490    #[serde(default)]
491    pub sections: HashMap<String, String>,
492
493    /// Whether AI tools should attempt to fetch this source
494    #[serde(default = "default_true")]
495    pub fetchable: bool,
496
497    /// When this source was last verified accessible
498    #[serde(skip_serializing_if = "Option::is_none", rename = "lastVerified")]
499    pub last_verified: Option<String>,
500}
501
502/// @acp:summary "Style guide definition (RFC-0002)"
503#[derive(Debug, Clone, Default, Serialize, Deserialize)]
504pub struct StyleGuideDefinition {
505    /// Base style guide to extend
506    #[serde(skip_serializing_if = "Option::is_none")]
507    pub extends: Option<String>,
508
509    /// Approved source ID for documentation
510    #[serde(skip_serializing_if = "Option::is_none")]
511    pub source: Option<String>,
512
513    /// Direct URL to style guide documentation
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub url: Option<String>,
516
517    /// Human-readable description
518    #[serde(skip_serializing_if = "Option::is_none")]
519    pub description: Option<String>,
520
521    /// Languages this guide applies to
522    #[serde(default)]
523    pub languages: Vec<String>,
524
525    /// Style rules (key or key=value format)
526    #[serde(default)]
527    pub rules: Vec<String>,
528
529    /// Glob patterns for auto-applying this guide
530    #[serde(default, rename = "filePatterns")]
531    pub file_patterns: Vec<String>,
532}
533
534/// @acp:summary "Default documentation settings"
535#[derive(Debug, Clone, Default, Serialize, Deserialize)]
536pub struct DocumentationDefaults {
537    /// Default value for @acp:ref-fetch
538    #[serde(default, rename = "fetchRefs")]
539    pub fetch_refs: bool,
540
541    /// Default style guide for all files
542    #[serde(skip_serializing_if = "Option::is_none")]
543    pub style: Option<String>,
544}
545
546/// @acp:summary "Documentation validation settings"
547#[derive(Debug, Clone, Serialize, Deserialize)]
548pub struct DocumentationValidation {
549    /// Only allow refs from approvedSources list
550    #[serde(default, rename = "requireApprovedSources")]
551    pub require_approved_sources: bool,
552
553    /// Warn when unknown style guide is referenced
554    #[serde(default = "default_true", rename = "warnUnknownStyle")]
555    pub warn_unknown_style: bool,
556}
557
558impl Default for DocumentationValidation {
559    fn default() -> Self {
560        Self {
561            require_approved_sources: false,
562            warn_unknown_style: true,
563        }
564    }
565}