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}