acp/primer/
types.rs

1//! @acp:module "Primer Types"
2//! @acp:summary "Type definitions matching primer.schema.json"
3//! @acp:domain cli
4//! @acp:layer types
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Main primer configuration structure
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct PrimerConfig {
13    pub version: String,
14
15    #[serde(default)]
16    pub extends: Option<String>,
17
18    #[serde(default)]
19    pub metadata: Option<PrimerMetadata>,
20
21    #[serde(default)]
22    pub capabilities: HashMap<String, Capability>,
23
24    #[serde(default)]
25    pub categories: Vec<Category>,
26
27    pub sections: Vec<Section>,
28
29    #[serde(default)]
30    pub additional_sections: Vec<Section>,
31
32    #[serde(default)]
33    pub disabled_sections: Vec<String>,
34
35    #[serde(default)]
36    pub section_overrides: HashMap<String, SectionOverride>,
37
38    #[serde(default)]
39    pub selection_strategy: SelectionStrategy,
40
41    #[serde(default)]
42    pub output_formats: Option<OutputFormatsConfig>,
43
44    #[serde(default)]
45    pub knowledge_store: Option<KnowledgeStoreConfig>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase")]
50pub struct PrimerMetadata {
51    pub name: Option<String>,
52    pub description: Option<String>,
53    pub author: Option<String>,
54    pub license: Option<String>,
55    pub min_acp_version: Option<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct Capability {
60    pub id: String,
61    pub name: String,
62    #[serde(default)]
63    pub description: Option<String>,
64    #[serde(default)]
65    pub tools: Vec<String>,
66    #[serde(default)]
67    pub detect_command: Option<String>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71#[serde(rename_all = "camelCase")]
72pub struct Category {
73    pub id: String,
74    pub name: String,
75    #[serde(default)]
76    pub description: Option<String>,
77    #[serde(default)]
78    pub priority: Option<u32>,
79    #[serde(default)]
80    pub color: Option<String>,
81    #[serde(default)]
82    pub icon: Option<String>,
83    #[serde(default)]
84    pub budget_constraints: Option<BudgetConstraints>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct BudgetConstraints {
90    #[serde(default)]
91    pub minimum: Option<u32>,
92    #[serde(default)]
93    pub maximum: Option<u32>,
94    #[serde(default)]
95    pub minimum_percent: Option<f32>,
96    #[serde(default)]
97    pub maximum_percent: Option<f32>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct Section {
103    pub id: String,
104
105    pub category: String,
106
107    pub tokens: TokenCount,
108
109    pub value: SectionValue,
110
111    #[serde(default)]
112    pub name: Option<String>,
113
114    #[serde(default)]
115    pub description: Option<String>,
116
117    #[serde(default)]
118    pub priority: Option<u32>,
119
120    #[serde(default)]
121    pub required: bool,
122
123    #[serde(default)]
124    pub required_if: Option<String>,
125
126    #[serde(default)]
127    pub capabilities: Vec<String>,
128
129    #[serde(default)]
130    pub capabilities_all: Vec<String>,
131
132    #[serde(default)]
133    pub depends_on: Vec<String>,
134
135    #[serde(default)]
136    pub conflicts_with: Vec<String>,
137
138    #[serde(default)]
139    pub replaces: Vec<String>,
140
141    #[serde(default)]
142    pub tags: Vec<String>,
143
144    pub formats: SectionFormats,
145
146    #[serde(default)]
147    pub data: Option<SectionData>,
148}
149
150/// Token count - either fixed or dynamic
151#[derive(Debug, Clone, Serialize, Deserialize)]
152#[serde(untagged)]
153pub enum TokenCount {
154    Fixed(u32),
155    Dynamic(String), // "dynamic"
156}
157
158impl TokenCount {
159    pub fn estimate(&self) -> u32 {
160        match self {
161            TokenCount::Fixed(n) => *n,
162            TokenCount::Dynamic(_) => 50, // Default estimate for dynamic sections
163        }
164    }
165}
166
167/// Multi-dimensional value scoring
168#[derive(Debug, Clone, Serialize, Deserialize, Default)]
169pub struct SectionValue {
170    #[serde(default)]
171    pub safety: u8,
172
173    #[serde(default)]
174    pub efficiency: u8,
175
176    #[serde(default)]
177    pub accuracy: u8,
178
179    #[serde(default = "default_base")]
180    pub base: u8,
181
182    #[serde(default)]
183    pub modifiers: Vec<ValueModifier>,
184}
185
186fn default_base() -> u8 {
187    50
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct ValueModifier {
192    pub condition: String,
193
194    #[serde(default)]
195    pub add: Option<i32>,
196
197    #[serde(default)]
198    pub multiply: Option<f64>,
199
200    #[serde(default)]
201    pub set: Option<i32>,
202
203    #[serde(default)]
204    pub dimension: Option<String>,
205
206    #[serde(default)]
207    pub reason: Option<String>,
208}
209
210/// Section override for project customization
211#[derive(Debug, Clone, Serialize, Deserialize, Default)]
212pub struct SectionOverride {
213    #[serde(default)]
214    pub value: Option<SectionValue>,
215
216    #[serde(default)]
217    pub tokens: Option<u32>,
218
219    #[serde(default)]
220    pub required: Option<bool>,
221
222    #[serde(default)]
223    pub required_if: Option<String>,
224
225    #[serde(default)]
226    pub formats: Option<SectionFormats>,
227}
228
229/// Output format templates per section
230#[derive(Debug, Clone, Serialize, Deserialize, Default)]
231pub struct SectionFormats {
232    #[serde(default)]
233    pub markdown: Option<FormatTemplate>,
234
235    #[serde(default)]
236    pub compact: Option<FormatTemplate>,
237
238    #[serde(default)]
239    pub json: Option<serde_json::Value>, // Can be null
240
241    #[serde(default)]
242    pub text: Option<FormatTemplate>,
243}
244
245impl SectionFormats {
246    /// Get template for format with fallback to markdown
247    pub fn get(&self, format: super::renderer::OutputFormat) -> Option<&FormatTemplate> {
248        match format {
249            super::renderer::OutputFormat::Markdown => self.markdown.as_ref(),
250            super::renderer::OutputFormat::Compact => {
251                self.compact.as_ref().or(self.markdown.as_ref())
252            }
253            super::renderer::OutputFormat::Json => None, // JSON uses raw data
254            super::renderer::OutputFormat::Text => self.text.as_ref().or(self.markdown.as_ref()),
255        }
256    }
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize, Default)]
260#[serde(rename_all = "camelCase")]
261pub struct FormatTemplate {
262    #[serde(default)]
263    pub template: Option<String>,
264
265    #[serde(default)]
266    pub header: Option<String>,
267
268    #[serde(default)]
269    pub footer: Option<String>,
270
271    #[serde(default)]
272    pub item_template: Option<String>,
273
274    #[serde(default)]
275    pub separator: Option<String>,
276
277    #[serde(default)]
278    pub empty_template: Option<String>,
279}
280
281/// Dynamic data source configuration
282#[derive(Debug, Clone, Serialize, Deserialize)]
283#[serde(rename_all = "camelCase")]
284pub struct SectionData {
285    pub source: String,
286
287    #[serde(default)]
288    pub fields: Vec<String>,
289
290    #[serde(default)]
291    pub filter: Option<DataFilter>,
292
293    #[serde(default)]
294    pub sort_by: Option<String>,
295
296    #[serde(default)]
297    pub sort_order: Option<String>,
298
299    #[serde(default)]
300    pub max_items: Option<usize>,
301
302    #[serde(default)]
303    pub item_tokens: Option<u32>,
304
305    #[serde(default)]
306    pub empty_behavior: Option<String>,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
310#[serde(untagged)]
311pub enum DataFilter {
312    Array(Vec<String>),
313    Object(HashMap<String, serde_json::Value>),
314}
315
316/// Selection strategy configuration
317#[derive(Debug, Clone, Serialize, Deserialize)]
318#[serde(rename_all = "camelCase")]
319pub struct SelectionStrategy {
320    #[serde(default = "default_algorithm")]
321    pub algorithm: String,
322
323    #[serde(default)]
324    pub weights: DimensionWeights,
325
326    #[serde(default)]
327    pub presets: HashMap<String, DimensionWeights>,
328
329    #[serde(default)]
330    pub phases: Vec<SelectionPhase>,
331
332    #[serde(default = "default_min_budget")]
333    pub minimum_budget: u32,
334
335    #[serde(default = "default_dynamic_enabled")]
336    pub dynamic_modifiers_enabled: bool,
337}
338
339impl Default for SelectionStrategy {
340    fn default() -> Self {
341        Self {
342            algorithm: default_algorithm(),
343            weights: DimensionWeights::default(),
344            presets: HashMap::new(),
345            phases: Vec::new(),
346            minimum_budget: default_min_budget(),
347            dynamic_modifiers_enabled: default_dynamic_enabled(),
348        }
349    }
350}
351
352fn default_algorithm() -> String {
353    "value-optimized".to_string()
354}
355
356fn default_min_budget() -> u32 {
357    80
358}
359
360fn default_dynamic_enabled() -> bool {
361    true
362}
363
364/// Weights for multi-dimensional value calculation
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct DimensionWeights {
367    #[serde(default = "default_safety_weight")]
368    pub safety: f64,
369
370    #[serde(default = "default_weight")]
371    pub efficiency: f64,
372
373    #[serde(default = "default_weight")]
374    pub accuracy: f64,
375
376    #[serde(default = "default_weight")]
377    pub base: f64,
378}
379
380impl Default for DimensionWeights {
381    fn default() -> Self {
382        Self {
383            safety: default_safety_weight(),
384            efficiency: default_weight(),
385            accuracy: default_weight(),
386            base: default_weight(),
387        }
388    }
389}
390
391fn default_safety_weight() -> f64 {
392    1.5
393}
394
395fn default_weight() -> f64 {
396    1.0
397}
398
399/// A phase in the selection process
400#[derive(Debug, Clone, Serialize, Deserialize)]
401#[serde(rename_all = "camelCase")]
402pub struct SelectionPhase {
403    pub name: String,
404
405    #[serde(default)]
406    pub filter: PhaseFilter,
407
408    #[serde(default = "default_sort")]
409    pub sort: String,
410
411    #[serde(default)]
412    pub budget_percent: Option<f32>,
413}
414
415fn default_sort() -> String {
416    "value-per-token".to_string()
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize, Default)]
420#[serde(rename_all = "camelCase")]
421pub struct PhaseFilter {
422    #[serde(default)]
423    pub required: Option<bool>,
424
425    #[serde(default)]
426    pub required_if: Option<bool>,
427
428    #[serde(default)]
429    pub safety_minimum: Option<u8>,
430
431    #[serde(default)]
432    pub categories: Option<Vec<String>>,
433
434    #[serde(default)]
435    pub tags: Option<Vec<String>>,
436}
437
438/// Output format configurations
439#[derive(Debug, Clone, Serialize, Deserialize, Default)]
440pub struct OutputFormatsConfig {
441    #[serde(default)]
442    pub markdown: Option<MarkdownConfig>,
443
444    #[serde(default)]
445    pub compact: Option<CompactConfig>,
446
447    #[serde(default)]
448    pub json: Option<JsonConfig>,
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
452#[serde(rename_all = "camelCase")]
453pub struct MarkdownConfig {
454    #[serde(default)]
455    pub section_separator: Option<String>,
456    #[serde(default)]
457    pub header_level: Option<u8>,
458    #[serde(default)]
459    pub list_style: Option<String>,
460    #[serde(default)]
461    pub code_block_style: Option<String>,
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct CompactConfig {
467    #[serde(default)]
468    pub section_separator: Option<String>,
469    #[serde(default)]
470    pub max_line_length: Option<usize>,
471    #[serde(default)]
472    pub abbreviate: Option<bool>,
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize)]
476#[serde(rename_all = "camelCase")]
477pub struct JsonConfig {
478    #[serde(default)]
479    pub pretty: Option<bool>,
480    #[serde(default)]
481    pub include_metadata: Option<bool>,
482    #[serde(default)]
483    pub include_token_counts: Option<bool>,
484}
485
486/// Knowledge store configuration
487#[derive(Debug, Clone, Serialize, Deserialize)]
488#[serde(rename_all = "camelCase")]
489pub struct KnowledgeStoreConfig {
490    #[serde(default)]
491    pub enabled: Option<bool>,
492    #[serde(default)]
493    pub index_path: Option<String>,
494    #[serde(default)]
495    pub semantic_db_path: Option<String>,
496    #[serde(default)]
497    pub fallback_to_index: Option<bool>,
498}
499
500/// A section that has been selected for output
501#[derive(Debug, Clone)]
502pub struct SelectedSection {
503    pub id: String,
504    pub priority: u32,
505    pub tokens: u32,
506    pub value: f64,
507    pub section: Section,
508}
509
510/// RFC-0015: Primer tier levels based on token budget
511#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
512#[serde(rename_all = "lowercase")]
513pub enum PrimerTier {
514    /// ~250 CLI tokens, ~178 MCP tokens - Minimal context, action-focused
515    Micro,
516    /// ~400 CLI tokens, ~320 MCP tokens - Standard IDE integrations
517    Minimal,
518    /// ~600 CLI tokens, ~480 MCP tokens - Full-featured agents (recommended)
519    Standard,
520    /// ~1,400 CLI tokens, ~1,100 MCP tokens - Dedicated context budget
521    Full,
522}
523
524impl PrimerTier {
525    /// Select tier based on token budget
526    /// ```text
527    /// if budget < 300:   → Micro
528    /// if budget < 450:   → Minimal
529    /// if budget < 700:   → Standard
530    /// else:              → Full
531    /// ```
532    pub fn from_budget(budget: u32) -> Self {
533        match budget {
534            0..=299 => PrimerTier::Micro,
535            300..=449 => PrimerTier::Minimal,
536            450..=699 => PrimerTier::Standard,
537            _ => PrimerTier::Full,
538        }
539    }
540
541    /// Get tier name as string
542    pub fn name(&self) -> &'static str {
543        match self {
544            PrimerTier::Micro => "micro",
545            PrimerTier::Minimal => "minimal",
546            PrimerTier::Standard => "standard",
547            PrimerTier::Full => "full",
548        }
549    }
550
551    /// Get target token count for CLI mode
552    pub fn cli_tokens(&self) -> u32 {
553        match self {
554            PrimerTier::Micro => 250,
555            PrimerTier::Minimal => 400,
556            PrimerTier::Standard => 600,
557            PrimerTier::Full => 1400,
558        }
559    }
560
561    /// Get target token count for MCP mode
562    pub fn mcp_tokens(&self) -> u32 {
563        match self {
564            PrimerTier::Micro => 178,
565            PrimerTier::Minimal => 320,
566            PrimerTier::Standard => 480,
567            PrimerTier::Full => 1100,
568        }
569    }
570
571    /// Get brief description of tier use case
572    pub fn description(&self) -> &'static str {
573        match self {
574            PrimerTier::Micro => "Minimal context, action-focused",
575            PrimerTier::Minimal => "Standard IDE integrations",
576            PrimerTier::Standard => "Full-featured agents (recommended)",
577            PrimerTier::Full => "Dedicated context budget, raw API",
578        }
579    }
580
581    /// Iterator over all tiers in order
582    pub fn all() -> impl Iterator<Item = PrimerTier> {
583        [
584            PrimerTier::Micro,
585            PrimerTier::Minimal,
586            PrimerTier::Standard,
587            PrimerTier::Full,
588        ]
589        .into_iter()
590    }
591}
592
593impl std::fmt::Display for PrimerTier {
594    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
595        write!(f, "{}", self.name())
596    }
597}