1use open_kioku_actions::{ActionKind, PolicyGate};
2use open_kioku_config::OkConfig;
3use open_kioku_context::ContextPackBuilder;
4use open_kioku_core::{PatchId, PatchPlan};
5use open_kioku_errors::{OkError, Result};
6use open_kioku_storage::OkStore;
7use sha2::{Digest, Sha256};
8
9pub struct PatchPlanner<'a> {
10 config: &'a OkConfig,
11 store: &'a dyn OkStore,
12}
13
14impl<'a> PatchPlanner<'a> {
15 pub fn new(config: &'a OkConfig, store: &'a dyn OkStore) -> Self {
16 Self { config, store }
17 }
18
19 pub fn plan(&self, task: &str) -> Result<PatchPlan> {
20 let context = ContextPackBuilder::new(self.store).build(task, 12)?;
21 Ok(PatchPlan {
22 id: PatchId::new(stable_id(task)),
23 task: task.into(),
24 allowed_files: context.recommended_change_boundary.allowed_files,
25 caution_files: context.recommended_change_boundary.caution_files,
26 forbidden_files: context.recommended_change_boundary.forbidden_files,
27 change_steps: vec![
28 "Inspect primary symbols and definitions from the context pack".into(),
29 "Constrain edits to allowed files unless evidence justifies expansion".into(),
30 "Run the recommended validation plan after approval".into(),
31 ],
32 risks: context.risk_report.reasons,
33 assumptions: vec![
34 "Generated and vendor files remain out of scope".into(),
35 "Patch application requires explicit write mode and approval".into(),
36 ],
37 tests: context.test_candidates,
38 rollback_notes: vec!["Revert the unified diff if validation fails".into()],
39 unified_diff: None,
40 requires_approval: self.config.security.approval_required,
41 evidence: context.evidence,
42 })
43 }
44
45 pub fn apply(&self, _patch: &PatchPlan, approved: bool) -> Result<()> {
46 PolicyGate::new(self.config).ensure_allowed(ActionKind::ApplyPatch)?;
47 if self.config.security.approval_required && !approved {
48 return Err(OkError::PolicyDenied(
49 "patch application requires explicit approval".into(),
50 ));
51 }
52 Err(OkError::Unsupported(
53 "patch application is intentionally not implemented without a diff applicator".into(),
54 ))
55 }
56}
57
58fn stable_id(value: &str) -> String {
59 let mut hasher = Sha256::new();
60 hasher.update(value.as_bytes());
61 format!("{:x}", hasher.finalize())
62}