Skip to main content

ferrify_syntax/
lib.rs

1//! Patch planning and budget enforcement.
2//!
3//! `agent-syntax` is where Ferrify turns a broad change plan into a narrower
4//! patch plan. In the current starter implementation it does not rewrite source
5//! files. Instead, it enforces the patch budget and produces explicit anchors
6//! that explain why each file was selected.
7//!
8//! That distinction matters: the rest of the workspace can already reason
9//! about bounded implementation, verification, and reporting without pretending
10//! that AST-level edits exist before they do.
11//!
12//! # Examples
13//!
14//! ```
15//! use std::collections::BTreeSet;
16//!
17//! use agent_domain::{
18//!     ApiImpact, BlastRadius, ChangeIntent, ChangePlan, OutcomeSpec, PatchBudget, RepoPath,
19//!     ScopeBoundary, SemanticConcern, TaskKind, VerificationKind, VerificationPlan,
20//! };
21//! use agent_syntax::PatchPlanner;
22//!
23//! # fn main() -> Result<(), agent_domain::DomainTypeError> {
24//! let mut target_files = BTreeSet::new();
25//! target_files.insert(RepoPath::new("crates/agent-cli/src/main.rs")?);
26//!
27//! let mut required = BTreeSet::new();
28//! required.insert(VerificationKind::CargoCheck);
29//!
30//! let change_plan = ChangePlan {
31//!     intent: ChangeIntent {
32//!         task_kind: TaskKind::CliEnhancement,
33//!         goal: "tighten CLI reporting".to_owned(),
34//!         desired_outcome: OutcomeSpec {
35//!             summary: "narrow the CLI plan".to_owned(),
36//!         },
37//!         scope_boundary: ScopeBoundary {
38//!             in_scope: Vec::new(),
39//!             out_of_scope: Vec::new(),
40//!             blast_radius_limit: BlastRadius::Small,
41//!         },
42//!         success_evidence: Vec::new(),
43//!         primary_risks: Vec::new(),
44//!     },
45//!     concern: SemanticConcern::FeatureAdd,
46//!     target_files,
47//!     selected_mode: "implementer".parse()?,
48//!     api_impact: ApiImpact::InternalOnly,
49//!     patch_budget: PatchBudget {
50//!         max_files: 1,
51//!         max_changed_lines: 40,
52//!         allow_manifest_changes: false,
53//!     },
54//!     verification_plan: VerificationPlan { required },
55//!     notes: vec!["limit the edit to the CLI entrypoint".to_owned()],
56//! };
57//!
58//! let patch_plan = PatchPlanner::build(&change_plan);
59//! assert_eq!(patch_plan.target_files.len(), 1);
60//! # Ok(())
61//! # }
62//! ```
63
64use std::collections::BTreeSet;
65
66use agent_domain::{ChangePlan, PatchAnchor, PatchPlan};
67
68/// Builds bounded patch plans from architect-stage change plans.
69#[derive(Debug, Default)]
70pub struct PatchPlanner;
71
72impl PatchPlanner {
73    /// Converts a change plan into a patch plan while enforcing the file budget.
74    #[must_use]
75    pub fn build(change_plan: &ChangePlan) -> PatchPlan {
76        let max_files = usize::from(change_plan.patch_budget.max_files);
77        let target_files = if max_files == 0 {
78            BTreeSet::new()
79        } else {
80            change_plan
81                .target_files
82                .iter()
83                .take(max_files)
84                .cloned()
85                .collect::<BTreeSet<_>>()
86        };
87
88        let anchors = target_files
89            .iter()
90            .map(|file| PatchAnchor {
91                file: file.clone(),
92                reason: format!(
93                    "Selected during planning for {:?} within the active patch budget.",
94                    change_plan.concern
95                ),
96            })
97            .collect();
98
99        PatchPlan {
100            concern: change_plan.concern,
101            target_files,
102            anchors,
103            budget: change_plan.patch_budget.clone(),
104            api_impact: change_plan.api_impact,
105            required_validation: change_plan.verification_plan.clone(),
106        }
107    }
108}