Skip to main content

agent_docs/
model.rs

1use std::fmt;
2use std::path::PathBuf;
3
4use clap::ValueEnum;
5use serde::Serialize;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, ValueEnum)]
8#[serde(rename_all = "kebab-case")]
9pub enum Context {
10    Startup,
11    SkillDev,
12    TaskTools,
13    ProjectDev,
14}
15
16impl Context {
17    pub const fn as_str(self) -> &'static str {
18        match self {
19            Self::Startup => "startup",
20            Self::SkillDev => "skill-dev",
21            Self::TaskTools => "task-tools",
22            Self::ProjectDev => "project-dev",
23        }
24    }
25
26    pub const fn supported_values() -> &'static [&'static str] {
27        &["startup", "skill-dev", "task-tools", "project-dev"]
28    }
29
30    pub fn from_config_value(value: &str) -> Option<Self> {
31        match value {
32            "startup" => Some(Self::Startup),
33            "skill-dev" => Some(Self::SkillDev),
34            "task-tools" => Some(Self::TaskTools),
35            "project-dev" => Some(Self::ProjectDev),
36            _ => None,
37        }
38    }
39}
40
41impl fmt::Display for Context {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        f.write_str(self.as_str())
44    }
45}
46
47pub const SUPPORTED_CONTEXTS: [Context; 4] = [
48    Context::Startup,
49    Context::SkillDev,
50    Context::TaskTools,
51    Context::ProjectDev,
52];
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, ValueEnum)]
55#[serde(rename_all = "kebab-case")]
56pub enum Scope {
57    Home,
58    Project,
59}
60
61impl Scope {
62    pub const fn as_str(self) -> &'static str {
63        match self {
64            Self::Home => "home",
65            Self::Project => "project",
66        }
67    }
68
69    pub const fn supported_values() -> &'static [&'static str] {
70        &["home", "project"]
71    }
72
73    pub fn from_config_value(value: &str) -> Option<Self> {
74        match value {
75            "home" => Some(Self::Home),
76            "project" => Some(Self::Project),
77            _ => None,
78        }
79    }
80}
81
82impl fmt::Display for Scope {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        f.write_str(self.as_str())
85    }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, ValueEnum, Default)]
89#[serde(rename_all = "kebab-case")]
90pub enum OutputFormat {
91    #[default]
92    Text,
93    Json,
94}
95
96impl OutputFormat {
97    pub const fn as_str(self) -> &'static str {
98        match self {
99            Self::Text => "text",
100            Self::Json => "json",
101        }
102    }
103}
104
105impl fmt::Display for OutputFormat {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        f.write_str(self.as_str())
108    }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, ValueEnum, Default)]
112#[serde(rename_all = "kebab-case")]
113pub enum FallbackMode {
114    #[default]
115    Auto,
116    LocalOnly,
117}
118
119impl FallbackMode {
120    pub const fn as_str(self) -> &'static str {
121        match self {
122            Self::Auto => "auto",
123            Self::LocalOnly => "local-only",
124        }
125    }
126}
127
128impl fmt::Display for FallbackMode {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        f.write_str(self.as_str())
131    }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, ValueEnum, Default)]
135#[serde(rename_all = "kebab-case")]
136pub enum ResolveFormat {
137    #[default]
138    Text,
139    Json,
140    Checklist,
141}
142
143impl ResolveFormat {
144    pub const fn as_str(self) -> &'static str {
145        match self {
146            Self::Text => "text",
147            Self::Json => "json",
148            Self::Checklist => "checklist",
149        }
150    }
151}
152
153impl fmt::Display for ResolveFormat {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        f.write_str(self.as_str())
156    }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, ValueEnum, Default)]
160#[serde(rename_all = "kebab-case")]
161pub enum BaselineTarget {
162    Home,
163    Project,
164    #[default]
165    All,
166}
167
168impl BaselineTarget {
169    pub const fn as_str(self) -> &'static str {
170        match self {
171            Self::Home => "home",
172            Self::Project => "project",
173            Self::All => "all",
174        }
175    }
176}
177
178impl fmt::Display for BaselineTarget {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        f.write_str(self.as_str())
181    }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
185#[serde(rename_all = "kebab-case")]
186pub enum DocumentStatus {
187    Present,
188    Missing,
189}
190
191impl DocumentStatus {
192    pub const fn as_str(self) -> &'static str {
193        match self {
194            Self::Present => "present",
195            Self::Missing => "missing",
196        }
197    }
198}
199
200impl fmt::Display for DocumentStatus {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        f.write_str(self.as_str())
203    }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
207#[serde(rename_all = "kebab-case")]
208pub enum DocumentSource {
209    Builtin,
210    BuiltinFallback,
211    ExtensionHome,
212    ExtensionProject,
213}
214
215impl DocumentSource {
216    pub const fn as_str(self) -> &'static str {
217        match self {
218            Self::Builtin => "builtin",
219            Self::BuiltinFallback => "builtin-fallback",
220            Self::ExtensionHome => "extension-home",
221            Self::ExtensionProject => "extension-project",
222        }
223    }
224}
225
226impl fmt::Display for DocumentSource {
227    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228        f.write_str(self.as_str())
229    }
230}
231
232#[derive(Debug, Clone, Serialize)]
233pub struct ResolvedDocument {
234    pub context: Context,
235    pub scope: Scope,
236    pub path: PathBuf,
237    pub required: bool,
238    pub status: DocumentStatus,
239    pub source: DocumentSource,
240    pub why: String,
241}
242
243#[derive(Debug, Clone, Serialize)]
244pub struct ResolveSummary {
245    pub required_total: usize,
246    pub present_required: usize,
247    pub missing_required: usize,
248}
249
250impl ResolveSummary {
251    pub fn from_documents(documents: &[ResolvedDocument]) -> Self {
252        let required_total = documents.iter().filter(|doc| doc.required).count();
253        let present_required = documents
254            .iter()
255            .filter(|doc| doc.required && doc.status == DocumentStatus::Present)
256            .count();
257        let missing_required = required_total.saturating_sub(present_required);
258
259        Self {
260            required_total,
261            present_required,
262            missing_required,
263        }
264    }
265}
266
267#[derive(Debug, Clone, Serialize)]
268pub struct ResolveReport {
269    pub context: Context,
270    pub strict: bool,
271    pub agent_home: PathBuf,
272    pub project_path: PathBuf,
273    pub is_linked_worktree: bool,
274    pub git_common_dir: Option<PathBuf>,
275    pub primary_worktree_path: Option<PathBuf>,
276    pub documents: Vec<ResolvedDocument>,
277    pub summary: ResolveSummary,
278}
279
280impl ResolveReport {
281    pub fn has_missing_required(&self) -> bool {
282        self.summary.missing_required > 0
283    }
284}
285
286#[derive(Debug, Clone, Serialize)]
287pub struct BaselineCheckItem {
288    pub scope: Scope,
289    pub context: Context,
290    pub label: String,
291    pub path: PathBuf,
292    pub required: bool,
293    pub status: DocumentStatus,
294    pub source: DocumentSource,
295    pub why: String,
296}
297
298#[derive(Debug, Clone, Serialize)]
299pub struct BaselineCheckReport {
300    pub target: BaselineTarget,
301    pub strict: bool,
302    pub agent_home: PathBuf,
303    pub project_path: PathBuf,
304    pub items: Vec<BaselineCheckItem>,
305    pub missing_required: usize,
306    pub missing_optional: usize,
307    pub suggested_actions: Vec<String>,
308}
309
310impl BaselineCheckReport {
311    pub fn from_items(
312        target: BaselineTarget,
313        strict: bool,
314        agent_home: PathBuf,
315        project_path: PathBuf,
316        items: Vec<BaselineCheckItem>,
317        suggested_actions: Vec<String>,
318    ) -> Self {
319        let missing_required = items
320            .iter()
321            .filter(|item| item.required && item.status == DocumentStatus::Missing)
322            .count();
323        let missing_optional = items
324            .iter()
325            .filter(|item| !item.required && item.status == DocumentStatus::Missing)
326            .count();
327
328        Self {
329            target,
330            strict,
331            agent_home,
332            project_path,
333            items,
334            missing_required,
335            missing_optional,
336            suggested_actions,
337        }
338    }
339
340    pub fn has_missing_required(&self) -> bool {
341        self.missing_required > 0
342    }
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, ValueEnum)]
346#[serde(rename_all = "kebab-case")]
347pub enum DocumentWhen {
348    Always,
349}
350
351impl DocumentWhen {
352    pub const fn as_str(self) -> &'static str {
353        match self {
354            Self::Always => "always",
355        }
356    }
357
358    pub const fn supported_values() -> &'static [&'static str] {
359        &["always"]
360    }
361
362    pub fn from_config_value(value: &str) -> Option<Self> {
363        match value {
364            "always" => Some(Self::Always),
365            _ => None,
366        }
367    }
368}
369
370impl fmt::Display for DocumentWhen {
371    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
372        f.write_str(self.as_str())
373    }
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
377pub struct ConfigDocumentEntry {
378    pub context: Context,
379    pub scope: Scope,
380    pub path: PathBuf,
381    pub required: bool,
382    pub when: DocumentWhen,
383    pub notes: Option<String>,
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
387pub struct ConfigScopeFile {
388    pub source_scope: Scope,
389    pub root: PathBuf,
390    pub file_path: PathBuf,
391    pub documents: Vec<ConfigDocumentEntry>,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
395pub struct LoadedConfigs {
396    pub home: Option<ConfigScopeFile>,
397    pub project: Option<ConfigScopeFile>,
398}
399
400impl LoadedConfigs {
401    pub fn in_load_order(&self) -> Vec<&ConfigScopeFile> {
402        let mut ordered = Vec::new();
403        if let Some(home) = self.home.as_ref() {
404            ordered.push(home);
405        }
406        if let Some(project) = self.project.as_ref() {
407            ordered.push(project);
408        }
409        ordered
410    }
411}
412
413#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
414#[serde(rename_all = "kebab-case")]
415pub enum ConfigErrorKind {
416    Io,
417    Parse,
418    Validation,
419}
420
421impl ConfigErrorKind {
422    pub const fn as_str(self) -> &'static str {
423        match self {
424            Self::Io => "io",
425            Self::Parse => "parse",
426            Self::Validation => "validation",
427        }
428    }
429}
430
431impl fmt::Display for ConfigErrorKind {
432    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433        f.write_str(self.as_str())
434    }
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
438pub struct ConfigErrorLocation {
439    pub line: usize,
440    pub column: usize,
441}
442
443#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
444pub struct ConfigLoadError {
445    pub kind: ConfigErrorKind,
446    pub file_path: PathBuf,
447    pub document_index: Option<usize>,
448    pub field: Option<String>,
449    pub location: Option<ConfigErrorLocation>,
450    pub message: String,
451}
452
453impl ConfigLoadError {
454    pub fn io(file_path: PathBuf, message: impl Into<String>) -> Self {
455        Self {
456            kind: ConfigErrorKind::Io,
457            file_path,
458            document_index: None,
459            field: None,
460            location: None,
461            message: message.into(),
462        }
463    }
464
465    pub fn parse(
466        file_path: PathBuf,
467        message: impl Into<String>,
468        location: Option<ConfigErrorLocation>,
469    ) -> Self {
470        Self {
471            kind: ConfigErrorKind::Parse,
472            file_path,
473            document_index: None,
474            field: None,
475            location,
476            message: message.into(),
477        }
478    }
479
480    pub fn validation(
481        file_path: PathBuf,
482        document_index: usize,
483        field: impl Into<String>,
484        message: impl Into<String>,
485    ) -> Self {
486        Self {
487            kind: ConfigErrorKind::Validation,
488            file_path,
489            document_index: Some(document_index),
490            field: Some(field.into()),
491            location: None,
492            message: message.into(),
493        }
494    }
495
496    pub fn validation_root(
497        file_path: PathBuf,
498        field: impl Into<String>,
499        message: impl Into<String>,
500    ) -> Self {
501        Self {
502            kind: ConfigErrorKind::Validation,
503            file_path,
504            document_index: None,
505            field: Some(field.into()),
506            location: None,
507            message: message.into(),
508        }
509    }
510}
511
512impl fmt::Display for ConfigLoadError {
513    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
514        match (self.document_index, self.field.as_deref(), self.location) {
515            (Some(index), Some(field), Some(location)) => write!(
516                f,
517                "{}:{}:{} [{}] document[{index}].{field}: {}",
518                self.file_path.display(),
519                location.line,
520                location.column,
521                self.kind,
522                self.message
523            ),
524            (Some(index), Some(field), None) => write!(
525                f,
526                "{} [{}] document[{index}].{field}: {}",
527                self.file_path.display(),
528                self.kind,
529                self.message
530            ),
531            (None, None, Some(location)) => write!(
532                f,
533                "{}:{}:{} [{}]: {}",
534                self.file_path.display(),
535                location.line,
536                location.column,
537                self.kind,
538                self.message
539            ),
540            (None, Some(field), Some(location)) => write!(
541                f,
542                "{}:{}:{} [{}] {field}: {}",
543                self.file_path.display(),
544                location.line,
545                location.column,
546                self.kind,
547                self.message
548            ),
549            (None, Some(field), None) => write!(
550                f,
551                "{} [{}] {field}: {}",
552                self.file_path.display(),
553                self.kind,
554                self.message
555            ),
556            _ => write!(
557                f,
558                "{} [{}]: {}",
559                self.file_path.display(),
560                self.kind,
561                self.message
562            ),
563        }
564    }
565}
566
567impl std::error::Error for ConfigLoadError {}
568
569#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
570#[serde(rename_all = "kebab-case")]
571pub enum AddDocumentAction {
572    Inserted,
573    Updated,
574}
575
576impl AddDocumentAction {
577    pub const fn as_str(self) -> &'static str {
578        match self {
579            Self::Inserted => "inserted",
580            Self::Updated => "updated",
581        }
582    }
583}
584
585impl fmt::Display for AddDocumentAction {
586    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
587        f.write_str(self.as_str())
588    }
589}
590
591#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
592pub struct AddDocumentReport {
593    pub target: Scope,
594    pub target_root: PathBuf,
595    pub config_path: PathBuf,
596    pub created_config: bool,
597    pub action: AddDocumentAction,
598    pub entry: ConfigDocumentEntry,
599    pub document_count: usize,
600}
601
602#[derive(Debug, Clone, Serialize)]
603pub struct StubReport {
604    pub command: String,
605    pub implemented: bool,
606    pub message: String,
607}