Skip to main content

weavatrix_refactor_plan/
operation.rs

1use crate::{Completeness, FileEdit, PlanError, PlanEvidence, PlanFingerprint, TextEdit};
2use blazingly_json::Value;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6/// Stable wire schema owned by this crate.
7pub const REFACTOR_PLAN_SCHEMA: &str = "weavatrix.refactor-plan.v1";
8
9/// A bounded, versioned collection of logical refactor operations and evidence.
10#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct RefactorPlan {
13    pub schema_version: String,
14    pub operation: String,
15    /// One simultaneous transition set; array order is identity, not execution order.
16    pub operations: Vec<RefactorOperation>,
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub completeness: Option<Completeness>,
19    #[serde(flatten)]
20    pub evidence: PlanEvidence,
21}
22
23impl RefactorPlan {
24    #[must_use]
25    pub fn new(operation: impl Into<String>, operations: Vec<RefactorOperation>) -> Self {
26        Self {
27            schema_version: REFACTOR_PLAN_SCHEMA.to_owned(),
28            operation: operation.into(),
29            operations,
30            completeness: None,
31            evidence: PlanEvidence::default(),
32        }
33    }
34
35    pub fn validate(&self) -> Result<crate::ValidatedConsumerPlan<'_>, PlanError> {
36        crate::validate_consumer_plan(self, crate::RefactorPlanLimits::default())
37    }
38
39    pub fn validate_with(
40        &self,
41        limits: crate::RefactorPlanLimits,
42    ) -> Result<crate::ValidatedConsumerPlan<'_>, PlanError> {
43        crate::validate_consumer_plan(self, limits)
44    }
45
46    pub fn fingerprint(&self) -> Result<PlanFingerprint, PlanError> {
47        crate::fingerprint_plan(self)
48    }
49
50    /// Converts a legacy edit envelope without losing text edits or extensions.
51    pub fn from_text_edit_plan(plan: crate::EditPlan) -> Result<Self, PlanError> {
52        crate::conversion::from_text_edit_plan(plan)
53    }
54
55    /// Converts to a legacy edit envelope if every operation is a text modify.
56    pub fn try_into_text_edit_plan(self) -> Result<crate::EditPlan, PlanError> {
57        crate::conversion::into_text_edit_plan(self)
58    }
59}
60
61/// One logical operation in a refactor plan.
62#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
63#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
64#[serde(deny_unknown_fields)]
65pub enum RefactorOperation {
66    Modify(FileEdit),
67    Create(CreateFile),
68    Delete(DeleteFile),
69    Rename(RenameFile),
70}
71
72/// Exact UTF-8 contents to create at a path that must be absent.
73#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct CreateFile {
76    pub path: String,
77    pub contents: String,
78    #[serde(default, skip_serializing_if = "CreatePermissions::is_default")]
79    pub permissions: CreatePermissions,
80    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
81    pub extensions: BTreeMap<String, Value>,
82}
83
84impl CreateFile {
85    #[must_use]
86    pub fn new(path: impl Into<String>, contents: impl Into<String>) -> Self {
87        Self {
88            path: path.into(),
89            contents: contents.into(),
90            permissions: CreatePermissions::default(),
91            extensions: BTreeMap::new(),
92        }
93    }
94
95    #[must_use]
96    pub const fn with_executable(mut self, executable: bool) -> Self {
97        self.permissions.executable = executable;
98        self
99    }
100}
101
102/// Deterministic portable permission policy for a newly created source file.
103#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105#[serde(deny_unknown_fields)]
106pub struct CreatePermissions {
107    pub executable: bool,
108}
109
110impl CreatePermissions {
111    #[must_use]
112    pub const fn is_default(&self) -> bool {
113        !self.executable
114    }
115
116    #[must_use]
117    pub const fn readonly(self) -> bool {
118        false
119    }
120
121    #[must_use]
122    pub const fn unix_mode(self) -> u32 {
123        if self.executable { 0o755 } else { 0o644 }
124    }
125}
126
127/// Delete an existing file only when its complete contents match.
128#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub struct DeleteFile {
131    pub path: String,
132    pub expected_sha256: String,
133    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
134    pub extensions: BTreeMap<String, Value>,
135}
136
137impl DeleteFile {
138    #[must_use]
139    pub fn new(path: impl Into<String>, expected_sha256: impl Into<String>) -> Self {
140        Self {
141            path: path.into(),
142            expected_sha256: expected_sha256.into(),
143            extensions: BTreeMap::new(),
144        }
145    }
146}
147
148/// Move one exact source to an absent destination.
149///
150/// `edits` use v1 UTF-16 coordinates against the original `from` contents
151/// guarded by `expected_source_sha256`, before the move.
152#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
153#[serde(rename_all = "camelCase")]
154pub struct RenameFile {
155    pub from: String,
156    pub to: String,
157    pub expected_source_sha256: String,
158    #[serde(default, skip_serializing_if = "Vec::is_empty")]
159    pub edits: Vec<TextEdit>,
160    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
161    pub extensions: BTreeMap<String, Value>,
162}
163
164impl RenameFile {
165    #[must_use]
166    pub fn new(
167        from: impl Into<String>,
168        to: impl Into<String>,
169        expected_source_sha256: impl Into<String>,
170    ) -> Self {
171        Self {
172            from: from.into(),
173            to: to.into(),
174            expected_source_sha256: expected_source_sha256.into(),
175            edits: Vec::new(),
176            extensions: BTreeMap::new(),
177        }
178    }
179
180    #[must_use]
181    pub fn with_edits(mut self, edits: Vec<TextEdit>) -> Self {
182        self.edits = edits;
183        self
184    }
185}